mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3668 from Taha-Mahmoodi/fix/dither-followups
Gradients: dithered fills sample only what shows; review follow-ups to #3637
This commit is contained in:
@@ -553,11 +553,25 @@ impl GradientPaint {
|
||||
/// as `rgb` gives them, mixed as the renderer mixes them: [`Ramp`]). Dithered, each colour
|
||||
/// channel of pixel (column, row) gets [`dither_noise`] before it is rounded to a level.
|
||||
pub fn pixels(&self, bounds: Rect, area: Rect, cols: u16, rows: u16, rgb: &dyn Fn(&Color) -> [f32; 3]) -> impl Iterator<Item = [u8; 4]> + use<> {
|
||||
self.pixels_within(bounds, area, cols, rows, [0..cols, 0..rows], rgb)
|
||||
}
|
||||
/// [`Self::pixels`] for the cells in columns `cells[0]` and rows `cells[1]` of the grid only,
|
||||
/// row by row: the same pixels, noise and all, the whole grid has there.
|
||||
pub fn pixels_within(
|
||||
&self,
|
||||
bounds: Rect,
|
||||
area: Rect,
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
cells: [std::ops::Range<u16>; 2],
|
||||
rgb: &dyn Fn(&Color) -> [f32; 3],
|
||||
) -> impl Iterator<Item = [u8; 4]> + use<> {
|
||||
let (kind, geom, dither) = (self.gradient.kind, self.resolve(bounds), self.gradient.dither);
|
||||
let ramp = Ramp::new(&self.gradient, rgb);
|
||||
let (cw, ch) = (area.width() / f64::from(cols.max(1)), area.height() / f64::from(rows.max(1)));
|
||||
let (x0, y0) = (area.x0, area.y0);
|
||||
(0..u32::from(rows)).flat_map(move |j| (0..u32::from(cols)).map(move |i| (i, j))).map(move |(i, j)| {
|
||||
let [across, down] = cells.map(|r| u32::from(r.start)..u32::from(r.end));
|
||||
down.flat_map(move |j| across.clone().map(move |i| (i, j))).map(move |(i, j)| {
|
||||
let p = Point::new(x0 + (f64::from(i) + 0.5) * cw, y0 + (f64::from(j) + 0.5) * ch);
|
||||
let c = ramp.at(geom.param_at(kind, p) as f32);
|
||||
let level = |k: usize| {
|
||||
|
||||
@@ -378,6 +378,18 @@ impl Paint {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// This stroke paint as the fill of the stroke's outline, so the outline looks as the stroke
|
||||
/// did: without dither, which strokes ignore ([`GradientPaint::draws_as_pixels`]).
|
||||
pub fn stroke_as_fill(&self) -> Paint {
|
||||
match self {
|
||||
Paint::Gradient(g) if g.gradient.dither => {
|
||||
let mut g = g.clone();
|
||||
g.gradient.dither = false;
|
||||
Paint::Gradient(g)
|
||||
}
|
||||
p => p.clone(),
|
||||
}
|
||||
}
|
||||
/// The Registration colour: 100% of every ink, linked to [`swatch::REGISTRATION`].
|
||||
pub fn registration() -> Self {
|
||||
Paint::Solid { color: Color::cmyk(1.0, 1.0, 1.0, 1.0), swatch: Some(swatch::REGISTRATION.into()), tint: 1.0 }
|
||||
|
||||
@@ -565,7 +565,7 @@ pub fn bake_appearance(n: &Node) -> Option<Node> {
|
||||
}
|
||||
None => {
|
||||
let effects = st.effects.clone();
|
||||
let fill = FillLayer { opacity: st.opacity, blend: st.blend, effects, ..FillLayer::new(st.paint.clone()) };
|
||||
let fill = FillLayer { opacity: st.opacity, blend: st.blend, effects, ..FillLayer::new(st.paint.stroke_as_fill()) };
|
||||
piece(outline, FillRule::NonZero, AppearanceItem::Fill(fill))
|
||||
}
|
||||
}
|
||||
@@ -673,6 +673,22 @@ mod tests {
|
||||
assert_eq!(f.effects.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), ["stylize.dropShadow"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_baked_stroke_leaves_dither_behind() {
|
||||
// Strokes ignore dither, so the filled outline goes without it and looks as the stroke did.
|
||||
let mut n = Node::path(NodeId(50), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default_art());
|
||||
if let AppearanceItem::Stroke(s) = &mut n.appearance.items[1] {
|
||||
let mut g = vectorcraft_color::GradientPaint::new(Default::default());
|
||||
g.gradient.dither = true;
|
||||
s.paint = Paint::Gradient(Box::new(g));
|
||||
}
|
||||
let m = bake_appearance(&n).unwrap();
|
||||
let ch = m.children().unwrap();
|
||||
let Some(AppearanceItem::Fill(f)) = ch[1].appearance.items.first() else { panic!("the outline is filled") };
|
||||
let Paint::Gradient(g) = &f.paint else { panic!("a gradient fill") };
|
||||
assert!(!g.gradient.dither);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_baked_stroke_with_a_gradient_along_it_becomes_meshes_clipped_to_its_outline() {
|
||||
let mut n = Node::path(NodeId(50), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default_art());
|
||||
|
||||
@@ -403,6 +403,10 @@ fn set_paint(s: &mut Session, p: &Value, fill: bool) -> Result<Value> {
|
||||
}
|
||||
check_keep_model(p, cmd)?;
|
||||
let paint = paint_from(s, p)?.ok_or_else(|| bad(cmd, "give color, none, swatch or gradient"))?;
|
||||
// As `paint.editGradient`: a `dither` given applies to gradient fills only.
|
||||
if !fill && matches!(&paint, Paint::Gradient(g) if g.gradient.dither) && p.get("swatch").is_none() {
|
||||
return Err(bad(cmd, "`dither` applies to gradient fills (paint.setFill)"));
|
||||
}
|
||||
apply_paint(s, p, paint, fill)
|
||||
}
|
||||
|
||||
|
||||
@@ -635,7 +635,7 @@ pub(crate) fn outlined_stroke(
|
||||
blend: st.blend,
|
||||
effects: st.effects.clone(),
|
||||
overprint: st.overprint,
|
||||
..vectorcraft_doc::appearance::FillLayer::new(st.paint.clone())
|
||||
..vectorcraft_doc::appearance::FillLayer::new(st.paint.stroke_as_fill())
|
||||
};
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Fill(fill)], ..Default::default() }.into();
|
||||
Some(n)
|
||||
|
||||
@@ -440,7 +440,29 @@ fn dither_is_a_fill_gradient_option_that_saves_and_loads() {
|
||||
s.execute("paint.setStroke", &json!({"gradient": {}})).unwrap();
|
||||
let before = node(&s, id).appearance.clone();
|
||||
assert!(s.execute("paint.editGradient", &json!({"dither": true, "stroke": true})).is_err());
|
||||
assert!(s.execute("paint.setStroke", &json!({"gradient": {"dither": true}})).is_err());
|
||||
assert!(s.execute("paint.editGradient", &json!({"dither": "yes"})).is_err());
|
||||
assert!(s.execute("paint.setFill", &json!({"gradient": {"dither": 1}})).is_err());
|
||||
assert_eq!(node(&s, id).appearance, before);
|
||||
}
|
||||
|
||||
/// A stroke ignores dither, so one that holds it anyway (swapped from a fill) outlines to a fill
|
||||
/// without it: Outline Stroke keeps the look.
|
||||
#[test]
|
||||
fn an_outlined_stroke_leaves_dither_behind() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
s.execute("paint.setStroke", &json!({"gradient": {}})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"gradient": {"dither": true}})).unwrap();
|
||||
s.execute("paint.swap", &json!({})).unwrap();
|
||||
let Paint::Gradient(st) = node(&s, id).appearance.stroke_paint() else { panic!("a gradient stroke") };
|
||||
assert!(st.gradient.dither, "the swapped stroke keeps the option");
|
||||
s.execute("object.path.outlineStroke", &json!({})).unwrap();
|
||||
let mut fills = vec![];
|
||||
s.doc().unwrap().doc.walk(|n| {
|
||||
if let Paint::Gradient(g) = n.appearance.fill_paint() {
|
||||
fills.push(g.gradient.dither);
|
||||
}
|
||||
});
|
||||
assert_eq!(fills, [false, false], "the fill and the outline, neither dithered");
|
||||
}
|
||||
|
||||
@@ -602,7 +602,8 @@ pub(crate) struct Exporter<'a> {
|
||||
pub overprint: bool,
|
||||
/// An overprinting fill or stroke was drawn so.
|
||||
pub overprinted: bool,
|
||||
/// Characters with neither fill nor stroke were written as text in forms marked invisible.
|
||||
/// Characters with neither fill nor stroke, or whose fill is drawn as art (outlines filled
|
||||
/// with an image or pattern), were written as text in forms marked invisible.
|
||||
pub invisible: bool,
|
||||
/// Inline graphics being drawn inside inline graphics (text in a symbol shown inline in
|
||||
/// text…): deeper ones are left out.
|
||||
@@ -1336,13 +1337,17 @@ impl Exporter<'_> {
|
||||
}
|
||||
|
||||
/// Draw gradient `g`, placed on `bounds`, as an image over `area`, sampled at the document's
|
||||
/// raster effects resolution: a freeform gradient's colour field (at most [`MAX_FIELD_PX`] a
|
||||
/// side), or a linear or radial gradient (at most [`MAX_GRADIENT_PX`] a side).
|
||||
/// raster effects resolution where it is drawn (`area` as the current transform sizes it, so
|
||||
/// scaled type gets its pixels too): a freeform gradient's colour field (at most
|
||||
/// [`MAX_FIELD_PX`] a side), or a linear or radial gradient (at most [`MAX_GRADIENT_PX`] a side).
|
||||
fn field_image(&mut self, s: &mut Surface, g: &vectorcraft_color::GradientPaint, bounds: Rect, area: Rect) {
|
||||
use vectorcraft_color::freeform::{painted_box, spread_scale};
|
||||
let Some(b) = painted_box(bounds) else { return };
|
||||
let area = area.abs();
|
||||
let long = area.width().max(area.height());
|
||||
// The area's sides in points on the page.
|
||||
let [m0, m1, m2, m3, _, _] = ctm(s).as_coeffs();
|
||||
let (w, h) = (area.width() * m0.hypot(m1), area.height() * m2.hypot(m3));
|
||||
let long = w.max(h);
|
||||
if !(long > 1e-9 && long.is_finite()) {
|
||||
return;
|
||||
}
|
||||
@@ -1350,7 +1355,7 @@ impl Exporter<'_> {
|
||||
let max = if freeform { MAX_FIELD_PX } else { MAX_GRADIENT_PX };
|
||||
let k = (self.doc.raster_effects_ppi / 72.0).min(max / long);
|
||||
let along = |len: f64| (len * k).ceil().clamp(1.0, max) as u16;
|
||||
let (cols, rows) = (along(area.width()), along(area.height()));
|
||||
let (cols, rows) = (along(w), along(h));
|
||||
let rgba: Vec<u8> = if freeform {
|
||||
let field = g.freeform_on(b).field_with(spread_scale(b), &|c| c.to_rgb());
|
||||
field.grid(area, cols, rows).flat_map(|(c, a)| [q(c[0]), q(c[1]), q(c[2]), q(a)]).collect()
|
||||
@@ -1588,9 +1593,12 @@ impl Exporter<'_> {
|
||||
}
|
||||
s.pop();
|
||||
});
|
||||
// Characters with neither fill nor stroke (a scanned page's recognized text) are still
|
||||
// text: written invisible (rendering mode 3), so they can still be selected and searched.
|
||||
if let Some(text) = plain.as_deref().filter(|_| fill.is_none() && !run.style.has_stroke()) {
|
||||
// Characters with neither fill nor stroke (a scanned page's recognized text), or whose
|
||||
// fill is drawn as art (a dithered gradient's image, a pattern: outlines, not text),
|
||||
// are still text: written invisible (rendering mode 3), so they can still be selected
|
||||
// and searched.
|
||||
let unpainted = fill.is_none() && !run.style.has_stroke();
|
||||
if let Some(text) = plain.as_deref().filter(|_| unpainted || self.area_paint(fill, true)) {
|
||||
self.invisible = true;
|
||||
crate::forms::form(s, Mark::Invisible, |s| {
|
||||
s.push_transform(&text_space);
|
||||
|
||||
@@ -573,3 +573,25 @@ fn evenly_tracked_type_on_a_path_keeps_its_length() {
|
||||
assert!(g.distance(*w) < 0.3, "{got:?}\nvs {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type filled with a dithered gradient is an image clipped to its outlines, sampled at the raster
|
||||
/// effects resolution on the page however the type is scaled, with its characters under it as
|
||||
/// invisible text (rendering mode 3), so they can still be selected and searched.
|
||||
#[test]
|
||||
fn dithered_type_keeps_its_resolution_when_scaled_and_stays_searchable() {
|
||||
let fill = Paint::Gradient(Box::new(GradientPaint::new(Gradient { dither: true, ..Default::default() })));
|
||||
let big = TextObject::point(Point::new(20.0, 80.0), "Dither", CharStyle { fill: fill.clone(), ..style(40.0) });
|
||||
let mut scaled = TextObject::point(Point::new(5.0, 20.0), "Dither", CharStyle { fill, ..style(10.0) });
|
||||
scaled.xf = Affine::scale(4.0) * scaled.xf;
|
||||
let width = |bytes: &[u8]| -> u32 {
|
||||
let s = String::from_utf8_lossy(bytes);
|
||||
s.split("/Width ").nth(1).and_then(|w| w.split(|c: char| !c.is_ascii_digit()).next()).and_then(|n| n.parse().ok()).unwrap()
|
||||
};
|
||||
let (big, scaled) = (pdf(&doc(vec![big]), false).bytes, pdf(&doc(vec![scaled]), false).bytes);
|
||||
let (wb, ws) = (width(&big), width(&scaled));
|
||||
// At 72 ppi a pixel a point: 40 pt "Dither" is about 100 points wide.
|
||||
assert!(wb >= 80 && wb.abs_diff(ws) <= 2, "40 pt type {wb} pixels wide, 10 pt type scaled 4 times {ws}");
|
||||
let ops = String::from_utf8_lossy(&scaled).split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
assert!(ops.contains("3 Tr"), "the characters as invisible text");
|
||||
assert_eq!(texts(&import_as(&scaled, TextAs::Text)), ["Dither"]);
|
||||
}
|
||||
|
||||
+56
-13
@@ -5,10 +5,13 @@
|
||||
//! the grid and a zoom rarely misses), each colour channel given fine noise of up to a level
|
||||
//! ([`vectorcraft_color::gradient::dither_noise`]) before it is rounded, and drawn as an image
|
||||
//! paint that the fill clips. Past [`MAX_CELLS`] a side (a box larger than that on screen), cells
|
||||
//! grow beyond a pixel and are mixed bilinearly, so the gradient stays smooth.
|
||||
//! grow beyond a pixel and are mixed bilinearly, so the gradient stays smooth. Only the cells the
|
||||
//! render target can show are sampled ([`window`]), so a large box mostly off-screen costs what
|
||||
//! its visible part does.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_color::GradientPaint;
|
||||
@@ -24,11 +27,23 @@ use crate::lru::{self, FrameCache};
|
||||
/// Most cells along a side of a dithered gradient's grid (64 MiB for a square grid).
|
||||
pub(crate) const MAX_CELLS: f64 = 4096.0;
|
||||
|
||||
/// Cells along a side of the part of a grid that is sampled snap out to multiples of this
|
||||
/// ([`window`]), so a pan within them keeps the sampled part.
|
||||
const TILE: f64 = 256.0;
|
||||
|
||||
thread_local! {
|
||||
/// Grids by [`key`], within [`lru::DITHER_BUDGET`], least recently used first out.
|
||||
static GRIDS: RefCell<FrameCache<u64, Arc<Pixmap>>> = RefCell::new(FrameCache::new(lru::DITHER_BUDGET, usize::MAX));
|
||||
}
|
||||
|
||||
/// Bytes of the grids this thread keeps, after `f` runs on an empty cache.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn grid_bytes_after(f: impl FnOnce()) -> usize {
|
||||
GRIDS.with(|c| c.borrow_mut().clear());
|
||||
f();
|
||||
GRIDS.with(|c| c.borrow().bytes())
|
||||
}
|
||||
|
||||
/// Cells along a side `px` device pixels long: at least one per pixel, rounded up to a
|
||||
/// quarter-octave step, 1 to [`MAX_CELLS`].
|
||||
fn cells(px: f64) -> u16 {
|
||||
@@ -36,9 +51,10 @@ fn cells(px: f64) -> u16 {
|
||||
2f64.powf(steps / 4.0).ceil().clamp(1.0, MAX_CELLS) as u16
|
||||
}
|
||||
|
||||
/// Cache key of gradient `g` placed on box `b` as a `cols` × `rows` grid painted with `ink` (and
|
||||
/// the active colour settings, which turn colours into display RGB or inks).
|
||||
fn key(g: &GradientPaint, b: Rect, cols: u16, rows: u16, ink: Ink) -> u64 {
|
||||
/// Cache key of gradient `g` placed on box `b` as a `cols` × `rows` grid, cells `win` of it,
|
||||
/// painted with `ink` (and the active colour settings, which turn colours into display RGB or
|
||||
/// inks).
|
||||
fn key(g: &GradientPaint, b: Rect, cols: u16, rows: u16, win: &[Range<u16>; 2], ink: Ink) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
let gr = &g.gradient;
|
||||
(gr.kind as u8, gr.dither).hash(&mut h);
|
||||
@@ -50,24 +66,47 @@ fn key(g: &GradientPaint, b: Rect, cols: u16, rows: u16, ink: Ink) -> u64 {
|
||||
(geom.start.x.to_bits(), geom.start.y.to_bits(), geom.end.x.to_bits(), geom.end.y.to_bits(), geom.aspect.to_bits()).hash(&mut h);
|
||||
geom.focal.map(|f| (f.x.to_bits(), f.y.to_bits())).hash(&mut h);
|
||||
(b.x0.to_bits(), b.y0.to_bits(), b.x1.to_bits(), b.y1.to_bits(), cols, rows).hash(&mut h);
|
||||
win.hash(&mut h);
|
||||
(Arc::as_ptr(&vectorcraft_color::cms::active()) as usize, ink).hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// `g` placed on `b` as a premultiplied `cols` × `rows` pixmap, colours as `ink` paints them.
|
||||
pub(crate) fn rasterize(g: &GradientPaint, b: Rect, cols: u16, rows: u16, ink: Ink) -> Pixmap {
|
||||
/// The cells of a `cols` × `rows` grid over box `b` that a `w` × `h` render target, which
|
||||
/// transform `xf` maps the box onto, can show: (columns, rows), a cell more each way for the
|
||||
/// bilinear mix, out to whole [`TILE`]s. The whole grid where `xf` has no inverse; None where
|
||||
/// no cell shows.
|
||||
fn window(xf: Affine, w: u16, h: u16, b: Rect, cols: u16, rows: u16) -> Option<[Range<u16>; 2]> {
|
||||
let seen = xf.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
|
||||
let span = |lo: f64, hi: f64, start: f64, len: f64, n: u16| {
|
||||
let cell = len / f64::from(n);
|
||||
let (first, last) = (((lo - start) / cell).floor() - 1.0, ((hi - start) / cell).ceil() + 1.0);
|
||||
if !(first.is_finite() && last.is_finite()) {
|
||||
return Some(0..n);
|
||||
}
|
||||
let a = ((first / TILE).floor() * TILE).clamp(0.0, f64::from(n));
|
||||
let z = ((last / TILE).ceil() * TILE).clamp(0.0, f64::from(n));
|
||||
(a < z).then_some(a as u16..z as u16)
|
||||
};
|
||||
Some([span(seen.x0, seen.x1, b.x0, b.width(), cols)?, span(seen.y0, seen.y1, b.y0, b.height(), rows)?])
|
||||
}
|
||||
|
||||
/// Cells `win` (columns, rows) of `g` placed on `b` as a `cols` × `rows` grid, as a
|
||||
/// premultiplied pixmap, colours as `ink` paints them.
|
||||
pub(crate) fn rasterize(g: &GradientPaint, b: Rect, cols: u16, rows: u16, win: [Range<u16>; 2], ink: Ink) -> Pixmap {
|
||||
let (w, h) = (win[0].len() as u16, win[1].len() as u16);
|
||||
let data = g
|
||||
.pixels(b, b, cols, rows, &|c| ink.rgb(c))
|
||||
.pixels_within(b, b, cols, rows, win, &|c| ink.rgb(c))
|
||||
.map(|[r, gr, bl, a]| {
|
||||
let pm = |v: u8| ((u16::from(v) * u16::from(a) + 127) / 255) as u8;
|
||||
vello_cpu::color::PremulRgba8 { r: pm(r), g: pm(gr), b: pm(bl), a }
|
||||
})
|
||||
.collect();
|
||||
Pixmap::from_parts(data, cols, rows)
|
||||
Pixmap::from_parts(data, w, h)
|
||||
}
|
||||
|
||||
/// Set dithered gradient `g` on box `bounds` (in the paint's space, which the context's current
|
||||
/// transform maps to pixels) as the context paint. Returns false for an empty box.
|
||||
/// transform maps to pixels) as the context paint. Returns false for an empty box or one off the
|
||||
/// render target.
|
||||
pub(crate) fn set_dithered_paint(ctx: &mut RenderContext, g: &GradientPaint, bounds: Rect, ink: Ink) -> bool {
|
||||
let Some(b) = painted_box(bounds) else { return false };
|
||||
// Device pixels per unit along the box's sides.
|
||||
@@ -78,7 +117,9 @@ pub(crate) fn set_dithered_paint(ctx: &mut RenderContext, g: &GradientPaint, bou
|
||||
return false;
|
||||
}
|
||||
let (cols, rows) = (cells(wpx), cells(hpx));
|
||||
let k = key(g, b, cols, rows, ink);
|
||||
let Some(win) = window(*ctx.transform(), ctx.width(), ctx.height(), b, cols, rows) else { return false };
|
||||
let (first_col, first_row) = (win[0].start, win[1].start);
|
||||
let k = key(g, b, cols, rows, &win, ink);
|
||||
let pm = GRIDS
|
||||
.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
@@ -86,11 +127,11 @@ pub(crate) fn set_dithered_paint(ctx: &mut RenderContext, g: &GradientPaint, bou
|
||||
c.get(&k, t).cloned()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let pm = Arc::new(rasterize(g, b, cols, rows, ink));
|
||||
let pm = Arc::new(rasterize(g, b, cols, rows, win, ink));
|
||||
GRIDS.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
let t = c.tick();
|
||||
c.insert(k, pm.clone(), cols as usize * rows as usize * 4, t);
|
||||
c.insert(k, pm.clone(), usize::from(pm.width()) * usize::from(pm.height()) * 4, t);
|
||||
});
|
||||
pm
|
||||
});
|
||||
@@ -100,6 +141,8 @@ pub(crate) fn set_dithered_paint(ctx: &mut RenderContext, g: &GradientPaint, bou
|
||||
let quality = if fine { ImageQuality::Low } else { ImageQuality::Medium };
|
||||
let sampler = ImageSampler { x_extend: Extend::Pad, y_extend: Extend::Pad, quality, alpha: 1.0 };
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler });
|
||||
ctx.set_paint_transform(Affine::translate(b.origin().to_vec2()) * Affine::scale_non_uniform(b.width() / cols as f64, b.height() / rows as f64));
|
||||
let (cw, ch) = (b.width() / f64::from(cols), b.height() / f64::from(rows));
|
||||
let origin = (b.x0 + f64::from(first_col) * cw, b.y0 + f64::from(first_row) * ch);
|
||||
ctx.set_paint_transform(Affine::translate(origin) * Affine::scale_non_uniform(cw, ch));
|
||||
true
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ pub(crate) fn set_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Fr
|
||||
}
|
||||
}
|
||||
|
||||
/// [`set_paint`] for a fill: a dithered gradient (dither applies to fills only) is drawn as
|
||||
/// pixels with fine noise ([`crate::dither`]).
|
||||
/// [`set_paint`] for a fill: a dithered gradient (dither applies to fills only) with stops is
|
||||
/// drawn as pixels with fine noise ([`crate::dither`]).
|
||||
pub(crate) fn set_fill_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Frame) -> bool {
|
||||
match p {
|
||||
Paint::Gradient(g) if g.gradient.dither && g.gradient.kind != GradientKind::Freeform => {
|
||||
Paint::Gradient(g) if g.gradient.dither && g.gradient.kind != GradientKind::Freeform && !g.gradient.stops.is_empty() => {
|
||||
crate::dither::set_dithered_paint(ctx, g, bounds, f.ink)
|
||||
}
|
||||
_ => set_paint(ctx, p, bounds, f),
|
||||
|
||||
@@ -98,3 +98,38 @@ fn a_zoomed_or_rotated_dithered_gradient_keeps_its_colours() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_dithered_box_mostly_off_screen_samples_only_what_shows() {
|
||||
// A 2000-unit square, the shallow ramp across it: a 2048-cell grid a side, 16 MiB.
|
||||
const S: u32 = 2000;
|
||||
let mut d = Document::new(f64::from(S), f64::from(S));
|
||||
let id = d.alloc_id();
|
||||
let n = Node::path(
|
||||
id,
|
||||
shapes::rectangle(Rect::new(0.0, 0.0, f64::from(S), f64::from(S))),
|
||||
Appearance::basic(shallow(GradientKind::Linear, true), Paint::None, 0.0),
|
||||
);
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let whole = Renderer::new().render(&d, S, S, Affine::IDENTITY, &opts);
|
||||
// A 64-pixel view of its middle draws the pixels the whole render has there...
|
||||
let mut part = None;
|
||||
let bytes = dither::grid_bytes_after(|| part = Some(Renderer::new().render(&d, 64, 64, Affine::translate((-1000.0, -1000.0)), &opts)));
|
||||
let part = part.unwrap();
|
||||
assert!((0..64).all(|y| (0..64).all(|x| part.pixel(x, y) == whole.pixel(1000 + x, 1000 + y))), "the view draws what the whole render does");
|
||||
// ...from the cells around it only, not the whole grid.
|
||||
assert!(bytes > 0 && bytes <= 512 * 512 * 4, "{bytes} bytes of grid for a 64-pixel view");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dithered_gradient_without_stops_draws_nothing_and_samples_nothing() {
|
||||
let mut g = Gradient::new(GradientKind::Linear, Vec::new());
|
||||
g.dither = true;
|
||||
let d = doc(Paint::Gradient(Box::new(GradientPaint::new(g))), Paint::None);
|
||||
let mut r = None;
|
||||
let bytes = dither::grid_bytes_after(|| r = Some(render(&d, Affine::IDENTITY)));
|
||||
assert_eq!(bytes, 0, "no grid is sampled");
|
||||
let r = r.unwrap();
|
||||
assert!((0..H).all(|y| (0..W).all(|x| r.pixel(x, y) == [255, 255, 255, 255])), "nothing is drawn");
|
||||
}
|
||||
|
||||
@@ -5386,4 +5386,5 @@ axis Y Axis Y ос
|
||||
Dither Дитъринг
|
||||
Fine noise hides banding in the gradient fill Фин шум скрива ивиците в градиентната запълка
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` се прилага към градиентни запълки (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` се прилага към градиентни запълки (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` трябва да бъде true или false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5431,4 +5431,5 @@ axis Y Axis osa Y
|
||||
Dither Rozptýlení
|
||||
Fine noise hides banding in the gradient fill Jemný šum skryje pruhy ve výplni přechodem
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` platí pro výplně přechodem (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` platí pro výplně přechodem (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` musí být true nebo false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5402,4 +5402,5 @@ axis Y Axis Y-akse
|
||||
Dither Dither
|
||||
Fine noise hides banding in the gradient fill Fin støj skjuler striber i gradientfyldet
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` gælder for gradientfyld (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` gælder for gradientfyld (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` skal være true eller false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5013,4 +5013,5 @@ axis Y Axis Y-Achse
|
||||
Dither Dithering
|
||||
Fine noise hides banding in the gradient fill Feines Rauschen verbirgt Streifen in der Verlaufsfläche
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` gilt für Verlaufsflächen (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` gilt für Verlaufsflächen (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` muss true oder false sein
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5383,4 +5383,5 @@ axis Y Axis Άξονας Y
|
||||
Dither Πρόσθεση θορύβου
|
||||
Fine noise hides banding in the gradient fill Ο λεπτός θόρυβος κρύβει τις λωρίδες στο γέμισμα ντεγκραντέ
|
||||
@msg `dither` applies to gradient fills (stroke: false) Το `dither` ισχύει για γεμίσματα ντεγκραντέ (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) Το `dither` ισχύει για γεμίσματα ντεγκραντέ (paint.setFill)
|
||||
@msg `dither` must be true or false Το `dither` πρέπει να είναι true ή false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2045 and column 19.
|
@@ -5373,4 +5373,5 @@ axis Y Axis Eje Y
|
||||
Dither Tramado
|
||||
Fine noise hides banding in the gradient fill Un ruido fino oculta las bandas del relleno de degradado
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` se aplica a rellenos de degradado (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` se aplica a rellenos de degradado (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` debe ser true o false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5462,4 +5462,5 @@ axis Y Axis Y-akseli
|
||||
Dither Rasterointi
|
||||
Fine noise hides banding in the gradient fill Hieno kohina piilottaa raidat liukuväritäytöstä
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` koskee liukuväritäyttöjä (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` koskee liukuväritäyttöjä (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` on oltava true tai false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5430,4 +5430,5 @@ axis Y Axis Axe Y
|
||||
Dither Simulation
|
||||
Fine noise hides banding in the gradient fill Un bruit fin masque les bandes du fond en dégradé
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` s’applique aux fonds en dégradé (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` s’applique aux fonds en dégradé (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` doit valoir true ou false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5417,4 +5417,5 @@ axis Y Axis Y tengely
|
||||
Dither Árnyalatkeverés
|
||||
Fine noise hides banding in the gradient fill A finom zaj elrejti a sávokat a színátmenetes kitöltésben
|
||||
@msg `dither` applies to gradient fills (stroke: false) A `dither` színátmenetes kitöltésekre vonatkozik (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) A `dither` színátmenetes kitöltésekre vonatkozik (paint.setFill)
|
||||
@msg `dither` must be true or false `dither`: true vagy false kell
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5402,4 +5402,5 @@ axis Y Axis Sumbu Y
|
||||
Dither Dither
|
||||
Fine noise hides banding in the gradient fill Derau halus menyembunyikan pita warna pada isian gradien
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` berlaku untuk isian gradien (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` berlaku untuk isian gradien (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` harus true atau false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3816 and column 19.
|
@@ -5047,4 +5047,5 @@ axis Y Axis Asse Y
|
||||
Dither Dithering
|
||||
Fine noise hides banding in the gradient fill Un rumore fine nasconde le bande nel riempimento sfumato
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` si applica ai riempimenti sfumati (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` si applica ai riempimenti sfumati (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` deve essere true o false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5109,4 +5109,5 @@ axis Y Axis Y 軸
|
||||
Dither ディザ
|
||||
Fine noise hides banding in the gradient fill 細かいノイズでグラデーションの塗りの縞を目立たなくします
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` はグラデーションの塗りに適用されます (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` はグラデーションの塗りに適用されます (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` には true または false を指定してください
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5347,4 +5347,5 @@ axis Y Axis Y ღერძი
|
||||
Dither დიზერინგი
|
||||
Fine noise hides banding in the gradient fill წვრილი ხმაური მალავს ზოლებს გრადიენტულ შევსებაში
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` ვრცელდება გრადიენტულ შევსებებზე (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` ვრცელდება გრადიენტულ შევსებებზე (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` უნდა იყოს true ან false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5010,4 +5010,5 @@ axis Y Axis Y축
|
||||
Dither 디더
|
||||
Fine noise hides banding in the gradient fill 미세한 노이즈로 그레이디언트 칠의 띠를 감춥니다
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither`는 그레이디언트 칠에 적용됩니다 (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither`는 그레이디언트 칠에 적용됩니다 (paint.setFill)
|
||||
@msg `dither` must be true or false `dither`은(는) true 또는 false여야 합니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5445,4 +5445,5 @@ axis Y Axis Y-as
|
||||
Dither Dither
|
||||
Fine noise hides banding in the gradient fill Fijne ruis verbergt banden in de verloopvulling
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` geldt voor verloopvullingen (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` geldt voor verloopvullingen (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` moet true of false zijn
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5283,4 +5283,5 @@ axis Y Axis Oś Y
|
||||
Dither Rozpraszanie
|
||||
Fine noise hides banding in the gradient fill Drobny szum ukrywa pasy w wypełnieniu gradientem
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` dotyczy wypełnień gradientem (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` dotyczy wypełnień gradientem (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` musi mieć wartość true lub false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5348,4 +5348,5 @@ axis Y Axis Eixo Y
|
||||
Dither Dither
|
||||
Fine noise hides banding in the gradient fill Um ruído fino esconde as faixas do preenchimento em gradiente
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` se aplica a preenchimentos em gradiente (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` se aplica a preenchimentos em gradiente (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` deve ser true ou false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -5043,4 +5043,5 @@ axis Y Axis Ось Y
|
||||
Dither Дизеринг
|
||||
Fine noise hides banding in the gradient fill Мелкий шум скрывает полосы в градиентной заливке
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` применяется к градиентным заливкам (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` применяется к градиентным заливкам (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` должно быть true или false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5387,4 +5387,5 @@ axis Y Axis Y-axel
|
||||
Dither Dithering
|
||||
Fine noise hides banding in the gradient fill Fint brus döljer bandningen i övertoningsfyllningen
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` gäller övertoningsfyllningar (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` gäller övertoningsfyllningar (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` måste vara true eller false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5395,4 +5395,5 @@ axis Y Axis Y ekseni
|
||||
Dither Titreme
|
||||
Fine noise hides banding in the gradient fill İnce gürültü, degrade dolgudaki bantları gizler
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` degrade dolgulara uygulanır (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` degrade dolgulara uygulanır (paint.setFill)
|
||||
@msg `dither` must be true or false "dither" doğru veya yanlış olmalıdır
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5027,4 +5027,5 @@ axis Y Axis Вісь Y
|
||||
Dither Дизеринг
|
||||
Fine noise hides banding in the gradient fill Дрібний шум приховує смуги в градієнтній заливці
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` застосовується до градієнтних заливок (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` застосовується до градієнтних заливок (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` має бути true або false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 733 and column 40.
|
@@ -5027,4 +5027,5 @@ axis Y Axis Trục Y
|
||||
Dither Dither
|
||||
Fine noise hides banding in the gradient fill Nhiễu mịn che dải màu trong vùng tô chuyển sắc
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` áp dụng cho vùng tô chuyển sắc (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` áp dụng cho vùng tô chuyển sắc (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` phải là true hoặc false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4028 and column 10.
|
@@ -5271,4 +5271,5 @@ axis Y Axis Y 轴
|
||||
Dither 仿色
|
||||
Fine noise hides banding in the gradient fill 细微杂色可隐藏渐变填色中的色带
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` 适用于渐变填色 (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` 适用于渐变填色 (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` 必须是 true 或 false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5066,4 +5066,5 @@ axis Y Axis Y 軸
|
||||
Dither 仿色
|
||||
Fine noise hides banding in the gradient fill 細微雜色可隱藏漸層填色中的色帶
|
||||
@msg `dither` applies to gradient fills (stroke: false) `dither` 適用於漸層填色 (stroke: false)
|
||||
@msg `dither` applies to gradient fills (paint.setFill) `dither` 適用於漸層填色 (paint.setFill)
|
||||
@msg `dither` must be true or false `dither` 必須是 true 或 false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
Reference in New Issue
Block a user