Files
vectorcraft/crates/doc/src/rastersettings.rs
T
bflatasticandClaude Opus 5.5 94f0f1b0d2 M4.56: Document Raster Effects Settings with colour model, background, anti-alias, clipping mask and room around
document.rasterEffectsSettings now also takes and reports colorModel,
background, antiAlias, clippingMask, addAround and preserveSpotColors in one
undo step (Document.raster_effects, RasterEffectsSettings, next to the
existing raster_effects_ppi so old files load unchanged). The raster effect
images of PDF export and Expand Appearance get the extra room and are
finished by the settings (hard edges, grayscale or bitmap, opaque on
white, white only under the art with a clipping mask). Rasterize takes
them as defaults (params override), and its clipping mask puts the image
in a clip group with the art's outline. The settings have their own dialog
(rasterEffectsSettings, ui.rasterEffectsSettingsDialog) and Rasterize…
opens its options prefilled from them.

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

136 lines
5.3 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.
//! Effect → Document Raster Effects Settings, besides the resolution
//! ([`crate::Document::raster_effects_ppi`]): how raster effects (shadows, glows, blurs, feathers)
//! become pixels when they are flattened for output or expanded, and the defaults of Rasterize.
//! Every field is defaulted, so documents saved before these settings existed load unchanged.
use serde::{Deserialize, Serialize};
use crate::setup::Background;
/// The colour model raster effect images are made in.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RasterColorModel {
/// The document's colour mode (RGB or CMYK).
#[default]
Document,
Grayscale,
/// Black and white pixels only.
Bitmap,
}
impl RasterColorModel {
pub const ALL: [RasterColorModel; 3] = [RasterColorModel::Document, RasterColorModel::Grayscale, RasterColorModel::Bitmap];
pub fn id(self) -> &'static str {
match self {
RasterColorModel::Document => "document",
RasterColorModel::Grayscale => "grayscale",
RasterColorModel::Bitmap => "bitmap",
}
}
}
/// Largest Add … Around Object, in points.
pub const MAX_ADD_AROUND: f64 = 1000.0;
/// Document Raster Effects Settings (resolution aside).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct RasterEffectsSettings {
pub color_model: RasterColorModel,
/// White: images are opaque, on white. Transparent: they keep their transparency.
pub background: Background,
/// Off: the art's edges are hard (raster effects stay smooth).
pub anti_alias: bool,
/// With a white background, the white stays only under the art (outside it the image is
/// transparent); Rasterize clips its image to the art's outline.
pub clipping_mask: bool,
/// Extra room around the art, in points (0–[`MAX_ADD_AROUND`]).
pub add_around: f64,
/// Keep spot colours in raster effects (stored; effect images are made in process colour).
pub preserve_spot_colors: bool,
}
impl Default for RasterEffectsSettings {
fn default() -> Self {
Self {
color_model: RasterColorModel::Document,
background: Background::Transparent,
anti_alias: true,
clipping_mask: false,
add_around: 0.0,
preserve_spot_colors: true,
}
}
}
impl RasterEffectsSettings {
/// Turn premultiplied RGBA8 pixels into what these settings make of them: the colour model,
/// then the background. (Anti-aliasing is the renderer's.)
pub fn finish_pixels(&self, pixels: &mut [u8]) {
let white = self.background == Background::White;
if self.color_model == RasterColorModel::Document && !white {
return;
}
for px in pixels.as_chunks_mut::<4>().0 {
let a = px[3];
if self.color_model != RasterColorModel::Document && a != 0 {
// Rec. 709 luma of the premultiplied colour (still premultiplied).
let y = 0.2126 * px[0] as f32 + 0.7152 * px[1] as f32 + 0.0722 * px[2] as f32;
let y = match self.color_model {
RasterColorModel::Bitmap => {
if y >= a as f32 / 2.0 {
a
} else {
0
}
}
_ => y.round().min(a as f32) as u8,
};
px[..3].fill(y);
}
// Over white; a clipping mask leaves what the art doesn't cover transparent.
if white && !(self.clipping_mask && a == 0) {
let under = 255 - a;
for c in &mut px[..3] {
*c = c.saturating_add(under);
}
px[3] = 255;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn finish(s: RasterEffectsSettings, px: [u8; 4]) -> [u8; 4] {
let mut p = px;
s.finish_pixels(&mut p);
p
}
#[test]
fn pixels_follow_the_settings() {
let d = RasterEffectsSettings::default();
assert_eq!(finish(d.clone(), [100, 0, 0, 100]), [100, 0, 0, 100], "the defaults change nothing");
let white = RasterEffectsSettings { background: Background::White, ..d.clone() };
assert_eq!(finish(white.clone(), [0, 0, 0, 0]), [255, 255, 255, 255]);
assert_eq!(finish(white.clone(), [100, 0, 0, 100]), [255, 155, 155, 255]);
let masked = RasterEffectsSettings { clipping_mask: true, ..white };
assert_eq!(finish(masked, [0, 0, 0, 0]), [0, 0, 0, 0]);
let gray = RasterEffectsSettings { color_model: RasterColorModel::Grayscale, ..d.clone() };
assert_eq!(finish(gray, [255, 0, 0, 255]), [54, 54, 54, 255]);
let bitmap = RasterEffectsSettings { color_model: RasterColorModel::Bitmap, ..d };
assert_eq!(finish(bitmap.clone(), [255, 0, 0, 255]), [0, 0, 0, 255]);
assert_eq!(finish(bitmap, [0, 255, 0, 255]), [255, 255, 255, 255]);
}
#[test]
fn partial_settings_fill_in_the_rest() {
let s: RasterEffectsSettings = serde_json::from_str(r#"{"background":"white"}"#).unwrap();
assert_eq!(s, RasterEffectsSettings { background: Background::White, ..Default::default() });
}
}