use std::borrow::Cow;
use image::{DynamicImage, ImageBuffer, Luma};
use imageproc::geometric_transformations::{Interpolation, rotate_about_center};
use crate::common::Result;
use crate::{Luma8LuminanceSource, LuminanceSource};
const MINUS_45_IN_RADIANS: f32 = std::f32::consts::FRAC_PI_4;
#[derive(Debug, Clone)]
pub struct BufferedImageLuminanceSource {
source: Luma8LuminanceSource,
}
impl BufferedImageLuminanceSource {
pub fn new(image: DynamicImage) -> Self {
let grey = build_local_grey_image(image);
let (width, height) = grey.dimensions();
let source = Luma8LuminanceSource::new(grey.into_raw(), width, height)
.expect("image dimensions match its buffer by construction");
Self { source }
}
}
impl LuminanceSource for BufferedImageLuminanceSource {
const SUPPORTS_CROP: bool = true;
const SUPPORTS_ROTATION: bool = true;
fn get_row(&'_ self, y: usize) -> Option<Cow<'_, [u8]>> {
self.source.get_row(y)
}
fn get_column(&self, x: usize) -> Cow<'_, [u8]> {
self.source.get_column(x)
}
fn get_matrix(&self) -> Cow<'_, [u8]> {
self.source.get_matrix()
}
fn get_width(&self) -> usize {
self.source.get_width()
}
fn get_height(&self) -> usize {
self.source.get_height()
}
fn crop(&self, left: usize, top: usize, width: usize, height: usize) -> Result<Self> {
Ok(Self {
source: self.source.crop(left, top, width, height)?,
})
}
fn invert(&mut self) {
self.source.invert()
}
fn rotate_counter_clockwise(&self) -> Result<Self> {
Ok(Self {
source: self.source.rotate_counter_clockwise()?,
})
}
fn rotate_counter_clockwise_45(&self) -> Result<Self> {
let (width, height) = (self.get_width() as u32, self.get_height() as u32);
let buffer = ImageBuffer::from_raw(width, height, self.source.get_matrix().into_owned())
.ok_or_else(|| {
crate::Exceptions::illegal_argument_with("matrix does not match its dimensions")
})?;
let rotated = rotate_about_center(
&buffer,
MINUS_45_IN_RADIANS,
Interpolation::Nearest,
Luma([u8::MAX / 2; 1]),
);
Ok(Self {
source: Luma8LuminanceSource::new(rotated.into_raw(), width, height)?,
})
}
fn get_luma8_point(&self, x: usize, y: usize) -> u8 {
self.source.get_luma8_point(x, y)
}
}
fn build_local_grey_image(source: DynamicImage) -> ImageBuffer<Luma<u8>, Vec<u8>> {
match source {
DynamicImage::ImageLuma8(img) => img,
DynamicImage::ImageLumaA8(img) => {
let mut raster: ImageBuffer<_, Vec<_>> = ImageBuffer::new(img.width(), img.height());
for (x, y, new_pixel) in raster.enumerate_pixels_mut() {
let pixel = img.get_pixel(x, y);
let [luma, alpha] = pixel.0;
if alpha == 0 {
*new_pixel = Luma([0xFF])
} else {
*new_pixel = Luma([luma])
}
}
raster
}
DynamicImage::ImageLuma16(img) => {
let mut raster: ImageBuffer<_, Vec<_>> = ImageBuffer::new(img.width(), img.height());
for (x, y, new_pixel) in raster.enumerate_pixels_mut() {
let pixel = img.get_pixel(x, y);
let [luma] = pixel.0;
*new_pixel = Luma([(luma >> 8) as u8])
}
raster
}
DynamicImage::ImageLumaA16(img) => {
let mut raster: ImageBuffer<_, Vec<_>> = ImageBuffer::new(img.width(), img.height());
for (x, y, new_pixel) in raster.enumerate_pixels_mut() {
let pixel = img.get_pixel(x, y);
let [luma, alpha] = pixel.0;
if alpha == 0 {
*new_pixel = Luma([0xFF])
} else {
*new_pixel = Luma([(luma >> 8) as u8])
}
}
raster
}
_ => {
let img = source.to_rgba8();
let mut raster: ImageBuffer<_, Vec<_>> =
ImageBuffer::new(source.width(), source.height());
for (x, y, new_pixel) in raster.enumerate_pixels_mut() {
let pixel = img.get_pixel(x, y);
let [red, green, blue, alpha] = pixel.0;
if alpha == 0 {
*new_pixel = Luma([0xFF])
} else {
*new_pixel = Luma([((306 * (red as u64)
+ 601 * (green as u64)
+ 117 * (blue as u64)
+ 0x200)
>> 10) as u8])
}
}
raster
}
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use image::{DynamicImage, ImageBuffer};
use crate::{BufferedImageLuminanceSource, LuminanceSource};
fn luma_4x4() -> BufferedImageLuminanceSource {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_raw(4, 4, (0..16).collect()).unwrap());
BufferedImageLuminanceSource::new(img)
}
#[test]
fn conversion_passes_luma8_through() {
let src = luma_4x4();
assert_eq!(src.get_width(), 4);
assert_eq!(src.get_height(), 4);
assert_eq!(&*src.get_matrix(), &(0..16).collect::<Vec<u8>>()[..]);
}
#[test]
fn conversion_luma_a8_matches_zxing_reference() {
let img = DynamicImage::ImageLumaA8(
ImageBuffer::from_raw(4, 1, vec![50, 0, 50, 255, 0, 255, 2, 3]).unwrap(),
);
let src = BufferedImageLuminanceSource::new(img);
assert_eq!(&*src.get_matrix(), &[255, 50, 0, 2]);
}
#[test]
fn conversion_luma16_matches_zxing_reference() {
let img = DynamicImage::ImageLuma16(
ImageBuffer::from_raw(3, 1, vec![0xFFFFu16, 0x0000, 0x7F00]).unwrap(),
);
let src = BufferedImageLuminanceSource::new(img);
assert_eq!(&*src.get_matrix(), &[255, 0, 127]);
}
#[test]
fn conversion_luma_a16_matches_zxing_reference() {
#[rustfmt::skip]
let pixels = vec![
0xFFFFu16, 0x0000, 0xFFFF, 0xFFFF, 0x0000, 0xFFFF, 0x7F00, 0x0101, ];
let img = DynamicImage::ImageLumaA16(ImageBuffer::from_raw(4, 1, pixels).unwrap());
let src = BufferedImageLuminanceSource::new(img);
assert_eq!(&*src.get_matrix(), &[255, 255, 0, 127]);
}
#[test]
#[cfg(all(feature = "encoders", feature = "decoders", feature = "qrcode"))]
fn grey_toned_opaque_luma_a8_decodes() {
use crate::common::HybridBinarizer;
use crate::{
BarcodeFormat, BinaryBitmap, DecodeHints, MultiFormatReader, MultiFormatWriter, Reader,
Writer,
};
let content = "grey on grey";
let bits = MultiFormatWriter
.encode(content, &BarcodeFormat::QR_CODE, 128, 128)
.expect("encode succeeds");
let (w, h) = (bits.width(), bits.height());
let mut pixels = Vec::with_capacity((w * h * 2) as usize);
for y in 0..h {
for x in 0..w {
pixels.extend([if bits.get(x, y) { 60u8 } else { 200 }, 255]);
}
}
let img = DynamicImage::ImageLumaA8(ImageBuffer::from_raw(w, h, pixels).unwrap());
let mut bitmap =
BinaryBitmap::new(HybridBinarizer::new(BufferedImageLuminanceSource::new(img)));
let result = MultiFormatReader::default()
.decode_with_hints(&mut bitmap, &DecodeHints::default())
.expect("grey-toned opaque QR must decode");
assert_eq!(result.getText(), content);
}
#[test]
fn conversion_pins_rgba_semantics() {
#[rustfmt::skip]
let pixels = vec![
0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 255, ];
let img = DynamicImage::ImageRgba8(ImageBuffer::from_raw(4, 1, pixels).unwrap());
let src = BufferedImageLuminanceSource::new(img);
assert_eq!(&*src.get_matrix(), &[255, 255, 0, 76]);
}
#[test]
fn row_column_and_point_accessors_agree() {
let src = luma_4x4();
assert_eq!(&*src.get_row(1).expect("row in bounds"), &[4, 5, 6, 7]);
assert_eq!(&*src.get_column(2), &[2, 6, 10, 14]);
assert_eq!(src.get_luma8_point(3, 2), 11);
}
#[test]
fn invert_inverts_all_accessors() {
let img =
DynamicImage::ImageLuma8(ImageBuffer::from_raw(2, 2, vec![0, 10, 100, 255]).unwrap());
let mut src = BufferedImageLuminanceSource::new(img);
src.invert();
assert_eq!(&*src.get_matrix(), &[255, 245, 155, 0]);
assert_eq!(src.get_luma8_point(0, 0), 255);
assert_eq!(&*src.get_column(1), &[245, 0]);
}
#[test]
fn crop_and_rotate_have_correct_values() {
let src = luma_4x4();
let cropped = src.crop(1, 1, 2, 2).expect("crop");
assert_eq!(&*cropped.get_matrix(), &[5, 6, 9, 10]);
let rotated = src.rotate_counter_clockwise().expect("rotate");
assert_eq!(
&*rotated.get_row(0).expect("row in bounds"),
&[3, 7, 11, 15]
);
}
#[test]
fn rotate_45_pins_current_output() {
let img = DynamicImage::ImageLuma8(ImageBuffer::from_raw(3, 3, (0..9).collect()).unwrap());
let src = BufferedImageLuminanceSource::new(img);
let rotated = src.rotate_counter_clockwise_45().expect("rotate 45");
assert_eq!(rotated.get_width(), 3);
assert_eq!(rotated.get_height(), 3);
assert_eq!(&*rotated.get_matrix(), &[127, 3, 1, 6, 7, 5, 127, 8, 8]);
}
#[test]
fn crop_is_zero_copy() {
let src = luma_4x4();
let cropped = src.crop(1, 1, 2, 2).expect("crop");
let Cow::Borrowed(parent) = src.get_matrix() else {
panic!("full-view matrix must be Cow::Borrowed");
};
let Some(Cow::Borrowed(row)) = cropped.get_row(0) else {
panic!("cropped row must be Cow::Borrowed");
};
let base = parent.as_ptr() as usize;
let p = row.as_ptr() as usize;
assert!(
p >= base && p < base + parent.len(),
"crop must share the parent's buffer, not copy the region"
);
}
}