mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
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:
@@ -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)
|
||||
|
||||
@@ -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))))?;
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user