Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Greatly improve capture_image_area on D2D, CG, and Cairo #513

56 changes: 53 additions & 3 deletions piet-cairo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod text;

use std::borrow::Cow;

use cairo::{Context, Filter, Format, ImageSurface, Matrix, SurfacePattern};
use cairo::{Context, Filter, Format, ImageSurface, Matrix, Rectangle, SurfacePattern};

use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size};
use piet::{
Expand Down Expand Up @@ -328,8 +328,47 @@ impl<'a> RenderContext for CairoRenderContext<'a> {
self.draw_image_inner(&image.0, Some(src_rect.into()), dst_rect.into(), interp);
}

fn capture_image_area(&mut self, _src_rect: impl Into<Rect>) -> Result<Self::Image, Error> {
Err(Error::Unimplemented)
fn capture_image_area(&mut self, src_rect: impl Into<Rect>) -> Result<Self::Image, Error> {
let src_rect: Rect = src_rect.into();

// In order to capture the correct image area, we first need to convert from
// user space (the logical rectangle) to device space (the "physical" rectangle).
// For example, in a HiDPI (2x) setting, a user-space rectangle of 20x20 would be
// 40x40 in device space.
let user_rect = Rectangle {
x: src_rect.x0,
y: src_rect.y0,
width: src_rect.width(),
height: src_rect.height(),
};
let device_rect = self.user_to_device(&user_rect);

// This is the surface to which we draw the captured image area
let target_surface = ImageSurface::create(
Format::ARgb32,
device_rect.width as i32,
device_rect.height as i32,
)
.map_err(convert_error)?;
let target_ctx = Context::new(&target_surface).map_err(convert_error)?;

// Since we (potentially) don't want to capture the entire surface, we crop the
// source surface to the requested "sub-surface" using `create_for_rectangle`.
let cropped_source_surface = self
.ctx
.target()
.create_for_rectangle(device_rect)
.map_err(convert_error)?;

// Finally, we fill the entirety of the target surface (via the target context)
// with the select region of the source surface.
target_ctx
.set_source_surface(&cropped_source_surface, 0.0, 0.0)
.map_err(convert_error)?;
target_ctx.rectangle(0.0, 0.0, device_rect.width, device_rect.height);
target_ctx.fill().map_err(convert_error)?;

Ok(CairoImage(target_surface))
}

fn blurred_rect(&mut self, rect: Rect, blur_radius: f64, brush: &impl IntoBrush<Self>) {
Expand Down Expand Up @@ -478,6 +517,17 @@ impl<'a> CairoRenderContext<'a> {
Ok(())
});
}

fn user_to_device(&self, user_rect: &Rectangle) -> Rectangle {
let (x, y) = self.ctx.user_to_device(user_rect.x, user_rect.y);
let (width, height) = self.ctx.user_to_device(user_rect.width, user_rect.height);
Rectangle {
x,
y,
width,
height,
}
}
}

