diff --git a/Cargo.lock b/Cargo.lock index 9eabddd..1055f09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -767,7 +767,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rxing-cli" -version = "0.0.8" +version = "0.0.9" dependencies = [ "anyhow", "base64", @@ -781,7 +781,7 @@ dependencies = [ [[package]] name = "rxing-reader" -version = "0.0.8" +version = "0.0.9" dependencies = [ "anyhow", "bitvec", @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "rxing-wasm" -version = "0.0.8" +version = "0.0.9" dependencies = [ "js-sys", "rxing-reader", diff --git a/Cargo.toml b/Cargo.toml index ea78cf9..757359e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ resolver = "3" [workspace.package] -version = "0.0.8" +version = "0.0.9" [profile.release] codegen-units = 1 diff --git a/rxing-cli/src/main.rs b/rxing-cli/src/main.rs index b0ae674..22683db 100644 --- a/rxing-cli/src/main.rs +++ b/rxing-cli/src/main.rs @@ -93,7 +93,7 @@ fn fetch_url(url: &str) -> Result> { Ok(buf) } -fn decode_image_bytes(bytes: &[u8]) -> Result<(Vec, u32, u32)> { +fn decode_image_bytes(bytes: &[u8]) -> Result<(Vec, usize, usize)> { let mut reader = ImageReader::new(Cursor::new(bytes)) .with_guessed_format() .context("could not guess image format")?; @@ -103,11 +103,11 @@ fn decode_image_bytes(bytes: &[u8]) -> Result<(Vec, u32, u32)> { limits.max_alloc = Some(512 * 1024 * 1024); reader.limits(limits); let rgba = reader.decode().context("image decode failed")?.into_rgba8(); - let (w, h) = (rgba.width(), rgba.height()); + let (w, h) = (rgba.width() as usize, rgba.height() as usize); Ok((rgba.into_raw(), w, h)) } -fn decode_payloads(rgba: &[u8], w: u32, h: u32, max: u32) -> Result>> { +fn decode_payloads(rgba: &[u8], w: usize, h: usize, max: usize) -> Result>> { let luma = rgba_to_luma(rgba, w, h).map_err(anyhow::Error::msg)?; let primary = decode_qr_codes_luma(&luma, w, h, true, true, true, max)?; if !primary.is_empty() { diff --git a/rxing-reader/src/binarizer.rs b/rxing-reader/src/binarizer.rs index 43f27ec..d6e4342 100644 --- a/rxing-reader/src/binarizer.rs +++ b/rxing-reader/src/binarizer.rs @@ -18,7 +18,7 @@ use anyhow::Result; use crate::{LuminanceSource, common::BitMatrix}; -/// Converts greyscale luminance data into one-bit black/white barcode data. +/// Converts greyscale luminance data into one-bit black/white QR image data. /// /// Implementations can choose different thresholding strategies while exposing /// the same row, line, and matrix accessors to readers. diff --git a/rxing-reader/src/binary_bitmap.rs b/rxing-reader/src/binary_bitmap.rs index 50589e2..ab7eceb 100644 --- a/rxing-reader/src/binary_bitmap.rs +++ b/rxing-reader/src/binary_bitmap.rs @@ -22,7 +22,7 @@ use crate::{Binarizer, common::BitMatrix}; /// One-bit view of an image produced by a [`Binarizer`]. /// -/// Readers consume `BinaryBitmap` values when searching for barcodes. +/// Readers consume `BinaryBitmap` values when searching for QR symbols. pub struct BinaryBitmap { binarizer: B, } @@ -49,7 +49,7 @@ impl BinaryBitmap { /// any pixel whose 3×3 neighborhood contains at least one black pixel), /// then erode (keep only pixels whose 3×3 neighborhood is entirely /// black). Useful as a post-binarization denoise step for marginal - /// photos (cf. zxing-cpp's `tryDenoise`). The 1-pixel border is left + /// photos. The 1-pixel border is left /// unchanged. No-op if the matrix is smaller than 3×3. pub fn close(&mut self) -> Result<()> { let matrix = self.black_matrix_mut()?; @@ -72,7 +72,7 @@ fn sum_filter_3x3 bool>(input: &BitMatrix, output: &mut BitMatrix, for row in 1..h - 1 { for col in 1..w - 1 { let mut sum: u8 = 0; - for dx in 0..3u32 { + for dx in 0..3 { sum += input.try_get(col + dx - 1, row - 1).unwrap_or(false) as u8 + input.try_get(col + dx - 1, row).unwrap_or(false) as u8 + input.try_get(col + dx - 1, row + 1).unwrap_or(false) as u8; diff --git a/rxing-reader/src/common/bit_matrix.rs b/rxing-reader/src/common/bit_matrix.rs index de65bf3..feaeae4 100644 --- a/rxing-reader/src/common/bit_matrix.rs +++ b/rxing-reader/src/common/bit_matrix.rs @@ -16,7 +16,7 @@ use std::fmt; -use crate::{Error, Point, point, point_i}; +use crate::{Error, Point, point}; use anyhow::Result; use super::BitArray; @@ -37,8 +37,8 @@ const BASE_SHIFT: usize = super::BIT_FIELD_SHIFT_BITS; /// meaning they represent lower x values. This is compatible with BitArray's implementation. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct BitMatrix { - width: u32, - height: u32, + width: usize, + height: usize, row_size: usize, bits: Vec, } @@ -47,7 +47,7 @@ impl BitMatrix { /// Creates an empty square `BitMatrix`. /// /// - `dimension`: height and width - pub fn with_single_dimension(dimension: u32) -> Result { + pub fn with_single_dimension(dimension: usize) -> Result { Self::new(dimension, dimension) } @@ -55,7 +55,7 @@ impl BitMatrix { /// /// - `width`: bit matrix width /// - `height`: bit matrix height - pub fn new(width: u32, height: u32) -> Result { + pub fn new(width: usize, height: usize) -> Result { if width < 1 || height < 1 { return Err(Error::InvalidArgument { message: format!( @@ -67,8 +67,8 @@ impl BitMatrix { Ok(Self { width, height, - row_size: (width as usize).div_ceil(BASE_BITS), - bits: vec![0; (width as usize).div_ceil(BASE_BITS) * height as usize], + row_size: width.div_ceil(BASE_BITS), + bits: vec![0; width.div_ceil(BASE_BITS) * height], }) } @@ -84,8 +84,8 @@ impl BitMatrix { #[allow(dead_code)] const fn with_all_data( &self, - width: u32, - height: u32, + width: usize, + height: usize, row_size: usize, bits: Vec, ) -> Self { @@ -106,12 +106,8 @@ impl BitMatrix { let Some(first_row) = image.first() else { return Self::empty(); }; - let Ok(height) = image.len().try_into() else { - return Self::empty(); - }; - let Ok(width) = first_row.len().try_into() else { - return Self::empty(); - }; + let height = image.len(); + let width = first_row.len(); if width == 0 || height == 0 { return Self::empty(); } @@ -119,10 +115,10 @@ impl BitMatrix { let Ok(mut bits) = BitMatrix::new(width, height) else { return Self::empty(); }; - for (i, image_i) in image.iter().enumerate().take(height as usize) { - for (j, image_i_j) in image_i.iter().enumerate().take(width as usize) { + for (i, image_i) in image.iter().enumerate().take(height) { + for (j, image_i_j) in image_i.iter().enumerate().take(width) { if *image_i_j { - bits.set(j as u32, i as u32); + bits.set(j, i); } } } @@ -173,7 +169,7 @@ impl BitMatrix { return Err(Error::InvalidArgument { message: format!( "illegal character encountered: {}", - string_representation[pos..].to_owned() + &string_representation[pos..] ).into(), } .into()); @@ -196,10 +192,10 @@ impl BitMatrix { n_rows += 1; } - let mut matrix = BitMatrix::new(row_length as u32, n_rows)?; + let mut matrix = BitMatrix::new(row_length, n_rows)?; for (i, bit) in bits.iter().enumerate().take(bits_pos) { if *bit { - matrix.set((i % row_length) as u32, (i / row_length) as u32); + matrix.set(i % row_length, i / row_length); } } Ok(matrix) @@ -211,17 +207,17 @@ impl BitMatrix { /// y The vertical component (i.e. which row) /// returns the value of given bit in matrix, or false if the requested point is out of bounds of the image #[inline(always)] - pub fn get(&self, x: u32, y: u32) -> bool { - let offset = self.offset(y, x); - if offset >= self.bits.len() { + pub fn get(&self, x: usize, y: usize) -> bool { + if x >= self.width || y >= self.height { return false; } - ((self.bits[offset] >> (x as usize & BASE_SHIFT)) & 1) != 0 + let offset = self.offset(y, x); + ((self.bits[offset] >> (x & BASE_SHIFT)) & 1) != 0 } #[inline(always)] pub fn at_point(&self, point: Point) -> bool { - self.get(point.x as u32, point.y as u32) + self.try_at_point(point).unwrap_or(false) } #[inline(always)] @@ -231,58 +227,59 @@ impl BitMatrix { #[inline(always)] fn calculate_point_from_index(&self, index: usize) -> Point { - let row = index / (self.width() as usize); - let column = index % (self.width() as usize); - point_i(column as u32, row as u32) + let row = index / self.width(); + let column = index % self.width(); + point(column as f32, row as f32) } #[inline(always)] - fn offset(&self, y: u32, x: u32) -> usize { - y as usize * self.row_size + (x as usize / BASE_BITS) + fn offset(&self, y: usize, x: usize) -> usize { + y * self.row_size + (x / BASE_BITS) } - pub fn try_get(&self, x: u32, y: u32) -> Option { - let offset = self.offset(y, x); - if offset >= self.bits.len() { + pub fn try_get(&self, x: usize, y: usize) -> Option { + if x >= self.width || y >= self.height { return None; } - Some(((self.bits[offset] >> (x as usize & BASE_SHIFT)) & 1) != 0) + let offset = self.offset(y, x); + Some(((self.bits[offset] >> (x & BASE_SHIFT)) & 1) != 0) } #[inline(always)] pub fn try_at_point(&self, point: Point) -> Option { - self.try_get(point.x as u32, point.y as u32) + if !point.x.is_finite() || !point.y.is_finite() || point.x < 0.0 || point.y < 0.0 { + return None; + } + self.try_get(point.x as usize, point.y as usize) } - pub fn try_area_majority(&self, x: u32, y: u32, box_size: u32) -> Option { - let mut matrix = Vec::with_capacity((box_size * box_size) as usize); - let start_x = (x as i32 - box_size as i32 / 2).max(0) as u32; - let end_x = x + box_size / 2; - let start_y = (y as i32 - box_size as i32 / 2).max(0) as u32; - let end_y = y + box_size / 2; + pub fn try_area_majority(&self, x: usize, y: usize, box_size: usize) -> Option { + let radius = box_size / 2; + let start_x = x.saturating_sub(radius); + let end_x = x.checked_add(radius)?; + let start_y = y.saturating_sub(radius); + let end_y = y.checked_add(radius)?; + let mut total = 0usize; + let mut total_set = 0usize; for get_x in start_x..=end_x { for get_y in start_y..=end_y { - matrix.push(self.try_get(get_x, get_y)?); + total += 1; + total_set += usize::from(self.try_get(get_x, get_y)?); } } - let total_set = matrix.iter().filter(|bit| **bit).count(); - if (total_set as f32 / matrix.len() as f32) >= 0.5 { - Some(true) - } else { - Some(false) - } + Some(total_set * 2 >= total) } #[inline(always)] - pub fn check_in_bounds(&self, x: u32, y: u32) -> bool { - self.offset(y, x) < self.bits.len() + pub fn check_in_bounds(&self, x: usize, y: usize) -> bool { + x < self.width && y < self.height } #[inline(always)] pub fn check_point_in_bounds(&self, point: Point) -> bool { - self.check_in_bounds(point.x as u32, point.y as u32) + self.try_at_point(point).is_some() } /// Sets the given bit to true. @@ -290,13 +287,13 @@ impl BitMatrix { /// - `x`: The horizontal component (i.e. which column) /// - `y`: The vertical component (i.e. which row) #[inline(always)] - pub fn set(&mut self, x: u32, y: u32) { + pub fn set(&mut self, x: usize, y: usize) { let offset = self.offset(y, x); - self.bits[offset] |= 1 << (x as usize & BASE_SHIFT); + self.bits[offset] |= 1 << (x & BASE_SHIFT); } #[inline(always)] - pub fn set_bool(&mut self, x: u32, y: u32, value: bool) { + pub fn set_bool(&mut self, x: usize, y: usize, value: bool) { if value { self.set(x, y) } else { @@ -305,9 +302,9 @@ impl BitMatrix { } #[inline(always)] - pub fn unset(&mut self, x: u32, y: u32) { + pub fn unset(&mut self, x: usize, y: usize) { let offset = self.offset(y, x); - self.bits[offset] &= !(1 << (x as usize & BASE_SHIFT)); + self.bits[offset] &= !(1 << (x & BASE_SHIFT)); } /// Flips the given bit. @@ -315,9 +312,9 @@ impl BitMatrix { /// - `x`: The horizontal component (i.e. which column) /// - `y`: The vertical component (i.e. which row) #[inline(always)] - pub fn flip_coords(&mut self, x: u32, y: u32) { + pub fn flip_coords(&mut self, x: usize, y: usize) { let offset = self.offset(y, x); - self.bits[offset] ^= 1 << (x as usize & BASE_SHIFT); + self.bits[offset] ^= 1 << (x & BASE_SHIFT); } /// Flips every bit in the matrix. @@ -345,7 +342,7 @@ impl BitMatrix { .into()); } for y in 0..self.height { - let offset = y as usize * self.row_size; + let offset = y * self.row_size; let row_array = mask.row(y); let row = row_array.words(); for (x, row_x) in row.iter().enumerate().take(self.row_size) { @@ -367,7 +364,7 @@ impl BitMatrix { /// - `top`: The vertical position to begin at (inclusive) /// - `width`: The width of the region /// - `height`: The height of the region - pub fn set_region(&mut self, left: u32, top: u32, width: u32, height: u32) -> Result<()> { + pub fn set_region(&mut self, left: usize, top: usize, width: usize, height: usize) -> Result<()> { if height < 1 || width < 1 { return Err(Error::InvalidArgument { message: format!( @@ -376,8 +373,16 @@ impl BitMatrix { } .into()); } - let right = left + width; - let bottom = top + height; + let right = left.checked_add(width).ok_or_else(|| Error::InvalidArgument { + message: format!( + "set_region: left + width overflowed (left={left}, width={width})" + ).into(), + })?; + let bottom = top.checked_add(height).ok_or_else(|| Error::InvalidArgument { + message: format!( + "set_region: top + height overflowed (top={top}, height={height})" + ).into(), + })?; if bottom > self.height || right > self.width { return Err(Error::InvalidArgument { message: format!( @@ -388,9 +393,9 @@ impl BitMatrix { .into()); } for y in top..bottom { - let offset = y as usize * self.row_size; + let offset = y * self.row_size; for x in left..right { - self.bits[offset + (x as usize / BASE_BITS)] |= 1 << (x as usize & BASE_SHIFT); + self.bits[offset + (x / BASE_BITS)] |= 1 << (x & BASE_SHIFT); } } Ok(()) @@ -401,10 +406,10 @@ impl BitMatrix { /// - `y`: The row to retrieve /// /// Returns the requested row as a new [`BitArray`]. - pub fn row(&self, y: u32) -> BitArray { - let mut rw = BitArray::with_size(self.width as usize); + pub fn row(&self, y: usize) -> BitArray { + let mut rw = BitArray::with_size(self.width); - let offset = y as usize * self.row_size; + let offset = y * self.row_size; for x in 0..self.row_size { rw.set_bulk(x * BASE_BITS, self.bits[offset + x]); } @@ -414,12 +419,12 @@ impl BitMatrix { /// Returns a column of the bit matrix. /// /// The current implementation may be very slow. - pub fn column(&self, x: u32) -> BitArray { - let mut cw = BitArray::with_size(self.height as usize); + pub fn column(&self, x: usize) -> BitArray { + let mut cw = BitArray::with_size(self.height); for y in 0..self.height { if self.get(x, y) { - cw.set(y as usize) + cw.set(y) } } @@ -428,8 +433,8 @@ impl BitMatrix { /// - `y`: row to set /// - `row`: [`BitArray`] to copy from - pub fn set_row(&mut self, y: u32, row: &BitArray) { - self.bits[y as usize * self.row_size..y as usize * self.row_size + self.row_size] + pub fn set_row(&mut self, y: usize, row: &BitArray) { + self.bits[y * self.row_size..y * self.row_size + self.row_size] .clone_from_slice(&row.words()[0..self.row_size]) } @@ -479,48 +484,47 @@ impl BitMatrix { pub fn rotate90(&mut self) { let new_width = self.height; let new_height = self.width; - let new_row_size = new_width.div_ceil(BASE_BITS as u32); - let mut new_bits = vec![0; (new_row_size * new_height) as usize]; + let new_row_size = new_width.div_ceil(BASE_BITS); + let mut new_bits = vec![0; new_row_size * new_height]; for y in 0..self.height { for x in 0..self.width { let offset = self.offset(y, x); - if ((self.bits[offset] >> (x as usize & BASE_SHIFT)) & 1) != 0 { - let new_offset: usize = - ((new_height - 1 - x) * new_row_size + (y / BASE_BITS as u32)) as usize; - new_bits[new_offset] |= 1 << (y as usize & BASE_SHIFT); + if ((self.bits[offset] >> (x & BASE_SHIFT)) & 1) != 0 { + let new_offset = (new_height - 1 - x) * new_row_size + (y / BASE_BITS); + new_bits[new_offset] |= 1 << (y & BASE_SHIFT); } } } self.width = new_width; self.height = new_height; - self.row_size = new_row_size as usize; + self.row_size = new_row_size; self.bits = new_bits; } - /// This is useful in detecting the enclosing rectangle of a 'pure' barcode. + /// This is useful in detecting the enclosing rectangle of a pure QR symbol. /// /// Returns the `left, top, width, height` rectangle enclosing all set bits. /// /// Returns `None` when the matrix is all white. - pub fn enclosing_rectangle(&self) -> Option<[u32; 4]> { + pub fn enclosing_rectangle(&self) -> Option<[usize; 4]> { let mut left = self.width; let mut top = self.height; - let mut right: u32 = 0; + let mut right = 0; let mut bottom = 0; for y in 0..self.height { for x32 in 0..self.row_size { - let the_bits = self.bits[y as usize * self.row_size + x32]; + let the_bits = self.bits[y * self.row_size + x32]; if the_bits != 0 { top = top.min(y); bottom = bottom.max(y); let bit_lo: usize = the_bits.trailing_zeros() as usize; - left = left.min(((x32 * BASE_BITS) + bit_lo) as u32); + left = left.min((x32 * BASE_BITS) + bit_lo); let bit_hi: usize = (BASE_BITS - 1) - (the_bits.leading_zeros() as usize); - right = right.max(((x32 * BASE_BITS) + bit_hi) as u32); + right = right.max((x32 * BASE_BITS) + bit_hi); } } } @@ -532,19 +536,13 @@ impl BitMatrix { Some([left, top, right - left + 1, bottom - top + 1]) } - /// This is useful in detecting a corner of a 'pure' barcode. + /// This is useful in detecting a corner of a pure QR symbol. /// /// Returns the coordinate of the top-left-most set bit. /// /// Returns `None` when the matrix is all white. pub fn top_left_on_bit(&self) -> Option { - let mut bits_offset = 0; - while bits_offset < self.bits.len() && self.bits[bits_offset] == 0 { - bits_offset += 1; - } - if bits_offset == self.bits.len() { - return None; - } + let bits_offset = self.bits.iter().position(|&bits| bits != 0)?; let y = bits_offset / self.row_size; let mut x = (bits_offset % self.row_size) * BASE_BITS; @@ -558,18 +556,11 @@ impl BitMatrix { } pub fn bottom_right_on_bit(&self) -> Option { - let mut bits_offset = self.bits.len() as i64 - 1; - while bits_offset >= 0 && self.bits[bits_offset as usize] == 0 { - bits_offset -= 1; - } - if bits_offset < 0 { - return None; - } - - let y = bits_offset as usize / self.row_size; - let mut x = (bits_offset as usize % self.row_size) * BASE_BITS; + let bits_offset = self.bits.iter().rposition(|&bits| bits != 0)?; + let y = bits_offset / self.row_size; + let mut x = (bits_offset % self.row_size) * BASE_BITS; - let the_bits = self.bits[bits_offset as usize]; + let the_bits = self.bits[bits_offset]; let mut bit = BASE_BITS - 1; while (the_bits >> bit) == 0 { bit -= 1; @@ -580,12 +571,12 @@ impl BitMatrix { } #[inline(always)] - pub const fn width(&self) -> u32 { + pub const fn width(&self) -> usize { self.width } #[inline(always)] - pub const fn height(&self) -> u32 { + pub const fn height(&self) -> usize { self.height } @@ -614,7 +605,7 @@ impl BitMatrix { line_separator: &str, ) -> String { let mut result = String::with_capacity( - (self.height as usize).saturating_mul((self.width as usize).saturating_add(1)), + self.height.saturating_mul(self.width.saturating_add(1)), ); for y in 0..self.height { for x in 0..self.width { @@ -638,8 +629,8 @@ impl BitMatrix { } .into()); } - if left.saturating_add(width) > self.width as usize - || top.saturating_add(height) > self.height as usize + if left.saturating_add(width) > self.width + || top.saturating_add(height) > self.height { return Err(Error::InvalidArgument { message: format!( @@ -649,13 +640,11 @@ impl BitMatrix { } .into()); } - let mut new_bm = BitMatrix::new(width as u32, height as u32)?; + let mut new_bm = BitMatrix::new(width, height)?; for y in top..top + height { for x in left..left + width { - if self.get(x as u32, y as u32) { - let nx = (x - left) as u32; - let ny = (y - top) as u32; - new_bm.set(nx, ny) + if self.get(x, y) { + new_bm.set(x - left, y - top) } } } @@ -692,10 +681,10 @@ impl TryFrom<&str> for BitMatrix { impl From<&BitMatrix> for Vec { fn from(value: &BitMatrix) -> Self { - let mut arr = vec![false; (value.width * value.height) as usize]; + let mut arr = vec![false; value.width * value.height]; for x in 0..value.width { for y in 0..value.height { - let insert_pos = ((y * value.width) + x) as usize; + let insert_pos = (y * value.width) + x; arr[insert_pos] = value.get(x, y); } } diff --git a/rxing-reader/src/common/cpp_essentials/base_extensions/bitmatrix.rs b/rxing-reader/src/common/cpp_essentials/base_extensions/bitmatrix.rs deleted file mode 100644 index 3c6f46f..0000000 --- a/rxing-reader/src/common/cpp_essentials/base_extensions/bitmatrix.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::Point; -use crate::common::BitMatrix; -use crate::point; -use anyhow::Result; - -impl BitMatrix { - pub fn deflate( - &self, - width: u32, - height: u32, - top: f32, - left: f32, - sub_sampling: f32, - ) -> Result { - let mut result = BitMatrix::new(width, height)?; - - for y in 0..result.height() { - let y_offset = top + y as f32 * sub_sampling; - for x in 0..result.width() { - let sample_x = left + x as f32 * sub_sampling; - if sample_x >= 0.0 - && sample_x < self.width() as f32 - && y_offset >= 0.0 - && y_offset < self.height() as f32 - && self.at_point(point(sample_x, y_offset)) - { - result.set(x, y); - } - } - } - - Ok(result) - } - - pub fn find_bounding_box( - &self, - left: u32, - top: u32, - width: u32, - height: u32, - min_size: u32, - ) -> (bool, u32, u32, u32, u32) { - let mut left = left; - let mut top = top; - let mut width = width; - let mut height = height; - - let (Some(Point { x: left_on_bit, y: top_on_bit }), Some(Point { x: right_on_bit, y: bottom_on_bit })) = - (self.top_left_on_bit(), self.bottom_right_on_bit()) - else { - return (false, left, top, width, height); - }; - left = left_on_bit as u32; - top = top_on_bit as u32; - let mut right = right_on_bit as u32; - let bottom = bottom_on_bit as u32; - if bottom - top + 1 < min_size || right - left + 1 < min_size { - return (false, left, top, width, height); - } - - for y in top..=bottom { - for x in 0..left { - if self.get(x, y) { - left = x; - break; - } - } - for x in ((right + 1)..self.width()).rev() { - if self.get(x, y) { - right = x; - break; - } - } - } - - width = right - left + 1; - height = bottom - top + 1; - - ( - width >= min_size && height >= min_size, - left, - top, - width, - height, - ) - } -} diff --git a/rxing-reader/src/common/cpp_essentials/base_extensions/mod.rs b/rxing-reader/src/common/cpp_essentials/base_extensions/mod.rs deleted file mode 100644 index 5c34e18..0000000 --- a/rxing-reader/src/common/cpp_essentials/base_extensions/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod bitmatrix; -mod qr_ec_level; -mod qr_formatinformation; -mod qrcode_version; diff --git a/rxing-reader/src/common/cpp_essentials/base_extensions/qr_ec_level.rs b/rxing-reader/src/common/cpp_essentials/base_extensions/qr_ec_level.rs deleted file mode 100644 index 4baf571..0000000 --- a/rxing-reader/src/common/cpp_essentials/base_extensions/qr_ec_level.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::qrcode::ErrorCorrectionLevel; - -impl ErrorCorrectionLevel { - pub fn eclevel_from_bits_signed(bits: i8) -> Self { - let level_for_bits: [ErrorCorrectionLevel; 4] = [ - ErrorCorrectionLevel::M, - ErrorCorrectionLevel::L, - ErrorCorrectionLevel::H, - ErrorCorrectionLevel::Q, - ]; - level_for_bits[(bits as u8 & 0x03) as usize] - } - - pub fn eclevel_from_bits(bits: u8) -> Self { - Self::eclevel_from_bits_signed(bits as i8) - } -} diff --git a/rxing-reader/src/common/cpp_essentials/base_extensions/qr_formatinformation.rs b/rxing-reader/src/common/cpp_essentials/base_extensions/qr_formatinformation.rs deleted file mode 100644 index 4909680..0000000 --- a/rxing-reader/src/common/cpp_essentials/base_extensions/qr_formatinformation.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::qrcode::{ - ErrorCorrectionLevel, FORMAT_INFO_MASK_MODEL2, FORMAT_INFO_MASK_QR, FormatInformation, -}; - -impl FormatInformation { - /// - `format_info_bits1`: format info indicator, with mask still applied - /// - `format_info_bits2`: second copy of same info; both are checked at the same time to establish best match - pub fn decode_qr(format_info_bits1: u32, format_info_bits2: u32) -> Self { - // maks out the 'Dark Module' for mirrored and non-mirrored case (see Figure 25 in ISO/IEC 18004:2015) - let mirrored_format_info_bits2 = Self::mirror_bits( - ((format_info_bits2 >> 1) & 0b111111110000000) | (format_info_bits2 & 0b1111111), - ); - let format_info_bits2 = - ((format_info_bits2 >> 1) & 0b111111100000000) | (format_info_bits2 & 0b11111111); - // Some QR codes do not apply the XOR mask. Try with standard masking and without it. - let mut format_info = Self::find_best_format_info( - &[FORMAT_INFO_MASK_QR, 0], - &[ - format_info_bits1, - format_info_bits2, - Self::mirror_bits(format_info_bits1), - mirrored_format_info_bits2, - ], - ); - - // Use bits 3/4 for error correction, and 0-2 for mask. - format_info.error_correction_level = - ErrorCorrectionLevel::eclevel_from_bits((format_info.data >> 3) as u8 & 0x03); - format_info.data_mask = format_info.data as u8 & 0x07; - format_info.is_mirrored = format_info.bits_index > 1; - - format_info - } - - #[inline(always)] - pub fn mirror_bits(bits: u32) -> u32 { - (bits.reverse_bits()) >> 17 - } - - pub fn find_best_format_info(masks: &[u32], bits: &[u32]) -> Self { - let mut fi = FormatInformation::default(); - - // See ISO 18004:2015, Annex C, Table C.1 - const MODEL2_MASKED_PATTERNS: [u32; 32] = [ - 0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0, 0x77C4, 0x72F3, 0x7DAA, - 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976, 0x1689, 0x13BE, 0x1CE7, 0x19D0, 0x0762, 0x0255, - 0x0D0C, 0x083B, 0x355F, 0x3068, 0x3F31, 0x3A06, 0x24B4, 0x2183, 0x2EDA, 0x2BED, - ]; - - for mask in masks { - for (bits_index, bits_item) in bits.iter().enumerate() { - for ref_pattern in MODEL2_MASKED_PATTERNS { - // 'unmask' the pattern first to get the original 5-data bits + 10-ec bits back - let pattern = ref_pattern ^ FORMAT_INFO_MASK_MODEL2; - // Find the pattern with fewest bits differing - let hamming_dist = ((bits_item ^ mask) ^ pattern).count_ones(); - if hamming_dist < fi.hamming_distance { - fi.mask = *mask; // store the used mask to discriminate between types/models - fi.data = pattern >> 10; // drop the 10 BCH error correction bits - fi.hamming_distance = hamming_dist; - fi.bits_index = bits_index as u8; - } - } - } - } - - fi - } -} diff --git a/rxing-reader/src/common/cpp_essentials/base_extensions/qrcode_version.rs b/rxing-reader/src/common/cpp_essentials/base_extensions/qrcode_version.rs deleted file mode 100644 index e2d6c11..0000000 --- a/rxing-reader/src/common/cpp_essentials/base_extensions/qrcode_version.rs +++ /dev/null @@ -1,94 +0,0 @@ -/* -* Copyright 2016 Nu-book Inc. -* Copyright 2016 ZXing authors -* Copyright 2023 Axel Waggershauser -*/ -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::Result; - -use crate::common::BitMatrix; -use crate::qrcode::{VERSION_DECODE_INFO, VERSIONS, Version, VersionRef}; -use crate::{Error, PointI, point}; - -impl Version { - pub fn model2(version_number: u32) -> Result { - if !(1..=40).contains(&version_number) { - Err(Error::InvalidArgument { - message: format!( - "Version::model2: version_number {version_number} out of range (expected 1..=40)" - ).into(), - } - .into()) - } else { - Ok(&VERSIONS[version_number as usize - 1]) - } - } - - pub const fn dimension_of_version(version: u32) -> u32 { - 17 + 4 * version - } - - pub fn decode_version_information_pair( - version_bits_a: i32, - version_bits_b: i32, - ) -> Result { - let mut best_difference = u32::MAX; - let mut best_version = 0; - for (i, target_version) in VERSION_DECODE_INFO.into_iter().enumerate() { - for bits in [version_bits_a, version_bits_b] { - let bits_difference = ((bits as u32) ^ target_version).count_ones(); - if bits_difference < best_difference { - best_version = i + 7; - best_difference = bits_difference; - } - } - if best_difference == 0 { - break; - } - } - // We can tolerate up to 3 bits of error since no two version info codewords will - // differ in less than 8 bits. - if best_difference <= 3 { - return Self::for_number(best_version as u32); - } - // If we didn't find a close enough match, fail - Err(Error::InvalidState { - message: "required internal state is missing".into(), - } - .into()) - } - - pub fn symbol_size(version: u32) -> PointI { - let version = version as i32; - - let square = |s: i32| point(s, s); - let valid = |v: i32, max: i32| v >= 1 && v <= max; - - if valid(version, 40) { - square(17 + 4 * version) - } else { - PointI::default() - } - } - - pub fn is_valid_size(size: PointI) -> bool { - size.x == size.y && size.x >= 21 && size.x <= 177 && (size.x % 4 == 1) - } - - pub fn has_valid_size(matrix: &BitMatrix) -> bool { - Self::is_valid_size(point(matrix.width() as i32, matrix.height() as i32)) - } - - pub fn number_from_size(size: PointI) -> u32 { - if Self::is_valid_size(size) { - ((size.x - 17) / 4) as u32 - } else { - 0 - } - } - - pub fn number_from_matrix(bit_matrix: &BitMatrix) -> u32 { - Self::number_from_size(point(bit_matrix.width() as i32, bit_matrix.height() as i32)) - } -} diff --git a/rxing-reader/src/common/cpp_essentials/direction.rs b/rxing-reader/src/common/cpp_essentials/direction.rs deleted file mode 100644 index 15fb6ac..0000000 --- a/rxing-reader/src/common/cpp_essentials/direction.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum Direction { - Left = -1, - Right = 1, -} - -impl From for i32 { - fn from(value: Direction) -> Self { - match value { - Direction::Left => -1, - Direction::Right => 1, - } - } -} diff --git a/rxing-reader/src/common/cpp_essentials/dm_regression_line.rs b/rxing-reader/src/common/cpp_essentials/dm_regression_line.rs deleted file mode 100644 index 75e9b42..0000000 --- a/rxing-reader/src/common/cpp_essentials/dm_regression_line.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::{Error, Point}; -use anyhow::Result; - -use super::RegressionLineTrait; - -#[derive(Clone)] -pub struct DMRegressionLine { - points: Vec, - direction_inward: Point, - pub(super) a: f32, - pub(super) b: f32, - pub(super) c: f32, -} - -impl Default for DMRegressionLine { - fn default() -> Self { - Self { - points: Default::default(), - direction_inward: Default::default(), - a: f32::NAN, - b: f32::NAN, - c: f32::NAN, - } - } -} - -impl RegressionLineTrait for DMRegressionLine { - fn is_valid(&self) -> bool { - !self.a.is_nan() - } - - fn normal(&self) -> Point { - if self.is_valid() { - Point { - x: self.a, - y: self.b, - } - } else { - self.direction_inward - } - } - - fn signed_distance(&self, p: Point) -> f32 { - Point::dot(self.normal(), p) - self.c - } - - fn distance_single(&self, p: Point) -> f32 { - (self.signed_distance(p)).abs() - } - - fn add(&mut self, p: Point) -> Result<()> { - if self.direction_inward == Point::default() { - return Err(Error::InvalidState { - message: "required internal state is missing".into(), - } - .into()); - } - self.points.push(p); - if self.points.len() == 1 { - self.c = Point::dot(self.normal(), p); - } - Ok(()) - } - - fn set_direction_inward(&mut self, d: Point) { - self.direction_inward = Point::normalized(d); - } - - fn evaluate_max_distance_with(&mut self, max_signed_dist: f64, update_points: bool) -> bool { - let mut ret = self.evaluate_self(); - if max_signed_dist > 0.0 { - let mut points = Vec::with_capacity(self.points.len()); - points.extend(self.points.iter().copied()); - loop { - let old_points_size = points.len(); - // remove points that are further 'inside' than maxSignedDist or further 'outside' than 2 x maxSignedDist - points.retain(|&p| { - let sd = self.signed_distance(p) as f64; - !(sd > max_signed_dist || sd < -2.0 * max_signed_dist) - }); - if old_points_size == points.len() { - break; - } - ret = self.evaluate(&points); - } - - if update_points { - self.points = points; - } - } - ret - } - - fn is_high_res(&self) -> bool { - let Some(first) = self.points.first().copied() else { - return false; - }; - let mut min = first; - let mut max = first; - for p in &self.points { - min.x = f32::min(min.x, p.x); - min.y = f32::min(min.y, p.y); - max.x = f32::max(max.x, p.x); - max.y = f32::max(max.y, p.y); - } - let diff = max - min; - let len = diff.max_abs_component(); - let steps = f32::min(diff.x.abs(), diff.y.abs()); - // due to aliasing we get bad extrapolations if the line is short and too close to vertical/horizontal - steps > 2.0 || len > 50.0 - } - - fn evaluate(&mut self, points: &[Point]) -> bool { - if points.is_empty() { - return false; - } - let mean = points.iter().sum::() / points.len() as f32; - - let mut sum_xx = 0.0; - let mut sum_yy = 0.0; - let mut sum_xy = 0.0; - for p in points { - let d = *p - mean; - sum_xx += d.x * d.x; - sum_yy += d.y * d.y; - sum_xy += d.x * d.y; - } - if sum_yy >= sum_xx { - let l = (sum_yy * sum_yy + sum_xy * sum_xy).sqrt(); - self.a = sum_yy / l; - self.b = -sum_xy / l; - } else { - let l = (sum_xx * sum_xx + sum_xy * sum_xy).sqrt(); - self.a = sum_xy / l; - self.b = -sum_xx / l; - } - if Point::dot(self.direction_inward, self.normal()) < 0.0 { - self.a = -self.a; - self.b = -self.b; - } - self.c = Point::dot(self.normal(), mean); - // angle between original and new direction is at most 60 degree - Point::dot(self.direction_inward, self.normal()) > 0.5 - } - - fn evaluate_self(&mut self) -> bool { - let points = std::mem::take(&mut self.points); - let result = self.evaluate(&points); - self.points = points; - result - } - - fn a(&self) -> f32 { - self.a - } - - fn b(&self) -> f32 { - self.b - } - - fn c(&self) -> f32 { - self.c - } -} - -impl DMRegressionLine { - pub fn new(point_1: Point, point_2: Point) -> Self { - let mut new = Self::default(); - RegressionLineTrait::evaluate(&mut new, &[point_1, point_2]); - new - } -} diff --git a/rxing-reader/src/common/cpp_essentials/regression_line_trait.rs b/rxing-reader/src/common/cpp_essentials/regression_line_trait.rs deleted file mode 100644 index fccd309..0000000 --- a/rxing-reader/src/common/cpp_essentials/regression_line_trait.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{Point, point}; -use anyhow::Result; - -/// Minimum determinant magnitude for `RegressionLineTrait::intersect` to treat -/// two lines as non-parallel. Line coefficients are normalized by -/// `RegressionLine::evaluate` so the determinant is unitless, but f32 round-off -/// at pixel scale makes `f32::EPSILON` too tight in practice — `1e-6` rejects -/// near-parallel pairs without false negatives on well-conditioned lines. -pub const LINE_INTERSECTION_EPS: f32 = 1e-6; - -pub trait RegressionLineTrait { - fn intersect( - l1: &T, - l2: &T2, - ) -> Option { - if !(l1.is_valid() && l2.is_valid()) { - return None; - } - - let d = l1.a() * l2.b() - l1.b() * l2.a(); - if d.abs() < LINE_INTERSECTION_EPS { - return None; - } - let x = (l1.c() * l2.b() - l1.b() * l2.c()) / d; - let y = (l1.a() * l2.c() - l1.c() * l2.a()) / d; - - Some(point(x, y)) - } - - fn evaluate(&mut self, points: &[Point]) -> bool; - fn evaluate_self(&mut self) -> bool; - - fn is_valid(&self) -> bool; - fn normal(&self) -> Point; - fn signed_distance(&self, p: Point) -> f32; - fn distance_single(&self, p: Point) -> f32; - - fn add(&mut self, p: Point) -> Result<()>; - - fn set_direction_inward(&mut self, d: Point); - - fn evaluate_max_distance_with(&mut self, max_signed_dist: f64, update_points: bool) -> bool; - - fn is_high_res(&self) -> bool; - fn a(&self) -> f32; - fn b(&self) -> f32; - fn c(&self) -> f32; -} diff --git a/rxing-reader/src/common/default_grid_sampler.rs b/rxing-reader/src/common/default_grid_sampler.rs index f96b726..38931f9 100644 --- a/rxing-reader/src/common/default_grid_sampler.rs +++ b/rxing-reader/src/common/default_grid_sampler.rs @@ -26,13 +26,13 @@ impl GridSampler for DefaultGridSampler { fn sample_grid( &self, image: &BitMatrix, - dimension_x: u32, - dimension_y: u32, + dimension_x: usize, + dimension_y: usize, controls: &[SamplerControl], ) -> Result { if dimension_x == 0 || dimension_y == 0 { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } @@ -51,7 +51,7 @@ impl GridSampler for DefaultGridSampler { || !is_inside(p0.x, p1.y - 1.0) { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } @@ -59,12 +59,12 @@ impl GridSampler for DefaultGridSampler { let mut bits = BitMatrix::new(dimension_x, dimension_y)?; for SamplerControl { p0, p1, transform } in controls { - for y in (p0.y as i32)..(p1.y as i32) { - for x in (p0.x as i32)..(p1.x as i32) { + for y in (p0.y as usize)..(p1.y as usize) { + for x in (p0.x as usize)..(p1.x as usize) { let p = transform - .transform_point(Point::from((x, y)).centered()) + .transform_point(point(x as f32, y as f32).centered()) .ok_or(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), })?; // Due to a "numerical instability" in the PerspectiveTransform generation/application it has been observed @@ -74,13 +74,13 @@ impl GridSampler for DefaultGridSampler { // TODO: Check some mathematical/numercial property of mod2_pix to determine if it is a perspective transforation. if !image.is_in(p) { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } if image.at_point(p) { - bits.set(x as u32, y as u32); + bits.set(x, y); } } } diff --git a/rxing-reader/src/common/cpp_essentials/bitmatrix_cursor_trait.rs b/rxing-reader/src/common/detect/bitmatrix_cursor_trait.rs similarity index 100% rename from rxing-reader/src/common/cpp_essentials/bitmatrix_cursor_trait.rs rename to rxing-reader/src/common/detect/bitmatrix_cursor_trait.rs diff --git a/rxing-reader/src/common/cpp_essentials/concentric_finder.rs b/rxing-reader/src/common/detect/concentric_finder.rs similarity index 98% rename from rxing-reader/src/common/cpp_essentials/concentric_finder.rs rename to rxing-reader/src/common/detect/concentric_finder.rs index 869e751..23b8635 100644 --- a/rxing-reader/src/common/cpp_essentials/concentric_finder.rs +++ b/rxing-reader/src/common/detect/concentric_finder.rs @@ -2,7 +2,7 @@ use crate::{ Point, common::{ BitMatrix, Quadrilateral, - cpp_essentials::{ + detect::{ Direction, FixedPattern, PatternRow, PatternType, PatternView, is_pattern, }, }, @@ -11,7 +11,7 @@ use crate::{ use super::{ BitMatrixCursorTrait, EdgeTracer, FastEdgeToEdgeCounter, Pattern, RegressionLine, - RegressionLineTrait, update_min_max, update_min_max_float, + RegressionLineTrait, intersect, update_min_max, update_min_max_float, }; pub fn read_symmetric_pattern( @@ -211,7 +211,7 @@ pub fn center_of_ring( return None; } - if !(cur.p != start) { + if cur.p == start { break; } } @@ -297,7 +297,7 @@ pub fn collect_ring_points( return Vec::default(); } - if !(cur.p != start) { + if cur.p == start { break; } } @@ -409,7 +409,7 @@ pub fn fit_quadrilateral_to_points(center: Point, points: &mut [Point]) -> Optio let mut res = Quadrilateral::default(); for i in 0..4 { - res[i] = RegressionLine::intersect(&lines[i], &lines[(i + 1) % 4])?; + res[i] = intersect(&lines[i], &lines[(i + 1) % 4])?; } Some(res) diff --git a/rxing-reader/src/common/cpp_essentials/decoder_result.rs b/rxing-reader/src/common/detect/decoder_result.rs similarity index 89% rename from rxing-reader/src/common/cpp_essentials/decoder_result.rs rename to rxing-reader/src/common/detect/decoder_result.rs index 6431da4..c9b7395 100644 --- a/rxing-reader/src/common/cpp_essentials/decoder_result.rs +++ b/rxing-reader/src/common/detect/decoder_result.rs @@ -20,8 +20,8 @@ impl DecoderResult { self.content.symbology.code != 0 && self.error.is_none() } - pub fn content(&self) -> &ECIStringBuilder { - &self.content + pub fn into_bytes(self) -> Vec { + self.content.into_bytes() } pub fn with_error(mut self, error: Option) -> DecoderResult { diff --git a/rxing-reader/src/common/detect/direction.rs b/rxing-reader/src/common/detect/direction.rs new file mode 100644 index 0000000..16faa5b --- /dev/null +++ b/rxing-reader/src/common/detect/direction.rs @@ -0,0 +1,20 @@ +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum Direction { + Left, + Right, +} + +impl Direction { + pub const fn sign(self) -> i32 { + match self { + Self::Left => -1, + Self::Right => 1, + } + } +} + +impl From for i32 { + fn from(value: Direction) -> Self { + value.sign() + } +} diff --git a/rxing-reader/src/common/cpp_essentials/edge_tracer.rs b/rxing-reader/src/common/detect/edge_tracer.rs similarity index 99% rename from rxing-reader/src/common/cpp_essentials/edge_tracer.rs rename to rxing-reader/src/common/detect/edge_tracer.rs index ee85589..5c09134 100644 --- a/rxing-reader/src/common/cpp_essentials/edge_tracer.rs +++ b/rxing-reader/src/common/detect/edge_tracer.rs @@ -137,7 +137,7 @@ impl BitMatrixCursorTrait for EdgeTracer<'_> { } } -impl<'a> EdgeTracer<'_> { +impl<'a> EdgeTracer<'a> { pub fn new(image: &'a BitMatrix, p: Point, d: Point) -> EdgeTracer<'a> { EdgeTracer { img: image, diff --git a/rxing-reader/src/common/cpp_essentials/fast_edge_to_edge_counter.rs b/rxing-reader/src/common/detect/fast_edge_to_edge_counter.rs similarity index 91% rename from rxing-reader/src/common/cpp_essentials/fast_edge_to_edge_counter.rs rename to rxing-reader/src/common/detect/fast_edge_to_edge_counter.rs index b961bfa..bf0db15 100644 --- a/rxing-reader/src/common/cpp_essentials/fast_edge_to_edge_counter.rs +++ b/rxing-reader/src/common/detect/fast_edge_to_edge_counter.rs @@ -5,10 +5,9 @@ use crate::{Error, common::BitMatrix}; use super::BitMatrixCursorTrait; pub struct FastEdgeToEdgeCounter<'a> { - p: u32, + p: usize, stride: isize, steps_to_border: i32, - _arr: isize, under_array: &'a BitMatrix, } @@ -37,7 +36,7 @@ impl FastEdgeToEdgeCounter<'_> { } .into()); } - let p = p as u32; + let p = p as usize; let max_steps_x: i32 = if cur.d().x != 0.0 { if cur.d().x > 0.0 { @@ -63,7 +62,6 @@ impl FastEdgeToEdgeCounter<'_> { p, stride, steps_to_border, - _arr: cur.p().y as isize * stride, under_array: cur.img(), }) } @@ -81,11 +79,11 @@ impl FastEdgeToEdgeCounter<'_> { } } - let Some(idx_pt) = self.get_array_check_index(steps) else { + let Some(idx_pt) = self.checked_index_for_steps(steps) else { return 0; }; - if self.under_array.at_index(idx_pt) != self.under_array.at_index(self.p as usize) { + if self.under_array.at_index(idx_pt) != self.under_array.at_index(self.p) { break; } } @@ -96,14 +94,14 @@ impl FastEdgeToEdgeCounter<'_> { // for negative stride. Caller never re-reads `self.p` in that terminal case, // but clamping keeps `self.p` in a defined state. let new_pos = self.p as isize + (steps as isize * self.stride); - self.p = new_pos.max(0) as u32; + self.p = new_pos.max(0) as usize; self.steps_to_border -= steps; steps as u32 } #[inline(always)] - fn get_array_check_index(&self, steps: i32) -> Option { + fn checked_index_for_steps(&self, steps: i32) -> Option { let idx = self.p as isize + (steps as isize * self.stride); if idx < 0 { return None; diff --git a/rxing-reader/src/common/cpp_essentials/matrix.rs b/rxing-reader/src/common/detect/matrix.rs similarity index 89% rename from rxing-reader/src/common/cpp_essentials/matrix.rs rename to rxing-reader/src/common/detect/matrix.rs index 12fb19d..7754247 100644 --- a/rxing-reader/src/common/cpp_essentials/matrix.rs +++ b/rxing-reader/src/common/detect/matrix.rs @@ -28,12 +28,9 @@ impl Matrix { pub fn get(&self, x: usize, y: usize) -> Option { if x >= self.width || y >= self.height { - None - } else if let Some(Some(d)) = self.data.get(Self::offset(x, y, self.width)) { - Some(*d) - } else { - None + return None; } + self.data.get(Self::offset(x, y, self.width)).copied().flatten() } pub fn set(&mut self, x: usize, y: usize, value: T) -> Result { diff --git a/rxing-reader/src/common/cpp_essentials/mod.rs b/rxing-reader/src/common/detect/mod.rs similarity index 87% rename from rxing-reader/src/common/cpp_essentials/mod.rs rename to rxing-reader/src/common/detect/mod.rs index a6a7ab4..01cf9ec 100644 --- a/rxing-reader/src/common/cpp_essentials/mod.rs +++ b/rxing-reader/src/common/detect/mod.rs @@ -1,10 +1,7 @@ -mod base_extensions; - pub mod bitmatrix_cursor_trait; pub mod concentric_finder; pub mod decoder_result; pub mod direction; -pub mod dm_regression_line; pub mod edge_tracer; pub mod fast_edge_to_edge_counter; pub mod matrix; @@ -18,7 +15,6 @@ pub use bitmatrix_cursor_trait::*; pub use concentric_finder::*; pub use decoder_result::*; pub use direction::*; -pub use dm_regression_line::*; pub use edge_tracer::*; pub use fast_edge_to_edge_counter::*; pub use matrix::*; diff --git a/rxing-reader/src/common/cpp_essentials/pattern.rs b/rxing-reader/src/common/detect/pattern.rs similarity index 96% rename from rxing-reader/src/common/cpp_essentials/pattern.rs rename to rxing-reader/src/common/detect/pattern.rs index 17adab3..5cc9ba0 100644 --- a/rxing-reader/src/common/cpp_essentials/pattern.rs +++ b/rxing-reader/src/common/detect/pattern.rs @@ -116,7 +116,7 @@ pub struct PatternView<'a> { impl<'a> PatternView<'a> { // A PatternRow always starts with the width of whitespace in front of the - // first black bar (see `get_pattern_row`). `start = 1` skips that + // first black bar (see `fill_pattern_row`). `start = 1` skips that // leading-whitespace element, so `view[0]` is the first bar; `count` // is the number of view elements (bars + spaces, excluding the // leading-whitespace prefix). @@ -132,7 +132,7 @@ impl<'a> PatternView<'a> { /// Construct a view directly over a bar-only buffer (no leading-whitespace /// element at index 0). `view[0]` is `bars[0]`; use this for buffers that /// were built as raw bar-width arrays rather than fed through - /// `get_pattern_row`. + /// `fill_pattern_row`. pub fn from_bars(bars: &'a [PatternType]) -> PatternView<'a> { PatternView { data: bars, @@ -573,8 +573,8 @@ pub fn find_left_guard_by) #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum Color { - White = 0, - Black = 1, + White, + Black, } impl> From for Color { @@ -586,7 +586,7 @@ impl> From for Color { } } -pub fn get_pattern_row_tp(matrix: &BitMatrix, r: u32, pr: &mut PatternRow, transpose: bool) { +pub fn read_pattern_row(matrix: &BitMatrix, r: usize, pr: &mut PatternRow, transpose: bool) { let row = if transpose { matrix.column(r) } else { @@ -595,10 +595,10 @@ pub fn get_pattern_row_tp(matrix: &BitMatrix, r: u32, pr: &mut PatternRow, trans let pixel_states: Vec = row.into(); - get_pattern_row(&pixel_states, pr) + fill_pattern_row(&pixel_states, pr) } -pub fn get_pattern_row + Copy + Default + From>( +pub fn fill_pattern_row + Copy + Default + From>( b_row: &[T], p_row: &mut PatternRow, ) { @@ -635,9 +635,9 @@ pub fn get_pattern_row + Copy + Default + From>( #[cfg(test)] mod tests { - use crate::common::cpp_essentials::PatternType; + use crate::common::detect::PatternType; - use super::{FixedPattern, PatternRow, PatternView, get_pattern_row, is_pattern}; + use super::{FixedPattern, PatternRow, PatternView, fill_pattern_row, is_pattern}; const N: usize = 33; #[test] @@ -645,7 +645,7 @@ mod tests { for s in 1..=N { let t_in: Vec = vec![0; s]; let mut pr = PatternRow::default(); - get_pattern_row(&t_in, &mut pr); + fill_pattern_row(&t_in, &mut pr); assert_eq!(pr.0.len(), 1); assert_eq!(pr.0[0], s as PatternType); @@ -657,7 +657,7 @@ mod tests { for s in 1..=N { let t_in: Vec = vec![0xff; s]; let mut pr = PatternRow::default(); - get_pattern_row(&t_in, &mut pr); + fill_pattern_row(&t_in, &mut pr); assert_eq!(pr.0.len(), 3); assert_eq!(pr.0[0], 0); @@ -672,7 +672,7 @@ mod tests { let mut t_in: Vec = vec![0; N]; t_in[..s].copy_from_slice(&vec![1; s]); let mut pr = PatternRow::default(); - get_pattern_row(&t_in, &mut pr); + fill_pattern_row(&t_in, &mut pr); assert_eq!(pr.0.len(), 3); assert_eq!(pr.0[0], 0); @@ -687,7 +687,7 @@ mod tests { let mut t_in: Vec = vec![0xff; N]; t_in[..s].copy_from_slice(&vec![0; s]); let mut pr = PatternRow::default(); - get_pattern_row(&t_in, &mut pr); + fill_pattern_row(&t_in, &mut pr); assert_eq!(pr.0.len(), 3); assert_eq!(pr.0[0], s as PatternType); @@ -711,7 +711,7 @@ mod tests { #[test] fn basic_pattern_view() { let mut p_row = PatternRow::default(); - get_pattern_row( + fill_pattern_row( &[ 0_u16, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, ], diff --git a/rxing-reader/src/common/cpp_essentials/regression_line.rs b/rxing-reader/src/common/detect/regression_line.rs similarity index 90% rename from rxing-reader/src/common/cpp_essentials/regression_line.rs rename to rxing-reader/src/common/detect/regression_line.rs index 291246c..c592004 100644 --- a/rxing-reader/src/common/cpp_essentials/regression_line.rs +++ b/rxing-reader/src/common/detect/regression_line.rs @@ -170,12 +170,22 @@ impl RegressionLineTrait for RegressionLine { } impl RegressionLine { + /// Constructs a line through two points. + /// + /// Evaluation can fail (e.g. when the two points coincide), in which case + /// the returned line has NaN coefficients. Callers must check + /// [`RegressionLineTrait::is_valid`] before relying on the result. pub fn with_two_points(point1: Point, point2: Point) -> Self { let mut new_rl = RegressionLine::default(); new_rl.set_direction_inward(point2 - point1); new_rl.evaluate(&[point1, point2]); new_rl } + /// Constructs a line fitted to a slice of points. + /// + /// Evaluation can fail (e.g. on an empty slice or degenerate input), in + /// which case the returned line has NaN coefficients. Callers must check + /// [`RegressionLineTrait::is_valid`] before relying on the result. pub fn with_point_slice(points: &[Point]) -> Self { let mut new_rl = RegressionLine::default(); if let (Some(first), Some(last)) = (points.first(), points.last()) diff --git a/rxing-reader/src/common/detect/regression_line_trait.rs b/rxing-reader/src/common/detect/regression_line_trait.rs new file mode 100644 index 0000000..9ce5520 --- /dev/null +++ b/rxing-reader/src/common/detect/regression_line_trait.rs @@ -0,0 +1,50 @@ +use crate::{Point, point}; +use anyhow::Result; + +/// Minimum determinant magnitude for [`intersect`] to treat two lines as +/// non-parallel. Line coefficients are normalized by `RegressionLine::evaluate` +/// so the determinant is unitless, but f32 round-off at pixel scale makes +/// `f32::EPSILON` too tight in practice — `1e-6` rejects near-parallel pairs +/// without false negatives on well-conditioned lines. +pub const LINE_INTERSECTION_EPS: f32 = 1e-6; + +/// Returns the intersection point of two regression lines, or `None` if either +/// line is invalid or the pair is (near-)parallel. +pub fn intersect( + l1: &T, + l2: &T2, +) -> Option { + if !(l1.is_valid() && l2.is_valid()) { + return None; + } + + let d = l1.a() * l2.b() - l1.b() * l2.a(); + if d.abs() < LINE_INTERSECTION_EPS { + return None; + } + let x = (l1.c() * l2.b() - l1.b() * l2.c()) / d; + let y = (l1.a() * l2.c() - l1.c() * l2.a()) / d; + + Some(point(x, y)) +} + +pub trait RegressionLineTrait { + fn evaluate(&mut self, points: &[Point]) -> bool; + fn evaluate_self(&mut self) -> bool; + + fn is_valid(&self) -> bool; + fn normal(&self) -> Point; + fn signed_distance(&self, p: Point) -> f32; + fn distance_single(&self, p: Point) -> f32; + + fn add(&mut self, p: Point) -> Result<()>; + + fn set_direction_inward(&mut self, d: Point); + + fn evaluate_max_distance_with(&mut self, max_signed_dist: f64, update_points: bool) -> bool; + + fn is_high_res(&self) -> bool; + fn a(&self) -> f32; + fn b(&self) -> f32; + fn c(&self) -> f32; +} diff --git a/rxing-reader/src/common/cpp_essentials/util.rs b/rxing-reader/src/common/detect/util.rs similarity index 72% rename from rxing-reader/src/common/cpp_essentials/util.rs rename to rxing-reader/src/common/detect/util.rs index 085eb12..9660177 100644 --- a/rxing-reader/src/common/cpp_essentials/util.rs +++ b/rxing-reader/src/common/detect/util.rs @@ -6,10 +6,9 @@ use super::Direction; #[allow(dead_code)] #[inline(always)] pub fn opposite(dir: Direction) -> Direction { - if dir == Direction::Left { - Direction::Right - } else { - Direction::Left + match dir { + Direction::Left => Direction::Right, + Direction::Right => Direction::Left, } } @@ -25,7 +24,7 @@ pub fn update_min_max_float(min: &mut f64, max: &mut f64, val: f64) { *max = f64::max(*max, val); } -pub fn to_string>(val: T, len: usize) -> Result { +pub fn to_fixed_len_string>(val: T, len: usize) -> Result { let mut val: usize = val.into(); let mut result = vec!['0'; len]; let mut idx = len; @@ -44,8 +43,10 @@ pub fn to_string>(val: T, len: usize) -> Result { Ok(result.iter().collect()) } -pub fn append_bit(val: &mut i32, bit: bool) { +pub fn append_bit(val: &mut T, bit: bool) +where + T: std::ops::ShlAssign + std::ops::BitOrAssign + From, +{ *val <<= 1; - - *val |= i32::from(bit) + *val |= T::from(bit); } diff --git a/rxing-reader/src/common/cpp_essentials/value.rs b/rxing-reader/src/common/detect/value.rs similarity index 93% rename from rxing-reader/src/common/cpp_essentials/value.rs rename to rxing-reader/src/common/detect/value.rs index 62025a0..691cf3d 100644 --- a/rxing-reader/src/common/cpp_essentials/value.rs +++ b/rxing-reader/src/common/detect/value.rs @@ -1,8 +1,8 @@ #[derive(Clone, Copy, PartialEq, Eq)] pub enum Value { - Invalid = -1, - White = 0, - Black = 1, + Invalid, + White, + Black, } impl Value { pub fn is_black(&self) -> bool { diff --git a/rxing-reader/src/common/eci.rs b/rxing-reader/src/common/eci.rs index b3908cc..8dc4815 100644 --- a/rxing-reader/src/common/eci.rs +++ b/rxing-reader/src/common/eci.rs @@ -4,62 +4,102 @@ use crate::Error; use super::CharacterSet; +const MAX_ECI_ASSIGNMENT_VALUE: u32 = 999_999; + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum Eci { - Unknown = -1, - Cp437 = 2, // obsolete - ISO8859_1 = 3, - ISO8859_2 = 4, - ISO8859_3 = 5, - ISO8859_4 = 6, - ISO8859_5 = 7, - ISO8859_6 = 8, - ISO8859_7 = 9, - ISO8859_8 = 10, - ISO8859_9 = 11, - ISO8859_10 = 12, - ISO8859_11 = 13, - ISO8859_13 = 15, - ISO8859_14 = 16, - ISO8859_15 = 17, - ISO8859_16 = 18, - ShiftJis = 20, - Cp1250 = 21, - Cp1251 = 22, - Cp1252 = 23, - Cp1256 = 24, - UTF16BE = 25, - UTF8 = 26, - Ascii = 27, - Big5 = 28, - GB2312 = 29, - EucKr = 30, - GB18030 = 32, - UTF16LE = 33, - UTF32BE = 34, - UTF32LE = 35, - Iso646Inv = 170, - Binary = 899, + Unknown, + Cp437, + ISO8859_1, + ISO8859_2, + ISO8859_3, + ISO8859_4, + ISO8859_5, + ISO8859_6, + ISO8859_7, + ISO8859_8, + ISO8859_9, + ISO8859_10, + ISO8859_11, + ISO8859_13, + ISO8859_14, + ISO8859_15, + ISO8859_16, + ShiftJis, + Cp1250, + Cp1251, + Cp1252, + Cp1256, + UTF16BE, + UTF8, + Ascii, + Big5, + GB2312, + EucKr, + GB18030, + UTF16LE, + UTF32BE, + UTF32LE, + Iso646Inv, + Binary, +} + +impl Eci { + pub const fn assignment_number(self) -> Option { + match self { + Eci::Unknown => None, + Eci::Cp437 => Some(2), + Eci::ISO8859_1 => Some(3), + Eci::ISO8859_2 => Some(4), + Eci::ISO8859_3 => Some(5), + Eci::ISO8859_4 => Some(6), + Eci::ISO8859_5 => Some(7), + Eci::ISO8859_6 => Some(8), + Eci::ISO8859_7 => Some(9), + Eci::ISO8859_8 => Some(10), + Eci::ISO8859_9 => Some(11), + Eci::ISO8859_10 => Some(12), + Eci::ISO8859_11 => Some(13), + Eci::ISO8859_13 => Some(15), + Eci::ISO8859_14 => Some(16), + Eci::ISO8859_15 => Some(17), + Eci::ISO8859_16 => Some(18), + Eci::ShiftJis => Some(20), + Eci::Cp1250 => Some(21), + Eci::Cp1251 => Some(22), + Eci::Cp1252 => Some(23), + Eci::Cp1256 => Some(24), + Eci::UTF16BE => Some(25), + Eci::UTF8 => Some(26), + Eci::Ascii => Some(27), + Eci::Big5 => Some(28), + Eci::GB2312 => Some(29), + Eci::EucKr => Some(30), + Eci::GB18030 => Some(32), + Eci::UTF16LE => Some(33), + Eci::UTF32BE => Some(34), + Eci::UTF32LE => Some(35), + Eci::Iso646Inv => Some(170), + Eci::Binary => Some(899), + } + } } impl TryFrom for Eci { type Error = anyhow::Error; fn try_from(value: u32) -> Result { - if value <= i32::MAX as u32 { - Ok((value as i32).into()) - } else { - Err(Error::InvalidArgument { - message: format!("ECI value {value} exceeds i32::MAX").into(), + if value > MAX_ECI_ASSIGNMENT_VALUE { + return Err(Error::InvalidArgument { + message: format!( + "ECI value {value} exceeds maximum assignment value {MAX_ECI_ASSIGNMENT_VALUE}" + ) + .into(), } - .into()) + .into()); } - } -} -impl From for Eci { - fn from(value: i32) -> Self { - match value { + Ok(match value { 0 | 2 => Eci::Cp437, 1 | 3 => Eci::ISO8859_1, 4 => Eci::ISO8859_2, @@ -94,7 +134,7 @@ impl From for Eci { 170 => Eci::Iso646Inv, 899 => Eci::Binary, _ => Eci::Unknown, - } + }) } } @@ -181,17 +221,20 @@ impl From for CharacterSet { impl Display for Eci { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", *self as i32) + match self.assignment_number() { + Some(value) => write!(f, "{value}"), + None => f.write_str("unknown"), + } } } #[cfg(test)] mod tests { - use super::Eci; + use super::{Eci, MAX_ECI_ASSIGNMENT_VALUE}; #[test] - fn try_from_u32_rejects_values_that_cannot_fit_i32() { + fn try_from_u32_rejects_values_outside_eci_range() { assert_eq!(Eci::try_from(26_u32).unwrap(), Eci::UTF8); - assert!(Eci::try_from(i32::MAX as u32 + 1).is_err()); + assert!(Eci::try_from(MAX_ECI_ASSIGNMENT_VALUE + 1).is_err()); } } diff --git a/rxing-reader/src/common/eci_string_builder.rs b/rxing-reader/src/common/eci_string_builder.rs index b45798e..aac81a5 100644 --- a/rxing-reader/src/common/eci_string_builder.rs +++ b/rxing-reader/src/common/eci_string_builder.rs @@ -33,8 +33,8 @@ pub struct ECIStringBuilder { } impl ECIStringBuilder { - pub fn bytes(&self) -> &[u8] { - &self.bytes + pub fn into_bytes(self) -> Vec { + self.bytes } /// Appends one raw byte. diff --git a/rxing-reader/src/common/global_histogram_binarizer.rs b/rxing-reader/src/common/global_histogram_binarizer.rs index d0334cc..62084e3 100644 --- a/rxing-reader/src/common/global_histogram_binarizer.rs +++ b/rxing-reader/src/common/global_histogram_binarizer.rs @@ -80,7 +80,7 @@ impl GlobalHistogramBinarizer { pub(super) fn build_black_matrix(source: &LS) -> Result { let width = source.width(); let height = source.height(); - let mut matrix = BitMatrix::new(width as u32, height as u32)?; + let mut matrix = BitMatrix::new(width, height)?; // Quickly calculates the histogram by sampling four rows from the image. This proved to be // more robust on the blackbox tests than sampling a diagonal as we used to do. @@ -92,7 +92,7 @@ impl GlobalHistogramBinarizer { })?; let right = (width * 4) / 5; for pixel in &local_luminances[(width / 5)..right] { - local_buckets[(pixel >> LUMINANCE_SHIFT) as usize] += 1; + local_buckets[usize::from(*pixel >> LUMINANCE_SHIFT)] += 1; } } let black_point = Self::estimate_black_point(&local_buckets)?; @@ -105,8 +105,8 @@ impl GlobalHistogramBinarizer { let offset = y * width; for x in 0..width { let pixel = local_luminances[offset + x]; - if (pixel as u32) < black_point { - matrix.set(x as u32, y as u32); + if usize::from(pixel) < black_point { + matrix.set(x, y); } } } @@ -115,8 +115,8 @@ impl GlobalHistogramBinarizer { } fn estimate_black_point( - buckets: &[u32; BUCKET_COUNT], - ) -> Result { + buckets: &[usize; BUCKET_COUNT], + ) -> Result { // Find the tallest peak in the histogram. let mut max_bucket_count = 0; let mut first_peak = 0; @@ -135,7 +135,7 @@ impl GlobalHistogramBinarizer { let mut second_peak = 0; let mut second_peak_score = 0; for (x, bucket) in buckets.iter().enumerate() { - let distance_to_biggest = (x as i32 - first_peak as i32).unsigned_abs(); + let distance_to_biggest = x.abs_diff(first_peak); // Encourage more distant second peaks by multiplying by square of distance. let score = *bucket * distance_to_biggest * distance_to_biggest; if score > second_peak_score { @@ -159,23 +159,23 @@ impl GlobalHistogramBinarizer { } // Find a valley between them that is low and closer to the white peak. - let mut best_valley = second_peak as isize - 1; - let mut best_valley_score = -1; + let mut best_valley = second_peak - 1; + let mut best_valley_score = None; - let mut x = second_peak as isize; - while x > first_peak as isize { - let from_first = x - first_peak as isize; + let mut x = second_peak; + while x > first_peak { + let from_first = x - first_peak; let score = from_first * from_first - * (second_peak as isize - x) - * (max_bucket_count - buckets[x as usize]) as isize; - if score as i32 > best_valley_score { + * (second_peak - x) + * (max_bucket_count - buckets[x]); + if best_valley_score.is_none_or(|best_score| score > best_score) { best_valley = x; - best_valley_score = score as i32; + best_valley_score = Some(score); } x -= 1; } - Ok((best_valley as u32) << LUMINANCE_SHIFT) + Ok(best_valley << LUMINANCE_SHIFT) } } diff --git a/rxing-reader/src/common/grid_sampler.rs b/rxing-reader/src/common/grid_sampler.rs index b5547a3..df05be5 100644 --- a/rxing-reader/src/common/grid_sampler.rs +++ b/rxing-reader/src/common/grid_sampler.rs @@ -20,6 +20,9 @@ use crate::{Error, Point}; use super::{BitMatrix, PerspectiveTransform}; +const NUDGE_OUTSIDE_MIN: i32 = -1; +const FIRST_PIXEL: f32 = 0.0; + /// Samples a perspective-corrected grid from an image. /// /// Implementations use finder-pattern locations to reconstruct a QR Code's @@ -29,18 +32,18 @@ pub trait GridSampler { fn sample_grid( &self, image: &BitMatrix, - dimension_x: u32, - dimension_y: u32, + dimension_x: usize, + dimension_y: usize, controls: &[SamplerControl], ) -> Result { if dimension_x == 0 || dimension_y == 0 { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } let mut bits = BitMatrix::new(dimension_x, dimension_y)?; - let mut points = vec![Point::default(); dimension_x as usize]; + let mut points = vec![Point::default(); dimension_x]; for y in 0..dimension_y { let i_value = y as f32 + 0.5; @@ -57,13 +60,13 @@ pub trait GridSampler { self.check_and_nudge_points(image, &mut points)?; for (x, point) in points.iter().enumerate() { if image - .try_get(point.x as u32, point.y as u32) + .try_at_point(*point) .ok_or(Error::NotFound { message: "grid point transformed outside image bounds".into(), })? { // Black(-ish) pixel - bits.set(x as u32, y); + bits.set(x, y); } } } @@ -86,8 +89,12 @@ pub trait GridSampler { /// /// Returns a not-found error if an endpoint is lies outside the image boundaries fn check_and_nudge_points(&self, image: &BitMatrix, points: &mut [Point]) -> Result<()> { - let width = image.width(); - let height = image.height(); + let width = i32::try_from(image.width()).map_err(|_| Error::InvalidArgument { + message: format!("image width {} does not fit in i32", image.width()).into(), + })?; + let height = i32::try_from(image.height()).map_err(|_| Error::InvalidArgument { + message: format!("image height {} does not fit in i32", image.height()).into(), + })?; // Check and nudge points from start until we see some that are OK: let mut nudged; if points.is_empty() { @@ -96,24 +103,24 @@ pub trait GridSampler { let max_offset = points.len() - 1; // points.length must be even for point in points.iter_mut().take(max_offset) { let (x, y) = (point.x as i32, point.y as i32); - if x < -1 || x > width as i32 || y < -1 || y > height as i32 { + if x < NUDGE_OUTSIDE_MIN || x > width || y < NUDGE_OUTSIDE_MIN || y > height { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } nudged = false; - if x == -1 { - point.x = 0.0; + if x == NUDGE_OUTSIDE_MIN { + point.x = FIRST_PIXEL; nudged = true; - } else if x == width as i32 { + } else if x == width { point.x = width as f32 - 1.0; nudged = true; } - if y == -1 { - point.y = 0.0; + if y == NUDGE_OUTSIDE_MIN { + point.y = FIRST_PIXEL; nudged = true; - } else if y == height as i32 { + } else if y == height { point.y = height as f32 - 1.0; nudged = true; } @@ -124,24 +131,24 @@ pub trait GridSampler { // Check and nudge points from end: for point in points.iter_mut().rev().take(max_offset).rev() { let (x, y) = (point.x as i32, point.y as i32); - if x < -1 || x > width as i32 || y < -1 || y > height as i32 { + if x < NUDGE_OUTSIDE_MIN || x > width || y < NUDGE_OUTSIDE_MIN || y > height { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } nudged = false; - if x == -1 { - point.x = 0.0; + if x == NUDGE_OUTSIDE_MIN { + point.x = FIRST_PIXEL; nudged = true; - } else if x == width as i32 { + } else if x == width { point.x = width as f32 - 1.0; nudged = true; } - if y == -1 { - point.y = 0.0; + if y == NUDGE_OUTSIDE_MIN { + point.y = FIRST_PIXEL; nudged = true; - } else if y == height as i32 { + } else if y == height { point.y = height as f32 - 1.0; nudged = true; } diff --git a/rxing-reader/src/common/hybrid_binarizer.rs b/rxing-reader/src/common/hybrid_binarizer.rs index 9d1fb87..c89585f 100644 --- a/rxing-reader/src/common/hybrid_binarizer.rs +++ b/rxing-reader/src/common/hybrid_binarizer.rs @@ -24,8 +24,8 @@ use super::{BitMatrix, GlobalHistogramBinarizer}; /// Local-thresholding binarizer. /// /// This is slower than [`GlobalHistogramBinarizer`] but handles high-frequency -/// barcode images with black data on white backgrounds more robustly. It is designed for -/// high frequency images of barcodes with black data on white backgrounds. For this application, +/// QR images with black data on white backgrounds more robustly. It is designed for +/// high-frequency QR images with black data on white backgrounds. For this application, /// it does a much better job than a global blackpoint with severe shadows and gradients. /// However it tends to produce artifacts on lower frequency images and is therefore not /// a good general purpose binarizer for uses outside ZXing. @@ -73,7 +73,6 @@ impl Binarizer for HybridBinarizer { // So this is the smallest dimension in each axis we can accept. const BLOCK_SIZE_POWER: usize = 3; const BLOCK_SIZE: usize = 1 << BLOCK_SIZE_POWER; // ...0100...00 -const BLOCK_SIZE_MASK: usize = BLOCK_SIZE - 1; // ...0011...11 const MINIMUM_DIMENSION: usize = BLOCK_SIZE * 5; const MIN_DYNAMIC_RANGE: usize = 24; @@ -95,29 +94,18 @@ impl HybridBinarizer { if width >= MINIMUM_DIMENSION && height >= MINIMUM_DIMENSION { let luminances = source.matrix(); - let mut sub_width = width >> BLOCK_SIZE_POWER; - if (width & BLOCK_SIZE_MASK) != 0 { - sub_width += 1; - } - let mut sub_height = height >> BLOCK_SIZE_POWER; - if (height & BLOCK_SIZE_MASK) != 0 { - sub_height += 1; - } - let black_points = Self::calculate_black_points( - &luminances, - sub_width as u32, - sub_height as u32, - width as u32, - height as u32, - ); + let sub_width = width.div_ceil(BLOCK_SIZE); + let sub_height = height.div_ceil(BLOCK_SIZE); + let black_points = + Self::calculate_black_points(&luminances, sub_width, sub_height, width, height); - let mut new_matrix = BitMatrix::new(width as u32, height as u32)?; + let mut new_matrix = BitMatrix::new(width, height)?; Self::calculate_threshold_for_block( &luminances, - sub_width as u32, - sub_height as u32, - width as u32, - height as u32, + sub_width, + sub_height, + width, + height, &black_points, &mut new_matrix, ); @@ -133,31 +121,31 @@ impl HybridBinarizer { /// on the last pixels in the row/column which are also used in the previous block). fn calculate_threshold_for_block( luminances: &[u8], - sub_width: u32, - sub_height: u32, - width: u32, - height: u32, - black_points: &[u32], + sub_width: usize, + sub_height: usize, + width: usize, + height: usize, + black_points: &[usize], matrix: &mut BitMatrix, ) { - let max_yoffset = height - BLOCK_SIZE as u32; - let max_xoffset = width - BLOCK_SIZE as u32; + let max_yoffset = height - BLOCK_SIZE; + let max_xoffset = width - BLOCK_SIZE; for y in 0..sub_height { - let yoffset = u32::min(y << BLOCK_SIZE_POWER, max_yoffset); + let yoffset = (y << BLOCK_SIZE_POWER).min(max_yoffset); - let top = u32::clamp(y, 2, sub_height - 3); + let top = y.clamp(2, sub_height - 3); for x in 0..sub_width { - let xoffset = u32::min(x << BLOCK_SIZE_POWER, max_xoffset); + let xoffset = (x << BLOCK_SIZE_POWER).min(max_xoffset); - let left = u32::clamp(x, 2, sub_width - 3); + let left = x.clamp(2, sub_width - 3); let mut sum = 0; - for z in -2..=2 { - let black_row = &black_points[((top as i32 + z) as u32 * sub_width) as usize..]; - sum += black_row[(left - 2) as usize] - + black_row[(left - 1) as usize] - + black_row[left as usize] - + black_row[(left + 1) as usize] - + black_row[(left + 2) as usize]; + for z in 0..5 { + let black_row = &black_points[(top + z - 2) * sub_width..]; + sum += black_row[left - 2] + + black_row[left - 1] + + black_row[left] + + black_row[left + 1] + + black_row[left + 2]; } let average = sum / 25; Self::threshold_block(luminances, xoffset, yoffset, average, width, matrix); @@ -168,18 +156,18 @@ impl HybridBinarizer { /// Applies a single threshold to a block of pixels. fn threshold_block( luminances: &[u8], - xoffset: u32, - yoffset: u32, - threshold: u32, - stride: u32, + xoffset: usize, + yoffset: usize, + threshold: usize, + stride: usize, matrix: &mut BitMatrix, ) { let mut offset = yoffset * stride + xoffset; for y in 0..BLOCK_SIZE { for x in 0..BLOCK_SIZE { // Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0. - if luminances[offset as usize + x] as u32 <= threshold { - matrix.set(xoffset + x as u32, yoffset + y as u32); + if usize::from(luminances[offset + x]) <= threshold { + matrix.set(xoffset + x, yoffset + y); } } offset += stride; @@ -191,43 +179,43 @@ impl HybridBinarizer { /// http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0 fn calculate_black_points( luminances: &[u8], - sub_width: u32, - sub_height: u32, - width: u32, - height: u32, - ) -> Vec { - let max_yoffset = height as usize - BLOCK_SIZE; - let max_xoffset = width as usize - BLOCK_SIZE; - let mut black_points = vec![0; (sub_height * sub_width) as usize]; + sub_width: usize, + sub_height: usize, + width: usize, + height: usize, + ) -> Vec { + let max_yoffset = height - BLOCK_SIZE; + let max_xoffset = width - BLOCK_SIZE; + let mut black_points = vec![0; sub_height * sub_width]; for y in 0..sub_height { - let yoffset = u32::min(y << BLOCK_SIZE_POWER, max_yoffset as u32); + let yoffset = (y << BLOCK_SIZE_POWER).min(max_yoffset); for x in 0..sub_width { - let xoffset = u32::min(x << BLOCK_SIZE_POWER, max_xoffset as u32); + let xoffset = (x << BLOCK_SIZE_POWER).min(max_xoffset); - let mut sum: u32 = 0; + let mut sum = 0usize; let mut min = u8::MAX; let mut max = u8::MIN; let mut offset = yoffset * width + xoffset; let mut yy = 0; while yy < BLOCK_SIZE { - for &pixel in &luminances[offset as usize..offset as usize + BLOCK_SIZE] { - sum += pixel as u32; + for &pixel in &luminances[offset..offset + BLOCK_SIZE] { + sum += usize::from(pixel); // still looking for good contrast min = min.min(pixel); max = max.max(pixel); } // short-circuit min/max tests once dynamic range is met - if (max - min) as usize > MIN_DYNAMIC_RANGE { + if usize::from(max - min) > MIN_DYNAMIC_RANGE { // finish the rest of the rows quickly offset += width; yy += 1; while yy < BLOCK_SIZE { - sum += luminances[offset as usize..offset as usize + BLOCK_SIZE] + sum += luminances[offset..offset + BLOCK_SIZE] .iter() - .map(|&b| b as u32) - .sum::(); + .map(|&b| usize::from(b)) + .sum::(); yy += 1; offset += width; } @@ -239,34 +227,34 @@ impl HybridBinarizer { // The default estimate is the average of the values in the block. let mut average = sum >> (BLOCK_SIZE_POWER * 2); - if (max - min) as usize <= MIN_DYNAMIC_RANGE { + if usize::from(max - min) <= MIN_DYNAMIC_RANGE { // If variation within the block is low, assume this is a block with only light or only // dark pixels. In that case we do not want to use the average, as it would divide this // low contrast area into black and white pixels, essentially creating data out of noise. // // The default assumption is that the block is light/background. Since no estimate for // the level of dark pixels exists locally, use half the min for the block. - average = min as u32 / 2; + average = usize::from(min) / 2; if y > 0 && x > 0 { // Correct the "white background" assumption for blocks that have neighbors by comparing // the pixels in this block to the previously calculated black points. This is based on - // the fact that dark barcode symbology is always surrounded by some amount of light + // the fact that dark QR symbols are always surrounded by some amount of light // background for which reasonable black point estimates were made. The bp estimated at // the boundaries is used for the interior. // The (min < bp) is arbitrary but works better than other heuristics that were tried. - let average_neighbor_black_point: u32 = (black_points - [(y as usize - 1) * sub_width as usize + x as usize] - + (2 * black_points[y as usize * sub_width as usize + x as usize - 1]) - + black_points[(y as usize - 1) * sub_width as usize + x as usize - 1]) + let average_neighbor_black_point = (black_points + [(y - 1) * sub_width + x] + + (2 * black_points[y * sub_width + x - 1]) + + black_points[(y - 1) * sub_width + x - 1]) / 4; - if (min as u32) < average_neighbor_black_point { + if usize::from(min) < average_neighbor_black_point { average = average_neighbor_black_point; } } } - black_points[(y * sub_width + x) as usize] = average; + black_points[y * sub_width + x] = average; } } black_points diff --git a/rxing-reader/src/common/mod.rs b/rxing-reader/src/common/mod.rs index 1afbe62..8d7d839 100644 --- a/rxing-reader/src/common/mod.rs +++ b/rxing-reader/src/common/mod.rs @@ -36,7 +36,7 @@ pub use eci::*; mod quad; pub use quad::*; -pub mod cpp_essentials; +pub mod detect; pub type BitFieldBaseType = usize; pub const BIT_FIELD_BASE_BITS: usize = BitFieldBaseType::BITS as usize; diff --git a/rxing-reader/src/common/perspective_transform.rs b/rxing-reader/src/common/perspective_transform.rs index 3c93b06..56050e9 100644 --- a/rxing-reader/src/common/perspective_transform.rs +++ b/rxing-reader/src/common/perspective_transform.rs @@ -99,7 +99,7 @@ impl PerspectiveTransform { pub fn transform_points_single(&self, points: &mut [Point]) -> Result<()> { for point in points.iter_mut() { *point = self.transform_point(*point).ok_or(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), })?; } Ok(()) diff --git a/rxing-reader/src/common/quad.rs b/rxing-reader/src/common/quad.rs index 81a48cd..9d2e811 100644 --- a/rxing-reader/src/common/quad.rs +++ b/rxing-reader/src/common/quad.rs @@ -131,7 +131,7 @@ impl Quadrilateral { // that can cause the corners being projected inside the image boundaries but // some points near the corners being projected outside. This has been observed // where one corner is almost in line with two others. The M/m ratio is below 2 - // for the complete existing sample set. For very "skewed" QRCodes a value of + // for the complete existing sample set. For very skewed QR codes a value of // around 3 is realistic. A value of 14 has been observed to trigger the // instability. if !m.is_finite() || m <= f32::EPSILON { @@ -249,10 +249,10 @@ impl From<[Point; 4]> for Quadrilateral { } } -impl TryFrom<&Vec> for Quadrilateral { +impl TryFrom<&[Point]> for Quadrilateral { type Error = anyhow::Error; - fn try_from(value: &Vec) -> Result { + fn try_from(value: &[Point]) -> Result { if value.len() == 4 { Ok(Self([value[0], value[1], value[2], value[3]])) } else { diff --git a/rxing-reader/src/decode.rs b/rxing-reader/src/decode.rs index e07f692..f980317 100644 --- a/rxing-reader/src/decode.rs +++ b/rxing-reader/src/decode.rs @@ -9,14 +9,13 @@ use crate::{ qrcode::QrReader, }; -/// Pyramid downscale threshold and factor, mirroring zxing-cpp's -/// `tryDownscale` defaults. -pub const PYRAMID_DOWNSCALE_THRESHOLD: u32 = 500; -pub const PYRAMID_DOWNSCALE_FACTOR: u32 = 3; +/// Pyramid downscale threshold and factor used by try-harder QR scanning. +pub const PYRAMID_DOWNSCALE_THRESHOLD: usize = 500; +pub const PYRAMID_DOWNSCALE_FACTOR: usize = 3; -pub fn rgba_to_luma(rgba: &[u8], width: u32, height: u32) -> Result, String> { - let expected = (width as usize) - .checked_mul(height as usize) +pub fn rgba_to_luma(rgba: &[u8], width: usize, height: usize) -> Result, String> { + let expected = width + .checked_mul(height) .and_then(|n| n.checked_mul(4)) .ok_or_else(|| "Image dimensions overflow".to_string())?; if rgba.len() != expected { @@ -43,11 +42,11 @@ pub fn rgba_to_luma(rgba: &[u8], width: u32, height: u32) -> Result, Str pub fn decode_with_optional_invert( bitmap: &mut BinaryBitmap, hints: &DecodeHints, - max_number_of_symbols: u32, + max_number_of_symbols: usize, try_invert: bool, ) -> Vec> { let results = QrReader - .decode_set_number_with_hints(bitmap, hints, max_number_of_symbols) + .decode_with_hints(bitmap, hints, max_number_of_symbols) .unwrap_or_default(); if !results.is_empty() { return results; @@ -55,7 +54,7 @@ pub fn decode_with_optional_invert( if try_invert && let Ok(matrix) = bitmap.black_matrix_mut() { matrix.flip_self(); return QrReader - .decode_set_number_with_hints(bitmap, hints, max_number_of_symbols) + .decode_with_hints(bitmap, hints, max_number_of_symbols) .unwrap_or_default(); } Vec::new() @@ -66,7 +65,7 @@ pub fn decode_one_layer( source: Luma8LuminanceSource<'_>, hints: &DecodeHints, use_hybrid_binarizer: bool, - max_number_of_symbols: u32, + max_number_of_symbols: usize, try_invert: bool, close: bool, ) -> Vec> { @@ -89,12 +88,12 @@ pub fn decode_one_layer( /// close-pass, binarizer, and optional-inversion pipeline. pub fn decode_qr_codes_luma( luma: &[u8], - width: u32, - height: u32, + width: usize, + height: usize, try_harder: bool, try_invert: bool, use_hybrid_binarizer: bool, - max_number_of_symbols: u32, + max_number_of_symbols: usize, ) -> RxingResult>> { let hints = DecodeHints { try_harder }; @@ -135,7 +134,9 @@ pub fn decode_qr_codes_luma( } let (next_luma, next_w, next_h) = downscale_luma_buffer(cur_luma.as_ref(), cur_w, cur_h, PYRAMID_DOWNSCALE_FACTOR)?; - cur_luma = Cow::Owned(next_luma); + if let Cow::Owned(next_luma) = next_luma { + cur_luma = Cow::Owned(next_luma); + } cur_w = next_w; cur_h = next_h; } diff --git a/rxing-reader/src/lib.rs b/rxing-reader/src/lib.rs index b36a369..61e2bd3 100644 --- a/rxing-reader/src/lib.rs +++ b/rxing-reader/src/lib.rs @@ -6,9 +6,9 @@ use binarizer::Binarizer; use binary_bitmap::BinaryBitmap; use decode_hints::DecodeHints; use error::Error; -use luma_luma_source::{Luma8LuminanceSource, downscale_luma_buffer}; +use luma_source::{Luma8LuminanceSource, downscale_luma_buffer}; use luminance_source::LuminanceSource; -use rxing_result_point::{Point, PointI, point, point_i}; +use point::{Point, point, point_i}; pub use decode::{decode_qr_codes_luma, rgba_to_luma}; @@ -17,7 +17,7 @@ mod qrcode; // Reading mod decode_hints; -mod rxing_result_point; +mod point; // Reading sources mod binarizer; @@ -26,4 +26,4 @@ mod binary_bitmap; mod luminance_source; -mod luma_luma_source; +mod luma_source; diff --git a/rxing-reader/src/luma_luma_source.rs b/rxing-reader/src/luma_source.rs similarity index 68% rename from rxing-reader/src/luma_luma_source.rs rename to rxing-reader/src/luma_source.rs index 298bfb1..2b8f280 100644 --- a/rxing-reader/src/luma_luma_source.rs +++ b/rxing-reader/src/luma_source.rs @@ -7,7 +7,7 @@ use anyhow::Result; #[derive(Debug)] pub struct Luma8LuminanceSource<'a> { /// image dimension in form (x,y) - dimensions: (u32, u32), + dimensions: (usize, usize), /// raw data for luma 8 data: Cow<'a, [u8]>, } @@ -24,19 +24,19 @@ impl LuminanceSource for Luma8LuminanceSource<'_> { } fn width(&self) -> usize { - self.dimensions.0 as usize + self.dimensions.0 } fn height(&self) -> usize { - self.dimensions.1 as usize + self.dimensions.1 } } impl<'a> Luma8LuminanceSource<'a> { - pub fn new(source: impl Into>, width: u32, height: u32) -> Result { + pub fn new(source: impl Into>, width: usize, height: usize) -> Result { let data = source.into(); - let expected = (width as usize) - .checked_mul(height as usize) + let expected = width + .checked_mul(height) .ok_or_else(|| Error::InvalidArgument { message: format!( "Luma8LuminanceSource::new: image dimensions overflow usize ({width} x {height})" @@ -62,22 +62,21 @@ impl<'a> Luma8LuminanceSource<'a> { /// New dimensions are `(width / factor, height / factor)`. Trailing edge /// pixels that don't fit a full `factor × factor` block are dropped (truncating /// division). `factor` must be ≥ 1; `factor == 1` returns a copy. Useful as a -/// pyramid-layer step for `try_harder`-style multi-resolution decoding -/// (mirrors zxing-cpp's `tryDownscale`). -pub fn downscale_luma_buffer( - src: &[u8], - width: u32, - height: u32, - factor: u32, -) -> Result<(Vec, u32, u32)> { +/// pyramid-layer step for `try_harder`-style multi-resolution QR decoding. +pub fn downscale_luma_buffer<'a>( + src: &'a [u8], + width: usize, + height: usize, + factor: usize, +) -> Result<(Cow<'a, [u8]>, usize, usize)> { if factor == 0 { return Err(Error::InvalidArgument { message: format!("downscale_luma_buffer: factor must be at least 1 (got {factor})").into(), } .into()); } - let expected = (width as usize) - .checked_mul(height as usize) + let expected = width + .checked_mul(height) .ok_or_else(|| Error::InvalidArgument { message: format!( "downscale_luma_buffer: image dimensions overflow usize ({width} x {height})" @@ -95,23 +94,20 @@ pub fn downscale_luma_buffer( let new_w = width / factor; let new_h = height / factor; if factor == 1 { - return Ok((src.to_vec(), new_w, new_h)); + return Ok((Cow::Borrowed(src), new_w, new_h)); } - let factor_us = factor as usize; - let w_us = width as usize; - let new_w_us = new_w as usize; - let mut out = vec![0u8; new_w_us * new_h as usize]; - let half_area = (factor_us * factor_us) / 2; - for dy in 0..new_h as usize { - for dx in 0..new_w_us { + let mut out = vec![0u8; new_w * new_h]; + let half_area = (factor * factor) / 2; + for dy in 0..new_h { + for dx in 0..new_w { let mut sum = half_area; - for ty in 0..factor_us { - for tx in 0..factor_us { - sum += src[(dy * factor_us + ty) * w_us + (dx * factor_us + tx)] as usize; + for ty in 0..factor { + for tx in 0..factor { + sum += usize::from(src[(dy * factor + ty) * width + (dx * factor + tx)]); } } - out[dy * new_w_us + dx] = (sum / (factor_us * factor_us)) as u8; + out[dy * new_w + dx] = (sum / (factor * factor)) as u8; } } - Ok((out, new_w, new_h)) + Ok((Cow::Owned(out), new_w, new_h)) } diff --git a/rxing-reader/src/rxing_result_point.rs b/rxing-reader/src/point.rs similarity index 98% rename from rxing-reader/src/rxing_result_point.rs rename to rxing-reader/src/point.rs index 9868e9a..47bb358 100644 --- a/rxing-reader/src/rxing_result_point.rs +++ b/rxing-reader/src/point.rs @@ -2,8 +2,8 @@ use std::{fmt, iter::Sum}; use std::hash::Hash; -/// Encapsulates a point of interest in an image containing a barcode. Typically, this -/// would be the location of a finder pattern or the corner of the barcode, for example. +/// Encapsulates a point of interest in an image containing a QR symbol. Typically, this +/// would be the location of a finder pattern or a symbol corner, for example. #[derive(Debug, Clone, Copy, Default, Hash, PartialEq, PartialOrd)] pub struct PointT { pub x: T, @@ -576,7 +576,7 @@ mod point_tests { assert_eq!(v1.cross(v2), -7.0); assert_eq!(v1.sum_abs_component(), 3.0); assert_eq!(v1.max_abs_component(), 2.0); - assert!((v1.length() - (5.0f32).hypot(0.0)).abs() > -1.0); // length >= 0 + assert!((v1.length() - (5.0f32).sqrt()).abs() < 1e-6); // squared_distance / distance assert_eq!(v1.squared_distance(v2), { let d = v1 - v2; diff --git a/rxing-reader/src/qrcode/cpp_port/data_mask.rs b/rxing-reader/src/qrcode/cpp_port/data_mask.rs deleted file mode 100644 index 2e1d7a9..0000000 --- a/rxing-reader/src/qrcode/cpp_port/data_mask.rs +++ /dev/null @@ -1,31 +0,0 @@ -/* -* Copyright 2016 Nu-book Inc. -* Copyright 2016 ZXing authors -*/ -// SPDX-License-Identifier: Apache-2.0 - -use crate::Error; -use anyhow::Result; - -/// Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. -/// -/// Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position -/// and j is row position. In fact, as the text says, i is row position and j is column position. -pub fn data_mask_bit(mask_index: u32, x: u32, y: u32) -> Result { - match mask_index { - 0 => return Ok((y + x).is_multiple_of(2)), - 1 => return Ok(y.is_multiple_of(2)), - 2 => return Ok(x.is_multiple_of(3)), - 3 => return Ok((y + x).is_multiple_of(3)), - 4 => return Ok(((y / 2) + (x / 3)).is_multiple_of(2)), - 5 => return Ok((y * x).is_multiple_of(6)), - 6 => return Ok(((y * x) % 6) < 3), - 7 => return Ok((y + x + ((y * x) % 3)).is_multiple_of(2)), - _ => {} - } - - Err(Error::InvalidArgument { - message: format!("QRCode maskIndex {mask_index} out of range (expected 0..=7)").into(), - } - .into()) -} diff --git a/rxing-reader/src/qrcode/cpp_port/detector_result.rs b/rxing-reader/src/qrcode/cpp_port/detector_result.rs deleted file mode 100644 index e600b8a..0000000 --- a/rxing-reader/src/qrcode/cpp_port/detector_result.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::common::BitMatrix; - -pub(crate) struct QRCodeDetectorResult { - bits: BitMatrix, -} - -impl QRCodeDetectorResult { - pub(crate) fn new(bits: BitMatrix) -> Self { - Self { bits } - } - - pub(crate) fn bits(&self) -> &BitMatrix { - &self.bits - } -} diff --git a/rxing-reader/src/qrcode/cpp_port/mod.rs b/rxing-reader/src/qrcode/cpp_port/mod.rs deleted file mode 100644 index c181c71..0000000 --- a/rxing-reader/src/qrcode/cpp_port/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod data_mask; -mod decoder; -mod detector; -mod detector_result; -mod qr_cpp_reader; - -pub(crate) use qr_cpp_reader::QrReader; - -mod bitmatrix_parser; diff --git a/rxing-reader/src/qrcode/error_correction_level.rs b/rxing-reader/src/qrcode/error_correction_level.rs index 4e34eb9..81c4548 100644 --- a/rxing-reader/src/qrcode/error_correction_level.rs +++ b/rxing-reader/src/qrcode/error_correction_level.rs @@ -25,14 +25,13 @@ use anyhow::Result; #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum ErrorCorrectionLevel { /// L = ~7% correction - L, //0x01 + L, /// M = ~15% correction - M, //0x00 + M, /// Q = ~25% correction - Q, //0x03 + Q, /// H = ~30% correction - H, //0x02 - Invalid, + H, } impl ErrorCorrectionLevel { @@ -52,23 +51,21 @@ impl ErrorCorrectionLevel { } } - pub const fn value(&self) -> u8 { + pub const fn format_bits(self) -> u8 { match self { ErrorCorrectionLevel::L => 0x01, ErrorCorrectionLevel::M => 0x00, ErrorCorrectionLevel::Q => 0x03, ErrorCorrectionLevel::H => 0x02, - ErrorCorrectionLevel::Invalid => 0xFF, } } - pub const fn ordinal(&self) -> u8 { + pub const fn ec_blocks_index(self) -> usize { match self { ErrorCorrectionLevel::L => 0, ErrorCorrectionLevel::M => 1, ErrorCorrectionLevel::Q => 2, ErrorCorrectionLevel::H => 3, - ErrorCorrectionLevel::Invalid => 100, } } } @@ -83,7 +80,7 @@ impl TryFrom for ErrorCorrectionLevel { impl From for u8 { fn from(value: ErrorCorrectionLevel) -> Self { - value.value() + value.format_bits() } } @@ -124,7 +121,6 @@ impl Display for ErrorCorrectionLevel { ErrorCorrectionLevel::M => "M", ErrorCorrectionLevel::Q => "Q", ErrorCorrectionLevel::H => "H", - ErrorCorrectionLevel::Invalid => "_", }) } } @@ -134,15 +130,11 @@ mod tests { use super::ErrorCorrectionLevel; #[test] - fn invalid_value_uses_distinct_sentinel() { - assert_eq!(ErrorCorrectionLevel::Invalid.value(), 0xFF); - assert_ne!( - ErrorCorrectionLevel::Invalid.value(), - ErrorCorrectionLevel::M.value() - ); - assert_eq!( - u8::from(ErrorCorrectionLevel::Invalid), - ErrorCorrectionLevel::Invalid.value() - ); + fn maps_format_bits_to_levels() { + assert_eq!(ErrorCorrectionLevel::for_bits(0).unwrap(), ErrorCorrectionLevel::M); + assert_eq!(ErrorCorrectionLevel::for_bits(1).unwrap(), ErrorCorrectionLevel::L); + assert_eq!(ErrorCorrectionLevel::for_bits(2).unwrap(), ErrorCorrectionLevel::H); + assert_eq!(ErrorCorrectionLevel::for_bits(3).unwrap(), ErrorCorrectionLevel::Q); + assert!(ErrorCorrectionLevel::for_bits(4).is_err()); } } diff --git a/rxing-reader/src/qrcode/format_information.rs b/rxing-reader/src/qrcode/format_information.rs index d7498f5..0b2855a 100644 --- a/rxing-reader/src/qrcode/format_information.rs +++ b/rxing-reader/src/qrcode/format_information.rs @@ -14,10 +14,100 @@ * limitations under the License. */ +use anyhow::Result; + +use crate::Error; + use super::ErrorCorrectionLevel; -pub const FORMAT_INFO_MASK_QR: u32 = 0x5412; -pub const FORMAT_INFO_MASK_MODEL2: u32 = FORMAT_INFO_MASK_QR; +pub const FORMAT_INFO_MASK_QR: u16 = 0x5412; +const FORMAT_INFO_BITS: u32 = 15; +const FORMAT_INFO_DATA_SHIFT: u16 = 10; +const FORMAT_INFO_ERROR_CORRECTION_SHIFT: u8 = 3; +const FORMAT_INFO_DATA_MASK: u8 = 0x07; +const MAX_FORMAT_INFO_ERRORS: u32 = 3; + +#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)] +pub enum DataMask { + Pattern000, + Pattern001, + Pattern010, + Pattern011, + Pattern100, + Pattern101, + Pattern110, + Pattern111, +} + +impl DataMask { + pub fn is_masked(self, x: usize, y: usize) -> bool { + match self { + DataMask::Pattern000 => (y + x).is_multiple_of(2), + DataMask::Pattern001 => y.is_multiple_of(2), + DataMask::Pattern010 => x.is_multiple_of(3), + DataMask::Pattern011 => (y + x).is_multiple_of(3), + DataMask::Pattern100 => ((y / 2) + (x / 3)).is_multiple_of(2), + DataMask::Pattern101 => (y * x).is_multiple_of(6), + DataMask::Pattern110 => ((y * x) % 6) < 3, + DataMask::Pattern111 => (y + x + ((y * x) % 3)).is_multiple_of(2), + } + } +} + +impl TryFrom for DataMask { + type Error = anyhow::Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Pattern000), + 1 => Ok(Self::Pattern001), + 2 => Ok(Self::Pattern010), + 3 => Ok(Self::Pattern011), + 4 => Ok(Self::Pattern100), + 5 => Ok(Self::Pattern101), + 6 => Ok(Self::Pattern110), + 7 => Ok(Self::Pattern111), + _ => Err(Error::InvalidArgument { + message: format!("QR data-mask bits {value} out of range (expected 0..=7)").into(), + } + .into()), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FormatInfoSource { + Primary, + Secondary, + MirroredPrimary, + MirroredSecondary, +} + +impl FormatInfoSource { + fn from_bits_index(index: usize) -> Option { + match index { + 0 => Some(Self::Primary), + 1 => Some(Self::Secondary), + 2 => Some(Self::MirroredPrimary), + 3 => Some(Self::MirroredSecondary), + _ => None, + } + } + + const fn is_mirrored(self) -> bool { + matches!( + self, + Self::MirroredPrimary | Self::MirroredSecondary + ) + } +} + +#[derive(Debug, Clone, Copy)] +struct FormatInfoCandidate { + hamming_distance: u32, + data: u8, + source: FormatInfoSource, +} /// Encapsulates a QR Code's format information, including the data mask used and /// error correction level. @@ -28,24 +118,92 @@ pub const FORMAT_INFO_MASK_MODEL2: u32 = FORMAT_INFO_MASK_QR; pub struct FormatInformation { pub hamming_distance: u32, pub error_correction_level: ErrorCorrectionLevel, - pub data_mask: u8, + pub data_mask: DataMask, pub is_mirrored: bool, +} - pub mask: u32, // = 0 - pub data: u32, // = 255 - pub bits_index: u8, // = 255; -} - -impl Default for FormatInformation { - fn default() -> Self { - Self { - hamming_distance: 255, - error_correction_level: ErrorCorrectionLevel::Invalid, - data_mask: Default::default(), - is_mirrored: false, - mask: 0, - data: 255, - bits_index: 255, +impl FormatInformation { + /// - `format_info_bits1`: format info indicator, with mask still applied + /// - `format_info_bits2`: second copy of same info; both are checked at the same time to establish best match + pub fn decode_qr(format_info_bits1: u16, format_info_bits2: u16) -> Result { + // Mask out the Dark Module for mirrored and non-mirrored cases. + let mirrored_format_info_bits2 = Self::mirror_bits( + ((format_info_bits2 >> 1) & 0b111111110000000) | (format_info_bits2 & 0b1111111), + ); + let format_info_bits2 = + ((format_info_bits2 >> 1) & 0b111111100000000) | (format_info_bits2 & 0b11111111); + // Some QR codes do not apply the XOR mask. Try with standard masking and without it. + let candidate = Self::find_best_format_info( + &[FORMAT_INFO_MASK_QR, 0], + &[ + format_info_bits1, + format_info_bits2, + Self::mirror_bits(format_info_bits1), + mirrored_format_info_bits2, + ], + ) + .filter(|candidate| candidate.hamming_distance <= MAX_FORMAT_INFO_ERRORS) + .ok_or_else(|| Error::InvalidFormat { + message: format!( + "QR format information is too damaged to decode (top-left=0x{format_info_bits1:04X}, top-right/bottom-left=0x{format_info_bits2:04X})" + ) + .into(), + })?; + + // Use bits 3/4 for error correction, and 0-2 for mask. + let error_correction_level = + ErrorCorrectionLevel::for_bits(candidate.data >> FORMAT_INFO_ERROR_CORRECTION_SHIFT)?; + let data_mask = DataMask::try_from(candidate.data & FORMAT_INFO_DATA_MASK)?; + + Ok(Self { + hamming_distance: candidate.hamming_distance, + error_correction_level, + data_mask, + is_mirrored: candidate.source.is_mirrored(), + }) + } + + #[inline(always)] + pub fn mirror_bits(bits: u16) -> u16 { + bits.reverse_bits() >> (u16::BITS - FORMAT_INFO_BITS) + } + + fn find_best_format_info(masks: &[u16], bits: &[u16]) -> Option { + // See ISO 18004:2015, Annex C, Table C.1 + const QR_MASKED_PATTERNS: [u16; 32] = [ + 0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0, 0x77C4, 0x72F3, 0x7DAA, + 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976, 0x1689, 0x13BE, 0x1CE7, 0x19D0, 0x0762, 0x0255, + 0x0D0C, 0x083B, 0x355F, 0x3068, 0x3F31, 0x3A06, 0x24B4, 0x2183, 0x2EDA, 0x2BED, + ]; + + let mut best = None; + for mask in masks { + for (bits_index, bits_item) in bits.iter().enumerate() { + let Some(source) = FormatInfoSource::from_bits_index(bits_index) else { + continue; + }; + for ref_pattern in QR_MASKED_PATTERNS { + let pattern = ref_pattern ^ FORMAT_INFO_MASK_QR; + let hamming_dist = ((bits_item ^ mask) ^ pattern).count_ones(); + // QR_MASKED_PATTERNS entries are 15-bit u16 values, so shifting by + // FORMAT_INFO_DATA_SHIFT (10) leaves at most 5 bits — always fits in u8. + let data = u8::try_from(pattern >> FORMAT_INFO_DATA_SHIFT) + .expect("FORMAT_INFO table yields <=5-bit values"); + let candidate = FormatInfoCandidate { + hamming_distance: hamming_dist, + data, + source, + }; + if best + .map(|best: FormatInfoCandidate| hamming_dist < best.hamming_distance) + .unwrap_or(true) + { + best = Some(candidate); + } + } + } } + + best } } diff --git a/rxing-reader/src/qrcode/mod.rs b/rxing-reader/src/qrcode/mod.rs index 2ab8563..09e78e4 100644 --- a/rxing-reader/src/qrcode/mod.rs +++ b/rxing-reader/src/qrcode/mod.rs @@ -1,13 +1,13 @@ -mod cpp_port; mod error_correction_level; mod format_information; mod mode; +mod reader; mod reed_solomon; mod version; mod version_build_versions_arrays; -pub(crate) use cpp_port::QrReader; pub(crate) use error_correction_level::*; pub(crate) use format_information::*; pub(crate) use mode::*; +pub(crate) use reader::QrReader; pub(crate) use version::*; diff --git a/rxing-reader/src/qrcode/mode.rs b/rxing-reader/src/qrcode/mode.rs index 54fbe13..02cf4ad 100644 --- a/rxing-reader/src/qrcode/mode.rs +++ b/rxing-reader/src/qrcode/mode.rs @@ -22,21 +22,24 @@ use super::Version; /// See ISO 18004:2006, 6.4.1, Tables 2 and 3. This enum encapsulates the various modes in which /// data can be encoded to bits in the QR code standard. #[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] pub enum Mode { - Terminator, - Numeric, - Alphanumeric, - StructuredAppend, - Byte, - Eci, - Kanji, - Fnc1FirstPosition, - Fnc1SecondPosition, + Terminator = 0x0, + Numeric = 0x1, + Alphanumeric = 0x2, + StructuredAppend = 0x3, + Byte = 0x4, + Fnc1FirstPosition = 0x5, + Eci = 0x7, + Kanji = 0x8, + Fnc1SecondPosition = 0x9, /// See GBT 18284-2000; "Hanzi" is a transliteration of this mode name. - Hanzi, + Hanzi = 0xD, } impl Mode { + const MODE_INDICATOR_BITS: usize = 4; + /// Converts the four-bit QR Code mode indicator into a [`Mode`]. /// /// - `bits`: four bits encoding a QR Code data mode. @@ -69,7 +72,7 @@ impl Mode { /// /// The returned value is the number of bits used to encode the count of /// characters that follow this mode indicator. - pub fn character_count_bits_u8(&self, version: &Version) -> u8 { + pub fn character_count_bits(&self, version: &Version) -> usize { let number = version.number(); let offset = if number <= 9 { @@ -82,44 +85,33 @@ impl Mode { self.character_counts()[offset] } - fn character_counts(&self) -> &[u8] { + fn character_counts(&self) -> [usize; 3] { match self { - Mode::Terminator => &[0, 0, 0], - Mode::Numeric => &[10, 12, 14], - Mode::Alphanumeric => &[9, 11, 13], - Mode::StructuredAppend => &[0, 0, 0], - Mode::Byte => &[8, 16, 16], - Mode::Eci => &[0, 0, 0], - Mode::Kanji => &[8, 10, 12], - Mode::Fnc1FirstPosition => &[0, 0, 0], - Mode::Fnc1SecondPosition => &[0, 0, 0], - Mode::Hanzi => &[8, 10, 12], + Mode::Terminator => [0, 0, 0], + Mode::Numeric => [10, 12, 14], + Mode::Alphanumeric => [9, 11, 13], + Mode::StructuredAppend => [0, 0, 0], + Mode::Byte => [8, 16, 16], + Mode::Eci => [0, 0, 0], + Mode::Kanji => [8, 10, 12], + Mode::Fnc1FirstPosition => [0, 0, 0], + Mode::Fnc1SecondPosition => [0, 0, 0], + Mode::Hanzi => [8, 10, 12], } } - pub fn bits(&self) -> u8 { - match self { - Mode::Terminator => 0x00, - Mode::Numeric => 0x01, - Mode::Alphanumeric => 0x02, - Mode::StructuredAppend => 0x03, - Mode::Byte => 0x04, - Mode::Eci => 0x07, - Mode::Kanji => 0x08, - Mode::Fnc1FirstPosition => 0x05, - Mode::Fnc1SecondPosition => 0x09, - Mode::Hanzi => 0x0D, - } + pub const fn bits(self) -> u8 { + self as u8 } - pub fn terminator_bit_length(version: &Version) -> u8 { + pub fn terminator_bit_length(version: &Version) -> usize { let _ = version; - 4 + Self::MODE_INDICATOR_BITS } - pub fn codec_mode_bits_length(version: &Version) -> u8 { + pub fn codec_mode_bits_length(version: &Version) -> usize { let _ = version; - 4 + Self::MODE_INDICATOR_BITS } /// Converts a QR mode indicator into a [`Mode`]. /// @@ -127,23 +119,11 @@ impl Mode { /// /// Returns an invalid-format error if `bits` does not correspond to a known mode. pub fn codec_mode_for_bits(bits: u32) -> Result { - if (0x00..=0x05).contains(&bits) || (0x07..=0x09).contains(&bits) || bits == 0x0d { - return Mode::try_from(bits); - } - - Err(Error::InvalidFormat { + Mode::try_from(bits).map_err(|_| Error::InvalidFormat { message: format!("Invalid QR codec mode bits 0x{bits:X}").into(), } .into()) } - - /// Returns the character-count field width for this mode and symbol version. - /// - /// The returned value is the number of bits used to encode the count of - /// characters that follow this mode indicator. - pub fn character_count_bits(&self, version: &Version) -> u32 { - self.character_count_bits_u8(version) as u32 - } } impl From for u8 { @@ -164,12 +144,9 @@ impl TryFrom for Mode { type Error = anyhow::Error; fn try_from(value: u32) -> Result { - if value > u32::from(u8::MAX) { - return Err(Error::InvalidArgument { - message: format!("{value} is not valid").into(), - } - .into()); - } - Self::for_bits(value as u8) + let value = u8::try_from(value).map_err(|_| Error::InvalidArgument { + message: format!("{value} is not valid").into(), + })?; + Self::for_bits(value) } } diff --git a/rxing-reader/src/qrcode/cpp_port/qr_cpp_reader.rs b/rxing-reader/src/qrcode/reader.rs similarity index 69% rename from rxing-reader/src/qrcode/cpp_port/qr_cpp_reader.rs rename to rxing-reader/src/qrcode/reader.rs index da76f87..b110f94 100644 --- a/rxing-reader/src/qrcode/cpp_port/qr_cpp_reader.rs +++ b/rxing-reader/src/qrcode/reader.rs @@ -1,32 +1,31 @@ -/* -* Copyright 2016 Nu-book Inc. -* Copyright 2016 ZXing authors -* Copyright 2022 Axel Waggershauser -*/ -// SPDX-License-Identifier: Apache-2.0 +mod bitmatrix_parser; +mod decoder; +mod detector; -use crate::{DecodeHints, common::cpp_essentials::ConcentricPattern}; +use crate::{ + Binarizer, BinaryBitmap, DecodeHints, + common::detect::ConcentricPattern, +}; -use super::{ +use self::{ decoder::decode, detector::{find_finder_patterns, generate_finder_pattern_sets, sample_qr}, }; #[derive(Default)] -pub struct QrReader; +pub(crate) struct QrReader; impl QrReader { /// Decode every QR symbol found in `image`. /// - /// `count` caps the number of results; pass `0` for unlimited. - pub fn decode_set_number_with_hints( + /// `max_symbols` caps the number of results; pass `0` for unlimited. + pub(crate) fn decode_with_hints( &self, - image: &mut crate::BinaryBitmap, + image: &mut BinaryBitmap, hints: &DecodeHints, - count: u32, + max_symbols: usize, ) -> anyhow::Result>> { let bin_img = image.black_matrix()?; - let max_symbols = count; let try_harder = hints.try_harder; let mut all_fps = find_finder_patterns(bin_img, try_harder); @@ -53,9 +52,9 @@ impl QrReader { used_fps.push(fp_set.tl); used_fps.push(fp_set.tr); - results.push(decoder_result.content().bytes().to_vec()); + results.push(decoder_result.into_bytes()); - if max_symbols != 0 && (results.len() as u32) == max_symbols { + if max_symbols != 0 && results.len() == max_symbols { break; } } @@ -64,4 +63,4 @@ impl QrReader { Ok(results) } -} +} \ No newline at end of file diff --git a/rxing-reader/src/qrcode/cpp_port/bitmatrix_parser.rs b/rxing-reader/src/qrcode/reader/bitmatrix_parser.rs similarity index 54% rename from rxing-reader/src/qrcode/cpp_port/bitmatrix_parser.rs rename to rxing-reader/src/qrcode/reader/bitmatrix_parser.rs index d39d166..857f3dc 100644 --- a/rxing-reader/src/qrcode/cpp_port/bitmatrix_parser.rs +++ b/rxing-reader/src/qrcode/reader/bitmatrix_parser.rs @@ -12,11 +12,10 @@ use crate::{ qrcode::{FormatInformation, Version, VersionRef}, }; -use super::data_mask::data_mask_bit; -use crate::common::cpp_essentials::append_bit; +use crate::common::detect::append_bit; -pub fn bit(bit_matrix: &BitMatrix, x: u32, y: u32, mirrored: Option) -> bool { - let mirrored = mirrored.unwrap_or(false); +/// Return the QR module bit, optionally reading from mirrored coordinates. +fn module_bit(bit_matrix: &BitMatrix, x: usize, y: usize, mirrored: bool) -> bool { if mirrored { bit_matrix.get(y, x) } else { @@ -25,90 +24,83 @@ pub fn bit(bit_matrix: &BitMatrix, x: u32, y: u32, mirrored: Option) -> bo } pub fn read_version(bit_matrix: &BitMatrix) -> Result { - if !Version::has_valid_size(bit_matrix) { - return Err(Error::InvalidFormat { - message: format!( - "QR data is malformed: matrix size {}x{} is not a valid QR size", - bit_matrix.width(), - bit_matrix.height(), - ).into(), - } - .into()); - } - - let number = Version::number_from_matrix(bit_matrix); + let number = Version::number_from_matrix(bit_matrix).ok_or_else(|| Error::InvalidFormat { + message: format!( + "QR data is malformed: matrix size {}x{} is not a valid QR size", + bit_matrix.width(), + bit_matrix.height(), + ) + .into(), + })?; Version::for_number(number) } pub fn read_format_information(bit_matrix: &BitMatrix) -> Result { // Read top-left format info bits - let mut format_info_bits1 = 0; + let mut format_info_bits1: u16 = 0; for x in 0..6 { - append_bit(&mut format_info_bits1, bit(bit_matrix, x, 8, None)); + append_bit(&mut format_info_bits1, module_bit(bit_matrix, x, 8, false)); } // .. and skip a bit in the timing pattern ... - append_bit(&mut format_info_bits1, bit(bit_matrix, 7, 8, None)); - append_bit(&mut format_info_bits1, bit(bit_matrix, 8, 8, None)); - append_bit(&mut format_info_bits1, bit(bit_matrix, 8, 7, None)); + append_bit(&mut format_info_bits1, module_bit(bit_matrix, 7, 8, false)); + append_bit(&mut format_info_bits1, module_bit(bit_matrix, 8, 8, false)); + append_bit(&mut format_info_bits1, module_bit(bit_matrix, 8, 7, false)); // .. and skip a bit in the timing pattern ... for y in (0..=5).rev() { - append_bit(&mut format_info_bits1, bit(bit_matrix, 8, y, None)); + append_bit(&mut format_info_bits1, module_bit(bit_matrix, 8, y, false)); } // Read the top-right/bottom-left pattern including the 'Dark Module' from the bottom-left // part that has to be considered separately when looking for mirrored symbols. // See also FormatInformation::decode_qr let dimension = bit_matrix.height(); - let mut format_info_bits2 = 0; + let mut format_info_bits2: u16 = 0; for y in ((dimension - 8)..=(dimension - 1)).rev() { - append_bit(&mut format_info_bits2, bit(bit_matrix, 8, y, None)); + append_bit(&mut format_info_bits2, module_bit(bit_matrix, 8, y, false)); } for x in (dimension - 8)..dimension { - append_bit(&mut format_info_bits2, bit(bit_matrix, x, 8, None)); + append_bit(&mut format_info_bits2, module_bit(bit_matrix, x, 8, false)); } - Ok(FormatInformation::decode_qr( - format_info_bits1 as u32, - format_info_bits2 as u32, - )) + FormatInformation::decode_qr(format_info_bits1, format_info_bits2) } -pub fn read_qrcodewords( +pub fn read_codewords( bit_matrix: &BitMatrix, version: VersionRef, format_info: &FormatInformation, ) -> Result> { let function_pattern: BitMatrix = version.build_function_pattern()?; - let mut result = Vec::with_capacity(version.total_codewords() as usize); - let mut current_byte = 0; + let mut result: Vec = Vec::with_capacity(version.total_codewords()); + let mut current_byte: u8 = 0; let mut reading_up = true; - let mut bits_read = 0; + let mut bits_read: usize = 0; let dimension = bit_matrix.height(); // Read columns in pairs, from right to left - let mut x = (dimension as i32) - 1; - while x > 0 { + let mut right_column = dimension - 1; + while right_column > 0 { // Skip whole column with vertical timing pattern. - if x == 6 { - x -= 1; + if right_column == 6 { + right_column -= 1; } // Read alternatingly from bottom to top then top to bottom for row in 0..dimension { let y = if reading_up { dimension - 1 - row } else { row }; for col in 0..2 { - let xx = (x - col) as u32; + let x = right_column - col; // Ignore bits covered by the function pattern - if !function_pattern.get(xx, y) { + if !function_pattern.get(x, y) { // Read a bit append_bit( &mut current_byte, - data_mask_bit(format_info.data_mask as u32, xx, y)? - != bit(bit_matrix, xx, y, Some(format_info.is_mirrored)), + format_info.data_mask.is_masked(x, y) + != module_bit(bit_matrix, x, y, format_info.is_mirrored), ); // If we've made a whole byte, save it off bits_read += 1; - if bits_read % 8 == 0 { + if bits_read.is_multiple_of(8) { result.push(std::mem::take(&mut current_byte)); } } @@ -116,9 +108,12 @@ pub fn read_qrcodewords( } reading_up = !reading_up; // switch directions - x -= 2; + if right_column < 2 { + break; + } + right_column -= 2; } - let expected_codewords = version.total_codewords() as usize; + let expected_codewords = version.total_codewords(); let actual_codewords = result.len(); if actual_codewords != expected_codewords { return Err(Error::InvalidFormat { @@ -129,13 +124,5 @@ pub fn read_qrcodewords( .into()); } - Ok(result.iter().copied().map(|x| x as u8).collect()) -} - -pub fn read_codewords( - bit_matrix: &BitMatrix, - version: VersionRef, - format_info: &FormatInformation, -) -> Result> { - read_qrcodewords(bit_matrix, version, format_info) + Ok(result) } diff --git a/rxing-reader/src/qrcode/cpp_port/decoder.rs b/rxing-reader/src/qrcode/reader/decoder.rs similarity index 58% rename from rxing-reader/src/qrcode/cpp_port/decoder.rs rename to rxing-reader/src/qrcode/reader/decoder.rs index 503f6f1..8b84134 100644 --- a/rxing-reader/src/qrcode/cpp_port/decoder.rs +++ b/rxing-reader/src/qrcode/reader/decoder.rs @@ -7,20 +7,158 @@ use anyhow::Result; use crate::Error; -use crate::common::cpp_essentials::DecoderResult; use crate::common::{ AIFlag, BitMatrix, BitSource, CharacterSet, ECIStringBuilder, Eci, SymbologyIdentifier, -}; -use crate::qrcode::cpp_port::bitmatrix_parser::{ - read_codewords, read_format_information, read_version, + detect::{DecoderResult, to_fixed_len_string as padded_digits}, }; use crate::qrcode::reed_solomon::correct_qr_errors; use crate::qrcode::{ErrorCorrectionLevel, Mode, Version, VersionRef}; +use super::bitmatrix_parser::{read_codewords, read_format_information, read_version}; + +const BYTE_BITS: usize = 8; +const DOUBLE_BYTE_SEGMENT_BITS: usize = 13; +const HANZI_SUBSET_BITS: usize = 4; +const GB2312_SUBSET: u32 = 1; +const HANZI_BYTE_MULTIPLIER: u32 = 0x060; +const HANZI_LOW_RANGE_LIMIT: u32 = 0x00A00; +const HANZI_LOW_RANGE_OFFSET: u32 = 0x0A1A1; +const HANZI_HIGH_RANGE_OFFSET: u32 = 0x0A6A1; +const KANJI_BYTE_MULTIPLIER: u32 = 0x0C0; +const KANJI_LOW_RANGE_LIMIT: u32 = 0x01F00; +const KANJI_LOW_RANGE_OFFSET: u32 = 0x08140; +const KANJI_HIGH_RANGE_OFFSET: u32 = 0x0C140; +const ALPHANUMERIC_PAIR_BITS: usize = 11; +const ALPHANUMERIC_SINGLE_BITS: usize = 6; +const ALPHANUMERIC_RADIX: usize = 45; +const ALPHANUMERIC_CHARS: [char; ALPHANUMERIC_RADIX] = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + ' ', '$', '%', '*', '+', '-', '.', '/', ':', +]; +const NUMERIC_DIGIT_GROUP_BITS: [usize; 4] = [0, 4, 7, 10]; +const FNC1_GROUP_SEPARATOR: u8 = 0x1D; +const STRUCTURED_APPEND_INDEX_BITS: usize = 4; +const STRUCTURED_APPEND_COUNT_BITS: usize = 4; +const STRUCTURED_APPEND_PARITY_BITS: usize = 8; +const AIM_NUMERIC_MAX: u32 = 99; +const AIM_ALPHA_OFFSET: u32 = 100; +const ECI_ONE_BYTE_PREFIX_MASK: u32 = 0x80; +const ECI_ONE_BYTE_PAYLOAD_MASK: u32 = 0x7F; +const ECI_TWO_BYTE_PREFIX_MASK: u32 = 0xC0; +const ECI_TWO_BYTE_PREFIX: u32 = 0x80; +const ECI_TWO_BYTE_PAYLOAD_MASK: u32 = 0x3F; +const ECI_THREE_BYTE_PREFIX_MASK: u32 = 0xE0; +const ECI_THREE_BYTE_PREFIX: u32 = 0xC0; +const ECI_THREE_BYTE_PAYLOAD_MASK: u32 = 0x1F; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HanziSubset { + Gb2312, +} + +impl TryFrom for HanziSubset { + type Error = anyhow::Error; + + fn try_from(value: u32) -> Result { + match value { + GB2312_SUBSET => Ok(Self::Gb2312), + _ => Err(Error::InvalidFormat { + message: format!( + "Unsupported HANZI subset {value} (only GB2312_SUBSET = {GB2312_SUBSET} is supported)" + ) + .into(), + } + .into()), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AimApplicationIndicator { + Numeric(usize), + Alphabetic(u8), +} + +impl TryFrom for AimApplicationIndicator { + type Error = anyhow::Error; + + fn try_from(value: u32) -> Result { + match value { + 0..=AIM_NUMERIC_MAX => Ok(Self::Numeric(usize::try_from(value).map_err(|_| { + Error::InvalidFormat { + message: format!("AIM numeric indicator {value} does not fit in usize").into(), + } + })?)), + 165..=190 | 197..=222 => Ok(Self::Alphabetic( + u8::try_from(value - AIM_ALPHA_OFFSET).map_err(|_| Error::InvalidFormat { + message: format!("AIM alphabetic indicator {value} does not fit in u8").into(), + })?, + )), + _ => Err(Error::InvalidFormat { + message: format!( + "Invalid AIM Application Indicator value {value} (expected 0..=99, 165..=190, or 197..=222)" + ) + .into(), + } + .into()), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EciValueLength { + OneByte, + TwoBytes, + ThreeBytes, +} + +impl EciValueLength { + fn from_first_byte(first_byte: u32) -> Result { + if (first_byte & ECI_ONE_BYTE_PREFIX_MASK) == 0 { + return Ok(Self::OneByte); + } + if (first_byte & ECI_TWO_BYTE_PREFIX_MASK) == ECI_TWO_BYTE_PREFIX { + return Ok(Self::TwoBytes); + } + if (first_byte & ECI_THREE_BYTE_PREFIX_MASK) == ECI_THREE_BYTE_PREFIX { + return Ok(Self::ThreeBytes); + } + Err(Error::InvalidFormat { + message: format!( + "parse_eci_value: invalid leading byte 0x{first_byte:02X} (top bits do not match any ECI length encoding)" + ) + .into(), + } + .into()) + } +} + +fn read_bits_as_usize(bits: &mut BitSource, num_bits: usize, context: &str) -> Result { + usize::try_from(bits.read_bits(num_bits)?).map_err(|_| Error::InvalidFormat { + message: format!("{context} does not fit in usize (num_bits={num_bits})").into(), + } + .into()) +} + +fn read_count(bits: &mut BitSource, num_bits: usize) -> Result { + read_bits_as_usize(bits, num_bits, "QR segment count") +} + +fn append_be_u16(result: &mut ECIStringBuilder, value: u32) -> Result<()> { + let value = u16::try_from(value).map_err(|_| Error::InvalidFormat { + message: format!("double-byte character value 0x{value:X} out of range").into(), + })?; + let bytes = value.to_be_bytes(); + *result += bytes[0]; + *result += bytes[1]; + Ok(()) +} + /// See specification GBT 18284-2000 pub fn decode_hanzi_segment( bits: &mut BitSource, - count: u32, + count: usize, result: &mut ECIStringBuilder, ) -> Result<()> { let mut count = count; @@ -28,21 +166,21 @@ pub fn decode_hanzi_segment( // Each character will require 2 bytes, decode as GB2312 // There is no ECI value for GB2312, use GB18030 which is a superset result.switch_encoding(CharacterSet::GB18030, false); - result.reserve(2 * count as usize); + result.reserve(2 * count); while count > 0 { // Each 13 bits encodes a 2-byte character - let two_bytes = bits.read_bits(13)?; - let mut assembled_two_bytes = ((two_bytes / 0x060) << 8) | (two_bytes % 0x060); - if assembled_two_bytes < 0x00A00 { + let two_bytes = bits.read_bits(DOUBLE_BYTE_SEGMENT_BITS)?; + let mut assembled_two_bytes = + ((two_bytes / HANZI_BYTE_MULTIPLIER) << 8) | (two_bytes % HANZI_BYTE_MULTIPLIER); + if assembled_two_bytes < HANZI_LOW_RANGE_LIMIT { // In the 0xA1A1 to 0xAAFE range - assembled_two_bytes += 0x0A1A1; + assembled_two_bytes += HANZI_LOW_RANGE_OFFSET; } else { // In the 0xB0A1 to 0xFAFE range - assembled_two_bytes += 0x0A6A1; + assembled_two_bytes += HANZI_HIGH_RANGE_OFFSET; } - *result += ((assembled_two_bytes >> 8) & 0xFF) as u8; - *result += (assembled_two_bytes & 0xFF) as u8; + append_be_u16(result, assembled_two_bytes)?; count -= 1; } Ok(()) @@ -50,28 +188,28 @@ pub fn decode_hanzi_segment( pub fn decode_kanji_segment( bits: &mut BitSource, - count: u32, + count: usize, result: &mut ECIStringBuilder, ) -> Result<()> { let mut count = count; // Each character will require 2 bytes. Read the characters as 2-byte pairs // and decode as ShiftJis afterwards result.switch_encoding(CharacterSet::ShiftJis, false); - result.reserve(2 * count as usize); + result.reserve(2 * count); while count > 0 { // Each 13 bits encodes a 2-byte character - let two_bytes = bits.read_bits(13)?; - let mut assembled_two_bytes = ((two_bytes / 0x0C0) << 8) | (two_bytes % 0x0C0); - if assembled_two_bytes < 0x01F00 { + let two_bytes = bits.read_bits(DOUBLE_BYTE_SEGMENT_BITS)?; + let mut assembled_two_bytes = + ((two_bytes / KANJI_BYTE_MULTIPLIER) << 8) | (two_bytes % KANJI_BYTE_MULTIPLIER); + if assembled_two_bytes < KANJI_LOW_RANGE_LIMIT { // In the 0x8140 to 0x9FFC range - assembled_two_bytes += 0x08140; + assembled_two_bytes += KANJI_LOW_RANGE_OFFSET; } else { // In the 0xE040 to 0xEBBF range - assembled_two_bytes += 0x0C140; + assembled_two_bytes += KANJI_HIGH_RANGE_OFFSET; } - *result += (assembled_two_bytes >> 8) as u8; - *result += (assembled_two_bytes) as u8; + append_be_u16(result, assembled_two_bytes)?; count -= 1; } Ok(()) @@ -79,43 +217,36 @@ pub fn decode_kanji_segment( pub fn decode_byte_segment( bits: &mut BitSource, - count: u32, + count: usize, result: &mut ECIStringBuilder, ) -> Result<()> { result.switch_encoding(CharacterSet::Unknown, false); - result.reserve(count as usize); + result.reserve(count); - for _i in 0..count { - *result += (bits.read_bits(8)?) as u8; + for _ in 0..count { + *result += u8::try_from(bits.read_bits(BYTE_BITS)?).map_err(|_| Error::InvalidFormat { + message: "byte segment produced a value outside u8".into(), + })?; } Ok(()) } -pub fn to_alpha_numeric_char(value: u32) -> Result { - let value = value as usize; - /// See ISO 18004:2006, 6.4.4 Table 5 - const ALPHANUMERIC_CHARS: [char; 45] = [ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - ' ', '$', '%', '*', '+', '-', '.', '/', ':', - ]; - - if value >= (ALPHANUMERIC_CHARS.len()) { - return Err(Error::InvalidFormat { +pub fn to_alphanumeric_char(value: usize) -> Result { + ALPHANUMERIC_CHARS + .get(value) + .copied() + .ok_or_else(|| Error::InvalidFormat { message: format!( - "to_alpha_numeric_char: value {value} out of range (expected 0..{})", + "to_alphanumeric_char: value {value} out of range (expected 0..{})", ALPHANUMERIC_CHARS.len() ).into(), } - .into()); - } - - Ok(ALPHANUMERIC_CHARS[value]) + .into()) } pub fn decode_alphanumeric_segment( bits: &mut BitSource, - count: u32, + count: usize, result: &mut ECIStringBuilder, ) -> Result<()> { let mut count = count; @@ -124,14 +255,20 @@ pub fn decode_alphanumeric_segment( let mut buffer = Vec::new(); while count > 1 { - let next_two_chars_bits = bits.read_bits(11)?; - buffer.push(to_alpha_numeric_char(next_two_chars_bits / 45)?); - buffer.push(to_alpha_numeric_char(next_two_chars_bits % 45)?); + let next_two_chars_bits = + read_bits_as_usize(bits, ALPHANUMERIC_PAIR_BITS, "alphanumeric pair value")?; + buffer.push(to_alphanumeric_char(next_two_chars_bits / ALPHANUMERIC_RADIX)?); + buffer.push(to_alphanumeric_char(next_two_chars_bits % ALPHANUMERIC_RADIX)?); count -= 2; } if count == 1 { // special case: one character left - buffer.push(to_alpha_numeric_char(bits.read_bits(6)?)?); + let value = read_bits_as_usize( + bits, + ALPHANUMERIC_SINGLE_BITS, + "alphanumeric single value", + )?; + buffer.push(to_alphanumeric_char(value)?); } // See section 6.4.8.1, 6.4.8.2 if result.symbology.ai_flag != AIFlag::None { @@ -143,7 +280,7 @@ pub fn decode_alphanumeric_segment( buffer.remove(i + 1); } else { // In alpha mode, % should be converted to FNC1 separator 0x1D - buffer[i] = char::from(0x1D); + buffer[i] = char::from(FNC1_GROUP_SEPARATOR); } } i += 1; @@ -158,53 +295,45 @@ pub fn decode_alphanumeric_segment( pub fn decode_numeric_segment( bits: &mut BitSource, - count: u32, + count: usize, result: &mut ECIStringBuilder, ) -> Result<()> { let mut count = count; result.switch_encoding(CharacterSet::ISO8859_1, false); - result.reserve(count as usize); + result.reserve(count); while count > 0 { let n = std::cmp::min(count, 3); - let n_digits = bits.read_bits(1 + 3 * n as usize)?; // read 4, 7 or 10 bits into 1, 2 or 3 digits - result.append_string(&crate::common::cpp_essentials::util::to_string( - n_digits as usize, - n as usize, - )?); + let n_digits = + read_bits_as_usize(bits, NUMERIC_DIGIT_GROUP_BITS[n], "numeric segment value")?; + result.append_string(&padded_digits(n_digits, n)?); count -= n; } Ok(()) } -pub fn parse_ecivalue(bits: &mut BitSource) -> Result { - let first_byte = bits.read_bits(8)?; - if (first_byte & 0x80) == 0 { - // just one byte - return Eci::try_from(first_byte & 0x7F); - } - if (first_byte & 0xC0) == 0x80 { - // two bytes - let second_byte = bits.read_bits(8)?; - return Eci::try_from(((first_byte & 0x3F) << 8) | second_byte); - } - if (first_byte & 0xE0) == 0xC0 { - // three bytes - let second_third_bytes = bits.read_bits(16)?; - return Eci::try_from(((first_byte & 0x1F) << 16) | second_third_bytes); - } - Err(Error::InvalidFormat { - message: format!( - "parse_ecivalue: invalid leading byte 0x{first_byte:02X} (top bits do not match any ECI length encoding)" - ).into(), +pub fn parse_eci_value(bits: &mut BitSource) -> Result { + let first_byte = bits.read_bits(BYTE_BITS)?; + match EciValueLength::from_first_byte(first_byte)? { + EciValueLength::OneByte => Eci::try_from(first_byte & ECI_ONE_BYTE_PAYLOAD_MASK), + EciValueLength::TwoBytes => { + let second_byte = bits.read_bits(BYTE_BITS)?; + Eci::try_from(((first_byte & ECI_TWO_BYTE_PAYLOAD_MASK) << BYTE_BITS) | second_byte) + } + EciValueLength::ThreeBytes => { + let second_third_bytes = bits.read_bits(2 * BYTE_BITS)?; + Eci::try_from( + ((first_byte & ECI_THREE_BYTE_PAYLOAD_MASK) << (2 * BYTE_BITS)) + | second_third_bytes, + ) + } } - .into()) } /// QR codes encode mode indicators and terminator codes into a constant bit length of 4. -/// IsTerminator peaks into the bit stream to see if the current position is at the start of +/// IsTerminator peeks into the bit stream to see if the current position is at the start of /// a terminator code. If true, then the decoding can finish. If false, then the decoding /// can read off the next mode code. /// @@ -214,7 +343,7 @@ pub fn parse_ecivalue(bits: &mut BitSource) -> Result { /// - `version`: the QR code version pub fn is_end_of_stream(bits: &mut BitSource, version: &Version) -> Result { let bits_required = Mode::terminator_bit_length(version); - let bits_available = std::cmp::min(bits.available(), bits_required as usize); + let bits_available = std::cmp::min(bits.available(), bits_required); Ok(bits_available == 0 || bits.peek_bits(bits_available)? == 0) } @@ -236,7 +365,7 @@ pub fn decode_bit_stream(bytes: &[u8], version: &Version) -> Result = (|| { while !is_end_of_stream(&mut bits, version)? { - let mode = Mode::codec_mode_for_bits(bits.read_bits(mode_bit_length as usize)?)?; + let mode = Mode::codec_mode_for_bits(bits.read_bits(mode_bit_length)?)?; match mode { Mode::Fnc1FirstPosition => { @@ -249,53 +378,36 @@ pub fn decode_bit_stream(bytes: &[u8], version: &Version) -> Result { + result += padded_digits(value, 2)?; + } + AimApplicationIndicator::Alphabetic(value) => { + result += value; } - .into()); } result.symbology.ai_flag = AIFlag::Aim; } Mode::StructuredAppend => { - bits.read_bits(4)?; - bits.read_bits(4)?; - bits.read_bits(8)?; + bits.read_bits(STRUCTURED_APPEND_INDEX_BITS)?; + bits.read_bits(STRUCTURED_APPEND_COUNT_BITS)?; + bits.read_bits(STRUCTURED_APPEND_PARITY_BITS)?; } Mode::Eci => { // Count doesn't apply to ECI - result.switch_encoding(parse_ecivalue(&mut bits)?.into(), true); + result.switch_encoding(parse_eci_value(&mut bits)?.into(), true); } Mode::Hanzi => { // First handle Hanzi mode which does not start with character count // chinese mode contains a sub set indicator right after mode indicator - let subset = bits.read_bits(4)?; - if subset != 1 { - // GB2312_SUBSET is the only supported one right now - return Err(Error::InvalidFormat { - message: format!( - "Unsupported HANZI subset {subset} (only GB2312_SUBSET = 1 is supported)" - ).into(), - } - .into()); - } - let count = bits.read_bits(mode.character_count_bits(version) as usize)?; + let _subset = HanziSubset::try_from(bits.read_bits(HANZI_SUBSET_BITS)?)?; + let count = read_count(&mut bits, mode.character_count_bits(version))?; decode_hanzi_segment(&mut bits, count, &mut result)?; } _ => { // "Normal" QR code modes: // How many characters will follow, encoded in this mode? - let count = bits.read_bits(mode.character_count_bits(version) as usize)?; + let count = read_count(&mut bits, mode.character_count_bits(version))?; match mode { Mode::Numeric => decode_numeric_segment(&mut bits, count, &mut result)?, Mode::Alphanumeric => { @@ -377,12 +489,12 @@ pub fn decode(bits: &BitMatrix) -> Result { let op = |total_bytes, data_block: &DataBlock| total_bytes + data_block.num_data_codewords(); let total_bytes = data_blocks.iter().fold(0, op); - let mut result_bytes = vec![0u8; total_bytes as usize]; + let mut result_bytes = vec![0u8; total_bytes]; let mut result_iterator = 0; // Error-correct and copy data blocks together into a stream of bytes for data_block in data_blocks { - let num_data_codewords = data_block.num_data_codewords() as usize; + let num_data_codewords = data_block.num_data_codewords(); let mut codeword_bytes = data_block.codewords; correct_errors(&mut codeword_bytes, num_data_codewords)?; @@ -397,12 +509,12 @@ pub fn decode(bits: &BitMatrix) -> Result { } struct DataBlock { - num_data_codewords: u32, + num_data_codewords: usize, codewords: Vec, } impl DataBlock { - fn new(num_data_codewords: u32, codewords: Vec) -> Self { + fn new(num_data_codewords: usize, codewords: Vec) -> Self { Self { num_data_codewords, codewords, @@ -414,7 +526,7 @@ impl DataBlock { version: VersionRef, ec_level: ErrorCorrectionLevel, ) -> Result> { - if raw_codewords.len() as u32 != version.total_codewords() { + if raw_codewords.len() != version.total_codewords() { return Err(Error::InvalidArgument { message: format!( "raw codewords length {} does not match expected total codewords {}", @@ -436,7 +548,7 @@ impl DataBlock { ec_blocks.ec_codewords_per_block() + num_data_codewords; result.push(DataBlock::new( num_data_codewords, - vec![0u8; num_block_codewords as usize], + vec![0u8; num_block_codewords], )); num_result_blocks += 1; } @@ -465,7 +577,7 @@ impl DataBlock { } let shorter_blocks_num_data_codewords = - shorter_blocks_total_codewords - ec_blocks.ec_codewords_per_block() as usize; + shorter_blocks_total_codewords - ec_blocks.ec_codewords_per_block(); let mut raw_codewords_offset = 0; for i in 0..shorter_blocks_num_data_codewords { for result_j in result.iter_mut().take(num_result_blocks) { @@ -492,7 +604,7 @@ impl DataBlock { Ok(result) } - fn num_data_codewords(&self) -> u32 { + fn num_data_codewords(&self) -> usize { self.num_data_codewords } diff --git a/rxing-reader/src/qrcode/cpp_port/detector.rs b/rxing-reader/src/qrcode/reader/detector.rs similarity index 76% rename from rxing-reader/src/qrcode/cpp_port/detector.rs rename to rxing-reader/src/qrcode/reader/detector.rs index 2b9b3a7..8eee115 100644 --- a/rxing-reader/src/qrcode/cpp_port/detector.rs +++ b/rxing-reader/src/qrcode/reader/detector.rs @@ -5,8 +5,8 @@ use crate::{ Error, common::{ DefaultGridSampler, GridSampler, SamplerControl, - cpp_essentials::{ - DMRegressionLine, Matrix, append_bit, center_of_ring, find_concentric_pattern_corners, + detect::{ + Matrix, append_bit, center_of_ring, find_concentric_pattern_corners, find_left_guard_by, }, }, @@ -18,15 +18,28 @@ use crate::{ Point, common::{ BitMatrix, PerspectiveTransform, Quadrilateral, - cpp_essentials::{ + detect::{ BitMatrixCursorTrait, ConcentricPattern, Direction, EdgeTracer, FixedPattern, PatternRow, PatternType, PatternView, RegressionLine, RegressionLineTrait, - get_pattern_row_tp, is_pattern, locate_concentric_pattern, read_symmetric_pattern, + intersect, is_pattern, locate_concentric_pattern, read_pattern_row, + read_symmetric_pattern, }, }, }; -use super::detector_result::QRCodeDetectorResult; +pub(super) struct DetectorResult { + bits: BitMatrix, +} + +impl DetectorResult { + fn new(bits: BitMatrix) -> Self { + Self { bits } + } + + pub(super) fn bits(&self) -> &BitMatrix { + &self.bits + } +} #[derive(Copy, Clone, Default, Debug, PartialEq)] pub struct FinderPatternSet { @@ -43,13 +56,22 @@ const SUM: usize = 7; const PATTERN: FixedPattern = FixedPattern::new([1, 1, 3, 1, 1]); const E2E: bool = true; +/// Search-window multiplier applied to the finder-pattern row sum when locating +/// the concentric pattern. The window is widened to tolerate strongly skewed +/// samples where the on-row run lengths underestimate the true pattern extent. +const SKEW_TOLERANCE_MULTIPLIER: i32 = 3; + +/// Step size (in module widths) for the 3x3 neighbour search around the initial +/// alignment-pattern estimate in [`locate_alignment_pattern`]. +const ALIGNMENT_SEARCH_RADIUS_MULTIPLIER: f32 = 2.25; + fn find_pattern(view: PatternView<'_>) -> Result> { find_left_guard_by::( view, LEN, |view: &PatternView, space_in_pixel: Option| { // perform a fast plausibility test for 1:1:3:1:1 pattern - if view[2] < 2 as PatternType * std::cmp::max(view[0], view[4]) + if view[2] < 2u16 * std::cmp::max(view[0], view[4]) || view[2] < std::cmp::max(view[1], view[3]) { return false; @@ -61,8 +83,8 @@ fn find_pattern(view: PatternView<'_>) -> Result> { /// Locate the finder patterns for the symbol. pub fn find_finder_patterns(image: &BitMatrix, try_harder: bool) -> FinderPatterns { - const MIN_SKIP: u32 = 3; // 1 pixel/module times 3 modules/center - const MAX_MODULES_FAST: u32 = 20 * 4 + 17; // support up to version 20 for mobile clients + const MIN_SKIP: usize = 3; // 1 pixel/module times 3 modules/center + const MAX_MODULES_FAST: usize = 20 * 4 + 17; // support up to version 20 for mobile clients // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the // image, and then account for the center being 3 modules in size. This gives the smallest @@ -79,7 +101,7 @@ pub fn find_finder_patterns(image: &BitMatrix, try_harder: bool) -> FinderPatter while y < height { let mut row = PatternRow::default(); - get_pattern_row_tp(image, y, &mut row, false); + read_pattern_row(image, y, &mut row, false); let mut next: PatternView = PatternView::new(&row); while { @@ -107,8 +129,8 @@ pub fn find_finder_patterns(image: &BitMatrix, try_harder: bool) -> FinderPatter image, &PATTERN.into(), p, - next.iter().sum::() as i32 * 3, - ); // 3 for very skewed samples + next.iter().sum::() as i32 * SKEW_TOLERANCE_MULTIPLIER, + ); if let Some(p) = pattern { res.push(p); } @@ -242,7 +264,7 @@ pub fn generate_finder_pattern_sets(patterns: &mut FinderPatterns) -> FinderPatt res } -pub fn estimate_module_size( +fn estimate_module_size( image: &BitMatrix, a: ConcentricPattern, b: ConcentricPattern, @@ -250,13 +272,13 @@ pub fn estimate_module_size( let mut cur = EdgeTracer::new(image, a.p, b.p - a.p); if !cur.is_black() { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } let pattern = read_symmetric_pattern::<5, _>(&mut cur, a.size * 2).ok_or(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), })?; if !(is_pattern::( @@ -268,7 +290,7 @@ pub fn estimate_module_size( ) != 0.0) { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } @@ -279,23 +301,32 @@ pub fn estimate_module_size( ) } -pub struct DimensionEstimate { +struct DimensionEstimate { dim: i32, ms: f64, err: i32, } -impl Default for DimensionEstimate { - fn default() -> Self { - Self { - dim: 0, - ms: 0.0, - err: 4, +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum FinderPatternEdge { + Outer, + Inner, +} + +impl FinderPatternEdge { + const fn nth(self) -> i32 { + match self { + Self::Outer => 2, + Self::Inner => 3, } } + + const fn should_backup(self) -> bool { + matches!(self, Self::Inner) + } } -pub fn estimate_dimension( +fn estimate_dimension( image: &BitMatrix, a: ConcentricPattern, b: ConcentricPattern, @@ -315,26 +346,26 @@ pub fn estimate_dimension( }) } -pub fn trace_line( +fn trace_line( image: &BitMatrix, p: Point, d: Point, - edge: i32, + edge: FinderPatternEdge, ) -> Result { let mut cur = EdgeTracer::new(image, p, d - p); let mut line = RegressionLine::default(); line.set_direction_inward(cur.back()); // collect points inside the black line -> backup on 3rd edge - cur.step_to_edge(edge, 0, edge == 3); - if edge == 3 { + cur.step_to_edge(edge.nth(), 0, edge.should_backup()); + if edge.should_backup() { cur.turn_back(); } let mut cur_i = EdgeTracer::new(image, cur.p, Point::main_direction(cur.d())); // make sure cur_i positioned such that the white->black edge is directly behind // Test image: fix-traceline.jpg - while !bool::from(cur_i.edge_at_back()) { + while !cur_i.edge_at_back().is_black() { if cur_i.edge_at_left().into() { cur_i.turn_right(); } else if cur_i.edge_at_right().into() { @@ -363,7 +394,7 @@ pub fn trace_line( } // estimate how tilted the symbol is (return value between 1 and 2, see also above) -pub fn estimate_tilt(fp: &FinderPatternSet) -> f64 { +fn estimate_tilt(fp: &FinderPatternSet) -> f64 { let min = [fp.bl.size, fp.tl.size, fp.tr.size] .iter() .min() @@ -378,7 +409,7 @@ pub fn estimate_tilt(fp: &FinderPatternSet) -> f64 { (max as f64) / (min as f64) } -pub fn mod2_pix( +fn mod2_pix( dimension: i32, br_offset: Point, pix: Quadrilateral, @@ -389,7 +420,7 @@ pub fn mod2_pix( PerspectiveTransform::quadrilateral_to_quadrilateral(quad, pix) } -pub fn locate_alignment_pattern( +fn locate_alignment_pattern( image: &BitMatrix, module_size: i32, estimate: Point, @@ -407,7 +438,7 @@ pub fn locate_alignment_pattern( ] { let Some(cor) = center_of_ring( image, - (estimate + module_size as f32 * 2.25 * d).floor(), + (estimate + module_size as f32 * ALIGNMENT_SEARCH_RADIUS_MULTIPLIER * d).floor(), module_size * 3, 1, false, @@ -434,62 +465,72 @@ pub fn locate_alignment_pattern( pub fn read_version( image: &BitMatrix, - dimension: u32, + dimension: usize, mod2_pix: PerspectiveTransform, ) -> Result { - let mut bits = [0; 2]; // + let mut bits = [None, None]; for mirror in [false, true] { // Read top-right/bottom-left version info: 3 wide by 6 tall (depending on mirrored) - let mut version_bits = 0; - for y in (0..=5).rev() { + let mut version_bits: u32 = 0; + let mut valid = true; + 'read_version_bits: for y in (0..=5).rev() { for x in ((dimension - 11)..=(dimension - 9)).rev() { - let mod_ = if mirror { point_i(y, x) } else { point_i(x, y) }; - let Some(pix) = mod2_pix.transform_point((mod_).centered()) else { - version_bits = -1; - continue; - }; - if !image.is_in(pix) { - version_bits = -1; + let module = if mirror { + point(y as f32, x as f32) } else { - append_bit(&mut version_bits, image.at_point(pix)); + point(x as f32, y as f32) + }; + let Some(pixel) = mod2_pix.transform_point(module.centered()) else { + valid = false; + break 'read_version_bits; + }; + if !image.is_in(pixel) { + valid = false; + break 'read_version_bits; } + append_bit(&mut version_bits, image.at_point(pixel)); } - bits[usize::from(mirror)] = version_bits; + } + if valid { + bits[usize::from(mirror)] = Some(version_bits); } } - Version::decode_version_information_pair(bits[0], bits[1]) + Version::decode_version_information_pair(bits) } -pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result { - // Tolerate one estimator failing — pick the surviving estimate via the - // existing err-based comparison below. Failure (Err) maps to the - // `DimensionEstimate::default()` (dim=0, err=4), preserving the prior - // sentinel-based control flow. - let top = estimate_dimension(image, fp.tl, fp.tr).unwrap_or_default(); - let left = estimate_dimension(image, fp.tl, fp.bl).unwrap_or_default(); - - if top.dim == 0 && left.dim == 0 { - return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), +fn module_dimension(dimension: i32) -> Result { + usize::try_from(dimension).map_err(|_| { + Error::NotFound { + message: "QR pattern was not detected".into(), } - .into()); - } + .into() + }) +} - let best = match top.err.cmp(&left.err) { - std::cmp::Ordering::Less => top, - std::cmp::Ordering::Equal => { - if top.dim > left.dim { - top - } else { - left +pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result { + let top = estimate_dimension(image, fp.tl, fp.tr).ok(); + let left = estimate_dimension(image, fp.tl, fp.bl).ok(); + + let best = match (top, left) { + (Some(top), Some(left)) => match top.err.cmp(&left.err) { + std::cmp::Ordering::Less => top, + std::cmp::Ordering::Equal if top.dim > left.dim => top, + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => left, + }, + (Some(top), None) => top, + (None, Some(left)) => left, + (None, None) => { + return Err(Error::NotFound { + message: "QR pattern was not detected".into(), } + .into()); } - std::cmp::Ordering::Greater => left, }; let mut dimension = best.dim; + let mut dimension_usize = module_dimension(dimension)?; let module_size = (best.ms + 1.0) as i32; let mut br = ConcentricPattern { @@ -504,17 +545,17 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result 21 @@ -527,10 +568,10 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result 1.1 - || (bl2.is_high_res() - && bl3.is_high_res() - && tr2.is_high_res() - && tr3.is_high_res())) + || (bl_outer.is_high_res() + && bl_inner.is_high_res() + && tr_outer.is_high_res() + && tr_inner.is_high_res())) { br = br_inter.into(); } @@ -548,22 +589,24 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result= Version::symbol_size(7).x { + if dimension_usize >= Version::dimension_for_number(7) { let version = - read_version(image, dimension as u32, mod_to_pix).map_err(|_| Error::NotFound { - message: "barcode pattern was not detected".into(), + read_version(image, dimension_usize, mod_to_pix).map_err(|_| Error::NotFound { + message: "QR pattern was not detected".into(), })?; - let version_dimension = version.dimension() as i32; + let version_dimension = version.dimension(); + let version_dimension_i32 = version_dimension as i32; // if the version bits are garbage -> discard the detection - if (version_dimension - dimension).abs() > 8 { + if (version_dimension_i32 - dimension).abs() > 8 { return Err(Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into()); } - if version_dimension != dimension { - dimension = version_dimension; + if version_dimension_i32 != dimension { + dimension = version_dimension_i32; + dimension_usize = version_dimension; mod_to_pix = mod2_pix( dimension, br_offset, @@ -577,10 +620,10 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result Result { mod2_pix - .transform_point(Point::centered(point_i(ap_m[x], ap_m[y]))) + .transform_point(point(ap_m[x] as f32, ap_m[y] as f32).centered()) .ok_or_else(|| { Error::NotFound { - message: "barcode pattern was not detected".into(), + message: "QR pattern was not detected".into(), } .into() }) @@ -635,7 +678,8 @@ pub fn sample_qr(image: &BitMatrix, fp: &FinderPatternSet) -> Result Result Result Result Result { if value == 0 { return Err(ReedSolomonError::TooManyErrors); } - Ok(self.exp_table[FIELD_SIZE - 1 - self.log_table[value as usize] as usize]) + Ok(self.exp_table[FIELD_ORDER - self.log_table[usize::from(value)]]) } fn multiply(&self, a: u8, b: u8) -> u8 { if a == 0 || b == 0 { return 0; } - let log_sum = self.log_table[a as usize] as usize + self.log_table[b as usize] as usize; - self.exp_table[log_sum % (FIELD_SIZE - 1)] + let log_sum = self.log_table[usize::from(a)] + self.log_table[usize::from(b)]; + self.exp_table[log_sum % FIELD_ORDER] } } @@ -160,7 +161,7 @@ fn run_euclidean_algorithm( } poly_trim(&mut q); - t = poly_add(&poly_multiply(field, &q, &t_last), &t_last_last); + t = poly_add(poly_multiply(field, &q, &t_last), &t_last_last); if poly_degree(&r) >= poly_degree(&r_last) { return Err(ReedSolomonError::TooManyErrors); } @@ -173,8 +174,8 @@ fn run_euclidean_algorithm( let inverse = field.inverse(sigma_tilde_at_zero)?; Ok(( - poly_multiply_scalar(field, &t, inverse), - poly_multiply_scalar(field, &r, inverse), + poly_multiply_scalar(field, t, inverse), + poly_multiply_scalar(field, r, inverse), )) } @@ -188,9 +189,9 @@ fn find_error_locations( } let mut result = Vec::with_capacity(num_errors); - for i in 1..FIELD_SIZE { - if poly_eval(field, error_locator, i as u8) == 0 { - result.push(field.inverse(i as u8)?); + for value in 1..=u8::MAX { + if poly_eval(field, error_locator, value) == 0 { + result.push(field.inverse(value)?); if result.len() == num_errors { break; } @@ -289,24 +290,23 @@ fn poly_eval(field: &QrField, poly: &[u8], value: u8) -> u8 { result } -fn poly_add(a: &[u8], b: &[u8]) -> Vec { - if poly_is_zero(a) { +fn poly_add(a: Vec, b: &[u8]) -> Vec { + if poly_is_zero(&a) { return b.to_vec(); } if poly_is_zero(b) { - return a.to_vec(); - } - - let (smaller, larger) = if a.len() < b.len() { (a, b) } else { (b, a) }; - let length_diff = larger.len() - smaller.len(); - let mut sum = Vec::with_capacity(larger.len()); - sum.extend_from_slice(&larger[..length_diff]); - sum.extend( - smaller - .iter() - .zip(larger[length_diff..].iter()) - .map(|(&left, &right)| left ^ right), - ); + return a; + } + + let (mut sum, smaller) = if a.len() >= b.len() { + (a, b) + } else { + (b.to_vec(), a.as_slice()) + }; + let length_diff = sum.len() - smaller.len(); + for (target, &coefficient) in sum[length_diff..].iter_mut().zip(smaller.iter()) { + *target ^= coefficient; + } poly_new(sum) } @@ -329,17 +329,18 @@ fn poly_multiply(field: &QrField, a: &[u8], b: &[u8]) -> Vec { poly_new(product) } -fn poly_multiply_scalar(field: &QrField, poly: &[u8], scalar: u8) -> Vec { - if scalar == 0 || poly_is_zero(poly) { +fn poly_multiply_scalar(field: &QrField, mut poly: Vec, scalar: u8) -> Vec { + if scalar == 0 || poly_is_zero(&poly) { return vec![0]; } if scalar == 1 { - return poly.to_vec(); + return poly; } - poly.iter() - .map(|&coefficient| field.multiply(coefficient, scalar)) - .collect() + for coefficient in &mut poly { + *coefficient = field.multiply(*coefficient, scalar); + } + poly } fn poly_add_scaled_in_place(field: &QrField, target: &mut Vec, term: &[u8], scale: u8) { diff --git a/rxing-reader/src/qrcode/version.rs b/rxing-reader/src/qrcode/version.rs index cb87747..23397f3 100755 --- a/rxing-reader/src/qrcode/version.rs +++ b/rxing-reader/src/qrcode/version.rs @@ -27,6 +27,16 @@ pub type VersionRef = &'static Version; pub static VERSIONS: Lazy> = Lazy::new(Version::build_versions); +const MIN_VERSION_NUMBER: u32 = 1; +const MAX_VERSION_NUMBER: u32 = 40; +const MIN_DIMENSION: usize = 21; +const MAX_DIMENSION: usize = 177; +const DIMENSION_STEP: usize = 4; +const DIMENSION_REMAINDER: usize = 1; +const DIMENSION_VERSION_OFFSET: usize = 17; +const VERSION_INFO_FIRST_VERSION: usize = 7; +const MAX_VERSION_INFO_ERRORS: u32 = 3; + /// See ISO 18004:2006 Annex D. /// Element i represents the raw version bits that specify version i + 7 pub const VERSION_DECODE_INFO: [u32; 34] = [ @@ -40,14 +50,14 @@ pub const VERSION_DECODE_INFO: [u32; 34] = [ #[derive(Debug)] pub struct Version { version_number: u32, - alignment_pattern_centers: Box<[u32]>, + alignment_pattern_centers: Box<[usize]>, ec_blocks: Box<[ECBlocks]>, - total_codewords: u32, + total_codewords: usize, } impl Version { pub(super) fn new( version_number: u32, - alignment_pattern_centers: Box<[u32]>, + alignment_pattern_centers: Box<[usize]>, ec_blocks: [ECBlocks; 4], ) -> Self { let mut total = 0; @@ -69,26 +79,26 @@ impl Version { self.version_number } - pub const fn alignment_pattern_centers(&self) -> &[u32] { + pub const fn alignment_pattern_centers(&self) -> &[usize] { &self.alignment_pattern_centers } - pub const fn total_codewords(&self) -> u32 { + pub const fn total_codewords(&self) -> usize { self.total_codewords } - pub fn dimension(&self) -> u32 { - Self::dimension_of_version(self.version_number) + pub fn dimension(&self) -> usize { + Self::dimension_for_number(self.version_number) } pub fn ec_blocks_for_level(&self, ec_level: ErrorCorrectionLevel) -> Result<&ECBlocks> { self.ec_blocks - .get(ec_level.ordinal() as usize) + .get(ec_level.ec_blocks_index()) .ok_or_else(|| { Error::InvalidArgument { message: format!( - "ErrorCorrectionLevel ordinal {} out of range for {} EC blocks", - ec_level.ordinal(), + "ErrorCorrectionLevel index {} out of range for {} EC blocks", + ec_level.ec_blocks_index(), self.ec_blocks.len() ).into(), } @@ -103,8 +113,8 @@ impl Version { /// Returns Version for a QR Code of that dimension. /// Version 1 has dimension 21. Returns an invalid-format error if /// dimension is less than 21 or (dimension - 1) % 4 != 0. - pub fn provisional_for_dimension(dimension: u32) -> Result { - if dimension % 4 != 1 || dimension < 21 { + pub fn provisional_for_dimension(dimension: usize) -> Result { + if dimension % DIMENSION_STEP != DIMENSION_REMAINDER || dimension < MIN_DIMENSION { return Err(Error::InvalidFormat { message: format!( "QR dimension {dimension} is invalid (expected >= 21 and (dimension - 1) % 4 == 0)" @@ -113,17 +123,65 @@ impl Version { } .into()); } - Self::for_number((dimension - 17) / 4) + Self::for_number(((dimension - DIMENSION_VERSION_OFFSET) / DIMENSION_STEP) as u32) } pub fn for_number(version_number: u32) -> Result { - if !(1..=40).contains(&version_number) { + if !(MIN_VERSION_NUMBER..=MAX_VERSION_NUMBER).contains(&version_number) { return Err(Error::InvalidArgument { message: format!("QR version {version_number} is out of spec (expected 1..=40)").into(), } .into()); } - Ok(&VERSIONS[version_number as usize - 1]) + let version_index = (version_number - 1) as usize; + Ok(&VERSIONS[version_index]) + } + + pub const fn dimension_for_number(version_number: u32) -> usize { + DIMENSION_VERSION_OFFSET + DIMENSION_STEP * version_number as usize + } + + pub fn decode_version_information_pair(version_bits: [Option; 2]) -> Result { + let mut best_difference = u32::MAX; + let mut best_version = None; + for (i, target_version) in VERSION_DECODE_INFO.into_iter().enumerate() { + for bits in version_bits.iter().flatten() { + let bits_difference = (*bits ^ target_version).count_ones(); + if bits_difference < best_difference { + best_version = Some(i + VERSION_INFO_FIRST_VERSION); + best_difference = bits_difference; + } + } + if best_difference == 0 { + break; + } + } + // We can tolerate up to 3 bits of error since no two version info codewords will + // differ in less than 8 bits. + if best_difference <= MAX_VERSION_INFO_ERRORS + && let Some(best_version) = best_version + { + return Self::for_number(best_version as u32); + } + Err(Error::InvalidState { + message: "required internal state is missing".into(), + } + .into()) + } + + fn is_valid_dimensions(width: usize, height: usize) -> bool { + width == height + && (MIN_DIMENSION..=MAX_DIMENSION).contains(&width) + && width % DIMENSION_STEP == DIMENSION_REMAINDER + } + + pub fn has_valid_size(matrix: &BitMatrix) -> bool { + Self::is_valid_dimensions(matrix.width(), matrix.height()) + } + + pub fn number_from_matrix(bit_matrix: &BitMatrix) -> Option { + Self::is_valid_dimensions(bit_matrix.width(), bit_matrix.height()) + .then_some(((bit_matrix.width() - DIMENSION_VERSION_OFFSET) / DIMENSION_STEP) as u32) } /// See ISO 18004:2006 Annex E @@ -179,23 +237,23 @@ impl fmt::Display for Version { /// will be the same across all blocks within one version. #[derive(Debug, Clone)] pub struct ECBlocks { - ec_codewords_per_block: u32, + ec_codewords_per_block: usize, ec_blocks: Box<[Ecb]>, } impl ECBlocks { - pub const fn new(ec_codewords_per_block: u32, ec_blocks: Box<[Ecb]>) -> Self { + pub const fn new(ec_codewords_per_block: usize, ec_blocks: Box<[Ecb]>) -> Self { Self { ec_codewords_per_block, ec_blocks, } } - pub const fn ec_codewords_per_block(&self) -> u32 { + pub const fn ec_codewords_per_block(&self) -> usize { self.ec_codewords_per_block } - pub fn num_blocks(&self) -> u32 { + pub fn num_blocks(&self) -> usize { let mut total = 0; for ec_block in self.ec_blocks.iter() { total += ec_block.count(); @@ -203,7 +261,7 @@ impl ECBlocks { total } - pub fn total_ec_codewords(&self) -> u32 { + pub fn total_ec_codewords(&self) -> usize { self.ec_codewords_per_block * self.num_blocks() } @@ -217,23 +275,23 @@ impl ECBlocks { /// parameters is used consecutively in the QR code version's format. #[derive(Debug, Clone, Copy)] pub struct Ecb { - count: u32, - data_codewords: u32, + count: usize, + data_codewords: usize, } impl Ecb { - pub const fn new(count: u32, data_codewords: u32) -> Self { + pub const fn new(count: usize, data_codewords: usize) -> Self { Self { count, data_codewords, } } - pub const fn count(&self) -> u32 { + pub const fn count(&self) -> usize { self.count } - pub const fn data_codewords(&self) -> u32 { + pub const fn data_codewords(&self) -> usize { self.data_codewords } } diff --git a/rxing-reader/tests/qr_decode.rs b/rxing-reader/tests/qr_decode.rs index 48726e7..2aeeb07 100644 --- a/rxing-reader/tests/qr_decode.rs +++ b/rxing-reader/tests/qr_decode.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use image::ImageReader; use rxing_reader::{decode_qr_codes_luma, rgba_to_luma}; -fn load_image_as_rgba(relative_path: &str) -> (Vec, u32, u32) { +fn load_image_as_rgba(relative_path: &str) -> (Vec, usize, usize) { let mut full = PathBuf::from(env!("CARGO_MANIFEST_DIR")); full.push(relative_path); let img = ImageReader::open(&full) @@ -14,7 +14,7 @@ fn load_image_as_rgba(relative_path: &str) -> (Vec, u32, u32) { .decode() .expect("decode image") .into_rgba8(); - let (w, h) = (img.width(), img.height()); + let (w, h) = (img.width() as usize, img.height() as usize); (img.into_raw(), w, h) } @@ -35,8 +35,8 @@ const ALL_COMBOS: [(bool, bool, bool); 8] = [ fn decode_combo( rgba: &[u8], - w: u32, - h: u32, + w: usize, + h: usize, (try_harder, try_invert, use_hybrid_binarizer): (bool, bool, bool), ) -> Option> { let luma = rgba_to_luma(rgba, w, h).expect("luma"); @@ -181,8 +181,8 @@ fn qr_sample_small_in_canvas_png_requires_try_harder() { /// pipeline with `primary_use_hybrid`; on miss, run again with the other. fn decode_with_binarizer_fallback( rgba: &[u8], - w: u32, - h: u32, + w: usize, + h: usize, try_harder: bool, try_invert: bool, primary_use_hybrid: bool, @@ -263,7 +263,7 @@ fn qr_zoo_jpg_requires_try_harder_and_try_invert() { /// Loop over `ALL_COMBOS` asserting that `rgba` decodes to `expected` iff /// `try_harder = true`. `label` is interpolated into panic messages so test /// failures point at the specific fixture/transform under test. -fn assert_requires_try_harder(rgba: &[u8], w: u32, h: u32, expected: &[u8], label: &str) { +fn assert_requires_try_harder(rgba: &[u8], w: usize, h: usize, expected: &[u8], label: &str) { for combo in ALL_COMBOS { let (try_harder, _, _) = combo; let result = decode_combo(rgba, w, h, combo); @@ -400,15 +400,14 @@ fn invert_rgba(rgba: &[u8]) -> Vec { } /// Rotate an RGBA buffer 90° clockwise. Source (x, y) maps to dst (h - 1 - y, x). -fn rotate_rgba_90_cw(rgba: &[u8], w: u32, h: u32) -> (Vec, u32, u32) { - let (w_us, h_us) = (w as usize, h as usize); +fn rotate_rgba_90_cw(rgba: &[u8], w: usize, h: usize) -> (Vec, usize, usize) { let mut out = vec![0u8; rgba.len()]; - for y in 0..h_us { - for x in 0..w_us { - let src = (y * w_us + x) * 4; - let dx = h_us - 1 - y; + for y in 0..h { + for x in 0..w { + let src = (y * w + x) * 4; + let dx = h - 1 - y; let dy = x; - let dst = (dy * h_us + dx) * 4; + let dst = (dy * h + dx) * 4; out[dst..dst + 4].copy_from_slice(&rgba[src..src + 4]); } } @@ -583,7 +582,7 @@ fn rotated_and_inverted_qr_sample_requires_try_invert() { /// by `decode_qr_codes_luma` because the multi-QR fixtures are clean upright /// composites. `count` is forwarded through the high-level decode API /// (0 = unlimited). -fn decode_all(rgba: &[u8], w: u32, h: u32, count: u32) -> Vec> { +fn decode_all(rgba: &[u8], w: usize, h: usize, count: usize) -> Vec> { let luma = rgba_to_luma(rgba, w, h).expect("luma"); decode_qr_codes_luma(&luma, w, h, false, false, true, count).unwrap_or_default() } diff --git a/rxing-wasm/src/lib.rs b/rxing-wasm/src/lib.rs index 2d04a41..f43af8c 100644 --- a/rxing-wasm/src/lib.rs +++ b/rxing-wasm/src/lib.rs @@ -12,13 +12,13 @@ use wasm_bindgen::prelude::*; #[allow(clippy::too_many_arguments)] // mirrors the wasm export signature 1:1 fn read_luma( luma: Vec, - width: u32, - height: u32, + width: usize, + height: usize, try_harder: bool, try_invert: bool, use_hybrid_binarizer: bool, binarizer_fallback: bool, - max_number_of_symbols: u32, + max_number_of_symbols: usize, ) -> Result>, JsValue> { let primary = decode_qr_codes_luma( &luma, @@ -97,6 +97,9 @@ pub fn read_qr_codes_rgba( binarizer_fallback: bool, max_number_of_symbols: u32, ) -> Result { + let width = width as usize; + let height = height as usize; + let max_number_of_symbols = max_number_of_symbols as usize; let luma = rgba_to_luma(rgba, width, height).map_err(|m| JsValue::from_str(&m))?; let payloads = read_luma( luma,