blob: 81c2898ba96e4269465bbe450b1a2ed8e2c17153 [file] [log] [blame]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Basic render operations.
use crate::RenderMode;
use crate::fine::{Fine, FineType};
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use vello_common::blurred_rounded_rect::BlurredRoundedRectangle;
use vello_common::coarse::Wide;
use vello_common::color::{AlphaColor, Srgb};
use vello_common::colr::{ColrPainter, ColrRenderer};
use vello_common::encode::{EncodeExt, EncodedPaint};
use vello_common::flatten::Line;
use vello_common::glyph::{GlyphRenderer, GlyphRunBuilder, GlyphType, PreparedGlyph};
use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Shape, Stroke};
use vello_common::mask::Mask;
use vello_common::paint::{Image, Paint, PaintType};
use vello_common::peniko::color::palette::css::BLACK;
use vello_common::peniko::{BlendMode, Compose, Fill, Gradient, Mix};
use vello_common::peniko::{Font, ImageQuality};
use vello_common::pixmap::Pixmap;
use vello_common::strip::Strip;
use vello_common::tile::Tiles;
use vello_common::{flatten, peniko, strip};
pub(crate) const DEFAULT_TOLERANCE: f64 = 0.1;
/// A render context.
#[derive(Debug)]
pub struct RenderContext {
pub(crate) width: u16,
pub(crate) height: u16,
pub(crate) wide: Wide,
pub(crate) alphas: Vec<u8>,
pub(crate) line_buf: Vec<Line>,
pub(crate) tiles: Tiles,
pub(crate) strip_buf: Vec<Strip>,
pub(crate) paint: PaintType,
pub(crate) paint_transform: Affine,
pub(crate) stroke: Stroke,
pub(crate) transform: Affine,
pub(crate) fill_rule: Fill,
pub(crate) encoded_paints: Vec<EncodedPaint>,
}
impl RenderContext {
/// Create a new render context with the given width and height in pixels.
pub fn new(width: u16, height: u16) -> Self {
let wide = Wide::new(width, height);
let alphas = vec![];
let line_buf = vec![];
let tiles = Tiles::new();
let strip_buf = vec![];
let transform = Affine::IDENTITY;
let fill_rule = Fill::NonZero;
let paint = BLACK.into();
let paint_transform = Affine::IDENTITY;
let stroke = Stroke {
width: 1.0,
join: Join::Bevel,
start_cap: Cap::Butt,
end_cap: Cap::Butt,
..Default::default()
};
let encoded_paints = vec![];
Self {
width,
height,
wide,
alphas,
line_buf,
tiles,
strip_buf,
transform,
paint,
paint_transform,
fill_rule,
stroke,
encoded_paints,
}
}
fn encode_current_paint(&mut self) -> Paint {
match self.paint.clone() {
PaintType::Solid(s) => s.into(),
PaintType::Gradient(g) => {
// TODO: Add caching?
g.encode_into(
&mut self.encoded_paints,
self.transform * self.paint_transform,
)
}
PaintType::Image(i) => i.encode_into(
&mut self.encoded_paints,
self.transform * self.paint_transform,
),
}
}
/// Fill a path.
pub fn fill_path(&mut self, path: &BezPath) {
flatten::fill(path, self.transform, &mut self.line_buf);
let paint = self.encode_current_paint();
self.render_path(self.fill_rule, paint);
}
/// Stroke a path.
pub fn stroke_path(&mut self, path: &BezPath) {
flatten::stroke(path, &self.stroke, self.transform, &mut self.line_buf);
let paint = self.encode_current_paint();
self.render_path(Fill::NonZero, paint);
}
/// Fill a rectangle.
pub fn fill_rect(&mut self, rect: &Rect) {
self.fill_path(&rect.to_path(DEFAULT_TOLERANCE));
}
/// Fill a blurred rectangle with the given radius and standard deviation.
///
/// Note that this only works properly if the current paint is set to a solid color.
/// If not, it will fall back to using black as the fill color.
pub fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) {
let color = match self.paint {
PaintType::Solid(s) => s,
// Fallback to black when attempting to blur a rectangle with an image/gradient paint
_ => BLACK,
};
let blurred_rect = BlurredRoundedRectangle {
rect: *rect,
color,
radius,
std_dev,
};
// The actual rectangle we paint needs to be larger so that the blurring effect
// is not cut off.
// The impulse response of a gaussian filter is infinite.
// For performance reason we cut off the filter at some extent where the response is close to zero.
let kernel_size = 2.5 * std_dev;
let inflated_rect = rect.inflate(f64::from(kernel_size), f64::from(kernel_size));
let transform = self.transform * self.paint_transform;
let paint = blurred_rect.encode_into(&mut self.encoded_paints, transform);
flatten::fill(&inflated_rect.to_path(0.1), transform, &mut self.line_buf);
self.render_path(Fill::NonZero, paint);
}
/// Stroke a rectangle.
pub fn stroke_rect(&mut self, rect: &Rect) {
self.stroke_path(&rect.to_path(DEFAULT_TOLERANCE));
}
/// Creates a builder for drawing a run of glyphs that have the same attributes.
pub fn glyph_run(&mut self, font: &Font) -> GlyphRunBuilder<'_, Self> {
GlyphRunBuilder::new(font.clone(), self.transform, self)
}
/// Push a new layer with the given properties.
///
/// Note that the mask, if provided, needs to have the same size as the render context. Otherwise,
/// it will be ignored. In addition to that, the mask will not be affected by the current
/// transformation matrix in place.
pub fn push_layer(
&mut self,
clip_path: Option<&BezPath>,
blend_mode: Option<BlendMode>,
opacity: Option<f32>,
mask: Option<Mask>,
) {
let clip = if let Some(c) = clip_path {
flatten::fill(c, self.transform, &mut self.line_buf);
self.make_strips(self.fill_rule);
Some((self.strip_buf.as_slice(), self.fill_rule))
} else {
None
};
let mask = mask.and_then(|m| {
if m.width() != self.width || m.height() != self.height {
None
} else {
Some(m)
}
});
self.wide.push_layer(
clip,
blend_mode.unwrap_or(BlendMode::new(Mix::Normal, Compose::SrcOver)),
mask,
opacity.unwrap_or(1.0),
);
}
/// Push a new clip layer.
pub fn push_clip_layer(&mut self, path: &BezPath) {
self.push_layer(Some(path), None, None, None);
}
/// Push a new blend layer.
pub fn push_blend_layer(&mut self, blend_mode: BlendMode) {
self.push_layer(None, Some(blend_mode), None, None);
}
/// Push a new opacity layer.
pub fn push_opacity_layer(&mut self, opacity: f32) {
self.push_layer(None, None, Some(opacity), None);
}
/// Push a new mask layer.
///
/// Note that the mask, if provided, needs to have the same size as the render context. Otherwise,
/// it will be ignored. In addition to that, the mask will not be affected by the current
/// transformation matrix in place.
pub fn push_mask_layer(&mut self, mask: Mask) {
self.push_layer(None, None, None, Some(mask));
}
/// Pop the last-pushed layer.
pub fn pop_layer(&mut self) {
self.wide.pop_layer();
}
/// Set the current stroke.
pub fn set_stroke(&mut self, stroke: Stroke) {
self.stroke = stroke;
}
/// Set the current paint.
pub fn set_paint(&mut self, paint: impl Into<PaintType>) {
self.paint = paint.into();
}
/// Set the current paint transform.
///
/// The paint transform is applied to the paint after the transform of the geometry the paint
/// is drawn in, i.e., the paint transform is applied after the global transform. This allows
/// transforming the paint independently from the drawn geometry.
pub fn set_paint_transform(&mut self, paint_transform: Affine) {
self.paint_transform = paint_transform;
}
/// Reset the current paint transform.
pub fn reset_paint_transform(&mut self) {
self.paint_transform = Affine::IDENTITY;
}
/// Set the current fill rule.
pub fn set_fill_rule(&mut self, fill_rule: Fill) {
self.fill_rule = fill_rule;
}
/// Set the current transform.
pub fn set_transform(&mut self, transform: Affine) {
self.transform = transform;
}
/// Reset the current transform.
pub fn reset_transform(&mut self) {
self.transform = Affine::IDENTITY;
}
/// Reset the render context.
pub fn reset(&mut self) {
self.line_buf.clear();
self.tiles.reset();
self.alphas.clear();
self.strip_buf.clear();
self.wide.reset();
}
/// Render the current context into a buffer.
/// The buffer is expected to be in premultiplied RGBA8 format with length `width * height * 4`
pub fn render_to_buffer(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
render_mode: RenderMode,
) {
assert!(
!self.wide.has_layers(),
"some layers haven't been popped yet"
);
assert_eq!(
buffer.len(),
(width as usize) * (height as usize) * 4,
"provided width ({}) and height ({}) do not match buffer size ({})",
width,
height,
buffer.len(),
);
match render_mode {
RenderMode::OptimizeSpeed => {
let mut fine = Fine::<u8>::new(width, height);
self.do_fine(buffer, &mut fine);
}
RenderMode::OptimizeQuality => {
let mut fine = Fine::<f32>::new(width, height);
self.do_fine(buffer, &mut fine);
}
}
}
fn do_fine<F: FineType>(&self, buffer: &mut [u8], fine: &mut Fine<F>) {
let width_tiles = self.wide.width_tiles();
let height_tiles = self.wide.height_tiles();
for y in 0..height_tiles {
for x in 0..width_tiles {
let wtile = self.wide.get(x, y);
fine.set_coords(x, y);
fine.clear(F::extract_color(&wtile.bg));
for cmd in &wtile.cmds {
fine.run_cmd(cmd, &self.alphas, &self.encoded_paints);
}
fine.pack(buffer);
}
}
}
/// Render the current context into a pixmap.
pub fn render_to_pixmap(&self, pixmap: &mut Pixmap, render_mode: RenderMode) {
let width = pixmap.width();
let height = pixmap.height();
self.render_to_buffer(pixmap.data_as_u8_slice_mut(), width, height, render_mode);
}
/// Return the width of the pixmap.
pub fn width(&self) -> u16 {
self.width
}
/// Return the height of the pixmap.
pub fn height(&self) -> u16 {
self.height
}
// Assumes that `line_buf` contains the flattened path.
fn render_path(&mut self, fill_rule: Fill, paint: Paint) {
self.make_strips(fill_rule);
self.wide.generate(&self.strip_buf, fill_rule, paint);
}
fn make_strips(&mut self, fill_rule: Fill) {
self.tiles
.make_tiles(&self.line_buf, self.width, self.height);
self.tiles.sort_tiles();
strip::render(
&self.tiles,
&mut self.strip_buf,
&mut self.alphas,
fill_rule,
&self.line_buf,
);
}
}
impl GlyphRenderer for RenderContext {
fn fill_glyph(&mut self, prepared_glyph: PreparedGlyph<'_>) {
match prepared_glyph.glyph_type {
GlyphType::Outline(glyph) => {
flatten::fill(glyph.path, prepared_glyph.transform, &mut self.line_buf);
let paint = self.encode_current_paint();
self.render_path(Fill::NonZero, paint);
}
GlyphType::Bitmap(glyph) => {
// We need to change the state of the render context
// to render the bitmap, but don't want to pollute the context,
// so simulate a `save` and `restore` operation.
let old_transform = self.transform;
let old_paint = self.paint.clone();
// If we scale down by a large factor, fall back to cubic scaling.
let quality = if prepared_glyph.transform.as_coeffs()[0] < 0.5
|| prepared_glyph.transform.as_coeffs()[3] < 0.5
{
ImageQuality::High
} else {
ImageQuality::Medium
};
let image = Image {
pixmap: Arc::new(glyph.pixmap),
x_extend: peniko::Extend::Pad,
y_extend: peniko::Extend::Pad,
quality,
};
self.set_paint(image);
self.set_transform(prepared_glyph.transform);
self.fill_rect(&glyph.area);
// Restore the state.
self.set_paint(old_paint);
self.transform = old_transform;
}
GlyphType::Colr(glyph) => {
// Same as for bitmap glyphs, save the state and restore it later on.
let old_transform = self.transform;
let old_paint = self.paint.clone();
let context_color = match old_paint {
PaintType::Solid(s) => s,
_ => BLACK,
};
let area = glyph.area;
let glyph_pixmap = {
let mut ctx = Self::new(glyph.pix_width, glyph.pix_height);
let mut pix = Pixmap::new(glyph.pix_width, glyph.pix_height);
let mut colr_painter = ColrPainter::new(glyph, context_color, &mut ctx);
colr_painter.paint();
ctx.render_to_pixmap(&mut pix, RenderMode::OptimizeQuality);
pix
};
let image = Image {
pixmap: Arc::new(glyph_pixmap),
x_extend: peniko::Extend::Pad,
y_extend: peniko::Extend::Pad,
// Since the pixmap will already have the correct size, no need to
// use a different image quality here.
quality: ImageQuality::Low,
};
self.set_paint(image);
self.set_transform(prepared_glyph.transform);
self.fill_rect(&area);
// Restore the state.
self.set_paint(old_paint);
self.transform = old_transform;
}
}
}
fn stroke_glyph(&mut self, prepared_glyph: PreparedGlyph<'_>) {
match prepared_glyph.glyph_type {
GlyphType::Outline(glyph) => {
flatten::stroke(
glyph.path,
&self.stroke,
prepared_glyph.transform,
&mut self.line_buf,
);
let paint = self.encode_current_paint();
self.render_path(Fill::NonZero, paint);
}
GlyphType::Bitmap(_) | GlyphType::Colr(_) => {
// The definitions of COLR and bitmap glyphs can't meaningfully support being stroked.
// (COLR's imaging model only has fills)
self.fill_glyph(prepared_glyph);
}
}
}
}
impl ColrRenderer for RenderContext {
fn push_clip_layer(&mut self, clip: &BezPath) {
Self::push_clip_layer(self, clip);
}
fn push_blend_layer(&mut self, blend_mode: BlendMode) {
Self::push_blend_layer(self, blend_mode);
}
fn fill_solid(&mut self, color: AlphaColor<Srgb>) {
self.set_paint(color);
self.fill_rect(&Rect::new(
0.0,
0.0,
f64::from(self.width),
f64::from(self.height),
));
}
fn fill_gradient(&mut self, gradient: Gradient) {
self.set_paint(gradient);
self.fill_rect(&Rect::new(
0.0,
0.0,
f64::from(self.width),
f64::from(self.height),
));
}
fn set_paint_transform(&mut self, affine: Affine) {
Self::set_paint_transform(self, affine);
}
fn pop_layer(&mut self) {
Self::pop_layer(self);
}
}
#[cfg(test)]
mod tests {
use crate::RenderContext;
use vello_common::kurbo::{Rect, Shape};
#[test]
fn reset_render_context() {
let mut ctx = RenderContext::new(100, 100);
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
ctx.fill_rect(&rect);
assert!(!ctx.line_buf.is_empty());
assert!(!ctx.strip_buf.is_empty());
assert!(!ctx.alphas.is_empty());
ctx.reset();
assert!(ctx.line_buf.is_empty());
assert!(ctx.strip_buf.is_empty());
assert!(ctx.alphas.is_empty());
}
#[test]
fn clip_overflow() {
let mut ctx = RenderContext::new(100, 100);
ctx.alphas
.extend(core::iter::repeat_n(255, u16::MAX as usize + 1));
ctx.push_clip_layer(&Rect::new(20.0, 20.0, 180.0, 180.0).to_path(0.1));
ctx.pop_layer();
}
}