1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.imaging.formats.ico;
19
20 import static org.junit.jupiter.api.Assertions.assertEquals;
21 import static org.junit.jupiter.api.Assertions.assertNotNull;
22 import static org.junit.jupiter.api.Assertions.assertThrows;
23
24 import java.awt.image.BufferedImage;
25 import java.io.ByteArrayOutputStream;
26 import java.io.File;
27 import java.io.IOException;
28 import java.nio.file.Files;
29 import java.util.HashMap;
30 import java.util.Map;
31
32 import org.apache.commons.imaging.Imaging;
33 import org.apache.commons.imaging.ImagingException;
34 import org.apache.commons.imaging.common.BinaryOutputStream;
35 import org.apache.commons.imaging.internal.Debug;
36 import org.apache.commons.io.FileUtils;
37 import org.junit.jupiter.api.Test;
38
39 public class IcoRoundtripTest extends AbstractIcoTest {
40 private interface BitmapGenerator {
41 byte[] generateBitmap(int foreground, int background, int paletteSize) throws IOException, ImagingException;
42 }
43
44 private static final class GeneratorFor16BitBitmaps implements BitmapGenerator {
45 @Override
46 public byte[] generateBitmap(final int foreground, final int background, final int paletteSize) throws IOException, ImagingException {
47 try (final ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
48 final BinaryOutputStream bos = BinaryOutputStream.littleEndian(byteArrayStream)) {
49
50 for (int i = 0; i < paletteSize; i++) {
51 bos.write4Bytes(0);
52 }
53
54 for (int y = 15; y >= 0; y--) {
55 for (int x = 0; x < 16; x++) {
56 if (IMAGE[y][x] == 1) {
57 bos.write2Bytes(0x1f & foreground >> 3 | (0x1f & foreground >> 11) << 5 | (0x1f & foreground >> 19) << 10);
58 } else {
59 bos.write2Bytes(0x1f & background >> 3 | (0x1f & background >> 11) << 5 | (0x1f & background >> 19) << 10);
60 }
61 }
62 }
63
64 for (int y = IMAGE.length - 1; y >= 0; y--) {
65 bos.write(0);
66 bos.write(0);
67
68 bos.write(0);
69 bos.write(0);
70 }
71 bos.flush();
72 return byteArrayStream.toByteArray();
73 }
74 }
75 }
76
77 private static final class GeneratorFor1BitBitmaps implements BitmapGenerator {
78 @Override
79 public byte[] generateBitmap(final int foreground, final int background, final int paletteSize) throws IOException, ImagingException {
80 try (final ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
81 final BinaryOutputStream bos = BinaryOutputStream.littleEndian(byteArrayStream)) {
82
83 bos.write3Bytes(background);
84 bos.write(0);
85 bos.write3Bytes(foreground);
86 bos.write(0);
87 for (int i = 2; i < paletteSize; i++) {
88 bos.write4Bytes(0);
89 }
90
91 for (int y = 15; y >= 0; y--) {
92 for (int x = 0; x < 16; x += 8) {
93 bos.write((0x1 & IMAGE[y][x]) << 7 | (0x1 & IMAGE[y][x + 1]) << 6 | (0x1 & IMAGE[y][x + 2]) << 5 | (0x1 & IMAGE[y][x + 3]) << 4
94 | (0x1 & IMAGE[y][x + 4]) << 3 | (0x1 & IMAGE[y][x + 5]) << 2 | (0x1 & IMAGE[y][x + 6]) << 1 | (0x1 & IMAGE[y][x + 7]) << 0);
95 }
96
97 bos.write(0);
98 bos.write(0);
99 }
100
101 for (int y = IMAGE.length - 1; y >= 0; y--) {
102 bos.write(0);
103 bos.write(0);
104
105 bos.write(0);
106 bos.write(0);
107 }
108 bos.flush();
109 return byteArrayStream.toByteArray();
110 }
111 }
112 }
113
114 private static final class GeneratorFor24BitBitmaps implements BitmapGenerator {
115 @Override
116 public byte[] generateBitmap(final int foreground, final int background, final int paletteSize) throws IOException, ImagingException {
117 try (final ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
118 final BinaryOutputStream bos = BinaryOutputStream.littleEndian(byteArrayStream)) {
119
120 for (int i = 0; i < paletteSize; i++) {
121 bos.write4Bytes(0);
122 }
123
124 for (int y = 15; y >= 0; y--) {
125 for (int x = 0; x < 16; x++) {
126 if (IMAGE[y][x] == 1) {
127 bos.write3Bytes(0xffffff & foreground);
128 } else {
129 bos.write3Bytes(0xffffff & background);
130 }
131 }
132 }
133
134 for (int y = IMAGE.length - 1; y >= 0; y--) {
135 bos.write(0);
136 bos.write(0);
137
138 bos.write(0);
139 bos.write(0);
140 }
141 bos.flush();
142 return byteArrayStream.toByteArray();
143 }
144 }
145 }
146
147 private static final class GeneratorFor32BitBitmaps implements BitmapGenerator {
148 public byte[] generate32bitRGBABitmap(final int foreground, final int background, final int paletteSize, final boolean writeMask) throws IOException {
149 try (final ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
150 final BinaryOutputStream bos = BinaryOutputStream.littleEndian(byteArrayStream)) {
151
152 for (int i = 0; i < paletteSize; i++) {
153 bos.write4Bytes(0);
154 }
155
156 for (int y = 15; y >= 0; y--) {
157 for (int x = 0; x < 16; x++) {
158 if (IMAGE[y][x] == 1) {
159 bos.write4Bytes(foreground);
160 } else {
161 bos.write4Bytes(background);
162 }
163 }
164 }
165
166 if (writeMask) {
167 for (int y = IMAGE.length - 1; y >= 0; y--) {
168 bos.write(0);
169 bos.write(0);
170
171 bos.write(0);
172 bos.write(0);
173 }
174 }
175 bos.flush();
176 return byteArrayStream.toByteArray();
177 }
178 }
179
180 @Override
181 public byte[] generateBitmap(final int foreground, final int background, final int paletteSize) throws IOException, ImagingException {
182 return generate32bitRGBABitmap(foreground, background, paletteSize, true);
183 }
184 }
185
186 private static final class GeneratorFor4BitBitmaps implements BitmapGenerator {
187 @Override
188 public byte[] generateBitmap(final int foreground, final int background, final int paletteSize) throws IOException, ImagingException {
189 try (final ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
190 final BinaryOutputStream bos = BinaryOutputStream.littleEndian(byteArrayStream)) {
191
192 bos.write3Bytes(background);
193 bos.write(0);
194 bos.write3Bytes(foreground);
195 bos.write(0);
196 for (int i = 2; i < paletteSize; i++) {
197 bos.write4Bytes(0);
198 }
199
200 for (int y = 15; y >= 0; y--) {
201 for (int x = 0; x < 16; x += 2) {
202 bos.write((0xf & IMAGE[y][x]) << 4 | 0xf & IMAGE[y][x + 1]);
203 }
204 }
205
206 for (int y = IMAGE.length - 1; y >= 0; y--) {
207 bos.write(0);
208 bos.write(0);
209
210 bos.write(0);
211 bos.write(0);
212 }
213 bos.flush();
214 return byteArrayStream.toByteArray();
215 }
216 }
217 }
218
219 private static final class GeneratorFor8BitBitmaps implements BitmapGenerator {
220 @Override
221 public byte[] generateBitmap(final int foreground, final int background, final int paletteSize) throws IOException, ImagingException {
222 try (final ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
223 final BinaryOutputStream bos = BinaryOutputStream.littleEndian(byteArrayStream)) {
224
225 bos.write3Bytes(background);
226 bos.write(0);
227 bos.write3Bytes(foreground);
228 bos.write(0);
229 for (int i = 2; i < paletteSize; i++) {
230 bos.write4Bytes(0);
231 }
232
233 for (int y = 15; y >= 0; y--) {
234 for (int x = 0; x < 16; x++) {
235 bos.write(IMAGE[y][x]);
236 }
237 }
238
239 for (int y = IMAGE.length - 1; y >= 0; y--) {
240 bos.write(0);
241 bos.write(0);
242
243 bos.write(0);
244 bos.write(0);
245 }
246 bos.flush();
247 return byteArrayStream.toByteArray();
248 }
249 }
250 }
251
252
253 private static final int[][] IMAGE = { { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
254 { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0 },
255 { 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 },
256 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 },
257 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0 },
258 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0 },
259 { 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0 },
260 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } };
261
262 private final Map<Integer, BitmapGenerator> generatorMap = new HashMap<>();
263
264 public IcoRoundtripTest() {
265 generatorMap.put(1, new GeneratorFor1BitBitmaps());
266 generatorMap.put(4, new GeneratorFor4BitBitmaps());
267 generatorMap.put(8, new GeneratorFor8BitBitmaps());
268 generatorMap.put(16, new GeneratorFor16BitBitmaps());
269 generatorMap.put(24, new GeneratorFor24BitBitmaps());
270 generatorMap.put(32, new GeneratorFor32BitBitmaps());
271 }
272
273 @Test
274 public void test32bitMask() throws Exception {
275 final int foreground = 0xFFF000E0;
276 final int background = 0xFF102030;
277 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
278 try (final BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) {
279
280 final byte[] bitmap = new GeneratorFor32BitBitmaps().generate32bitRGBABitmap(foreground, background, 0, false);
281 writeICONDIR(bos, 0, 1, 1);
282 writeICONDIRENTRY(bos, 16, 16, 0, 0, 1, 32, 40 + bitmap.length);
283 writeBITMAPINFOHEADER(bos, 16, 2 * 16, 1, 32, 0, 0, 0);
284 bos.write(bitmap);
285 bos.flush();
286 }
287 writeAndReadImageData("16x16x32-no-mask", baos.toByteArray(), foreground, background);
288 }
289
290 @Test
291 public void testAlphaVersusANDMask() throws Exception {
292 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
293 try (final BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) {
294 final byte[] bitmap = new GeneratorFor32BitBitmaps().generate32bitRGBABitmap(0xFF000000, 0x00000000, 0, true);
295 writeICONDIR(bos, 0, 1, 1);
296 writeICONDIRENTRY(bos, 16, 16, 0, 0, 1, 32, 40 + bitmap.length);
297 writeBITMAPINFOHEADER(bos, 16, 2 * 16, 1, 32, 0, 0, 0);
298 bos.write(bitmap);
299 bos.flush();
300 }
301
302
303 writeAndReadImageData("16x16x32-alpha-vs-mask", baos.toByteArray(), 0xFF000000, 0x00000000);
304 }
305
306 @Test
307 public void testBadICONDIRENTRYIcons() throws Exception {
308 final int foreground = 0xFFF000E0;
309 final int background = 0xFF102030;
310
311 for (final Map.Entry<Integer, BitmapGenerator> entry : generatorMap.entrySet()) {
312 final int bitDepth = entry.getKey();
313 final BitmapGenerator bitmapGenerator = entry.getValue();
314
315 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
316 try (final BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) {
317 final byte[] bitmap = bitmapGenerator.generateBitmap(foreground, background, bitDepth <= 8 ? 1 << bitDepth : 0);
318 writeICONDIR(bos, 0, 1, 1);
319 writeICONDIRENTRY(bos, 3 , 4
320
321 , 7
322
323 , 20 , 11
324
325 , 19
326
327 , 40 + bitmap.length);
328 writeBITMAPINFOHEADER(bos, 16, 2 * 16, 1, bitDepth, 0, 0, 0);
329 bos.write(bitmap);
330 bos.flush();
331 }
332 writeAndReadImageData("16x16x" + bitDepth + "-corrupt-icondirentry", baos.toByteArray(), foreground, background);
333 }
334 }
335
336 @Test
337 public void testBitfieldCompression() throws Exception {
338 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
339 try (final BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) {
340 final byte[] bitmap = new GeneratorFor32BitBitmaps().generate32bitRGBABitmap(0xFFFF0000, 0xFFFFFFFF, 0, true);
341 writeICONDIR(bos, 0, 1, 1);
342 writeICONDIRENTRY(bos, 16, 16, 0, 0, 1, 32, 40 + bitmap.length);
343 writeBITMAPINFOHEADER(bos, 16, 2 * 16, 1, 32, 3 , 0, 0);
344 bos.write4Bytes(0x000000FF);
345 bos.write4Bytes(0x0000FF00);
346 bos.write4Bytes(0x00FF0000);
347 bos.write(bitmap);
348 bos.flush();
349 }
350 writeAndReadImageData("16x16x32-bitfield-compressed", baos.toByteArray(), 0xFF0000FF, 0xFFFFFFFF);
351 }
352
353 @Test
354 public void testColorsUsed() throws Exception {
355 final int foreground = 0xFFF000E0;
356 final int background = 0xFF102030;
357 for (final Map.Entry<Integer, BitmapGenerator> entry : generatorMap.entrySet()) {
358 final int bitDepth = entry.getKey();
359 final BitmapGenerator bitmapGenerator = entry.getValue();
360
361 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
362 try (final BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) {
363 final byte[] bitmap = bitmapGenerator.generateBitmap(foreground, background, 2);
364 writeICONDIR(bos, 0, 1, 1);
365 writeICONDIRENTRY(bos, 3, 4, 7, 20, 11, 19, 40 + bitmap.length);
366 writeBITMAPINFOHEADER(bos, 16, 2 * 16, 1, bitDepth, 0, 2, 0);
367 bos.write(bitmap);
368 bos.flush();
369 writeAndReadImageData("16x16x" + bitDepth + "-custom-palette", baos.toByteArray(), foreground, background);
370 }
371 }
372 }
373
374 @Test
375 public void testFullyTransparent32bitRGBA() throws Exception {
376 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
377 try (final BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) {
378 final byte[] bitmap = new GeneratorFor32BitBitmaps().generate32bitRGBABitmap(0x00000000, 0x00FFFFFF, 0, true);
379 writeICONDIR(bos, 0, 1, 1);
380 writeICONDIRENTRY(bos, 16, 16, 0, 0, 1, 32, 40 + bitmap.length);
381 writeBITMAPINFOHEADER(bos, 16, 2 * 16, 1, 32, 0, 0, 0);
382 bos.write(bitmap);
383 bos.flush();
384 }
385
386 writeAndReadImageData("16x16x32-fully-transparent", baos.toByteArray(), 0xFF000000, 0xFFFFFFFF);
387 }
388
389 @Test
390 public void testNormalIcons() throws Exception {
391 final int foreground = 0xFFF000E0;
392 final int background = 0xFF102030;
393 for (final Map.Entry<Integer, BitmapGenerator> entry : generatorMap.entrySet()) {
394 final int bitDepth = entry.getKey();
395 final BitmapGenerator bitmapGenerator = entry.getValue();
396
397 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
398 try (final BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) {
399 final byte[] bitmap = bitmapGenerator.generateBitmap(foreground, background, bitDepth <= 8 ? 1 << bitDepth : 0);
400 writeICONDIR(bos, 0, 1, 1);
401 writeICONDIRENTRY(bos, 16, 16, 0, 0, 1, bitDepth, 40 + bitmap.length);
402 writeBITMAPINFOHEADER(bos, 16, 2 * 16, 1, bitDepth, 0, 0, 0);
403 bos.write(bitmap);
404 bos.flush();
405 writeAndReadImageData("16x16x" + bitDepth, baos.toByteArray(), foreground, background);
406 }
407 }
408 }
409
410 @Test
411 public void testZeroColorPlanes() throws Exception {
412 final int foreground = 0xFFF000E0;
413 final int background = 0xFF102030;
414 for (final Map.Entry<Integer, BitmapGenerator> entry : generatorMap.entrySet()) {
415 final int bitDepth = entry.getKey();
416 final BitmapGenerator bitmapGenerator = entry.getValue();
417
418 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
419 try (final BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) {
420 final byte[] bitmap = bitmapGenerator.generateBitmap(foreground, background, bitDepth <= 8 ? 1 << bitDepth : 0);
421 writeICONDIR(bos, 0, 1, 1);
422 writeICONDIRENTRY(bos, 16, 16, 0, 0, 1, bitDepth, 40 + bitmap.length);
423 writeBITMAPINFOHEADER(bos, 16, 2 * 16, 0 , bitDepth, 0, 0, 0);
424 bos.write(bitmap);
425 bos.flush();
426 }
427
428 assertThrows(ImagingException.class,
429 () -> writeAndReadImageData("16x16x" + bitDepth + "-zero-colorPlanes", baos.toByteArray(), foreground, background));
430 }
431 }
432
433 private void verify(final BufferedImage data, final int foreground, final int background) {
434 assertNotNull(data);
435 assertEquals(data.getHeight(), IMAGE.length);
436
437 for (int y = 0; y < data.getHeight(); y++) {
438 assertEquals(data.getWidth(), IMAGE[y].length);
439 for (int x = 0; x < data.getWidth(); x++) {
440 final int imageARGB = IMAGE[y][x] == 1 ? foreground : background;
441 final int dataARGB = data.getRGB(x, y);
442
443 if (imageARGB != dataARGB) {
444 Debug.debug("x: " + x + ", y: " + y + ", image: " + imageARGB + " (0x" + Integer.toHexString(imageARGB) + ")" + ", data: " + dataARGB
445 + " (0x" + Integer.toHexString(dataARGB) + ")");
446 }
447 assertEquals(imageARGB, dataARGB);
448 }
449 }
450 }
451
452 private void writeAndReadImageData(final String description, final byte[] rawData, final int foreground, final int background)
453 throws IOException, ImagingException {
454
455
456
457
458 final File tempFile = Files.createTempFile("temp", ".ico").toFile();
459 FileUtils.writeByteArrayToFile(tempFile, rawData);
460
461 final BufferedImage dstImage = Imaging.getBufferedImage(tempFile);
462
463 assertNotNull(dstImage);
464 assertEquals(dstImage.getWidth(), IMAGE[0].length);
465 assertEquals(dstImage.getHeight(), IMAGE.length);
466
467 verify(dstImage, foreground, background);
468 }
469
470 private void writeBITMAPINFOHEADER(final BinaryOutputStream bos, final int width, final int height, final int colorPlanes, final int bitCount,
471 final int compression, final int colorsUsed, final int colorsImportant) throws IOException {
472
473 bos.write4Bytes(40);
474 bos.write4Bytes(width);
475 bos.write4Bytes(height);
476 bos.write2Bytes(colorPlanes);
477 bos.write2Bytes(bitCount);
478 bos.write4Bytes(compression);
479 bos.write4Bytes(0);
480 bos.write4Bytes(0);
481 bos.write4Bytes(0);
482 bos.write4Bytes(colorsUsed);
483 bos.write4Bytes(colorsImportant);
484 }
485
486 private void writeICONDIR(final BinaryOutputStream bos, final int reserved, final int type, final int count) throws IOException {
487 bos.write2Bytes(reserved);
488 bos.write2Bytes(type);
489 bos.write2Bytes(count);
490 }
491
492 private void writeICONDIRENTRY(final BinaryOutputStream bos, final int width, final int height, final int colorCount, final int reserved, final int planes,
493 final int bitCount, final int bytesInRes) throws IOException {
494 bos.write(width);
495 bos.write(height);
496 bos.write(colorCount);
497 bos.write(reserved);
498 bos.write2Bytes(planes);
499 bos.write2Bytes(bitCount);
500 bos.write4Bytes(bytesInRes);
501 bos.write4Bytes(22);
502 }
503
504 }