mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-10 12:29:34 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77374f4493 | ||
|
|
8505093e6b |
@@ -38,6 +38,8 @@ pub struct Imp<'a> {
|
|||||||
pub vml_previews: HashMap<String, HashMap<String, String>>,
|
pub vml_previews: HashMap<String, HashMap<String, String>>,
|
||||||
/// Aggregate byte count of retained chart sidecars across the presentation.
|
/// Aggregate byte count of retained chart sidecars across the presentation.
|
||||||
pub kept_sidecar_bytes: usize,
|
pub kept_sidecar_bytes: usize,
|
||||||
|
/// Data slots of the charts decoded so far (see `chart::MAX_PRESENTATION_CHART_POINTS`).
|
||||||
|
pub chart_points: usize,
|
||||||
/// Cached kept sidecars by normalized chart part name.
|
/// Cached kept sidecars by normalized chart part name.
|
||||||
pub kept_charts: HashMap<String, deckcraft_model::chart::ChartKept>,
|
pub kept_charts: HashMap<String, deckcraft_model::chart::ChartKept>,
|
||||||
/// Add-in tags parts, parsed once each, by normalized part name.
|
/// Add-in tags parts, parsed once each, by normalized part name.
|
||||||
@@ -442,6 +444,7 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<
|
|||||||
table_style_ids: Vec::new(),
|
table_style_ids: Vec::new(),
|
||||||
vml_previews: HashMap::new(),
|
vml_previews: HashMap::new(),
|
||||||
kept_sidecar_bytes: 0,
|
kept_sidecar_bytes: 0,
|
||||||
|
chart_points: 0,
|
||||||
kept_charts: HashMap::new(),
|
kept_charts: HashMap::new(),
|
||||||
tags_by_part: HashMap::new(),
|
tags_by_part: HashMap::new(),
|
||||||
tag_bytes: 0,
|
tag_bytes: 0,
|
||||||
@@ -483,6 +486,7 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
|
|||||||
table_style_ids: Vec::new(),
|
table_style_ids: Vec::new(),
|
||||||
vml_previews: HashMap::new(),
|
vml_previews: HashMap::new(),
|
||||||
kept_sidecar_bytes: 0,
|
kept_sidecar_bytes: 0,
|
||||||
|
chart_points: 0,
|
||||||
kept_charts: HashMap::new(),
|
kept_charts: HashMap::new(),
|
||||||
tags_by_part: HashMap::new(),
|
tags_by_part: HashMap::new(),
|
||||||
tag_bytes: 0,
|
tag_bytes: 0,
|
||||||
|
|||||||
@@ -39,6 +39,24 @@ pub(crate) fn rich_text(t: &El) -> Option<String> {
|
|||||||
/// Most category/value/size slots one chart may decode across all its series and caches, so
|
/// Most category/value/size slots one chart may decode across all its series and caches, so
|
||||||
/// sparse `ptCount`/`idx` declarations can't expand far beyond the XML they came from.
|
/// sparse `ptCount`/`idx` declarations can't expand far beyond the XML they came from.
|
||||||
pub(crate) const MAX_CHART_POINTS: usize = 1_000_000;
|
pub(crate) const MAX_CHART_POINTS: usize = 1_000_000;
|
||||||
|
/// Data slots (values and categories) the charts of one presentation may hold in all; a chart past
|
||||||
|
/// it is read as a placeholder frame. Each frame that references a chart decodes its own copy.
|
||||||
|
pub(crate) const MAX_PRESENTATION_CHART_POINTS: usize = 4_000_000;
|
||||||
|
|
||||||
|
/// Slots a decoded chart holds, counting the chart itself as at least one.
|
||||||
|
pub(crate) fn point_count(c: &Chart) -> usize {
|
||||||
|
let series = |s: &deckcraft_model::chart::Series| {
|
||||||
|
let bars = s.error_bars.as_ref().map_or(0, |e| e.plus.len().saturating_add(e.minus.len()));
|
||||||
|
s.values.len().saturating_add(s.x_values.len()).saturating_add(s.sizes.len()).saturating_add(bars)
|
||||||
|
};
|
||||||
|
let levels: usize = c.category_levels.iter().map(Vec::len).sum();
|
||||||
|
1usize.saturating_add(c.categories.len()).saturating_add(levels).saturating_add(c.series.iter().map(series).fold(0, usize::saturating_add))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes of the strings a kept relationship owns.
|
||||||
|
fn rel_bytes(r: &deckcraft_model::chart::ChartRel) -> usize {
|
||||||
|
r.id.len() + r.rel_type.len() + r.target.len() + r.part.len()
|
||||||
|
}
|
||||||
|
|
||||||
/// Most slots one cache may hold.
|
/// Most slots one cache may hold.
|
||||||
const MAX_CACHE_POINTS: usize = 100_000;
|
const MAX_CACHE_POINTS: usize = 100_000;
|
||||||
@@ -466,6 +484,13 @@ pub(crate) fn capture_kept(imp: &mut super::Imp, chart_part: &str) -> deckcraft_
|
|||||||
|
|
||||||
let key = crate::opc::norm(chart_part);
|
let key = crate::opc::norm(chart_part);
|
||||||
if let Some(cached) = imp.kept_charts.get(&key) {
|
if let Some(cached) = imp.kept_charts.get(&key) {
|
||||||
|
// Every frame that shows this chart gets its own copy, so each copy is charged like the first.
|
||||||
|
let bytes: usize = cached.parts.iter().map(|p| p.bytes.len()).sum::<usize>() + cached.rels.iter().map(rel_bytes).sum::<usize>();
|
||||||
|
if imp.kept_sidecar_bytes.saturating_add(bytes) > MAX_KEPT_TOTAL_BYTES {
|
||||||
|
log::warn!("pptx: chart sidecar retained byte budget exceeded for {chart_part}");
|
||||||
|
return ChartKept::default();
|
||||||
|
}
|
||||||
|
imp.kept_sidecar_bytes += bytes;
|
||||||
return cached.clone();
|
return cached.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +510,13 @@ pub(crate) fn capture_kept(imp: &mut super::Imp, chart_part: &str) -> deckcraft_
|
|||||||
kept.rels.push(ChartRel { id: r.id, rel_type: r.ty, target: r.target_raw, part: r.target, external: false });
|
kept.rels.push(ChartRel { id: r.id, rel_type: r.ty, target: r.target_raw, part: r.target, external: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The first capture's relationship strings are retained too (the parts were charged as copied).
|
||||||
|
let rel_total: usize = kept.rels.iter().map(rel_bytes).sum();
|
||||||
|
if imp.kept_sidecar_bytes.saturating_add(rel_total) > MAX_KEPT_TOTAL_BYTES {
|
||||||
|
log::warn!("pptx: chart sidecar retained byte budget exceeded for {chart_part}");
|
||||||
|
return ChartKept::default();
|
||||||
|
}
|
||||||
|
imp.kept_sidecar_bytes += rel_total;
|
||||||
imp.kept_charts.insert(key, kept.clone());
|
imp.kept_charts.insert(key, kept.clone());
|
||||||
kept
|
kept
|
||||||
}
|
}
|
||||||
@@ -590,6 +622,16 @@ fn text_size(e: Option<&El>) -> Option<f64> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn point_count_charges_every_retained_vector() {
|
||||||
|
let mut c = Chart { categories: vec!["a".into()], category_levels: vec![vec!["x".into(); 2], vec!["y".into(); 3]], ..Default::default() };
|
||||||
|
let mut s =
|
||||||
|
deckcraft_model::chart::Series { values: vec![Some(1.0); 4], x_values: vec![None; 5], sizes: vec![None; 6], ..Default::default() };
|
||||||
|
s.error_bars = Some(deckcraft_model::chart::ErrorBars { plus: vec![None; 7], minus: vec![None; 8], ..Default::default() });
|
||||||
|
c.series = vec![s];
|
||||||
|
assert_eq!(point_count(&c), 1 + 1 + 5 + 4 + 5 + 6 + 15);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sparse_caches_share_one_chart_wide_budget() {
|
fn sparse_caches_share_one_chart_wide_budget() {
|
||||||
// Every series declares 100000 points but writes one: without a chart-wide budget each
|
// Every series declares 100000 points but writes one: without a chart-wide budget each
|
||||||
|
|||||||
@@ -428,10 +428,14 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
if let Some(c) = gd.child("chart")
|
if let Some(c) = gd.child("chart")
|
||||||
|
&& imp.chart_points < super::chart::MAX_PRESENTATION_CHART_POINTS
|
||||||
&& let Some(rel) = part.rels.get(c.attr("r:id").unwrap_or(""))
|
&& let Some(rel) = part.rels.get(c.attr("r:id").unwrap_or(""))
|
||||||
&& let Some(bytes) = imp.pkg.xml_bytes(&rel.target)
|
&& let Some(bytes) = imp.pkg.xml_bytes(&rel.target)
|
||||||
&& let Some(mut chart) = if uri.ends_with("/chartex") { super::read_chartex_xml(bytes) } else { super::read_chart_xml(bytes) }
|
&& let Some(mut chart) = if uri.ends_with("/chartex") { super::read_chartex_xml(bytes) } else { super::read_chart_xml(bytes) }
|
||||||
|
// A chart that would cross the presentation limit is read as the placeholder frame instead.
|
||||||
|
&& imp.chart_points.saturating_add(super::chart::point_count(&chart)) <= super::chart::MAX_PRESENTATION_CHART_POINTS
|
||||||
{
|
{
|
||||||
|
imp.chart_points = imp.chart_points.saturating_add(super::chart::point_count(&chart));
|
||||||
chart.kept = super::chart::capture_kept(imp, &rel.target);
|
chart.kept = super::chart::capture_kept(imp, &rel.target);
|
||||||
s.kind = ShapeKind::Chart(Box::new(chart));
|
s.kind = ShapeKind::Chart(Box::new(chart));
|
||||||
return s;
|
return s;
|
||||||
|
|||||||
@@ -125,9 +125,7 @@ pub fn draw(ctx: &mut RenderContext, bevel: &Bevel, fill: Option<Rgba>, path: &B
|
|||||||
let longest = bounds.width().max(bounds.height()) * k;
|
let longest = bounds.width().max(bounds.height()) * k;
|
||||||
let s = (MAX_SIDE / longest).min(1.0) * k;
|
let s = (MAX_SIDE / longest).min(1.0) * k;
|
||||||
let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 2048), ((bounds.height() * s).ceil() as u32).clamp(1, 2048));
|
let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 2048), ((bounds.height() * s).ceil() as u32).clamp(1, 2048));
|
||||||
// Each pixel tests every edge, so a dense outline is simplified to the raster's edge budget.
|
let polys = crate::pathgrad::polygons(path);
|
||||||
let pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph));
|
|
||||||
let polys = crate::pathgrad::simplify(crate::pathgrad::polygons(path), pixel, crate::pathgrad::edge_budget(pw, ph));
|
|
||||||
if polys.is_empty() {
|
if polys.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ use vello_cpu::{RenderContext, peniko};
|
|||||||
|
|
||||||
/// Nesting depth of clips and layers past which a glyph opens no more (hostile fonts).
|
/// Nesting depth of clips and layers past which a glyph opens no more (hostile fonts).
|
||||||
const MAX_DEPTH: usize = 64;
|
const MAX_DEPTH: usize = 64;
|
||||||
/// Fills and gradient stops one colour glyph may paint; the rest of a hostile glyph is skipped.
|
|
||||||
const MAX_WORK: usize = 100_000;
|
|
||||||
|
|
||||||
/// Paint `gid` of `face` as a colour glyph, its origin mapped by `at` (y down, points) at `scale`
|
/// Paint `gid` of `face` as a colour glyph, its origin mapped by `at` (y down, points) at `scale`
|
||||||
/// points per font unit. `fg` is the text colour (palette index 0xFFFF) and `alpha` fades it all.
|
/// points per font unit. `fg` is the text colour (palette index 0xFFFF) and `alpha` fades it all.
|
||||||
@@ -28,7 +26,7 @@ pub fn paint(ctx: &mut RenderContext, face: &FontFace, gid: u32, at: Affine, sca
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
// Font units are y up; the slide is y down.
|
// Font units are y up; the slide is y down.
|
||||||
let base = at * Affine::scale(scale) * Affine::scale_non_uniform(1.0, -1.0);
|
let base = at * Affine::scale(scale) * Affine::scale_non_uniform(1.0, -1.0);
|
||||||
let mut p = Painter { ctx, face, base, stack: vec![Affine::IDENTITY], palette, fg, opened: Vec::new(), work: 0 };
|
let mut p = Painter { ctx, face, base, stack: vec![Affine::IDENTITY], palette, fg, opened: Vec::new() };
|
||||||
p.ctx.push_layer(None, None, Some(alpha.clamp(0.0, 1.0) as f32), None, None);
|
p.ctx.push_layer(None, None, Some(alpha.clamp(0.0, 1.0) as f32), None, None);
|
||||||
let ok = glyph.paint(face.location(), &mut p).is_ok();
|
let ok = glyph.paint(face.location(), &mut p).is_ok();
|
||||||
// A malformed glyph may leave layers open: close them all.
|
// A malformed glyph may leave layers open: close them all.
|
||||||
@@ -52,8 +50,6 @@ struct Painter<'a> {
|
|||||||
/// Clips and composite groups pushed by the glyph, innermost last: true when a layer was
|
/// Clips and composite groups pushed by the glyph, innermost last: true when a layer was
|
||||||
/// opened on the context for it (false past `MAX_DEPTH`, so its pop is skipped too).
|
/// opened on the context for it (false past `MAX_DEPTH`, so its pop is skipped too).
|
||||||
opened: Vec<bool>,
|
opened: Vec<bool>,
|
||||||
/// Fills plus gradient stops painted so far, against [`MAX_WORK`].
|
|
||||||
work: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn affine(t: &Transform) -> Affine {
|
fn affine(t: &Transform) -> Affine {
|
||||||
@@ -178,16 +174,6 @@ impl ColorPainter for Painter<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fill(&mut self, brush: Brush<'_>) {
|
fn fill(&mut self, brush: Brush<'_>) {
|
||||||
let stops = match &brush {
|
|
||||||
Brush::Solid { .. } => 0,
|
|
||||||
Brush::LinearGradient { color_stops, .. } | Brush::RadialGradient { color_stops, .. } | Brush::SweepGradient { color_stops, .. } => {
|
|
||||||
color_stops.len()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.work = self.work.saturating_add(1 + stops);
|
|
||||||
if self.work > MAX_WORK {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match brush {
|
match brush {
|
||||||
Brush::Solid { palette_index, alpha } => {
|
Brush::Solid { palette_index, alpha } => {
|
||||||
let c = self.color(palette_index, alpha);
|
let c = self.color(palette_index, alpha);
|
||||||
|
|||||||
@@ -25,16 +25,6 @@ const MAX_EDGE_WORK: u64 = 64_000_000;
|
|||||||
/// Fewest edges a simplified polygon keeps, so a coarse outline still has a shape.
|
/// Fewest edges a simplified polygon keeps, so a coarse outline still has a shape.
|
||||||
const MIN_POLY_EDGES: usize = 8;
|
const MIN_POLY_EDGES: usize = 8;
|
||||||
|
|
||||||
/// Most points `polygons` keeps from one path; a path flattening past this (a huge finite curve) is
|
|
||||||
/// not drawn rather than held in memory first.
|
|
||||||
const MAX_POINTS: usize = 2_000_000;
|
|
||||||
|
|
||||||
/// The edge budget for a `pw` x `ph` raster where each pixel tests every edge.
|
|
||||||
pub(crate) fn edge_budget(pw: u32, ph: u32) -> usize {
|
|
||||||
let pixels = u64::from(pw).saturating_mul(u64::from(ph)).max(1);
|
|
||||||
usize::try_from(MAX_EDGE_WORK / pixels).unwrap_or(usize::MAX).max(MIN_POLY_EDGES)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Colour position (0 at the focus, 1 at the edge) of `p` for path `kind` in `bounds` with the
|
/// Colour position (0 at the focus, 1 at the edge) of `p` for path `kind` in `bounds` with the
|
||||||
/// focus rect `focus` (fractions inset from left, top, right, bottom), `outline` being the
|
/// focus rect `focus` (fractions inset from left, top, right, bottom), `outline` being the
|
||||||
/// shape's flattened polygon(s) for `shape`.
|
/// shape's flattened polygon(s) for `shape`.
|
||||||
@@ -124,31 +114,21 @@ fn shape_position(outline: &[Vec<Point>], c: Point, p: Point) -> f64 {
|
|||||||
pub fn polygons(path: &BezPath) -> Vec<Vec<Point>> {
|
pub fn polygons(path: &BezPath) -> Vec<Vec<Point>> {
|
||||||
let mut out: Vec<Vec<Point>> = Vec::new();
|
let mut out: Vec<Vec<Point>> = Vec::new();
|
||||||
let mut cur: Vec<Point> = Vec::new();
|
let mut cur: Vec<Point> = Vec::new();
|
||||||
let mut total = 0usize;
|
kurbo::flatten(path.iter(), 0.25, |el| match el {
|
||||||
kurbo::flatten(path.iter(), 0.25, |el| {
|
PathEl::MoveTo(p) => {
|
||||||
total += 1;
|
if cur.len() > 1 {
|
||||||
if total > MAX_POINTS {
|
out.push(std::mem::take(&mut cur));
|
||||||
return;
|
|
||||||
}
|
|
||||||
match el {
|
|
||||||
PathEl::MoveTo(p) => {
|
|
||||||
if cur.len() > 1 {
|
|
||||||
out.push(std::mem::take(&mut cur));
|
|
||||||
}
|
|
||||||
cur = vec![p];
|
|
||||||
}
|
}
|
||||||
PathEl::LineTo(p) => cur.push(p),
|
cur = vec![p];
|
||||||
PathEl::ClosePath => {
|
|
||||||
if let Some(f) = cur.first().copied() {
|
|
||||||
cur.push(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
PathEl::LineTo(p) => cur.push(p),
|
||||||
|
PathEl::ClosePath => {
|
||||||
|
if let Some(f) = cur.first().copied() {
|
||||||
|
cur.push(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
});
|
});
|
||||||
if total > MAX_POINTS {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
if cur.len() > 1 {
|
if cur.len() > 1 {
|
||||||
out.push(cur);
|
out.push(cur);
|
||||||
}
|
}
|
||||||
@@ -166,7 +146,7 @@ pub fn polygons(path: &BezPath) -> Vec<Vec<Point>> {
|
|||||||
/// `outline` cut down to at most about `max_edges` edges in total: first by dropping points
|
/// `outline` cut down to at most about `max_edges` edges in total: first by dropping points
|
||||||
/// closer than `tol` to the last kept one (detail finer than a raster pixel), then, if still too
|
/// closer than `tol` to the last kept one (detail finer than a raster pixel), then, if still too
|
||||||
/// many, by keeping every k-th point. Each polygon stays closed. Returns it unchanged when it fits.
|
/// many, by keeping every k-th point. Each polygon stays closed. Returns it unchanged when it fits.
|
||||||
pub(crate) fn simplify(outline: Vec<Vec<Point>>, tol: f64, max_edges: usize) -> Vec<Vec<Point>> {
|
fn simplify(outline: Vec<Vec<Point>>, tol: f64, max_edges: usize) -> Vec<Vec<Point>> {
|
||||||
let edges = |o: &[Vec<Point>]| o.iter().map(|p| p.len().saturating_sub(1)).sum::<usize>();
|
let edges = |o: &[Vec<Point>]| o.iter().map(|p| p.len().saturating_sub(1)).sum::<usize>();
|
||||||
if edges(&outline) <= max_edges {
|
if edges(&outline) <= max_edges {
|
||||||
return outline;
|
return outline;
|
||||||
@@ -253,17 +233,13 @@ pub fn fill(ctx: &mut RenderContext, kind: &str, focus: [f64; 4], stops: &[(f64,
|
|||||||
let s = (MAX_SIDE / longest).min(1.0) * k;
|
let s = (MAX_SIDE / longest).min(1.0) * k;
|
||||||
let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 4096), ((bounds.height() * s).ceil() as u32).clamp(1, 4096));
|
let (pw, ph) = (((bounds.width() * s).ceil() as u32).clamp(1, 4096), ((bounds.height() * s).ceil() as u32).clamp(1, 4096));
|
||||||
let outline = if kind == "shape" {
|
let outline = if kind == "shape" {
|
||||||
let max_edges = edge_budget(pw, ph);
|
let pixels = u64::from(pw).saturating_mul(u64::from(ph)).max(1);
|
||||||
|
let max_edges = usize::try_from(MAX_EDGE_WORK / pixels).unwrap_or(usize::MAX).max(MIN_POLY_EDGES);
|
||||||
let pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph));
|
let pixel = (bounds.width() / f64::from(pw)).min(bounds.height() / f64::from(ph));
|
||||||
simplify(polygons(path), pixel, max_edges)
|
simplify(polygons(path), pixel, max_edges)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
// An outline `polygons` refused (too many points) has no contours: skip the shape rather than
|
|
||||||
// paint it with the rectangle fallback's different gradient.
|
|
||||||
if kind == "shape" && outline.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let lut = ramp(stops);
|
let lut = ramp(stops);
|
||||||
let a = alpha.clamp(0.0, 1.0);
|
let a = alpha.clamp(0.0, 1.0);
|
||||||
let mut data = Vec::with_capacity((pw * ph) as usize);
|
let mut data = Vec::with_capacity((pw * ph) as usize);
|
||||||
@@ -396,20 +372,6 @@ mod tests {
|
|||||||
assert!(t.elapsed() < std::time::Duration::from_secs(20), "{:?}", t.elapsed());
|
assert!(t.elapsed() < std::time::Duration::from_secs(20), "{:?}", t.elapsed());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_shape_gradient_with_no_usable_outline_paints_nothing() {
|
|
||||||
// One polygon over the per-polygon cap is dropped by `polygons`.
|
|
||||||
let path = many_segment_square(26_000);
|
|
||||||
assert!(polygons(&path).is_empty());
|
|
||||||
let mut ctx = RenderContext::new(64, 64);
|
|
||||||
let stops = [(0.0, Rgba::rgb(0, 0, 0)), (1.0, Rgba::rgb(255, 255, 255))];
|
|
||||||
fill(&mut ctx, "shape", CENTRE, &stops, Rect::new(0.0, 0.0, 64.0, 64.0), &path, Affine::IDENTITY, 1.0);
|
|
||||||
ctx.flush();
|
|
||||||
let mut px = vec![0u8; 64 * 64 * 4];
|
|
||||||
ctx.render(vello_cpu::PixmapMut::new(64, 64, &mut px).unwrap(), &mut vello_cpu::Resources::new());
|
|
||||||
assert!(px.chunks(4).all(|p| p[3] == 0), "nothing was painted");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_ramp_runs_through_the_stops() {
|
fn the_ramp_runs_through_the_stops() {
|
||||||
let stops = [(0.0, Rgba::rgb(0, 0, 0)), (0.5, Rgba::rgb(128, 128, 128)), (1.0, Rgba::rgb(255, 255, 255))];
|
let stops = [(0.0, Rgba::rgb(0, 0, 0)), (0.5, Rgba::rgb(128, 128, 128)), (1.0, Rgba::rgb(255, 255, 255))];
|
||||||
|
|||||||
Reference in New Issue
Block a user