fix(render): Blend modes don't blend with the paper

The renderer painted the paper, then blended page items with it: black
text in Color Dodge over blank paper dodged the white to white and
vanished, and other modes showed the same way. In InDesign (and in PDF,
where the page group starts transparent) the paper is not a backdrop:
over blank paper an object in any blend mode shows as it is, and only
the objects below it are blended with. On spreads that use a blend mode
the items are now drawn in their own group, laid over the paper once;
other spreads render as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeer
2026-10-12 10:33:52 +02:00
co-authored by Claude Opus 5.5
parent 835edae659
commit 6cc6f92dad
3 changed files with 70 additions and 0 deletions
+1
View File
@@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-12** · On screen, blend modes blend with the objects below and not with the paper, as in InDesign and in PDF export: a Color Dodge or Screen object over blank paper shows as it is. No readiness percentage change.
- **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change.
- **2026-10-11** · `transform.move {copy}`, Edit › Duplicate and Step and Repeat return the copies' ids, so a script can go on with them. No readiness percentage change.
- **2026-10-11** · `object.fill` and `object.stroke` refuse a swatch name the document doesn't have (it painted nothing) and suggest the name meant (`Paper` → `[Paper]`). No readiness percentage change.
+21
View File
@@ -394,6 +394,13 @@ impl Renderer {
}
}
}
// Blend modes blend with the objects below, not with the paper (as in InDesign and in PDF,
// where the page's group starts transparent): a Color Dodge or Screen object over blank paper
// shows as it is. So the items are drawn in their own group, laid over the paper once.
let isolate = f.opts.paper && spread_blends(doc, sp);
if isolate {
ctx.push_layer(None, None, None, None, None);
}
// Layers back to front; within each layer parent items first (document spreads only).
for layer in doc.layers.iter().rev() {
if !layer.visible || f.opts.printing_only && !layer.printable {
@@ -428,6 +435,9 @@ impl Renderer {
}
}
}
if isolate {
ctx.pop_layer();
}
}
fn draw_item(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, parent: Affine, page: Option<usize>) {
@@ -920,6 +930,17 @@ pub(crate) fn overprints(f: &Frame, swatch: &str, tint: f32, flag: bool) -> bool
f.opts.overprint_preview && (flag || (f.doc.settings.overprint_black && swatch == designcraft_color::swatch::BLACK && tint >= 0.999))
}
/// Does any item on the spread, or on the parent pages it shows, use a blend mode other than
/// Normal (groups and pasted-in items included)?
fn spread_blends(doc: &Document, sp: &designcraft_doc::Spread) -> bool {
let blends = |it: &Item| {
let mut any = false;
it.walk(&mut |i| any |= i.blend != Default::default());
any
};
sp.items.iter().any(|i| blends(i)) || doc.parents.iter().any(|p| p.items.iter().any(|i| blends(i)))
}
pub fn blend_mode(b: DcBlend) -> BlendMode {
use DcBlend as B;
let mix = match b {
+48
View File
@@ -0,0 +1,48 @@
//! Blend modes blend with the objects below, never with the paper: over blank paper an object in
//! Color Dodge (or any other mode) shows as it is, as in InDesign and in the exported PDF.
use designcraft_color::{BlendMode, Color, Swatch};
use designcraft_compose::Cache;
use designcraft_doc::{Document, Fill, Item, ItemId, Shape, SpreadRef, build::NewDocument};
use designcraft_geom::{Affine, Rect, shapes};
use designcraft_render::{Placed, RenderOptions, Renderer};
/// A 200 x 100 page: a mid-blue rectangle on the right half, and over both halves a black
/// rectangle in `blend`.
fn document(blend: BlendMode) -> Document {
let mut d = Document::new(&NewDocument { width: 200.0, height: 100.0, facing_pages: false, ..Default::default() });
d.swatches.push(Swatch::color("Mid blue", Color::rgb(0.2, 0.4, 0.6)));
let mut rect = |r: Rect, swatch: &str, blend: BlendMode| {
let id = ItemId(d.alloc());
let mut it = Item::new(id, d.default_layer(), Shape::Rectangle, shapes::rectangle(r));
it.fill = Fill::swatch(swatch);
it.blend = blend;
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
};
rect(Rect::new(100.0, 0.0, 200.0, 100.0), "Mid blue", BlendMode::Normal);
rect(Rect::new(20.0, 20.0, 180.0, 80.0), "[Black]", blend);
d
}
fn pixel(d: &Document, x: u32, y: u32) -> [u8; 4] {
let mut r = Renderer::new();
r.threads = 0;
let placed = [Placed { spread: SpreadRef::Doc(0), xf: Affine::IDENTITY }];
let img = r.render(d, &Cache::new(), &placed, 200, 100, Affine::IDENTITY, &RenderOptions::default());
let i = ((y * 200 + x) * 4) as usize;
[img.pixels[i], img.pixels[i + 1], img.pixels[i + 2], img.pixels[i + 3]]
}
#[test]
fn a_blend_mode_over_blank_paper_shows_the_object_as_it_is() {
let d = document(BlendMode::ColorDodge);
// Over the paper: black ([Black] shows as about 52 grey), as in Normal (it used to dodge the white paper to white, vanishing).
let over_paper = pixel(&d, 50, 50);
assert!(over_paper[0] < 80 && over_paper[1] < 80 && over_paper[2] < 80, "{over_paper:?}");
// Over the blue rectangle it still blends: black in Color Dodge leaves the blue as it is.
let over_blue = pixel(&d, 150, 50);
assert!(over_blue[2] > 120 && over_blue[2] > over_blue[0], "{over_blue:?}");
// Normal is unchanged.
let normal = pixel(&document(BlendMode::Normal), 150, 50);
assert!(normal[0] < 80 && normal[2] < 80, "{normal:?}");
}