/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use base::Epoch; use canvas_traits::canvas::*; use compositing_traits::CrossProcessCompositorApi; use euclid::default::{Point2D, Rect, Size2D, Transform2D}; use pixels::Snapshot; use webrender_api::ImageKey; use crate::backend::GenericDrawTarget; // Asserts on WR texture cache update for zero sized image with raw data. // https://github.com/servo/webrender/blob/main/webrender/src/texture_cache.rs#L1475 const MIN_WR_IMAGE_SIZE: Size2D = Size2D::new(1, 1); #[derive(Clone, Copy)] pub(crate) enum Filter { Bilinear, Nearest, } pub(crate) struct CanvasData { drawtarget: DrawTarget, compositor_api: CrossProcessCompositorApi, image_key: ImageKey, } impl CanvasData { pub(crate) fn new( size: Size2D, compositor_api: CrossProcessCompositorApi, ) -> CanvasData { let size = size.max(MIN_WR_IMAGE_SIZE); let mut draw_target = DrawTarget::new(size.cast()); let image_key = compositor_api.generate_image_key_blocking().unwrap(); let (descriptor, data) = draw_target.image_descriptor_and_serializable_data(); compositor_api.add_image(image_key, descriptor, data); CanvasData { drawtarget: draw_target, compositor_api, image_key, } } pub(crate) fn image_key(&self) -> ImageKey { self.image_key } #[allow(clippy::too_many_arguments)] pub(crate) fn draw_image( &mut self, snapshot: Snapshot, dest_rect: Rect, source_rect: Rect, smoothing_enabled: bool, shadow_options: ShadowOptions, composition_options: CompositionOptions, transform: Transform2D, ) { // We round up the floating pixel values to draw the pixels let source_rect = source_rect.ceil(); // It discards the extra pixels (if any) that won't be painted let snapshot = if Rect::from_size(snapshot.size().to_f64()).contains_rect(&source_rect) { snapshot.get_rect(source_rect.to_u32()) } else { snapshot }; let writer = |draw_target: &mut DrawTarget, transform| { write_image::( draw_target, snapshot, dest_rect, smoothing_enabled, composition_options, transform, ); }; if shadow_options.need_to_draw_shadow() { let rect = Rect::new( Point2D::new(dest_rect.origin.x as f32, dest_rect.origin.y as f32), Size2D::new(dest_rect.size.width as f32, dest_rect.size.height as f32), ); self.draw_with_shadow( &rect, shadow_options, composition_options, transform, writer, ); } else { writer(&mut self.drawtarget, transform); } } pub(crate) fn fill_text( &mut self, text_bounds: Rect, text_runs: Vec, fill_or_stroke_style: FillOrStrokeStyle, _shadow_options: ShadowOptions, composition_options: CompositionOptions, transform: Transform2D, ) { self.maybe_bound_shape_with_pattern( fill_or_stroke_style, composition_options, &text_bounds, transform, |self_, style| { self_ .drawtarget .fill_text(text_runs, style, composition_options, transform); }, ); } pub(crate) fn stroke_text( &mut self, text_bounds: Rect, text_runs: Vec, fill_or_stroke_style: FillOrStrokeStyle, line_options: LineOptions, _shadow_options: ShadowOptions, composition_options: CompositionOptions, transform: Transform2D, ) { self.maybe_bound_shape_with_pattern( fill_or_stroke_style, composition_options, &text_bounds, transform, |self_, style| { self_.drawtarget.stroke_text( text_runs, style, line_options, composition_options, transform, ); }, ); } pub(crate) fn fill_rect( &mut self, rect: &Rect, style: FillOrStrokeStyle, shadow_options: ShadowOptions, composition_options: CompositionOptions, transform: Transform2D, ) { if style.is_zero_size_gradient() { return; // Paint nothing if gradient size is zero. } if shadow_options.need_to_draw_shadow() { self.draw_with_shadow( rect, shadow_options, composition_options, transform, |new_draw_target, transform| { new_draw_target.fill_rect(rect, style, composition_options, transform); }, ); } else { self.maybe_bound_shape_with_pattern( style, composition_options, &rect.cast(), transform, |self_, style| { self_ .drawtarget .fill_rect(rect, style, composition_options, transform); }, ); } } pub(crate) fn clear_rect(&mut self, rect: &Rect, transform: Transform2D) { self.drawtarget.clear_rect(rect, transform); } pub(crate) fn stroke_rect( &mut self, rect: &Rect, style: FillOrStrokeStyle, line_options: LineOptions, shadow_options: ShadowOptions, composition_options: CompositionOptions, transform: Transform2D, ) { if style.is_zero_size_gradient() { return; // Paint nothing if gradient size is zero. } if shadow_options.need_to_draw_shadow() { self.draw_with_shadow( rect, shadow_options, composition_options, transform, |new_draw_target, transform| { new_draw_target.stroke_rect( rect, style, line_options, composition_options, transform, ); }, ); } else { self.maybe_bound_shape_with_pattern( style, composition_options, &rect.cast(), transform, |self_, style| { self_.drawtarget.stroke_rect( rect, style, line_options, composition_options, transform, ); }, ) } } pub(crate) fn fill_path( &mut self, path: &Path, fill_rule: FillRule, style: FillOrStrokeStyle, _shadow_options: ShadowOptions, composition_options: CompositionOptions, transform: Transform2D, ) { if style.is_zero_size_gradient() { return; // Paint nothing if gradient size is zero. } self.maybe_bound_shape_with_pattern( style, composition_options, &path.bounding_box(), transform, |self_, style| { self_ .drawtarget .fill(path, fill_rule, style, composition_options, transform) }, ) } pub(crate) fn stroke_path( &mut self, path: &Path, style: FillOrStrokeStyle, line_options: LineOptions, _shadow_options: ShadowOptions, composition_options: CompositionOptions, transform: Transform2D, ) { if style.is_zero_size_gradient() { return; // Paint nothing if gradient size is zero. } self.maybe_bound_shape_with_pattern( style, composition_options, &path.bounding_box(), transform, |self_, style| { self_ .drawtarget .stroke(path, style, line_options, composition_options, transform); }, ) } pub(crate) fn clip_path( &mut self, path: &Path, fill_rule: FillRule, transform: Transform2D, ) { self.drawtarget.push_clip(path, fill_rule, transform); } /// pub(crate) fn recreate(&mut self, size: Option>) { let size = size .unwrap_or_else(|| self.drawtarget.get_size().to_u64()) .max(MIN_WR_IMAGE_SIZE); // Step 1. Clear canvas's bitmap to transparent black. self.drawtarget = self .drawtarget .create_similar_draw_target(&Size2D::new(size.width, size.height).cast()); self.update_image_rendering(None); } /// Update image in WebRender pub(crate) fn update_image_rendering(&mut self, canvas_epoch: Option) { let (descriptor, data) = { #[cfg(feature = "tracing")] let _span = tracing::trace_span!( "image_descriptor_and_serializable_data", servo_profiling = true, ) .entered(); self.drawtarget.image_descriptor_and_serializable_data() }; self.compositor_api .update_image(self.image_key, descriptor, data, canvas_epoch); } // https://html.spec.whatwg.org/multipage/#dom-context-2d-putimagedata pub(crate) fn put_image_data(&mut self, snapshot: Snapshot, rect: Rect) { assert_eq!(rect.size, snapshot.size()); let source_surface = self .drawtarget .create_source_surface_from_data(snapshot) .unwrap(); self.drawtarget.copy_surface( source_surface, Rect::from_size(rect.size.to_i32()), rect.origin.to_i32(), ); } fn create_draw_target_for_shadow(&self, source_rect: &Rect) -> DrawTarget { self.drawtarget.create_similar_draw_target(&Size2D::new( source_rect.size.width as i32, source_rect.size.height as i32, )) } fn draw_with_shadow( &self, rect: &Rect, shadow_options: ShadowOptions, composition_options: CompositionOptions, transform: Transform2D, draw_shadow_source: F, ) where F: FnOnce(&mut DrawTarget, Transform2D), { let shadow_src_rect = transform.outer_transformed_rect(&rect.cast()); // Because this comes from the rect on f32 precision, casting it down should be ok. let mut new_draw_target = self.create_draw_target_for_shadow(&shadow_src_rect.cast()); let shadow_transform = transform .then(&Transform2D::identity().pre_translate(-shadow_src_rect.origin.to_vector())); draw_shadow_source(&mut new_draw_target, shadow_transform); self.drawtarget.draw_surface_with_shadow( new_draw_target.surface(), &Point2D::new( shadow_src_rect.origin.x as f32, shadow_src_rect.origin.y as f32, ), shadow_options, composition_options, ); } /// Push a clip to the draw target to respect the non-repeating bound (either x, y, or both) /// of the given pattern. fn maybe_bound_shape_with_pattern( &mut self, style: FillOrStrokeStyle, composition_options: CompositionOptions, path_bound_box: &Rect, transform: Transform2D, draw_shape: F, ) where F: FnOnce(&mut Self, FillOrStrokeStyle), { let x_bound = style.x_bound(); let y_bound = style.y_bound(); // Clear operations are also unbounded. if matches!( composition_options.composition_operation, CompositionOrBlending::Composition(CompositionStyle::Clear) ) || (x_bound.is_none() && y_bound.is_none()) { draw_shape(self, style); return; } let rect = Rect::from_size(Size2D::new( x_bound.unwrap_or(path_bound_box.size.width.ceil() as u32), y_bound.unwrap_or(path_bound_box.size.height.ceil() as u32), )) .cast(); let rect = transform.outer_transformed_rect(&rect); self.drawtarget.push_clip_rect(&rect.cast()); draw_shape(self, style); self.drawtarget.pop_clip(); } /// It reads image data from the canvas /// canvas_size: The size of the canvas we're reading from /// read_rect: The area of the canvas we want to read from #[servo_tracing::instrument(skip_all)] pub(crate) fn read_pixels(&mut self, read_rect: Option>) -> Snapshot { let canvas_size = self.drawtarget.get_size().cast(); if let Some(read_rect) = read_rect { let canvas_rect = Rect::from_size(canvas_size); if canvas_rect .intersection(&read_rect) .is_none_or(|rect| rect.is_empty()) { Snapshot::empty() } else { self.drawtarget.snapshot().get_rect(read_rect) } } else { self.drawtarget.snapshot() } } pub(crate) fn pop_clips(&mut self, clips: usize) { for _ in 0..clips { self.drawtarget.pop_clip(); } } } impl Drop for CanvasData { fn drop(&mut self) { self.compositor_api.delete_image(self.image_key); } } /// It writes an image to the destination target /// draw_target: the destination target where the image_data will be copied /// image_data: Pixel information of the image to be written. It takes RGBA8 /// image_size: The size of the image to be written /// dest_rect: Area of the destination target where the pixels will be copied /// smoothing_enabled: It determines if smoothing is applied to the image result /// premultiply: Determines whenever the image data should be premultiplied or not fn write_image( draw_target: &mut DrawTarget, snapshot: Snapshot, dest_rect: Rect, smoothing_enabled: bool, composition_options: CompositionOptions, transform: Transform2D, ) { if snapshot.size().is_empty() { return; } let image_rect = Rect::new(Point2D::zero(), snapshot.size().cast()); // From spec https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage // When scaling up, if the imageSmoothingEnabled attribute is set to true, the user agent should attempt // to apply a smoothing algorithm to the image data when it is scaled. // Otherwise, the image must be rendered using nearest-neighbor interpolation. let filter = if smoothing_enabled { Filter::Bilinear } else { Filter::Nearest }; let source_surface = draw_target .create_source_surface_from_data(snapshot) .unwrap(); draw_target.draw_surface( source_surface, dest_rect, image_rect, filter, composition_options, transform, ); } pub(crate) trait RectToi32 { fn ceil(&self) -> Rect; } impl RectToi32 for Rect { fn ceil(&self) -> Rect { Rect::new( Point2D::new(self.origin.x.ceil(), self.origin.y.ceil()), Size2D::new(self.size.width.ceil(), self.size.height.ceil()), ) } }