E: transparency blend space as the PDF page group
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
23098f0bd3
commit
cf5188a272
|
|
@ -49,7 +49,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Endnotes (`endnote.insert/edit/delete/list/options`, Type ▸ Insert Endnote): references number through the document, the endnote frame on a new last page lists them under a heading; IDML keeps the listed text only.
|
||||
- Editorial notes (`note.new/edit/delete/convertToText/list`, Notes panel): anchored in text, flagged on screen, never printed or exported.
|
||||
- File ▸ Print Booklet (`file.printBooklet`): saddle-stitch or 2-up consecutive printer spreads as PDF, padded with blanks, with a gap between pages.
|
||||
- Edit ▸ Transparency Blend Space (`edit.transparencyBlendSpace`; RGB for web/mobile documents; IDML TransparencyPreference) — stored; compositing is still RGB on screen and in PDF.
|
||||
- Edit ▸ Transparency Blend Space (`edit.transparencyBlendSpace`; RGB for web/mobile documents; IDML TransparencyPreference) — exported PDFs give pages with transparency a DeviceCMYK or DeviceRGB page group, so viewers and RIPs blend in that space; on-screen compositing is still RGB.
|
||||
- High Contrast interface theme (fifth Interface Color Theme: black chrome, white text, yellow selection).
|
||||
- Place Excel workbooks (.xlsx): the first worksheet's used range becomes a table (shared/inline strings, numbers, booleans).
|
||||
- Object Library (`library.new/open/add/place/remove/list/json/close`, Library panel): `.dclib` files of snippets that keep stories, styles, swatches and images.
|
||||
|
|
|
|||
|
|
@ -247,11 +247,18 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let doc: &designcraft_doc::Document = flattened.as_ref().map_or(&st.doc, |f| &f.0);
|
||||
let r = designcraft_pdf::export_pdf_with_report(doc, &s.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);
|
||||
let blended: Vec<bool> = spreads.iter().map(|si| doc.spreads.get(*si).is_some_and(|sp| sp.items.iter().any(|it| transparent(it)))).collect();
|
||||
if blended.iter().any(|b| *b) {
|
||||
match designcraft_pdf::add_blend_space(&r.bytes, &blended, doc.settings.blend_space == designcraft_doc::BlendSpace::Cmyk) {
|
||||
Some(b) => r.bytes = b,
|
||||
None => r.warnings.push("the transparency blend space couldn't be set".into()),
|
||||
}
|
||||
}
|
||||
// Page transitions (each spread's first page holds them).
|
||||
let trans: Vec<Option<designcraft_doc::PageTransition>> = designcraft_pdf::sheet_spreads(doc, &opts)
|
||||
.into_iter()
|
||||
.map(|si| doc.spreads.get(si).and_then(|sp| sp.pages.first()).and_then(|pg| pg.transition))
|
||||
.collect();
|
||||
let trans: Vec<Option<designcraft_doc::PageTransition>> =
|
||||
spreads.into_iter().map(|si| doc.spreads.get(si).and_then(|sp| sp.pages.first()).and_then(|pg| pg.transition)).collect();
|
||||
if trans.iter().any(Option::is_some) {
|
||||
match designcraft_pdf::add_transitions(&r.bytes, &trans) {
|
||||
Some(b) => r.bytes = b,
|
||||
|
|
@ -305,6 +312,10 @@ mod text_tests {
|
|||
// The document itself is untouched.
|
||||
assert!(s.doc().unwrap().doc.item(designcraft_doc::ItemId(id.as_u64().unwrap())).is_some());
|
||||
assert!(s.execute("file.exportPdf", &json!({"flatten": 5})).is_err());
|
||||
// Unflattened, the page blends in the document's blend space (CMYK for print).
|
||||
let r = s.execute("file.exportPdf", &json!({})).unwrap();
|
||||
let text = String::from_utf8_lossy(&super::super::file::base64_decode(r["base64"].as_str().unwrap())).to_string();
|
||||
assert_eq!(text.matches("/S/Transparency/CS/DeviceCMYK").count(), 1, "only the page with transparency");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -99,6 +99,17 @@ mod tests {
|
|||
let r = s.execute("file.exportPdf", &json!({"spreads": true})).unwrap();
|
||||
let bytes = super::super::file::base64_decode(r["base64"].as_str().unwrap());
|
||||
assert_eq!(String::from_utf8_lossy(&bytes).matches("/Trans<<").count(), 1);
|
||||
// With transparency too, the page keeps both its group and its transition.
|
||||
let id = s.execute("frame.create", &json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].clone();
|
||||
s.execute("object.opacity", &json!({"ids": [id], "opacity": 0.5})).unwrap();
|
||||
let r = s.execute("file.exportPdf", &json!({"pages": [1]})).unwrap();
|
||||
let bytes = super::super::file::base64_decode(r["base64"].as_str().unwrap());
|
||||
assert_eq!(designcraft_render::pdf_page_count(&bytes), Some(1));
|
||||
s.execute("page.transition", &json!({"pages": [1], "kind": "fade"})).unwrap();
|
||||
let r = s.execute("file.exportPdf", &json!({"pages": [1]})).unwrap();
|
||||
let text = String::from_utf8_lossy(&super::super::file::base64_decode(r["base64"].as_str().unwrap())).to_string();
|
||||
let last = &text[text.rfind("/Type/Page/").unwrap()..];
|
||||
assert!(last.contains("/S/Fade") && last[..last.find("endobj").unwrap()].contains("/S/Transparency"), "{last}");
|
||||
s.execute("page.transition", &json!({"all": true, "kind": "none"})).unwrap();
|
||||
assert!(s.execute("page.transitions", &json!({})).unwrap().as_array().unwrap().is_empty());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ mod pdfx;
|
|||
mod transitions;
|
||||
pub use export::{BookletKind, BookletOptions, booklet_pairs, export_booklet, export_pdf, export_pdf_with_report, merge_pdfs, sheet_spreads};
|
||||
pub use pdfx::{check_pdfx4, make_pdfx4};
|
||||
pub use transitions::add_transitions;
|
||||
pub use transitions::{add_blend_space, add_transitions};
|
||||
|
||||
/// PDF standard to target.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ pub(crate) fn int_after(s: &str, key: &str) -> Option<usize> {
|
|||
|
||||
/// Byte range of object `id`'s body (between `obj` and `endobj`).
|
||||
pub(crate) fn object(pdf: &[u8], id: usize) -> Option<std::ops::Range<usize>> {
|
||||
// The latest copy (incremental updates re-state objects further on).
|
||||
let head = format!("\n{id} 0 obj");
|
||||
let start = find(pdf, head.as_bytes(), 0)? + head.len();
|
||||
let start = rfind(pdf, head.as_bytes())? + head.len();
|
||||
let end = find(pdf, b"endobj", start)?;
|
||||
Some(start..end)
|
||||
}
|
||||
|
|
@ -53,7 +54,19 @@ pub(crate) fn object(pdf: &[u8], id: usize) -> Option<std::ops::Range<usize>> {
|
|||
/// Add transitions to the pages of `pdf` (index = PDF page). `None` when the file's structure
|
||||
/// isn't the simple kind this understands (classic xref table, a flat page tree).
|
||||
pub fn add_transitions(pdf: &[u8], trans: &[Option<PageTransition>]) -> Option<Vec<u8>> {
|
||||
if trans.iter().all(Option::is_none) {
|
||||
add_page_entries(pdf, &trans.iter().map(|t| t.as_ref().map(trans_dict)).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Transparency blend space: pages with transparency get a page group in DeviceCMYK or DeviceRGB
|
||||
/// (`pages[i]` true = PDF page i).
|
||||
pub fn add_blend_space(pdf: &[u8], pages: &[bool], cmyk: bool) -> Option<Vec<u8>> {
|
||||
let cs = if cmyk { "DeviceCMYK" } else { "DeviceRGB" };
|
||||
add_page_entries(pdf, &pages.iter().map(|on| on.then(|| format!("/Group<</Type/Group/S/Transparency/CS/{cs}>>"))).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Append dictionary entries to pages (index = PDF page) as an incremental update.
|
||||
pub fn add_page_entries(pdf: &[u8], extra: &[Option<String>]) -> Option<Vec<u8>> {
|
||||
if extra.iter().all(Option::is_none) {
|
||||
return Some(pdf.to_vec());
|
||||
}
|
||||
let t_at = rfind(pdf, b"trailer")?;
|
||||
|
|
@ -76,14 +89,14 @@ pub fn add_transitions(pdf: &[u8], trans: &[Option<PageTransition>]) -> Option<V
|
|||
out.push(b'\n');
|
||||
}
|
||||
let mut entries: Vec<(usize, usize)> = Vec::new();
|
||||
for (i, t) in trans.iter().enumerate() {
|
||||
for (i, t) in extra.iter().enumerate() {
|
||||
let (Some(t), Some(&kid)) = (t, kids.get(i)) else { continue };
|
||||
let body = String::from_utf8_lossy(&pdf[object(pdf, kid)?]).to_string();
|
||||
if !body.contains("/Type/Page") && !body.contains("/Type /Page") {
|
||||
return None;
|
||||
}
|
||||
let close = body.rfind(">>")?;
|
||||
let new_body = format!("{}{}{}", &body[..close], trans_dict(t), &body[close..]);
|
||||
let new_body = format!("{}{}{}", &body[..close], t, &body[close..]);
|
||||
entries.push((kid, out.len()));
|
||||
out.extend_from_slice(format!("{kid} 0 obj{new_body}endobj\n").as_bytes());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue