PDF import: an editor's .ai keeps its RGB numbers, and a fill faded or masked by its group keeps its stroke apart
This commit is contained in:
parent
04bc43c802
commit
34b3c59a03
|
|
@ -221,7 +221,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||||
.then(|| vectorcraft_pdf::editing_with(bytes, opts.password.as_deref()))
|
.then(|| vectorcraft_pdf::editing_with(bytes, opts.password.as_deref()))
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|e| (e.intact, move || Some(e.data)));
|
.map(|e| (e.intact, move || Some(e.data)));
|
||||||
restore_or_import(editing, || match super::pdfimport::import(bytes, opts) {
|
restore_or_import(editing, || match super::pdfimport::import(bytes, opts, format.id != "pdf") {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
// Pages of text alone: the editing copy alone where it reads, as for a file
|
// 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).
|
// saved without its PDF part (#1387: else the page, which is the art).
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ impl LoadOptions {
|
||||||
/// The PDF import options: `pages` resolved against the file's page count. A PDF-compatible
|
/// The PDF import options: `pages` resolved against the file's page count. A PDF-compatible
|
||||||
/// `.ai`, or a PDF saved with its editing data, asked for its crop box opens on its trim boxes,
|
/// `.ai`, or a PDF saved with its editing data, asked for its crop box opens on its trim boxes,
|
||||||
/// its editor's artboards, with the bleed round them (its crop box takes the bleed in).
|
/// its editor's artboards, with the bleed round them (its crop box takes the bleed in).
|
||||||
fn import_options(&self, bytes: &[u8]) -> Result<ImportOptions> {
|
fn import_options(&self, bytes: &[u8], editor_document: bool) -> Result<ImportOptions> {
|
||||||
let pages = match &self.pages {
|
let pages = match &self.pages {
|
||||||
None => None,
|
None => None,
|
||||||
Some(range) => {
|
Some(range) => {
|
||||||
|
|
@ -114,6 +114,7 @@ impl LoadOptions {
|
||||||
password: self.password.clone(),
|
password: self.password.clone(),
|
||||||
text_as: self.text_as,
|
text_as: self.text_as,
|
||||||
layers: self.layers,
|
layers: self.layers,
|
||||||
|
uncalibrated_rgb: editor_document,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -131,8 +132,9 @@ fn choice<T: Choice + Default>(cmd: &str, key: &str, id: Option<&str>) -> Result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import the pages `o` pick → the document, import notes and what else the import tells.
|
/// Import the pages `o` pick → the document, import notes and what else the import tells.
|
||||||
pub(super) fn import(bytes: &[u8], o: &LoadOptions) -> Result<vectorcraft_pdf::ImportReport> {
|
/// `editor_document`: a `.ai` file.
|
||||||
vectorcraft_pdf::import_with_report(bytes, &o.import_options(bytes)?).map_err(err)
|
pub(super) fn import(bytes: &[u8], o: &LoadOptions, editor_document: bool) -> Result<vectorcraft_pdf::ImportReport> {
|
||||||
|
vectorcraft_pdf::import_with_report(bytes, &o.import_options(bytes, editor_document)?).map_err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Page `page` (0-based) of a PDF as a document of one artboard (its `o.crop` box) and one
|
/// Page `page` (0-based) of a PDF as a document of one artboard (its `o.crop` box) and one
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||||
b.unsure = route && ocgs.list.iter().any(|g| (all_on && !g.on) || !g.print);
|
b.unsure = route && ocgs.list.iter().any(|g| (all_on && !g.on) || !g.print);
|
||||||
b.taken = taken;
|
b.taken = taken;
|
||||||
b.warnings = notes;
|
b.warnings = notes;
|
||||||
|
b.colors.uncalibrated_rgb = opts.uncalibrated_rgb && picked.iter().filter_map(|&n| pages.get(n)).any(crate::pages::has_private_data);
|
||||||
let cache = InterpreterCache::new();
|
let cache = InterpreterCache::new();
|
||||||
let mut x = 0.0;
|
let mut x = 0.0;
|
||||||
// Every page only text over private data: a file saved without its PDF part shows a notice
|
// Every page only text over private data: a file saved without its PDF part shows a notice
|
||||||
|
|
@ -1673,9 +1674,8 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
PathDrawMode::Stroke(props) => {
|
PathDrawMode::Stroke(props) => {
|
||||||
let mut st = stroke_layer(paint, opacity, props, mean_scale(transform));
|
let mut st = stroke_layer(paint, opacity, props, mean_scale(transform));
|
||||||
st.overprint = overprint;
|
st.overprint = overprint;
|
||||||
// Fill-then-stroke of the same path (the `B` operator, or a fill and a stroke of
|
// Fill-then-stroke of the same path (the `B` operator, or a fill and then a stroke
|
||||||
// the outline as Illustrator writes an object) becomes one object: a path with a
|
// of the same outline) becomes one object: a path with a fill and a live stroke.
|
||||||
// fill and a live stroke.
|
|
||||||
let blend = self.blend;
|
let blend = self.blend;
|
||||||
let fill = self.last_fill.take();
|
let fill = self.last_fill.take();
|
||||||
let last = match self.mask {
|
let last = match self.mask {
|
||||||
|
|
@ -1687,6 +1687,10 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||||
&& let Some((id, outline)) = fill
|
&& let Some((id, outline)) = fill
|
||||||
&& last.id == id
|
&& last.id == id
|
||||||
&& last.blend == blend
|
&& last.blend == blend
|
||||||
|
// Opacity or a mask of its own came from a transparency group the fill was
|
||||||
|
// drawn in and this stroke wasn't.
|
||||||
|
&& last.opacity == 1.0
|
||||||
|
&& last.extras().mask.is_none()
|
||||||
&& last.appearance.stroke().is_none()
|
&& last.appearance.stroke().is_none()
|
||||||
&& same_outline(&outline, &bp)
|
&& same_outline(&outline, &bp)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,8 @@ struct Ink {
|
||||||
pub(crate) enum Space {
|
pub(crate) enum Space {
|
||||||
Gray,
|
Gray,
|
||||||
Rgb,
|
Rgb,
|
||||||
|
/// An `ICCBased` RGB space whose numbers are taken as they are (see `Colors::uncalibrated_rgb`).
|
||||||
|
IccRgb,
|
||||||
Cmyk,
|
Cmyk,
|
||||||
/// A Separation or DeviceN space (index into `Colors::spaces`).
|
/// A Separation or DeviceN space (index into `Colors::spaces`).
|
||||||
Inks(usize),
|
Inks(usize),
|
||||||
|
|
@ -119,6 +121,10 @@ pub(crate) struct Colors<'p> {
|
||||||
rgb: usize,
|
rgb: usize,
|
||||||
/// A DeviceN colour mixed several inks (imported as RGB).
|
/// A DeviceN colour mixed several inks (imported as RGB).
|
||||||
pub mixed: bool,
|
pub mixed: bool,
|
||||||
|
/// RGB colours in an `ICCBased` space keep their numbers instead of going through the profile:
|
||||||
|
/// on for an editor's own document (`.ai`), so its art keeps the RGB numbers the file has; off
|
||||||
|
/// for PDFs, whose profiles stand.
|
||||||
|
pub uncalibrated_rgb: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `v`'s `Debug` text, or `None` past [`DEBUG_LIMIT`] (formatting stops there).
|
/// `v`'s `Debug` text, or `None` past [`DEBUG_LIMIT`] (formatting stops there).
|
||||||
|
|
@ -152,7 +158,7 @@ pub(crate) fn split_color(t: &str) -> Option<(&str, Vec<f32>)> {
|
||||||
|
|
||||||
impl<'p> Colors<'p> {
|
impl<'p> Colors<'p> {
|
||||||
pub fn new(pdf: &'p Pdf, taken: Vec<String>) -> Self {
|
pub fn new(pdf: &'p Pdf, taken: Vec<String>) -> Self {
|
||||||
Self { pdf, taken, scanned: false, spaces: vec![], inks: vec![], cmyk: 0, rgb: 0, mixed: false }
|
Self { pdf, taken, scanned: false, spaces: vec![], inks: vec![], cmyk: 0, rgb: 0, mixed: false, uncalibrated_rgb: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A solid colour in its own model; `None`: use its RGB.
|
/// A solid colour in its own model; `None`: use its RGB.
|
||||||
|
|
@ -178,7 +184,7 @@ impl<'p> Colors<'p> {
|
||||||
fn count(&mut self, space: Space) {
|
fn count(&mut self, space: Space) {
|
||||||
match space {
|
match space {
|
||||||
Space::Cmyk => self.cmyk += 1,
|
Space::Cmyk => self.cmyk += 1,
|
||||||
Space::Rgb | Space::Other => self.rgb += 1,
|
Space::Rgb | Space::IccRgb | Space::Other => self.rgb += 1,
|
||||||
Space::Gray | Space::Inks(_) => {}
|
Space::Gray | Space::Inks(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,6 +206,7 @@ impl<'p> Colors<'p> {
|
||||||
match (kind, n) {
|
match (kind, n) {
|
||||||
("DeviceGray" | "ICCBased", 1) => Space::Gray,
|
("DeviceGray" | "ICCBased", 1) => Space::Gray,
|
||||||
("DeviceRgb", 3) => Space::Rgb,
|
("DeviceRgb", 3) => Space::Rgb,
|
||||||
|
("ICCBased", 3) if self.uncalibrated_rgb => Space::IccRgb,
|
||||||
("DeviceCmyk" | "ICCBased", 4) => Space::Cmyk,
|
("DeviceCmyk" | "ICCBased", 4) => Space::Cmyk,
|
||||||
("Separation", 1) | ("DeviceN", _) => {
|
("Separation", 1) | ("DeviceN", _) => {
|
||||||
self.scan();
|
self.scan();
|
||||||
|
|
@ -223,6 +230,7 @@ impl<'p> Colors<'p> {
|
||||||
match space {
|
match space {
|
||||||
Space::Gray => Some(Native::plain(Color::gray(round3(1.0 - c(0))))),
|
Space::Gray => Some(Native::plain(Color::gray(round3(1.0 - c(0))))),
|
||||||
Space::Cmyk => Some(Native::plain(Color::cmyk(c(0), c(1), c(2), c(3)))),
|
Space::Cmyk => Some(Native::plain(Color::cmyk(c(0), c(1), c(2), c(3)))),
|
||||||
|
Space::IccRgb => Some(Native::plain(Color::rgb(c(0), c(1), c(2)))),
|
||||||
Space::Rgb | Space::Other => None,
|
Space::Rgb | Space::Other => None,
|
||||||
Space::Inks(i) => self.inks_color(i, comps),
|
Space::Inks(i) => self.inks_color(i, comps),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,11 +122,23 @@ pub struct ImportOptions {
|
||||||
/// Optional content groups become layers (else each page is one layer, without the art
|
/// Optional content groups become layers (else each page is one layer, without the art
|
||||||
/// that is off).
|
/// that is off).
|
||||||
pub layers: bool,
|
pub layers: bool,
|
||||||
|
/// The file is an editor's own document (`.ai`): RGB in its `ICCBased` spaces keeps its numbers
|
||||||
|
/// (see `Colors::uncalibrated_rgb`). Off for PDFs, whose profiles stand.
|
||||||
|
pub uncalibrated_rgb: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ImportOptions {
|
impl Default for ImportOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { max_pages: None, artboard_gap: 36.0, pages: None, crop: CropTo::default(), password: None, text_as: TextAs::default(), layers: true }
|
Self {
|
||||||
|
max_pages: None,
|
||||||
|
artboard_gap: 36.0,
|
||||||
|
pages: None,
|
||||||
|
crop: CropTo::default(),
|
||||||
|
password: None,
|
||||||
|
text_as: TextAs::default(),
|
||||||
|
layers: true,
|
||||||
|
uncalibrated_rgb: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,3 +245,20 @@ fn spot_colours_with_an_icc_lab_alternate_keep_their_lab_values() {
|
||||||
// An RGB profile stays RGB.
|
// An RGB profile stays RGB.
|
||||||
assert!(near(d.swatch("Leafy").unwrap().paint.color().unwrap(), Color::rgb(0.5, 0.9, 0.1)), "{:?}", d.swatch("Leafy"));
|
assert!(near(d.swatch("Leafy").unwrap().paint.color().unwrap(), Color::rgb(0.5, 0.9, 0.1)), "{:?}", d.swatch("Leafy"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rgb_numbers_stand_in_editor_documents_only() {
|
||||||
|
// GUARDS: an editor's document keeps its `ICCBased` RGB numbers; a PDF's profile stands.
|
||||||
|
let f = first_extra(1);
|
||||||
|
let icc = vectorcraft_color::cms::icc_bytes(vectorcraft_color::cms::PROPHOTO_RGB).unwrap();
|
||||||
|
let hex: String = icc.iter().map(|b| format!("{b:02x}")).collect();
|
||||||
|
let profile = format!("<< /N 3 /Filter /ASCIIHexDecode /Length {} >>\nstream\n{hex}>\nendstream", hex.len() + 1);
|
||||||
|
let mut page = page_with(&format!("/CS0 [/ICCBased {f} 0 R]"), &[("CS0", "0.2 0.6 0.4")]);
|
||||||
|
page.entries = "/PieceInfo << /Illustrator << /Private << /AIPrivateData1 7 /NumBlock 1 >> >> >>".into();
|
||||||
|
let bytes = pdf_with(&[page], &[&profile], None);
|
||||||
|
let color = |uncalibrated_rgb| {
|
||||||
|
solid(&fills(&import_with_report(&bytes, &ImportOptions { uncalibrated_rgb, ..Default::default() }).unwrap().document)[0]).0
|
||||||
|
};
|
||||||
|
assert!(near(color(true), Color::rgb(0.2, 0.6, 0.4)), "{:?}", color(true));
|
||||||
|
assert!(!near(color(false), Color::rgb(0.2, 0.6, 0.4)), "{:?}", color(false));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -432,8 +432,8 @@ fn objects(d: &Document) -> Vec<Node> {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_object_written_as_a_fill_and_a_closed_stroke_stays_one_path_with_a_live_stroke() {
|
fn an_object_written_as_a_fill_and_a_closed_stroke_stays_one_path_with_a_live_stroke() {
|
||||||
// As Illustrator writes an object: the fill of the outline left open, then the stroke of it
|
// The fill of the outline left open, then the stroke of it closed, each in its own
|
||||||
// closed, each in its own transform. Two subpaths: one ending on its start, one not.
|
// transform. Two subpaths: one ending on its start, one not.
|
||||||
let outline = "0 0 m -20 1 l -30 -12 l -30 30 l 0 0 l 5 5 m 10 5 l 10 10 l";
|
let outline = "0 0 m -20 1 l -30 -12 l -30 30 l 0 0 l 5 5 m 10 5 l 10 10 l";
|
||||||
let content = format!(
|
let content = format!(
|
||||||
"0.2 0.2 0.2 rg q 1 0 0 1 50 50 cm {outline} f Q 1 1 1 RG 1.5 w 1 J 1 j 4 M [3 2] 1 d q 1 0 0 1 50 50 cm {} S Q",
|
"0.2 0.2 0.2 rg q 1 0 0 1 50 50 cm {outline} f Q 1 1 1 RG 1.5 w 1 J 1 j 4 M [3 2] 1 d q 1 0 0 1 50 50 cm {} S Q",
|
||||||
|
|
@ -457,6 +457,45 @@ fn an_object_written_as_a_fill_and_a_closed_stroke_stays_one_path_with_a_live_st
|
||||||
assert!(p.is_closed());
|
assert!(p.is_closed());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fill_in_a_translucent_group_doesnt_take_the_stroke_drawn_after_it() {
|
||||||
|
// A fill at 75 % under an opaque stroke of the same outline: the fill in a transparency group
|
||||||
|
// drawn at 75 %, the stroke after the group.
|
||||||
|
let x = first_extra(1);
|
||||||
|
let outline = "10 10 m 60 10 l 60 60 l 10 60 l h";
|
||||||
|
let form = stream("/Type /XObject /Subtype /Form /BBox [0 0 100 100] /Group << /S /Transparency >>", &format!("1 0 0 rg {outline} f"));
|
||||||
|
let content = format!("q /G gs /X0 Do Q 0 0 1 RG 2 w {outline} S");
|
||||||
|
let d = open(&one_page(&content, &format!("/ExtGState << /G << /ca 0.75 /CA 0.75 >> >> /XObject << /X0 {x} 0 R >>"), &[&form]));
|
||||||
|
let objs = objects(&d);
|
||||||
|
let stroke = objs.iter().find(|n| n.appearance.stroke().is_some()).expect("a stroke");
|
||||||
|
let alpha = all(&d)
|
||||||
|
.iter()
|
||||||
|
.filter(|n| n.appearance.stroke().is_some() || n.children().is_some_and(|c| c.iter().any(|k| k.id == stroke.id)))
|
||||||
|
.map(|n| n.opacity)
|
||||||
|
.product::<f32>();
|
||||||
|
assert_eq!(alpha, 1.0, "the stroke stays opaque: {objs:#?}");
|
||||||
|
let fill = objs.iter().find(|n| n.appearance.fill().is_some()).expect("a fill");
|
||||||
|
assert!((fill.opacity * fill.appearance.fill().unwrap().opacity - 0.75).abs() < 1e-3, "{fill:#?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fill_in_a_soft_masked_group_doesnt_take_the_stroke_drawn_after_it() {
|
||||||
|
// ISO 32000-1 §11.6.5.2: the soft mask in the graphics state applies to the group drawn under
|
||||||
|
// it, not to the stroke after `Q`.
|
||||||
|
let (m, x) = (first_extra(1), first_extra(1) + 1);
|
||||||
|
let outline = "10 10 m 60 10 l 60 60 l 10 60 l h";
|
||||||
|
let mask = stream("/Type /XObject /Subtype /Form /BBox [0 0 100 100] /Group << /S /Transparency /CS /DeviceRGB >>", "1 1 1 rg 0 0 35 100 re f");
|
||||||
|
let form = stream("/Type /XObject /Subtype /Form /BBox [0 0 100 100] /Group << /S /Transparency >>", &format!("1 0 0 rg {outline} f"));
|
||||||
|
let content = format!("q /GS gs /X0 Do Q 0 0 1 RG 2 w {outline} S");
|
||||||
|
let resources = format!("/ExtGState << /GS << /SMask << /Type /Mask /S /Luminosity /G {m} 0 R >> >> >> /XObject << /X0 {x} 0 R >>");
|
||||||
|
let d = open(&one_page(&content, &resources, &[&mask, &form]));
|
||||||
|
let nodes = all(&d);
|
||||||
|
let stroke = nodes.iter().find(|n| n.appearance.stroke().is_some()).expect("a stroke");
|
||||||
|
assert!(stroke.appearance.fill().is_none() && stroke.extras().mask.is_none(), "the stroke is its own unmasked object: {stroke:#?}");
|
||||||
|
let masked = nodes.iter().filter(|n| n.extras().mask.is_some()).collect::<Vec<_>>();
|
||||||
|
assert!(!masked.is_empty() && masked.iter().all(|n| n.appearance.stroke().is_none()), "{masked:#?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn different_outlines_filled_and_stroked_stay_apart() {
|
fn different_outlines_filled_and_stroked_stay_apart() {
|
||||||
let d = open(&one_page("1 0 0 rg 10 10 m 40 10 l 40 40 l f 0 0 1 RG 10 10 m 40 10 l 40 40 l 10 40 l h S 0 g 50 50 m 60 60 l S", "", &[]));
|
let d = open(&one_page("1 0 0 rg 10 10 m 40 10 l 40 40 l f 0 0 1 RG 10 10 m 40 10 l 40 40 l 10 40 l h S 0 g 50 50 m 60 60 l S", "", &[]));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# File format parity
|
# File format parity
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266) · **Target:** Adobe Illustrator 2026 (30.x)
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||||
|
|
||||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/mod.rs`). Illustrator's list comes from its public
|
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/mod.rs`). Illustrator's list comes from its public
|
||||||
|
|
@ -32,6 +32,9 @@ private editing data. VectorCraft:
|
||||||
Level 3 interpreter. A `.ai` in PostScript form (versions 3 to 8, and what Rhino and other CAD apps
|
Level 3 interpreter. A `.ai` in PostScript form (versions 3 to 8, and what Rhino and other CAD apps
|
||||||
write) is its own editing data: it opens through the layers its program has, compared with its page
|
write) is its own editing data: it opens through the layers its program has, compared with its page
|
||||||
when the program runs, from the layers alone when it names its prolog without including it (#1027).
|
when the program runs, from the layers alone when it names its prolog without including it (#1027).
|
||||||
|
RGB in the PDF page's `ICCBased` spaces keeps the file's numbers in a `.ai` (the `uncalibrated_rgb`
|
||||||
|
import option, on for editor documents, off for PDFs), so `.ai` files tagged with a non-sRGB RGB profile
|
||||||
|
open with different, unconverted RGB colours than before.
|
||||||
A PDF-compatible `.ai` opens on its PDF part's trim boxes (its artboards), the bleed boxes round them
|
A PDF-compatible `.ai` opens on its PDF part's trim boxes (its artboards), the bleed boxes round them
|
||||||
becoming the document's bleed, so art in the bleed shows outside the artboard, within the bleed guide (#1266).
|
becoming the document's bleed, so art in the bleed shows outside the artboard, within the bleed guide (#1266).
|
||||||
A PDF saved with its editing data (`AIPDFPrivateData`) opens the same way.
|
A PDF saved with its editing data (`AIPDFPrivateData`) opens the same way.
|
||||||
|
|
@ -83,6 +86,8 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
|
||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-12 | minor | `.ai` read as its PDF part keeps the RGB numbers of its `ICCBased` spaces (`uncalibrated_rgb`, on for `.ai`, off for PDFs): a user-visible colour change for `.ai` files tagged with a non-sRGB RGB profile |
|
||||||
|
| 2026-10-12 | minor | PDF import: a fill that took its opacity or soft mask from a transparency group no longer absorbs the same-outline stroke drawn after the group, so the stroke keeps neither |
|
||||||
| 2026-10-12 | minor | A `.ai` whose PDF part draws only text opens from that part when its editing data can't be read, instead of being refused as saved without PDF compatibility: a page that states an art, trim or bleed box is art, not the placeholder (#1387); scores unchanged |
|
| 2026-10-12 | minor | A `.ai` whose PDF part draws only text opens from that part when its editing data can't be read, instead of being refused as saved without PDF compatibility: a page that states an art, trim or bleed box is art, not the placeholder (#1387); scores unchanged |
|
||||||
| 2026-10-12 | minor | PDF and `.ai` import, and placed PDFs, keep overprint: the graphics state's `/OP` and `/op` (ISO 32000-1 §8.6.7) become the overprint of fills, strokes and type, so a spot colour set to overprint still does after export; `/OPM 0` is reported in a note (#1388); scores unchanged |
|
| 2026-10-12 | minor | PDF and `.ai` import, and placed PDFs, keep overprint: the graphics state's `/OP` and `/op` (ISO 32000-1 §8.6.7) become the overprint of fills, strokes and type, so a spot colour set to overprint still does after export; `/OPM 0` is reported in a note (#1388); scores unchanged |
|
||||||
| 2026-10-12 | minor | `.ai` editing data: a gradient with middle stops runs its whole ramp (#1390); type of objects with an appearance, of hidden layers and on later artboards goes to its own text object (#1391); area type in a frame too short for the fallback font shows its first line (#1392); scores unchanged |
|
| 2026-10-12 | minor | `.ai` editing data: a gradient with middle stops runs its whole ramp (#1390); type of objects with an appearance, of hidden layers and on later artboards goes to its own text object (#1391); area type in a frame too short for the fallback font shows its first line (#1392); scores unchanged |
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Gaps
|
# Gaps
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)
|
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` keeps its RGB numbers, PDF import keeps strokes apart from fills faded or masked by their group; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||||
|
|
||||||
Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from
|
Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from
|
||||||
switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make
|
switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make
|
||||||
|
|
@ -234,6 +234,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-12 | minor | G9: `.ai` keeps the RGB numbers of its `ICCBased` spaces (`uncalibrated_rgb`; colours change for non-sRGB-profiled `.ai` files); estimate unchanged |
|
||||||
|
| 2026-10-12 | minor | G9: PDF import no longer merges a stroke into a fill that got its opacity or soft mask from a transparency group; estimate unchanged |
|
||||||
| 2026-10-12 | minor | G9: in `.ai` editing data, gradients with middle stops (#1390), type of objects with an appearance, hidden layers and later artboards (#1391) and area type in a frame too short for the fallback font (#1392) open as saved; G15: the art placed for symbol instances and inline graphics is kept between frames (#1386; the budget run on an idle machine remains); G1: dialogs take the keyboard focus on every open (#1367, #1395); estimates unchanged |
|
| 2026-10-12 | minor | G9: in `.ai` editing data, gradients with middle stops (#1390), type of objects with an appearance, hidden layers and later artboards (#1391) and area type in a frame too short for the fallback font (#1392) open as saved; G15: the art placed for symbol instances and inline graphics is kept between frames (#1386; the budget run on an idle machine remains); G1: dialogs take the keyboard focus on every open (#1367, #1395); estimates unchanged |
|
||||||
| 2026-10-12 | minor | G9: placed (linked) files in `.ai` editing data come in as linked images showing their saved preview, so such files open through their layers with their guides (#1264); estimate unchanged |
|
| 2026-10-12 | minor | G9: placed (linked) files in `.ai` editing data come in as linked images showing their saved preview, so such files open through their layers with their guides (#1264); estimate unchanged |
|
||||||
| 2026-10-12 | minor | G9: gradient-filled shapes from a PDF or `.ai` open as paths, not clip groups (#1304); CMYK files with transparency show in applications reading soft masks by luminosity (#1286, in part: the layer groups' bounding boxes remain); G15: hiding or locking a layer deselects in linear time (#1353); estimates unchanged |
|
| 2026-10-12 | minor | G9: gradient-filled shapes from a PDF or `.ai` open as paths, not clip groups (#1304); CMYK files with transparency show in applications reading soft masks by luminosity (#1286, in part: the layer groups' bounding boxes remain); G15: hiding or locking a layer deselects in linear time (#1353); estimates unchanged |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue