diff options
| author | Daniel Gredler <daniel.gredler@gmail.com> | 2025-11-11 05:59:52 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-11 05:59:52 +0300 |
| commit | a1ba421bb4b7aa5cd455e670e3983f809dd17112 (patch) | |
| tree | fd5e90f848ca6d2a96b99635128a2d1258b15d03 | |
| parent | 2b1499246b11415889e4a02d65023b86201a0610 (diff) | |
| download | zxing-a1ba421bb4b7aa5cd455e670e3983f809dd17112.tar.xz | |
Add GrayscaleLuminanceSource (#2013)
3 files changed, 276 insertions, 120 deletions
diff --git a/core/src/main/java/com/google/zxing/GrayscaleLuminanceSource.java b/core/src/main/java/com/google/zxing/GrayscaleLuminanceSource.java new file mode 100644 index 000000000..7941bad81 --- /dev/null +++ b/core/src/main/java/com/google/zxing/GrayscaleLuminanceSource.java @@ -0,0 +1,144 @@ +/* + * 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; + +/** + * This class is used to help decode images from files which arrive as a grayscale + * pixel array. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Betaminos + */ +public class GrayscaleLuminanceSource extends LuminanceSource { + + private final byte[] luminances; + private final int dataWidth; + private final int dataHeight; + private final int left; + private final int top; + + public GrayscaleLuminanceSource(int width, int height, byte[] pixels) { + super(width, height); + luminances = pixels; + dataWidth = width; + dataHeight = height; + left = 0; + top = 0; + } + + private GrayscaleLuminanceSource(byte[] pixels, + int dataWidth, + int dataHeight, + int left, + int top, + int width, + int height) { + super(width, height); + if (left + width > dataWidth || top + height > dataHeight) { + throw new IllegalArgumentException("Crop rectangle does not fit within image data."); + } + this.luminances = pixels; + this.dataWidth = dataWidth; + this.dataHeight = dataHeight; + 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]; + } + int offset = (y + top) * dataWidth + left; + System.arraycopy(luminances, offset, row, 0, width); + return row; + } + + @Override + public byte[] getMatrix() { + int width = getWidth(); + int height = getHeight(); + + // 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 == dataWidth && height == dataHeight) { + return luminances; + } + + int area = width * height; + byte[] matrix = new byte[area]; + int inputOffset = top * dataWidth + left; + + // If the width matches the full width of the underlying data, perform a single copy. + if (width == dataWidth) { + System.arraycopy(luminances, inputOffset, matrix, 0, area); + return matrix; + } + + // Otherwise copy one cropped row at a time. + for (int y = 0; y < height; y++) { + int outputOffset = y * width; + System.arraycopy(luminances, inputOffset, matrix, outputOffset, width); + inputOffset += dataWidth; + } + return matrix; + } + + @Override + public boolean isCropSupported() { + return true; + } + + @Override + public LuminanceSource crop(int left, int top, int width, int height) { + return new GrayscaleLuminanceSource(luminances, + dataWidth, + dataHeight, + this.left + left, + this.top + top, + width, + height); + } + + @Override + public boolean isRotateSupported() { + return true; + } + + @Override + public LuminanceSource rotateCounterClockwise() { + byte[] rotated = new byte[luminances.length]; + for (int y = 0; y < dataHeight; y++) { + for (int x = 0; x < dataWidth; x++) { + int i = (y * dataWidth) + x; + int x2 = y; + int y2 = dataWidth - 1 - x; + int j = (y2 * dataHeight) + x2; + rotated[j] = luminances[i]; + } + } + int newWidth = getHeight(); + int newHeight = getWidth(); + int newLeft = top; + int newTop = dataWidth - (left + getWidth()); + return new GrayscaleLuminanceSource(rotated, dataHeight, dataWidth, newLeft, newTop, newWidth, newHeight); + } +} diff --git a/core/src/main/java/com/google/zxing/RGBLuminanceSource.java b/core/src/main/java/com/google/zxing/RGBLuminanceSource.java index 25ba4975e..5e04e2e1a 100644 --- a/core/src/main/java/com/google/zxing/RGBLuminanceSource.java +++ b/core/src/main/java/com/google/zxing/RGBLuminanceSource.java @@ -23,31 +23,18 @@ package com.google.zxing; * @author dswitkin@google.com (Daniel Switkin) * @author Betaminos */ -public final class RGBLuminanceSource extends LuminanceSource { - - private final byte[] luminances; - private final int dataWidth; - private final int dataHeight; - private final int left; - private final int top; +public final class RGBLuminanceSource extends GrayscaleLuminanceSource { public RGBLuminanceSource(int width, int height, int[] pixels) { - super(width, height); - - dataWidth = width; - dataHeight = height; - left = 0; - top = 0; + super(width, height, toGrayscale(width, height, pixels)); + } - // In order to measure pure decoding speed, we convert the entire image to a greyscale array - // up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. - // - // Total number of pixels suffices, can ignore shape + private static byte[] toGrayscale(int width, int height, int[] pixels) { int size = width * height; if (pixels == null || pixels.length < size) { throw new IllegalArgumentException("Pixel array length is less than width * height"); } - luminances = new byte[size]; + byte[] luminances = new byte[size]; for (int offset = 0; offset < size; offset++) { int pixel = pixels[offset]; int r = (pixel >> 16) & 0xff; // red @@ -56,107 +43,6 @@ public final class RGBLuminanceSource extends LuminanceSource { // Calculate green-favouring average cheaply luminances[offset] = (byte) ((r + g2 + b) / 4); } - } - - private RGBLuminanceSource(byte[] pixels, - int dataWidth, - int dataHeight, - int left, - int top, - int width, - int height) { - super(width, height); - if (left + width > dataWidth || top + height > dataHeight) { - throw new IllegalArgumentException("Crop rectangle does not fit within image data."); - } - this.luminances = pixels; - this.dataWidth = dataWidth; - this.dataHeight = dataHeight; - 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]; - } - int offset = (y + top) * dataWidth + left; - System.arraycopy(luminances, offset, row, 0, width); - return row; - } - - @Override - public byte[] getMatrix() { - int width = getWidth(); - int height = getHeight(); - - // 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 == dataWidth && height == dataHeight) { - return luminances; - } - - int area = width * height; - byte[] matrix = new byte[area]; - int inputOffset = top * dataWidth + left; - - // If the width matches the full width of the underlying data, perform a single copy. - if (width == dataWidth) { - System.arraycopy(luminances, inputOffset, matrix, 0, area); - return matrix; - } - - // Otherwise copy one cropped row at a time. - for (int y = 0; y < height; y++) { - int outputOffset = y * width; - System.arraycopy(luminances, inputOffset, matrix, outputOffset, width); - inputOffset += dataWidth; - } - return matrix; - } - - @Override - public boolean isCropSupported() { - return true; - } - - @Override - public LuminanceSource crop(int left, int top, int width, int height) { - return new RGBLuminanceSource(luminances, - dataWidth, - dataHeight, - this.left + left, - this.top + top, - width, - height); - } - - @Override - public boolean isRotateSupported() { - return true; - } - - @Override - public LuminanceSource rotateCounterClockwise() { - byte[] rotated = new byte[luminances.length]; - for (int y = 0; y < dataHeight; y++) { - for (int x = 0; x < dataWidth; x++) { - int i = (y * dataWidth) + x; - int x2 = y; - int y2 = dataWidth - 1 - x; - int j = (y2 * dataHeight) + x2; - rotated[j] = luminances[i]; - } - } - int newWidth = getHeight(); - int newHeight = getWidth(); - int newLeft = top; - int newTop = dataWidth - (left + getWidth()); - return new RGBLuminanceSource(rotated, dataHeight, dataWidth, newLeft, newTop, newWidth, newHeight); + return luminances; } } diff --git a/core/src/test/java/com/google/zxing/GrayscaleLuminanceSourceTestCase.java b/core/src/test/java/com/google/zxing/GrayscaleLuminanceSourceTestCase.java new file mode 100644 index 000000000..bd911f6d6 --- /dev/null +++ b/core/src/test/java/com/google/zxing/GrayscaleLuminanceSourceTestCase.java @@ -0,0 +1,126 @@ +/* + * 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 GrayscaleLuminanceSource}. + */ +public final class GrayscaleLuminanceSourceTestCase extends Assert { + + private static final GrayscaleLuminanceSource SOURCE = + new GrayscaleLuminanceSource(3, 3, new byte[] { + 0x00, 0x7F, (byte) 0xFF, + 0x3F, 0x7F, 0x3F, + 0x3F, 0x7F, 0x3F}); + + @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 testRotate() { + assertTrue(SOURCE.isRotateSupported()); + assertArrayEquals(new byte[] { 0x00, 0x7F, (byte) 0xFF}, SOURCE.getRow(0, null)); + assertArrayEquals(new byte[] { 0x3F, 0x7F, 0x3F}, SOURCE.getRow(1, null)); + assertArrayEquals(new byte[] { 0x3F, 0x7F, 0x3F}, SOURCE.getRow(2, null)); + LuminanceSource rot90 = SOURCE.rotateCounterClockwise(); + assertArrayEquals(new byte[] { (byte) 0xFF, 0x3F, 0x3F}, rot90.getRow(0, null)); + assertArrayEquals(new byte[] { 0x7F, 0x7F, 0x7F}, rot90.getRow(1, null)); + assertArrayEquals(new byte[] { 0x00, 0x3F, 0x3F}, rot90.getRow(2, null)); + LuminanceSource rot180 = rot90.rotateCounterClockwise(); + assertArrayEquals(new byte[] { 0x3F, 0x7F, 0x3F}, rot180.getRow(0, null)); + assertArrayEquals(new byte[] { 0x3F, 0x7F, 0x3F}, rot180.getRow(1, null)); + assertArrayEquals(new byte[] { (byte) 0xFF, 0x7F, 0x00}, rot180.getRow(2, null)); + LuminanceSource rot270 = rot180.rotateCounterClockwise(); + assertArrayEquals(new byte[] { 0x3F, 0x3F, 0x00}, rot270.getRow(0, null)); + assertArrayEquals(new byte[] { 0x7F, 0x7F, 0x7F}, rot270.getRow(1, null)); + assertArrayEquals(new byte[] { 0x3F, 0x3F, (byte) 0xFF}, rot270.getRow(2, null)); + LuminanceSource rot360 = rot270.rotateCounterClockwise(); + assertArrayEquals(new byte[] { 0x00, 0x7F, (byte) 0xFF}, rot360.getRow(0, null)); + assertArrayEquals(new byte[] { 0x3F, 0x7F, 0x3F}, rot360.getRow(1, null)); + assertArrayEquals(new byte[] { 0x3F, 0x7F, 0x3F}, rot360.getRow(2, null)); + assertArrayEquals(SOURCE.getMatrix(), rot360.getMatrix()); + } + + @Test + public void testRotateCropped() { + assertTrue(SOURCE.isCropSupported()); + assertTrue(SOURCE.isRotateSupported()); + LuminanceSource cropped = SOURCE.crop(1, 1, 2, 2); + assertArrayEquals(new byte[] { 0x7F, 0x3F}, cropped.getRow(0, null)); + assertArrayEquals(new byte[] { 0x7F, 0x3F}, cropped.getRow(1, null)); + LuminanceSource rot90 = cropped.rotateCounterClockwise(); + assertArrayEquals(new byte[] { 0x3F, 0x3F}, rot90.getRow(0, null)); + assertArrayEquals(new byte[] { 0x7F, 0x7F}, rot90.getRow(1, null)); + LuminanceSource rot180 = rot90.rotateCounterClockwise(); + assertArrayEquals(new byte[] { 0x3F, 0x7F}, rot180.getRow(0, null)); + assertArrayEquals(new byte[] { 0x3F, 0x7F}, rot180.getRow(1, null)); + LuminanceSource rot270 = rot180.rotateCounterClockwise(); + assertArrayEquals(new byte[] { 0x7F, 0x7F}, rot270.getRow(0, null)); + assertArrayEquals(new byte[] { 0x3F, 0x3F}, rot270.getRow(1, null)); + LuminanceSource rot360 = rot270.rotateCounterClockwise(); + assertArrayEquals(new byte[] { 0x7F, 0x3F}, rot360.getRow(0, null)); + assertArrayEquals(new byte[] { 0x7F, 0x3F}, rot360.getRow(1, null)); + assertArrayEquals(cropped.getMatrix(), rot360.getMatrix()); + } + + @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()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullPixelArray() { + // Test regression: null pixel array should throw IllegalArgumentException + new RGBLuminanceSource(3, 3, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testPixelArrayTooSmall() { + // Test regression: pixel array smaller than width * height should throw IllegalArgumentException + int width = 3; + int height = 3; + int[] pixels = new int[width * height - 1]; // One pixel short + new RGBLuminanceSource(width, height, pixels); + } + +} |