fn convert_line_cap(line_cap: LineCap) -> cairo::LineCap {
Expand Down
21 changes: 15 additions & 6 deletions piet-coregraphics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,26 +390,35 @@ impl<'a> RenderContext for CoreGraphicsContext<'a> {

fn capture_image_area(&mut self, src_rect: impl Into<Rect>) -> Result<Self::Image, Error> {
let src_rect = src_rect.into();
if src_rect.width() < 1.0 || src_rect.height() < 1.0 {

// When creating a CoreGraphicsContext, a transformation matrix is applied to map
// between piet's coordinate system and CoreGraphic's coordinate system
// (see [`CoreGraphicsContext::new_impl`] for details). Since the `src_rect` we receive
// as parameter is in piet's coordinate system, we need to first convert it to the CG one,
// as otherwise our captured image area would be wrong.
let transformation_matrix = self.ctx.get_ctm();
let src_cgrect = to_cgrect(src_rect).apply_transform(&transformation_matrix);

if src_cgrect.size.width < 1.0 || src_cgrect.size.height < 1.0 {
return Err(Error::InvalidInput);
}

if src_rect.width() > self.ctx.width() as f64
|| src_rect.height() > self.ctx.height() as f64
if src_cgrect.size.width > self.ctx.width() as f64
|| src_cgrect.size.height > self.ctx.height() as f64
{
return Err(Error::InvalidInput);
}

let full_image = self.ctx.create_image().ok_or(Error::InvalidInput)?;

if src_rect.width().round() as usize == self.ctx.width()
&& src_rect.height().round() as usize == self.ctx.height()
if src_cgrect.size.width.round() as usize == self.ctx.width()
&& src_cgrect.size.height.round() as usize == self.ctx.height()
{
return Ok(CoreGraphicsImage::NonEmpty(full_image));
}

full_image
.cropped(to_cgrect(src_rect))
.cropped(src_cgrect)
.map(CoreGraphicsImage::NonEmpty)
.ok_or(Error::InvalidInput)
}
Expand Down
12 changes: 12 additions & 0 deletions piet-direct2d/src/conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ pub(crate) fn affine_to_matrix3x2f(affine: Affine) -> D2D1_MATRIX_3X2_F {
}
}

// TODO: Needs tests
pub(crate) fn matrix3x2f_to_affine(matrix: D2D1_MATRIX_3X2_F) -> Affine {
Affine::new([
matrix.matrix[0][0] as f64,
matrix.matrix[0][1] as f64,
matrix.matrix[1][0] as f64,
matrix.matrix[1][1] as f64,
matrix.matrix[2][0] as f64,
matrix.matrix[2][1] as f64,
])
}

// TODO: consider adding to kurbo.
pub(crate) fn rect_to_rectf(rect: Rect) -> D2D1_RECT_F {
D2D1_RECT_F {
Expand Down
10 changes: 10 additions & 0 deletions piet-direct2d/src/d2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,16 @@ impl DeviceContext {
}
}

pub fn get_dpi_scale(&self) -> (f32, f32) {
let mut dpi_x = 0.0f32;
let mut dpi_y = 0.0f32;
unsafe {
self.0.GetDpi(&mut dpi_x, &mut dpi_y);
}
// https://docs.microsoft.com/en-us/windows/win32/direct2d/direct2d-and-high-dpi
(dpi_x / 96., dpi_y / 96.)
}

/// Begin drawing.
///
/// This must be done before any piet drawing operations.
Expand Down
41 changes: 30 additions & 11 deletions piet-direct2d/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub use crate::text::{D2DLoadedFonts, D2DText, D2DTextLayout, D2DTextLayoutBuild

use crate::conv::{
affine_to_matrix3x2f, color_to_colorf, convert_stroke_style, gradient_stop_to_d2d,
rect_to_rectf, rect_to_rectu, to_point2f, to_point2u,
matrix3x2f_to_affine, rect_to_rectf, rect_to_rectu, to_point2f, to_point2u,
};
use crate::d2d::{Bitmap, Brush, DeviceContext, FillRule, Geometry};

Expand Down Expand Up @@ -335,6 +335,7 @@ impl<'a> RenderContext for D2DRenderContext<'a> {
// Move this code into impl to avoid duplication with transform?
self.rt
.set_transform(&affine_to_matrix3x2f(self.current_transform()));

Ok(())
}

Expand Down Expand Up @@ -456,21 +457,39 @@ impl<'a> RenderContext for D2DRenderContext<'a> {
fn capture_image_area(&mut self, rect: impl Into<Rect>) -> Result<Self::Image, Error> {
let r = rect.into();

let (dpi_scale, _) = self.rt.get_dpi_scale();
let dpi_scale = dpi_scale as f64;

let transform_matrix = self.rt.get_transform();
let affine_transform = matrix3x2f_to_affine(transform_matrix);

let device_size = Point {
x: r.width() * dpi_scale,
y: r.height() * dpi_scale,
};
// TODO: This transformation is untested with the current test pictures
let device_size = affine_transform * device_size;
x3ro marked this conversation as resolved.
Show resolved Hide resolved
let device_size = device_size.to_vec2().to_size();

let device_origin = Point {
x: r.x0 * dpi_scale,
y: r.y0 * dpi_scale,
};
// TODO: This transformation is untested with the current test pictures
let device_origin = affine_transform * device_origin;

let mut target_bitmap = self.rt.create_blank_bitmap(
r.width() as usize,
r.height() as usize,
device_size.width as usize,
device_size.height as usize,
D2D1_ALPHA_MODE_PREMULTIPLIED,
)?;

let dest_point = to_point2u((0.0f32, 0.0f32));
let src_rect = rect_to_rectu(Rect {
x0: r.x0,
y0: r.y0,
x1: r.width(),
y1: r.height(),
});
let src_rect = Rect::from_origin_size(device_origin, device_size);

let d2d_dest_point = to_point2u((0.0f32, 0.0f32));
let d2d_src_rect = rect_to_rectu(src_rect);
target_bitmap.copy_from_render_target(d2d_dest_point, self.rt, d2d_src_rect);

target_bitmap.copy_from_render_target(dest_point, self.rt, src_rect);
Ok(target_bitmap)
}

Expand Down
4 changes: 3 additions & 1 deletion piet/src/samples/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod picture_12;
mod picture_13;
mod picture_14;
mod picture_15;
mod picture_16;

type BoxErr = Box<dyn std::error::Error>;

Expand All @@ -36,7 +37,7 @@ type BoxErr = Box<dyn std::error::Error>;
pub const DEFAULT_SCALE: f64 = 2.0;

/// The total number of samples in this module.
pub const SAMPLE_COUNT: usize = 16;
pub const SAMPLE_COUNT: usize = 17;

/// file we save an os fingerprint to
pub const GENERATED_BY: &str = "GENERATED_BY";
Expand All @@ -60,6 +61,7 @@ pub fn get<R: RenderContext>(number: usize) -> Result<SamplePicture<R>, BoxErr>
13 => SamplePicture::new(picture_13::SIZE, picture_13::draw),
14 => SamplePicture::new(picture_14::SIZE, picture_14::draw),
15 => SamplePicture::new(picture_15::SIZE, picture_15::draw),
16 => SamplePicture::new(picture_16::SIZE, picture_16::draw),
x3ro marked this conversation as resolved.
Show resolved Hide resolved
_ => return Err(format!("No sample #{} exists", number).into()),
})
}
Expand Down
42 changes: 42 additions & 0 deletions piet/src/samples/picture_16.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! capture_image_rect
//!
//! This tests makes sure that copying part of an image works

use crate::kurbo::{Rect, Size};
use crate::{Color, Error, InterpolationMode, RenderContext};

pub const SIZE: Size = Size::new(200., 200.);

const RED: Color = Color::rgb8(255, 0, 0);
const BLUE: Color = Color::rgb8(0, 0, 255);
const INTERPOLATION_MODE: InterpolationMode = InterpolationMode::NearestNeighbor;
const BORDER_WIDTH: f64 = 4.0;

pub fn draw<R: RenderContext>(rc: &mut R) -> Result<(), Error> {
rc.clear(None, Color::FUCHSIA);

let outer_rect_red = Rect::new(20., 20., 180., 180.);
let inner_rect_blue = outer_rect_red.inset(-BORDER_WIDTH);

// Draw a box with a red border
rc.fill(outer_rect_red, &RED);
rc.fill(inner_rect_blue, &BLUE);

// Cache the box, clear the image and re-draw the box from the cache
let cache = rc.capture_image_area(outer_rect_red).unwrap();
rc.clear(None, Color::BLACK);
rc.draw_image(&cache, outer_rect_red, INTERPOLATION_MODE);

// Draw the cached image, scaled, in all four corners of the image
let top_left_corner = Rect::from_origin_size((5., 5.), (40., 40.));
let top_right_corner = Rect::from_origin_size((155., 5.), (40., 40.));
let bottom_left_corner = Rect::from_origin_size((5., 155.), (40., 40.));
let bottom_right_corner = Rect::from_origin_size((155., 155.), (40., 40.));

rc.draw_image(&cache, top_left_corner, INTERPOLATION_MODE);
rc.draw_image(&cache, top_right_corner, INTERPOLATION_MODE);
rc.draw_image(&cache, bottom_left_corner, INTERPOLATION_MODE);
rc.draw_image(&cache, bottom_right_corner, INTERPOLATION_MODE);

Ok(())
}