use std::sync::{LazyLock, Mutex};
use camino::Utf8Path;
use tiny_skia::{Pixmap, PixmapMut, Transform};
use crate::{
files::FileProviderExt,
meme::{Meme, MemeBase, TextBox, VAlign},
text::render_layout,
};
impl Meme {
pub fn render<'f>(
self,
file_provider: &'f impl crate::FileProvider<'f>,
debug: bool,
) -> crate::Result<image::RgbaImage> {
let mut pixmap = match self.base {
MemeBase::Canvas {
color,
size: (width, height),
} => {
let mut pixmap = Pixmap::new(width, height).unwrap();
pixmap.fill(color.into());
pixmap
}
MemeBase::Image(ref path) => {
let image = file_provider.load_image(path)?;
let width = image.width();
let height = image.height();
Pixmap::from_vec(
premultiply_slice(image.into_vec()),
tiny_skia::IntSize::from_wh(width, height).unwrap(),
)
.unwrap()
}
};
render_text(pixmap.as_mut(), self, debug)?;
Ok(image::RgbaImage::from_vec(
pixmap.width(),
pixmap.height(),
demultiply_slice(pixmap.take()),
)
.unwrap())
}
pub fn render_to_file(self, output_path: &Utf8Path, debug: bool) -> crate::Result<()> {
self.render(&crate::FsFileProvider, debug)?
.save(output_path)?;
Ok(())
}
}
fn render_text(mut pixmap: PixmapMut<'_>, meme: Meme, debug: bool) -> crate::Result<()> {
static FONT_CONTEXT: LazyLock<Mutex<parley::FontContext>> = LazyLock::new(Default::default);
static LAYOUT_CONTEXT: LazyLock<Mutex<parley::LayoutContext<()>>> =
LazyLock::new(Default::default);
let mut font_context = FONT_CONTEXT.lock().unwrap();
let mut layout_context = LAYOUT_CONTEXT.lock().unwrap();
let debug_stroke = tiny_skia::Stroke {
width: 2.0,
..Default::default()
};
let center_paint = {
let mut paint = tiny_skia::Paint::default();
paint.set_color_rgba8(0, 0, 255, 255);
paint
};
let border_paint = {
let mut paint = tiny_skia::Paint::default();
paint.set_color_rgba8(255, 0, 0, 255);
paint
};
let rotated_paint = {
let mut paint = tiny_skia::Paint::default();
paint.set_color_rgba8(0, 255, 0, 255);
paint
};
for TextBox {
text,
position: (x, y),
size: (width, height),
rotate,
font,
caps,
color,
outline,
font_size: max_font_size,
line_height,
halign,
valign,
} in meme.text
{
if text.is_empty() {
continue;
};
let center = tiny_skia::Point {
x: x + width / 2.0,
y: y + height / 2.0,
};
let content = if caps { text.to_uppercase() } else { text };
let mut font_size = max_font_size;
let layout = loop {
let max_width = outline
.map(|outline| width - 2.0 * outline.width_for_font_size(font_size))
.unwrap_or(width);
let mut builder = layout_context.ranged_builder(&mut font_context, &content, 1.0, true);
builder.push_default(parley::StyleProperty::FontStack(parley::FontStack::Single(
parley::FontFamily::parse(&font).unwrap(),
)));
builder.push_default(parley::StyleProperty::FontSize(font_size));
builder.push_default(parley::StyleProperty::LineHeight(
parley::LineHeight::FontSizeRelative(line_height),
));
let mut layout = builder.build(&*content);
layout.break_all_lines(Some(max_width));
if layout.height() <= height && layout.width() <= width {
layout.align(
Some(max_width),
halign.into(),
parley::AlignmentOptions {
align_when_overflowing: true,
},
);
break layout;
} else {
font_size -= 0.02 * max_font_size;
}
};
let mut transform = Transform::from_translate(x, y);
match valign {
VAlign::Top => {}
VAlign::Center => {
transform = transform.post_translate(0.0, (height - layout.height()) / 2.0)
}
VAlign::Bottom => transform = transform.post_translate(0.0, height - layout.height()),
};
if let Some(angle) = rotate {
if angle != 0.0 {
transform = transform.post_rotate_at(angle, center.x, center.y);
}
}
render_layout(
layout,
&mut pixmap,
transform,
color,
outline.map(|mut outline| {
outline.width = outline.width_for_font_size(font_size);
outline
}),
);
if debug {
let mut builder = tiny_skia::PathBuilder::new();
builder.move_to(center.x, center.y - 10.0);
builder.line_to(center.x, center.y + 10.0);
builder.move_to(center.x - 10.0, center.y);
builder.line_to(center.x + 10.0, center.y);
let path = builder.finish().unwrap();
pixmap.stroke_path(
&path,
¢er_paint,
&debug_stroke,
Transform::identity(),
None,
);
let mut builder = path.clear();
builder.push_rect(tiny_skia::Rect::from_xywh(x, y, width, height).unwrap());
let path = builder.finish().unwrap();
pixmap.stroke_path(
&path,
&border_paint,
&debug_stroke,
Transform::identity(),
None,
);
if let Some(angle) = rotate {
if angle != 0.0 {
pixmap.stroke_path(
&path,
&rotated_paint,
&debug_stroke,
Transform::from_rotate_at(angle, center.x, center.y),
None,
);
}
}
}
}
Ok(())
}
fn premultiply_slice<B: AsMut<[u8]>>(mut bytes: B) -> B {
for pixel in bytes.as_mut().chunks_exact_mut(4) {
let [r, g, b, a] = pixel.try_into().expect("slice must have a length of 4");
let premultiplied = tiny_skia::ColorU8::from_rgba(r, g, b, a).premultiply();
pixel[0] = premultiplied.red();
pixel[1] = premultiplied.green();
pixel[2] = premultiplied.blue();
pixel[3] = premultiplied.alpha();
}
bytes
}
fn demultiply_slice<B: AsMut<[u8]>>(mut bytes: B) -> B {
for pixel in bytes.as_mut().chunks_exact_mut(4) {
let [r, g, b, a] = pixel.try_into().expect("slice must have a length of 4");
let demultiplied = tiny_skia::PremultipliedColorU8::from_rgba(r, g, b, a)
.unwrap()
.demultiply();
pixel[0] = demultiplied.red();
pixel[1] = demultiplied.green();
pixel[2] = demultiplied.blue();
pixel[3] = demultiplied.alpha();
}
bytes
}