mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
G12: honor Use Low Resolution Proxy for Linked EPS (#394)
Squashed from #2975 onto main.
This commit is contained in:
@@ -370,7 +370,7 @@ pub fn open_bytes(s: &mut Session, name: &str, bytes: &[u8], path: Option<String
|
||||
/// [`open_bytes`] with the `document.open` options in `p` ([`LoadOptions::from_params`]).
|
||||
pub fn open_bytes_with(s: &mut Session, name: &str, bytes: &[u8], path: Option<String>, p: &Value) -> Result<Value> {
|
||||
let update_links = s.prefs.update_links == "automatically";
|
||||
open_read(s, read_for_open(name, bytes, path, p, update_links)?)
|
||||
open_read(s, read_for_open(name, bytes, path, p, update_links, s.prefs.low_res_proxy_eps)?)
|
||||
}
|
||||
|
||||
/// A file read for `document.open` but not opened yet ([`read_for_open`], [`open_read`]).
|
||||
@@ -396,10 +396,10 @@ impl ReadFile {
|
||||
/// [`open_read`] opens. The linked files are looked for from `path`, where the file was read from,
|
||||
/// and modified ones are read again when `update_links` (Preferences › File Handling › Update
|
||||
/// Links set to Automatically; see [`crate::cmd::links::resolve`]).
|
||||
pub fn read_for_open(name: &str, bytes: &[u8], path: Option<String>, p: &Value, update_links: bool) -> Result<ReadFile> {
|
||||
pub fn read_for_open(name: &str, bytes: &[u8], path: Option<String>, p: &Value, update_links: bool, low_res_proxy_eps: bool) -> Result<ReadFile> {
|
||||
let opts = LoadOptions::from_params("document.open", p)?;
|
||||
let mut loaded = load_with(name, bytes, &opts)?;
|
||||
let links = crate::cmd::links::resolve(&mut loaded.doc, path.as_deref(), update_links);
|
||||
let links = crate::cmd::links::resolve(&mut loaded.doc, path.as_deref(), update_links, low_res_proxy_eps);
|
||||
Session::refresh_text_bounds(&mut loaded.doc);
|
||||
Ok(ReadFile { loaded, opts, path, links })
|
||||
}
|
||||
@@ -419,7 +419,7 @@ pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&st
|
||||
"only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first",
|
||||
));
|
||||
}
|
||||
let links = crate::cmd::links::resolve(&mut loaded.doc, path, s.prefs.update_links == "automatically");
|
||||
let links = crate::cmd::links::resolve(&mut loaded.doc, path, s.prefs.update_links == "automatically", s.prefs.low_res_proxy_eps);
|
||||
open_loaded(s, loaded, links, path.map(str::to_string), &LoadOptions::default(), true)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
//! Link) link to their files the same way: checking, updating, relinking and finding them on open
|
||||
//! work alike, and they are listed. Embed (Break Link) turns one into an editable copy of its art.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Component, Path};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
@@ -286,6 +287,18 @@ fn previewing(d: &Document, key: &str) -> bool {
|
||||
d.images.get(key).is_none_or(ImageBlob::is_proxy)
|
||||
}
|
||||
|
||||
/// A linked raster image whose file is an EPS (Preferences › File Handling › Use Low Resolution
|
||||
/// Proxy for Linked EPS).
|
||||
fn linked_eps(link: &LinkInfo) -> bool {
|
||||
fileio::format_for_name(link.name()).is_some_and(|f| f.id == "eps")
|
||||
}
|
||||
|
||||
/// With `low_res_proxy_eps`, a linked EPS image that already shows its saved preview keeps it on
|
||||
/// open instead of reading the file again (Update Links still reads modified files).
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -324,6 +337,47 @@ pub(crate) fn linked_files(d: &Document, doc_path: Option<&str>) -> Vec<LinkedFi
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `d` for output: linked raster images that still show only their saved preview read their files
|
||||
/// again (Preferences › Use Low Resolution Proxy for Linked EPS keeps the preview on screen only).
|
||||
pub(crate) fn full_linked_images<'a>(d: &'a Document, doc_path: Option<&str>) -> (Cow<'a, Document>, Vec<String>) {
|
||||
let mut keys = BTreeSet::new();
|
||||
d.visit_images(|_, im| {
|
||||
if im.link.is_some() && d.images.get(&im.key).is_some_and(ImageBlob::is_proxy) {
|
||||
keys.insert(im.key.clone());
|
||||
}
|
||||
});
|
||||
if keys.is_empty() {
|
||||
return (Cow::Borrowed(d), vec![]);
|
||||
}
|
||||
let mut out = d.clone();
|
||||
let mut previews = BTreeSet::new();
|
||||
let all = groups(d, None);
|
||||
for key in keys {
|
||||
let Some(g) = all.iter().find(|g| g.document.is_none() && g.key == key) else { continue };
|
||||
let Some(mut f) = locate(&g.link, doc_path) else {
|
||||
previews.insert(key);
|
||||
continue;
|
||||
};
|
||||
let Some(bytes) = f.read().ok() else {
|
||||
previews.insert(key);
|
||||
continue;
|
||||
};
|
||||
let Ok(img) = fileio::raster_image(&bytes) else {
|
||||
previews.insert(key);
|
||||
continue;
|
||||
};
|
||||
let preview = d.images.get(&key).map(|b| b.bytes.clone());
|
||||
let mime = fileio::format_for_name(g.link.name()).map(|f| f.mime.to_string()).unwrap_or_else(|| img.blob.mime.clone());
|
||||
out.images.insert(key, ImageBlob { mime, bytes: img.blob.bytes, proxy: preview });
|
||||
}
|
||||
let mut warnings = vec![];
|
||||
if !previews.is_empty() {
|
||||
warnings
|
||||
.push(format!("{} linked image(s) output as their previews: their files can't be found or read (relink or update them)", previews.len()));
|
||||
}
|
||||
(Cow::Owned(out), warnings)
|
||||
}
|
||||
|
||||
/// The linked files of the open document `st` that `links.findFiles` looks for, each with the
|
||||
/// objects in the layers that show it: the files of objects `ids` whatever their status, else the
|
||||
/// files that can't be found.
|
||||
@@ -559,11 +613,12 @@ impl Resolved {
|
||||
/// away from their path take the new path. A link nothing is known of whose image shows a preview
|
||||
/// (a file another app wrote kept only that) was never read: its file is read now, into the
|
||||
/// image's bounds, which isn't a change to report. Nothing here is an undo step.
|
||||
pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool) -> Resolved {
|
||||
pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_proxy_eps: bool) -> Resolved {
|
||||
let mut out = Resolved::default();
|
||||
for g in groups(d, None) {
|
||||
let row = rows(std::iter::once(&g), doc_path);
|
||||
let unread = g.document.is_none() && g.link.hash.is_none() && g.link.size.is_none() && previewing(d, &g.key);
|
||||
let eps_preview = keep_eps_preview(low_res_proxy_eps, &g, d);
|
||||
match probe(&g, doc_path) {
|
||||
(Status::Missing, _) => out.missing.extend(row),
|
||||
(Status::Modified, Some(mut f)) if update || unread => {
|
||||
@@ -598,8 +653,8 @@ pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool) -> Resolv
|
||||
(Status::Ok, Some(mut f)) => {
|
||||
let preview = previewing(d, &g.key);
|
||||
// Read when the images show the preview, or when the file's details changed.
|
||||
let bytes = if preview || f.bytes.is_some() { f.read().ok() } else { None };
|
||||
if preview {
|
||||
let bytes = if (preview && !eps_preview) || f.bytes.is_some() { f.read().ok() } else { None };
|
||||
if preview && !eps_preview {
|
||||
let Some(img) = bytes.as_deref().and_then(|b| fileio::raster_image(b).ok()) else {
|
||||
out.missing.extend(row);
|
||||
continue;
|
||||
|
||||
@@ -127,7 +127,7 @@ pub(crate) fn document_art(
|
||||
}
|
||||
let mut l = fileio::load(name, bytes)?;
|
||||
// Its linked images show their files; its placed documents stay as they are.
|
||||
super::super::links::resolve(&mut l.doc, path, false);
|
||||
super::super::links::resolve(&mut l.doc, path, false, false);
|
||||
l.doc.drop_edit_modes();
|
||||
let n = l.doc.artboards.len();
|
||||
let board = (page as usize).checked_sub(1).and_then(|i| l.doc.artboards.get(i)).map(|a| a.rect);
|
||||
@@ -281,22 +281,30 @@ pub(crate) fn node(d: &mut Document, s: &Source, link: LinkInfo, bounding: bool)
|
||||
Node::new(d.alloc_id(), NodeKind::PlacedDocument(Box::new(placed)))
|
||||
}
|
||||
|
||||
/// `d` for output: the placed documents that keep only their previews get their files back,
|
||||
/// else (their files gone or changed) output as their previews, with a warning.
|
||||
/// `d` for output: linked images and placed documents that keep only their previews get their
|
||||
/// files back, else output as their previews, with a warning.
|
||||
pub(crate) fn full_documents(d: &Document) -> (Cow<'_, Document>, Vec<String>) {
|
||||
let (d, mut warnings) = super::super::links::full_linked_images(d, None);
|
||||
let doc = match &d {
|
||||
Cow::Borrowed(doc) => doc,
|
||||
Cow::Owned(doc) => doc,
|
||||
};
|
||||
let mut need: Vec<PlacedDocument> = vec![];
|
||||
d.visit_placed(|_, p| {
|
||||
if d.images.get(&p.key).is_some_and(ImageBlob::is_proxy) && !need.iter().any(|n| n.key == p.key) {
|
||||
doc.visit_placed(|_, p| {
|
||||
if doc.images.get(&p.key).is_some_and(ImageBlob::is_proxy) && !need.iter().any(|n| n.key == p.key) {
|
||||
need.push(p.clone());
|
||||
}
|
||||
});
|
||||
if need.is_empty() {
|
||||
return (Cow::Borrowed(d), vec![]);
|
||||
return (d, warnings);
|
||||
}
|
||||
let mut out = d.clone();
|
||||
let mut out = match d {
|
||||
Cow::Borrowed(doc) => doc.clone(),
|
||||
Cow::Owned(doc) => doc,
|
||||
};
|
||||
let mut previews = BTreeSet::new();
|
||||
for p in need {
|
||||
let Some(blob) = d.images.get(&p.key) else { continue };
|
||||
let Some(blob) = out.images.get(&p.key) else { continue };
|
||||
match vectorcraft_doc::placed_document::full_bytes(&p, blob) {
|
||||
Some(bytes) => {
|
||||
out.images.insert(p.key.clone(), ImageBlob { mime: mime(p.link.name(), &bytes).into(), bytes, proxy: blob.proxy.clone() });
|
||||
@@ -306,7 +314,6 @@ pub(crate) fn full_documents(d: &Document) -> (Cow<'_, Document>, Vec<String>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut warnings = vec![];
|
||||
if !previews.is_empty() {
|
||||
warnings.push(format!(
|
||||
"{} placed document(s) output as their previews: their files can't be found, or changed since they were read (relink or update them)",
|
||||
|
||||
@@ -242,7 +242,7 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>, link_pdf: bool) -> Result<Loa
|
||||
));
|
||||
}
|
||||
// Linked images show their files, found from its folder.
|
||||
super::links::resolve(&mut l.doc, src.path, false);
|
||||
super::links::resolve(&mut l.doc, src.path, false, false);
|
||||
(l.doc, l.warnings)
|
||||
};
|
||||
doc.drop_edit_modes();
|
||||
@@ -297,7 +297,7 @@ pub(crate) fn pdf_art(
|
||||
fileio::page_or_editing_data(src, bytes, o.password.as_deref(), read).map_err(|e| bad(cmd, format!("{name}: {e}")))?;
|
||||
if alone {
|
||||
// Linked images show their files, found from its folder.
|
||||
super::links::resolve(&mut doc, path, false);
|
||||
super::links::resolve(&mut doc, path, false, false);
|
||||
}
|
||||
doc.drop_edit_modes();
|
||||
let layers = std::mem::take(&mut doc.layers);
|
||||
|
||||
@@ -999,7 +999,7 @@ fn restore_one(s: &mut Session, store: &Arc<dyn RecoveryStore>, file: &str) -> R
|
||||
doc.title = if meta.title.is_empty() { stem.to_string() } else { meta.title.clone() };
|
||||
}
|
||||
// Linked files are looked for from the document's own folder.
|
||||
let links = super::links::resolve(&mut doc, meta.path.as_deref(), s.prefs.update_links == "automatically");
|
||||
let links = super::links::resolve(&mut doc, meta.path.as_deref(), s.prefs.update_links == "automatically", s.prefs.low_res_proxy_eps);
|
||||
let index = s.add_document(doc, meta.path.clone());
|
||||
let st = s.doc_mut()?;
|
||||
st.recovered = true;
|
||||
|
||||
@@ -293,6 +293,60 @@ fn a_modified_link_follows_the_update_links_preference() {
|
||||
assert!(s.doc().unwrap().history.undo.is_empty(), "opening is not an undo step");
|
||||
}
|
||||
|
||||
/// Preferences › File Handling › Use Low Resolution Proxy for Linked EPS (#394): an unchanged linked
|
||||
/// `.eps` keeps its saved preview on open; Update Links still reads a modified file when set to
|
||||
/// Automatically.
|
||||
#[test]
|
||||
fn low_res_proxy_eps_keeps_unchanged_preview_and_automatic_update_reads_modified_eps() {
|
||||
let dir = Folder::new("eps-proxy");
|
||||
let pic = dir.file("photo.eps");
|
||||
write(&pic, &png(600, 300, RED));
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &pic);
|
||||
let doc_path = dir.file("poster.vectorcraft");
|
||||
save(&mut s, &doc_path);
|
||||
s.execute("prefs.set", &json!({"key": "lowResProxyEps", "value": true})).unwrap();
|
||||
s.execute("prefs.set", &json!({"key": "updateLinks", "value": "askWhenModified"})).unwrap();
|
||||
let r = open(&mut s, &doc_path);
|
||||
assert_eq!((r["modifiedLinks"].clone(), r["updatedLinks"].clone()), (json!([]), json!([])), "{r}");
|
||||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED), "unchanged EPS stays on its preview");
|
||||
assert!(s.doc().unwrap().doc.images[&image(&s, id).key].is_proxy());
|
||||
write(&pic, &png(400, 400, BLUE));
|
||||
s.execute("prefs.set", &json!({"key": "updateLinks", "value": "automatically"})).unwrap();
|
||||
let r = open(&mut s, &doc_path);
|
||||
assert_eq!(r["updatedLinks"][0]["ids"].clone(), json!([id.0]), "modified EPS updates on open: {r}");
|
||||
assert!(!s.doc().unwrap().doc.images[&image(&s, id).key].is_proxy());
|
||||
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
|
||||
}
|
||||
|
||||
/// With Low Resolution Proxy on, export reads the linked EPS file's full pixels, not the preview.
|
||||
#[test]
|
||||
fn low_res_proxy_eps_export_uses_full_resolution_from_the_linked_file() {
|
||||
let dir = Folder::new("eps-proxy-export");
|
||||
let pic = dir.file("photo.eps");
|
||||
write(&pic, &png(600, 300, RED));
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &pic);
|
||||
let doc_path = dir.file("poster.vectorcraft");
|
||||
save(&mut s, &doc_path);
|
||||
s.execute("prefs.set", &json!({"key": "lowResProxyEps", "value": true})).unwrap();
|
||||
let r = open(&mut s, &doc_path);
|
||||
assert_eq!((r["modifiedLinks"].clone(), r["updatedLinks"].clone()), (json!([]), json!([])), "{r}");
|
||||
let key = image(&s, id).key;
|
||||
assert!(s.doc().unwrap().doc.images[&key].is_proxy());
|
||||
let (full, warnings) = cmd::links::full_linked_images(&s.doc().unwrap().doc, None);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert!(!full.images[&key].is_proxy());
|
||||
let dim =
|
||||
image::ImageReader::new(std::io::Cursor::new(full.images[&key].bytes.as_ref())).with_guessed_format().unwrap().into_dimensions().unwrap();
|
||||
assert_eq!(dim, (600, 300));
|
||||
let png_out = s.execute("document.export", &json!({"format": "png"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(png_out["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let rendered = image::load_from_memory(&bytes).unwrap();
|
||||
assert!(rendered.width() >= 400 && rendered.height() >= 300);
|
||||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED), "canvas still shows the preview");
|
||||
}
|
||||
|
||||
/// The half of opening that the desktop app runs on a thread of its own (`read_for_open`) reads
|
||||
/// the linked files and lays out the type. The half left for the UI thread (`open_read`) reads no
|
||||
/// file: with a linked file moved away in between, it opens the document that `document.open` opens.
|
||||
@@ -326,7 +380,7 @@ fn a_file_read_for_opening_has_its_links_read_and_its_type_laid_out() {
|
||||
let expected = open(&mut at_once, &doc_path);
|
||||
let rows = if update == "automatically" { &expected["updatedLinks"] } else { &expected["modifiedLinks"] };
|
||||
assert!(expected["missingLinks"][0]["name"] == "gone.png" && rows[0]["name"] == "changed.png", "{update}: {expected}");
|
||||
let file = cmd::fileio::read_for_open(&doc_path, &bytes, Some(doc_path.clone()), &Value::Null, update == "automatically").unwrap();
|
||||
let file = cmd::fileio::read_for_open(&doc_path, &bytes, Some(doc_path.clone()), &Value::Null, update == "automatically", false).unwrap();
|
||||
let doc = file.document();
|
||||
let NodeKind::Image(im) = &doc.node(photo).unwrap().kind else { panic!("not an image") };
|
||||
assert!(!doc.images[&im.key].is_proxy(), "{update}: the TIFF is read, not the preview saved for it");
|
||||
|
||||
@@ -1393,6 +1393,33 @@ fn highlight_alternate_glyphs_preference_is_off_by_default() {
|
||||
assert!(!s.prefs.highlight_alternate_glyphs);
|
||||
}
|
||||
|
||||
/// File Handling › Use Low Resolution Proxy for Linked EPS (#394): off by default; toggling it does
|
||||
/// not replace full-resolution pixels already in the document.
|
||||
#[test]
|
||||
fn low_res_proxy_eps_preference_does_not_swap_full_resolution_pixels() {
|
||||
use crate::tests_links::{Folder, RED, place, png, save, write};
|
||||
let dir = Folder::new("eps-proxy-pref");
|
||||
let pic = dir.file("photo.eps");
|
||||
write(&pic, &png(600, 300, RED));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
assert!(!s.prefs.low_res_proxy_eps);
|
||||
let id = place(&mut s, &pic);
|
||||
let doc_path = dir.file("doc.vectorcraft");
|
||||
save(&mut s, &doc_path);
|
||||
s.execute("links.update", &json!({})).unwrap();
|
||||
let key = {
|
||||
let st = s.doc().unwrap();
|
||||
let NodeKind::Image(im) = &st.doc.node(id).unwrap().kind else { panic!("image") };
|
||||
im.key.clone()
|
||||
};
|
||||
assert!(!s.doc().unwrap().doc.images[&key].is_proxy());
|
||||
set_pref(&mut s, "lowResProxyEps", json!(true));
|
||||
assert!(!s.doc().unwrap().doc.images[&key].is_proxy(), "full pixels stay in the document");
|
||||
set_pref(&mut s, "lowResProxyEps", json!(false));
|
||||
assert!(!s.prefs.low_res_proxy_eps);
|
||||
}
|
||||
|
||||
/// Preferences › Hyphenation › Exceptions (#394): `prefs.set` pushes the list into the hyphenator
|
||||
/// and bumps open documents so type reflows. Parsing of the list is covered in `vectorcraft-text`.
|
||||
#[test]
|
||||
|
||||
@@ -49,13 +49,14 @@ pub fn start(app: &mut VectorcraftApp, path: &str, bytes: Vec<u8>) -> Result<Val
|
||||
let (name, file) = (path.to_string(), bytes.clone());
|
||||
// Update Links (Preferences › File Handling) applies there as it is set now.
|
||||
let update_links = app.session.prefs.update_links == "automatically";
|
||||
let low_res_proxy_eps = app.session.prefs.low_res_proxy_eps;
|
||||
// The automation roots in force here are in force there.
|
||||
let scope = ScopeHandle::capture();
|
||||
let spawned = std::thread::Builder::new().name("vectorcraft-open".into()).spawn(move || {
|
||||
let r = scope
|
||||
.run(|| {
|
||||
vectorcraft_engine::guard::catch_panic(|| {
|
||||
fileio::read_for_open(&name, &file, Some(name.clone()), &Value::Null, update_links).map_err(|e| e.to_string())
|
||||
fileio::read_for_open(&name, &file, Some(name.clone()), &Value::Null, update_links, low_res_proxy_eps).map_err(|e| e.to_string())
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|msg| Err(format!("internal error: {msg} (please report this bug)")));
|
||||
|
||||
Reference in New Issue
Block a user