Files
designcraft/crates/render/tests/colorize.rs
T
aiongg 6e4f122b8c Merge PR #1100: Placed greyscale and bitmap images take the swatch applied to them (IDML, screen, PDF)
Graphic literals (cli perf, doc assets/item, engine datamerge/export/file/links/mathexpr/media/table/sample, epub, format, pdf, pptx, render, tools, ui-egui properties, and tests): name the branch's clip and wrap (#561, #381) and the PR's fill.
crates/doc/src/item.rs: Graphic keeps wrap and gains fill.
crates/idml/src/import.rs: read the image swatch, then the branch's styled FrameFittingOption lookup; the graphic keeps its wrap.
crates/idml/src/tests/assets.rs: keep both new tests.
crates/render/src/lib.rs: keep #362's crop-box sizing of placed PDFs and colour the resulting pixmap.
crates/render/tests/colorize.rs, crates/epub/tests/grouped_reading_order.rs, crates/compose/src/tests.rs: literals name the branch's Asset and Graphic fields.
crates/pdf/src/export.rs: the soft mask interpolates by the standard's raster mode (the branch has no per-exporter interpolate switch).
docs/gaps.md, docs/target-app-parity.md: status lines list both sides' changes; gap text keeps the clipping-path items and adds the image swatch.
2026-10-16 04:07:57 +00:00

105 lines
4.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! A swatch applied to a placed greyscale or bitmap image colours it, as in InDesign (#1094):
//! a greyscale pixel is a tint of the swatch as strong as the pixel is dark, and white stays
//! opaque; a bitmap's black takes the swatch and its white lets the frame's fill show through.
//! Colour images, and swatches that no longer exist, leave the image as it is.
use std::sync::Arc;
use designcraft_color::swatch::apply_tint;
use designcraft_compose::Cache;
use designcraft_doc::build::NewDocument;
use designcraft_doc::{Asset, AssetId, Content, Document, Fill, Graphic, Item, ItemId, Shape, SpreadRef, Stroke};
use designcraft_geom::{Affine, Rect, shapes};
use designcraft_render::{RenderOptions, Renderer, color_of};
use image::{ExtendedColorType, ImageEncoder};
const RED: &str = "C=0 M=100 Y=100 K=0";
const CYAN: &str = "C=100 M=0 Y=0 K=0";
fn encode(px: &[u8], w: u32, ct: ExtendedColorType, gif: bool) -> Vec<u8> {
let mut b = Vec::new();
let c = std::io::Cursor::new(&mut b);
if gif {
image::codecs::gif::GifEncoder::new(c).encode(px, w, 1, ct).unwrap();
} else {
image::codecs::png::PngEncoder::new(c).write_image(px, w, 1, ct).unwrap();
}
b
}
/// The pixels at the centres of the image's three columns, placed 150 × 50 pt at (100, 100) in a
/// cyan frame, with `fill` applied to the image.
fn shown(file: Vec<u8>, fill: Fill) -> Vec<[u8; 4]> {
let mut d = Document::new(&NewDocument::default());
let aid = AssetId(d.alloc());
d.assets.insert(
aid,
Arc::new(Asset { page: 0, id: aid, name: "a".into(), mime: "image/png".into(), link: None, data: Arc::new(file), pixels: Some((3, 1)), pdf_crop: Default::default(), pdf_box: None }),
);
let id = ItemId(d.alloc());
let mut it = Item::new(id, d.default_layer(), Shape::Rectangle, shapes::rectangle(Rect::new(100.0, 100.0, 250.0, 150.0)));
it.fill = Fill::swatch(CYAN);
it.stroke = Stroke::default();
it.content = Content::Graphic(Graphic {
asset: aid,
size: (150.0, 50.0),
xf: Affine::translate((100.0, 100.0)),
clip: None,
auto_fit: Default::default(),
fit_align: 4,
crop: [0.0; 4],
wrap: Default::default(),
fill,
});
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
let mut r = Renderer::new();
r.threads = 0;
let img = r.render_page(&d, &Cache::new(), 0, 1.0, false, &RenderOptions::default()).unwrap();
[125, 175, 225].iter().map(|&x| img.pixel(x, 125)).collect()
}
/// The swatch `name` at `tint` on screen.
fn swatch_rgb(name: &str, tint: f32) -> [u8; 4] {
let d = Document::new(&NewDocument::default());
let [r, g, b, a] = color_of(&apply_tint(d.resolve_color(name, 1.0).unwrap(), tint), 1.0).to_rgba8().to_u8_array();
[r, g, b, a]
}
fn near(a: [u8; 4], b: [u8; 4]) -> bool {
a.iter().zip(b).all(|(x, y)| x.abs_diff(y) <= 4)
}
#[test]
fn a_greyscale_image_is_a_tint_of_the_swatch_on_opaque_white() {
let grey = encode(&[0, 128, 255], 3, ExtendedColorType::L8, false);
let got = shown(grey.clone(), Fill::swatch(RED));
assert!(near(got[0], swatch_rgb(RED, 1.0)), "black is the swatch: {got:?}");
assert!(near(got[1], swatch_rgb(RED, 127.0 / 255.0)), "mid grey is half the swatch: {got:?}");
// White is 0% of the swatch, opaque: the cyan frame does not show through.
assert!(near(got[2], swatch_rgb(RED, 0.0)), "white stays white: {got:?}");
assert!(got[2][0] > 240, "{got:?}");
// The swatch's tint scales every pixel's.
let half = shown(grey, Fill { tint: 0.5, ..Fill::swatch(RED) });
assert!(near(half[0], swatch_rgb(RED, 0.5)), "{half:?}");
}
#[test]
fn a_bitmap_image_shows_the_frame_through_its_white() {
let bw = [[0u8, 0, 0, 255], [255, 255, 255, 255], [0, 0, 0, 255]].concat();
let got = shown(encode(&bw, 3, ExtendedColorType::Rgba8, true), Fill::swatch(RED));
assert!(near(got[0], swatch_rgb(RED, 1.0)) && near(got[2], got[0]), "{got:?}");
assert!(near(got[1], swatch_rgb(CYAN, 1.0)), "the frame's cyan through the white: {got:?}");
}
#[test]
fn colour_images_and_missing_swatches_are_left_alone() {
let rgb = encode(&[0, 0, 0, 200, 40, 40, 255, 255, 255], 3, ExtendedColorType::Rgb8, false);
assert_eq!(shown(rgb.clone(), Fill::swatch(RED)), shown(rgb, Fill::none()));
// A swatch that was deleted (or never imported): the image as it is.
let grey = encode(&[0, 128, 255], 3, ExtendedColorType::L8, false);
let plain = shown(grey.clone(), Fill::none());
assert_eq!(shown(grey, Fill::swatch("Gone")), plain);
assert!(plain[0][0] < 10, "the image is drawn: {plain:?}");
}