mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2436 from marcoarnulfo/fix/ai3-paths-import
EPS import: an Illustrator 3 file without layers opens its paths on one layer (fixes storytold/photocraft#6036)
This commit is contained in:
@@ -26,10 +26,11 @@
|
||||
//! of a PostScript file without one. A file whose program can't be read (an operator the
|
||||
//! interpreter doesn't know, an error, a limit reached) or that draws nothing comes in from its
|
||||
//! layers when its program has them (a `.ai` in PostScript form whose prolog is named but not
|
||||
//! included, as other apps write it), else as its preview (a TIFF image, a Windows metafile or an
|
||||
//! EPSI bitmap), with a warning that names the error, the operator and the procedures it ran in;
|
||||
//! without a preview, what was drawn before the error is kept (with that warning), else the file
|
||||
//! is refused.
|
||||
//! included, as other apps write it), or with its art on one layer when it is in the legacy format
|
||||
//! without layers (Illustrator 3, as image editors still export their paths), else as its preview
|
||||
//! (a TIFF image, a Windows metafile or an EPSI bitmap), with a warning that names the error, the
|
||||
//! operator and the procedures it ran in; without a preview, what was drawn before the error is
|
||||
//! kept (with that warning), else the file is refused.
|
||||
|
||||
mod ai;
|
||||
mod ate;
|
||||
@@ -198,7 +199,7 @@ pub fn import_with(bytes: &[u8], editing_data: bool) -> Result<Imported, String>
|
||||
// printed).
|
||||
if editing_data
|
||||
&& dsc.own_legacy
|
||||
&& let Some(Ok((document, warnings))) = native::program_alone(ps)
|
||||
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator)
|
||||
{
|
||||
return Ok(Imported { document, warnings, preview: false });
|
||||
}
|
||||
@@ -224,8 +225,9 @@ pub fn import_with(bytes: &[u8], editing_data: bool) -> Result<Imported, String>
|
||||
(Err(e), _) => reason(e, fault.as_ref()),
|
||||
};
|
||||
// A file in the legacy Illustrator format whose program can't be run (a `.ai` that names the
|
||||
// prolog defining its operators without including it): its layers, which the program has.
|
||||
let why = match editing_data.then(|| native::program_alone(ps)).flatten() {
|
||||
// prolog defining its operators without including it): its layers, which the program has, or
|
||||
// its art on one layer when it has none (Illustrator 3).
|
||||
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator)).flatten() {
|
||||
Some(Ok((document, warnings))) => return Ok(Imported { document, warnings, preview: false }),
|
||||
Some(Err(e)) => format!("{why}; its layers couldn't be read either ({e})"),
|
||||
None => why,
|
||||
|
||||
@@ -1334,9 +1334,40 @@ fn editing_copy(ps: &[u8]) -> Option<Result<Cow<'_, [u8]>, String>> {
|
||||
|
||||
/// A file in the legacy format whose page can't be read (a `.ai` in PostScript form that names its
|
||||
/// prolog without including it) from its layers alone (`ps`: its program, which has them) → the
|
||||
/// document and the import's notes. `None` when the layers aren't in the program.
|
||||
pub(super) fn program_alone(ps: &[u8]) -> Option<Result<(Document, Vec<String>), String>> {
|
||||
(!ai::has_eps_data(ps) && has_layers(ps)).then(|| build(ps, None, Page::Art, false, false, 1.0))
|
||||
/// document and the import's notes. A file whose header says it is in the format (`illustrator`)
|
||||
/// but whose program has no layers (Illustrator 3 had none; image editors still export their
|
||||
/// paths so, storytold/photocraft#6036) is read with its art on one layer. `None` when the program
|
||||
/// has no layers and isn't in the format: other PostScript isn't read as the format's operators.
|
||||
pub(super) fn program_alone(ps: &[u8], illustrator: bool) -> Option<Result<(Document, Vec<String>), String>> {
|
||||
if ai::has_eps_data(ps) {
|
||||
return None;
|
||||
}
|
||||
if has_layers(ps) {
|
||||
return Some(build(ps, None, Page::Art, false, false, 1.0));
|
||||
}
|
||||
let program = one_layer(ps).filter(|_| illustrator)?;
|
||||
Some(build(&program, None, Page::Art, false, false, 1.0))
|
||||
}
|
||||
|
||||
/// `ps` with its art (from its `%%EndSetup` to its `%%PageTrailer`, else its `%%Trailer`, else its
|
||||
/// end) on a layer that shows, prints and isn't locked, as the version 5 format writes one. `None`
|
||||
/// without a `%%EndSetup`: where the art starts isn't known.
|
||||
fn one_layer(ps: &[u8]) -> Option<Vec<u8>> {
|
||||
let setup = line_with(ps, 0, b"%%EndSetup")?;
|
||||
let art = line_with(ps, setup + 1, b"").unwrap_or(ps.len());
|
||||
let end = line_with(ps, art, b"%%PageTrailer").or_else(|| line_with(ps, art, b"%%Trailer")).unwrap_or(ps.len());
|
||||
let mut out = Vec::with_capacity(ps.len() + 80);
|
||||
out.extend_from_slice(ps.get(..art)?);
|
||||
out.extend_from_slice(b"\n%AI5_BeginLayer\n1 1 1 1 0 0 0 79 128 255 Lb\n(Layer 1) Ln\n");
|
||||
out.extend_from_slice(ps.get(art..end)?);
|
||||
out.extend_from_slice(b"\nLB\n%AI5_EndLayer--\n");
|
||||
out.extend_from_slice(ps.get(end..)?);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Where the first line of `ps` from `from` on that starts with `key` starts.
|
||||
fn line_with(ps: &[u8], from: usize, key: &[u8]) -> Option<usize> {
|
||||
(from..ps.len()).find(|&i| (i == 0 || matches!(ps.get(i - 1), Some(b'\n' | b'\r'))) && ps.get(i..).is_some_and(|rest| rest.starts_with(key)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -121,6 +121,43 @@ fn a_postscript_ai_whose_layers_cant_be_read_says_why() {
|
||||
assert!(e.contains("`Adobe_packedarray`") && e.contains("its layers couldn't be read either") && e.contains("`frobnicate`"), "{e}");
|
||||
}
|
||||
|
||||
/// An Illustrator 3 file of paths alone, as image editors export their paths (storytold/photocraft#6036):
|
||||
/// no prolog and no layers, the paths unpainted (`n` closes, `N` doesn't), a compound path of two.
|
||||
const PATHS_ONLY: &str = "%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: an image editor\n%%BoundingBox: 0 0 200 100\n\
|
||||
%AI3_Cropmarks: 0 0 200 100\n%%EndComments\n%%EndProlog\n%%BeginSetup\n%%EndSetup\n\
|
||||
%%Note: Outline\n10 10 m\n100 10 L\n50 80 60 90 10 10 C\nn\n\
|
||||
*u\n20 20 m\n30 20 L\nN\n40 40 m\n50 50 L\n60 40 L\nn\n*U\n%%PageTrailer\n%%Trailer\n%%EOF\n";
|
||||
|
||||
#[test]
|
||||
fn an_illustrator_3_file_without_layers_opens_its_paths_on_one_layer() {
|
||||
let r = import(PATHS_ONLY.as_bytes()).unwrap();
|
||||
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
|
||||
let d = &r.document;
|
||||
assert_eq!(d.layers.len(), 1);
|
||||
assert_eq!(d.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0));
|
||||
let art = d.layers[0].children().unwrap();
|
||||
assert_eq!(art.len(), 2);
|
||||
// Art space has y up: (10, 10) is 10 above the artboard's bottom.
|
||||
let NodeKind::Path { path, .. } = &art[0].kind else { panic!("{:?}", art[0].kind) };
|
||||
let ends: Vec<_> = path.subpaths[0].anchors.iter().map(|a| (a.p.x, a.p.y)).collect();
|
||||
assert_eq!(ends, [(10.0, 90.0), (100.0, 90.0)]);
|
||||
assert!(path.subpaths[0].closed);
|
||||
assert!(art[0].appearance.items.is_empty(), "unpainted");
|
||||
let NodeKind::Compound { children, .. } = &art[1].kind else { panic!("{:?}", art[1].kind) };
|
||||
let closed: Vec<_> = children.iter().map(|c| if let NodeKind::Path { path, .. } = &c.kind { path.is_closed() } else { false }).collect();
|
||||
assert_eq!(closed, [false, true]);
|
||||
assert_eq!(art[1].geometric_bounds(), Some(Rect::new(20.0, 50.0, 60.0, 80.0)));
|
||||
}
|
||||
|
||||
/// The same program in a file that doesn't say it is in Illustrator's format is PostScript whose
|
||||
/// operators the reader doesn't know: it is still refused.
|
||||
#[test]
|
||||
fn the_same_program_in_other_postscript_is_still_refused() {
|
||||
let other = PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "");
|
||||
let e = import(other.as_bytes()).unwrap_err();
|
||||
assert!(e.contains("it uses `m`") && !e.contains("layers"), "{e}");
|
||||
}
|
||||
|
||||
/// The editing text of a newer file saved as PostScript (no prolog at all, a non-printing setup):
|
||||
/// it opens with its sublayers.
|
||||
#[test]
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user