use std::io;
use std::ops::Deref;
use std::sync::Arc;
use image::{self, DynamicImage, FilterType, GenericImage, ImageFormat};
use rusttype::{point, Rect, vector};
use model::{Caption, ImageMacro};
use resources::{Loader, Font, FontLoader, Template, TemplateLoader};
use util::animated_gif;
use util::text::{self, Style};
use super::error::CaptionError;
use super::engine;
use super::output::CaptionOutput;
pub(super) struct CaptionTask<Tl = TemplateLoader, Fl = FontLoader>
where Tl: Loader<Item=Template>, Fl: Loader<Item=Font>
{
image_macro: ImageMacro,
engine: Arc<engine::Inner<Tl, Fl>>,
}
impl<Tl, Fl> Deref for CaptionTask<Tl, Fl>
where Tl: Loader<Item=Template>, Fl: Loader<Item=Font>
{
type Target = ImageMacro;
fn deref(&self) -> &Self::Target {
&self.image_macro }
}
impl<Tl, Fl> CaptionTask<Tl, Fl>
where Tl: Loader<Item=Template>, Fl: Loader<Item=Font>
{
#[inline]
pub fn new(image_macro: ImageMacro, engine: Arc<engine::Inner<Tl, Fl>>) -> Self {
CaptionTask{image_macro, engine}
}
}
impl<Tl, Fl> CaptionTask<Tl, Fl>
where Tl: Loader<Item=Template>, Fl: Loader<Item=Font>
{
pub fn perform(self) -> Result<CaptionOutput, CaptionError<Tl, Fl>> {
debug!("Rendering {:?}", self.image_macro);
let template = self.engine.template_loader.load(&self.template)
.map_err(|e| CaptionError::Template(self.template.clone(), e))?;
if template.is_animated() {
debug!("Image macro uses an animated template `{}` with {} frames",
self.template, template.image_count());
}
let mut images = Vec::with_capacity(template.image_count());
for mut img in template.iter_images().cloned() {
img = self.resize_template(img);
if self.has_text() {
img = self.draw_texts(img)?;
}
images.push(img);
}
let bytes = self.encode_result(images, &*template)?;
let output = CaptionOutput::new(template.preferred_format(), bytes);
Ok(output)
}
fn resize_template(&self, template: DynamicImage) -> DynamicImage {
let (orig_width, orig_height) = template.dimensions();
trace!("Original size of the template image `{}`: {}x{}",
self.template, orig_width, orig_height);
let target_width = self.width.unwrap_or(orig_width);
let target_height = self.height.unwrap_or(orig_height);
let img;
if target_width != orig_width || target_height != orig_height {
debug!("Resizing template image `{}` from {}x{} to {}x{}",
self.template, orig_width, orig_height, target_width, target_height);
img = template.resize(target_width, target_height, FilterType::Lanczos3);
} else {
debug!("Using original template image size of {}x{}", orig_width, orig_height);
img = template;
}
let (width, height) = img.dimensions();
trace!("Final image size: {}x{}", width, height);
img
}
fn draw_texts(&self, img: DynamicImage) -> Result<DynamicImage, CaptionError<Tl, Fl>> {
let mut img = img;
if img.as_rgba8().is_none() {
trace!("Converting image to RGBA...");
img = DynamicImage::ImageRgba8(img.to_rgba());
}
for cap in &self.captions {
img = self.draw_single_caption(img, cap)?;
}
Ok(img)
}
fn draw_single_caption(&self, img: DynamicImage,
caption: &Caption) -> Result<DynamicImage, CaptionError<Tl, Fl>> {
let mut img = img;
if caption.text.is_empty() {
debug!("Empty caption text, skipping.");
return Ok(img);
}
debug!("Rendering {v}-{h} text: {text:?}", text = caption.text,
v = format!("{:?}", caption.valign).to_lowercase(),
h = format!("{:?}", caption.halign).to_lowercase());
trace!("Loading font `{}`...", caption.font);
let font = self.engine.font_loader.load(&caption.font)
.map_err(|e| CaptionError::Font(caption.font.clone(), e))?;
trace!("Checking if font `{}` has all glyphs for caption: {}",
caption.font, caption.text);
text::check(&*font, &caption.text);
let (width, height) = img.dimensions();
let width = width as f32;
let height = height as f32;
let max_vmargin: f32 = 16.0;
let vmargin = max_vmargin.min(height * 0.02);
trace!("Vertical text margin computed as {}", vmargin);
let max_hmargin: f32 = 16.0;
let hmargin = max_hmargin.min(width * 0.02);
trace!("Horizontal text margin computed as {}", hmargin);
let margin_vector = vector(hmargin, vmargin);
let rect: Rect<f32> = Rect{
min: point(0.0, 0.0) + margin_vector,
max: point(width, height) - margin_vector,
};
let alignment = (caption.halign, caption.valign);
let text_size = 64.0;
if let Some(outline_color) = caption.outline {
let outline_width = 2.0;
for &v in [vector(-outline_width, -outline_width),
vector(outline_width, -outline_width),
vector(outline_width, outline_width),
vector(-outline_width, outline_width)].iter() {
let style = Style::new(&font, text_size, outline_color);
let rect = Rect{min: rect.min + v, max: rect.max + v};
img = text::render_text(img, &caption.text, alignment, rect, style);
}
}
let style = Style::new(&font, text_size, caption.color);
img = text::render_text(img, &caption.text, alignment, rect, style);
Ok(img)
}
fn encode_result(&self, images: Vec<DynamicImage>,
template: &Template) -> Result<Vec<u8>, CaptionError<Tl, Fl>> {
let format = template.preferred_format();
debug!("Encoding final image as {:?}...", format);
let mut result = vec![];
match format {
ImageFormat::PNG => {
trace!("Writing PNG image");
assert_eq!(1, images.len());
let img = &images[0];
let (width, height) = img.dimensions();
let pixels = &*img.raw_pixels();
image::png::PNGEncoder::new(&mut result)
.encode(pixels, width, height, img.color())
.map_err(CaptionError::Encode)?;
}
ImageFormat::JPEG => {
let quality = 85; trace!("Writing JPEG with quality {}", quality);
assert_eq!(1, images.len());
let img = &images[0];
let (width, height) = img.dimensions();
let pixels = &*img.raw_pixels();
image::jpeg::JPEGEncoder::new_with_quality(&mut result, quality)
.encode(pixels, width, height, img.color())
.map_err(CaptionError::Encode)?;
}
ImageFormat::GIF => {
if let &Template::Animation(ref gif_anim) = template {
trace!("Writing animated GIF with {} frame(s)", gif_anim.frames_count());
animated_gif::encode_modified(gif_anim, images, &mut result)
.map_err(CaptionError::Encode)?;
} else {
trace!("Writing regular (still) GIF");
assert_eq!(1, images.len());
let img = &images[0];
let (width, height) = img.dimensions();
let mut frame = image::gif::Frame::default();
let (buffer, palette, transparent) = animated_gif::quantize_image(img);
frame.width = width as u16;
frame.height = height as u16;
frame.buffer = buffer.into();
frame.palette = Some(palette);
frame.transparent = transparent;
image::gif::Encoder::new(&mut result).encode(frame).map_err(|e| {
let io_error = match e {
image::ImageError::IoError(e) => e,
e => io::Error::new(io::ErrorKind::Other, e),
};
CaptionError::Encode(io_error)
})?;
}
}
f => {
panic!("Unexpected image format in CaptionTask::encode_result: {:?}", f);
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::CaptionTask;
#[test]
fn thread_safe() {
fn assert_sync<T: Sync>() {}
fn assert_send<T: Send>() {}
assert_sync::<CaptionTask>();
assert_send::<CaptionTask>();
}
}