copy over tests

This commit is contained in:
Henry Schimke
2022-08-30 10:05:57 -05:00
parent 26f3b18d5f
commit 9831fa5e5a
4 changed files with 629 additions and 0 deletions

View File

@@ -0,0 +1,357 @@
/*
* Copyright 2008 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.common;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.BufferedImageLuminanceSource;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.ReaderException;
import com.google.zxing.RXingResult;
import com.google.zxing.RXingResultMetadataType;
import org.junit.Assert;
import org.junit.Test;
import javax.imageio.ImageIO;
import java.awt.Graphics;
import java.awt.geom.AffineTransform;
import java.awt.geom.RectangularShape;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
/**
* @author Sean Owen
* @author dswitkin@google.com (Daniel Switkin)
*/
public abstract class AbstractBlackBoxTestCase extends Assert {
private static final Logger log = Logger.getLogger(AbstractBlackBoxTestCase.class.getSimpleName());
private final Path testBase;
private final Reader barcodeReader;
private final BarcodeFormat expectedFormat;
private final List<TestRXingResult> testRXingResults;
private final EnumMap<DecodeHintType,Object> hints = new EnumMap<>(DecodeHintType.class);
public static Path buildTestBase(String testBasePathSuffix) {
// A little workaround to prevent aggravation in my IDE
Path testBase = Paths.get(testBasePathSuffix);
if (!Files.exists(testBase)) {
// try starting with 'core' since the test base is often given as the project root
testBase = Paths.get("core").resolve(testBasePathSuffix);
}
return testBase;
}
protected AbstractBlackBoxTestCase(String testBasePathSuffix,
Reader barcodeReader,
BarcodeFormat expectedFormat) {
this.testBase = buildTestBase(testBasePathSuffix);
this.barcodeReader = barcodeReader;
this.expectedFormat = expectedFormat;
testRXingResults = new ArrayList<>();
System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s: %5$s%6$s%n");
}
protected final Path getTestBase() {
return testBase;
}
protected final void addTest(int mustPassCount, int tryHarderCount, float rotation) {
addTest(mustPassCount, tryHarderCount, 0, 0, rotation);
}
protected void addHint(DecodeHintType hint) {
hints.put(hint, Boolean.TRUE);
}
/**
* Adds a new test for the current directory of images.
*
* @param mustPassCount The number of images which must decode for the test to pass.
* @param tryHarderCount The number of images which must pass using the try harder flag.
* @param maxMisreads Maximum number of images which can fail due to successfully reading the wrong contents
* @param maxTryHarderMisreads Maximum number of images which can fail due to successfully
* reading the wrong contents using the try harder flag
* @param rotation The rotation in degrees clockwise to use for this test.
*/
protected final void addTest(int mustPassCount,
int tryHarderCount,
int maxMisreads,
int maxTryHarderMisreads,
float rotation) {
testRXingResults.add(new TestRXingResult(mustPassCount, tryHarderCount, maxMisreads, maxTryHarderMisreads, rotation));
}
protected final List<Path> getImageFiles() throws IOException {
assertTrue("Please download and install test images, and run from the 'core' directory", Files.exists(testBase));
List<Path> paths = new ArrayList<>();
try (DirectoryStream<Path> pathIt = Files.newDirectoryStream(testBase, "*.{jpg,jpeg,gif,png,JPG,JPEG,GIF,PNG}")) {
for (Path path : pathIt) {
paths.add(path);
}
}
return paths;
}
final Reader getReader() {
return barcodeReader;
}
@Test
public void testBlackBox() throws IOException {
assertFalse(testRXingResults.isEmpty());
List<Path> imageFiles = getImageFiles();
int testCount = testRXingResults.size();
int[] passedCounts = new int[testCount];
int[] misreadCounts = new int[testCount];
int[] tryHarderCounts = new int[testCount];
int[] tryHarderMisreadCounts = new int[testCount];
for (Path testImage : imageFiles) {
log.info(String.format("Starting %s", testImage));
BufferedImage image = ImageIO.read(testImage.toFile());
String testImageFileName = testImage.getFileName().toString();
String fileBaseName = testImageFileName.substring(0, testImageFileName.indexOf('.'));
Path expectedTextFile = testBase.resolve(fileBaseName + ".txt");
String expectedText;
if (Files.exists(expectedTextFile)) {
expectedText = readFileAsString(expectedTextFile, StandardCharsets.UTF_8);
} else {
expectedTextFile = testBase.resolve(fileBaseName + ".bin");
assertTrue(Files.exists(expectedTextFile));
expectedText = readFileAsString(expectedTextFile, StandardCharsets.ISO_8859_1);
}
Path expectedMetadataFile = testBase.resolve(fileBaseName + ".metadata.txt");
Properties expectedMetadata = new Properties();
if (Files.exists(expectedMetadataFile)) {
try (BufferedReader reader = Files.newBufferedReader(expectedMetadataFile, StandardCharsets.UTF_8)) {
expectedMetadata.load(reader);
}
}
for (int x = 0; x < testCount; x++) {
float rotation = testRXingResults.get(x).getRotation();
BufferedImage rotatedImage = rotateImage(image, rotation);
LuminanceSource source = new BufferedImageLuminanceSource(rotatedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
if (decode(bitmap, rotation, expectedText, expectedMetadata, false)) {
passedCounts[x]++;
} else {
misreadCounts[x]++;
}
} catch (ReaderException ignored) {
log.fine(String.format("could not read at rotation %f", rotation));
}
try {
if (decode(bitmap, rotation, expectedText, expectedMetadata, true)) {
tryHarderCounts[x]++;
} else {
tryHarderMisreadCounts[x]++;
}
} catch (ReaderException ignored) {
log.fine(String.format("could not read at rotation %f w/TH", rotation));
}
}
}
// Print the results of all tests first
int totalFound = 0;
int totalMustPass = 0;
int totalMisread = 0;
int totalMaxMisread = 0;
for (int x = 0; x < testRXingResults.size(); x++) {
TestRXingResult testRXingResult = testRXingResults.get(x);
log.info(String.format("Rotation %d degrees:", (int) testRXingResult.getRotation()));
log.info(String.format(" %d of %d images passed (%d required)",
passedCounts[x], imageFiles.size(), testRXingResult.getMustPassCount()));
int failed = imageFiles.size() - passedCounts[x];
log.info(String.format(" %d failed due to misreads, %d not detected",
misreadCounts[x], failed - misreadCounts[x]));
log.info(String.format(" %d of %d images passed with try harder (%d required)",
tryHarderCounts[x], imageFiles.size(), testRXingResult.getTryHarderCount()));
failed = imageFiles.size() - tryHarderCounts[x];
log.info(String.format(" %d failed due to misreads, %d not detected",
tryHarderMisreadCounts[x], failed - tryHarderMisreadCounts[x]));
totalFound += passedCounts[x] + tryHarderCounts[x];
totalMustPass += testRXingResult.getMustPassCount() + testRXingResult.getTryHarderCount();
totalMisread += misreadCounts[x] + tryHarderMisreadCounts[x];
totalMaxMisread += testRXingResult.getMaxMisreads() + testRXingResult.getMaxTryHarderMisreads();
}
int totalTests = imageFiles.size() * testCount * 2;
log.info(String.format("Decoded %d images out of %d (%d%%, %d required)",
totalFound, totalTests, totalFound * 100 / totalTests, totalMustPass));
if (totalFound > totalMustPass) {
log.warning(String.format("+++ Test too lax by %d images", totalFound - totalMustPass));
} else if (totalFound < totalMustPass) {
log.warning(String.format("--- Test failed by %d images", totalMustPass - totalFound));
}
if (totalMisread < totalMaxMisread) {
log.warning(String.format("+++ Test expects too many misreads by %d images", totalMaxMisread - totalMisread));
} else if (totalMisread > totalMaxMisread) {
log.warning(String.format("--- Test had too many misreads by %d images", totalMisread - totalMaxMisread));
}
// Then run through again and assert if any failed
for (int x = 0; x < testCount; x++) {
TestRXingResult testRXingResult = testRXingResults.get(x);
String label = "Rotation " + testRXingResult.getRotation() + " degrees: Too many images failed";
assertTrue(label,
passedCounts[x] >= testRXingResult.getMustPassCount());
assertTrue("Try harder, " + label,
tryHarderCounts[x] >= testRXingResult.getTryHarderCount());
label = "Rotation " + testRXingResult.getRotation() + " degrees: Too many images misread";
assertTrue(label,
misreadCounts[x] <= testRXingResult.getMaxMisreads());
assertTrue("Try harder, " + label,
tryHarderMisreadCounts[x] <= testRXingResult.getMaxTryHarderMisreads());
}
}
private boolean decode(BinaryBitmap source,
float rotation,
String expectedText,
Map<?,?> expectedMetadata,
boolean tryHarder) throws ReaderException {
String suffix = String.format(" (%srotation: %d)", tryHarder ? "try harder, " : "", (int) rotation);
Map<DecodeHintType,Object> hints = this.hints.clone();
if (tryHarder) {
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
}
// Try in 'pure' mode mostly to exercise PURE_BARCODE code paths for exceptions;
// not expected to pass, generally
RXingResult result = null;
try {
Map<DecodeHintType,Object> pureHints = new EnumMap<>(hints);
pureHints.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE);
result = barcodeReader.decode(source, pureHints);
} catch (ReaderException re) {
// continue
}
if (result == null) {
result = barcodeReader.decode(source, hints);
}
if (expectedFormat != result.getBarcodeFormat()) {
log.info(String.format("Format mismatch: expected '%s' but got '%s'%s",
expectedFormat, result.getBarcodeFormat(), suffix));
return false;
}
String resultText = result.getText();
if (!expectedText.equals(resultText)) {
log.info(String.format("Content mismatch: expected '%s' but got '%s'%s",
expectedText, resultText, suffix));
return false;
}
Map<RXingResultMetadataType,?> resultMetadata = result.getRXingResultMetadata();
for (Map.Entry<?,?> metadatum : expectedMetadata.entrySet()) {
RXingResultMetadataType key = RXingResultMetadataType.valueOf(metadatum.getKey().toString());
Object expectedValue = metadatum.getValue();
Object actualValue = resultMetadata == null ? null : resultMetadata.get(key);
if (!expectedValue.equals(actualValue)) {
log.info(String.format("Metadata mismatch for key '%s': expected '%s' but got '%s'",
key, expectedValue, actualValue));
return false;
}
}
return true;
}
protected static String readFileAsString(Path file, Charset charset) throws IOException {
String stringContents = new String(Files.readAllBytes(file), charset);
if (stringContents.endsWith("\n")) {
log.info("String contents of file " + file + " end with a newline. " +
"This may not be intended and cause a test failure");
}
return stringContents;
}
protected static BufferedImage rotateImage(BufferedImage original, float degrees) {
if (degrees == 0.0f) {
return original;
}
switch (original.getType()) {
case BufferedImage.TYPE_BYTE_INDEXED:
case BufferedImage.TYPE_BYTE_BINARY:
BufferedImage argb = new BufferedImage(original.getWidth(),
original.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics g = argb.createGraphics();
g.drawImage(original, 0, 0, null);
g.dispose();
original = argb;
break;
}
double radians = Math.toRadians(degrees);
// Transform simply to find out the new bounding box (don't actually run the image through it)
AffineTransform at = new AffineTransform();
at.rotate(radians, original.getWidth() / 2.0, original.getHeight() / 2.0);
BufferedImageOp op = new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC);
RectangularShape r = op.getBounds2D(original);
int width = (int) Math.ceil(r.getWidth());
int height = (int) Math.ceil(r.getHeight());
// Real transform, now that we know the size of the new image and how to translate after we rotate
// to keep it centered
at = new AffineTransform();
at.rotate(radians, width / 2.0, height / 2.0);
at.translate((width - original.getWidth()) / 2.0,
(height - original.getHeight()) / 2.0);
op = new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC);
return op.filter(original, new BufferedImage(width, height, original.getType()));
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2008 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.common;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.BufferedImageLuminanceSource;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.RXingResult;
import org.junit.Test;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
* This abstract class looks for negative results, i.e. it only allows a certain number of false
* positives in images which should not decode. This helps ensure that we are not too lenient.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
public abstract class AbstractNegativeBlackBoxTestCase extends AbstractBlackBoxTestCase {
private static final Logger log = Logger.getLogger(AbstractNegativeBlackBoxTestCase.class.getSimpleName());
private final List<TestRXingResult> testRXingResults;
private static final class TestRXingResult {
private final int falsePositivesAllowed;
private final float rotation;
TestRXingResult(int falsePositivesAllowed, float rotation) {
this.falsePositivesAllowed = falsePositivesAllowed;
this.rotation = rotation;
}
int getFalsePositivesAllowed() {
return falsePositivesAllowed;
}
float getRotation() {
return rotation;
}
}
// Use the multiformat reader to evaluate all decoders in the system.
protected AbstractNegativeBlackBoxTestCase(String testBasePathSuffix) {
super(testBasePathSuffix, new MultiFormatReader(), null);
testRXingResults = new ArrayList<>();
}
protected final void addTest(int falsePositivesAllowed, float rotation) {
testRXingResults.add(new TestRXingResult(falsePositivesAllowed, rotation));
}
@Override
@Test
public void testBlackBox() throws IOException {
assertFalse(testRXingResults.isEmpty());
List<Path> imageFiles = getImageFiles();
int[] falsePositives = new int[testRXingResults.size()];
for (Path testImage : imageFiles) {
log.info(String.format("Starting %s", testImage));
BufferedImage image = ImageIO.read(testImage.toFile());
if (image == null) {
throw new IOException("Could not read image: " + testImage);
}
for (int x = 0; x < testRXingResults.size(); x++) {
TestRXingResult testRXingResult = testRXingResults.get(x);
if (!checkForFalsePositives(image, testRXingResult.getRotation())) {
falsePositives[x]++;
}
}
}
int totalFalsePositives = 0;
int totalAllowed = 0;
for (int x = 0; x < testRXingResults.size(); x++) {
TestRXingResult testRXingResult = testRXingResults.get(x);
totalFalsePositives += falsePositives[x];
totalAllowed += testRXingResult.getFalsePositivesAllowed();
}
if (totalFalsePositives < totalAllowed) {
log.warning(String.format("+++ Test too lax by %d images", totalAllowed - totalFalsePositives));
} else if (totalFalsePositives > totalAllowed) {
log.warning(String.format("--- Test failed by %d images", totalFalsePositives - totalAllowed));
}
for (int x = 0; x < testRXingResults.size(); x++) {
TestRXingResult testRXingResult = testRXingResults.get(x);
log.info(String.format("Rotation %d degrees: %d of %d images were false positives (%d allowed)",
(int) testRXingResult.getRotation(), falsePositives[x], imageFiles.size(),
testRXingResult.getFalsePositivesAllowed()));
assertTrue("Rotation " + testRXingResult.getRotation() + " degrees: Too many false positives found",
falsePositives[x] <= testRXingResult.getFalsePositivesAllowed());
}
}
/**
* Make sure ZXing does NOT find a barcode in the image.
*
* @param image The image to test
* @param rotationInDegrees The amount of rotation to apply
* @return true if nothing found, false if a non-existent barcode was detected
*/
private boolean checkForFalsePositives(BufferedImage image, float rotationInDegrees) {
BufferedImage rotatedImage = rotateImage(image, rotationInDegrees);
LuminanceSource source = new BufferedImageLuminanceSource(rotatedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
RXingResult result;
try {
result = getReader().decode(bitmap);
log.info(String.format("Found false positive: '%s' with format '%s' (rotation: %d)",
result.getText(), result.getBarcodeFormat(), (int) rotationInDegrees));
return false;
} catch (ReaderException re) {
// continue
}
// Try "try harder" getMode
Map<DecodeHintType,Object> hints = new EnumMap<>(DecodeHintType.class);
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
try {
result = getReader().decode(bitmap, hints);
log.info(String.format("Try harder found false positive: '%s' with format '%s' (rotation: %d)",
result.getText(), result.getBarcodeFormat(), (int) rotationInDegrees));
return false;
} catch (ReaderException re) {
// continue
}
return true;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2007 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.common;
import org.junit.Assert;
import org.junit.Test;
/**
* @author Sean Owen
*/
public final class BitSourceTestCase extends Assert {
@Test
public void testSource() {
byte[] bytes = {(byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5};
BitSource source = new BitSource(bytes);
assertEquals(40, source.available());
assertEquals(0, source.readBits(1));
assertEquals(39, source.available());
assertEquals(0, source.readBits(6));
assertEquals(33, source.available());
assertEquals(1, source.readBits(1));
assertEquals(32, source.available());
assertEquals(2, source.readBits(8));
assertEquals(24, source.available());
assertEquals(12, source.readBits(10));
assertEquals(14, source.available());
assertEquals(16, source.readBits(8));
assertEquals(6, source.available());
assertEquals(5, source.readBits(6));
assertEquals(0, source.available());
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2007 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.common;
import org.junit.Assert;
import org.junit.Test;
/**
* @author Sean Owen
*/
public final class PerspectiveTransformTestCase extends Assert {
private static final float EPSILON = 1.0E-4f;
@Test
public void testSquareToQuadrilateral() {
PerspectiveTransform pt = PerspectiveTransform.squareToQuadrilateral(
2.0f, 3.0f, 10.0f, 4.0f, 16.0f, 15.0f, 4.0f, 9.0f);
assertPointEquals(2.0f, 3.0f, 0.0f, 0.0f, pt);
assertPointEquals(10.0f, 4.0f, 1.0f, 0.0f, pt);
assertPointEquals(4.0f, 9.0f, 0.0f, 1.0f, pt);
assertPointEquals(16.0f, 15.0f, 1.0f, 1.0f, pt);
assertPointEquals(6.535211f, 6.8873234f, 0.5f, 0.5f, pt);
assertPointEquals(48.0f, 42.42857f, 1.5f, 1.5f, pt);
}
@Test
public void testQuadrilateralToQuadrilateral() {
PerspectiveTransform pt = PerspectiveTransform.quadrilateralToQuadrilateral(
2.0f, 3.0f, 10.0f, 4.0f, 16.0f, 15.0f, 4.0f, 9.0f,
103.0f, 110.0f, 300.0f, 120.0f, 290.0f, 270.0f, 150.0f, 280.0f);
assertPointEquals(103.0f, 110.0f, 2.0f, 3.0f, pt);
assertPointEquals(300.0f, 120.0f, 10.0f, 4.0f, pt);
assertPointEquals(290.0f, 270.0f, 16.0f, 15.0f, pt);
assertPointEquals(150.0f, 280.0f, 4.0f, 9.0f, pt);
assertPointEquals(7.1516876f, -64.60185f, 0.5f, 0.5f, pt);
assertPointEquals(328.09116f, 334.16385f, 50.0f, 50.0f, pt);
}
private static void assertPointEquals(float expectedX,
float expectedY,
float sourceX,
float sourceY,
PerspectiveTransform pt) {
float[] points = {sourceX, sourceY};
pt.transformPoints(points);
assertEquals(expectedX, points[0], EPSILON);
assertEquals(expectedY, points[1], EPSILON);
}
}