From 443d4fa5f73c3d17d0349ded10921e1b15c8ec36 Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Tue, 30 Aug 2022 17:35:04 -0500 Subject: [PATCH] PlanarYUV Tests pass --- src/BufferedImageLuminanceSource.java | 183 +++++++++++++++++++++++ src/InvertedLuminanceSourceTestCase.java | 39 +++++ src/PlanarYUVLuminanceSourceTestCase.rs | 138 +++++++++++++++++ src/RGBLuminanceSourceTestCase.java | 63 ++++++++ src/lib.rs | 87 ++++++----- 5 files changed, 470 insertions(+), 40 deletions(-) create mode 100644 src/BufferedImageLuminanceSource.java create mode 100644 src/InvertedLuminanceSourceTestCase.java create mode 100644 src/PlanarYUVLuminanceSourceTestCase.rs create mode 100644 src/RGBLuminanceSourceTestCase.java diff --git a/src/BufferedImageLuminanceSource.java b/src/BufferedImageLuminanceSource.java new file mode 100644 index 0000000..a777be5 --- /dev/null +++ b/src/BufferedImageLuminanceSource.java @@ -0,0 +1,183 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; + +/** + * This LuminanceSource implementation is meant for J2SE clients and our blackbox unit tests. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + * @author code@elektrowolle.de (Wolfgang Jung) + */ +public final class BufferedImageLuminanceSource extends LuminanceSource { + + private static final double MINUS_45_IN_RADIANS = -0.7853981633974483; // Math.toRadians(-45.0) + + private final BufferedImage image; + private final int left; + private final int top; + + public BufferedImageLuminanceSource(BufferedImage image) { + this(image, 0, 0, image.getWidth(), image.getHeight()); + } + + public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height) { + super(width, height); + + if (image.getType() == BufferedImage.TYPE_BYTE_GRAY) { + this.image = image; + } else { + int sourceWidth = image.getWidth(); + int sourceHeight = image.getHeight(); + if (left + width > sourceWidth || top + height > sourceHeight) { + throw new IllegalArgumentException("Crop rectangle does not fit within image data."); + } + + this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY); + + WritableRaster raster = this.image.getRaster(); + int[] buffer = new int[width]; + for (int y = top; y < top + height; y++) { + image.getRGB(left, y, width, 1, buffer, 0, sourceWidth); + for (int x = 0; x < width; x++) { + int pixel = buffer[x]; + + // The color of fully-transparent pixels is irrelevant. They are often, technically, fully-transparent + // black (0 alpha, and then 0 RGB). They are often used, of course as the "white" area in a + // barcode image. Force any such pixel to be white: + if ((pixel & 0xFF000000) == 0) { + // white, so we know its luminance is 255 + buffer[x] = 0xFF; + } else { + // .299R + 0.587G + 0.114B (YUV/YIQ for PAL and NTSC), + // (306*R) >> 10 is approximately equal to R*0.299, and so on. + // 0x200 >> 10 is 0.5, it implements rounding. + buffer[x] = + (306 * ((pixel >> 16) & 0xFF) + + 601 * ((pixel >> 8) & 0xFF) + + 117 * (pixel & 0xFF) + + 0x200) >> 10; + } + } + raster.setPixels(left, y, width, 1, buffer); + } + + } + this.left = left; + this.top = top; + } + + @Override + public byte[] getRow(int y, byte[] row) { + if (y < 0 || y >= getHeight()) { + throw new IllegalArgumentException("Requested row is outside the image: " + y); + } + int width = getWidth(); + if (row == null || row.length < width) { + row = new byte[width]; + } + // The underlying raster of image consists of bytes with the luminance values + image.getRaster().getDataElements(left, top + y, width, 1, row); + return row; + } + + @Override + public byte[] getMatrix() { + int width = getWidth(); + int height = getHeight(); + int area = width * height; + byte[] matrix = new byte[area]; + // The underlying raster of image consists of area bytes with the luminance values + image.getRaster().getDataElements(left, top, width, height, matrix); + return matrix; + } + + @Override + public boolean isCropSupported() { + return true; + } + + @Override + public LuminanceSource crop(int left, int top, int width, int height) { + return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height); + } + + /** + * This is always true, since the image is a gray-scale image. + * + * @return true + */ + @Override + public boolean isRotateSupported() { + return true; + } + + @Override + public LuminanceSource rotateCounterClockwise() { + int sourceWidth = image.getWidth(); + int sourceHeight = image.getHeight(); + + // Rotate 90 degrees counterclockwise. + AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth); + + // Note width/height are flipped since we are rotating 90 degrees. + BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY); + + // Draw the original image into rotated, via transformation + Graphics2D g = rotatedImage.createGraphics(); + g.drawImage(image, transform, null); + g.dispose(); + + // Maintain the cropped region, but rotate it too. + int width = getWidth(); + return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width); + } + + @Override + public LuminanceSource rotateCounterClockwise45() { + int width = getWidth(); + int height = getHeight(); + + int oldCenterX = left + width / 2; + int oldCenterY = top + height / 2; + + // Rotate 45 degrees counterclockwise. + AffineTransform transform = AffineTransform.getRotateInstance(MINUS_45_IN_RADIANS, oldCenterX, oldCenterY); + + int sourceDimension = Math.max(image.getWidth(), image.getHeight()); + BufferedImage rotatedImage = new BufferedImage(sourceDimension, sourceDimension, BufferedImage.TYPE_BYTE_GRAY); + + // Draw the original image into rotated, via transformation + Graphics2D g = rotatedImage.createGraphics(); + g.drawImage(image, transform, null); + g.dispose(); + + int halfDimension = Math.max(width, height) / 2; + int newLeft = Math.max(0, oldCenterX - halfDimension); + int newTop = Math.max(0, oldCenterY - halfDimension); + int newRight = Math.min(sourceDimension - 1, oldCenterX + halfDimension); + int newBottom = Math.min(sourceDimension - 1, oldCenterY + halfDimension); + + return new BufferedImageLuminanceSource(rotatedImage, newLeft, newTop, newRight - newLeft, newBottom - newTop); + } + +} diff --git a/src/InvertedLuminanceSourceTestCase.java b/src/InvertedLuminanceSourceTestCase.java new file mode 100644 index 0000000..932a7e2 --- /dev/null +++ b/src/InvertedLuminanceSourceTestCase.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import org.junit.Assert; +import org.junit.Test; + +import java.awt.image.BufferedImage; + +/** + * Tests {@link InvertedLuminanceSource}. + */ +public final class InvertedLuminanceSourceTestCase extends Assert { + + @Test + public void testInverted() { + BufferedImage image = new BufferedImage(2, 1, BufferedImage.TYPE_INT_RGB); + image.setRGB(0, 0, 0xFFFFFF); + LuminanceSource source = new BufferedImageLuminanceSource(image); + assertArrayEquals(new byte[] { (byte) 0xFF, 0 }, source.getRow(0, null)); + LuminanceSource inverted = new InvertedLuminanceSource(source); + assertArrayEquals(new byte[] { 0, (byte) 0xFF }, inverted.getRow(0, null)); + } + +} diff --git a/src/PlanarYUVLuminanceSourceTestCase.rs b/src/PlanarYUVLuminanceSourceTestCase.rs new file mode 100644 index 0000000..d5a96ee --- /dev/null +++ b/src/PlanarYUVLuminanceSourceTestCase.rs @@ -0,0 +1,138 @@ +/* + * Copyright 2014 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +// */ + +// package com.google.zxing; + +// import org.junit.Assert; +// import org.junit.Test; + +// /** +// * Tests {@link PlanarYUVLuminanceSource}. +// */ +// public final class PlanarYUVLuminanceSourceTestCase extends Assert { + +use crate::{LuminanceSource, PlanarYUVLuminanceSource}; + +static YUV: [u8; 36] = [ + // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 0, -1, -1, -2, -3, -5, -8, -13, -21, -34, -55, -89, + 128, 129, 129, 130, 131, 133, 136, 141, 149, 162, 183, 217, 128, 127, 127, 126, 125, 123, 120, + 115, 107, 94, 73, 39, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +]; +static COLS: usize = 6; +static ROWS: usize = 4; +static Y: [u8; 24] = [ + 128, 129, 129, 130, 131, 133, 136, 141, 149, 162, 183, 217, 128, 127, 127, 126, 125, 123, 120, + 115, 107, 94, 73, 39, +]; + +#[test] +fn test_no_crop() { + let source = PlanarYUVLuminanceSource::new_with_all( + YUV.to_vec(), + COLS, + ROWS, + 0, + 0, + COLS, + ROWS, + false, + false, + ) + .unwrap(); + assert_equals(&Y, 0, &source.getMatrix(), 0, Y.len()); + for r in 0..ROWS { + // for (int r = 0; r < ROWS; r++) { + assert_equals(&Y, r * COLS, &source.getRow(r, &vec![0; 0]), 0, COLS); + } +} + +#[test] +fn test_crop() { + let source = PlanarYUVLuminanceSource::new_with_all( + YUV.to_vec(), + COLS, + ROWS, + 1, + 1, + COLS - 2, + ROWS - 2, + false, + false, + ) + .unwrap(); + assert!(source.isCropSupported()); + let cropMatrix = source.getMatrix(); + for r in 0..ROWS-2 { + // for (int r = 0; r < ROWS - 2; r++) { + assert_equals( + &Y, + (r + 1) * COLS + 1, + &cropMatrix, + r * (COLS - 2), + COLS - 2, + ); + } + for r in 0..ROWS-2 { + // for (int r = 0; r < ROWS - 2; r++) { + assert_equals( + &Y, + (r + 1) * COLS + 1, + &source.getRow(r, &vec![0; 0]), + 0, + COLS - 2, + ); + } +} + +#[test] +fn test_thumbnail() { + let source = PlanarYUVLuminanceSource::new_with_all( + YUV.to_vec(), + COLS, + ROWS, + 0, + 0, + COLS, + ROWS, + false, + false, + ) + .unwrap(); + let c_vec = vec![ + ((0x00FF0000 << 8) + 0x00808080) as u8, + ((0x00FF0000 << 8) + 0x00818181) as u8, + ((0x00FF0000 << 8) + 0x00838383) as u8, + ((0x00FF0000 << 8) + 0x00808080) as u8, + ((0x00FF0000 << 8) + 0x007F7F7F) as u8, + ((0x00FF0000 << 8) + 0x007D7D7D) as u8, + ]; + assert_eq!(c_vec, source.renderThumbnail()); +} + +fn assert_equals( + expected: &[u8], + expectedFrom: usize, + actual: &[u8], + actualFrom: usize, + length: usize, +) { + for i in 0..length { + // for (int i = 0; i < length; i++) { + assert_eq!(expected[expectedFrom + i], actual[actualFrom + i]); + } +} + +// } diff --git a/src/RGBLuminanceSourceTestCase.java b/src/RGBLuminanceSourceTestCase.java new file mode 100644 index 0000000..97f9794 --- /dev/null +++ b/src/RGBLuminanceSourceTestCase.java @@ -0,0 +1,63 @@ +/* + * Copyright 2014 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests {@link RGBLuminanceSource}. + */ +public final class RGBLuminanceSourceTestCase extends Assert { + + private static final RGBLuminanceSource SOURCE = new RGBLuminanceSource(3, 3, new int[] { + 0x000000, 0x7F7F7F, 0xFFFFFF, + 0xFF0000, 0x00FF00, 0x0000FF, + 0x0000FF, 0x00FF00, 0xFF0000}); + + @Test + public void testCrop() { + assertTrue(SOURCE.isCropSupported()); + LuminanceSource cropped = SOURCE.crop(1, 1, 1, 1); + assertEquals(1, cropped.getHeight()); + assertEquals(1, cropped.getWidth()); + assertArrayEquals(new byte[] { 0x7F }, cropped.getRow(0, null)); + } + + @Test + public void testMatrix() { + assertArrayEquals(new byte[] { 0x00, 0x7F, (byte) 0xFF, 0x3F, 0x7F, 0x3F, 0x3F, 0x7F, 0x3F }, + SOURCE.getMatrix()); + LuminanceSource croppedFullWidth = SOURCE.crop(0, 1, 3, 2); + assertArrayEquals(new byte[] { 0x3F, 0x7F, 0x3F, 0x3F, 0x7F, 0x3F }, + croppedFullWidth.getMatrix()); + LuminanceSource croppedCorner = SOURCE.crop(1, 1, 2, 2); + assertArrayEquals(new byte[] { 0x7F, 0x3F, 0x7F, 0x3F }, + croppedCorner.getMatrix()); + } + + @Test + public void testGetRow() { + assertArrayEquals(new byte[] { 0x3F, 0x7F, 0x3F }, SOURCE.getRow(2, new byte[3])); + } + + @Test + public void testToString() { + assertEquals("#+ \n#+#\n#+#\n", SOURCE.toString()); + } + +} diff --git a/src/lib.rs b/src/lib.rs index ce26982..8336507 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ use std::fmt; use std::hash::Hash; use std::time::{SystemTime, UNIX_EPOCH}; +#[cfg(test)] +mod PlanarYUVLuminanceSourceTestCase; /* * Copyright 2007 ZXing authors * @@ -1561,9 +1563,9 @@ const THUMBNAIL_SCALE_FACTOR: usize = 2; */ #[derive(Debug, Clone)] pub struct PlanarYUVLuminanceSource { - yuvData: Vec, - dataWidth: usize, - dataHeight: usize, + yuv_data: Vec, + data_width: usize, + data_height: usize, left: usize, top: usize, width: usize, @@ -1573,26 +1575,26 @@ pub struct PlanarYUVLuminanceSource { impl PlanarYUVLuminanceSource { pub fn new_with_all( - yuvData: Vec, - dataWidth: usize, - dataHeight: usize, + yuv_data: Vec, + data_width: usize, + data_height: usize, left: usize, top: usize, width: usize, height: usize, - reverseHorizontal: bool, + reverse_horizontal: bool, inverted: bool, ) -> Result { - if left + width > dataWidth || top + height > dataHeight { + if left + width > data_width || top + height > data_height { return Err(Exceptions::IllegalArgumentException( "Crop rectangle does not fit within image data.".to_owned(), )); } let mut new_s: Self = Self { - yuvData, - dataWidth, - dataHeight, + yuv_data, + data_width, + data_height, left, top, width, @@ -1600,7 +1602,7 @@ impl PlanarYUVLuminanceSource { invert: inverted, }; - if reverseHorizontal { + if reverse_horizontal { new_s.reverseHorizontal(width, height); } @@ -1610,19 +1612,19 @@ impl PlanarYUVLuminanceSource { pub fn renderThumbnail(&self) -> Vec { let width = self.getWidth() / THUMBNAIL_SCALE_FACTOR; let height = self.getHeight() / THUMBNAIL_SCALE_FACTOR; - let mut pixels = Vec::with_capacity(width * height); - let yuv: Vec = Vec::new(); - let mut inputOffset = self.top * self.dataWidth + self.left; + let mut pixels = vec![0;width * height]; + let yuv = &self.yuv_data; + let mut input_offset = self.top * self.data_width + self.left; for y in 0..height { //for (int y = 0; y < height; y++) { - let outputOffset = y * width; + let output_offset = y * width; for x in 0..width { //for (int x = 0; x < width; x++) { - let grey = yuv[inputOffset + x * THUMBNAIL_SCALE_FACTOR] & 0xff; - pixels[outputOffset + x] = (0xFF000000 | (grey as u32 * 0x00010101)) as u8; + let grey = yuv[input_offset + x * THUMBNAIL_SCALE_FACTOR] & 0xff; + pixels[output_offset + x] = (0xFF000000 | (grey as u32 * 0x00010101)) as u8; } - inputOffset += self.dataWidth * THUMBNAIL_SCALE_FACTOR; + input_offset += self.data_width * THUMBNAIL_SCALE_FACTOR; } return pixels; } @@ -1643,18 +1645,18 @@ impl PlanarYUVLuminanceSource { fn reverseHorizontal(&mut self, width: usize, height: usize) { //let mut yuvData = self.yuvData; - let mut rowStart = self.top * self.dataWidth + self.left; + let mut rowStart = self.top * self.data_width + self.left; for y in 0..height { let middle = rowStart + width / 2; let mut x2 = rowStart + width - 1; for x1 in rowStart..middle { //for (int x1 = rowStart, x2 = rowStart + width - 1; x1 < middle; x1++, x2--) { - let temp = self.yuvData[x1]; - self.yuvData[x1] = self.yuvData[x2]; - self.yuvData[x2] = temp; + let temp = self.yuv_data[x1]; + self.yuv_data[x1] = self.yuv_data[x2]; + self.yuv_data[x2] = temp; x2 -= 1; } - rowStart += self.dataWidth; + rowStart += self.data_width; } //self.yuvData = yuvData; /*for (int y = 0, rowStart = top * dataWidth + left; y < height; y++, rowStart += dataWidth) { @@ -1670,16 +1672,21 @@ impl PlanarYUVLuminanceSource { impl LuminanceSource for PlanarYUVLuminanceSource { fn getRow(&self, y: usize, row: &Vec) -> Vec { - let mut row = row.to_vec(); if y < 0 || y >= self.getHeight() { //throw new IllegalArgumentException("Requested row is outside the image: " + y); panic!("Requested row is outside the image: {}", y); } let width = self.getWidth(); - let offset = (y + self.top) * self.dataWidth + self.left; + let offset = (y + self.top) * self.data_width + self.left; - row.clone_from_slice(&self.yuvData[offset..width]); + let mut row = if row.len() >= width{ + row.to_vec()} + else{ + vec![0;width] + }; + + row[..width].clone_from_slice(&self.yuv_data[offset..width+offset]); //System.arraycopy(yuvData, offset, row, 0, width); if self.invert { row = self.invert_block_of_bytes(row); @@ -1693,8 +1700,8 @@ impl LuminanceSource for PlanarYUVLuminanceSource { // If the caller asks for the entire underlying image, save the copy and give them the // original data. The docs specifically warn that result.length must be ignored. - if width == self.dataWidth && height == self.dataHeight { - let mut v = self.yuvData.clone(); + if width == self.data_width && height == self.data_height { + let mut v = self.yuv_data.clone(); if self.invert { v = self.invert_block_of_bytes(v); } @@ -1702,12 +1709,12 @@ impl LuminanceSource for PlanarYUVLuminanceSource { } let area = width * height; - let mut matrix = Vec::with_capacity(area); - let mut inputOffset = self.top * self.dataWidth + self.left; + let mut matrix = vec![0;area]; + let mut inputOffset = self.top * self.data_width + self.left; // If the width matches the full width of the underlying data, perform a single copy. - if width == self.dataWidth { - matrix[0..area].clone_from_slice(&self.yuvData[inputOffset..area]); + if width == self.data_width { + matrix[0..area].clone_from_slice(&self.yuv_data[inputOffset..area]); //System.arraycopy(yuvData, inputOffset, matrix, 0, area); if self.invert { matrix = self.invert_block_of_bytes(matrix); @@ -1719,9 +1726,9 @@ impl LuminanceSource for PlanarYUVLuminanceSource { for y in 0..height { //for (int y = 0; y < height; y++) { let outputOffset = y * width; - matrix[outputOffset..width].clone_from_slice(&self.yuvData[inputOffset..width]); + matrix[outputOffset..outputOffset+width].clone_from_slice(&self.yuv_data[inputOffset..inputOffset+width]); //System.arraycopy(yuvData, inputOffset, matrix, outputOffset, width); - inputOffset += self.dataWidth; + inputOffset += self.data_width; } if self.invert { @@ -1751,9 +1758,9 @@ impl LuminanceSource for PlanarYUVLuminanceSource { height: usize, ) -> Result, Exceptions> { match PlanarYUVLuminanceSource::new_with_all( - self.yuvData.clone(), - self.dataWidth, - self.dataHeight, + self.yuv_data.clone(), + self.data_width, + self.data_height, self.left + left, self.top + top, width, @@ -1844,7 +1851,7 @@ impl LuminanceSource for RGBLuminanceSource { } let area = width * height; - let mut matrix = Vec::with_capacity(area); + let mut matrix = vec![0;area]; let mut inputOffset = self.top * self.dataWidth + self.left; // If the width matches the full width of the underlying data, perform a single copy. @@ -1924,7 +1931,7 @@ impl RGBLuminanceSource { // // Total number of pixels suffices, can ignore shape let size = width * height; - let mut luminances: Vec = Vec::with_capacity(size); + let mut luminances: Vec = vec![0;size]; for offset in 0..size { //for (int offset = 0; offset < size; offset++) { let pixel = pixels[offset];