mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 11:11:30 +00:00
Merge pull request #6355 from dotCooCoo/fix/issue-6125
Fix #6125: raster exports keep a Screen glow on the bare page
This commit is contained in:
@@ -125,7 +125,7 @@ const BACKGROUND: FormatOption = FormatOption {
|
||||
name: "background",
|
||||
ty: "string",
|
||||
default: "\"transparent\"",
|
||||
description: "transparent, white, black or a colour such as \"#ff8800\" (JPEG: white when transparent)",
|
||||
description: "transparent, white, black or a colour such as \"#ff8800\" (JPEG: white when transparent); drawn under the finished art, so blend modes do not read it",
|
||||
};
|
||||
|
||||
const ANTI_ALIAS: FormatOption = FormatOption {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! cut into atomic regions: the faces of the planar arrangement of every fill, outlined stroke and
|
||||
//! clipping path in them ([`po::regions`]). The same paints cover each whole region, so its colour
|
||||
//! is exact: those paints composited as the renderer composites them ([`Composer`], blend modes by
|
||||
//! [`composite`]), over white (or over nothing with Preserve Alpha); in CMYK documents their inks
|
||||
//! [`composite`]), with white under the finished art (or nothing with Preserve Alpha); in CMYK documents their inks
|
||||
//! composite, plane by plane ([`cmyk_planes`]), and the colour is CMYK. Each region becomes a path
|
||||
//! filled with that flat colour. Where gradients, patterns, images, opacity masks or raster
|
||||
//! effects reach there is no single colour: those regions are rendered into one image per group,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::analysis::{Scratch, outlined_text, overlapping, reach};
|
||||
use super::compose::{Builder, CLEAR, Composer, Elem, Fill, Kind, WHITE};
|
||||
use super::compose::{Builder, CLEAR, Composer, Elem, Fill, Kind, Px, WHITE};
|
||||
use super::options::FlattenOptions;
|
||||
use super::{
|
||||
Affine, Appearance, AppearanceItem, Arc, BlendMode, Color, ColorMode, Document, EngineError, FillLayer, FillRule, ImageBlob, ImageObject, Inks,
|
||||
MAX_PIXELS, Node, NodeId, NodeKind, Paint, PathData, Rect, Result, effects, isolated_doc, outline_strokes_under, planes_cmyk, po, separate,
|
||||
shape_node, shapes, unique_key,
|
||||
MAX_PIXELS, Node, NodeId, NodeKind, Paint, PathData, Rect, Result, composite, effects, isolated_doc, outline_strokes_under, planes_cmyk, po,
|
||||
separate, shape_node, shapes, unique_key,
|
||||
};
|
||||
|
||||
// ---------- plan ----------
|
||||
@@ -235,18 +235,14 @@ fn region_fill(elems: &[Elem], sources: &[usize], o: &FlattenOptions, cmyk: bool
|
||||
if alone[3] < 0.5 / 255.0 {
|
||||
return None;
|
||||
}
|
||||
let start = if o.preserve_alpha { CLEAR } else { WHITE };
|
||||
let px = if o.preserve_alpha {
|
||||
alone
|
||||
} else {
|
||||
c.top = None;
|
||||
c.eval(elems, start)
|
||||
};
|
||||
// The white goes under the finished art, as under the renderer's: blend modes read only the art.
|
||||
let on_page = |p: Px| if o.preserve_alpha { p } else { composite(BlendMode::Normal, WHITE, p) };
|
||||
let px = on_page(alone);
|
||||
let rgb = [px[0], px[1], px[2]];
|
||||
// The K plane composites the same way.
|
||||
let k = cmyk.then(|| {
|
||||
c.plane = 1;
|
||||
c.eval(elems, start)[0]
|
||||
on_page(c.eval(elems, CLEAR))[0]
|
||||
});
|
||||
// A paint showing as it is keeps its colour (spot, process, swatch) and overprint.
|
||||
let near = |a: &[f32], b: &[f32]| a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-3);
|
||||
@@ -266,7 +262,7 @@ fn region_fill(elems: &[Elem], sources: &[usize], o: &FlattenOptions, cmyk: bool
|
||||
}
|
||||
|
||||
/// `art` rendered over `rect` at `ppi`: over white with the white the art doesn't cover taken out
|
||||
/// again (so blend modes see the white page), or over nothing with Preserve Alpha. The image and
|
||||
/// again, or over nothing with Preserve Alpha. The image and
|
||||
/// its `clip` reach a pixel further. An opaque image in a CMYK document holds the art's ink
|
||||
/// amounts (a CMYK TIFF), so its CMYK colours keep their numbers (#2160); other images are PNGs.
|
||||
/// Fails when the image can't be encoded.
|
||||
|
||||
@@ -447,6 +447,27 @@ fn export_selection_keeps_the_effects_in_the_crop() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An Outer Glow at its default Screen mode on the bare page is in a PNG on white and in a JPEG,
|
||||
/// and in a transparent PNG (#6125).
|
||||
#[test]
|
||||
fn a_screen_outer_glow_is_in_raster_exports() {
|
||||
let mut s = session();
|
||||
s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 20, "height": 20})).unwrap();
|
||||
s.execute("effect.apply", &json!({"effect": "stylize.outerGlow"})).unwrap();
|
||||
let mut pixel = |p: serde_json::Value| {
|
||||
let v = s.execute("document.export", &p).unwrap();
|
||||
let img = image::load_from_memory(&vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap()).unwrap().to_rgba8();
|
||||
// 2 pt past the square's stroke, on the right.
|
||||
*img.get_pixel(122, 110)
|
||||
};
|
||||
for p in [json!({"format": "png", "background": "white"}), json!({"format": "jpg"})] {
|
||||
let px = pixel(p.clone());
|
||||
assert!(px[0] > 235 && px[1] > 235 && px[2] < 235, "{p}: yellow on white: {px:?}");
|
||||
}
|
||||
let px = pixel(json!({"format": "png", "background": "transparent"}));
|
||||
assert!(px[3] > 0 && px[0] > 235 && px[1] > 235 && px[2] < 40, "the glow is yellow in the transparent image: {px:?}");
|
||||
}
|
||||
|
||||
/// A group's own drop shadow is inside the crop of the group's selection export too.
|
||||
#[test]
|
||||
fn export_selection_keeps_a_groups_shadow_in_the_crop() {
|
||||
|
||||
@@ -251,11 +251,16 @@ fn every_builtin_style_paints_visibly_on_a_rectangle() {
|
||||
let (_, lib) = cmd::stylelib::library(&s, b.id).unwrap();
|
||||
for g in &lib.styles {
|
||||
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
|
||||
// Art under the rectangle: a blend mode shows only against art, not the bare page.
|
||||
let under = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#336699", "ids": [under.0]}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [under.0]}));
|
||||
let id = rect(&mut s, 25.0, 30.0, 50.0, 40.0);
|
||||
run(&mut s, "graphicStyle.addFromLibrary", json!({"library": b.id, "name": g.name, "apply": true}));
|
||||
assert_eq!(doc(&s).node(id).unwrap().extras().graphic_style, doc(&s).graphic_style(&g.name).map(|x| x.id), "{}", g.name);
|
||||
let img = renderer.render_region(doc(&s), Rect::new(0.0, 0.0, 100.0, 100.0), 0.5, true);
|
||||
let painted = img.pixels.as_chunks::<4>().0.iter().filter(|p| p[..3] != [255, 255, 255]).count();
|
||||
let bare = img.pixel(0, 0);
|
||||
let painted = (0..img.height).flat_map(|y| (0..img.width).map(move |x| (x, y))).filter(|&(x, y)| img.pixel(x, y) != bare).count();
|
||||
assert!(painted > 40, "{} / {} paints {painted} pixels", b.id, g.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +182,8 @@ fn shadow_rasters_keep_their_node_and_a_byte_budget() {
|
||||
/// kept for any pan: its grid starts at the reach's first whole device pixel, which a pan by whole
|
||||
/// pixels moves with the art, so the raster drawn after the pan is the one a fresh renderer makes,
|
||||
/// also for pans that aren't multiples of the raster's scale. A pan by a fraction of a pixel
|
||||
/// places the raster at the nearest whole pixel, as a full-size raster kept whole is placed, and
|
||||
/// here draws as a fresh renderer draws too. A 100 pt glow at 500% (scale 2) and a 2 pt glow
|
||||
/// keeps the raster and places it at the nearest whole pixel, as a full-size raster kept whole is
|
||||
/// placed. A 100 pt glow at 500% (scale 2) and a 2 pt glow
|
||||
/// round the page at 800% (scale 3, near the narrowest blur made coarser).
|
||||
#[test]
|
||||
fn a_coarser_glow_kept_whole_draws_after_a_pan_as_made_again() {
|
||||
@@ -210,7 +210,9 @@ fn a_coarser_glow_kept_whole_draws_after_a_pan_as_made_again() {
|
||||
assert_eq!(image(&r), kept, "blur {blur}, pan {pan:?}: the raster kept is drawn");
|
||||
let mut fresh = Renderer::new();
|
||||
fresh.threads = 2;
|
||||
assert!(panned.pixels == fresh.render(&d, size.0, size.1, xf, &opts).pixels, "blur {blur}, pan {pan:?}");
|
||||
if pan.0.fract() == 0.0 && pan.1.fract() == 0.0 {
|
||||
assert!(panned.pixels == fresh.render(&d, size.0, size.1, xf, &opts).pixels, "blur {blur}, pan {pan:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ impl Ink {
|
||||
peniko::Color::from_rgba8(r, g, b, a)
|
||||
}
|
||||
|
||||
fn fixed8(self, [r, g, b, a]: [u8; 4]) -> [u8; 4] {
|
||||
pub(crate) fn fixed8(self, [r, g, b, a]: [u8; 4]) -> [u8; 4] {
|
||||
match self.base() {
|
||||
Ink::Display | Ink::RichBlack => [r, g, b, a],
|
||||
_ => {
|
||||
|
||||
@@ -232,7 +232,9 @@ impl Renderer {
|
||||
self.live.sweep(g);
|
||||
}
|
||||
|
||||
/// Draw frame `f` (`w`×`h` pixels): the background, the artboards and the page's art.
|
||||
/// Draw frame `f` (`w`×`h` pixels): the artboards and the page's art, then the background
|
||||
/// under them. The art blends only with itself, as it does on the canvas, where the paper is
|
||||
/// drawn apart from it: a Screen glow on the bare page stays visible on a white background (#6125).
|
||||
fn draw_frame(&mut self, f: &Frame, w: u16, h: u16) -> Vec<u8> {
|
||||
let (threads, opts) = (self.threads, f.opts);
|
||||
let kept = self.contexts.iter().position(|(t, c)| *t == threads && c.width() == w && c.height() == h);
|
||||
@@ -247,11 +249,6 @@ impl Renderer {
|
||||
ctx.set_aliasing_threshold(opts.anti_alias.threshold());
|
||||
let render_mode = if opts.precise { vello_cpu::RenderMode::OptimizeQuality } else { vello_cpu::RenderMode::OptimizeSpeed };
|
||||
self.raster = vello_cpu::RasterizerSettings { render_mode, ..Default::default() };
|
||||
if let Some(bg) = opts.background {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.set_paint(f.ink.fixed(bg));
|
||||
ctx.fill_rect(&kurbo::Rect::new(0.0, 0.0, w as f64, h as f64));
|
||||
}
|
||||
if opts.artboards && !opts.outline {
|
||||
// Paper: white on screen, no ink on the ink planes.
|
||||
ctx.set_transform(f.view);
|
||||
@@ -278,7 +275,11 @@ impl Renderer {
|
||||
if self.contexts.len() > 2 {
|
||||
self.contexts.remove(0);
|
||||
}
|
||||
pm.data_as_u8_slice().to_vec()
|
||||
let mut pixels = pm.data_as_u8_slice().to_vec();
|
||||
if let Some(bg) = opts.background {
|
||||
over_background(&mut pixels, f.ink.fixed8(bg));
|
||||
}
|
||||
pixels
|
||||
}
|
||||
|
||||
/// The page's art: the layers (or the pattern being edited), inside the page group when the
|
||||
@@ -392,10 +393,6 @@ impl Renderer {
|
||||
let view = Affine::translate((w as f64 / 2.0, w as f64 / 2.0)) * Affine::scale(s) * Affine::translate(-b.center().to_vec2());
|
||||
let mut ctx = single_threaded_context(w, w);
|
||||
ctx.set_aliasing_threshold(opts.anti_alias.threshold());
|
||||
if let Some(bg) = background {
|
||||
ctx.set_paint(peniko::Color::from_rgba8(bg[0], bg[1], bg[2], bg[3]));
|
||||
ctx.fill_rect(&kurbo::Rect::new(0.0, 0.0, w as f64, w as f64));
|
||||
}
|
||||
let symbols = doc.symbol_boxes().quick();
|
||||
let frame = Frame {
|
||||
mt: false,
|
||||
@@ -415,6 +412,22 @@ impl Renderer {
|
||||
self.draw_forced_arc(&mut ctx, &frame, &Arc::new(n.clone()));
|
||||
let pm = self.finish(&mut ctx, vello_cpu::RasterizerSettings::default());
|
||||
self.end_frame();
|
||||
Some(Rendered { width: w as u32, height: w as u32, pixels: pm.data_as_u8_slice().to_vec() })
|
||||
let mut pixels = pm.data_as_u8_slice().to_vec();
|
||||
if let Some(bg) = background {
|
||||
over_background(&mut pixels, bg);
|
||||
}
|
||||
Some(Rendered { width: w as u32, height: w as u32, pixels })
|
||||
}
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA `pixels` drawn over the solid color `background` (`[r, g, b, a]`, straight).
|
||||
pub(crate) fn over_background(pixels: &mut [u8], [r, g, b, a]: [u8; 4]) {
|
||||
let alpha = u32::from(a);
|
||||
let under = [u32::from(r) * alpha / 255, u32::from(g) * alpha / 255, u32::from(b) * alpha / 255, alpha];
|
||||
for p in pixels.as_chunks_mut::<4>().0 {
|
||||
let rest = 255 - u32::from(p[3]);
|
||||
for (v, bg) in p.iter_mut().zip(under) {
|
||||
*v = (u32::from(*v) + (bg * rest + 127) / 255).min(255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,15 +116,63 @@ fn page_knockout_and_isolated_blending() {
|
||||
Arc::make_mut(&mut d.layers[0]).knockout = Knockout::Off;
|
||||
assert!(close(render(&d).pixel(50, 50), BLUE_OVER_RED));
|
||||
|
||||
// Page Isolated Blending: a Difference object no longer blends with the white under the page.
|
||||
// A Difference object on the bare page keeps its color: the background goes under the
|
||||
// finished art (#6125), with Page Isolated Blending or without it.
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let mut red = half(&mut d, 10.0, (1.0, 0.0, 0.0));
|
||||
red.opacity = 1.0;
|
||||
red.blend = Blend::Difference;
|
||||
d.insert(Some(d.layers[0].id), 0, red).unwrap();
|
||||
assert!(close(render(&d).pixel(30, 50), [0, 255, 255, 255]), "inverts the white: {:?}", render(&d).pixel(30, 50));
|
||||
assert!(close(render(&d).pixel(30, 50), [255, 0, 0, 255]), "the bare page: {:?}", render(&d).pixel(30, 50));
|
||||
d.page_isolate = true;
|
||||
assert!(close(render(&d).pixel(30, 50), [255, 0, 0, 255]), "isolated: {:?}", render(&d).pixel(30, 50));
|
||||
// A white object under it is art: the Difference object inverts it.
|
||||
let id = d.alloc_id();
|
||||
let white = Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::basic(Paint::solid(Color::WHITE), Paint::None, 0.0));
|
||||
d.insert(Some(d.layers[0].id), 0, white).unwrap();
|
||||
assert!(close(render(&d).pixel(30, 50), [0, 255, 255, 255]), "inverts the white object: {:?}", render(&d).pixel(30, 50));
|
||||
}
|
||||
|
||||
/// On a white or blue background, every blend mode on the bare page gives the transparent
|
||||
/// render over that color (#6125).
|
||||
#[test]
|
||||
fn no_blend_mode_reads_the_background() {
|
||||
for mode in Blend::ALL {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let mut red = half(&mut d, 10.0, (1.0, 0.0, 0.0));
|
||||
red.blend = mode;
|
||||
d.insert(Some(d.layers[0].id), 0, red).unwrap();
|
||||
let clear = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions::default()).pixel(30, 50);
|
||||
for bg in [[255, 255, 255, 255], [0, 0, 255, 255]] {
|
||||
let got =
|
||||
Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions { background: Some(bg), ..Default::default() }).pixel(30, 50);
|
||||
let a = i32::from(clear[3]);
|
||||
let want = [0, 1, 2].map(|c| (i32::from(clear[c]) * a + i32::from(bg[c]) * (255 - a)) / 255);
|
||||
assert!((0..3).all(|c| (i32::from(got[c]) - want[c]).abs() <= 3) && got[3] == 255, "{mode:?} on {bg:?}: {got:?}, want {want:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Premultiplied pixels of alpha 0, 128 and 255 over backgrounds of alpha 0, 128 and 255.
|
||||
#[test]
|
||||
fn over_background_composites_premultiplied_pixels() {
|
||||
let over = |p: [u8; 4], a: u8| {
|
||||
let mut px = p.to_vec();
|
||||
crate::renderer::over_background(&mut px, [200, 100, 0, a]);
|
||||
[px[0], px[1], px[2], px[3]]
|
||||
};
|
||||
const CLEAR: [u8; 4] = [0; 4];
|
||||
const HALF: [u8; 4] = [64, 10, 100, 128];
|
||||
const SOLID: [u8; 4] = [9, 8, 7, 255];
|
||||
assert_eq!(over(CLEAR, 0), CLEAR);
|
||||
assert_eq!(over(HALF, 0), HALF);
|
||||
assert_eq!(over(SOLID, 0), SOLID);
|
||||
assert_eq!(over(CLEAR, 128), [100, 50, 0, 128]);
|
||||
assert_eq!(over(HALF, 128), [114, 35, 100, 192]);
|
||||
assert_eq!(over(SOLID, 128), SOLID);
|
||||
assert_eq!(over(CLEAR, 255), [200, 100, 0, 255]);
|
||||
assert_eq!(over(HALF, 255), [164, 60, 100, 255]);
|
||||
assert_eq!(over(SOLID, 255), SOLID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -771,6 +771,64 @@ fn rasterize_on_an_image_takes_in_its_strokes_shape() {
|
||||
assert_eq!((lum(img.pixel(8, 35)), lum(img.pixel(12, 35)), img.pixel(35, 35)), (WHITE, 0, [255, 0, 0, 255]));
|
||||
}
|
||||
|
||||
/// A 40 pt square filled with `fill` carrying effect `id`, in a 100×100 document.
|
||||
fn glowing_square(fill: Color, id: &str, params: Value) -> Document {
|
||||
let mut n = Node::path(NodeId(0), shapes::rectangle(Rect::new(30.0, 30.0, 70.0, 70.0)), Appearance::basic(Paint::solid(fill), Paint::None, 0.0));
|
||||
n.appearance.effects.push(fx(id, params));
|
||||
doc_with(Document::new(100.0, 100.0), n)
|
||||
}
|
||||
|
||||
/// A glow on the bare page is drawn before the background goes under the art: the pixel is the
|
||||
/// transparent render's over that color, also for a background other than white (#6125).
|
||||
#[test]
|
||||
fn a_screen_outer_glow_on_the_bare_page_shows_on_a_background() {
|
||||
let d = glowing_square(Color::WHITE, "stylize.outerGlow", json!({}));
|
||||
let clear = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions::default()).pixel(27, 50);
|
||||
assert!(clear[0] > 20 && clear[0] == clear[1] && clear[2] == 0, "the glow is yellow: {clear:?}");
|
||||
for (bg, name) in [([255, 255, 255, 255], "white"), ([0, 0, 255, 255], "blue")] {
|
||||
let p = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions { background: Some(bg), ..Default::default() }).pixel(27, 50);
|
||||
let a = u32::from(clear[3]);
|
||||
let want: Vec<u32> = (0..3).map(|c| (u32::from(clear[c]) * a + u32::from(bg[c]) * (255 - a)) / 255).collect();
|
||||
assert!(a > 0 && (0..3).all(|c| u32::from(p[c]).abs_diff(want[c]) <= 2), "{name}: {p:?}, want {want:?}");
|
||||
assert_eq!(p[3], 255, "{name}: opaque");
|
||||
}
|
||||
}
|
||||
|
||||
/// A thumbnail with a background shows a Screen glow on the bare page, opaque (#6125).
|
||||
#[test]
|
||||
fn a_screen_outer_glow_is_in_a_thumbnail_on_a_background() {
|
||||
let d = glowing_square(Color::WHITE, "stylize.outerGlow", json!({}));
|
||||
let n = d.layers[0].children().unwrap()[0].clone();
|
||||
let img = Renderer::new().render_node_thumbnail(&d, &n, 100, Some([255; 4])).unwrap();
|
||||
let pixels = img.pixels.as_chunks::<4>().0;
|
||||
assert!(pixels.iter().all(|p| p[3] == 255), "every pixel is opaque");
|
||||
assert!(pixels.iter().any(|p| p[0] > 235 && p[1] > 235 && p[2] < 235), "the glow shows as yellow on white");
|
||||
}
|
||||
|
||||
/// The ink planes put the background under the art too: on a white background a Screen glow on
|
||||
/// the bare page prints yellow ink, and the bare page around it prints none (#6125).
|
||||
#[test]
|
||||
fn a_screen_outer_glow_prints_ink_on_a_background() {
|
||||
let d = glowing_square(Color::WHITE, "stylize.outerGlow", json!({}));
|
||||
let opts = RenderOptions { background: Some([255; 4]), ..Default::default() };
|
||||
let inks = Renderer::new().render_region_inks(&d, Rect::new(0.0, 0.0, 100.0, 100.0), 1.0, &opts);
|
||||
let at = |x: usize, y: usize| &inks[(y * 100 + x) * 4..][..4];
|
||||
assert!(at(27, 50)[2] > 10, "yellow ink in the glow: {:?}", at(27, 50));
|
||||
assert_eq!(at(5, 50), [0; 4], "no ink on the bare page");
|
||||
}
|
||||
|
||||
/// An Inner Glow blends with the object's own fill whatever is under the page: white Screen
|
||||
/// lightens a black square, red Screen leaves a white one white, and red Normal shows on it.
|
||||
#[test]
|
||||
fn an_inner_glow_blends_with_its_own_fill_on_a_background() {
|
||||
let red = |mode: &str| json!({"color": "#ff0000", "mode": mode, "opacity": 100});
|
||||
let dark = render(&glowing_square(Color::BLACK, "stylize.innerGlow", json!({"color": "#ffffff", "opacity": 100}))).pixel(32, 50);
|
||||
assert!(dark[0] > 20 && dark[0] == dark[1], "the white glow lightens the black edge: {dark:?}");
|
||||
assert_eq!(render(&glowing_square(Color::WHITE, "stylize.innerGlow", red("screen"))).pixel(32, 50), [255, 255, 255, 255]);
|
||||
let normal = render(&glowing_square(Color::WHITE, "stylize.innerGlow", red("normal"))).pixel(32, 50);
|
||||
assert!(normal[0] == 255 && normal[1] < 245, "red on the white edge: {normal:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_strokes_own_glow_paints_around_that_stroke_only() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
|
||||
+2
-2
@@ -779,7 +779,7 @@ point (72 times that value in ppi); `options.ppi` wins over either scale option.
|
||||
default 72 ppi still use 72 ppi; use `--ppi 300` for print output from those documents.
|
||||
|
||||
Raster exports take more options in `options`: `ppi` (72, 150, 300…; stored in the file, except in GIF, WebP and
|
||||
Targa), `background` (`transparent`, `white`, `black` or `"#rrggbb"`; `#rrggbbaa` and `#rgba` are refused), `antiAlias` (`none`, `art`, `type`: text
|
||||
Targa), `background` (`transparent`, `white`, `black` or `"#rrggbb"`; `#rrggbbaa` and `#rgba` are refused; drawn under the finished art, so blend modes do not read it), `antiAlias` (`none`, `art`, `type`: text
|
||||
snapped to pixels) and, for PNG, `interlaced`. `width` or `height` sets the size of each file in pixels: each artboard
|
||||
(with `useArtboards`, each file), `rect` or art bounds gets the scale that makes it that wide or high, within 0.01–64
|
||||
pixels per point, and the file stores the resolution that results, as with `ppi`. `width` wins over `height`, and
|
||||
@@ -2872,7 +2872,7 @@ where the layout puts it.
|
||||
`object.flattenTransparency {ids?, preset?, …options}` turns transparent art into opaque art that looks the same, in
|
||||
one undo step. The targets split into groups of overlapping objects; groups without transparency stay as they are.
|
||||
Each other group becomes one group of atomic regions: paths filled with the flat colour the art showed there (over
|
||||
white; over nothing, keeping alpha, with `preserveAlpha: true`), plus one image where gradients, patterns, images,
|
||||
white, which blend modes do not read; over nothing, keeping alpha, with `preserveAlpha: true`), plus one image where gradients, patterns, images,
|
||||
opacity masks or raster effects reach (clipped to those regions with `clipComplexRegions`, a rectangle without).
|
||||
`preset` is `high`, `medium` (the default) or `low`; option keys (`balance` 0–100, `lineArtPpi`, `gradientPpi`,
|
||||
`textToOutlines`, `strokesToOutlines`, `clipComplexRegions`, `antiAlias`, `preserveAlpha`, `preserveOverprints`)
|
||||
|
||||
@@ -595,6 +595,8 @@ since 2026-10-01.
|
||||
|
||||
- **Effects in the art's bounds:** Export with Use Artboards off, Fit to Artwork Bounds, and the other exports of the whole art (EPS without artboards, Export for Screens' Full Document, Save for Web (Legacy) with Clip to Artboard off) measure each visible object as the export crops above do. The drop shadows and other effects of groups, layers, type and images are inside the image or the artboard. A Shaper group without effects of its own is measured by the art it shows, and the hidden members of a group with a drop shadow, a glow or another raster effect do not count unless it is a Shaper group or also has a geometry, Pathfinder, Crop Marks or 3D effect or a visible effect on its own fill or stroke. The SVG of an artboard keeps an object whose effects alone reach the artboard. Tests measure a group, type and an image with a drop shadow, export a group and type with Use Artboards off and fit an artboard to them.
|
||||
|
||||
- **Raster export backgrounds (#6125):** a raster export draws its background under the finished art, and blend modes read only the art below them, as on the canvas. An Outer Glow in Screen mode on the bare page shows in a PNG on white and in a JPEG. Flatten Transparency puts its white under the finished art the same way. Evidence: `tests::file::a_screen_outer_glow_is_in_raster_exports`, `tests::objectfx::a_screen_outer_glow_on_the_bare_page_shows_on_a_background`, `tests::knockout::page_knockout_and_isolated_blending`, `tests::knockout::no_blend_mode_reads_the_background`, `tests::cmykflatten::every_blend_mode_flattens_to_the_canvas_pixels_in_cmyk`.
|
||||
|
||||
- **Selected artboard deletion:** the Artboards panel menu, panel trash button and Properties use the same selected set; selecting all retains artboard 1. Pointer-event tests cover nonadjacent and single selections, the original first-board survivor, ordinary artwork, Undo/Redo and the single-board guard. Public reference: [Delete artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html).
|
||||
- **Delete Empty Artboards:** the Artboards panel menu item ([Delete artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html)) runs `artboard.deleteEmpty`: every unlocked artboard with no artwork goes with its guides in one undo step, artboard 1 stays when every artboard is empty and none is locked, and the panel makes an artboard that is left active; an artboard is empty when no object (hidden, locked and template art included; guides not) has visual bounds that meet or touch it, a group counting by its members and a clip group or clipping layer by its members inside the clip (our reading of "no artwork"; the pages give no finer rule). Engine tests in `tests/panelcmds.rs` (`delete_empty_artboards_*`) and the panel test `delete_empty_artboards_deletes_the_empty_ones_and_selects_one_left`.
|
||||
The menu item's enabled check measures nothing in a document with one artboard, and Save Gradient Preset's check reads the proxy's paint without measuring the object; both give the same answer as before. An artboard that no path's control points, grown by the farthest its strokes reach, can meet is found empty without measuring the path. With an open path of 200,000 anchors selected, the checks take under 0.01 ms instead of 21.9 ms and 17.2 ms in a release build; with a second, empty artboard, Delete Empty Artboards' check takes 0.85 ms instead of 42.1 ms. Tests `delete_empty_artboards_is_enabled_exactly_when_it_deletes_something`, `delete_empty_artboards_enabled_state_matches_the_command_on_varied_documents`, `empty_artboard_checks_match_the_visual_bounds_on_varied_strokes`, `the_empty_artboard_check_matches_measuring_every_artboard`, `the_save_check_matches_saving_with_and_without_a_gradient`, `the_unmeasured_proxy_paint_is_the_measured_one` and `the_unmeasured_proxy_paint_is_the_measured_one_on_varied_objects`.
|
||||
|
||||
Reference in New Issue
Block a user