mirror of
https://github.com/penpot/penpot.git
synced 2026-08-14 00:38:42 +00:00
Store encoded media in RAM, upload a shape-sized display texture eagerly, and only promote the native full texture when zoom needs it so oversized fillImages do not saturate integrated GPUs.
404 lines
12 KiB
Rust
404 lines
12 KiB
Rust
use skia_safe::{self as skia, Paint, RRect};
|
|
|
|
use super::{filters, RenderState, SurfaceId};
|
|
use crate::error::Result;
|
|
use crate::get_resources;
|
|
use crate::render::get_source_rect;
|
|
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type};
|
|
|
|
// Set the clipping area to the shape outline within the container bounds
|
|
fn clip_to_shape(
|
|
canvas: &skia::Canvas,
|
|
shape: &Shape,
|
|
container: &crate::math::Rect,
|
|
antialias: bool,
|
|
) {
|
|
match &shape.shape_type {
|
|
Type::Rect(Rect {
|
|
corners: Some(corners),
|
|
})
|
|
| Type::Frame(Frame {
|
|
corners: Some(corners),
|
|
..
|
|
}) => {
|
|
let rrect: RRect = RRect::new_rect_radii(container, corners);
|
|
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, antialias);
|
|
}
|
|
Type::Rect(_) | Type::Frame(_) => {
|
|
canvas.clip_rect(container, skia::ClipOp::Intersect, antialias);
|
|
}
|
|
Type::Circle => {
|
|
let oval_path = {
|
|
let mut pb = skia::PathBuilder::new();
|
|
pb.add_oval(container, None, None);
|
|
pb.detach()
|
|
};
|
|
canvas.clip_path(&oval_path, skia::ClipOp::Intersect, antialias);
|
|
}
|
|
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
|
if let Some(path) = shape_type.path() {
|
|
if let Some(path_transform) = shape.to_path_transform() {
|
|
canvas.clip_path(
|
|
&path
|
|
.to_skia_path(shape.svg_attrs.as_ref())
|
|
.make_transform(&path_transform),
|
|
skia::ClipOp::Intersect,
|
|
antialias,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Type::SVGRaw(_) => {
|
|
canvas.clip_rect(container, skia::ClipOp::Intersect, antialias);
|
|
}
|
|
Type::Group(_) => unreachable!("A group should not have fills"),
|
|
Type::Text(_) => unimplemented!("TODO"),
|
|
}
|
|
}
|
|
|
|
/// Axis-aligned rect/frame with no corner radii: `dest` fills `selrect`, so a
|
|
/// clip to the container is a no-op before `draw_image_rect`.
|
|
fn is_axis_aligned_image_rect(shape: &Shape) -> bool {
|
|
matches!(
|
|
&shape.shape_type,
|
|
Type::Rect(Rect { corners: None }) | Type::Frame(Frame { corners: None, .. })
|
|
)
|
|
}
|
|
|
|
fn draw_image_fill(
|
|
render_state: &mut RenderState,
|
|
shape: &Shape,
|
|
image_fill: &ImageFill,
|
|
paint: &Paint,
|
|
antialias: bool,
|
|
surface_id: SurfaceId,
|
|
) {
|
|
if draw_svg_image_fill(
|
|
render_state,
|
|
shape,
|
|
image_fill,
|
|
paint,
|
|
antialias,
|
|
surface_id,
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let view_scale = render_state.get_view_scale();
|
|
let dpr = render_state.options.dpr.max(1.0);
|
|
let display_side = super::images::shape_side_px(&shape.selrect, dpr);
|
|
let needed_side = super::images::shape_side_px(&shape.selrect, view_scale);
|
|
let Some(image) = get_resources().images.get_for_draw(
|
|
&image_fill.id(),
|
|
display_side,
|
|
needed_side,
|
|
) else {
|
|
return;
|
|
};
|
|
|
|
let size = image.dimensions();
|
|
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
|
|
let container = &shape.selrect;
|
|
|
|
let src_rect = get_source_rect(size, container, image_fill);
|
|
let dest_rect = container;
|
|
let sampling = get_resources().sampling_options;
|
|
|
|
// `save_layer` is only required when a shape-level image filter (blur) must
|
|
// run over the clipped image. Otherwise a plain save/clip (or no clip for
|
|
// axis-aligned rects) avoids an offscreen buffer per fill — the hot path
|
|
// for photo-heavy boards during tile walks.
|
|
if let Some(filter) = shape.image_filter(1.) {
|
|
let mut layer_paint = skia::Paint::default();
|
|
layer_paint.set_anti_alias(antialias);
|
|
layer_paint.set_image_filter(filter);
|
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&layer_paint);
|
|
canvas.save_layer(&layer_rec);
|
|
clip_to_shape(canvas, shape, container, antialias);
|
|
canvas.draw_image_rect_with_sampling_options(
|
|
image,
|
|
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
|
dest_rect,
|
|
sampling,
|
|
paint,
|
|
);
|
|
canvas.restore();
|
|
return;
|
|
}
|
|
|
|
let mut draw_paint = paint.clone();
|
|
draw_paint.set_anti_alias(antialias);
|
|
|
|
if is_axis_aligned_image_rect(shape) {
|
|
canvas.draw_image_rect_with_sampling_options(
|
|
image,
|
|
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
|
dest_rect,
|
|
sampling,
|
|
&draw_paint,
|
|
);
|
|
return;
|
|
}
|
|
|
|
canvas.save();
|
|
clip_to_shape(canvas, shape, container, antialias);
|
|
canvas.draw_image_rect_with_sampling_options(
|
|
image,
|
|
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
|
dest_rect,
|
|
sampling,
|
|
&draw_paint,
|
|
);
|
|
canvas.restore();
|
|
}
|
|
|
|
// Draws an SVG image fill from its parsed DOM. The canvas transform carries
|
|
// the current zoom, so the SVG rasterizes at display resolution instead of
|
|
// being stretched from a fixed-size texture. Returns false when the stored
|
|
// image is not an SVG.
|
|
fn draw_svg_image_fill(
|
|
render_state: &mut RenderState,
|
|
shape: &Shape,
|
|
image_fill: &ImageFill,
|
|
paint: &Paint,
|
|
antialias: bool,
|
|
surface_id: SurfaceId,
|
|
) -> bool {
|
|
let Some((dom, size)) = get_resources().images.get_svg(&image_fill.id()) else {
|
|
return false;
|
|
};
|
|
|
|
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
|
|
let container = &shape.selrect;
|
|
let size = skia::ISize::new(size.width as i32, size.height as i32);
|
|
let src_rect = get_source_rect(size, container, image_fill);
|
|
if src_rect.width() <= 0.0 || src_rect.height() <= 0.0 {
|
|
return true;
|
|
}
|
|
|
|
let mut image_paint = skia::Paint::default();
|
|
image_paint.set_anti_alias(antialias);
|
|
if let Some(filter) = shape.image_filter(1.) {
|
|
image_paint.set_image_filter(filter.clone());
|
|
}
|
|
|
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
|
|
canvas.save_layer(&layer_rec);
|
|
|
|
clip_to_shape(canvas, shape, container, antialias);
|
|
|
|
// Apply the fill paint (opacity/blend) to the whole SVG as one layer.
|
|
let fill_layer = skia::canvas::SaveLayerRec::default().paint(paint);
|
|
canvas.save_layer(&fill_layer);
|
|
|
|
// Map the cropped source rect onto the container: cover semantics when
|
|
// keep-aspect-ratio is set, stretch otherwise (same math as the raster
|
|
// path, expressed as a canvas transform).
|
|
let scale_x = container.width() / src_rect.width();
|
|
let scale_y = container.height() / src_rect.height();
|
|
canvas.translate((
|
|
container.left - src_rect.left * scale_x,
|
|
container.top - src_rect.top * scale_y,
|
|
));
|
|
canvas.scale((scale_x, scale_y));
|
|
dom.render(canvas);
|
|
|
|
canvas.restore();
|
|
canvas.restore();
|
|
|
|
true
|
|
}
|
|
|
|
/**
|
|
* This SHOULD be the only public function in this module.
|
|
*/
|
|
pub fn render(
|
|
render_state: &mut RenderState,
|
|
shape: &Shape,
|
|
fills: &[Fill],
|
|
antialias: bool,
|
|
surface_id: SurfaceId,
|
|
outset: Option<f32>,
|
|
) -> Result<()> {
|
|
if fills.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let scale = render_state.get_scale().max(1e-6);
|
|
let inset = if shape.has_inner_stroke() {
|
|
Some(1.0 / scale)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Image fills use draw_image_fill which needs render_state for GPU images
|
|
// and sampling options that get_fill_shader (used by merge_fills) lacks.
|
|
let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_)));
|
|
if has_image_fills {
|
|
for fill in fills.iter().rev() {
|
|
render_single_fill(
|
|
render_state,
|
|
shape,
|
|
fill,
|
|
antialias,
|
|
surface_id,
|
|
outset,
|
|
inset,
|
|
)?;
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let mut paint = merge_fills(fills, shape.selrect);
|
|
paint.set_anti_alias(antialias);
|
|
|
|
if let Some(image_filter) = shape.image_filter(1.) {
|
|
let bounds = image_filter.compute_fast_bounds(shape.selrect);
|
|
if filters::render_with_filter_surface(
|
|
render_state,
|
|
bounds,
|
|
surface_id,
|
|
|state, temp_surface| {
|
|
let mut filtered_paint = paint.clone();
|
|
filtered_paint.set_image_filter(image_filter.clone());
|
|
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, outset, inset);
|
|
Ok(())
|
|
},
|
|
)? {
|
|
return Ok(());
|
|
} else {
|
|
paint.set_image_filter(image_filter);
|
|
}
|
|
}
|
|
|
|
draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset);
|
|
Ok(())
|
|
}
|
|
|
|
/// Draws a single paint (with a merged shader) to the appropriate surface
|
|
/// based on the shape type.
|
|
/// When `inset` is Some(eps), the fill is inset by eps (e.g. to avoid seam with inner strokes).
|
|
fn draw_fill_to_surface(
|
|
render_state: &mut RenderState,
|
|
shape: &Shape,
|
|
surface_id: SurfaceId,
|
|
paint: &Paint,
|
|
outset: Option<f32>,
|
|
inset: Option<f32>,
|
|
) {
|
|
match &shape.shape_type {
|
|
Type::Rect(_) | Type::Frame(_) => {
|
|
render_state
|
|
.surfaces
|
|
.draw_rect_to(surface_id, shape, paint, outset, inset);
|
|
}
|
|
Type::Circle => {
|
|
render_state
|
|
.surfaces
|
|
.draw_circle_to(surface_id, shape, paint, outset, inset);
|
|
}
|
|
Type::Path(_) | Type::Bool(_) => {
|
|
render_state
|
|
.surfaces
|
|
.draw_path_to(surface_id, shape, paint, outset, inset);
|
|
}
|
|
Type::Group(_) => {}
|
|
_ => unreachable!("This shape should not have fills"),
|
|
}
|
|
}
|
|
|
|
fn render_single_fill(
|
|
render_state: &mut RenderState,
|
|
shape: &Shape,
|
|
fill: &Fill,
|
|
antialias: bool,
|
|
surface_id: SurfaceId,
|
|
outset: Option<f32>,
|
|
inset: Option<f32>,
|
|
) -> Result<()> {
|
|
let mut paint = fill.to_paint(&shape.selrect, antialias);
|
|
if let Some(image_filter) = shape.image_filter(1.) {
|
|
let bounds = image_filter.compute_fast_bounds(shape.selrect);
|
|
if filters::render_with_filter_surface(
|
|
render_state,
|
|
bounds,
|
|
surface_id,
|
|
|state, temp_surface| {
|
|
let mut filtered_paint = paint.clone();
|
|
filtered_paint.set_image_filter(image_filter.clone());
|
|
draw_single_fill_to_surface(
|
|
state,
|
|
shape,
|
|
fill,
|
|
antialias,
|
|
temp_surface,
|
|
&filtered_paint,
|
|
outset,
|
|
inset,
|
|
);
|
|
Ok(())
|
|
},
|
|
)? {
|
|
return Ok(());
|
|
} else {
|
|
paint.set_image_filter(image_filter);
|
|
}
|
|
}
|
|
|
|
draw_single_fill_to_surface(
|
|
render_state,
|
|
shape,
|
|
fill,
|
|
antialias,
|
|
surface_id,
|
|
&paint,
|
|
outset,
|
|
inset,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn draw_single_fill_to_surface(
|
|
render_state: &mut RenderState,
|
|
shape: &Shape,
|
|
fill: &Fill,
|
|
antialias: bool,
|
|
surface_id: SurfaceId,
|
|
paint: &Paint,
|
|
outset: Option<f32>,
|
|
inset: Option<f32>,
|
|
) {
|
|
match (fill, &shape.shape_type) {
|
|
(Fill::Image(image_fill), _) => {
|
|
draw_image_fill(
|
|
render_state,
|
|
shape,
|
|
image_fill,
|
|
paint,
|
|
antialias,
|
|
surface_id,
|
|
);
|
|
}
|
|
(_, Type::Rect(_) | Type::Frame(_)) => {
|
|
render_state
|
|
.surfaces
|
|
.draw_rect_to(surface_id, shape, paint, outset, inset);
|
|
}
|
|
(_, Type::Circle) => {
|
|
render_state
|
|
.surfaces
|
|
.draw_circle_to(surface_id, shape, paint, outset, inset);
|
|
}
|
|
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
|
|
render_state
|
|
.surfaces
|
|
.draw_path_to(surface_id, shape, paint, outset, inset);
|
|
}
|
|
(_, Type::Group(_)) => {
|
|
// Groups can have fills but they propagate them to their children
|
|
}
|
|
_ => unreachable!("This shape should not have fills"),
|
|
}
|
|
}
|