use super::*;
use crate::render::curves::{CurveTolerance, RoundedRectPath, TinySkiaCurveSink};
use crate::types::{Rect, Size, Vector};
pub(crate) struct BlurredCoverageMask {
coverage: image::GrayImage,
raster_clip: Option<RoundedCoverageClip>,
pub(crate) overflow_pt: f32,
filter_dpi: f32,
}
impl BlurredCoverageMask {
pub(crate) const fn coverage(&self) -> &image::GrayImage {
&self.coverage
}
pub(crate) fn raster_dimensions(&self) -> crate::util::RasterDimensions {
crate::util::RasterDimensions {
width: self.coverage.width(),
height: self.coverage.height(),
}
}
pub(crate) fn pixel_density(&self) -> crate::layout::engine::RasterPixelDensity {
crate::layout::engine::RasterPixelDensity::from_dpi(self.filter_dpi)
}
pub(crate) fn raster_size_pt(&self) -> Size {
let pt_per_pixel = 72.0 / self.filter_dpi;
Size::new(
self.coverage.width() as f32 * pt_per_pixel,
self.coverage.height() as f32 * pt_per_pixel,
)
}
pub(crate) fn tinted_raster(&self, color: (f32, f32, f32, f32)) -> Option<BlurredRaster> {
let (red, green, blue, alpha) = color;
if alpha <= 0.0 {
return None;
}
let color = [
quantize_color(red),
quantize_color(green),
quantize_color(blue),
];
let alpha = quantize_color(alpha);
let clipped_coverage = match self.raster_clip {
Some(clip) => Some(clip.apply(self.coverage.clone())?),
None => None,
};
let coverage = clipped_coverage.as_ref().unwrap_or(&self.coverage);
let rgba = image::RgbaImage::from_fn(coverage.width(), coverage.height(), |x, y| {
let coverage = coverage.get_pixel(x, y)[0];
image::Rgba([
color[0],
color[1],
color[2],
multiply_coverage(coverage, alpha),
])
});
Some(BlurredRaster {
asset: rgba_to_png_alpha_asset(rgba, self.filter_dpi)?,
overflow_pt: self.overflow_pt,
})
}
}
fn quantize_color(component: f32) -> u8 {
(component.clamp(0.0, 1.0) * 255.0).round() as u8
}
fn multiply_coverage(coverage: u8, alpha: u8) -> u8 {
((u16::from(coverage) * u16::from(alpha) + 127) / 255) as u8
}
#[derive(Clone, Copy)]
struct InsetMaskBounds {
source_margin_px: u32,
blur_support_px: u32,
}
impl InsetMaskBounds {
fn for_shadow(shadow: &BoxShadow, blur: CoverageBlurKernel) -> Option<Self> {
let authored_sigma_px = shadow.blur / PT_PER_PX / 2.0;
let source_margin_px = CoverageBlurKernel::from_sigma(authored_sigma_px)?.support_px;
Some(Self {
source_margin_px,
blur_support_px: blur.support_px,
})
}
fn raster_padding_px(self) -> Option<u32> {
self.source_margin_px.checked_add(self.blur_support_px)
}
fn source_bounds(self, box_rect: Rect) -> Rect {
box_rect.outset(EdgeSizes::uniform(self.source_margin_px as f32))
}
}
pub(crate) fn blur_shadow_mask(
width_pt: f32,
height_pt: f32,
radii: CornerRadii,
shadow: &BoxShadow,
filter_dpi: f32,
) -> Option<BlurredCoverageMask> {
if width_pt <= 0.0 || height_pt <= 0.0 || shadow.color.alpha() <= 0.0 {
return None;
}
use resvg::tiny_skia;
let s = RasterScale::at_dpi(filter_dpi).pixels_per_css_pixel();
let sigma = (shadow.blur / PT_PER_PX) * s / 2.0;
let kernel = CoverageBlurKernel::from_sigma(sigma)?;
let pad = kernel.padding_px;
let box_x = filter_raster_axis(width_pt, s)?;
let box_y = filter_raster_axis(height_pt, s)?;
let buf_w = padded_pixels(box_x.pixels, pad)?;
let buf_h = padded_pixels(box_y.pixels, pad)?;
let mut pixmap = tiny_skia::Pixmap::new(buf_w, buf_h)?;
let ox = pad as f32;
let oy = pad as f32;
let mut paint = tiny_skia::Paint::default();
paint.set_color(tiny_skia::Color::WHITE);
paint.anti_alias = true;
let radii_px = radii.fit_to(width_pt, height_pt) * (s / PT_PER_PX);
if !radii_px.is_zero() {
let mut path = tiny_skia::PathBuilder::new();
append_rounded_box_path(&mut path, ox, oy, box_x.paint_px, box_y.paint_px, radii_px);
if let Some(path) = path.finish() {
pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
tiny_skia::Transform::identity(),
None,
);
}
} else if let Some(rect) = tiny_skia::Rect::from_xywh(ox, oy, box_x.paint_px, box_y.paint_px) {
pixmap.fill_rect(rect, &paint, tiny_skia::Transform::identity(), None);
}
let coverage = crate::render::raster_pixels::pixmap_to_alpha_mask(&pixmap);
let coverage = blur_coverage(coverage, kernel)?;
let overflow_pt = pad as f32 / s * PT_PER_PX;
Some(BlurredCoverageMask {
coverage,
raster_clip: None,
overflow_pt,
filter_dpi,
})
}
pub(crate) fn blur_inset_shadow_mask(
width_pt: f32,
height_pt: f32,
radii: CornerRadii,
shadow: &BoxShadow,
filter_dpi: f32,
) -> Option<BlurredCoverageMask> {
if width_pt <= 0.0 || height_pt <= 0.0 || shadow.color.alpha() <= 0.0 {
return None;
}
use resvg::tiny_skia;
let s = RasterScale::at_dpi(filter_dpi).pixels_per_css_pixel();
let sigma = (shadow.blur / PT_PER_PX) * s / 2.0;
let kernel = CoverageBlurKernel::from_sigma(sigma)?;
let bounds = InsetMaskBounds::for_shadow(shadow, kernel)?;
let pad = bounds.raster_padding_px()?;
let box_x = filter_raster_axis(width_pt, s)?;
let box_y = filter_raster_axis(height_pt, s)?;
let buf_w = padded_pixels(box_x.pixels, pad)?;
let buf_h = padded_pixels(box_y.pixels, pad)?;
let mut pixmap = tiny_skia::Pixmap::new(buf_w, buf_h)?;
let mut paint = tiny_skia::Paint::default();
paint.set_color(tiny_skia::Color::WHITE);
paint.anti_alias = true;
let pt_to_px = s / PT_PER_PX;
let spread_px = shadow.spread * pt_to_px;
let offset = Vector::new(shadow.offset_x * pt_to_px, shadow.offset_y * pt_to_px);
let box_rect = Rect::from_xywh(pad as f32, pad as f32, box_x.paint_px, box_y.paint_px);
let source_bounds = bounds.source_bounds(box_rect);
let caster_outset = shadow.blur * pt_to_px + (-shadow.spread).max(0.0) * pt_to_px;
let caster = box_rect
.outset(EdgeSizes::uniform(caster_outset))
.union(
box_rect
.outset(EdgeSizes::uniform(caster_outset))
.translate(offset),
)
.intersection(source_bounds)?;
let hole = box_rect
.translate(offset)
.inset(EdgeSizes::uniform(spread_px));
let mut path = tiny_skia::PathBuilder::new();
RoundedRectPath::new(caster, CornerRadii::ZERO).write_to(
&mut TinySkiaCurveSink::new(&mut path),
CurveTolerance::RASTER_PIXEL,
);
if hole.size.width > 0.0 && hole.size.height > 0.0 {
let hole_radii = radii.grow(-shadow.spread) * pt_to_px;
RoundedRectPath::new(hole, hole_radii).write_to(
&mut TinySkiaCurveSink::new(&mut path),
CurveTolerance::RASTER_PIXEL,
);
}
if let Some(path) = path.finish() {
pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::EvenOdd,
tiny_skia::Transform::identity(),
None,
);
}
let coverage = crate::render::raster_pixels::pixmap_to_alpha_mask(&pixmap);
let coverage = blur_coverage(coverage, kernel)?;
let overflow_pt = pad as f32 / s * PT_PER_PX;
Some(BlurredCoverageMask {
coverage,
raster_clip: Some(RoundedCoverageClip {
rect: box_rect,
radii: radii * pt_to_px,
}),
overflow_pt,
filter_dpi,
})
}
fn blur_coverage(
coverage: image::GrayImage,
kernel: CoverageBlurKernel,
) -> Option<image::GrayImage> {
let encoded = image::RgbaImage::from_fn(coverage.width(), coverage.height(), |x, y| {
let value = coverage.get_pixel(x, y)[0];
image::Rgba([value, value, value, value])
});
let blurred = kernel.blur(&encoded)?;
Some(image::GrayImage::from_fn(
blurred.width(),
blurred.height(),
|x, y| image::Luma([blurred.get_pixel(x, y)[3]]),
))
}
#[derive(Clone, Copy)]
struct RoundedCoverageClip {
rect: Rect,
radii: CornerRadii,
}
impl RoundedCoverageClip {
fn apply(self, mut coverage: image::GrayImage) -> Option<image::GrayImage> {
use resvg::tiny_skia;
let mut mask = tiny_skia::Pixmap::new(coverage.width(), coverage.height())?;
let mut path = tiny_skia::PathBuilder::new();
RoundedRectPath::new(self.rect, self.radii).write_to(
&mut TinySkiaCurveSink::new(&mut path),
CurveTolerance::RASTER_PIXEL,
);
let path = path.finish()?;
let mut paint = tiny_skia::Paint::default();
paint.set_color(tiny_skia::Color::WHITE);
paint.anti_alias = true;
mask.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
tiny_skia::Transform::identity(),
None,
);
for (index, pixel) in coverage.pixels_mut().enumerate() {
let mask_alpha = u16::from(mask.pixels()[index].alpha());
pixel[0] = multiply_coverage(pixel[0], mask_alpha as u8);
}
Some(coverage)
}
}
fn append_rounded_box_path(
path: &mut resvg::tiny_skia::PathBuilder,
x: f32,
y: f32,
width: f32,
height: f32,
radii: CornerRadii,
) {
RoundedRectPath::new(Rect::from_xywh(x, y, width, height), radii).write_to(
&mut TinySkiaCurveSink::new(path),
CurveTolerance::RASTER_PIXEL,
);
}
pub(crate) fn blur_shadow_alpha_mask(
mask: &image::GrayImage,
blur_pt: f32,
color: (f32, f32, f32, f32),
filter_dpi: f32,
) -> Option<(BlurredRaster, u32)> {
let (width, height) = mask.dimensions();
let (red, green, blue, alpha) = color;
if width == 0 || height == 0 || alpha <= 0.0 {
return None;
}
let scale = RasterScale::at_dpi(filter_dpi).pixels_per_css_pixel();
let sigma = (blur_pt / PT_PER_PX) * scale / 2.0;
let kernel = CoverageBlurKernel::from_sigma(sigma)?;
let padding = kernel.padding_px;
let buffer_width = padded_pixels(width, padding)?;
let buffer_height = padded_pixels(height, padding)?;
let mut padded = image::GrayImage::new(buffer_width, buffer_height);
let mut painted = false;
for y in 0..height {
for x in 0..width {
let coverage = mask.get_pixel(x, y)[0];
if coverage == 0 {
continue;
}
painted = true;
padded.put_pixel(x + padding, y + padding, image::Luma([coverage]));
}
}
if !painted {
return None;
}
let coverage = blur_coverage(padded, kernel)?;
let overflow_pt = padding as f32 / scale * PT_PER_PX;
let blurred = BlurredCoverageMask {
coverage,
raster_clip: None,
overflow_pt,
filter_dpi,
}
.tinted_raster((red, green, blue, alpha))?;
Some((blurred, padding))
}
#[cfg(test)]
mod tests;