#![deny(clippy::trivially_copy_pass_by_ref)]
mod ct_helpers;
mod gradient;
mod text;
use std::borrow::Cow;
use std::sync::Arc;
use core_graphics::base::{
kCGImageAlphaLast, kCGImageAlphaPremultipliedLast, kCGRenderingIntentDefault, CGFloat,
};
use core_graphics::color_space::CGColorSpace;
use core_graphics::context::{CGContextRef, CGInterpolationQuality, CGLineCap, CGLineJoin};
use core_graphics::data_provider::CGDataProvider;
use core_graphics::geometry::{CGAffineTransform, CGPoint, CGRect, CGSize};
use core_graphics::gradient::CGGradientDrawingOptions;
use core_graphics::image::CGImage;
use foreign_types::ForeignTypeRef;
use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size};
use piet::{
Color, Error, FixedGradient, Image, ImageFormat, InterpolationMode, IntoBrush, LineCap,
LineJoin, RenderContext, RoundInto, StrokeStyle,
};
pub use crate::text::{CoreGraphicsText, CoreGraphicsTextLayout, CoreGraphicsTextLayoutBuilder};
use gradient::Gradient;
const GRADIENT_DRAW_BEFORE_AND_AFTER: CGGradientDrawingOptions =
CGGradientDrawingOptions::from_bits_truncate(
CGGradientDrawingOptions::CGGradientDrawsAfterEndLocation.bits()
| CGGradientDrawingOptions::CGGradientDrawsBeforeStartLocation.bits(),
);
pub struct CoreGraphicsContext<'a> {
ctx: &'a mut CGContextRef,
text: CoreGraphicsText,
transform_stack: Vec<Affine>,
y_down: bool,
height: f64,
}
impl<'a> CoreGraphicsContext<'a> {
pub fn new_y_up(
ctx: &mut CGContextRef,
height: f64,
text: Option<CoreGraphicsText>,
) -> CoreGraphicsContext {
Self::new_impl(ctx, Some(height), text, false)
}
pub fn new_y_down(
ctx: &mut CGContextRef,
text: Option<CoreGraphicsText>,
) -> CoreGraphicsContext {
Self::new_impl(ctx, None, text, true)
}
fn new_impl(
ctx: &mut CGContextRef,
height: Option<f64>,
text: Option<CoreGraphicsText>,
y_down: bool,
) -> CoreGraphicsContext {
ctx.save();
if let Some(height) = height {
let xform = Affine::FLIP_Y * Affine::translate((0.0, -height));
ctx.concat_ctm(to_cgaffine(xform));
}
let text = text.unwrap_or_else(CoreGraphicsText::new_with_unique_state);
CoreGraphicsContext {
ctx,
text,
transform_stack: Vec::new(),
y_down,
height: height.unwrap_or_default(),
}
}
}
impl<'a> Drop for CoreGraphicsContext<'a> {
fn drop(&mut self) {
self.ctx.restore();
}
}
#[derive(Clone)]
pub enum Brush {
Solid(Color),
Gradient(Gradient),
}
#[derive(Clone)]
pub enum CoreGraphicsImage {
Empty,
YUp(CGImage),
YDown(CGImage),
}
impl CoreGraphicsImage {
fn from_cgimage_and_ydir(image: CGImage, y_down: bool) -> Self {
match y_down {
true => CoreGraphicsImage::YDown(image),
false => CoreGraphicsImage::YUp(image),
}
}
pub fn as_cgimage(&self) -> Option<&CGImage> {
match self {
CoreGraphicsImage::Empty => None,
CoreGraphicsImage::YUp(image) | CoreGraphicsImage::YDown(image) => Some(image),
}
}
}
impl<'a> RenderContext for CoreGraphicsContext<'a> {
type Brush = Brush;
type Text = CoreGraphicsText;
type TextLayout = CoreGraphicsTextLayout;
type Image = CoreGraphicsImage;
fn clear(&mut self, region: impl Into<Option<Rect>>, color: Color) {
let _ = self.save();
unsafe {
CGContextResetClip(self.ctx.as_ptr());
}
let current_xform = self.current_transform();
let xform = current_xform.inverse();
self.transform(xform);
let region = region
.into()
.map(to_cgrect)
.unwrap_or_else(|| self.ctx.clip_bounding_box());
let (r, g, b, a) = color.as_rgba();
self.ctx
.set_blend_mode(core_graphics::context::CGBlendMode::Copy);
self.ctx.set_rgb_fill_color(r, g, b, a);
self.ctx.fill_rect(region);
self.restore().unwrap();
}
fn solid_brush(&mut self, color: Color) -> Brush {
Brush::Solid(color)
}
fn gradient(&mut self, gradient: impl Into<FixedGradient>) -> Result<Brush, Error> {
let gradient = Gradient::from_piet_gradient(gradient.into());
Ok(Brush::Gradient(gradient))
}
fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
let brush = brush.make_brush(self, || shape.bounding_box());
self.set_path(shape);
match brush.as_ref() {
Brush::Solid(color) => {
self.set_fill_color(*color);
self.ctx.fill_path();
}
Brush::Gradient(grad) => {
self.ctx.save();
self.ctx.clip();
grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
self.ctx.restore();
}
}
}
fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
let brush = brush.make_brush(self, || shape.bounding_box());
self.set_path(shape);
match brush.as_ref() {
Brush::Solid(color) => {
self.set_fill_color(*color);
self.ctx.fill_path();
}
Brush::Gradient(grad) => {
self.ctx.save();
self.ctx.eo_clip();
grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
self.ctx.restore();
}
}
}
fn clip(&mut self, shape: impl Shape) {
self.set_path(shape);
self.ctx.clip();
}
fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
let brush = brush.make_brush(self, || shape.bounding_box());
self.set_path(shape);
self.set_stroke(width.round_into(), None);
match brush.as_ref() {
Brush::Solid(color) => {
self.set_stroke_color(*color);
self.ctx.stroke_path();
}
Brush::Gradient(grad) => {
self.ctx.save();
self.ctx.replace_path_with_stroked_path();
self.ctx.clip();
grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
self.ctx.restore();
}
}
}
fn stroke_styled(
&mut self,
shape: impl Shape,
brush: &impl IntoBrush<Self>,
width: f64,
style: &StrokeStyle,
) {
let brush = brush.make_brush(self, || shape.bounding_box());
self.set_path(shape);
self.set_stroke(width.round_into(), Some(style));
match brush.as_ref() {
Brush::Solid(color) => {
self.set_stroke_color(*color);
self.ctx.stroke_path();
}
Brush::Gradient(grad) => {
self.ctx.save();
self.ctx.replace_path_with_stroked_path();
self.ctx.clip();
grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
self.ctx.restore();
}
}
}
fn text(&mut self) -> &mut Self::Text {
&mut self.text
}
fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into<Point>) {
let pos = pos.into();
self.ctx.save();
self.ctx.translate(pos.x, layout.frame_size.height + pos.y);
self.ctx.scale(1.0, -1.0);
layout.draw(self.ctx);
self.ctx.restore();
}
fn save(&mut self) -> Result<(), Error> {
self.ctx.save();
let state = self.transform_stack.last().copied().unwrap_or_default();
self.transform_stack.push(state);
Ok(())
}
fn restore(&mut self) -> Result<(), Error> {
if self.transform_stack.pop().is_some() {
self.ctx.restore();
Ok(())
} else {
Err(Error::StackUnbalance)
}
}
fn finish(&mut self) -> Result<(), Error> {
Ok(())
}
fn transform(&mut self, transform: Affine) {
if let Some(last) = self.transform_stack.last_mut() {
*last *= transform;
} else {
self.transform_stack.push(transform);
}
self.ctx.concat_ctm(to_cgaffine(transform));
}
fn make_image(
&mut self,
width: usize,
height: usize,
buf: &[u8],
format: ImageFormat,
) -> Result<Self::Image, Error> {
if width == 0 || height == 0 {
return Ok(CoreGraphicsImage::Empty);
}
assert!(!buf.is_empty() && buf.len() <= format.bytes_per_pixel() * width * height);
let data = Arc::new(buf.to_owned());
let data_provider = CGDataProvider::from_buffer(data);
let (colorspace, bitmap_info, bytes) = match format {
ImageFormat::Rgb => (CGColorSpace::create_device_rgb(), 0, 3),
ImageFormat::RgbaPremul => (
CGColorSpace::create_device_rgb(),
kCGImageAlphaPremultipliedLast,
4,
),
ImageFormat::RgbaSeparate => (CGColorSpace::create_device_rgb(), kCGImageAlphaLast, 4),
ImageFormat::Grayscale => (CGColorSpace::create_device_gray(), 0, 1),
_ => unimplemented!(),
};
let bits_per_component = 8;
let should_interpolate = false;
let rendering_intent = kCGRenderingIntentDefault;
let image = CGImage::new(
width,
height,
bits_per_component,
bytes * bits_per_component,
width * bytes,
&colorspace,
bitmap_info,
&data_provider,
should_interpolate,
rendering_intent,
);
Ok(CoreGraphicsImage::from_cgimage_and_ydir(image, self.y_down))
}
fn draw_image(
&mut self,
src_image: &Self::Image,
rect: impl Into<Rect>,
interp: InterpolationMode,
) {
let image_y_down: bool;
let image = match src_image {
CoreGraphicsImage::YDown(img) => {
image_y_down = true;
img
}
CoreGraphicsImage::YUp(img) => {
image_y_down = false;
img
}
CoreGraphicsImage::Empty => return,
};
self.ctx.save();
let quality = match interp {
InterpolationMode::NearestNeighbor => {
CGInterpolationQuality::CGInterpolationQualityNone
}
InterpolationMode::Bilinear => CGInterpolationQuality::CGInterpolationQualityDefault,
};
self.ctx.set_interpolation_quality(quality);
let rect = rect.into();
if self.y_down && !image_y_down {
self.ctx.draw_image(to_cgrect(rect), image);
} else {
self.ctx.translate(rect.min_x(), rect.max_y());
self.ctx.scale(1.0, -1.0);
self.ctx
.draw_image(to_cgrect(rect.with_origin(Point::ZERO)), image);
}
self.ctx.restore();
}
fn draw_image_area(
&mut self,
image: &Self::Image,
src_rect: impl Into<Rect>,
dst_rect: impl Into<Rect>,
interp: InterpolationMode,
) {
if let CoreGraphicsImage::YDown(image) = image {
if let Some(cropped) = image.cropped(to_cgrect(src_rect)) {
self.draw_image(&CoreGraphicsImage::YDown(cropped), dst_rect, interp);
}
} else if let CoreGraphicsImage::YUp(image) = image {
if let Some(cropped) = image.cropped(to_cgrect(src_rect)) {
self.draw_image(&CoreGraphicsImage::YUp(cropped), dst_rect, interp);
}
}
}
fn capture_image_area(&mut self, src_rect: impl Into<Rect>) -> Result<Self::Image, Error> {
let src_rect = src_rect.into();
let src_cgrect = if self.y_down {
let matrix = self.ctx.get_ctm();
to_cgrect(src_rect).apply_transform(&matrix)
} else {
let y_dir_adjusted_src_rect = Rect::new(
src_rect.x0,
self.height - src_rect.y0,
src_rect.x1,
self.height - src_rect.y1,
);
let matrix = self.ctx.get_ctm();
to_cgrect(y_dir_adjusted_src_rect).apply_transform(&matrix)
};
if src_cgrect.size.width < 1.0 || src_cgrect.size.height < 1.0 {
return Err(Error::InvalidInput);
}
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_cgrect.size.width.round() as usize == self.ctx.width()
&& src_cgrect.size.height.round() as usize == self.ctx.height()
{
return Ok(CoreGraphicsImage::from_cgimage_and_ydir(
full_image,
self.y_down,
));
}
let cropped_image_result = full_image.cropped(src_cgrect);
if let Some(image) = cropped_image_result {
let cropped_image_size = Size::new(src_cgrect.size.width, src_cgrect.size.height);
let cropped_image_rect = Rect::from_origin_size(Point::ZERO, cropped_image_size);
let cropped_image_context = core_graphics::context::CGContext::create_bitmap_context(
None,
cropped_image_size.width as usize,
cropped_image_size.height as usize,
8,
0,
&core_graphics::color_space::CGColorSpace::create_device_rgb(),
core_graphics::base::kCGImageAlphaPremultipliedLast,
);
cropped_image_context.draw_image(to_cgrect(cropped_image_rect), &image);
let cropped_image = cropped_image_context
.create_image()
.expect("Failed to capture cropped image from resize context");
Ok(CoreGraphicsImage::from_cgimage_and_ydir(
cropped_image,
self.y_down,
))
} else {
Err(Error::InvalidInput)
}
}
fn blurred_rect(&mut self, rect: Rect, blur_radius: f64, brush: &impl IntoBrush<Self>) {
let (image, rect) = compute_blurred_rect(rect, blur_radius);
let cg_rect = to_cgrect(rect);
self.ctx.save();
self.ctx.clip_to_mask(cg_rect, &image);
self.fill(rect, brush);
self.ctx.restore()
}
fn current_transform(&self) -> Affine {
self.transform_stack.last().copied().unwrap_or_default()
}
fn status(&mut self) -> Result<(), Error> {
Ok(())
}
}
impl<'a> IntoBrush<CoreGraphicsContext<'a>> for Brush {
fn make_brush<'b>(
&'b self,
_piet: &mut CoreGraphicsContext,
_bbox: impl FnOnce() -> Rect,
) -> std::borrow::Cow<'b, Brush> {
Cow::Borrowed(self)
}
}
impl Image for CoreGraphicsImage {
fn size(&self) -> Size {
match self {
CoreGraphicsImage::Empty => Size::new(0., 0.),
CoreGraphicsImage::YDown(image) | CoreGraphicsImage::YUp(image) => {
Size::new(image.width() as f64, image.height() as f64)
}
}
}
}
fn convert_line_join(line_join: LineJoin) -> CGLineJoin {
match line_join {
LineJoin::Miter { .. } => CGLineJoin::CGLineJoinMiter,
LineJoin::Round => CGLineJoin::CGLineJoinRound,
LineJoin::Bevel => CGLineJoin::CGLineJoinBevel,
}
}
fn convert_line_cap(line_cap: LineCap) -> CGLineCap {
match line_cap {
LineCap::Butt => CGLineCap::CGLineCapButt,
LineCap::Round => CGLineCap::CGLineCapRound,
LineCap::Square => CGLineCap::CGLineCapSquare,
}
}
impl<'a> CoreGraphicsContext<'a> {
fn set_fill_color(&mut self, color: Color) {
let (r, g, b, a) = Color::as_rgba(color);
self.ctx.set_rgb_fill_color(r, g, b, a);
}
fn set_stroke_color(&mut self, color: Color) {
let (r, g, b, a) = Color::as_rgba(color);
self.ctx.set_rgb_stroke_color(r, g, b, a);
}
fn set_stroke(&mut self, width: f64, style: Option<&StrokeStyle>) {
let default_style = StrokeStyle::default();
let style = style.unwrap_or(&default_style);
self.ctx.set_line_width(width);
self.ctx.set_line_join(convert_line_join(style.line_join));
self.ctx.set_line_cap(convert_line_cap(style.line_cap));
if let Some(limit) = style.miter_limit() {
self.ctx.set_miter_limit(limit);
}
self.ctx
.set_line_dash(style.dash_offset, &style.dash_pattern);
}
fn set_path(&mut self, shape: impl Shape) {
self.ctx.begin_path();
let mut last = Point::default();
for el in shape.path_elements(1e-3) {
match el {
PathEl::MoveTo(p) => {
self.ctx.move_to_point(p.x, p.y);
last = p;
}
PathEl::LineTo(p) => {
self.ctx.add_line_to_point(p.x, p.y);
last = p;
}
PathEl::QuadTo(p1, p2) => {
let q = QuadBez::new(last, p1, p2);
let c = q.raise();
self.ctx
.add_curve_to_point(c.p1.x, c.p1.y, c.p2.x, c.p2.y, p2.x, p2.y);
last = p2;
}
PathEl::CurveTo(p1, p2, p3) => {
self.ctx
.add_curve_to_point(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
last = p3;
}
PathEl::ClosePath => self.ctx.close_path(),
}
}
}
}
fn compute_blurred_rect(rect: Rect, radius: f64) -> (CGImage, Rect) {
let size = piet::util::size_for_blurred_rect(rect, radius);
let width = size.width as usize;
let height = size.height as usize;
let mut data = vec![0u8; width * height];
let rect_exp = piet::util::compute_blurred_rect(rect, radius, width, &mut data);
let data_provider = CGDataProvider::from_buffer(Arc::new(data));
let color_space = CGColorSpace::create_device_gray();
let image = CGImage::new(
width,
height,
8,
8,
width,
&color_space,
0,
&data_provider,
false,
0,
);
(image, rect_exp)
}
fn to_cgpoint(point: Point) -> CGPoint {
CGPoint::new(point.x as CGFloat, point.y as CGFloat)
}
fn to_cgsize(size: Size) -> CGSize {
CGSize::new(size.width, size.height)
}
fn to_cgrect(rect: impl Into<Rect>) -> CGRect {
let rect = rect.into();
CGRect::new(&to_cgpoint(rect.origin()), &to_cgsize(rect.size()))
}
fn to_cgaffine(affine: Affine) -> CGAffineTransform {
let [a, b, c, d, tx, ty] = affine.as_coeffs();
CGAffineTransform::new(a, b, c, d, tx, ty)
}
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
fn CGContextResetClip(c: core_graphics::sys::CGContextRef);
}
#[cfg(test)]
mod tests {
use super::*;
use core_graphics::color_space::CGColorSpace;
use core_graphics::context::CGContext;
fn make_context(size: impl Into<Size>) -> CGContext {
let size = size.into();
CGContext::create_bitmap_context(
None,
size.width as usize,
size.height as usize,
8,
0,
&CGColorSpace::create_device_rgb(),
core_graphics::base::kCGImageAlphaPremultipliedLast,
)
}
fn equalish_affine(one: Affine, two: Affine) -> bool {
one.as_coeffs()
.iter()
.zip(two.as_coeffs().iter())
.all(|(a, b)| (a - b).abs() < f64::EPSILON)
}
macro_rules! assert_affine_eq {
($left:expr, $right:expr) => {{
if !equalish_affine($left, $right) {
panic!(
"assertion failed: `(one == two)`\n\
one: {:?}\n\
two: {:?}",
$left.as_coeffs(),
$right.as_coeffs()
)
}
}};
}
#[test]
fn get_affine_y_up() {
let mut ctx = make_context((400.0, 400.0));
let mut piet = CoreGraphicsContext::new_y_up(&mut ctx, 400.0, None);
let affine = piet.current_transform();
assert_affine_eq!(affine, Affine::default());
let one = Affine::translate((50.0, 20.0));
let two = Affine::rotate(2.2);
let three = Affine::FLIP_Y;
let four = Affine::scale_non_uniform(2.0, -1.5);
piet.save().unwrap();
piet.transform(one);
piet.transform(one);
piet.save().unwrap();
piet.transform(two);
piet.save().unwrap();
piet.transform(three);
assert_affine_eq!(piet.current_transform(), one * one * two * three);
piet.transform(four);
piet.save().unwrap();
assert_affine_eq!(piet.current_transform(), one * one * two * three * four);
piet.restore().unwrap();
assert_affine_eq!(piet.current_transform(), one * one * two * three * four);
piet.restore().unwrap();
assert_affine_eq!(piet.current_transform(), one * one * two);
piet.restore().unwrap();
assert_affine_eq!(piet.current_transform(), one * one);
piet.restore().unwrap();
assert_affine_eq!(piet.current_transform(), Affine::default());
}
#[test]
fn capture_image_area() {
let mut ctx = make_context((400.0, 400.0));
let mut piet = CoreGraphicsContext::new_y_down(&mut ctx, None);
assert!(piet
.capture_image_area(Rect::new(0.0, 0.0, 0.0, 0.0))
.is_err());
assert!(piet
.capture_image_area(Rect::new(0.0, 0.0, 500.0, 400.0))
.is_err());
assert!(piet
.capture_image_area(Rect::new(100.0, 100.0, 200.0, 200.0))
.is_ok());
let copy = piet
.capture_image_area(Rect::new(100.0, 100.0, 200.0, 200.0))
.unwrap();
let unwrapped_copy = copy.as_cgimage().unwrap();
let rewrapped_copy = CoreGraphicsImage::from_cgimage_and_ydir(unwrapped_copy.clone(), true);
piet.draw_image(
&rewrapped_copy,
Rect::new(0.0, 0.0, 400.0, 400.0),
InterpolationMode::Bilinear,
);
}
}