Issue 563: Support non-rectangular Data Matrix
[zxing.git] / core / test / src / com / google / zxing / common / AbstractBlackBoxTestCase.java
1 /*
2  * Copyright 2008 ZXing authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.google.zxing.common;
18
19 import com.google.zxing.BarcodeFormat;
20 import com.google.zxing.BinaryBitmap;
21 import com.google.zxing.DecodeHintType;
22 import com.google.zxing.LuminanceSource;
23 import com.google.zxing.Reader;
24 import com.google.zxing.ReaderException;
25 import com.google.zxing.Result;
26 import com.google.zxing.ResultMetadataType;
27 import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
28 import org.junit.Assert;
29 import org.junit.Test;
30
31 import javax.imageio.ImageIO;
32 import java.awt.geom.AffineTransform;
33 import java.awt.geom.Rectangle2D;
34 import java.awt.image.AffineTransformOp;
35 import java.awt.image.BufferedImage;
36 import java.awt.image.BufferedImageOp;
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.FilenameFilter;
40 import java.io.IOException;
41 import java.io.InputStreamReader;
42 import java.nio.charset.Charset;
43 import java.util.ArrayList;
44 import java.util.Hashtable;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Properties;
48
49 /**
50  * @author Sean Owen
51  * @author dswitkin@google.com (Daniel Switkin)
52  */
53 public abstract class AbstractBlackBoxTestCase extends Assert {
54
55   protected static final Hashtable<DecodeHintType, Object> TRY_HARDER_HINT;
56   static {
57     TRY_HARDER_HINT = new Hashtable<DecodeHintType, Object>();
58     TRY_HARDER_HINT.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
59   }
60
61   private static final FilenameFilter IMAGE_NAME_FILTER = new FilenameFilter() {
62     public boolean accept(File dir, String name) {
63       String lowerCase = name.toLowerCase();
64       return lowerCase.endsWith(".jpg") || lowerCase.endsWith(".jpeg") ||
65              lowerCase.endsWith(".gif") || lowerCase.endsWith(".png");
66     }
67   };
68
69   public static class SummaryResults {
70     private int totalFound;
71     private int totalMustPass;
72     private int totalTests;
73
74     public SummaryResults() {
75       totalFound = 0;
76       totalMustPass = 0;
77       totalTests = 0;
78     }
79
80     public SummaryResults(int found, int mustPass, int total) {
81       totalFound = found;
82       totalMustPass = mustPass;
83       totalTests = total;
84     }
85
86     public void add(SummaryResults other) {
87       totalFound += other.totalFound;
88       totalMustPass += other.totalMustPass;
89       totalTests += other.totalTests;
90     }
91
92     public String toString() {
93       return "\nSUMMARY RESULTS:\n  Decoded " + totalFound + " images out of " + totalTests +
94         " (" + (totalFound * 100 / totalTests) + "%, " + totalMustPass + " required)";
95     }
96   }
97
98   private static class TestResult {
99     private final int mustPassCount;
100     private final int tryHarderCount;
101     private final float rotation;
102
103     TestResult(int mustPassCount, int tryHarderCount, float rotation) {
104       this.mustPassCount = mustPassCount;
105       this.tryHarderCount = tryHarderCount;
106       this.rotation = rotation;
107     }
108     public int getMustPassCount() {
109       return mustPassCount;
110     }
111     public int getTryHarderCount() {
112       return tryHarderCount;
113     }
114     public float getRotation() {
115       return rotation;
116     }
117   }
118
119   private final File testBase;
120   private final Reader barcodeReader;
121   private final BarcodeFormat expectedFormat;
122   private final List<TestResult> testResults;
123
124   protected AbstractBlackBoxTestCase(String testBasePathSuffix,
125                                      Reader barcodeReader,
126                                      BarcodeFormat expectedFormat) {
127     // A little workaround to prevent aggravation in my IDE
128     File testBase = new File(testBasePathSuffix);
129     if (!testBase.exists()) {
130       // try starting with 'core' since the test base is often given as the project root
131       testBase = new File("core/" + testBasePathSuffix);
132     }
133     this.testBase = testBase;
134     this.barcodeReader = barcodeReader;
135     this.expectedFormat = expectedFormat;
136     testResults = new ArrayList<TestResult>();
137   }
138
139   /**
140    * Adds a new test for the current directory of images.
141    *
142    * @param mustPassCount The number of images which must decode for the test to pass.
143    * @param tryHarderCount The number of images which must pass using the try harder flag.
144    * @param rotation The rotation in degrees clockwise to use for this test.
145    */
146   protected void addTest(int mustPassCount, int tryHarderCount, float rotation) {
147     testResults.add(new TestResult(mustPassCount, tryHarderCount, rotation));
148   }
149
150   protected File[] getImageFiles() {
151     assertTrue("Please run from the 'core' directory", testBase.exists());
152     return testBase.listFiles(IMAGE_NAME_FILTER);
153   }
154
155   protected Reader getReader() {
156     return barcodeReader;
157   }
158
159   protected Hashtable<DecodeHintType, Object> getHints() {
160     return null;
161   }
162
163   // This workaround is used because AbstractNegativeBlackBoxTestCase overrides this method but does
164   // not return SummaryResults.
165   @Test
166   public void testBlackBox() throws IOException {
167     testBlackBoxCountingResults(true);
168   }
169
170   public SummaryResults testBlackBoxCountingResults(boolean assertOnFailure) throws IOException {
171     assertFalse(testResults.isEmpty());
172
173     File[] imageFiles = getImageFiles();
174     int testCount = testResults.size();
175     int[] passedCounts = new int[testCount];
176     int[] tryHarderCounts = new int[testCount];
177     for (File testImage : imageFiles) {
178       System.out.println("Starting " + testImage.getAbsolutePath());
179
180       BufferedImage image = ImageIO.read(testImage);
181
182       String testImageFileName = testImage.getName();
183       String fileBaseName = testImageFileName.substring(0, testImageFileName.indexOf('.'));
184       File expectedTextFile = new File(testBase, fileBaseName + ".txt");
185       String expectedText = readFileAsString(expectedTextFile);
186
187       File expectedMetadataFile = new File(testBase, fileBaseName + ".metadata.txt");
188       Properties expectedMetadata = new Properties();
189       if (expectedMetadataFile.exists()) {
190         expectedMetadata.load(new FileInputStream(expectedMetadataFile));
191       }
192
193       for (int x = 0; x < testCount; x++) {
194         float rotation = testResults.get(x).getRotation();
195         BufferedImage rotatedImage = rotateImage(image, rotation);
196         LuminanceSource source = new BufferedImageLuminanceSource(rotatedImage);
197         BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
198         if (decode(bitmap, rotation, expectedText, expectedMetadata, false)) {
199           passedCounts[x]++;
200         }
201         if (decode(bitmap, rotation, expectedText, expectedMetadata, true)) {
202           tryHarderCounts[x]++;
203         }
204       }
205     }
206
207     // Print the results of all tests first
208     int totalFound = 0;
209     int totalMustPass = 0;
210     for (int x = 0; x < testCount; x++) {
211       System.out.println("Rotation " + testResults.get(x).getRotation() + " degrees:");
212       System.out.println("  " + passedCounts[x] + " of " + imageFiles.length + " images passed ("
213           + testResults.get(x).getMustPassCount() + " required)");
214       System.out.println("  " + tryHarderCounts[x] + " of " + imageFiles.length +
215           " images passed with try harder (" + testResults.get(x).getTryHarderCount() +
216           " required)");
217       totalFound += passedCounts[x];
218       totalFound += tryHarderCounts[x];
219       totalMustPass += testResults.get(x).getMustPassCount();
220       totalMustPass += testResults.get(x).getTryHarderCount();
221     }
222
223     int totalTests = imageFiles.length * testCount * 2;
224     System.out.println("TOTALS:\n  Decoded " + totalFound + " images out of " + totalTests +
225       " (" + (totalFound * 100 / totalTests) + "%, " + totalMustPass + " required)");
226     if (totalFound > totalMustPass) {
227       System.out.println("  *** Test too lax by " + (totalFound - totalMustPass) + " images");
228     } else if (totalFound < totalMustPass) {
229       System.out.println("  *** Test failed by " + (totalMustPass - totalFound) + " images");
230     }
231
232     // Then run through again and assert if any failed
233     if (assertOnFailure) {
234       for (int x = 0; x < testCount; x++) {
235         assertTrue("Rotation " + testResults.get(x).getRotation() +
236             " degrees: Too many images failed",
237             passedCounts[x] >= testResults.get(x).getMustPassCount());
238         assertTrue("Try harder, Rotation " + testResults.get(x).getRotation() +
239             " degrees: Too many images failed",
240             tryHarderCounts[x] >= testResults.get(x).getTryHarderCount());
241       }
242     }
243     return new SummaryResults(totalFound, totalMustPass, totalTests);
244   }
245
246   private boolean decode(BinaryBitmap source,
247                          float rotation,
248                          String expectedText,
249                          Properties expectedMetadata,
250                          boolean tryHarder) {
251     Result result;
252     String suffix = " (" + (tryHarder ? "try harder, " : "") + "rotation: " + rotation + ')';
253
254     try {
255       Hashtable<DecodeHintType, Object> hints = getHints();
256       if (tryHarder) {
257         if (hints == null) {
258           hints = TRY_HARDER_HINT;
259         } else {
260           hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
261         }
262       }
263       result = barcodeReader.decode(source, hints);
264     } catch (ReaderException re) {
265       System.out.println(re + suffix);
266       return false;
267     }
268
269     if (!expectedFormat.equals(result.getBarcodeFormat())) {
270       System.out.println("Format mismatch: expected '" + expectedFormat + "' but got '" +
271           result.getBarcodeFormat() + '\'' + suffix);
272       return false;
273     }
274
275     String resultText = result.getText();
276     if (!expectedText.equals(resultText)) {
277       System.out.println("Mismatch: expected '" + expectedText + "' but got '" + resultText +
278           '\'' +  suffix);
279       return false;
280     }
281
282     Hashtable resultMetadata = result.getResultMetadata();
283     for (Map.Entry<Object,Object> metadatum : expectedMetadata.entrySet()) {
284       ResultMetadataType key = ResultMetadataType.valueOf(metadatum.getKey().toString());
285       Object expectedValue = metadatum.getValue();
286       Object actualValue = resultMetadata == null ? null : resultMetadata.get(key);
287       if (!expectedValue.equals(actualValue)) {
288         System.out.println("Metadata mismatch: for key '" + key + "' expected '" + expectedValue +
289             "' but got '" + actualValue + '\'');
290         return false;
291       }
292     }
293
294     return true;
295   }
296
297   private static String readFileAsString(File file) throws IOException {
298     StringBuilder result = new StringBuilder((int) file.length());
299     InputStreamReader reader = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF8"));
300     try {
301       char[] buffer = new char[256];
302       int charsRead;
303       while ((charsRead = reader.read(buffer)) > 0) {
304         result.append(buffer, 0, charsRead);
305       }
306     } finally {
307       reader.close();
308     }
309     return result.toString();
310   }
311
312   protected static BufferedImage rotateImage(BufferedImage original, float degrees) {
313     if (degrees == 0.0f) {
314       return original;
315     } else {
316       double radians = Math.toRadians(degrees);
317
318       // Transform simply to find out the new bounding box (don't actually run the image through it)
319       AffineTransform at = new AffineTransform();
320       at.rotate(radians, original.getWidth() / 2.0, original.getHeight() / 2.0);
321       BufferedImageOp op = new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC);
322
323       Rectangle2D r = op.getBounds2D(original);
324       int width = (int) Math.ceil(r.getWidth());
325       int height = (int) Math.ceil(r.getHeight());
326
327       // Real transform, now that we know the size of the new image and how to translate after we rotate
328       // to keep it centered
329       at = new AffineTransform();
330       at.rotate(radians, width / 2.0, height / 2.0);
331       at.translate(((width - original.getWidth()) / 2.0),
332                    ((height - original.getHeight()) / 2.0));
333       op = new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC);
334
335       return op.filter(original, null);
336     }
337   }
338
339 }