1 | // Jomic - a viewer for comic book archives. |
2 | // Copyright (C) 2004-2011 Thomas Aglassinger |
3 | // |
4 | // This program is free software: you can redistribute it and/or modify |
5 | // it under the terms of the GNU General Public License as published by |
6 | // the Free Software Foundation, either version 3 of the License, or |
7 | // (at your option) any later version. |
8 | // |
9 | // This program is distributed in the hope that it will be useful, |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | // GNU General Public License for more details. |
13 | // |
14 | // You should have received a copy of the GNU General Public License |
15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | package net.sf.jomic.jaiunit; |
17 | |
18 | import java.awt.Color; |
19 | import java.awt.Font; |
20 | import java.awt.FontMetrics; |
21 | import java.awt.GradientPaint; |
22 | import java.awt.Graphics2D; |
23 | import java.awt.RenderingHints; |
24 | import java.awt.geom.GeneralPath; |
25 | import java.awt.geom.Path2D; |
26 | import java.awt.geom.Rectangle2D; |
27 | import java.awt.image.BufferedImage; |
28 | import java.awt.image.RenderedImage; |
29 | import java.awt.image.renderable.ParameterBlock; |
30 | |
31 | import javax.media.jai.Histogram; |
32 | import javax.media.jai.JAI; |
33 | import javax.media.jai.ParameterBlockJAI; |
34 | |
35 | import net.sf.jomic.tools.StringTools; |
36 | import org.apache.commons.logging.Log; |
37 | import org.apache.commons.logging.LogFactory; |
38 | |
39 | /** |
40 | * Utility methods to implement JAI-related programmer tests based on the JUnit framework. |
41 | * |
42 | * @author Thomas Aglassinger |
43 | */ |
44 | public final class Tools |
45 | { |
46 | private static final double LABEL_FONT_SIZE_IN_PERCENT = 0.7; |
47 | private static final Color[] RAINBOW_COLORS = new Color[]{Color.RED, Color.YELLOW, |
48 | Color.GREEN, Color.CYAN, Color.BLUE}; |
49 | private static final Color[] WHITE_AND_BLACK = new Color[]{Color.WHITE, Color.BLACK}; |
50 | |
51 | private static Tools instance; |
52 | private static Log logger; |
53 | |
54 | private JAI jai; |
55 | private StringTools stringTools; |
56 | |
57 | private Tools() { |
58 | jai = new JAI(); |
59 | stringTools = StringTools.instance(); |
60 | } |
61 | |
62 | public RenderedImage getDeltaImage(RenderedImage some, RenderedImage other) { |
63 | assert some != null; |
64 | assert other != null; |
65 | RenderedImage result = null; |
66 | |
67 | ParameterBlock pbSubtract = new ParameterBlock(); |
68 | |
69 | pbSubtract.addSource(some); |
70 | pbSubtract.addSource(other); |
71 | result = JAI.create("Subtract", pbSubtract); |
72 | if (logger.isDebugEnabled()) { |
73 | logger.debug("subtracted image: " + result); |
74 | } |
75 | |
76 | ParameterBlock pbInvert = new ParameterBlock(); |
77 | RenderingHints hints = new RenderingHints(null); |
78 | |
79 | pbInvert.addSource(result); |
80 | hints.put(JAI.KEY_TRANSFORM_ON_COLORMAP, Boolean.FALSE); |
81 | result = jai.createNS("Invert", pbInvert, hints); |
82 | |
83 | ParameterBlockJAI pbHistogram = new ParameterBlockJAI("Histogram"); |
84 | |
85 | pbHistogram.addSource(result); |
86 | result = jai.createNS("Histogram", pbHistogram, null); |
87 | |
88 | if (logger.isDebugEnabled()) { |
89 | Histogram histogram = (Histogram) result.getProperty("histogram"); |
90 | double[] highValues = histogram.getHighValue(); |
91 | |
92 | logger.debug("histogram = " + histogram); |
93 | logger.debug("highValues = " + stringTools.arrayToString(highValues)); |
94 | logger.debug("lowValues = " + stringTools.arrayToString(histogram.getLowValue())); |
95 | logger.debug("standardDeviation = " + stringTools.arrayToString(histogram.getStandardDeviation())); |
96 | } |
97 | |
98 | return result; |
99 | } |
100 | |
101 | public static synchronized Tools instance() { |
102 | if (instance == null) { |
103 | logger = LogFactory.getLog(Tools.class); |
104 | instance = new Tools(); |
105 | } |
106 | return instance; |
107 | } |
108 | |
109 | public RenderedImage createTestImage(int width, int height) { |
110 | assert width > 0; |
111 | assert height > 0; |
112 | return createTestImage(width, height, null); |
113 | } |
114 | |
115 | public RenderedImage createTestImage(int width, int height, String labelText) { |
116 | return createTestImage(width, height, labelText, null); |
117 | } |
118 | |
119 | public RenderedImage createTestImage(int width, int height, String labelText, String details) { |
120 | return createTestImage(width, height, BufferedImage.TYPE_BYTE_GRAY, labelText, details); |
121 | } |
122 | |
123 | public RenderedImage createTestImage(int width, int height, int imageType, String labelText, |
124 | String details) { |
125 | // TODO: do something useful about small width or height |
126 | assert width > 0; |
127 | assert height > 0; |
128 | int lastX = width - 1; |
129 | int lastY = height - 1; |
130 | int triangleWidth = Math.max(5, Math.min(width, height) / 25); |
131 | int triangleHeight = (int) (Math.sqrt(0.75) * triangleWidth); |
132 | int halfWidth = width / 2; |
133 | int halfHeight = height / 2; |
134 | BufferedImage result = new BufferedImage(width, height, imageType); |
135 | Graphics2D g = result.createGraphics(); |
136 | Rectangle2D textBox; |
137 | |
138 | g.setColor(Color.WHITE); |
139 | g.fillRect(0, 0, width, height); |
140 | |
141 | if (labelText != null) { |
142 | int fontHeight = Math.min(10, g.getFontMetrics().getHeight()); |
143 | Font newFont = new Font("Dialog", Font.PLAIN, fontHeight); |
144 | Font labelFont; |
145 | |
146 | do { |
147 | labelFont = newFont; |
148 | fontHeight = (int) (1.1 * fontHeight); |
149 | newFont = newFont.deriveFont((float) fontHeight); |
150 | textBox = newFont.getStringBounds(labelText, g.getFontRenderContext()); |
151 | // TODO: add support for jomic-logging-test.properties, and |
152 | // if (logger.isDebugEnabled()) { |
153 | // comment this in |
154 | // logger.debug("try font: fontHeight=" + |
155 | // labelFont.getSize() |
156 | // + ", boxWidth=" + textBox.getWidth() |
157 | // + ", boxHeight=" + textBox.getHeight()); |
158 | // } |
159 | } while ((textBox.getWidth() < width * LABEL_FONT_SIZE_IN_PERCENT) |
160 | && (textBox.getHeight() < height * LABEL_FONT_SIZE_IN_PERCENT)); |
161 | g.setColor(Color.LIGHT_GRAY); |
162 | g.setFont(labelFont); |
163 | textBox = labelFont.getStringBounds(labelText, g.getFontRenderContext()); |
164 | |
165 | int labelLeftX = (int) (width - textBox.getWidth()) / 2; |
166 | int labelTopY = (int) (height - textBox.getHeight()) / 2 |
167 | + g.getFontMetrics().getAscent(); |
168 | |
169 | if (logger.isDebugEnabled()) { |
170 | logger.debug("label: font=" + labelFont + ", x=" + labelLeftX + ", y=" + labelTopY); |
171 | } |
172 | g.drawString(labelText, labelLeftX, labelTopY); |
173 | } |
174 | |
175 | // draw X |
176 | g.setColor(Color.BLACK); |
177 | g.drawLine(0, 0, lastX, lastY); |
178 | g.drawLine(0, lastY, lastX, 0); |
179 | |
180 | // draw triangles |
181 | drawFilledTriangle(g, halfWidth, 0, halfWidth - triangleHeight, triangleHeight, halfWidth |
182 | + triangleHeight, triangleHeight); |
183 | drawFilledTriangle(g, halfWidth, lastY, halfWidth - triangleHeight, lastY - triangleHeight, |
184 | halfWidth + triangleHeight, lastY - triangleHeight); |
185 | drawFilledTriangle(g, 0, halfHeight, triangleHeight, halfHeight - triangleHeight, |
186 | triangleHeight, halfHeight + triangleHeight); |
187 | drawFilledTriangle(g, lastX, halfHeight, lastX - triangleHeight, halfHeight |
188 | - triangleHeight, lastX - triangleHeight, halfHeight + triangleHeight); |
189 | |
190 | // print image dimension |
191 | drawCenteredText(g, "" + width + "x" + height, halfWidth, 1 * height / 8, 2 * triangleWidth); |
192 | |
193 | // draw rainbow gradient |
194 | Color[] colors; |
195 | |
196 | if ((imageType == BufferedImage.TYPE_BYTE_GRAY) |
197 | || (imageType == BufferedImage.TYPE_USHORT_GRAY)) { |
198 | colors = WHITE_AND_BLACK; |
199 | } else { |
200 | colors = RAINBOW_COLORS; |
201 | } |
202 | |
203 | int colorCount = colors.length; |
204 | int rainbowWidth = width / 8; |
205 | int rainbowHeight = height / 16; |
206 | int rainbowCenterX = width / 2; |
207 | int rainbowCenterY = 6 * height / 8; |
208 | int rainbowStartX = rainbowCenterX - rainbowWidth / 2; |
209 | int rainbowStartY = rainbowCenterY - rainbowHeight / 2; |
210 | |
211 | if ((rainbowWidth >= 2 * colorCount) && (rainbowHeight > 1)) { |
212 | if (logger.isDebugEnabled()) { |
213 | logger.debug("draw rainbow from (" + rainbowStartX + ", " + rainbowStartY |
214 | + ") with width=" + rainbowWidth + " and height=" + rainbowHeight); |
215 | } |
216 | for (int i = 0; i < colorCount - 1; i += 1) { |
217 | Color startColor = colors[i]; |
218 | Color endColor = colors[i + 1]; |
219 | int startX = (rainbowStartX + i * rainbowWidth / (colorCount - 1)); |
220 | int endX = (rainbowStartX + (i + 1) * rainbowWidth / (colorCount - 1)); |
221 | GradientPaint paint = new GradientPaint(startX, 0, startColor, endX, 0, endColor); |
222 | |
223 | g.setPaint(paint); |
224 | if (logger.isDebugEnabled()) { |
225 | logger.debug("draw rainbow element from (" + startX + ", " + rainbowStartY |
226 | + ") with width=" + (endX - startX) + " and height=" + rainbowHeight); |
227 | } |
228 | g.fill(new Rectangle2D.Double(startX, rainbowStartY, endX - startX, rainbowHeight)); |
229 | } |
230 | } |
231 | |
232 | // print details |
233 | if (details != null) { |
234 | g.setColor(Color.BLACK); |
235 | drawCenteredText(g, details, width / 2, 7 * height / 8, triangleHeight); |
236 | } |
237 | |
238 | return result; |
239 | } |
240 | |
241 | public boolean imagesAreEqual(RenderedImage some, RenderedImage other) { |
242 | boolean result = true; |
243 | RenderedImage deltaImage = getDeltaImage(some, other); |
244 | Histogram histogram = (Histogram) deltaImage.getProperty("histogram"); |
245 | double[] deviations = histogram.getStandardDeviation(); |
246 | |
247 | if (logger.isDebugEnabled()) { |
248 | logger.debug("deviations = " + stringTools.arrayToString(deviations)); |
249 | logger.debug("totals = " + stringTools.arrayToString(histogram.getTotals())); |
250 | } |
251 | for (int i = 0; i < deviations.length; i += 1) { |
252 | if (deviations[i] > 0) { |
253 | result = false; |
254 | } |
255 | } |
256 | return result; |
257 | } |
258 | |
259 | public JAI jai() { |
260 | return jai; |
261 | } |
262 | |
263 | public void showDeltaImage(RenderedImage some, RenderedImage other, Class clazz, String id) { |
264 | ImageComparisonViewer frame = new ImageComparisonViewer(some, other, clazz, id); |
265 | |
266 | frame.showImages(); |
267 | } |
268 | |
269 | /** |
270 | * Show image. |
271 | * |
272 | * @param creator the class that created the image |
273 | * @param id an unique ID (within the creator class) to identify the test image |
274 | */ |
275 | public void showImage(RenderedImage image, Class creator, String id) { |
276 | assert image != null; |
277 | assert creator != null; |
278 | assert id != null; |
279 | ImageViewer frame = new ImageViewer(image, creator, id); |
280 | |
281 | frame.showImage(); |
282 | } |
283 | |
284 | private void drawCenteredText(Graphics2D g, String text, int x, int y, int fontSize) { |
285 | Font font = new Font("Dialog", Font.BOLD, fontSize); |
286 | |
287 | g.setFont(font); |
288 | |
289 | FontMetrics metrics = g.getFontMetrics(); |
290 | Rectangle2D textBox = metrics.getStringBounds(text, g); |
291 | double textWidth = textBox.getWidth(); |
292 | int textX = (int) (x - textWidth / 2); |
293 | double textHeight = textBox.getHeight(); |
294 | int textY = (int) (y - textHeight / 2 + g.getFontMetrics().getAscent()); |
295 | |
296 | g.drawString(text, textX, textY); |
297 | |
298 | } |
299 | |
300 | private void drawFilledTriangle(Graphics2D g, int x1, int y1, int x2, int y2, int x3, int y3) { |
301 | GeneralPath filledPolygon = new GeneralPath(Path2D.WIND_EVEN_ODD, 3); |
302 | |
303 | filledPolygon.moveTo(x1, y1); |
304 | filledPolygon.lineTo(x2, y2); |
305 | filledPolygon.lineTo(x3, y3); |
306 | filledPolygon.closePath(); |
307 | g.fill(filledPolygon); |
308 | } |
309 | } |