Files
vectorcraft/crates/pdf/src/tests_cmykblend.rs
T
bflatasticandClaude Opus 5.5 4304a7c0b1 M4.31: Save PDF dialog and document.exportPdf with PDF settings
PdfSettings (serde, camelCase) is the Save PDF model: Standard, Compatibility
PDF 1.4-2.0, General (Preserve editing, Thumbnails, Fast web view, View after
saving, Create layers), Compression, Marks and Bleeds, Output, Advanced and
Security. PdfOptions = settings + pages, title and date.

document.exportPdf {path?, preset?, artboard?|artboards?|range?, standard?,
compatibility?, ...sections} -> {path|dataBase64, bytes, warnings} applies the
options over the preset (null keeps the preset's value); PDF/X, passwords,
PDF/A-2b at 2.0 and out-of-range values are refused; options accepted but not
applied yet come back as warnings. document.export {format: pdf} and
document.formats take and list the same options.

The Save PDF dialog (file.export.pdf, ui.savePdfDialog) has Preset,
Standard and Compatibility on top and a section list; it runs the export
before asking for a path, so bad options never open a save dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:22:27 +07:00

79 lines
3.1 KiB
Rust

//! Blending in CMYK documents: transparency groups blend in DeviceCMYK.
use std::sync::Arc;
use vectorcraft_color::{BlendMode, Color, Paint};
use vectorcraft_doc::{Appearance, ColorMode, Document, Node, NodeId, OpacityMask};
use vectorcraft_geom::{Rect, shapes};
use crate::*;
fn rect(r: Rect, c: Color) -> Node {
Node::path(NodeId(0), shapes::rectangle(r), Appearance::basic(Paint::solid(c), Paint::None, 0.0))
}
fn add(d: &mut Document, mut n: Node) {
n.id = d.alloc_id();
let l = d.layers[0].id;
d.insert(Some(l), usize::MAX, n).unwrap();
}
/// Magenta with cyan multiplied over it: in a group when `grouped`, else at the top of the page.
fn doc(mode: ColorMode, grouped: bool) -> Document {
let mut d = Document::new_with_mode(100.0, 100.0, mode);
add(&mut d, rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::cmyk(0.0, 1.0, 0.0, 0.0)));
let mut top = rect(Rect::new(20.0, 20.0, 80.0, 80.0), Color::cmyk(1.0, 0.0, 0.0, 0.0));
top.blend = BlendMode::Multiply;
if grouped {
top.id = d.alloc_id();
let mut g = Node::group(NodeId(0), vec![Arc::new(top)]);
g.opacity = 0.5;
add(&mut d, g);
} else {
add(&mut d, top);
}
d
}
/// The file's text with spaces removed (content streams uncompressed), after checking it reads.
fn text(d: &Document) -> String {
let bytes = export(d, &PdfOptions::uncompressed()).unwrap();
import(&bytes).expect("the rewritten file still reads");
String::from_utf8_lossy(&bytes).replace(' ', "")
}
#[test]
fn groups_of_cmyk_documents_blend_in_device_cmyk() {
for grouped in [true, false] {
let t = text(&doc(ColorMode::Cmyk, grouped));
// Top-level blending gets a group of its own, so it blends in CMYK too.
assert!(t.contains("/S/Transparency") && t.contains("/CS/DeviceCMYK"), "grouped {grouped}: {t}");
assert!(!t.contains("/CS/DeviceRGB"), "grouped {grouped}: {t}");
assert!(t.contains("/BM/Multiply"));
}
// Compressed output gets the same group dictionaries.
let bytes = export(&doc(ColorMode::Cmyk, true), &PdfOptions::default()).unwrap();
assert!(String::from_utf8_lossy(&bytes).contains("/CS/DeviceCMYK"));
}
#[test]
fn rgb_documents_and_opaque_cmyk_pages_are_unchanged() {
let t = text(&doc(ColorMode::Rgb, true));
assert!(t.contains("/CS/DeviceRGB") && !t.contains("/CS/DeviceCMYK"), "{t}");
let mut d = doc(ColorMode::Cmyk, false);
let top = d.layers[0].children().unwrap()[1].id;
d.node_mut(top).unwrap().blend = BlendMode::Normal;
assert!(!text(&d).contains("/Transparency"));
}
#[test]
fn soft_mask_groups_keep_screen_luminance() {
let mut d = Document::new_with_mode(100.0, 100.0, ColorMode::Cmyk);
let mut n = rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::cmyk(0.0, 0.0, 0.0, 1.0));
n.mask = Some(Box::new(OpacityMask::new(rect(Rect::new(0.0, 0.0, 50.0, 100.0), Color::WHITE), true)));
add(&mut d, n);
let t = text(&d);
// The page's group blends in CMYK; the mask's group measures luminance in RGB.
assert!(t.contains("/CS/DeviceCMYK") && t.contains("/S/Luminosity") && t.contains("/CS/DeviceRGB"), "{t}");
}