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.examples.tiff;
18  
19  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_1D;
20  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3;
21  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_4;
22  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_ADOBE;
23  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_DEFLATE_PKZIP;
24  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_LZW;
25  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_PACKBITS;
26  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED;
27  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED_1;
28  
29  import java.io.File;
30  import java.io.IOException;
31  import java.io.PrintStream;
32  import java.util.Formatter;
33  
34  import org.apache.commons.imaging.FormatCompliance;
35  import org.apache.commons.imaging.ImagingException;
36  import org.apache.commons.imaging.bytesource.ByteSource;
37  import org.apache.commons.imaging.formats.tiff.TiffContents;
38  import org.apache.commons.imaging.formats.tiff.TiffDirectory;
39  import org.apache.commons.imaging.formats.tiff.TiffField;
40  import org.apache.commons.imaging.formats.tiff.TiffReader;
41  import org.apache.commons.imaging.formats.tiff.constants.TiffEpTagConstants;
42  import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
43  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
44  
45  /**
46   * Provides methods to collect data for a tiff file. This class is intended for use with SurveyTiffFolder, though it could be integrated into other
47   * applications.
48   */
49  public class SurveyTiffFile {
50  
51      /**
52       * Formats a header allowing space for the maximum length of the file paths in the list. If the comma-separated-value option is set, spaces will be
53       * suppressed and commas introduced as separators.
54       *
55       * @param maxPathLen the maximum length of a file path (used if csv option is not set)
56       * @param csv        true if formatting is configured for comma-separated-value files.
57       * @return a valid string.
58       */
59      String formatHeader(final int maxPathLen, final boolean csv) {
60          // After some false starts, it turned out that the easiest
61          // way to do this is just to create a regular header and then
62          // search-and-replace spaces with comma as appropriate.
63          int n = maxPathLen;
64          if (n < 10) {
65              n = 10;
66          }
67          final int k0 = (n - 4) / 2;
68          final int k1 = n - 4 - k0;
69  
70          final String header = String.format("%" + k0 + "sPath%" + k1 + "s%s", "", "",
71                  "    Size     Layout  Blk_sz     P_conf  Compress  " + "Predict  Data_Fmt   B/P B/S      Photo     ICC_Pro");
72          if (csv) {
73              return reformatHeaderForCsv(header);
74          }
75          return header;
76      }
77  
78      String getBitsPerSampleString(final int[] bitsPerSample) {
79          final StringBuilder s = new StringBuilder();
80          for (int i = 0; i < bitsPerSample.length; i++) {
81              if (i > 0) {
82                  s.append(".");
83              }
84              s.append(Integer.toString(bitsPerSample[i], 10));
85          }
86          return s.toString();
87      }
88  
89      private String getCompressionString(final TiffDirectory directory) throws ImagingException {
90          final short compressionFieldValue;
91          if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) {
92              compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION);
93          } else {
94              compressionFieldValue = TIFF_COMPRESSION_UNCOMPRESSED_1;
95          }
96          final int compression = 0xffff & compressionFieldValue;
97          switch (compression) {
98          case TIFF_COMPRESSION_UNCOMPRESSED: // None;
99              return "None";
100         case TIFF_COMPRESSION_CCITT_1D: // CCITT Group 3 1-Dimensional Modified
101             // Huffman run-length encoding.
102             return "CCITT_1D";
103         case TIFF_COMPRESSION_CCITT_GROUP_3:
104             return "CCITT_3";
105         case TIFF_COMPRESSION_CCITT_GROUP_4:
106             return "CCITT_4";
107         case TIFF_COMPRESSION_LZW:
108             return "LZW";
109         case TIFF_COMPRESSION_PACKBITS:
110             return "PACKBITS";
111         case TIFF_COMPRESSION_DEFLATE_ADOBE:
112         case TIFF_COMPRESSION_DEFLATE_PKZIP:
113             return "Deflate";
114         default:
115             return "None";
116         }
117     }
118 
119     String getIccProfileString(final TiffDirectory directory) throws ImagingException {
120         final byte[] b = directory.getFieldValue(TiffEpTagConstants.EXIF_TAG_INTER_COLOR_PROFILE, false);
121         if (b == null || b.length == 0) {
122             return "N";
123         }
124         return "Y";
125     }
126 
127     private String getPhotometricInterpreterString(final TiffDirectory directory, final int[] bitsPerSample) throws ImagingException {
128         final int photometricInterpretation = 0xffff & directory.getFieldValue(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION);
129 
130         switch (photometricInterpretation) {
131         case 0:
132             return "BiLev Inv";
133         case 1:
134             return "BiLevel";
135         case 2:
136             String a = "RGB";
137             if (bitsPerSample.length == 4) {
138                 final Object o = directory.getFieldValue(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES);
139                 short extraSamples = 0;
140                 if (o instanceof Short) {
141                     extraSamples = (Short) o;
142                 }
143                 if (extraSamples == 1) {
144                     a = "RGB Pre-A";
145                 } else {
146                     a = "RGBA";
147                 }
148             }
149 
150             return a;
151         case 3:
152             return "Palette";
153 
154         case 5: // CMYK
155             return "CMYK";
156         case 6:
157             return "YCbCr";
158         case 8:
159             return "CieLab";
160 
161         case 32844:
162         case 32845:
163             return "LogLuv";
164         default:
165             return "Unknown";
166 
167         }
168 
169     }
170 
171     String getPlanarConfigurationString(final TiffDirectory directory) throws ImagingException {
172 
173         // Obtain the planar configuration
174         final TiffField pcField = directory.findField(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION);
175         final TiffPlanarConfiguration planarConfiguration = pcField == null ? TiffPlanarConfiguration.CHUNKY
176                 : TiffPlanarConfiguration.lenientValueOf(pcField.getIntValue());
177 
178         if (planarConfiguration == TiffPlanarConfiguration.CHUNKY) {
179             return "Chunky";
180         }
181         return "Planar";
182     }
183 
184     String getPredictorString(final TiffDirectory directory) throws ImagingException {
185         int predictor = -1;
186 
187         final TiffField predictorField = directory.findField(TiffTagConstants.TIFF_TAG_PREDICTOR);
188         if (null != predictorField) {
189             predictor = predictorField.getIntValueOrArraySum();
190         }
191 
192         switch (predictor) {
193         case TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING:
194             return "Diff";
195         case TiffTagConstants.PREDICTOR_VALUE_FLOATING_POINT_DIFFERENCING:
196             return "FP Diff";
197         default:
198             return "None";
199 
200         }
201     }
202 
203     String getSampleFormatString(final TiffDirectory directory) throws ImagingException {
204         final short[] sSampleFmt = directory.getFieldValue(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, false);
205         if (sSampleFmt == null || sSampleFmt.length == 0) {
206             return "Unknown";
207         }
208         String heterogeneous = "";
209         for (int i = 1; i < sSampleFmt.length; i++) {
210             if (sSampleFmt[i] != sSampleFmt[0]) {
211                 heterogeneous = "*";
212                 break;
213             }
214         }
215         final int test = sSampleFmt[0];
216         switch (test) {
217         case TiffTagConstants.SAMPLE_FORMAT_VALUE_COMPLEX_INTEGER:
218             return "Comp I" + heterogeneous;
219         case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT:
220             return "Float" + heterogeneous;
221         case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_COMPLEX_FLOAT:
222             return "Comp F" + heterogeneous;
223         case TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER:
224             return "Sgn Int" + heterogeneous;
225         case TiffTagConstants.SAMPLE_FORMAT_VALUE_UNSIGNED_INTEGER:
226             return "Uns Int" + heterogeneous;
227         default:
228             return "Unknown" + heterogeneous;
229         }
230     }
231 
232     /**
233      * Prints the legend information to the output stream
234      *
235      * @param ps a valid instance
236      */
237     void printLegend(final PrintStream ps) {
238         ps.println("Legend:");
239         ps.println("  Size      Size of image (width-by-height)");
240         ps.println("  Layout    Organization of the image file (strips versus tiles)");
241         ps.println("  Blk_sz    Size of internal image blocks (strips versus tiles)");
242         ps.println("  P_conf    Planar configuration, Chunky (interleaved samples) versus Planar ");
243         ps.println("  Compress  Compression format");
244         ps.println("  Predict   Predictor");
245         ps.println("  Data_Fmt  Data format");
246         ps.println("  B/P       Bits per pixel");
247         ps.println("  B/S       Bits per sample");
248         ps.println("  Photo     Photometric Interpretation (pixel color type)");
249         ps.println("  ICC_Pro   Is ICC color profile supplied");
250         ps.println("");
251         ps.println("  RGBA       RGB with unassociated alpha (transparency)");
252         ps.println("  RGBA_Pre-A RGB with associated (premultiplied) alpha");
253         ps.println("");
254     }
255 
256     /**
257      * Reformats the header inserting commas and removing spaces
258      *
259      * @param s a valid string
260      * @return a header suitable for a CSV file.
261      */
262     private String reformatHeaderForCsv(final String s) {
263         final StringBuilder sb = new StringBuilder(s.length());
264         boolean enableComma = false;
265         for (int i = 0; i < s.length(); i++) {
266             char c = s.charAt(i);
267             if (Character.isWhitespace(c)) {
268                 if (enableComma) {
269                     enableComma = false;
270                     sb.append(',');
271                 }
272             } else {
273                 enableComma = true;
274                 if (Character.isUpperCase(c)) {
275                     c = Character.toLowerCase(c);
276                 }
277                 sb.append(c);
278             }
279         }
280         return sb.toString();
281     }
282 
283     public String surveyFile(final File file, final boolean csv) throws ImagingException, IOException {
284         String delimiter = "  ";
285         if (csv) {
286             delimiter = ", ";
287         }
288 
289         final StringBuilder sb = new StringBuilder();
290         try (final Formatter fmt = new Formatter(sb)) {
291 
292             // Establish a TiffReader. This is just a simple constructor that
293             // does not actually access the file. So the application cannot
294             // obtain the byteOrder, or other details, until the contents have
295             // been read. Then read the directories associated with the
296             // file by passing in the byte source and options.
297             final ByteSource byteSource = ByteSource.file(file);
298             final TiffReader tiffReader = new TiffReader(true);
299             final TiffContents contents = tiffReader.readDirectories(byteSource, false, // read image data, if present
300                     FormatCompliance.getDefault());
301 
302             if (contents.directories.isEmpty()) {
303                 throw new ImagingException("No Image File Directory (IFD) found");
304             }
305             final TiffDirectory directory = contents.directories.get(0);
306 
307             // Get the metadata (Tags) and write them to standard output
308             final boolean hasTiffImageData = directory.hasTiffImageData();
309             if (!hasTiffImageData) {
310                 throw new ImagingException("No image data in file");
311             }
312 
313             final int width = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH);
314             final int height = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);
315 
316             int samplesPerPixel = 1;
317             final TiffField samplesPerPixelField = directory.findField(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL);
318             if (samplesPerPixelField != null) {
319                 samplesPerPixel = samplesPerPixelField.getIntValue();
320             }
321             int[] bitsPerSample = { 1 };
322             int bitsPerPixel = samplesPerPixel;
323             final TiffField bitsPerSampleField = directory.findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE);
324             if (bitsPerSampleField != null) {
325                 bitsPerSample = bitsPerSampleField.getIntArrayValue();
326                 bitsPerPixel = bitsPerSampleField.getIntValueOrArraySum();
327             }
328             if (samplesPerPixel != bitsPerSample.length) {
329                 throw new ImagingException("Tiff: samplesPerPixel (" + samplesPerPixel + ")!=fBitsPerSample.length (" + bitsPerSample.length + ")");
330             }
331 
332             int rowsPerStrip = 0;
333             int tileWidth = 0;
334             int tileHeight = 0;
335 
336             final boolean imageDataInStrips = directory.imageDataInStrips();
337             if (imageDataInStrips) {
338                 final TiffField rowsPerStripField = directory.findField(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP);
339                 rowsPerStrip = Integer.MAX_VALUE;
340                 if (null != rowsPerStripField) {
341                     rowsPerStrip = rowsPerStripField.getIntValue();
342                 } else {
343                     final TiffField imageHeight = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);
344                     /*
345                      * if rows per strip not present then rowsPerStrip is equal to imageLength or an infinity value;
346                      */
347                     if (imageHeight != null) {
348                         rowsPerStrip = imageHeight.getIntValue();
349                     }
350                 }
351             } else {
352                 final TiffField tileWidthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_WIDTH);
353                 if (null == tileWidthField) {
354                     throw new ImagingException("Can't find tile width field.");
355                 }
356                 tileWidth = tileWidthField.getIntValue();
357                 final TiffField tileLengthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_LENGTH);
358                 if (null == tileLengthField) {
359                     throw new ImagingException("Can't find tile length field.");
360                 }
361                 tileHeight = tileLengthField.getIntValue();
362             }
363 
364             final String compressionString = getCompressionString(directory);
365             final String predictorString = getPredictorString(directory);
366             final String planarConfigurationString = getPlanarConfigurationString(directory);
367             final String bitsPerSampleString = getBitsPerSampleString(bitsPerSample);
368             final String sampleFmtString = getSampleFormatString(directory);
369             final String piString = getPhotometricInterpreterString(directory, bitsPerSample);
370             final String iccString = getIccProfileString(directory);
371 
372             fmt.format("%s%4dx%-4d", delimiter, width, height);
373             if (imageDataInStrips) {
374                 fmt.format("%sStrips%s%4dx%-4d", delimiter, delimiter, width, rowsPerStrip);
375             } else {
376                 fmt.format("%sTiles %s%4dx%-4d", delimiter, delimiter, tileWidth, tileHeight);
377             }
378 
379             fmt.format("%s%s", delimiter, planarConfigurationString);
380             fmt.format("%s%-8s", delimiter, compressionString);
381             fmt.format("%s%-7s", delimiter, predictorString);
382             fmt.format("%s%-8s", delimiter, sampleFmtString);
383             fmt.format("%s%3d", delimiter, bitsPerPixel);
384             fmt.format("%s%-7s", delimiter, bitsPerSampleString);
385             fmt.format("%s%-9s", delimiter, piString);
386             fmt.format("%s%-7s", delimiter, iccString);
387 
388             if (csv) {
389                 return trimForCsv(sb);
390             }
391             return sb.toString();
392         }
393     }
394 
395     /**
396      * Trims spaces from a range of characters intended for a CSV output
397      *
398      * @param source the standard source file
399      * @return the equivalent string with spaces removed.
400      */
401     private String trimForCsv(final StringBuilder source) {
402         int n = source.length();
403         final StringBuilder sb = new StringBuilder(n);
404         boolean spaceEnabled = false;
405         boolean spacePending = false;
406         for (int i = 0; i < n; i++) {
407             final char c = source.charAt(i);
408             if (Character.isWhitespace(c)) {
409                 if (spaceEnabled) {
410                     spacePending = true;
411                     spaceEnabled = false;
412                 }
413             } else {
414 
415                 if (Character.isLetter(c) || Character.isDigit(c)) {
416                     if (spacePending) {
417                         sb.append(' ');
418                         spacePending = false;
419                     }
420                     spaceEnabled = true;
421                 } else {
422                     spacePending = false;
423                     spaceEnabled = false;
424                 }
425                 sb.append(c);
426             }
427         }
428         n = sb.length();
429         if (n > 0 && sb.charAt(n - 1) == ' ') {
430             sb.setLength(n - 1);
431         }
432         return sb.toString();
433     }
434 }