#![cfg(feature = "image")]
use crate::{Canvas, Pixel, RenderError, StyledPixel};
use qrcode_core::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 StyledPixel for Rgb<u8> {
fn from_hex(hex: &str) -> Self {
let (r, g, b) = crate::colors::hex_to_rgb(hex).unwrap_or((0, 0, 0));
Rgb([r, g, b])
}
}
impl StyledPixel for Rgba<u8> {
fn from_hex(hex: &str) -> Self {
let (r, g, b) = crate::colors::hex_to_rgb(hex).unwrap_or((0, 0, 0));
Rgba([r, g, b, 255])
}
}
impl<P: image::Pixel + 'static> Canvas for (P, ImageBuffer<P, Vec<P::Subpixel>>) {
type Pixel = P;
type Image = ImageBuffer<P, Vec<P::Subpixel>>;
fn validate_dimensions(width: u32, height: u32, _dark_pixel: &P, _light_pixel: &P) -> Result<(), RenderError> {
let bytes = (width as usize)
.checked_mul(usize::from(P::CHANNEL_COUNT))
.and_then(|row_samples| row_samples.checked_mul(height as usize))
.and_then(|samples| samples.checked_mul(core::mem::size_of::<P::Subpixel>()))
.ok_or(RenderError::OutputTooLarge)?;
crate::check_buffer_size(bytes)
}
fn new(width: u32, height: u32, dark_pixel: P, light_pixel: P) -> Self {
if let Err(error) = Self::validate_dimensions(width, height, &dark_pixel, &light_pixel) {
panic!("image canvas dimensions are too large: {error}");
}
(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 draw_dark_rect(&mut self, left: u32, top: u32, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
let (image_width, image_height) = self.1.dimensions();
assert!(
left < image_width && top < image_height && width <= image_width - left && height <= image_height - top,
"rectangle exceeds image dimensions"
);
let channels = self.0.channels();
let channel_count = usize::from(P::CHANNEL_COUNT);
let row_stride = image_width as usize * channel_count;
let row_width = width as usize * channel_count;
let row_left = left as usize * channel_count;
let data: &mut [P::Subpixel] = &mut self.1;
if channel_count == 3 && width >= 8 && height > 1 {
let first_start = top as usize * row_stride + row_left;
let (first_rows, remaining_rows) = data.split_at_mut(first_start + row_stride);
let first_row = &mut first_rows[first_start..first_start + row_width];
for pixel in first_row.chunks_exact_mut(channel_count) {
pixel.copy_from_slice(channels);
}
for row in remaining_rows.chunks_mut(row_stride).take(height as usize - 1) {
row[..row_width].copy_from_slice(first_row);
}
return;
}
for y in top..top + height {
let start = y as usize * row_stride + row_left;
let row = &mut data[start..start + row_width];
for pixel in row.chunks_exact_mut(channel_count) {
pixel.copy_from_slice(channels);
}
}
}
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();
if qr_w == 0 || qr_h == 0 {
return DynamicImage::ImageRgba8(result);
}
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.into_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 mut result = image.to_rgba8();
let (w, h) = result.dimensions();
let sc = gradient.start_color.0;
let ec = gradient.end_color.0;
if gradient.direction == GradientDirection::Vertical && w > 1 {
for (y, row) in result.rows_mut().enumerate() {
let mut row_color = None;
for pixel in row {
let [r, g, b, a] = pixel.0;
let lum = (r as u32 + g as u32 + b as u32) / 3;
if lum > 200 && a > 0 {
*pixel = *row_color.get_or_insert_with(|| {
let t = if h <= 1 { 0.0 } else { y as u32 as f32 / (h - 1) as f32 };
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;
Rgba([nr, ng, nb, na])
});
}
}
}
return DynamicImage::ImageRgba8(result);
}
if gradient.direction == GradientDirection::Horizontal && h >= 4 && (2..=16_384).contains(&w) {
let mut colors = Vec::new();
if colors.try_reserve_exact(w as usize).is_ok() {
for x in 0..w {
let t = x as f32 / (w - 1) as f32;
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;
colors.push(Rgba([nr, ng, nb, na]));
}
for row in result.rows_mut() {
for (pixel, &color) in row.zip(&colors) {
let [r, g, b, a] = pixel.0;
let lum = (r as u32 + g as u32 + b as u32) / 3;
if lum > 200 && a > 0 {
*pixel = color;
}
}
}
return DynamicImage::ImageRgba8(result);
}
}
if gradient.direction == GradientDirection::Diagonal && w > 1 && h > 1 {
for (y, row) in result.rows_mut().enumerate() {
let row_ratio = y as u32 as f32 / (h - 1) as f32;
for (x, pixel) in row.enumerate() {
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 = (x as u32 as f32 / (w - 1) as f32 + row_ratio) / 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;
*pixel = Rgba([nr, ng, nb, na]);
}
}
}
return DynamicImage::ImageRgba8(result);
}
for (x, y, pixel) in result.enumerate_pixels_mut() {
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;
*pixel = Rgba([nr, ng, nb, na]);
}
}
DynamicImage::ImageRgba8(result)
}
#[cfg(test)]
mod render_tests {
use crate::{Canvas, MAX_BUFFER_BYTES, RenderError, Renderer};
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, LumaA, Rgb, Rgba};
use qrcode_core::Color;
fn assert_image_buffer_budget<P>(dark: P, light: P, bytes_per_pixel: usize)
where
P: image::Pixel + 'static,
{
let validate = <(P, ImageBuffer<P, Vec<P::Subpixel>>) as Canvas>::validate_dimensions;
let max_pixels = u32::try_from(MAX_BUFFER_BYTES / bytes_per_pixel).unwrap();
assert_eq!(validate(max_pixels, 1, &dark, &light), Ok(()));
assert_eq!(validate(1, max_pixels, &dark, &light), Ok(()));
assert_eq!(validate(max_pixels + 1, 1, &dark, &light), Err(RenderError::OutputTooLarge));
assert_eq!(validate(1, max_pixels + 1, &dark, &light), Err(RenderError::OutputTooLarge));
assert_eq!(validate(65_536, 65_536, &dark, &light), Err(RenderError::OutputTooLarge));
assert_eq!(validate(u32::MAX, u32::MAX, &dark, &light), Err(RenderError::OutputTooLarge));
let wide_empty =
if usize::BITS == 32 && P::CHANNEL_COUNT > 1 { Err(RenderError::OutputTooLarge) } else { Ok(()) };
assert_eq!(validate(u32::MAX, 0, &dark, &light), wide_empty);
assert_eq!(validate(0, u32::MAX, &dark, &light), Ok(()));
}
#[test]
fn image_buffer_budget_accounts_for_channels_and_subpixel_sizes_without_allocating() {
assert_image_buffer_budget(Luma([0u8]), Luma([255]), 1);
assert_image_buffer_budget(LumaA([0u8, 255]), LumaA([255, 255]), 2);
assert_image_buffer_budget(Rgb([0u8, 0, 0]), Rgb([255, 255, 255]), 3);
assert_image_buffer_budget(Rgba([0u8, 0, 0, 255]), Rgba([255, 255, 255, 255]), 4);
assert_image_buffer_budget(Luma([0u16]), Luma([65535]), 2);
assert_image_buffer_budget(LumaA([0u16, 65535]), LumaA([65535, 65535]), 4);
assert_image_buffer_budget(Rgb([0u16, 0, 0]), Rgb([65535, 65535, 65535]), 6);
assert_image_buffer_budget(Rgba([0u16, 0, 0, 65535]), Rgba([65535, 65535, 65535, 65535]), 8);
assert_image_buffer_budget(Luma([0.0f32]), Luma([1.0]), 4);
assert_image_buffer_budget(LumaA([0.0f32, 1.0]), LumaA([1.0, 1.0]), 8);
assert_image_buffer_budget(Rgb([0.0f32, 0.0, 0.0]), Rgb([1.0, 1.0, 1.0]), 12);
assert_image_buffer_budget(Rgba([0.0f32, 0.0, 0.0, 1.0]), Rgba([1.0, 1.0, 1.0, 1.0]), 16);
assert_image_buffer_budget(Luma([0.0f64]), Luma([1.0]), 8);
assert_image_buffer_budget(LumaA([0.0f64, 1.0]), LumaA([1.0, 1.0]), 16);
assert_image_buffer_budget(Rgb([0.0f64, 0.0, 0.0]), Rgb([1.0, 1.0, 1.0]), 24);
assert_image_buffer_budget(Rgba([0.0f64, 0.0, 0.0, 1.0]), Rgba([1.0, 1.0, 1.0, 1.0]), 32);
}
#[test]
#[should_panic(expected = "image canvas dimensions are too large")]
fn direct_image_canvas_construction_rejects_oversized_dimensions_before_allocating() {
let _ = <(Rgba<u8>, ImageBuffer<Rgba<u8>, Vec<u8>>) as Canvas>::new(
65_536,
65_536,
Rgba([0, 0, 0, 255]),
Rgba([255, 255, 255, 255]),
);
}
fn assert_rectangle_matches_scalar<P>(
image_width: u32,
image_height: u32,
rectangle: (u32, u32, u32, u32),
dark: P,
light: P,
) where
P: image::Pixel + 'static,
P::Subpixel: std::fmt::Debug,
{
let (left, top, width, height) = rectangle;
let mut canvas = <(P, ImageBuffer<P, Vec<P::Subpixel>>) as Canvas>::new(image_width, image_height, dark, light);
let mut expected = ImageBuffer::from_pixel(image_width, image_height, light);
canvas.draw_dark_rect(left, top, width, height);
for y in top..top + height {
for x in left..left + width {
expected.put_pixel(x, y, dark);
}
}
assert_eq!(
canvas.into_image().as_raw(),
expected.as_raw(),
"image {image_width}x{image_height}, rectangle ({left}, {top}, {width}, {height})"
);
}
fn assert_rectangles_match_scalar<P>(dark: P, light: P)
where
P: image::Pixel + 'static,
P::Subpixel: std::fmt::Debug,
{
for (image_width, image_height) in [(0, 0), (0, 5), (7, 0), (1, 5), (7, 1), (7, 5)] {
for left in 0..=image_width {
for top in 0..=image_height {
for width in 0..=image_width - left {
for height in 0..=image_height - top {
assert_rectangle_matches_scalar(
image_width,
image_height,
(left, top, width, height),
dark,
light,
);
}
}
}
}
}
for width in [1, 2, 7, 8, 32] {
for height in [1, 8, 64] {
for (left, top) in [(0, 0), (3, 2), (6, 4)] {
assert_rectangle_matches_scalar(width + 6, height + 4, (left, top, width, height), dark, light);
}
}
}
}
#[test]
fn rectangles_match_scalar_for_all_pixel_formats_and_primitives() {
macro_rules! check_primitives {
($($subpixel:ty),+ $(,)?) => {
$(
assert_rectangles_match_scalar(Luma([2 as $subpixel]), Luma([11 as $subpixel]));
assert_rectangles_match_scalar(
LumaA([2 as $subpixel, 3 as $subpixel]),
LumaA([11 as $subpixel, 12 as $subpixel]),
);
assert_rectangles_match_scalar(
Rgb([2 as $subpixel, 3 as $subpixel, 4 as $subpixel]),
Rgb([11 as $subpixel, 12 as $subpixel, 13 as $subpixel]),
);
assert_rectangles_match_scalar(
Rgba([2 as $subpixel, 3 as $subpixel, 4 as $subpixel, 5 as $subpixel]),
Rgba([11 as $subpixel, 12 as $subpixel, 13 as $subpixel, 14 as $subpixel]),
);
)+
};
}
check_primitives!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64);
}
fn assert_rectangle_bits<P>(dark: P, light: P, bits: impl Fn(P::Subpixel) -> u64 + Copy)
where
P: image::Pixel + 'static,
{
for width in [1, 2, 7, 8, 32] {
for height in [1, 8, 64] {
for (left, top) in [(0, 0), (3, 2), (6, 4)] {
let mut canvas =
<(P, ImageBuffer<P, Vec<P::Subpixel>>) as Canvas>::new(width + 6, height + 4, dark, light);
let mut expected = ImageBuffer::from_pixel(width + 6, height + 4, light);
canvas.draw_dark_rect(left, top, width, height);
canvas.draw_dark_rect(left, top, width, height);
for y in top..top + height {
for x in left..left + width {
expected.put_pixel(x, y, dark);
}
}
let actual = canvas.into_image().into_raw().into_iter().map(bits).collect::<Vec<_>>();
let expected = expected.into_raw().into_iter().map(bits).collect::<Vec<_>>();
assert_eq!(actual, expected, "rectangle ({left}, {top}, {width}, {height})");
}
}
}
}
#[test]
fn float_rectangle_copies_preserve_nan_payloads_and_signed_zero_bits() {
macro_rules! check_float {
($float:ty, $dark_nan:expr, $signaling_nan:expr, $light_nan:expr, $bits:expr) => {{
let dark =
[<$float>::from_bits($dark_nan), <$float>::from_bits($signaling_nan), -0.0, <$float>::INFINITY];
let light = [<$float>::from_bits($light_nan), 0.0, <$float>::NEG_INFINITY, 1.0];
let bits = $bits;
assert_rectangle_bits(Luma([dark[0]]), Luma([light[0]]), bits);
assert_rectangle_bits(LumaA([dark[0], dark[2]]), LumaA([light[0], light[1]]), bits);
assert_rectangle_bits(Rgb([dark[0], dark[1], dark[2]]), Rgb([light[0], light[1], light[2]]), bits);
assert_rectangle_bits(Rgba(dark), Rgba(light), bits);
}};
}
check_float!(f32, 0x7fc0_0123, 0x7fa0_0067, 0xffc0_0456, |value: f32| u64::from(value.to_bits()));
check_float!(f64, 0x7ff8_0000_0000_0123, 0x7ff0_0000_0000_0067, 0xfff8_0000_0000_0456, f64::to_bits);
}
#[test]
fn rectangles_reject_out_of_bounds_ranges() {
for (left, top, width, height) in [
(7, 0, 1, 1),
(0, 5, 1, 1),
(6, 0, 2, 1),
(0, 4, 1, 2),
(u32::MAX, 0, 1, 1),
(0, u32::MAX, 1, 1),
(1, 0, u32::MAX, 1),
(0, 1, 1, u32::MAX),
] {
let result = std::panic::catch_unwind(|| {
let mut canvas = (Luma([0u8]), ImageBuffer::from_pixel(7, 5, Luma([255])));
canvas.draw_dark_rect(left, top, width, height);
});
assert!(result.is_err(), "rectangle ({left}, {top}, {width}, {height}) should panic");
}
}
fn scalar_gradient(image: &DynamicImage, gradient: &super::Gradient) -> DynamicImage {
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
let mut result = rgba.clone();
for y in 0..height {
for x in 0..width {
let [r, g, b, a] = rgba.get_pixel(x, y).0;
let luminance = (u32::from(r) + u32::from(g) + u32::from(b)) / 3;
if luminance <= 200 || a == 0 {
continue;
}
let t = match gradient.direction {
super::GradientDirection::Vertical if height > 1 => y as f32 / (height - 1) as f32,
super::GradientDirection::Horizontal if width > 1 => x as f32 / (width - 1) as f32,
super::GradientDirection::Diagonal if width > 1 && height > 1 => {
(x as f32 / (width - 1) as f32 + y as f32 / (height - 1) as f32) / 2.0
}
_ => 0.0,
};
let mut color = [0u8; 4];
for (channel, value) in color.iter_mut().enumerate() {
*value = (gradient.start_color.0[channel] as f32 * (1.0 - t)
+ gradient.end_color.0[channel] as f32 * t) as u8;
}
result.put_pixel(x, y, Rgba(color));
}
}
DynamicImage::ImageRgba8(result)
}
#[test]
fn gradient_matches_scalar_for_all_directions_dimensions_and_image_formats() {
use super::{Gradient, GradientDirection, apply_gradient_background};
let colors = [
Rgba([255, 255, 255, 255]),
Rgba([0, 0, 0, 255]),
Rgba([200, 200, 200, 255]),
Rgba([201, 200, 202, 1]),
Rgba([255, 255, 255, 0]),
Rgba([254, 247, 241, 127]),
Rgba([0, 255, 255, 255]),
];
for (width, height) in [(0, 0), (0, 3), (3, 0), (1, 1), (1, 7), (7, 1), (5, 3), (9, 7), (2, 31), (127, 33)] {
let rgba = DynamicImage::ImageRgba8(ImageBuffer::from_fn(width, height, |x, y| {
colors[((y * width + x) as usize) % colors.len()]
}));
let images = [
rgba.clone(),
DynamicImage::ImageLuma8(rgba.to_luma8()),
DynamicImage::ImageLumaA8(rgba.to_luma_alpha8()),
DynamicImage::ImageRgb8(rgba.to_rgb8()),
DynamicImage::ImageLuma16(rgba.to_luma16()),
DynamicImage::ImageLumaA16(rgba.to_luma_alpha16()),
DynamicImage::ImageRgb16(rgba.to_rgb16()),
DynamicImage::ImageRgba16(rgba.to_rgba16()),
DynamicImage::ImageRgb32F(rgba.to_rgb32f()),
DynamicImage::ImageRgba32F(rgba.to_rgba32f()),
];
for image in images {
for direction in
[GradientDirection::Vertical, GradientDirection::Horizontal, GradientDirection::Diagonal]
{
let gradient = Gradient {
direction,
start_color: Rgba([23, 117, 251, 19]),
end_color: Rgba([250, 61, 9, 231]),
};
assert_eq!(
apply_gradient_background(&image, &gradient).as_rgba8().unwrap().as_raw(),
scalar_gradient(&image, &gradient).as_rgba8().unwrap().as_raw(),
"{direction:?}, {width}x{height}, {:?}",
image.color()
);
}
}
}
}
#[test]
fn vertical_gradient_cache_preserves_first_light_pixels_thresholds_and_alpha() {
use super::{Gradient, GradientDirection, apply_gradient_background};
for (width, height) in [(0, 0), (0, 31), (31, 0), (1, 31), (31, 1), (2, 31), (17, 31)] {
for phase in [0, 3, 4] {
let image =
DynamicImage::ImageRgba8(ImageBuffer::from_fn(width, height, |x, y| match (y + phase) % 7 {
1 => Rgba([255, 255, 255, 0]),
2 => Rgba([200, 200, 202, 255]),
3 if x + 1 == width => Rgba([255, 255, 255, 127]),
4 if x <= 1 => Rgba([201, 200, 202, 1]),
5 if x % 3 == 0 => Rgba([254, 247, 241, 255]),
_ => Rgba([12, 47, 91, 255]),
}));
for (start, end) in [
([23, 117, 251, 19], [250, 61, 9, 231]),
([255, 255, 255, 0], [0, 0, 0, 255]),
([1, 2, 3, 255], [255, 254, 253, 0]),
] {
let gradient = Gradient {
direction: GradientDirection::Vertical,
start_color: Rgba(start),
end_color: Rgba(end),
};
assert_eq!(
apply_gradient_background(&image, &gradient).as_rgba8().unwrap().as_raw(),
scalar_gradient(&image, &gradient).as_rgba8().unwrap().as_raw(),
"{width}x{height}, phase {phase}, {start:?} -> {end:?}"
);
}
}
}
}
#[test]
fn horizontal_gradient_cache_matches_scalar_across_cache_limits_and_rounding() {
use super::{Gradient, GradientDirection, apply_gradient_background};
let pixels = [
Rgba([255, 255, 255, 255]),
Rgba([201, 200, 202, 1]),
Rgba([200, 200, 202, 255]),
Rgba([255, 255, 255, 0]),
Rgba([0, 0, 0, 255]),
];
for (width, height) in [(1, 4), (2, 3), (2, 4), (127, 4), (16_384, 4), (16_385, 4)] {
let image = DynamicImage::ImageRgba8(ImageBuffer::from_fn(width, height, |x, y| {
pixels[((x + 3 * y) as usize) % pixels.len()]
}));
for (start, end) in [([23, 117, 251, 19], [250, 61, 9, 231]), ([219; 4], [219; 4])] {
let gradient = Gradient {
direction: GradientDirection::Horizontal,
start_color: Rgba(start),
end_color: Rgba(end),
};
assert_eq!(
apply_gradient_background(&image, &gradient).as_rgba8().unwrap().as_raw(),
scalar_gradient(&image, &gradient).as_rgba8().unwrap().as_raw(),
"{width}x{height}, {start:?} -> {end:?}"
);
}
}
}
#[test]
fn diagonal_gradient_rows_preserve_per_pixel_rounding_and_alpha() {
use super::{Gradient, GradientDirection, apply_gradient_background};
let pixels = [
Rgba([255, 255, 255, 255]),
Rgba([201, 200, 202, 1]),
Rgba([200, 200, 202, 255]),
Rgba([255, 255, 255, 0]),
Rgba([0, 0, 0, 255]),
];
for (width, height) in [(1, 7), (7, 1), (2, 31), (31, 2), (127, 33), (33, 127)] {
let image = DynamicImage::ImageRgba8(ImageBuffer::from_fn(width, height, |x, y| {
pixels[((x + 3 * y) as usize) % pixels.len()]
}));
for (start, end) in [([23, 117, 251, 19], [250, 61, 9, 231]), ([219; 4], [219; 4])] {
let gradient =
Gradient { direction: GradientDirection::Diagonal, start_color: Rgba(start), end_color: Rgba(end) };
assert_eq!(
apply_gradient_background(&image, &gradient).as_rgba8().unwrap().as_raw(),
scalar_gradient(&image, &gradient).as_rgba8().unwrap().as_raw(),
"{width}x{height}, {start:?} -> {end:?}"
);
}
}
}
#[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 transparent_logo_on_transparent_qr_keeps_the_white_backplate_and_outside_alpha() {
use super::overlay_logo;
let original = Rgba([12, 47, 91, 0]);
let qr = DynamicImage::ImageRgba8(ImageBuffer::from_pixel(40, 40, original));
let logo = DynamicImage::ImageRgba8(ImageBuffer::from_pixel(1, 1, Rgba([17, 43, 89, 0])));
let result = overlay_logo(&qr, &logo, 0.25).into_rgba8();
let expected = ImageBuffer::from_fn(40, 40, |x, y| {
if (15..25).contains(&x) && (15..25).contains(&y) { Rgba([255; 4]) } else { original }
});
assert_eq!(result, expected);
}
#[test]
fn overlay_logo_preserves_empty_qr_dimensions_in_rgba8() {
use super::overlay_logo;
for (width, height) in [(0, 0), (0, 1), (1, 0), (0, 9), (13, 0)] {
for qr in [
DynamicImage::ImageRgba8(ImageBuffer::new(width, height)),
DynamicImage::ImageLuma8(ImageBuffer::new(width, height)),
DynamicImage::ImageRgb32F(ImageBuffer::new(width, height)),
] {
for alpha in [0, 127, 255] {
let logo = DynamicImage::ImageRgba8(ImageBuffer::from_pixel(1, 1, Rgba([17, 43, 89, alpha])));
for ratio in [0.25, f32::NAN, f32::NEG_INFINITY, f32::INFINITY] {
let result = overlay_logo(&qr, &logo, ratio);
assert_eq!(result.dimensions(), (width, height));
assert!(result.as_rgba8().unwrap().as_raw().is_empty());
}
}
}
}
}
#[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));
}
}