274 lines
13 KiB
Rust
274 lines
13 KiB
Rust
//! Placed documents (`file.place` of a VectorCraft document from a file, with `link`): one
|
||
//! locked object showing an artboard of the document, a [`PlacedDocument`].
|
||
//!
|
||
//! The object keeps the file's bytes (in the document's images, like image data) and the link; the
|
||
//! art it shows is read from them the way placing the file without Link reads it
|
||
//! ([`document_art`]: the artboard's art, with the file's own linked images found), so Break Link
|
||
//! and Object › Expand ([`expand`]) give exactly that editable copy. The Links commands
|
||
//! ([`super::super::links`]) tell when the file changed and read it again.
|
||
//!
|
||
//! Each placed file also gets a preview ([`PREVIEW_PX_PER_PT`]; JPEG when opaque, else PNG), kept
|
||
//! as the blob's [`ImageBlob::proxy`]: a save writes only the preview (unless Include Linked
|
||
//! Files), as for linked images. The file is read again when it is needed ([`read_file_again`]:
|
||
//! output, or the screen zoomed past the preview), while it is unchanged, and output falls back to
|
||
//! the preview with a warning when it can't be ([`full_documents`]).
|
||
|
||
use std::borrow::Cow;
|
||
use std::collections::BTreeSet;
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_doc::links::hash_bytes;
|
||
use vectorcraft_doc::placed_document::{PREVIEW_MAX, PREVIEW_PX_PER_PT, key_for};
|
||
use vectorcraft_doc::{Document, ImageBlob, LinkInfo, Node, NodeKind, PlacedDocument, PlacementOptions};
|
||
use vectorcraft_geom::{Affine, Rect};
|
||
|
||
use super::super::fileio;
|
||
use super::super::*;
|
||
|
||
/// The largest file placed (bytes).
|
||
pub(crate) const MAX_FILE: usize = 256 << 20;
|
||
/// The format ids of VectorCraft documents.
|
||
const NATIVE: [&str; 2] = ["vectorcraft", "template"];
|
||
/// The type a placed file's bytes are stored with.
|
||
const MIME: &str = "application/json";
|
||
|
||
/// Is `name` with `bytes` a VectorCraft document?
|
||
pub(crate) fn is_native(name: &str, bytes: &[u8]) -> bool {
|
||
fileio::detect(name, bytes).is_some_and(|f| NATIVE.contains(&f.id))
|
||
}
|
||
|
||
/// An artboard of a VectorCraft document, read as art to place.
|
||
pub(crate) struct DocumentArt {
|
||
/// The document, its layers taken: the art's resources.
|
||
pub doc: Document,
|
||
/// The art on the artboard (guides, hidden and template layers left out).
|
||
pub nodes: Vec<Node>,
|
||
/// The artboard the art is clipped to (`None`: the art's bounds are shown).
|
||
pub clip: Option<Rect>,
|
||
/// What is shown: the artboard, or the art's bounds.
|
||
pub frame: Rect,
|
||
pub warnings: Vec<String>,
|
||
}
|
||
|
||
/// Artboard `page` (1-based) of the VectorCraft document `name` (`bytes`, read from `path` when
|
||
/// there is one, so its linked images are found), clipped to the artboard unless `bounding`.
|
||
pub(crate) fn document_art(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bounding: bool, cmd: &str) -> Result<DocumentArt> {
|
||
let file = fileio::file_name(name);
|
||
if !is_native(name, bytes) {
|
||
return Err(bad(cmd, format!("`{file}` isn't a VectorCraft document")));
|
||
}
|
||
if bytes.len() > MAX_FILE {
|
||
return Err(bad(cmd, format!("`{file}` is over {} MB", MAX_FILE >> 20)));
|
||
}
|
||
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);
|
||
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);
|
||
let board = board.ok_or_else(|| bad(cmd, format!("page {page}: `{file}` has {n} artboard(s)")))?;
|
||
let layers = std::mem::take(&mut l.doc.layers);
|
||
let nodes = super::on_artboard(&layers, board);
|
||
let frame = if bounding { nodes.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.visual_bounds())) } else { Some(board) };
|
||
let frame = frame.filter(|r| r.width() > 0.0 || r.height() > 0.0).ok_or_else(|| bad(cmd, format!("`{file}` has no art to place")))?;
|
||
Ok(DocumentArt { doc: l.doc, nodes, clip: (!bounding).then_some(board), frame, warnings: l.warnings })
|
||
}
|
||
|
||
/// `nodes` as one group, clipped to `clip` (its own ids: what drawing reads).
|
||
fn group(nodes: Vec<Node>, clip: Option<Rect>) -> Node {
|
||
let mut children: Vec<Arc<Node>> = nodes.into_iter().map(Arc::new).collect();
|
||
if let Some(r) = clip {
|
||
children.insert(0, Arc::new(super::clip_path(NodeId(0), r)));
|
||
}
|
||
Node::new(NodeId(0), NodeKind::Group { children, clip: clip.is_some() })
|
||
}
|
||
|
||
/// `p`'s file (`bytes`) read as the art it shows: how the document model reads placed documents
|
||
/// ([`vectorcraft_doc::placed_document::set_loader`]).
|
||
pub(crate) fn read_document(p: &PlacedDocument, bytes: &[u8]) -> Option<(Document, Node, Rect)> {
|
||
let a = document_art(p.link.name(), bytes, Some(&p.link.path), p.page(), p.bounding, "draw").ok()?;
|
||
Some((a.doc, group(a.nodes, a.clip), a.frame))
|
||
}
|
||
|
||
/// `p`'s file read again, while it is the file it was read from (how the document model gets it
|
||
/// when only the preview is stored, [`vectorcraft_doc::placed_document::set_file_reader`]).
|
||
pub(crate) fn read_file_again(p: &PlacedDocument) -> Option<Vec<u8>> {
|
||
let bytes = fileio::read_file(&p.link.path).ok()?;
|
||
if p.link.hash.as_ref().is_some_and(|h| *h != hash_bytes(&bytes)) {
|
||
return None;
|
||
}
|
||
Some(bytes)
|
||
}
|
||
|
||
/// A VectorCraft document read for placing linked.
|
||
#[derive(Clone)]
|
||
pub(crate) struct Source {
|
||
/// The file's bytes, with their preview.
|
||
pub blob: ImageBlob,
|
||
/// The blob's key ([`key_for`]).
|
||
pub key: String,
|
||
/// What is shown, in the file's coordinates: the artboard or the art's bounds.
|
||
pub frame: Rect,
|
||
pub warnings: Vec<String>,
|
||
}
|
||
|
||
/// The VectorCraft document `name` (`bytes`, from `path` when there is one) read for showing
|
||
/// artboard `page` (its art's bounds when `bounding`).
|
||
pub(crate) fn source(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bounding: bool, cmd: &str) -> Result<Source> {
|
||
// The same document placed the same way is read once (placing it again is free).
|
||
let key = key_for(&hash_bytes(bytes), page, bounding);
|
||
if let Some(s) = lock_memo().iter().find(|s| s.key == key).cloned() {
|
||
return Ok(s);
|
||
}
|
||
let a = document_art(name, bytes, path, page, bounding, cmd)?;
|
||
let mut blob = ImageBlob::new(MIME, bytes.to_vec());
|
||
let shown = PlacedDocument {
|
||
link: LinkInfo { page: Some(page), ..LinkInfo::new(path.unwrap_or(name)) },
|
||
key: key.clone(),
|
||
bounding,
|
||
width: a.frame.width(),
|
||
height: a.frame.height(),
|
||
xf: Affine::IDENTITY,
|
||
placement: PlacementOptions::default(),
|
||
};
|
||
let art = group(a.nodes, a.clip);
|
||
blob.proxy = preview(&a.doc, &art, a.frame).map(Arc::new);
|
||
// Drawing it needn't read it again.
|
||
vectorcraft_doc::placed_document::prime(&shown, blob.bytes.clone(), (a.doc, art, a.frame));
|
||
let s = Source { blob, key, frame: a.frame, warnings: a.warnings };
|
||
let mut m = lock_memo();
|
||
if m.len() >= MEMO_SIZE {
|
||
m.remove(0);
|
||
}
|
||
m.push(s.clone());
|
||
Ok(s)
|
||
}
|
||
|
||
/// The most files remembered.
|
||
const MEMO_SIZE: usize = 32;
|
||
|
||
/// Files read lately for placing, by key.
|
||
static MEMO: std::sync::Mutex<Vec<Source>> = std::sync::Mutex::new(Vec::new());
|
||
|
||
fn lock_memo() -> std::sync::MutexGuard<'static, Vec<Source>> {
|
||
// Entries are only pushed and removed: a panic while held leaves it whole.
|
||
MEMO.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||
}
|
||
|
||
/// The preview of `art` (of document `doc`, showing `frame`): [`PREVIEW_PX_PER_PT`], at most
|
||
/// [`PREVIEW_MAX`] pixels a side; JPEG when opaque (inside its outer pixels), else PNG.
|
||
fn preview(doc: &Document, art: &Node, frame: Rect) -> Option<Vec<u8>> {
|
||
let k = PREVIEW_PX_PER_PT.min(PREVIEW_MAX as f64 / frame.width().max(frame.height()).max(1e-9));
|
||
let side = |v: f64| (v * k).round().clamp(1.0, PREVIEW_MAX as f64) as u32;
|
||
let (w, h) = (side(frame.width()), side(frame.height()));
|
||
let view = Affine::scale_non_uniform(w as f64 / frame.width().max(1e-9), h as f64 / frame.height().max(1e-9))
|
||
* Affine::translate((-frame.x0, -frame.y0));
|
||
let mut r = vectorcraft_render::Renderer::new();
|
||
let img = vectorcraft_render::placed_document::render_inside(&mut r, doc, &Arc::new(art.clone()), w as u16, h as u16, view)?;
|
||
// Premultiplied → straight.
|
||
let px = img
|
||
.pixels
|
||
.as_chunks::<4>()
|
||
.0
|
||
.iter()
|
||
.flat_map(|&[r, g, b, a]| {
|
||
let un = |c: u8| if a == 0 { 0 } else { ((c as u32 * 255 + a as u32 / 2) / a as u32).min(255) as u8 };
|
||
[un(r), un(g), un(b), a]
|
||
})
|
||
.collect();
|
||
let rgba = image::RgbaImage::from_raw(w, h, px)?;
|
||
let mut out = vec![];
|
||
// Opaque but for the edge's anti-aliasing (an artboard drawn to whole pixels) and compositing's
|
||
// rounding: JPEG.
|
||
let inner = |x: u32, y: u32| x > 0 && y > 0 && x + 1 < w && y + 1 < h;
|
||
if rgba.enumerate_pixels().all(|(x, y, p)| p[3] >= 250 || !inner(x, y)) {
|
||
let rgb = image::DynamicImage::ImageRgba8(rgba).to_rgb8();
|
||
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, 80).encode_image(&rgb).ok()?;
|
||
} else {
|
||
rgba.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?;
|
||
}
|
||
Some(out)
|
||
}
|
||
|
||
/// `s` stored in `d` (objects showing the same thing share one blob).
|
||
pub(crate) fn store(d: &mut Document, s: &Source) {
|
||
d.images.entry(s.key.clone()).or_insert_with(|| s.blob.clone());
|
||
}
|
||
|
||
/// A new placed document of `d` showing `s` at 100%, where its art lies in the file.
|
||
pub(crate) fn node(d: &mut Document, s: &Source, link: LinkInfo, bounding: bool) -> Node {
|
||
store(d, s);
|
||
let placed = PlacedDocument {
|
||
link,
|
||
key: s.key.clone(),
|
||
bounding,
|
||
width: s.frame.width(),
|
||
height: s.frame.height(),
|
||
xf: Affine::translate((s.frame.x0, s.frame.y0)),
|
||
placement: PlacementOptions::default(),
|
||
};
|
||
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.
|
||
pub(crate) fn full_documents(d: &Document) -> (Cow<'_, Document>, Vec<String>) {
|
||
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) {
|
||
need.push(p.clone());
|
||
}
|
||
});
|
||
if need.is_empty() {
|
||
return (Cow::Borrowed(d), vec![]);
|
||
}
|
||
let mut out = d.clone();
|
||
let mut previews = BTreeSet::new();
|
||
for p in need {
|
||
let Some(blob) = d.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.into(), bytes, proxy: blob.proxy.clone() });
|
||
}
|
||
None => {
|
||
previews.insert(p.key);
|
||
}
|
||
}
|
||
}
|
||
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)",
|
||
previews.len()
|
||
));
|
||
out.placed_as_previews(&previews);
|
||
}
|
||
(Cow::Owned(out), warnings)
|
||
}
|
||
|
||
/// Placed document `id` of `d` replaced by an editable copy of its art, as placing its file
|
||
/// without Link gives (its resources joining `d`, renamed where `d` uses a name for something
|
||
/// else), keeping the object's id, place, name and transparency: Break Link and Object › Expand.
|
||
pub(crate) fn expand(d: &mut Document, id: NodeId, cmd: &str) -> Result<()> {
|
||
let n = d.node(id).cloned().ok_or(EngineError::NoNode(id))?;
|
||
let NodeKind::PlacedDocument(p) = &n.kind else { return Err(bad(cmd, format!("object {} is not a placed document", id.0))) };
|
||
let blob = d.images.get(&p.key).ok_or_else(|| bad(cmd, format!("object {} has lost its file's contents: relink it", id.0)))?;
|
||
let bytes = vectorcraft_doc::placed_document::full_bytes(p, blob)
|
||
.ok_or_else(|| bad(cmd, format!("{} can't be read, or changed since it was read: relink or update it first", p.link.path)))?;
|
||
let a = document_art(p.link.name(), &bytes, Some(&p.link.path), p.page(), p.bounding, cmd)?;
|
||
let m = p.art_xf(a.frame).ok_or_else(|| bad(cmd, format!("object {} has no size", id.0)))?;
|
||
let mut nodes = a.nodes;
|
||
super::adopt::adopt(d, &a.doc, &mut nodes);
|
||
let mut children: Vec<Arc<Node>> = nodes.iter().map(|n| Arc::new(d.reid(n))).collect();
|
||
if let Some(r) = a.clip {
|
||
children.insert(0, Arc::new(super::clip_path(d.alloc_id(), r)));
|
||
}
|
||
let art = super::transformed(Node::new(d.alloc_id(), NodeKind::Group { children, clip: a.clip.is_some() }), m);
|
||
let mut out = n.clone();
|
||
out.kind = art.kind;
|
||
let (par, index, _) = d.position(id).ok_or(EngineError::NoNode(id))?;
|
||
d.remove(id)?;
|
||
d.insert(par, index, out)?;
|
||
Ok(())
|
||
}
|