mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 14:16:49 -06:00
Package the PDF as file.exportPdf makes it; refuse a bad pdf
file.package now writes its PDF through export's `pdf_report`, so a preset's transparency flattening, the effects and hidden-layer rasterising, the blend space, the view options and the transitions all reach the packaged PDF. A `pdf` that is neither a switch nor export options is refused, as is a bad flattener, before anything is written. Georgian: the printing term for spot inks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2c43330c43
commit
63d3f5a19d
@@ -710,9 +710,20 @@ fn first_page_ref(pdf: &[u8]) -> Option<usize> {
|
||||
|
||||
fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let opts = options(p, st.doc.page_count())?;
|
||||
// Transparency Flattener presets: High / Medium / Low Resolution, or a ppi.
|
||||
let ppi = match p.get("flatten") {
|
||||
let r = pdf_report(&st.doc, &s.cache, p)?;
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
write_file(path, &r.bytes)?;
|
||||
Ok(json!({"path": path, "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings}))
|
||||
}
|
||||
None => Ok(json!({"base64": super::base64_encode(&r.bytes), "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings})),
|
||||
}
|
||||
}
|
||||
|
||||
/// The Transparency Flattener's resolution `p.flatten` asks for: High, Medium or Low Resolution,
|
||||
/// or a ppi; `None` when the PDF isn't flattened.
|
||||
pub(super) fn flatten_ppi(p: &Value) -> Result<Option<f64>> {
|
||||
Ok(match p.get("flatten") {
|
||||
None | Some(Value::Null) | Some(Value::Bool(false)) => None,
|
||||
Some(Value::String(s)) if s == "high" => Some(300.0),
|
||||
Some(Value::String(s)) if s == "medium" => Some(150.0),
|
||||
@@ -721,17 +732,24 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(v) => Some(
|
||||
v.as_f64().filter(|v| (36.0..=1200.0).contains(v)).ok_or_else(|| bad("file.exportPdf", "`flatten`: high|medium|low or 36–1200 ppi"))?,
|
||||
),
|
||||
};
|
||||
let flattened = ppi.map(|ppi| flatten(&st.doc, &s.cache, ppi));
|
||||
})
|
||||
}
|
||||
|
||||
/// The PDF of `doc` as `file.exportPdf` makes it from options `p`: flattened when asked, placed
|
||||
/// PDFs with hidden layers and soft effects as images, the blend space, the view options and the
|
||||
/// page transitions. `file.package` writes its PDF with it too.
|
||||
pub(super) fn pdf_report(doc: &designcraft_doc::Document, cache: &designcraft_compose::Cache, p: &Value) -> Result<designcraft_pdf::ExportReport> {
|
||||
let opts = options(p, doc.page_count())?;
|
||||
let flattened = flatten_ppi(p)?.map(|ppi| flatten(doc, cache, ppi));
|
||||
// Placed PDFs with hidden layers go out as images showing just their visible layers.
|
||||
let base: &designcraft_doc::Document = flattened.as_ref().map_or(&st.doc, |f| &f.0);
|
||||
let base: &designcraft_doc::Document = flattened.as_ref().map_or(doc, |f| &f.0);
|
||||
let (layered, layer_count) = rasterize_layered(base)?;
|
||||
let base2: &designcraft_doc::Document = layered.as_ref().unwrap_or(base);
|
||||
let (fx_doc, fx_count) = rasterize_effects(base2, &s.cache);
|
||||
let (fx_doc, fx_count) = rasterize_effects(base2, cache);
|
||||
let doc: &designcraft_doc::Document = fx_doc.as_ref().unwrap_or(base2);
|
||||
let boxed = super::interchange::with_pdf_boxes(doc);
|
||||
let doc: &designcraft_doc::Document = &boxed;
|
||||
let r = designcraft_pdf::export_pdf_with_report(doc, &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let r = designcraft_pdf::export_pdf_with_report(doc, cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let mut r = r;
|
||||
// Transparency Blend Space: the page group of pages with transparency.
|
||||
let spreads = designcraft_pdf::sheet_spreads(doc, &opts);
|
||||
@@ -805,13 +823,7 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
{
|
||||
r.warnings.push(format!("transparency flattened: {n} region(s) rasterised"));
|
||||
}
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
write_file(path, &r.bytes)?;
|
||||
Ok(json!({"path": path, "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings}))
|
||||
}
|
||||
None => Ok(json!({"base64": super::base64_encode(&r.bytes), "bytes": r.bytes.len(), "pages": r.pages, "warnings": r.warnings})),
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -389,12 +389,17 @@ fn drawn_faces(cs: &ComposedStory, depth: usize, out: &mut Vec<FaceRef>) {
|
||||
fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = PathBuf::from(str_param(p, "dir").ok_or_else(|| bad("file.package", "missing `dir`"))?);
|
||||
let st = s.doc()?;
|
||||
// The PDF: with the default settings, or with the options of a PDF preset (as
|
||||
// file.exportPdf's). Read first, so that bad options write nothing.
|
||||
// The PDF: with the default settings, or with the options of a PDF preset, made as
|
||||
// file.exportPdf makes it. Read first, so that bad options write nothing.
|
||||
let pdf = match p.get("pdf") {
|
||||
Some(o @ Value::Object(_)) => Some(super::export::options(o, st.doc.page_count())?),
|
||||
Some(Value::Bool(true)) => Some(Default::default()),
|
||||
_ => None,
|
||||
None | Some(Value::Null) | Some(Value::Bool(false)) => None,
|
||||
Some(Value::Bool(true)) => Some(json!({})),
|
||||
Some(o @ Value::Object(_)) => {
|
||||
super::export::options(o, st.doc.page_count())?;
|
||||
super::export::flatten_ppi(o)?;
|
||||
Some(o.clone())
|
||||
}
|
||||
Some(v) => return Err(bad("file.package", format!("`pdf` is true, false or PDF export options, not {v}"))),
|
||||
};
|
||||
let root = OutputRoot::new(dir.clone())?;
|
||||
let display_title = st.doc.title.clone();
|
||||
@@ -440,11 +445,9 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
root.write(&path, &designcraft_idml::export_idml_with(&packed, &opts))?;
|
||||
files.push(path);
|
||||
}
|
||||
if let Some(opts) = pdf {
|
||||
if let Some(params) = pdf {
|
||||
let path = package_output_path(&dir, &title, "pdf")?;
|
||||
let bytes = designcraft_pdf::export_pdf(&super::interchange::with_pdf_boxes(&packed), &s.cache, &opts)
|
||||
.map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
root.write(&path, &bytes)?;
|
||||
root.write(&path, &super::export::pdf_report(&packed, &s.cache, ¶ms)?.bytes)?;
|
||||
files.push(path);
|
||||
}
|
||||
// The report: fonts (missing first), links, preflight, the user's instructions.
|
||||
@@ -531,8 +534,33 @@ mod tests {
|
||||
let plain = dir.join("plain");
|
||||
s.execute("file.package", &json!({"dir": plain.to_string_lossy(), "idml": false, "pdf": true})).unwrap();
|
||||
assert!(!has(&plain.join("Options.pdf"), b"GTS_PDFX"));
|
||||
assert!(s.execute("file.package", &json!({"dir": dir.join("bad").to_string_lossy(), "pdf": {"standard": "nope"}})).is_err());
|
||||
assert!(!dir.join("bad").exists(), "bad options write nothing");
|
||||
// Refused before anything is written: bad options, a bad flattener and a `pdf` that is
|
||||
// neither a switch nor options.
|
||||
for bad in [json!({"standard": "nope"}), json!({"flatten": "extreme"}), json!("print"), json!(3)] {
|
||||
let out = dir.join("bad");
|
||||
assert!(s.execute("file.package", &json!({"dir": out.to_string_lossy(), "pdf": bad})).is_err(), "{bad}");
|
||||
assert!(!out.exists(), "{bad}: bad options write nothing");
|
||||
}
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// A preset that flattens transparency (Screen and Email's `flatten: low`) gives the packaged
|
||||
/// PDF the flattening `file.exportPdf` does.
|
||||
#[test]
|
||||
fn the_packaged_pdf_is_flattened_as_export_flattens_it() {
|
||||
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("dc-package-pdf-flatten-{}-{nonce}", std::process::id()));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"title": "Flat", "width": 144, "height": 144})).unwrap();
|
||||
let id = s.execute("frame.create", &json!({"rect": [10, 10, 100, 100], "content": "unassigned"})).unwrap()["id"].clone();
|
||||
s.execute("object.fill", &json!({"ids": [id], "swatch": "[Black]"})).unwrap();
|
||||
s.execute("object.opacity", &json!({"ids": [id], "opacity": 0.5})).unwrap();
|
||||
let exported = super::super::base64_decode(s.execute("file.exportPdf", &json!({"flatten": "low"})).unwrap()["base64"].as_str().unwrap());
|
||||
s.execute("file.package", &json!({"dir": dir.to_string_lossy(), "idml": false, "pdf": {"flatten": "low"}})).unwrap();
|
||||
let packaged = std::fs::read(dir.join("Flat.pdf")).unwrap();
|
||||
let images = |pdf: &[u8]| pdf.windows(b"/Subtype/Image".len()).filter(|w| *w == b"/Subtype/Image").count();
|
||||
assert!(images(&exported) > 0, "the export rasterises the transparent object");
|
||||
assert_eq!(images(&packaged), images(&exported), "so does the package");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Create Printing Instructions", "ბეჭდვის ინსტრუქციების შექმნა"),
|
||||
("Entire Publication", "მთელი პუბლიკაცია"),
|
||||
("Scope:", "არეალი:"),
|
||||
("Spot", "ლაქა"),
|
||||
("Spot", "სპოტ-საღებავი"),
|
||||
("Process", "პროცესი"),
|
||||
("Source", "წყარო"),
|
||||
("Installed", "დაყენებული"),
|
||||
@@ -49,7 +49,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Substituted", "ჩანაცვლებული"),
|
||||
("Modified", "შეცვლილი"),
|
||||
("Missing", "აკლია"),
|
||||
("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "ფერები და მელნები: {process} პროცესის; {spot} ლაქის"),
|
||||
("Colors and Inks: {process} Process Inks; {spot} Spot Inks", "ფერები და მელნები: {process} პროცესის მელანი; {spot} სპოტ-საღებავი"),
|
||||
("Images: {embedded} Embedded", "გამოსახულებები: ჩაშენებულია {embedded}"),
|
||||
(
|
||||
"Links and Images: {found} Links Found; {modified} Modified, {missing} Missing",
|
||||
|
||||
Reference in New Issue
Block a user