View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.tiff;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.fail;
21  
22  import java.awt.AlphaComposite;
23  import java.awt.Color;
24  import java.awt.Graphics2D;
25  import java.awt.image.BufferedImage;
26  import java.io.BufferedOutputStream;
27  import java.io.File;
28  import java.io.FileOutputStream;
29  import java.nio.ByteOrder;
30  import java.nio.file.Path;
31  
32  import org.apache.commons.imaging.ImageFormats;
33  import org.apache.commons.imaging.Imaging;
34  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
35  import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
36  import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
37  import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
38  import org.junit.jupiter.api.Test;
39  import org.junit.jupiter.api.io.TempDir;
40  
41  /**
42   * Performs a round-trip that writes an image containing Alpha and then reads it back. Selected non-opaque pixels are tested for correctness,
43   */
44  public class TiffAlphaRoundTripTest {
45  
46      @TempDir
47      Path tempDir;
48  
49      /**
50       * Checks to see if a pixel component (A, R, G, or B) for two specified values are within a specified tolerance.
51       *
52       * @param a          the first value
53       * @param b          the second value
54       * @param iShift     a multiple of 8 telling how far to shift values to extract components (24, 16, 8, or zero for ARGB)
55       * @param iTolerance a small positive integer
56       * @return true if the components of the values match
57       */
58      boolean componentMatch(final int a, final int b, final int iShift, final int iTolerance) {
59          int delta = (a >> iShift & 0xff) - (b >> iShift & 0xff);
60          if (delta < 0) {
61              delta = -delta;
62          }
63          return delta < iTolerance;
64      }
65  
66      void doPixelsMatch(final int x, final int y, final int a, final int b) {
67          if (!componentMatch(a, b, 0, 2) || !componentMatch(a, b, 8, 2) || !componentMatch(a, b, 16, 2) || !componentMatch(a, b, 24, 2)) {
68  
69              final String complaint = String.format("Pixel mismatch at (%d,%d): 0x%08x 0x%08x", x, y, a, b);
70              fail(complaint);
71          }
72      }
73  
74      @Test
75      public void test() throws Exception {
76  
77          // This test will exercise two passes to test the implementation
78          // of the TIFF support for writing and reading images containing
79          // an alpha channel. In the first pass, the alpha writing is enabled
80          // in the second pass it is suppressed.
81          for (int i = 0; i < 2; i++) {
82              // Step 0, create a buffered image that includes transparency
83              // in the form of two rectangles, one completely opaque,
84              // and one giving 50 percent opaque red.
85              final int width = 400;
86              final int height = 400;
87              BufferedImage image0;
88              if (i == 0) {
89                  image0 = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
90              } else {
91                  image0 = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
92              }
93              Graphics2D g2d = image0.createGraphics();
94              g2d.setColor(Color.red);
95              g2d.fillRect(0, 0, width, height);
96              g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
97              g2d.setColor(new Color(0, 0, 0, 0));
98              g2d.fillRect(100, 100, 100, 100);
99              g2d.setColor(new Color(0xff, 0, 0, 0x80));
100             g2d.fillRect(200, 200, 100, 100);
101 
102             // Step 1: write the Buffered Image to an output file and
103             // then read it back in. This action will test the
104             // correctness of a round-trip test.
105             final File file = new File(tempDir.toFile(), "TiffAlphaRoundTripTest.tif");
106             file.delete();
107             Imaging.writeImage(image0, file, ImageFormats.TIFF);
108             final BufferedImage image1 = Imaging.getBufferedImage(file);
109 
110             // Step 2: create a composite image overlaying a white background
111             // with the results from the TIFF file.
112             final BufferedImage compImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
113             g2d = compImage.createGraphics();
114             g2d.setColor(Color.white);
115             g2d.fillRect(0, 0, width, height);
116             g2d.drawImage(image1, 0, 0, null);
117 
118             // Step 3, verify that the correct values are in the image.
119             final int test1 = compImage.getRGB(150, 150); // in the transparent rectangle
120             final int test2 = compImage.getRGB(250, 250);
121             if (i == 0) {
122                 doPixelsMatch(150, 150, 0xffffffff, test1);
123                 doPixelsMatch(250, 250, 0xffff7f7f, test2);
124             } else {
125                 doPixelsMatch(151, 151, 0xff000000, test1);
126                 doPixelsMatch(251, 251, 0xffff0000, test2);
127             }
128         }
129     }
130 
131     @Test
132     void testExtraSamples() throws Exception {
133 
134         final int bytesPerSample = 4;
135         final int width = 10;
136         final int height = 10;
137         final int nBytesPerStrip = bytesPerSample * height * width;
138         final ByteOrder byteOrder = ByteOrder.nativeOrder();
139 
140         final int[] samples = new int[width * height];
141         for (int i = 0; i < 10; i++) {
142             for (int j = 0; j < 10; j++) {
143                 final int index = i * width + j;
144                 samples[index] = j > i ? 0xffff0000 : 0x88ff0000;
145             }
146         }
147 
148         for (int iExtra = 0; iExtra < 3; iExtra++) {
149             final TiffOutputSet outputSet = new TiffOutputSet(byteOrder);
150             final TiffOutputDirectory outDir = outputSet.addRootDirectory();
151             outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width);
152             outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height);
153             outDir.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, (short) 4);
154             outDir.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, new short[] { 8, 8, 8, 8 });
155             outDir.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, (short) TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB);
156             outDir.add(TiffTagConstants.TIFF_TAG_COMPRESSION, (short) TiffTagConstants.COMPRESSION_VALUE_UNCOMPRESSED);
157             outDir.add(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION, (short) TiffTagConstants.PLANAR_CONFIGURATION_VALUE_CHUNKY);
158             outDir.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, height);
159             outDir.add(TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS, nBytesPerStrip);
160 
161             outDir.add(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES, (short) iExtra);
162 
163             final byte[] b = new byte[nBytesPerStrip];
164             int k = 0;
165             for (final int sample : samples) {
166                 b[k++] = (byte) (sample >> 16 & 0xff); // R
167                 b[k++] = (byte) (sample >> 8 & 0xff); // G
168                 b[k++] = (byte) (sample & 0xff); // B
169                 b[k++] = (byte) (sample >> 24 & 0xff); // A
170             }
171 
172             final AbstractTiffElement.DataElement[] imageData = new AbstractTiffElement.DataElement[1];
173             imageData[0] = new AbstractTiffImageData.Data(0, b.length, b);
174 
175             final AbstractTiffImageData abstractTiffImageData = new AbstractTiffImageData.Strips(imageData, height);
176 
177             outDir.setTiffImageData(abstractTiffImageData);
178 
179             final File outputFile = new File(tempDir.toFile(), "TestExtraSamples" + iExtra + ".tiff");
180             try (FileOutputStream fos = new FileOutputStream(outputFile);
181                     BufferedOutputStream bos = new BufferedOutputStream(fos)) {
182                 final TiffImageWriterLossy writer = new TiffImageWriterLossy(byteOrder);
183                 writer.write(bos, outputSet);
184                 bos.flush();
185             }
186 
187             final BufferedImage result = Imaging.getBufferedImage(outputFile);
188             final int[] argb = new int[samples.length];
189             result.getRGB(0, 0, width, height, argb, 0, width);
190             final int index = 3 * width + 1;
191             int iSample = samples[index];
192             final int iArgb = argb[index];
193             if (iExtra == 0) {
194                 // when extra samples is zero, the alpha channel is ignored.
195                 // We expect ARGB to start with 0xff. So we OR in 0xff for
196                 // the alpha value of the sample
197                 iSample |= 0xff000000;
198             } else if (iExtra == 1) {
199                 // The pre-multiply alpha case
200                 iSample = 0x89de0000;
201             }
202             final String p = String.format("%08x", iSample);
203             final String q = String.format("%08x", iArgb);
204             assertEquals(p, q, "Failure on ExtraSamples=" + iExtra);
205         }
206     }
207 }