mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
DES-10 review: export linked graphics in EPUB/HTML, PPTX, IDML and packages; cache output bytes in PDF export
This commit is contained in:
1 parent
829e5b86c3
commit
3bd9332a6c
12 files changed
+175
-44
No files matched your search
+13
-7
@@ -659,6 +659,8 @@ pub struct OutputBytes {
|
||||
/// A warning to report: the file on disk replaced a changed original, or the proxy stands in
|
||||
/// for a missing one.
|
||||
pub warning: Option<String>,
|
||||
/// The bytes are the asset's preview, not the original (a missing or unreadable link).
|
||||
pub proxied: bool,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
@@ -667,16 +669,20 @@ impl Asset {
|
||||
/// there is neither.
|
||||
pub fn output_bytes(&self) -> std::result::Result<OutputBytes, Unavailable> {
|
||||
match self.data.load_for_output() {
|
||||
Ok((bytes, false)) => Ok(OutputBytes { bytes, warning: None }),
|
||||
Ok((bytes, true)) => {
|
||||
Ok(OutputBytes { bytes, warning: Some(format!("the linked file `{}` has changed; the file on disk is used", self.name)) })
|
||||
}
|
||||
Ok((bytes, false)) => Ok(OutputBytes { bytes, warning: None, proxied: false }),
|
||||
Ok((bytes, true)) => Ok(OutputBytes {
|
||||
bytes,
|
||||
warning: Some(format!("the linked file `{}` has changed; the file on disk is used", self.name)),
|
||||
proxied: false,
|
||||
}),
|
||||
Err(e) => {
|
||||
let proxy = self.proxy.as_ref().and_then(|p| p.data.load().ok());
|
||||
match proxy {
|
||||
Some(bytes) => {
|
||||
Ok(OutputBytes { bytes, warning: Some(format!("the linked file `{}` is unavailable ({e}); its preview is used", self.name)) })
|
||||
}
|
||||
Some(bytes) => Ok(OutputBytes {
|
||||
bytes,
|
||||
warning: Some(format!("the linked file `{}` is unavailable ({e}); its preview is used", self.name)),
|
||||
proxied: true,
|
||||
}),
|
||||
None => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ pub(super) fn print_booklet(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// HTML and reflowable EPUB embed the EPS preview; keep the session's source bytes intact.
|
||||
/// HTML and reflowable EPUB read embedded bytes only: every used linked asset gets its output
|
||||
/// bytes embedded, and EPS its preview. The session's own assets stay as they are.
|
||||
fn eps_previews(d: &designcraft_doc::Document) -> (Cow<'_, designcraft_doc::Document>, Vec<String>) {
|
||||
// Match the exporter's reading order (including Article exclusions), not every asset
|
||||
// in the document: unused or hidden graphics must not prevent an export.
|
||||
@@ -48,20 +49,40 @@ fn eps_previews(d: &designcraft_doc::Document) -> (Cow<'_, designcraft_doc::Docu
|
||||
let mut out = Cow::Borrowed(d);
|
||||
let mut warnings = Vec::new();
|
||||
for id in used {
|
||||
let Some((a, got)) = d.assets.get(&id).and_then(|a| Some((a, a.output_bytes().ok()?))) else { continue };
|
||||
let Some(a) = d.assets.get(&id) else { continue };
|
||||
let linked = a.data.is_linked();
|
||||
let got = match a.output_bytes() {
|
||||
Ok(got) => got,
|
||||
Err(e) => {
|
||||
if linked {
|
||||
warnings.push(format!("{}: the linked file is unavailable ({e}); graphic omitted", a.name));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let bytes = got.bytes;
|
||||
if !designcraft_images::is_eps(&bytes) {
|
||||
let eps = designcraft_images::is_eps(&bytes);
|
||||
// Embedded bytes the writers read as they are; a linked asset's bytes are read here
|
||||
// (the writers only see embedded data), and EPS is replaced by its preview.
|
||||
if !eps && !linked {
|
||||
continue;
|
||||
}
|
||||
warnings.extend(got.warning);
|
||||
let png = designcraft_images::eps_png(&bytes);
|
||||
if png.is_none() {
|
||||
warnings.push(format!("{}: unusable EPS preview; graphic omitted", a.name));
|
||||
}
|
||||
let data = if eps {
|
||||
let png = designcraft_images::eps_png(&bytes);
|
||||
if png.is_none() {
|
||||
warnings.push(format!("{}: unusable EPS preview; graphic omitted", a.name));
|
||||
}
|
||||
png.unwrap_or_default()
|
||||
} else {
|
||||
bytes.as_ref().clone()
|
||||
};
|
||||
if let Some(a) = out.to_mut().assets.get_mut(&id) {
|
||||
let a = Arc::make_mut(a);
|
||||
a.mime = "image/png".into();
|
||||
a.data = designcraft_doc::AssetData::embedded(png.unwrap_or_default());
|
||||
if eps {
|
||||
a.mime = "image/png".into();
|
||||
}
|
||||
a.data = designcraft_doc::AssetData::embedded(data);
|
||||
}
|
||||
}
|
||||
(out, warnings)
|
||||
|
||||
@@ -134,3 +134,37 @@ fn opening_a_document_with_a_linked_alpha_wrap_image_reads_no_original() {
|
||||
let o = d.alpha_outlines.first().expect("the outline is traced from the proxy");
|
||||
assert_eq!((o.width, o.height), (64, 64), "scaled to the original's size");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epub_and_html_carry_a_linked_graphic_and_a_modified_one_from_disk() {
|
||||
let dir = Dir::new("epub");
|
||||
let file = dir.0.join("art.png");
|
||||
let (mut s, _) = linked_session(&file, &png(40, 20, false));
|
||||
let html = s.execute("file.exportHtml", &json!({})).unwrap();
|
||||
assert!(html["text"].as_str().unwrap().contains("<img"), "the linked graphic is in the HTML");
|
||||
let r = s.execute("file.exportEpub", &json!({})).unwrap();
|
||||
let z = zip::ZipArchive::new(std::io::Cursor::new(crate::cmd::base64_decode(r["base64"].as_str().unwrap()))).unwrap();
|
||||
let names: Vec<String> = z.file_names().map(str::to_string).collect();
|
||||
let image = names.iter().find(|n| n.starts_with("OEBPS/images/")).unwrap_or_else(|| panic!("no image in {names:?}")).clone();
|
||||
// A modified link exports the file on disk, with a warning.
|
||||
std::fs::write(&file, png(77, 33, false)).unwrap();
|
||||
let r = s.execute("file.exportEpub", &json!({})).unwrap();
|
||||
assert!(warnings(&r).contains("file on disk"), "{}", warnings(&r));
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(crate::cmd::base64_decode(r["base64"].as_str().unwrap()))).unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
std::io::Read::read_to_end(&mut z.by_name(&image).unwrap(), &mut bytes).unwrap();
|
||||
assert_eq!(bytes, std::fs::read(&file).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_package_copies_a_modified_link_from_disk_with_a_warning() {
|
||||
let dir = Dir::new("package-modified");
|
||||
let file = dir.0.join("art.png");
|
||||
let (mut s, _) = linked_session(&file, &png(40, 20, false));
|
||||
let changed = png(77, 33, false);
|
||||
std::fs::write(&file, &changed).unwrap();
|
||||
let out = dir.0.join("out");
|
||||
let r = s.execute("file.package", &json!({"dir": out.to_string_lossy(), "pdf": false, "idml": false})).unwrap();
|
||||
assert_eq!(std::fs::read(out.join("Links/art.png")).unwrap(), changed);
|
||||
assert!(r.to_string().contains("file on disk"), "{r}");
|
||||
}
|
||||
@@ -230,7 +230,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Package…",
|
||||
["File"],
|
||||
None,
|
||||
"{dir: folder to create, idml?: true, pdf?: false|true|{…options of file.exportPdf}, instructions?: text} — the document (its placed files relinked to Links/), Links/, the font files that draw its text in Document Fonts/ (fallback fonts included; unless their licence restricts it), an IDML copy (linking the files in Links/, nothing embedded), an optional PDF and a report → {dir, files, report, warnings: PDF export warnings}",
|
||||
"{dir: folder to create, idml?: true, pdf?: false|true|{…options of file.exportPdf}, instructions?: text} — the document (its placed files relinked to Links/), Links/, the font files that draw its text in Document Fonts/ (fallback fonts included; unless their licence restricts it), an IDML copy (linking the files in Links/, nothing embedded), an optional PDF and a report → {dir, files, report, warnings: link and PDF export warnings}",
|
||||
has_doc,
|
||||
package
|
||||
),
|
||||
@@ -325,11 +325,18 @@ fn unique_name(base: String, taken: &mut Taken) -> String {
|
||||
/// `relative_to` names the folder `dir` is, beside the saved document: the links then read
|
||||
/// `Links/logo.png`, so a package holds no path from the machine that made it (#340). `None` links
|
||||
/// the copies by their full paths (Links › Copy Link(s) To, which relinks the open document).
|
||||
fn write_links(d: &Document, root: &OutputRoot, dir: &Path, only: Option<&[AssetId]>, relative_to: Option<&str>) -> Result<(Document, Vec<PathBuf>)> {
|
||||
fn write_links(
|
||||
d: &Document,
|
||||
root: &OutputRoot,
|
||||
dir: &Path,
|
||||
only: Option<&[AssetId]>,
|
||||
relative_to: Option<&str>,
|
||||
) -> Result<(Document, Vec<PathBuf>, Vec<String>)> {
|
||||
root.validate_directory(dir)?;
|
||||
let mut out = d.clone();
|
||||
let mut taken = Taken::default();
|
||||
let mut written = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
// Only files something places; the saved document leaves the others out.
|
||||
let ids = d.used_assets();
|
||||
for id in ids {
|
||||
@@ -340,9 +347,13 @@ fn write_links(d: &Document, root: &OutputRoot, dir: &Path, only: Option<&[Asset
|
||||
// A missing link has no bytes: an empty file under its name would read as present, so it
|
||||
// keeps its old (missing) path instead.
|
||||
// Bytes that can't be had are left like a missing link.
|
||||
let Some(bytes) = a.data.load().ok().filter(|b| !b.is_empty()) else {
|
||||
// A changed file is copied as it is on disk now, with a warning.
|
||||
let Some((bytes, modified)) = a.data.load_for_output().ok().filter(|(b, _)| !b.is_empty()) else {
|
||||
continue;
|
||||
};
|
||||
if modified {
|
||||
warnings.push(format!("the linked file `{}` has changed; the file on disk is copied", a.name));
|
||||
}
|
||||
let name = link_name(a.link.as_deref().unwrap_or(&a.name), id, &mut taken);
|
||||
let path = dir.join(&name);
|
||||
root.write(&path, bytes.as_slice())?;
|
||||
@@ -377,7 +388,7 @@ fn write_links(d: &Document, root: &OutputRoot, dir: &Path, only: Option<&[Asset
|
||||
out.assets.insert(id, Arc::new(na));
|
||||
written.push(path);
|
||||
}
|
||||
Ok((out, written))
|
||||
Ok((out, written, warnings))
|
||||
}
|
||||
|
||||
/// Copy the font file of `face` into `dir` (created when first needed) unless its licence
|
||||
@@ -476,7 +487,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let pre = s.execute("preflight.run", &json!({})).unwrap_or(Value::Null);
|
||||
let d = s.doc()?.doc.clone();
|
||||
let links_dir = root.child(LINKS_FOLDER)?;
|
||||
let (packed, mut files) = write_links(&d, &root, &links_dir, None, Some(LINKS_FOLDER))?;
|
||||
let (packed, mut files, mut warnings) = write_links(&d, &root, &links_dir, None, Some(LINKS_FOLDER))?;
|
||||
let doc_path = package_output_path(&dir, &title, "designcraft")?;
|
||||
root.write(&doc_path, &super::to_bytes(&packed)?)?;
|
||||
files.insert(0, doc_path);
|
||||
@@ -519,12 +530,11 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
idml_layer_note = "\nPlaced PDF layers\nThe IDML copy links the placed PDFs, so it shows their file defaults. The DesignCraft document keeps each placement's Object Layer Options choice.\n";
|
||||
}
|
||||
}
|
||||
let mut warnings = Vec::new();
|
||||
if let Some(params) = pdf {
|
||||
let path = package_output_path(&dir, &title, "pdf")?;
|
||||
let pdf = super::export::pdf_report(&packed, &s.cache, ¶ms)?;
|
||||
root.write(&path, &pdf.bytes)?;
|
||||
warnings = pdf.warnings;
|
||||
warnings.extend(pdf.warnings);
|
||||
files.push(path);
|
||||
}
|
||||
// The printing instructions first, as the form gave them; then the report: fonts (missing
|
||||
@@ -552,7 +562,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
if !warnings.is_empty() {
|
||||
r += "\nPDF export warnings\n";
|
||||
r += "\nWarnings\n";
|
||||
for warning in &warnings {
|
||||
r += &format!(" {warning}\n");
|
||||
}
|
||||
@@ -570,12 +580,12 @@ fn copy_links(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let only: Option<Vec<AssetId>> = p.get("assets").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(AssetId).collect());
|
||||
let d = s.doc()?.doc.clone();
|
||||
let root = OutputRoot::new(dir.clone())?;
|
||||
let (relinked, written) = write_links(&d, &root, &dir, only.as_deref(), None)?;
|
||||
let (relinked, written, warnings) = write_links(&d, &root, &dir, only.as_deref(), None)?;
|
||||
s.edit(|doc, _| {
|
||||
doc.assets = relinked.assets;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"copied": written.len()}))
|
||||
Ok(json!({"copied": written.len(), "warnings": warnings}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -283,9 +283,26 @@ impl<'a> Ex<'a> {
|
||||
|
||||
fn image_el(&mut self, asset: &designcraft_doc::Asset, g: &designcraft_doc::Graphic) -> El {
|
||||
let is_pdf = asset.mime == "application/pdf";
|
||||
// Bytes that can't be had are written like a link without data.
|
||||
let data = asset.data.load().unwrap_or_default();
|
||||
let is_eps = asset.mime == "application/postscript" || designcraft_images::is_eps(&data);
|
||||
// Bytes are read only when they are embedded (an image with no link of its own always
|
||||
// is); a link without embedding stays a link. Bytes that can't be had are written like
|
||||
// a link without data.
|
||||
let embedding = self.opts.embed_images || asset.link.is_none();
|
||||
let data = if embedding {
|
||||
match asset.output_bytes() {
|
||||
Ok(out) => {
|
||||
if let Some(w) = out.warning
|
||||
&& !self.warnings.contains(&w)
|
||||
{
|
||||
self.warnings.push(w);
|
||||
}
|
||||
out.bytes
|
||||
}
|
||||
Err(_) => Default::default(),
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let is_eps = asset.mime == "application/postscript" || (!data.is_empty() && designcraft_images::is_eps(&data));
|
||||
let mut el = El::new(if is_eps {
|
||||
"EPS"
|
||||
} else if is_pdf {
|
||||
@@ -310,7 +327,7 @@ impl<'a> Ex<'a> {
|
||||
el.set("FillColor", id.clone());
|
||||
el.set("FillTint", pct(g.fill.tint as f64));
|
||||
}
|
||||
let wanted = !data.is_empty() && (self.opts.embed_images || asset.link.is_none());
|
||||
let wanted = !data.is_empty() && embedding;
|
||||
// An image with no link of its own has nowhere else to live, so it is always embedded
|
||||
// (dropping its data would lose it); only an image with a real link can fall back to it.
|
||||
let embed = wanted && (asset.link.is_none() || self.embedded.saturating_add(data.len()) <= self.embed_limit);
|
||||
|
||||
@@ -57,6 +57,10 @@ impl std::hash::Hasher for ConstantImageHasher {
|
||||
fn write(&mut self, _: &[u8]) {}
|
||||
}
|
||||
|
||||
/// An asset's bytes for this export, its name, and whether they are its preview standing in
|
||||
/// for an unavailable linked file.
|
||||
pub(crate) type Output = (Arc<Vec<u8>>, String, bool);
|
||||
|
||||
pub(crate) struct Exporter<'a> {
|
||||
pub doc: &'a Document,
|
||||
/// The document's inks with its Ink Manager settings.
|
||||
@@ -71,6 +75,8 @@ pub(crate) struct Exporter<'a> {
|
||||
pub warnings: Vec<String>,
|
||||
drawing_items: Vec<designcraft_doc::ItemId>,
|
||||
images: HashMap<AssetId, Option<Image>>,
|
||||
/// Each asset's output bytes, read once per export (`None`: nothing to export, warned once).
|
||||
outputs: HashMap<AssetId, Option<Output>>,
|
||||
// Immutable export options (including rgb_only) make bytes the complete conversion key.
|
||||
// PDF-page/SVG/media routing precedes this path. Failures remain per AssetId.
|
||||
images_by_content: ImageContentCache,
|
||||
|
||||
@@ -103,6 +103,7 @@ fn export_booklet_inner(doc: &Document, cache: &Cache, opts: &BookletOptions) ->
|
||||
clip: Rect::ZERO,
|
||||
warnings,
|
||||
images: HashMap::new(),
|
||||
outputs: HashMap::new(),
|
||||
images_by_content: ImageContentCache::default(),
|
||||
#[cfg(test)]
|
||||
image_conversions: 0,
|
||||
|
||||
@@ -60,24 +60,34 @@ impl Exporter<'_> {
|
||||
)))
|
||||
}
|
||||
|
||||
/// The bytes of asset `id` for this export ([`designcraft_doc::Asset::output_bytes`]), with
|
||||
/// its warning recorded. `None` when there is nothing to export (warned once, by name).
|
||||
pub(super) fn asset_bytes(&mut self, id: AssetId) -> Option<(Arc<Vec<u8>>, String)> {
|
||||
/// The output of asset `id` ([`designcraft_doc::Asset::output_bytes`]), read once per
|
||||
/// export with its warning recorded. `None` when there is nothing to export (warned once, by
|
||||
/// name).
|
||||
pub(super) fn asset_output(&mut self, id: AssetId) -> Option<Output> {
|
||||
if let Some(o) = self.outputs.get(&id) {
|
||||
return o.clone();
|
||||
}
|
||||
let asset = self.doc.assets.get(&id)?;
|
||||
match asset.output_bytes() {
|
||||
let name = asset.name.clone();
|
||||
let out = match asset.output_bytes() {
|
||||
Ok(out) => {
|
||||
let name = asset.name.clone();
|
||||
if let Some(w) = out.warning {
|
||||
self.warn(w);
|
||||
}
|
||||
Some((out.bytes, name))
|
||||
Some((out.bytes, name, out.proxied))
|
||||
}
|
||||
Err(e) => {
|
||||
let w = format!("image `{}` was skipped: {e}", asset.name);
|
||||
self.warn(w);
|
||||
self.warn(format!("image `{name}` was skipped: {e}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
self.outputs.insert(id, out.clone());
|
||||
out
|
||||
}
|
||||
|
||||
/// The bytes of asset `id` for this export, and its name.
|
||||
pub(super) fn asset_bytes(&mut self, id: AssetId) -> Option<(Arc<Vec<u8>>, String)> {
|
||||
self.asset_output(id).map(|(b, n, _)| (b, n))
|
||||
}
|
||||
|
||||
pub(super) fn load_image(&mut self, id: AssetId) -> Option<Image> {
|
||||
@@ -215,7 +225,19 @@ impl Exporter<'_> {
|
||||
fn graphic_placed_pdf(&mut self, s: &mut Surface, g: &designcraft_doc::Graphic, hidden: Option<&[String]>) -> bool {
|
||||
let doc = self.doc;
|
||||
let Some(asset) = doc.assets.get(&g.asset) else { return false };
|
||||
let Some((bytes, _)) = self.asset_bytes(g.asset).filter(|(b, _)| b.starts_with(b"%PDF")) else { return false };
|
||||
if asset.mime != "application/pdf" {
|
||||
return false;
|
||||
}
|
||||
let Some((bytes, _, proxied)) = self.asset_output(g.asset) else { return false };
|
||||
if proxied {
|
||||
// The preview shows every layer: with layers hidden or overridden it would show what
|
||||
// the document hides, so nothing is drawn (the canvas fails closed too).
|
||||
if hidden.is_some() {
|
||||
self.warn(format!("{}: the linked PDF is unavailable, so its layer choice can't be applied; the graphic was left out", asset.name));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Cropped to a box: the whole page, placed so that the box spans graphic space.
|
||||
let shown = asset.shown_box();
|
||||
let [l, t, r, b] = shown.unwrap_or([0.0, 0.0, 1.0, 1.0]);
|
||||
@@ -286,7 +308,10 @@ impl Exporter<'_> {
|
||||
fn graphic_placed_svg(&mut self, s: &mut Surface, g: &designcraft_doc::Graphic) -> bool {
|
||||
let doc = self.doc;
|
||||
let Some(asset) = doc.assets.get(&g.asset) else { return false };
|
||||
let Some((bytes, _)) = self.asset_bytes(g.asset).filter(|(b, _)| designcraft_images::is_svg(b)) else { return false };
|
||||
if asset.mime != "image/svg+xml" {
|
||||
return false;
|
||||
}
|
||||
let Some((bytes, _, false)) = self.asset_output(g.asset) else { return false };
|
||||
let Some(size) = Size::from_wh(g.size.0.max(1e-3) as f32, g.size.1.max(1e-3) as f32) else { return true };
|
||||
let tree = self
|
||||
.svgs
|
||||
|
||||
@@ -227,6 +227,7 @@ mod image_content_cache_tests {
|
||||
clip: Rect::ZERO,
|
||||
warnings: Vec::new(),
|
||||
images: HashMap::new(),
|
||||
outputs: HashMap::new(),
|
||||
images_by_content: ImageContentCache::default(),
|
||||
image_conversions: 0,
|
||||
pdfs: HashMap::new(),
|
||||
|
||||
@@ -212,6 +212,7 @@ fn export_pdf_with_report_inner(doc: &Document, cache: &Cache, opts: &PdfOptions
|
||||
clip: Rect::ZERO,
|
||||
warnings,
|
||||
images: HashMap::new(),
|
||||
outputs: HashMap::new(),
|
||||
images_by_content: ImageContentCache::default(),
|
||||
#[cfg(test)]
|
||||
image_conversions: 0,
|
||||
|
||||
@@ -560,8 +560,17 @@ impl<'a> Exporter<'a> {
|
||||
|
||||
fn convert(&mut self, asset: AssetId) -> Option<(String, Option<String>)> {
|
||||
let a = self.doc.assets.get(&asset)?.clone();
|
||||
// Bytes that can't be had are left out like a missing file.
|
||||
let bytes = a.data.load().unwrap_or_default();
|
||||
// A changed file is used as it is on disk, a missing one by its preview (both warned);
|
||||
// with neither the image is left out like a missing file.
|
||||
let bytes = match a.output_bytes() {
|
||||
Ok(out) => {
|
||||
if let Some(w) = out.warning {
|
||||
self.warn(w);
|
||||
}
|
||||
out.bytes
|
||||
}
|
||||
Err(_) => Default::default(),
|
||||
};
|
||||
let data: &[u8] = &bytes;
|
||||
if data.is_empty() {
|
||||
self.warn(format!("`{}` is missing (linked file not found); left out", a.name));
|
||||
|
||||
@@ -70,7 +70,7 @@ The status line reports once: "N images will be saved as links to their files (L
|
||||
## Packages, export and preflight
|
||||
|
||||
- **Package** and **Copy Link(s) To** copy the original files disk to disk into `Links/` and relink to the copies, hash-checked. A linked asset stays linked, now to its copy. A missing or unreadable file is left as a missing link; no empty file is written in its place.
|
||||
- **Export** (PDF, flattened output, EPS and the other formats, alpha wraps) uses the file on disk. When the link is Modified, the export uses the current file and warns. When the file is Missing, the image is skipped and the export warns by name ("image `x` was skipped: …").
|
||||
- **Export** (PDF, flattened output, EPS and the other formats, alpha wraps) uses the file on disk. When the link is Modified, the export uses the current file and warns. When the file is Missing, the export uses the proxy (a preview of at most 1024 px) and warns by name; with no proxy the image is skipped and the export warns ("image `x` was skipped: …"). A placed PDF whose layers are hidden or overridden is left out instead of drawn from the proxy, which shows every layer. HTML, EPUB, PowerPoint and IDML read linked files the same way; Package and Copy Link(s) To warn when they copy a Modified file.
|
||||
- **Preflight** reports `missingLink` (error) for Missing links, with the reason when the path is refused, and `modifiedLink` for Modified ones ("exports use the file as it is now").
|
||||
|
||||
## Web builds
|
||||
|
||||
Reference in new issue
Block a user