File formats: PostScript (.ps) and .epsf files open and place

The EPS reader read PostScript by any name, but the Open and Place
dialogs hid .ps and .epsf files, and a .ps or .epsf file whose header
is only %! could not be opened or placed ("not a format VectorCraft
reads"). The reference app lists .eps, .epsf and .ps under Open and
Place.

- A read-only PostScript format (ps) joins the format table. A .ps
  file opens and places through the EPS reader and reports the
  ps format. document.export does not write .ps; file.print does.
- .epsf is an EPS extension.
- OPEN_EXTS and PLACE_EXTS list epsf and ps, so the dialogs, the CLI
  help and the MCP open_file description list them.
  UNASSOCIATED_EXTS lists them too: the macOS bundle declares neither.
- A .ps or .epsf file whose header is only %! now pastes as EPS when
  copied in a file manager, as one with a %!PS header already did, and
  clipboard.importEps reads PostScript whose header is only %!, which
  it rejected before.

Docs: docs/mcp.md, docs/file-format-parity.md (.ps row),
docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-17 14:43:14 +07:00
committed by bflatastic
parent 63b95c8dd3
commit 08dc614938
10 changed files with 86 additions and 17 deletions
+3 -1
View File
@@ -81,6 +81,7 @@ pub fn file_flavour(name: &str, head: &[u8], mimes: &[&str]) -> Option<&'static
// reads SVGZ data.
"svgz" if unpacks_to_svg(head) => (SVG, SVG),
"pdf" => (PDF, PDF),
"ps" => (EPS, EPS),
"eps" => (EPS, EPS),
"ai" | "ait" if head.starts_with(b"%PDF") => (PDF, PDF),
"ai" | "ait" if vectorcraft_pdf::is_postscript(head) => (EPS, EPS),
@@ -418,7 +419,8 @@ fn import_emf(s: &mut Session, p: &Value) -> Result<Value> {
fn import_eps(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "clipboard.importEps";
let bytes = data_param(p, C)?;
if !vectorcraft_pdf::is_postscript(&bytes) {
// Programs whose header is only `%!` are read too, as they are when a `.ps` or `.eps` file opens.
if !bytes.starts_with(b"%!") && !vectorcraft_pdf::is_postscript(&bytes) {
return Err(bad(C, "the data isn't an EPS or PostScript picture"));
}
import_picture(s, p, C, "Clipboard.eps", &bytes)
+6 -5
View File
@@ -76,9 +76,10 @@ pub fn detect(name: &str, bytes: &[u8]) -> Option<&'static Format> {
return format("svgz");
}
if vectorcraft_pdf::is_postscript(bytes) {
// PostScript (.eps, and .ai saved without PDF compatibility) by any name; .ai and .ait
// keep their meaning (a template), read as PostScript all the same.
return by_name.filter(|f| matches!(f.id, "ai" | "ait")).or_else(|| format("eps"));
// PostScript (.eps, .ps, and .ai saved without PDF compatibility) by any name; .ai, .ait
// and .ps keep their meaning (a template, a PostScript file), read as PostScript all the
// same.
return by_name.filter(|f| matches!(f.id, "ai" | "ait" | "ps")).or_else(|| format("eps"));
}
if bytes.starts_with(b"%PDF") {
// .ai and .ait files are PDF inside; the extension keeps the template meaning.
@@ -197,8 +198,8 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
let mut converted = false;
let mut preview_only = false;
let (mut doc, mut warnings, restored) = match format.id {
// EPS and PostScript .ai: the document our EPS files carry, else the PostScript read.
_ if format.id == "eps" || vectorcraft_pdf::is_postscript(bytes) => {
// EPS, .ps and PostScript .ai: the document our EPS files carry, else the PostScript read.
_ if matches!(format.id, "eps" | "ps") || vectorcraft_pdf::is_postscript(bytes) => {
let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes)));
restore_or_import(editing, || {
let r = vectorcraft_eps::import_with(bytes, opts.editing_data).map_err(|e| err(format!("can't open `{}`: {e}", file_name(name))))?;
+10 -3
View File
@@ -635,7 +635,7 @@ pub const FORMATS: &[Format] = &[
Format {
id: "eps",
label: "EPS",
extensions: &["eps"],
extensions: &["eps", "epsf"],
mime: "application/postscript",
read: true,
write: true,
@@ -651,6 +651,8 @@ pub const FORMATS: &[Format] = &[
reader("affinity", "Affinity", &["af", "afdesign", "afpub"], "application/vnd.affinity", false),
// Photoshop's large document format: read like a PSD (its merged image), never written.
reader("psb", "PSB", &["psb"], "image/x-psb", true),
// PostScript programs, read as EPS files are; Print to PostScript writes them, exports don't.
reader("ps", "PostScript", &["ps"], "application/postscript", false),
];
/// Every extension `document.open` reads (the "All readable files" filter of open dialogs).
@@ -677,6 +679,8 @@ pub const OPEN_EXTS: &[&str] = &[
"emf",
"wmf",
"eps",
"epsf",
"ps",
"af",
"afdesign",
"afpub",
@@ -684,8 +688,9 @@ pub const OPEN_EXTS: &[&str] = &[
/// Extensions in [`OPEN_EXTS`] the desktop packaging doesn't associate with VectorCraft: Photoshop
/// documents belong to raster editors, so Finder and file managers don't offer a vector app for
/// them (File › Open and Place still read them).
pub const UNASSOCIATED_EXTS: &[&str] = &["psd", "psb"];
/// them, and the packaging declares no `.epsf` or `.ps` type (File › Open and Place still read
/// them all).
pub const UNASSOCIATED_EXTS: &[&str] = &["psd", "psb", "epsf", "ps"];
/// The extension that picks each writable format when exporting (the format's first; PNG-8 shares
/// `.png` with PNG, so `.png` comes once), in [`FORMATS`] order.
@@ -726,6 +731,8 @@ pub const PLACE_EXTS: &[&str] = &[
"emf",
"wmf",
"eps",
"epsf",
"ps",
"af",
"afdesign",
"afpub",
@@ -172,3 +172,44 @@ fn eps_files_place_as_one_group_clipped_to_their_bounding_box() {
assert!(format("eps").is_some_and(|f| f.read && f.write));
assert!(OPEN_EXTS.contains(&"eps") && PLACE_EXTS.contains(&"eps"));
}
/// PostScript `.ps` files open and place as EPS files do, under a PostScript format that exports
/// don't write, and `.epsf` files are EPS files; the open and place dialogs list both.
#[test]
fn ps_and_epsf_files_open_and_place() {
use crate::cmd::clipboard::{EPS, PASTE_ORDER, file_flavour};
// Pages without a bounding box (each artboard is a letter page), one of them a program whose
// header is only `%!`.
let page = b"%!PS-Adobe-3.0\n%%Pages: 1\n%%EndComments\n%%Page: 1 1\n1 0 0 setrgbcolor\n\
newpath 72 72 moveto 144 0 rlineto 0 72 rlineto -144 0 rlineto closepath fill\nshowpage\n%%EOF\n";
let bare = b"%!\n0 0 1 setrgbcolor\n100 100 50 50 rectfill\nshowpage\n";
let mut s = Session::new();
for (name, bytes, id) in [("page.ps", &page[..], "ps"), ("PAGE.PS", page, "ps"), ("bare.ps", bare, "ps"), ("bare.epsf", bare, "eps")] {
let r = open(&mut s, name, bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!((&r["format"], &r["warnings"]), (&json!(id), &json!([])), "{name}: {r}");
assert_eq!(objects(&s), 1, "{name}");
assert_eq!(s.doc().unwrap().doc.artboards[0].rect.size(), vectorcraft_geom::Size::new(612.0, 792.0), "{name}");
}
s.execute("file.new", &json!({"width": 600, "height": 600})).unwrap();
for (name, bytes, id) in [("page.ps", &page[..], "ps"), ("bare.ps", bare, "ps"), ("bare.epsf", bare, "eps")] {
let r = s.execute("file.place", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap();
assert_eq!((&r["format"], r["ids"].as_array().map(Vec::len)), (&json!(id), Some(1)), "{name}: {r}");
}
// Copied in a file manager, they paste as EPS.
for (name, bytes) in [("page.ps", &page[..]), ("bare.ps", bare), ("bare.epsf", bare), ("bare.eps", bare)] {
assert_eq!(file_flavour(name, bytes, &PASTE_ORDER), Some(EPS), "{name}");
let r = s
.execute("clipboard.importEps", &json!({"dataBase64": vectorcraft_format::base64_encode(bytes)}))
.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(r["count"], json!(1), "{name}: {r}");
}
// The dialogs list them, and the desktop packaging doesn't associate them with the app.
assert!(format("ps").is_some_and(|f| f.read && !f.write) && format("epsf").is_some_and(|f| f.id == "eps" && f.write));
for e in ["ps", "epsf"] {
assert!(OPEN_EXTS.contains(&e) && PLACE_EXTS.contains(&e) && UNASSOCIATED_EXTS.contains(&e), "{e}");
}
assert!(open_filters().any(|f| f == ("PostScript", &["ps"][..])) && place_filters().any(|f| f == ("PostScript", &["ps"][..])));
// Exports write no .ps: Print to PostScript does.
let e = writable_format(None, Some("out.ps")).unwrap_err();
assert_eq!(e, "PostScript files can be opened but not written (see document.formats)");
}