mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
E: region-based transparency flattener (the rest stays vector)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b655a5f75c
commit
8ac240889f
+1
-1
@@ -92,7 +92,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Live Distribute (Space while dragging a selection handle; `transform.resize {distribute}`): objects keep their size and spread with the bounds.
|
||||
- Liquid Layout (`liquid.pageRule/object`, `guide.liquid`, `layout.createAlternate`, Liquid Layout panel): scale / re-center / guide-based / object-based page rules applied on page resize and Document Setup; dashed liquid guides; alternate layouts as resized page copies in a named section. Alternate layouts aren't linked to their source.
|
||||
- Split Window / New Window (`window.split`, `window.newWindow`, Window ▸ Arrange): two side-by-side views of the document with their own zoom, scroll and render cache. New Window opens that second view rather than a separate OS window.
|
||||
- Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): spreads with transparency are rasterised per page in the exported PDF. It doesn't yet keep text and vectors outside transparent areas.
|
||||
- Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): each object involving transparency becomes an opaque image of its area over what's beneath it, so everything else stays vector (spreads with transparent parent items are rasterised whole).
|
||||
- Video and sound (place .mp4/.mov/.webm/.mp3/.wav…, `media.options/get`, Media panel): media frames show a poster or placeholder on screen, in print and in PDF, and export as HTML5 `<video>`/`<audio>` in EPUB. No in-app playback or PDF rich media.
|
||||
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Kashida justification, caret movement in visual order and digit substitution aren't there yet.
|
||||
- Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish and Japanese menu titles, common menu items and panel names (our own translations); dialogs and the rest of the UI stay English.
|
||||
|
||||
@@ -166,8 +166,10 @@ fn transparent(it: &designcraft_doc::Item) -> bool {
|
||||
it.opacity < 1.0 || it.blend != Default::default() || it.effects.any() || it.knockout || it.children().iter().any(|c| transparent(c))
|
||||
}
|
||||
|
||||
/// Transparency Flattener: spreads with transparency become one raster per page at `ppi`
|
||||
/// (in a copy of the document made for export). Returns the copy and the flattened page count.
|
||||
/// Transparency Flattener: each object involving transparency becomes an opaque image of its
|
||||
/// area as it looks over what's beneath it (so the rest stays vector); spreads whose parent items
|
||||
/// involve transparency are rasterised whole. Works on a copy of the document made for export.
|
||||
/// Returns the copy and the number of rasterised regions.
|
||||
fn flatten(d: &designcraft_doc::Document, cache: &designcraft_compose::Cache, ppi: f64) -> (designcraft_doc::Document, usize) {
|
||||
use designcraft_doc::{Asset, AssetId, Content, Graphic, Item, ItemId, Shape, SpreadRef};
|
||||
let mut out = d.clone();
|
||||
@@ -178,24 +180,9 @@ fn flatten(d: &designcraft_doc::Document, cache: &designcraft_compose::Cache, pp
|
||||
let parent_transparent = |pid: Option<designcraft_doc::SpreadId>| {
|
||||
pid.and_then(|id| d.parents.iter().find(|p| p.id == id)).is_some_and(|p| p.items.iter().any(|it| transparent(it)))
|
||||
};
|
||||
for (si, sp) in d.spreads.iter().enumerate() {
|
||||
if !sp.items.iter().any(|it| transparent(it)) && !sp.pages.iter().any(|pg| pg.show_parent_items && parent_transparent(pg.parent)) {
|
||||
continue;
|
||||
}
|
||||
let first = d.first_page_of_spread(si);
|
||||
let mut images = Vec::new();
|
||||
for (pi, pg) in sp.pages.iter().enumerate() {
|
||||
if let Some(img) = rr.render_page(d, cache, first + pi, k, false, &Default::default()) {
|
||||
images.push((pg.bounds(), img));
|
||||
}
|
||||
}
|
||||
let lid = out.default_layer();
|
||||
let osp = std::sync::Arc::make_mut(&mut out.spreads[si]);
|
||||
osp.items.clear();
|
||||
for pg in &mut osp.pages {
|
||||
pg.show_parent_items = false;
|
||||
}
|
||||
for (b, img) in images {
|
||||
// An opaque image item showing `img` over `r`.
|
||||
let image_item =
|
||||
|out: &mut designcraft_doc::Document, r: designcraft_geom::Rect, img: designcraft_render::Rendered, layer: designcraft_doc::LayerId| {
|
||||
let (w, h) = (img.width, img.height);
|
||||
let aid = AssetId(out.alloc());
|
||||
out.assets.insert(
|
||||
@@ -210,19 +197,80 @@ fn flatten(d: &designcraft_doc::Document, cache: &designcraft_compose::Cache, pp
|
||||
pixels: Some((w, h)),
|
||||
}),
|
||||
);
|
||||
let id = ItemId(out.alloc());
|
||||
let mut it = Item::new(id, lid, Shape::Rectangle, designcraft_geom::shapes::rectangle(b));
|
||||
let mut it = Item::new(ItemId(out.alloc()), layer, Shape::Rectangle, designcraft_geom::shapes::rectangle(r));
|
||||
it.content = Content::Graphic(Graphic {
|
||||
asset: aid,
|
||||
size: (w as f64, h as f64),
|
||||
xf: designcraft_geom::Affine::translate((b.x0, b.y0))
|
||||
* designcraft_geom::Affine::scale_non_uniform(b.width() / w as f64, b.height() / h as f64),
|
||||
xf: designcraft_geom::Affine::translate((r.x0, r.y0))
|
||||
* designcraft_geom::Affine::scale_non_uniform(r.width() / w as f64, r.height() / h as f64),
|
||||
auto_fit: Default::default(),
|
||||
fit_align: 4,
|
||||
crop: [0.0; 4],
|
||||
});
|
||||
it.stroke.weight = 0.0;
|
||||
let _ = out.insert_item(SpreadRef::Doc(si), it, None);
|
||||
it
|
||||
};
|
||||
for (si, sp) in d.spreads.iter().enumerate() {
|
||||
let whole = sp.pages.iter().any(|pg| pg.show_parent_items && parent_transparent(pg.parent));
|
||||
if whole {
|
||||
// Parent items involve transparency: every page of the spread as one image.
|
||||
let first = d.first_page_of_spread(si);
|
||||
let lid = out.default_layer();
|
||||
let mut images = Vec::new();
|
||||
for (pi, pg) in sp.pages.iter().enumerate() {
|
||||
if let Some(img) = rr.render_page(d, cache, first + pi, k, false, &Default::default()) {
|
||||
images.push((pg.bounds(), img));
|
||||
}
|
||||
}
|
||||
let osp = std::sync::Arc::make_mut(&mut out.spreads[si]);
|
||||
osp.items.clear();
|
||||
for pg in &mut osp.pages {
|
||||
pg.show_parent_items = false;
|
||||
}
|
||||
for (b, img) in images {
|
||||
let it = image_item(&mut out, b, img, lid);
|
||||
let _ = out.insert_item(SpreadRef::Doc(si), it, None);
|
||||
n += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Region by region, bottom to top.
|
||||
for (idx, it) in sp.items.iter().enumerate() {
|
||||
if it.hidden || !transparent(it) {
|
||||
continue;
|
||||
}
|
||||
let reach = it.bounds().inflate(
|
||||
designcraft_render::effect_outset(it) + it.stroke.extent() + 1.0,
|
||||
designcraft_render::effect_outset(it) + it.stroke.extent() + 1.0,
|
||||
);
|
||||
// Only the part on the spread's pages and pasteboard that prints.
|
||||
let (w, h) = ((reach.width() * k).ceil().max(1.0) as u32, (reach.height() * k).ceil().max(1.0) as u32);
|
||||
if w as u64 * h as u64 > 80_000_000 {
|
||||
continue;
|
||||
}
|
||||
// The stack up to and including this object.
|
||||
let mut below = d.clone();
|
||||
std::sync::Arc::make_mut(&mut below.spreads[si]).items.truncate(idx + 1);
|
||||
let view = designcraft_geom::Affine::scale(k) * designcraft_geom::Affine::translate((-reach.x0, -reach.y0));
|
||||
let opts = designcraft_render::RenderOptions { printing_only: true, ..Default::default() };
|
||||
let img = rr.render(
|
||||
&below,
|
||||
cache,
|
||||
&[designcraft_render::Placed { spread: SpreadRef::Doc(si), offset: designcraft_geom::Vec2::ZERO }],
|
||||
w,
|
||||
h,
|
||||
view,
|
||||
&opts,
|
||||
);
|
||||
let new = image_item(&mut out, reach, img, it.layer);
|
||||
let osp = std::sync::Arc::make_mut(&mut out.spreads[si]);
|
||||
let Some(pos) = osp.items.iter().position(|x| x.id == it.id) else { continue };
|
||||
if threaded(d, it) {
|
||||
// Keep the frame (its story flows on) under the image.
|
||||
osp.items.insert(pos + 1, std::sync::Arc::new(new));
|
||||
} else {
|
||||
osp.items[pos] = std::sync::Arc::new(new);
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
@@ -418,7 +466,7 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some((_, n)) = &flattened
|
||||
&& *n > 0
|
||||
{
|
||||
r.warnings.push(format!("transparency flattened: {n} page(s) rasterised"));
|
||||
r.warnings.push(format!("transparency flattened: {n} region(s) rasterised"));
|
||||
}
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
@@ -454,8 +502,12 @@ mod text_tests {
|
||||
let r = s.execute("file.exportPdf", &json!({"flatten": "low"})).unwrap();
|
||||
assert!(r["warnings"].as_array().unwrap().iter().all(|w| !w.as_str().unwrap().contains("flattened")));
|
||||
s.execute("object.opacity", &json!({"ids": [id], "opacity": 0.5})).unwrap();
|
||||
// Text away from the transparency stays live text.
|
||||
s.execute("frame.create", &json!({"rect": [72, 400, 400, 450], "content": "text", "text": "Still vector", "caret": false})).unwrap();
|
||||
let r = s.execute("file.exportPdf", &json!({"flatten": "low"})).unwrap();
|
||||
assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("flattened: 1 page")), "{r}");
|
||||
let pdf = super::super::file::base64_decode(r["base64"].as_str().unwrap());
|
||||
assert!(String::from_utf8_lossy(&pdf).contains("/FontFile"), "the text is still text");
|
||||
assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("flattened: 1 ")), "{r}");
|
||||
assert_eq!(r["pages"], 3);
|
||||
let bytes = super::super::file::base64_decode(r["base64"].as_str().unwrap());
|
||||
assert_eq!(designcraft_render::pdf_page_count(&bytes), Some(3));
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 100 | 2 | 0 | 99% |
|
||||
| P1 | 97 | 94 | 3 | 0 | 98% |
|
||||
| P2 | 76 | 49 | 27 | 0 | 82% |
|
||||
| P2 | 76 | 50 | 26 | 0 | 83% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
@@ -21,7 +21,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Frames, shapes & paths | 100% |
|
||||
| Transform | 100% |
|
||||
| Fill, stroke, colour | 99% |
|
||||
| Effects & transparency | 88% |
|
||||
| Effects & transparency | 92% |
|
||||
| Placing & links | 91% |
|
||||
| Type & text frames | 99% |
|
||||
| Typography | 98% |
|
||||
@@ -165,7 +165,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
|
||||
| Effects & transparency | Basic/Directional/Gradient Feather | P2 | D |
|
||||
| Effects & transparency | Global Light | P2 | D |
|
||||
| Effects & transparency | Transparency blend space | P1 | P |
|
||||
| Effects & transparency | Transparency flattener | P2 | P |
|
||||
| Effects & transparency | Transparency flattener | P2 | D |
|
||||
| Placing & links | Place | P0 | D |
|
||||
| Placing & links | Place into frame / by click or drag | P0 | D |
|
||||
| Placing & links | Raster formats | P0 | D |
|
||||
|
||||
Reference in New Issue
Block a user