use std::io;
use image::{self, imageops, DynamicImage, GenericImageView, ImageFormat, RgbaImage};
use rusttype::Font;
use crate::annotation::Annotation;
use crate::outline;
use crate::Result;
use crate::AA_FACTOR;
struct RenderedLayer {
buffer: RgbaImage,
x: u32,
y: u32,
pad: u32,
}
pub struct Canvas {
base: DynamicImage,
layers: Vec<RenderedLayer>,
width: u32,
height: u32,
}
impl Canvas {
pub fn read_from_buffer(buf: &[u8]) -> Result<Canvas> {
let base = image::load_from_memory(buf)?;
let (width, height) = base.dimensions();
Ok(Canvas {
base,
layers: Vec::new(),
width,
height,
})
}
pub fn add_annotation(&mut self, annotation: &Annotation, font: &Font, scale_multiplier: f32) {
let scale_factor = (self.height as f32 / 10.0) * scale_multiplier;
let lines = annotation.layout(font, scale_factor, self.width, self.height);
for line in lines {
let (buffer, pad) = render_line_to_small_buffer(
&line.text,
font,
scale_factor,
line.width,
line.height,
);
self.layers.push(RenderedLayer {
buffer,
x: line.x,
y: line.y,
pad,
});
}
}
pub fn add_annotations(
&mut self,
annotations: &[Annotation],
font: &Font,
scale_multiplier: f32,
) {
use rayon::prelude::*;
let scale_factor = (self.height as f32 / 10.0) * scale_multiplier;
let (canvas_w, canvas_h) = (self.width, self.height);
let new_layers: Vec<RenderedLayer> = annotations
.par_iter()
.map(|annotation| {
let lines = annotation.layout(font, scale_factor, canvas_w, canvas_h);
lines
.into_iter()
.map(|line| {
let (buffer, pad) = render_line_to_small_buffer(
&line.text,
font,
scale_factor,
line.width,
line.height,
);
RenderedLayer {
buffer,
x: line.x,
y: line.y,
pad,
}
})
.collect::<Vec<_>>()
})
.flatten()
.collect();
self.layers.extend(new_layers);
}
pub fn render(&mut self) {
use rayon::prelude::*;
let layers = std::mem::take(&mut self.layers);
let resized: Vec<(RgbaImage, u32, u32, u32)> = layers
.par_iter()
.map(|layer| {
let (w, h) = layer.buffer.dimensions();
let downsampled = imageops::resize(
&layer.buffer,
w / AA_FACTOR,
h / AA_FACTOR,
imageops::FilterType::Lanczos3,
);
(downsampled, layer.x, layer.y, layer.pad)
})
.collect();
for (downsampled, x, y, pad) in resized {
let downsampled = DynamicImage::ImageRgba8(downsampled);
let offset = (pad / AA_FACTOR) as i64;
let cx = (x as i64 - offset).max(0);
let cy = (y as i64 - offset).max(0);
imageops::overlay(&mut self.base, &downsampled, cx, cy);
}
}
pub fn save_jpg(&self, stream: &mut (impl io::Write + io::Seek)) -> io::Result<()> {
self.base
.write_to(stream, ImageFormat::Jpeg)
.map_err(io::Error::other)
}
pub fn save_png(&self, stream: &mut (impl io::Write + io::Seek)) -> io::Result<()> {
self.base
.write_to(stream, ImageFormat::Png)
.map_err(io::Error::other)
}
}
fn render_line_to_small_buffer(
text: &str,
font: &Font,
scale_factor: f32,
text_width: u32,
text_height: u32,
) -> (RgbaImage, u32) {
let radius_3x = (0.09 * scale_factor) as i32;
let kernel_3x: i32 = 9;
let raw_pad = (radius_3x + kernel_3x).max(0) as u32;
let pad = raw_pad.div_ceil(AA_FACTOR) * AA_FACTOR;
let buf_w = text_width * AA_FACTOR + 2 * pad;
let buf_h = text_height * AA_FACTOR + 2 * pad;
let mut buffer = DynamicImage::ImageRgba8(RgbaImage::new(buf_w, buf_h));
outline::render_line(
text,
0,
(pad / AA_FACTOR, pad / AA_FACTOR),
(text_width, text_height),
scale_factor,
font,
&mut buffer,
);
(buffer.into_rgba8(), pad)
}