#![deny(clippy::trivially_copy_pass_by_ref)]
#[cfg(feature = "evcxr")]
mod evcxr;
mod text;
use std::{borrow::Cow, fmt, fmt::Write, io, mem};
use image::{DynamicImage, GenericImageView, ImageBuffer};
use piet::kurbo::{Affine, Point, Rect, Shape, Size};
use piet::{
Color, Error, FixedGradient, FontStyle, Image, ImageFormat, InterpolationMode, IntoBrush,
LineCap, LineJoin, StrokeStyle, TextAlignment, TextLayout as _,
};
use svg::node::Node;
pub use crate::text::{Text, TextLayout};
pub use piet;
#[cfg(feature = "evcxr")]
pub use evcxr::draw_evcxr;
type Result<T> = std::result::Result<T, Error>;
pub struct RenderContext {
size: Size,
stack: Vec<State>,
state: State,
doc: svg::Document,
next_id: u64,
text: Text,
}
impl RenderContext {
pub fn new(size: Size) -> Self {
Self {
size,
stack: Vec::new(),
state: State::default(),
doc: svg::Document::new(),
next_id: 0,
text: Text::new(),
}
}
pub fn size(&self) -> Size {
self.size
}
pub fn write(&self, writer: impl io::Write) -> io::Result<()> {
svg::write(writer, &self.doc)
}
pub fn display(&self) -> &impl fmt::Display {
&self.doc
}
fn new_id(&mut self) -> Id {
let x = Id(self.next_id);
self.next_id += 1;
x
}
}
impl piet::RenderContext for RenderContext {
type Brush = Brush;
type Text = Text;
type TextLayout = TextLayout;
type Image = SvgImage;
fn status(&mut self) -> Result<()> {
Ok(())
}
fn clear(&mut self, rect: impl Into<Option<Rect>>, color: Color) {
let rect = rect.into();
let mut rect = match rect {
Some(rect) => svg::node::element::Rectangle::new()
.set("width", rect.width())
.set("height", rect.height())
.set("x", rect.x0)
.set("y", rect.y0),
None => svg::node::element::Rectangle::new()
.set("width", "100%")
.set("height", "100%"),
}
.set("fill", fmt_color(color))
.set("fill-opacity", fmt_opacity(color));
if let Some(id) = self.state.clip {
rect.assign("clip-path", format!("url(#{})", id.to_string()));
}
self.doc.append(rect);
}
fn solid_brush(&mut self, color: Color) -> Brush {
Brush {
kind: BrushKind::Solid(color),
}
}
fn gradient(&mut self, gradient: impl Into<FixedGradient>) -> Result<Brush> {
let id = self.new_id();
match gradient.into() {
FixedGradient::Linear(x) => {
let mut gradient = svg::node::element::LinearGradient::new()
.set("gradientUnits", "userSpaceOnUse")
.set("id", id)
.set("x1", x.start.x)
.set("y1", x.start.y)
.set("x2", x.end.x)
.set("y2", x.end.y);
for stop in x.stops {
gradient.append(
svg::node::element::Stop::new()
.set("offset", stop.pos)
.set("stop-color", fmt_color(stop.color))
.set("stop-opacity", fmt_opacity(stop.color)),
);
}
self.doc.append(gradient);
}
FixedGradient::Radial(x) => {
let mut gradient = svg::node::element::RadialGradient::new()
.set("gradientUnits", "userSpaceOnUse")
.set("id", id)
.set("cx", x.center.x)
.set("cy", x.center.y)
.set("fx", x.center.x + x.origin_offset.x)
.set("fy", x.center.y + x.origin_offset.y)
.set("r", x.radius);
for stop in x.stops {
gradient.append(
svg::node::element::Stop::new()
.set("offset", stop.pos)
.set("stop-color", fmt_color(stop.color))
.set("stop-opacity", fmt_opacity(stop.color)),
);
}
self.doc.append(gradient);
}
}
Ok(Brush {
kind: BrushKind::Ref(id),
})
}
fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
let brush = brush.make_brush(self, || shape.bounding_box());
add_shape(
&mut self.doc,
shape,
&Attrs {
xf: self.state.xf,
clip: self.state.clip,
fill: Some((brush.into_owned(), None)),
..Attrs::default()
},
);
}
fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
let brush = brush.make_brush(self, || shape.bounding_box());
add_shape(
&mut self.doc,
shape,
&Attrs {
xf: self.state.xf,
clip: self.state.clip,
fill: Some((brush.into_owned(), Some("evenodd"))),
..Attrs::default()
},
);
}
fn clip(&mut self, shape: impl Shape) {
let id = self.new_id();
let mut clip = svg::node::element::ClipPath::new().set("id", id);
add_shape(
&mut clip,
shape,
&Attrs {
xf: self.state.xf,
clip: self.state.clip,
..Attrs::default()
},
);
self.doc.append(clip);
self.state.clip = Some(id);
}
fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
let brush = brush.make_brush(self, || shape.bounding_box());
add_shape(
&mut self.doc,
shape,
&Attrs {
xf: self.state.xf,
clip: self.state.clip,
stroke: Some((brush.into_owned(), width, &StrokeStyle::new())),
..Attrs::default()
},
);
}
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());
add_shape(
&mut self.doc,
shape,
&Attrs {
xf: self.state.xf,
clip: self.state.clip,
stroke: Some((brush.into_owned(), width, style)),
..Attrs::default()
},
);
}
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();
let color = {
let (r, g, b, a) = layout.text_color.as_rgba8();
format!("rgba({}, {}, {}, {})", r, g, b, a as f64 * (100. / 255.))
};
let mut x = pos.x;
let anchor = match (layout.max_width, layout.alignment) {
(width, TextAlignment::End) if width.is_finite() && width > 0. => {
x += width;
"text-anchor:end"
}
(width, TextAlignment::Center) if width.is_finite() && width > 0. => {
x += width * 0.5;
"text-anchor:middle"
}
_ => "",
};
self.text()
.seen_fonts
.lock()
.unwrap()
.insert(layout.font_face.clone());
let y = pos.y + 0.06 * layout.size().height;
let mut text = svg::node::element::Text::new()
.set("x", x)
.set("y", y)
.set("dominant-baseline", "hanging")
.set(
"style",
format!(
"font-size:{}pt;\
font-family:\"{}\";\
font-weight:{};\
font-style:{};\
text-decoration:{};\
fill:{};\
{}",
layout.font_size,
layout.font_face.family.name(),
layout.font_face.weight.to_raw(),
match layout.font_face.style {
FontStyle::Regular => "normal",
FontStyle::Italic => "italic",
},
match (layout.underline, layout.strikethrough) {
(false, false) => "none",
(false, true) => "line-through",
(true, false) => "underline",
(true, true) => "underline line-through",
},
color,
anchor,
),
)
.add(svg::node::Text::new(layout.text()));
let affine = self.current_transform();
if affine != Affine::IDENTITY {
text.assign("transform", xf_val(&affine));
}
if let Some(id) = self.state.clip {
text.assign("clip-path", format!("url(#{})", id.to_string()));
}
self.doc.append(text);
}
fn save(&mut self) -> Result<()> {
let new = self.state.clone();
self.stack.push(mem::replace(&mut self.state, new));
Ok(())
}
fn restore(&mut self) -> Result<()> {
self.state = self.stack.pop().ok_or(Error::StackUnbalance)?;
Ok(())
}
fn finish(&mut self) -> Result<()> {
self.doc
.assign("viewBox", (0, 0, self.size.width, self.size.height));
self.doc.assign(
"style",
format!("width:{}px;height:{}px;", self.size.width, self.size.height),
);
let text = (*self.text()).clone();
let mut seen_fonts = text.seen_fonts.lock().unwrap();
if !seen_fonts.is_empty() {
let mut style = String::new();
for face in &*seen_fonts {
if face.family.name().contains('"') {
panic!("font family name contains `\"`");
}
writeln!(
&mut style,
"@font-face {{\n\
font-family: \"{}\";\n\
font-weight: {};\n\
font-style: {};\n\
src: url(\"data:application/x-font-opentype;charset=utf-8;base64,{}\");\n\
}}",
face.family.name(),
face.weight.to_raw(),
match face.style {
FontStyle::Regular => "normal",
FontStyle::Italic => "italic",
},
base64::display::Base64Display::with_config(
&text.font_data(face)?,
base64::STANDARD
),
)
.unwrap();
}
self.doc.append(svg::node::element::Style::new(style));
}
seen_fonts.clear();
Ok(())
}
fn transform(&mut self, transform: Affine) {
self.state.xf *= transform;
}
fn current_transform(&self) -> Affine {
self.state.xf
}
fn make_image(
&mut self,
width: usize,
height: usize,
buf: &[u8],
format: ImageFormat,
) -> Result<Self::Image> {
Ok(SvgImage(match format {
ImageFormat::Grayscale => {
let image = ImageBuffer::from_raw(width as _, height as _, buf.to_owned())
.ok_or(Error::InvalidInput)?;
DynamicImage::ImageLuma8(image)
}
ImageFormat::Rgb => {
let image = ImageBuffer::from_raw(width as _, height as _, buf.to_owned())
.ok_or(Error::InvalidInput)?;
DynamicImage::ImageRgb8(image)
}
ImageFormat::RgbaSeparate => {
let image = ImageBuffer::from_raw(width as _, height as _, buf.to_owned())
.ok_or(Error::InvalidInput)?;
DynamicImage::ImageRgba8(image)
}
ImageFormat::RgbaPremul => {
use image::Rgba;
use piet::util::unpremul;
let mut image =
ImageBuffer::<Rgba<u8>, _>::from_raw(width as _, height as _, buf.to_owned())
.ok_or(Error::InvalidInput)?;
for px in image.pixels_mut() {
px[0] = unpremul(px[0], px[3]);
px[1] = unpremul(px[1], px[3]);
px[2] = unpremul(px[2], px[3]);
}
DynamicImage::ImageRgba8(image)
}
_ => return Err(Error::Unimplemented),
}))
}
#[inline]
fn draw_image(
&mut self,
image: &Self::Image,
dst_rect: impl Into<Rect>,
interp: InterpolationMode,
) {
draw_image(self, image, None, dst_rect.into(), interp);
}
#[inline]
fn draw_image_area(
&mut self,
image: &Self::Image,
src_rect: impl Into<Rect>,
dst_rect: impl Into<Rect>,
interp: InterpolationMode,
) {
draw_image(self, image, Some(src_rect.into()), dst_rect.into(), interp);
}
fn capture_image_area(&mut self, _src_rect: impl Into<Rect>) -> Result<Self::Image> {
Err(Error::Unimplemented)
}
fn blurred_rect(&mut self, rect: Rect, _blur_radius: f64, brush: &impl IntoBrush<Self>) {
self.fill(rect, brush)
}
}
fn draw_image(
ctx: &mut RenderContext,
image: &<RenderContext as piet::RenderContext>::Image,
_src_rect: Option<Rect>,
dst_rect: Rect,
_interp: InterpolationMode,
) {
use image::ImageEncoder as _;
let mut writer = base64::write::EncoderStringWriter::from(
String::from("data:image/png;base64,"),
base64::STANDARD,
);
image::codecs::png::PngEncoder::new(&mut writer)
.write_image(
image.0.as_bytes(),
image.0.width(),
image.0.height(),
image.0.color(),
)
.unwrap();
let data_url = writer.into_inner();
let mut node = svg::node::element::Image::new()
.set("x", dst_rect.x0)
.set("y", dst_rect.y0)
.set("width", dst_rect.x1 - dst_rect.x0)
.set("height", dst_rect.y1 - dst_rect.y0)
.set("href", data_url);
let affine = piet::RenderContext::current_transform(ctx);
if affine != Affine::IDENTITY {
node.assign("transform", xf_val(&affine));
}
if let Some(id) = ctx.state.clip {
node.assign("clip-path", format!("url(#{})", id.to_string()));
}
ctx.doc.append(node);
}
#[derive(Default)]
struct Attrs<'a> {
xf: Affine,
clip: Option<Id>,
fill: Option<(Brush, Option<&'a str>)>,
stroke: Option<(Brush, f64, &'a StrokeStyle)>,
}
impl Attrs<'_> {
#[allow(clippy::float_cmp)]
fn apply_to(&self, node: &mut impl Node) {
node.assign("transform", xf_val(&self.xf));
if let Some(id) = self.clip {
node.assign("clip-path", format!("url(#{})", id.to_string()));
}
if let Some((ref brush, rule)) = self.fill {
node.assign("fill", brush.color());
if let Some(opacity) = brush.opacity() {
node.assign("fill-opacity", opacity);
}
if let Some(rule) = rule {
node.assign("fill-rule", rule);
}
} else {
node.assign("fill", "none");
}
if let Some((ref stroke, width, style)) = self.stroke {
node.assign("stroke", stroke.color());
if let Some(opacity) = stroke.opacity() {
node.assign("stroke-opacity", opacity);
}
if width != 1.0 {
node.assign("stroke-width", width);
}
match style.line_join {
LineJoin::Miter { limit } if limit == LineJoin::DEFAULT_MITER_LIMIT => (),
LineJoin::Miter { limit } => {
node.assign("stroke-miterlimit", limit);
}
LineJoin::Round => {
node.assign("stroke-linejoin", "round");
}
LineJoin::Bevel => {
node.assign("stroke-linejoin", "bevel");
}
}
match style.line_cap {
LineCap::Round => {
node.assign("stroke-linecap", "round");
}
LineCap::Square => {
node.assign("stroke-linecap", "square");
}
LineCap::Butt => (),
}
if !style.dash_pattern.is_empty() {
node.assign("stroke-dasharray", style.dash_pattern.to_vec());
}
if style.dash_offset != 0.0 {
node.assign("stroke-dashoffset", style.dash_offset);
}
}
}
}
fn xf_val(xf: &Affine) -> svg::node::Value {
let xf = xf.as_coeffs();
format!(
"matrix({} {} {} {} {} {})",
xf[0], xf[1], xf[2], xf[3], xf[4], xf[5]
)
.into()
}
fn add_shape(node: &mut impl Node, shape: impl Shape, attrs: &Attrs) {
if let Some(circle) = shape.as_circle() {
let mut x = svg::node::element::Circle::new()
.set("cx", circle.center.x)
.set("cy", circle.center.y)
.set("r", circle.radius);
attrs.apply_to(&mut x);
node.append(x);
} else if let Some(round_rect) = shape
.as_rounded_rect()
.filter(|r| r.radii().as_single_radius().is_some())
{
let mut x = svg::node::element::Rectangle::new()
.set("x", round_rect.origin().x)
.set("y", round_rect.origin().y)
.set("width", round_rect.width())
.set("height", round_rect.height())
.set("rx", round_rect.radii().as_single_radius().unwrap())
.set("ry", round_rect.radii().as_single_radius().unwrap());
attrs.apply_to(&mut x);
node.append(x);
} else if let Some(rect) = shape.as_rect() {
let mut x = svg::node::element::Rectangle::new()
.set("x", rect.origin().x)
.set("y", rect.origin().y)
.set("width", rect.width())
.set("height", rect.height());
attrs.apply_to(&mut x);
node.append(x);
} else {
let mut path = svg::node::element::Path::new().set("d", shape.into_path(1e-3).to_svg());
attrs.apply_to(&mut path);
node.append(path);
}
}
#[derive(Debug, Clone, Default)]
struct State {
xf: Affine,
clip: Option<Id>,
}
#[derive(Debug, Clone)]
pub struct Brush {
kind: BrushKind,
}
#[derive(Debug, Clone)]
enum BrushKind {
Solid(Color),
Ref(Id),
}
impl Brush {
fn color(&self) -> svg::node::Value {
match self.kind {
BrushKind::Solid(color) => fmt_color(color).into(),
BrushKind::Ref(id) => format!("url(#{})", id.to_string()).into(),
}
}
fn opacity(&self) -> Option<svg::node::Value> {
match self.kind {
BrushKind::Solid(color) => Some(fmt_opacity(color).into()),
BrushKind::Ref(_) => None,
}
}
}
impl IntoBrush<RenderContext> for Brush {
fn make_brush<'b>(
&'b self,
_piet: &mut RenderContext,
_bbox: impl FnOnce() -> Rect,
) -> Cow<'b, Brush> {
Cow::Owned(self.clone())
}
}
fn fmt_color(color: Color) -> String {
format!("#{:06x}", color.as_rgba_u32() >> 8)
}
fn fmt_opacity(color: Color) -> String {
format!("{}", color.as_rgba().3)
}
#[derive(Clone)]
pub struct SvgImage(image::DynamicImage);
impl Image for SvgImage {
fn size(&self) -> Size {
let (width, height) = self.0.dimensions();
Size {
width: width as _,
height: height as _,
}
}
}
#[derive(Debug, Copy, Clone)]
struct Id(u64);
impl Id {
#[allow(clippy::inherent_to_string)]
fn to_string(self) -> String {
const ALPHABET: &[u8; 52] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let mut out = String::with_capacity(4);
let mut x = self.0;
loop {
let digit = (x % ALPHABET.len() as u64) as usize;
out.push(ALPHABET[digit] as char);
x /= ALPHABET.len() as u64;
if x == 0 {
break;
}
}
out
}
}
impl From<Id> for svg::node::Value {
fn from(x: Id) -> Self {
x.to_string().into()
}
}