#![cfg(feature = "image")]
use crate::render::{Canvas, Pixel};
use crate::types::Color;
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, LumaA, Primitive, Rgb, Rgba};
macro_rules! impl_pixel_for_image_pixel {
($p:ident<$s:ident>: $c:pat => $d:expr) => {
impl<$s> Pixel for $p<$s>
where
$s: Primitive + 'static,
$p<$s>: image::Pixel<Subpixel = $s>,
{
type Image = ImageBuffer<Self, Vec<$s>>;
type Canvas = (Self, Self::Image);
fn default_color(color: Color) -> Self {
match color.select($s::zero(), $s::max_value()) {
$c => $p($d),
}
}
}
};
}
impl_pixel_for_image_pixel! { Luma<S>: p => [p] }
impl_pixel_for_image_pixel! { LumaA<S>: p => [p, S::max_value()] }
impl_pixel_for_image_pixel! { Rgb<S>: p => [p, p, p] }
impl_pixel_for_image_pixel! { Rgba<S>: p => [p, p, p, S::max_value()] }
impl<P: image::Pixel + 'static> Canvas for (P, ImageBuffer<P, Vec<P::Subpixel>>) {
type Pixel = P;
type Image = ImageBuffer<P, Vec<P::Subpixel>>;
fn new(width: u32, height: u32, dark_pixel: P, light_pixel: P) -> Self {
(dark_pixel, ImageBuffer::from_pixel(width, height, light_pixel))
}
fn draw_dark_pixel(&mut self, x: u32, y: u32) {
self.1.put_pixel(x, y, self.0);
}
fn into_image(self) -> ImageBuffer<P, Vec<P::Subpixel>> {
self.1
}
}
pub fn overlay_logo(qr_image: &DynamicImage, logo: &DynamicImage, size_ratio: f32) -> DynamicImage {
let (qr_w, qr_h) = qr_image.dimensions();
let ratio = size_ratio.clamp(0.05, 0.5);
let mut result = qr_image.to_rgba8();
let max_logo_dim = ((qr_w.min(qr_h) as f32 * ratio) as u32).max(1);
let padding = (max_logo_dim as f32 * 0.1) as u32;
let logo_target = max_logo_dim.saturating_sub(2 * padding).max(1);
let logo_resized = logo.resize(logo_target, logo_target, image::imageops::FilterType::Lanczos3);
let (lw, lh) = logo_resized.dimensions();
let x_off = (qr_w.saturating_sub(lw)) / 2;
let y_off = (qr_h.saturating_sub(lh)) / 2;
let bg_x = x_off.saturating_sub(padding);
let bg_y = y_off.saturating_sub(padding);
let bg_w = (lw + 2 * padding).min(qr_w - bg_x);
let bg_h = (lh + 2 * padding).min(qr_h - bg_y);
let white = Rgba([255u8, 255, 255, 255]);
for py in bg_y..bg_y + bg_h {
for px in bg_x..bg_x + bg_w {
result.put_pixel(px, py, white);
}
}
let logo_rgba = logo_resized.to_rgba8();
for py in 0..lh {
for px in 0..lw {
let src = logo_rgba.get_pixel(px, py);
let [sr, sg, sb, sa] = src.0;
if sa == 0 {
continue;
}
let dst = *result.get_pixel(x_off + px, y_off + py);
let [dr, dg, db, _da] = dst.0;
let af = sa as f32 / 255.0;
let inv = 1.0 - af;
let r = (dr as f32 * inv + sr as f32 * af) as u8;
let g = (dg as f32 * inv + sg as f32 * af) as u8;
let b = (db as f32 * inv + sb as f32 * af) as u8;
result.put_pixel(x_off + px, y_off + py, Rgba([r, g, b, 255]));
}
}
DynamicImage::ImageRgba8(result)
}
pub use image::ImageFormat;
pub fn encode_to_format(image: &DynamicImage, format: ImageFormat) -> image::ImageResult<Vec<u8>> {
let mut buf = std::io::Cursor::new(Vec::new());
image.write_to(&mut buf, format)?;
Ok(buf.into_inner())
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum GradientDirection {
Vertical,
Horizontal,
Diagonal,
}
#[derive(Copy, Clone, Debug)]
pub struct Gradient {
pub direction: GradientDirection,
pub start_color: Rgba<u8>,
pub end_color: Rgba<u8>,
}
pub fn apply_gradient_background(image: &DynamicImage, gradient: &Gradient) -> DynamicImage {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
let mut result = rgba.clone();
let sc = gradient.start_color.0;
let ec = gradient.end_color.0;
for y in 0..h {
for x in 0..w {
let pixel = *rgba.get_pixel(x, y);
let [r, g, b, a] = pixel.0;
let lum = (r as u32 + g as u32 + b as u32) / 3;
if lum > 200 && a > 0 {
let t = match gradient.direction {
GradientDirection::Vertical => {
if h <= 1 {
0.0
} else {
y as f32 / (h - 1) as f32
}
}
GradientDirection::Horizontal => {
if w <= 1 {
0.0
} else {
x as f32 / (w - 1) as f32
}
}
GradientDirection::Diagonal => {
if w <= 1 || h <= 1 {
0.0
} else {
(x as f32 / (w - 1) as f32 + y as f32 / (h - 1) as f32) / 2.0
}
}
};
let inv = 1.0 - t;
let nr = (sc[0] as f32 * inv + ec[0] as f32 * t) as u8;
let ng = (sc[1] as f32 * inv + ec[1] as f32 * t) as u8;
let nb = (sc[2] as f32 * inv + ec[2] as f32 * t) as u8;
let na = (sc[3] as f32 * inv + ec[3] as f32 * t) as u8;
result.put_pixel(x, y, Rgba([nr, ng, nb, na]));
}
}
}
DynamicImage::ImageRgba8(result)
}
#[cfg(test)]
mod render_tests {
use crate::render::Renderer;
use crate::types::Color;
use image::{GenericImageView, ImageBuffer, Luma, Rgba};
#[test]
fn test_render_luma8_unsized() {
let image = Renderer::<Luma<u8>>::new(
&[
Color::Light,
Color::Dark,
Color::Dark,
Color::Dark,
Color::Light,
Color::Light,
Color::Light,
Color::Dark,
Color::Light,
],
3,
1,
)
.module_dimensions(1, 1)
.build();
#[rustfmt::skip]
let expected = [
255, 255, 255, 255, 255,
255, 255, 0, 0, 255,
255, 0, 255, 255, 255,
255, 255, 0, 255, 255,
255, 255, 255, 255, 255,
];
assert_eq!(image.into_raw(), expected);
}
#[test]
fn test_render_rgba_unsized() {
let image = Renderer::<Rgba<u8>>::new(&[Color::Light, Color::Dark, Color::Dark, Color::Dark], 2, 1)
.module_dimensions(1, 1)
.build();
#[rustfmt::skip]
let expected: &[u8] = &[
255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
255,255,255,255, 255,255,255,255, 0, 0, 0,255, 255,255,255,255,
255,255,255,255, 0, 0, 0,255, 0, 0, 0,255, 255,255,255,255,
255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
];
assert_eq!(image.into_raw(), expected);
}
#[test]
fn test_render_resized_min() {
let image = Renderer::<Luma<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 1)
.min_dimensions(10, 10)
.build();
#[rustfmt::skip]
let expected: &[u8] = &[
255,255,255, 255,255,255, 255,255,255, 255,255,255,
255,255,255, 255,255,255, 255,255,255, 255,255,255,
255,255,255, 255,255,255, 255,255,255, 255,255,255,
255,255,255, 0, 0, 0, 255,255,255, 255,255,255,
255,255,255, 0, 0, 0, 255,255,255, 255,255,255,
255,255,255, 0, 0, 0, 255,255,255, 255,255,255,
255,255,255, 255,255,255, 0, 0, 0, 255,255,255,
255,255,255, 255,255,255, 0, 0, 0, 255,255,255,
255,255,255, 255,255,255, 0, 0, 0, 255,255,255,
255,255,255, 255,255,255, 255,255,255, 255,255,255,
255,255,255, 255,255,255, 255,255,255, 255,255,255,
255,255,255, 255,255,255, 255,255,255, 255,255,255,
];
assert_eq!(image.dimensions(), (12, 12));
assert_eq!(image.into_raw(), expected);
}
#[test]
fn test_render_resized_max() {
let image = Renderer::<Luma<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 1)
.max_dimensions(10, 5)
.build();
#[rustfmt::skip]
let expected: &[u8] = &[
255,255, 255,255, 255,255, 255,255,
255,255, 0, 0, 255,255, 255,255,
255,255, 255,255, 0, 0, 255,255,
255,255, 255,255, 255,255, 255,255,
];
assert_eq!(image.dimensions(), (8, 4));
assert_eq!(image.into_raw(), expected);
}
#[test]
fn test_overlay_logo() {
use super::overlay_logo;
use image::DynamicImage;
let qr = Renderer::<Rgba<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 0)
.module_dimensions(10, 10)
.build();
let qr_dyn = DynamicImage::ImageRgba8(qr);
let logo = DynamicImage::ImageRgba8(ImageBuffer::from_pixel(4, 4, Rgba([255u8, 0, 0, 255])));
let result = overlay_logo(&qr_dyn, &logo, 0.5);
assert_eq!(result.dimensions(), (20, 20));
let center = result.as_rgba8().unwrap().get_pixel(10, 10);
assert_eq!(center.0[0], 255); assert_eq!(center.0[3], 255); }
#[test]
fn test_overlay_logo_small_ratio() {
use super::overlay_logo;
use image::DynamicImage;
let qr = Renderer::<Rgba<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 0)
.module_dimensions(100, 100)
.build();
let qr_dyn = DynamicImage::ImageRgba8(qr);
let logo = DynamicImage::ImageRgba8(ImageBuffer::from_pixel(4, 4, Rgba([0u8, 255, 0, 255])));
let result = overlay_logo(&qr_dyn, &logo, 0.2);
assert_eq!(result.dimensions(), (200, 200));
}
#[test]
fn test_encode_to_format_png() {
use super::{ImageFormat, encode_to_format};
use image::DynamicImage;
let img = Renderer::<Rgba<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 0)
.module_dimensions(10, 10)
.build();
let dyn_img = DynamicImage::ImageRgba8(img);
let png_bytes = encode_to_format(&dyn_img, ImageFormat::Png).unwrap();
assert!(!png_bytes.is_empty());
assert_eq!(&png_bytes[..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
}
#[test]
fn test_encode_to_format_jpeg() {
use super::{ImageFormat, encode_to_format};
use image::DynamicImage;
let img = Renderer::<Rgba<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 0)
.module_dimensions(10, 10)
.build();
let dyn_img = DynamicImage::ImageRgba8(img);
let jpeg_bytes = encode_to_format(&dyn_img, ImageFormat::Jpeg).unwrap();
assert!(!jpeg_bytes.is_empty());
assert_eq!(&jpeg_bytes[..3], &[0xFF, 0xD8, 0xFF]);
}
#[test]
fn test_for_web() {
let img =
Renderer::<Luma<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 1).for_web().build();
assert!(img.dimensions().0 >= 200);
assert!(img.dimensions().1 >= 200);
}
#[test]
fn test_for_print_300() {
let img = Renderer::<Luma<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 1)
.for_print(300)
.build();
assert!(img.dimensions().0 >= 300);
assert!(img.dimensions().1 >= 300);
}
#[test]
fn test_for_social_twitter() {
let img = Renderer::<Luma<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 1)
.for_social("twitter")
.build();
assert!(img.dimensions().0 >= 400);
}
#[test]
fn test_for_social_instagram() {
let img = Renderer::<Luma<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 1)
.for_social("instagram")
.build();
assert!(img.dimensions().0 >= 1080);
}
#[test]
fn test_gradient_vertical() {
use super::{Gradient, GradientDirection, apply_gradient_background};
use image::DynamicImage;
let qr = Renderer::<Rgba<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 0)
.module_dimensions(10, 10)
.build();
let qr_dyn = DynamicImage::ImageRgba8(qr);
let gradient = Gradient {
direction: GradientDirection::Vertical,
start_color: Rgba([255, 0, 0, 255]),
end_color: Rgba([0, 0, 255, 255]),
};
let result = apply_gradient_background(&qr_dyn, &gradient);
assert_eq!(result.dimensions(), (20, 20));
let dark = result.as_rgba8().unwrap().get_pixel(0, 0);
assert_eq!(dark.0[0], 0);
assert_eq!(dark.0[1], 0);
assert_eq!(dark.0[2], 0);
}
#[test]
fn test_gradient_horizontal() {
use super::{Gradient, GradientDirection, apply_gradient_background};
use image::DynamicImage;
let qr = Renderer::<Rgba<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 0)
.module_dimensions(10, 10)
.build();
let qr_dyn = DynamicImage::ImageRgba8(qr);
let gradient = Gradient {
direction: GradientDirection::Horizontal,
start_color: Rgba([255, 0, 0, 255]),
end_color: Rgba([0, 0, 255, 255]),
};
let result = apply_gradient_background(&qr_dyn, &gradient);
assert_eq!(result.dimensions(), (20, 20));
}
#[test]
fn test_gradient_diagonal() {
use super::{Gradient, GradientDirection, apply_gradient_background};
use image::DynamicImage;
let qr = Renderer::<Rgba<u8>>::new(&[Color::Dark, Color::Light, Color::Light, Color::Dark], 2, 0)
.module_dimensions(10, 10)
.build();
let qr_dyn = DynamicImage::ImageRgba8(qr);
let gradient = Gradient {
direction: GradientDirection::Diagonal,
start_color: Rgba([255, 255, 0, 255]),
end_color: Rgba([0, 255, 255, 255]),
};
let result = apply_gradient_background(&qr_dyn, &gradient);
assert_eq!(result.dimensions(), (20, 20));
}
}