mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 13:36:53 +00:00
Merge pull request #7059 from dotCooCoo/fix/dcs-eps-proxy-plates
Place: DCS links reopen with their plates under the low-resolution EPS proxy
This commit is contained in:
@@ -387,6 +387,26 @@ fn keep_eps_preview(low_res_proxy_eps: bool, g: &Group, d: &Document) -> bool {
|
||||
low_res_proxy_eps && g.document.is_none() && linked_eps(&g.link) && previewing(d, &g.key)
|
||||
}
|
||||
|
||||
/// Is the linked file `f` found a DCS file? Only the start of the file is read, unless a binary EPS
|
||||
/// header puts its PostScript further on; a DCS file read in full stays in `f`. A file that can't
|
||||
/// be read is not one.
|
||||
fn is_dcs_file(f: &mut Found) -> bool {
|
||||
use super::place::dcs;
|
||||
if let Some(b) = &f.bytes {
|
||||
return dcs::is_dcs(b);
|
||||
}
|
||||
let Ok(head) = fileio::read_file_head(&f.path, dcs::HEAD_BYTES) else { return false };
|
||||
if let Some(is) = dcs::head_is_dcs(&head, f.size) {
|
||||
return is;
|
||||
}
|
||||
let Ok(b) = f.read() else { return false };
|
||||
let is = dcs::is_dcs(&b);
|
||||
if is {
|
||||
f.bytes = Some(b);
|
||||
}
|
||||
is
|
||||
}
|
||||
|
||||
/// Does group `g` of `d` show something other than its file: an image its preview, a placed
|
||||
/// document nothing (its file's bytes are gone)?
|
||||
fn stale(d: &Document, g: &Group) -> bool {
|
||||
@@ -1159,6 +1179,9 @@ pub fn resolve(d: &mut Document, base: LinkBase, update: bool, low_res_proxy_eps
|
||||
}
|
||||
(Status::Ok, Some(mut f)) => {
|
||||
let preview = previewing(d, &g.key);
|
||||
// A DCS file is read in full even with the proxy: its plates print and separate on
|
||||
// their own inks.
|
||||
let eps_preview = eps_preview && !is_dcs_file(&mut f);
|
||||
if let Some((blob, hash)) = read_images.get(&f.path).filter(|_| preview && !eps_preview).cloned() {
|
||||
store_image(d, &g.key, blob, true);
|
||||
let link = f.link_hashed(hash, &g.link);
|
||||
|
||||
@@ -39,6 +39,55 @@ pub(crate) fn is_dcs(bytes: &[u8]) -> bool {
|
||||
vectorcraft_eps::dcs::read(bytes).is_some()
|
||||
}
|
||||
|
||||
/// The most bytes read from the start of a file to look at its DCS header.
|
||||
pub(crate) const HEAD_BYTES: usize = 1 << 20;
|
||||
|
||||
/// Does the header of the PostScript in file `head` (the first bytes of a file `size` bytes long,
|
||||
/// at most [`HEAD_BYTES`]) name DCS plates? `None` when `head` can't tell: that PostScript doesn't
|
||||
/// start within it (a binary EPS header that puts it further on), or its header goes on past it. A
|
||||
/// `head` that holds the whole file is read as [`is_dcs`] reads it, and a binary EPS header whose
|
||||
/// PostScript or TIFF preview ends past `size` names none, as [`is_dcs`] finds for the whole file.
|
||||
pub(crate) fn head_is_dcs(head: &[u8], size: u64) -> Option<bool> {
|
||||
const DOS_MAGIC: [u8; 4] = [0xC5, 0xD0, 0xD3, 0xC6];
|
||||
if head.len() < HEAD_BYTES || head.len() as u64 >= size {
|
||||
return Some(is_dcs(head));
|
||||
}
|
||||
let (ps, whole) = if head.starts_with(&DOS_MAGIC) {
|
||||
let word = |at: usize| head.get(at..at + 4).and_then(|b| <[u8; 4]>::try_from(b).ok()).map(|b| u32::from_le_bytes(b) as usize);
|
||||
let fits = |at: usize| Some((word(at)? as u64).checked_add(word(at + 4)? as u64).is_some_and(|end| end <= size));
|
||||
if !fits(4)? || (word(24)? != 0 && !fits(20)?) {
|
||||
return Some(false);
|
||||
}
|
||||
let (start, len) = (word(4)?, word(8)?);
|
||||
let ps = head.get(start..).filter(|ps| !ps.is_empty())?;
|
||||
(ps.get(..len).unwrap_or(ps), start.checked_add(len).is_some_and(|end| end <= head.len()))
|
||||
} else {
|
||||
(head, false)
|
||||
};
|
||||
if is_dcs(ps) {
|
||||
return Some(true);
|
||||
}
|
||||
(whole || header_ended(ps)).then_some(false)
|
||||
}
|
||||
|
||||
/// Does `ps`, the start of some PostScript, hold all of the header [`vectorcraft_eps::dcs::read`]
|
||||
/// looks at (up to `%%EndComments`, a line that isn't a comment, or its 10,000th line)? Its last
|
||||
/// line may be cut off, so it doesn't count.
|
||||
fn header_ended(ps: &[u8]) -> bool {
|
||||
let mut lines = ps.split(|b| matches!(b, b'\n' | b'\r')).filter(|l| !l.is_empty()).peekable();
|
||||
let mut n = 0;
|
||||
while let Some(line) = lines.next() {
|
||||
if lines.peek().is_none() {
|
||||
return false;
|
||||
}
|
||||
n += 1;
|
||||
if line.starts_with(b"%%EndComments") || !line.starts_with(b"%") || n >= 10_000 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// DCS file `bytes`, read from `path` (when there is one: plate files of their own are found in its
|
||||
/// folder), with every plate.
|
||||
pub(crate) fn read(bytes: &[u8], path: Option<&str>) -> Result<DcsArt> {
|
||||
|
||||
@@ -632,6 +632,86 @@ fn a_saved_document_reads_the_plates_again_and_prints_the_spot_ink() {
|
||||
assert_eq!(inks, ["Cyan", "Magenta", "Yellow", "Black", "PANTONE 185 C"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_document_reads_the_plates_again_with_the_low_resolution_eps_proxy() {
|
||||
let dir = temp_dir("saved-proxy");
|
||||
// Plates wider than a preview, so the saved document keeps a preview of the image.
|
||||
let wide = |gray: u8| {
|
||||
let hex = format!("{gray:02x}").repeat(300);
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n20 10 scale\n300 1 8 [300 0 0 -1 0 1] <{hex}> image\nshowpage\n%%Trailer\n%%EOF\n"
|
||||
)
|
||||
};
|
||||
let file = single_file(&[("Cyan", wide(0)), ("PANTONE 185 C", wide(128))], "%%CMYKCustomColor: 0 .91 .76 0 (PANTONE 185 C)\n");
|
||||
let path = write(&dir, "pic.eps", &file);
|
||||
let saved = dir.join("doc.vectorcraft").to_string_lossy().into_owned();
|
||||
let mut s = session();
|
||||
s.execute("file.place", &json!({ "path": path })).unwrap();
|
||||
s.execute("document.save", &json!({ "path": saved })).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.execute("prefs.set", &json!({"key": "lowResProxyEps", "value": true})).unwrap();
|
||||
s.execute("document.open", &json!({ "path": saved })).unwrap();
|
||||
let list = s.execute("links.list", &json!({})).unwrap();
|
||||
assert_eq!(list["links"][0]["format"], "DCS", "{list}");
|
||||
let id = NodeId(list["links"][0]["id"].as_u64().unwrap());
|
||||
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
|
||||
assert!(s.doc().unwrap().doc.images[&im.key].separated().is_some(), "the plates, read from the file again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dcs_header_is_found_at_the_start_of_a_file_or_behind_a_binary_eps_header() {
|
||||
use crate::cmd::place::dcs::{HEAD_BYTES, head_is_dcs, is_dcs};
|
||||
// A whole file, as `is_dcs` reads it.
|
||||
let whole = |b: &[u8]| {
|
||||
let is = head_is_dcs(b, b.len() as u64);
|
||||
assert_eq!(is, Some(is_dcs(b)));
|
||||
is
|
||||
};
|
||||
let ps = two_plates();
|
||||
assert_eq!(whole(&ps), Some(true));
|
||||
assert_eq!(whole(b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n"), Some(false));
|
||||
let binary = |start: u32| {
|
||||
let mut b = vec![0xC5, 0xD0, 0xD3, 0xC6];
|
||||
b.extend(start.to_le_bytes());
|
||||
b.extend((ps.len() as u32).to_le_bytes());
|
||||
b.resize(start as usize, 0);
|
||||
b.extend(&ps);
|
||||
b
|
||||
};
|
||||
assert_eq!(whole(&binary(30)), Some(true));
|
||||
// A binary header whose PostScript or TIFF preview ends past the end of the file names none.
|
||||
let mut cut = binary(30);
|
||||
cut[8..12].copy_from_slice(&(64u32 << 20).to_le_bytes());
|
||||
assert_eq!(whole(&cut), Some(false));
|
||||
let mut tiff = binary(30);
|
||||
tiff[20..24].copy_from_slice(&30u32.to_le_bytes());
|
||||
tiff[24..28].copy_from_slice(&(64u32 << 20).to_le_bytes());
|
||||
assert_eq!(whole(&tiff), Some(false));
|
||||
// A file longer than the bytes read, its PostScript running past them: its header is read from
|
||||
// the part there is, unless the header puts the PostScript or the preview past the file's end.
|
||||
let mut big = binary(30);
|
||||
big.resize(HEAD_BYTES, b' ');
|
||||
big[8..12].copy_from_slice(&(64u32 << 20).to_le_bytes());
|
||||
assert_eq!(head_is_dcs(&big, 30 + (64 << 20)), Some(true));
|
||||
assert_eq!(head_is_dcs(&big, 30 + (64 << 20) - 1), Some(false));
|
||||
big[20..24].copy_from_slice(&30u32.to_le_bytes());
|
||||
big[24..28].copy_from_slice(&(65u32 << 20).to_le_bytes());
|
||||
assert_eq!(head_is_dcs(&big, 30 + (64 << 20)), Some(false));
|
||||
// PostScript that starts past the bytes read: the whole file is needed.
|
||||
let far = binary(HEAD_BYTES as u32 + 10);
|
||||
assert_eq!(head_is_dcs(&far[..HEAD_BYTES], far.len() as u64), None);
|
||||
// A header that goes on past the bytes read may name its plates further on.
|
||||
let mut long = b"%!PS-Adobe-3.0 EPSF-3.0\n".to_vec();
|
||||
while long.len() < HEAD_BYTES {
|
||||
long.extend(format!("%%Title: {}\n", "x".repeat(190)).as_bytes());
|
||||
}
|
||||
long.truncate(HEAD_BYTES);
|
||||
assert_eq!(head_is_dcs(&long, HEAD_BYTES as u64 + 1), None);
|
||||
// One that ended within them names none.
|
||||
long[30..45].copy_from_slice(b"\n%%EndComments\n");
|
||||
assert_eq!(head_is_dcs(&long, HEAD_BYTES as u64 + 1), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dcs_transparency_marks_transparent_and_overlapped_dcs_images() {
|
||||
let dir = temp_dir("transparency");
|
||||
|
||||
+2
-1
@@ -6069,7 +6069,8 @@ stays as a plain group of the image, with its id, name, visibility and transpare
|
||||
even-odd fill rule stays as it is). The link shows as modified when a plate file of its own changes
|
||||
or is gone, as it does when the DCS file changes, and the app's link watch acts on a changed plate file as on a
|
||||
changed DCS file. `links.relink {copy: true}` fails for a DCS file that reads plate files of its own, and copies
|
||||
nothing. These rules are VectorCraft's own: the screen, composite print and exports show the image as its plates
|
||||
nothing. Opening a document reads a DCS file and its plates again even with the `lowResProxyEps` preference, which
|
||||
keeps the saved preview of other linked EPS files. These rules are VectorCraft's own: the screen, composite print and exports show the image as its plates
|
||||
mixed into CMYK (each ink amount times the ink's CMYK values, plates combined as `1 − (1 − a)(1 − b)`); Separations
|
||||
Preview and `file.print` separations show each plate on its own ink, and a spot plate prints as the document's swatch
|
||||
of its ink (else its CMYK values) with `spotsToProcess`. Separations Preview shows a reduced image's plates as the
|
||||
|
||||
Reference in New Issue
Block a user