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 java.awt.image.BufferedImage;
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.PrintStream;
23  import java.util.HashMap;
24  import java.util.List;
25  
26  import javax.imageio.ImageIO;
27  
28  import org.apache.commons.imaging.FormatCompliance;
29  import org.apache.commons.imaging.ImagingException;
30  import org.apache.commons.imaging.bytesource.ByteSource;
31  import org.apache.commons.imaging.formats.tiff.TiffContents;
32  import org.apache.commons.imaging.formats.tiff.TiffDirectory;
33  import org.apache.commons.imaging.formats.tiff.TiffField;
34  import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
35  import org.apache.commons.imaging.formats.tiff.TiffReader;
36  import org.apache.commons.imaging.formats.tiff.constants.GdalLibraryTagConstants;
37  import org.apache.commons.imaging.formats.tiff.constants.GeoTiffTagConstants;
38  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
39  
40  /**
41   * Provides a example application showing how to access metadata and imagery from TIFF files using the low-level access routines. This approach is especially
42   * useful if the TIFF file includes multiple images.
43   */
44  public class ReadTagsAndImages {
45  
46      /**
47       * Provides specifications for Coordinate Transformation Codes as defined in Appendix 6.3.3.3 "Coordinate Transformation Codes" of the original GeoTiff
48       * specification (Ritter, 1995).
49       */
50      enum CoordinateTransformationCode {
51          TransverseMercator(1), TransvMercator_Modified_Alaska(2), ObliqueMercator(3), ObliqueMercator_Laborde(4), ObliqueMercator_Rosenmund(5),
52          ObliqueMercator_Spherical(6), Mercator(7), LambertConfConic_2SP(8), LambertConfConic_Helmert(9), LambertAzimEqualArea(10), AlbersEqualArea(11),
53          AzimuthalEquidistant(12), EquidistantConic(13), Stereographic(14), PolarStereographic(15), ObliqueStereographic(16), Equirectangular(17),
54          CassiniSoldner(18), Gnomonic(19), MillerCylindrical(20), Orthographic(21), Polyconic(22), Robinson(23), Sinusoidal(24), VanDerGrinten(25),
55          NewZealandMapGrid(26), TransvMercator_SouthOriented(27);
56  
57          /**
58           * Gets the enumeration value associated with the specified key if any.
59           *
60           * @param key a positive integer
61           * @return if the key is matched, a value enumeration value; otherwise, a null.
62           */
63          static CoordinateTransformationCode getValueForKey(final int key) {
64              for (final CoordinateTransformationCode v : values()) {
65                  if (v.key == key) {
66                      return v;
67                  }
68              }
69              return null;
70          }
71  
72          int key;
73  
74          CoordinateTransformationCode(final int key) {
75              this.key = key;
76          }
77      }
78  
79      enum GeoKey {
80          // From 6.2.1 GeoTiff Configuration Keys
81          GTModelTypeGeoKey(1024), /* Section 6.3.1.1 Codes */
82          GTRasterTypeGeoKey(1025), /* Section 6.3.1.2 Codes */
83          GTCitationGeoKey(1026), /* documentation */
84  
85          // From 6.2.2 Geographic Coordinate System Parameter Keys
86          GeographicTypeGeoKey(2048), /* Section 6.3.2.1 Codes */
87          GeogCitationGeoKey(2049), /* documentation */
88          GeogGeodeticDatumGeoKey(2050), /* Section 6.3.2.2 Codes */
89          GeogPrimeMeridianGeoKey(2051), /* Section 6.3.2.4 codes */
90          GeogLinearUnitsGeoKey(2052), /* Section 6.3.1.3 Codes */
91          GeogLinearUnitSizeGeoKey(2053), /* meters */
92          GeogAngularUnitsGeoKey(2054), /* Section 6.3.1.4 Codes */
93          GeogAngularUnitSizeGeoKey(2055), /* radians */
94          GeogEllipsoidGeoKey(2056), /* Section 6.3.2.3 Codes */
95          GeogSemiMajorAxisGeoKey(2057), /* GeogLinearUnits */
96          GeogSemiMinorAxisGeoKey(2058), /* GeogLinearUnits */
97          GeogInvFlatteningGeoKey(2059), /* ratio */
98          GeogAzimuthUnitsGeoKey(2060), /* Section 6.3.1.4 Codes */
99          GeogPrimeMeridianLongGeoKey(2061), /* GeogAngularUnit */
100 
101         // From 6.2.3 Projected Coordinate System Parameter Keys
102         ProjectedCRSGeoKey(3072), /* Section 6.3.3.1 codes */
103         PCSCitationGeoKey(3073), /* documentation */
104         ProjectionGeoKey(3074), /* Section 6.3.3.2 codes */
105         ProjCoordTransGeoKey(3075), /* Section 6.3.3.3 codes */
106         ProjLinearUnitsGeoKey(3076), /* Section 6.3.1.3 codes */
107         ProjLinearUnitSizeGeoKey(3077), /* meters */
108         ProjStdParallel1GeoKey(3078), /* GeogAngularUnit */
109         ProjStdParallel2GeoKey(3079), /* GeogAngularUnit */
110         ProjNatOriginLongGeoKey(3080), /* GeogAngularUnit */
111         ProjNatOriginLatGeoKey(3081), /* GeogAngularUnit */
112         ProjFalseEastingGeoKey(3082), /* ProjLinearUnits */
113         ProjFalseNorthingGeoKey(3083), /* ProjLinearUnits */
114         ProjFalseOriginLongGeoKey(3084), /* GeogAngularUnit */
115         ProjFalseOriginLatGeoKey(3085), /* GeogAngularUnit */
116         ProjFalseOriginEastingGeoKey(3086), /* ProjLinearUnits */
117         ProjFalseOriginNorthingGeoKey(3087), /* ProjLinearUnits */
118         ProjCenterLongGeoKey(3088), /* GeogAngularUnit */
119         ProjCenterLatGeoKey(3089), /* GeogAngularUnit */
120         ProjCenterEastingGeoKey(3090), /* ProjLinearUnits */
121         ProjCenterNorthingGeoKey(3091), /* ProjLinearUnits */
122         ProjScaleAtNatOriginGeoKey(3092), /* ratio */
123         ProjScaleAtCenterGeoKey(3093), /* ratio */
124         ProjAzimuthAngleGeoKey(3094), /* GeogAzimuthUnit */
125         ProjStraightVertPoleLongGeoKey(3095), /* GeogAngularUnit */
126         // From 6.2.4 Vertical Coordinate System Keys
127         VerticalCSTypeGeoKey(4096), /* Section 6.3.4.1 codes */
128         VerticalCitationGeoKey(4097), /* documentation */
129         VerticalDatumGeoKey(4098), /* Section 6.3.4.2 codes */
130         VerticalUnitsGeoKey(4099), /* Section 6.3.1.3 codes */
131 
132         // Widely used key not defined in original specification
133         To_WGS84_GeoKey(2062); /* Not in original spec */
134 
135         int key;
136 
137         GeoKey(final int key) {
138             this.key = key;
139         }
140     }
141 
142     // The following elements were copied from the original
143     // GeoTIFF specification document
144     // Ritter, Niles and Ruth, Mike (1995). GeoTIFF Format Specification,
145     // GeoTIFF Revision 1.0. Specification Version 1.8.1. 31 October 1995
146     // Appendix 6.
147     // See also:
148     // Open Geospatial Consortium [OGC] (2019) OGC GeoTIFF Standard Version 1.1
149     // http://www.opengis.net/doc/IS/GeoTIFF/1.1
150 
151     private static final String[] USAGE = { "Usage ReadTagsAndImages <input file>  [output file]", "   input file: mandatory file to be read",
152             "   output file: optional root name and path for files to be written" };
153 
154     private static HashMap<Integer, GeoKey> keyMap;
155     private static String nameFormat;
156 
157     /**
158      * Extract a sub-string from the ASCII parameters. The ASCII parameters include a vertical-bar symbol to act as a separator between strings. This method
159      * includes some safety-checking logic which should not be necessary except in the case of a badly formatted GeoTIFF file.
160      *
161      * @param asciiParameters the content of TIFF Tag ID=34737.
162      * @param pos             the position of the sub-string within the content
163      * @param len             the length of the sub-string.
164      * @return a valid string.
165      */
166     private static String extractAscii(final String asciiParameters, final int pos, final int len) {
167         if (asciiParameters != null && len > 0 && pos + len <= asciiParameters.length()) {
168             return asciiParameters.substring(pos, pos + len - 1);
169         }
170         return "~~~";
171     }
172 
173     /**
174      * Extract a string giving the floating-point values for the content taken from TIFF Tag ID=34736. Because a TAG might potentially include a large number of
175      * entries, this method limits the return value to the first three entries. This method includes some safety-checking logic which should not be necessary
176      * except in the case of a badly formed GeoTIFF file.
177      *
178      * @param doubleParameters an array of values
179      * @param pos              the starting position of the values for the GeoKey
180      * @param len              the number of values for the GeoKey
181      * @return a formatted string.
182      */
183     private static String extractDouble(final double[] doubleParameters, final int pos, final int len) {
184         if (doubleParameters != null && doubleParameters.length >= pos + len) {
185             final StringBuilder sb = new StringBuilder();
186             for (int i = 0; i < len && i < 3; i++) {
187                 if (i > 0) {
188                     sb.append(" | ");
189                 }
190                 sb.append(Double.toString(doubleParameters[pos + i]));
191             }
192             if (len > 3) {
193                 sb.append(" | ...");
194             }
195             return sb.toString();
196         }
197         return "~~~";
198     }
199 
200     /**
201      * Interprets elements from one row of the GeoKey table, returning a descriptive string where possible. The GeoTIFF specification is extensive, and only a
202      * subset of the possible descriptions are supported here.
203      *
204      * @param geoKey           a valid GeoKey enumeration
205      * @param ref              the reference (not used at this time)
206      * @param len              the length of the associated string or floating-point value array (if used)
207      * @param valueOrPosition  a single integer value or the position within the associated floating point array
208      * @param doubleParameters an array of doubles
209      * @param asciiParameters  a String consisting of ASCII characters.
210      * @return a valid string, potentially a note to see the specification if a more useful description is not available.
211      */
212     private static String interpretElements(final GeoKey geoKey, final int ref, final int len, final int valueOrPosition, final double[] doubleParameters,
213             final String asciiParameters) {
214         switch (geoKey) {
215         case GTModelTypeGeoKey:
216             switch (valueOrPosition) {
217             case 1:
218                 return "Projected Coordinate System";
219             case 2:
220                 return "Geographic Coordinate System";
221             case 3:
222                 // the Geocentric coordinate system is seldom used
223                 return "Geocentric Coordinate System";
224             default:
225                 break;
226             }
227         case GTRasterTypeGeoKey:
228             switch (valueOrPosition) {
229             case 1:
230                 return "RasterPixelIsArea";
231             case 2:
232                 return "RasterPixelIsPoint";
233             default:
234                 return "User Defined";
235             }
236         case GeographicTypeGeoKey:
237             switch (valueOrPosition) {
238             case 4269:
239                 return "North American Datum 1983";
240             case 4030:
241                 return "World Geodetic Survey 1984";
242             case 4326:
243                 return "EPSG 4326, Geographic 2D WGS 84";
244             default:
245                 break;
246             }
247         case GTCitationGeoKey:
248             return extractAscii(asciiParameters, valueOrPosition, len);
249         case GeogCitationGeoKey:
250             return extractAscii(asciiParameters, valueOrPosition, len);
251         case GeogAngularUnitsGeoKey:
252             switch (valueOrPosition) {
253             case 9101:
254                 return "Radians";
255             case 9102:
256                 return "Degrees";
257             default:
258                 break;
259             }
260         case GeogSemiMajorAxisGeoKey:
261             return extractDouble(doubleParameters, valueOrPosition, len);
262         case GeogInvFlatteningGeoKey:
263             return extractDouble(doubleParameters, valueOrPosition, len);
264         case To_WGS84_GeoKey:
265             return extractDouble(doubleParameters, valueOrPosition, len);
266         case ProjectedCRSGeoKey:
267             // in original spec was "ProjectedCSTypeGeoKey"
268             if (0 <= valueOrPosition && valueOrPosition <= 1023) {
269                 return "Reserved";
270             }
271             if (1024 <= valueOrPosition && valueOrPosition <= 32766) {
272                 return "EPSG Code #" + valueOrPosition;
273             }
274             if (valueOrPosition == 32767) {
275                 return "User-Defined Projection";
276             }
277             break;
278         case ProjectionGeoKey:
279             if (valueOrPosition == 32767) {
280                 return "User-Defined";
281             }
282             break;
283         case ProjCoordTransGeoKey:
284             final CoordinateTransformationCode code = CoordinateTransformationCode.getValueForKey(valueOrPosition);
285             if (code != null) {
286                 return code.name();
287             }
288             break;
289         case ProjLinearUnitsGeoKey:
290             switch (valueOrPosition) {
291             case 9001:
292                 return "Meter";
293             case 9002:
294                 return "Foot";
295             case 9003:
296                 return "Survey Foot"; // used in U.S.
297             default:
298                 break;
299             }
300             break;
301         case ProjStdParallel1GeoKey:
302         case ProjStdParallel2GeoKey:
303         case ProjNatOriginLongGeoKey:
304         case ProjFalseEastingGeoKey:
305         case ProjFalseNorthingGeoKey:
306         case ProjFalseOriginLongGeoKey:
307         case ProjFalseOriginLatGeoKey:
308         case ProjFalseOriginEastingGeoKey:
309         case ProjFalseOriginNorthingGeoKey:
310         case ProjCenterLongGeoKey:
311         case ProjCenterLatGeoKey:
312             return String.format("%13.4f", doubleParameters[valueOrPosition]);
313 
314         default:
315             break;
316         }
317         return "See GeoTIFF specification";
318     }
319 
320     /**
321      * Open the specified TIFF file and print its metadata (fields) to standard output. If an output root-name is specified, write images to specified path.
322      *
323      * @param args the command line arguments
324      * @throws org.apache.commons.imaging.ImagingException in the event of an internal data format or version compatibility error reading the image.
325      * @throws IOException                                 in the event of an I/O error.
326      */
327     public static void main(final String[] args) throws ImagingException, IOException {
328         if (args.length == 0) {
329             // Print usage and exit
330             for (final String s : USAGE) {
331                 System.err.println(s);
332             }
333             System.exit(0);
334         }
335 
336         // For brevity, map System.out to a PrintStream reference.
337         // In the future, this might also be used for writing to a text file
338         // rather than standard output.
339         final PrintStream ps = System.out;
340 
341         final File target = new File(args[0]);
342         String rootName = null;
343         if (args.length == 2) {
344             rootName = args[1];
345         }
346         final boolean optionalImageReadingEnabled = rootName != null && !rootName.isEmpty();
347 
348         final ByteSource byteSource = ByteSource.file(target);
349         final TiffImagingParameters params = new TiffImagingParameters();
350 
351         // Establish a TiffReader. This is just a simple constructor that
352         // does not actually access the file. So the application cannot
353         // obtain the byteOrder, or other details, until the contents has
354         // been read. Then read the directories associated with the
355         // file by passing in the byte source and options.
356         final TiffReader tiffReader = new TiffReader(true);
357         final TiffContents contents = tiffReader.readDirectories(byteSource, optionalImageReadingEnabled, // read image data, if present
358                 FormatCompliance.getDefault());
359 
360         // Loop on the directories and fetch the metadata and
361         // image (if available, and configured to do so)
362         int iDirectory = 0;
363         for (final TiffDirectory directory : contents.directories) {
364             // Get the metadata (Tags) and write them to standard output
365             final boolean hasTiffImageData = directory.hasTiffImageData();
366             if (iDirectory > 0) {
367                 ps.println("\n-----------------------------------------------------\n");
368             }
369 
370             String contentType = "";
371             if (directory.hasTiffRasterData()) {
372                 contentType = "Numeric raster data";
373             } else if (directory.hasTiffImageData()) {
374                 contentType = "Image data";
375             }
376             ps.format("Directory %2d %s, description: %s%n", iDirectory, contentType, directory.description());
377             // Loop on the fields, printing the metadata (fields)
378             final List<TiffField> fieldList = directory.getDirectoryEntries();
379             for (final TiffField tiffField : fieldList) {
380                 String s = tiffField.toString();
381                 if (s.length() > 90) {
382                     s = s.substring(0, 90);
383                 }
384                 // In the case if the offsets (file positions) for the Strips
385                 // or Tiles, the string may be way too long for output and
386                 // will be truncated. Therefore, indicate the numnber of entries.
387                 // These fields are indicated by numerical tags 0x144 and 0x145
388                 if (tiffField.getTag() == 0x144 || tiffField.getTag() == 0x145) {
389                     final int i = s.indexOf(')');
390                     final int[] a = tiffField.getIntArrayValue();
391                     s = s.substring(0, i + 2) + " [" + a.length + " entries]";
392                 }
393                 ps.println(" " + s);
394             }
395 
396             summarizeGeoTiffTags(ps, directory);
397 
398             if (optionalImageReadingEnabled && hasTiffImageData) {
399                 final File output = new File(rootName + "_" + iDirectory + ".jpg");
400                 ps.println("Writing image to " + output.getPath());
401                 final BufferedImage bImage = directory.getTiffImage(params);
402                 ImageIO.write(bImage, "JPEG", output);
403             }
404             ps.println("");
405             iDirectory++;
406         }
407     }
408 
409     /**
410      * Checks to see if the directory has GeoTIFF tags and, if so, provides a summary of their content.
411      *
412      * @param ps        a valid instance to receive output
413      * @param directory a valid directory
414      * @throws ImagingException in the event of a data-format error or unhandled I/O error.
415      */
416     private static void summarizeGeoTiffTags(final PrintStream ps, final TiffDirectory directory) throws ImagingException {
417 
418         if (keyMap == null) {
419             final GeoKey[] values = GeoKey.values();
420             int maxNameLength = 0;
421             keyMap = new HashMap<>();
422             for (final GeoKey g : values) {
423                 final String name = g.name();
424                 if (name.length() > maxNameLength) {
425                     maxNameLength = name.length();
426                 }
427                 keyMap.put(g.key, g);
428             }
429             // create a formatting string that will pad all names
430             // out with trailing spaces to provide text alignment in code below.
431             nameFormat = String.format("   %%-%ds", maxNameLength);
432         }
433 
434         // check to see if the directory has GeoTIFF tags.
435         final short[] geoKeyDirectory = directory.getFieldValue(GeoTiffTagConstants.EXIF_TAG_GEO_KEY_DIRECTORY_TAG, false);
436         if (geoKeyDirectory == null || geoKeyDirectory.length < 4) {
437             // The TIFF directory does not contain GeoTIFF information
438             return;
439         }
440         ps.println("");
441         ps.println("Summary of GeoTIFF Elements ----------------------------");
442 
443         final short[] bitsPerSample = directory.getFieldValue(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, false);
444         final short[] sampleFormat = directory.getFieldValue(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, false);
445         String contentTypeString = null;
446         if (bitsPerSample != null && sampleFormat != null) {
447             if (bitsPerSample[0] == 16 && sampleFormat[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER) {
448                 contentTypeString = "Numeric, Short Integer";
449             } else if ((bitsPerSample[0] == 32 || bitsPerSample[0] == 64) && sampleFormat[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT) {
450                 contentTypeString = "Numeric, Floating Point (" + bitsPerSample[0] + "-bit samples)";
451             }
452         }
453         if (contentTypeString != null) {
454             ps.format("%nContent Type: %s", contentTypeString);
455             final String[] gdalNoDataString = directory.getFieldValue(GdalLibraryTagConstants.EXIF_TAG_GDAL_NO_DATA, false);
456             if (gdalNoDataString != null && gdalNoDataString.length > 0) {
457                 ps.format("    GDAL No-Data value: %s", gdalNoDataString[0]);
458             }
459             ps.format("%n");
460         }
461 
462         // all GeoKeyDirectory elements are unsigned shorts (2 bytes).
463         // Some of which exceed the value 32767, the maximum value of
464         // a signed short). Because Java does not support an unsigned short type,
465         // we need to mask in the low-order 2 bytes and obtain a 4-byte integer
466         // equivalent.
467         final int[] elements = new int[geoKeyDirectory.length];
468         for (int i = 0; i < geoKeyDirectory.length; i++) {
469             elements[i] = geoKeyDirectory[i] & 0xffff;
470         }
471 
472         // Get the double field, Tag ID=34736 (0x87B0). This field
473         // will usually be present. Some TIFF products supply only the
474         // European Petroleum Survey Group (EPSG) map projection ID
475         // and may omit the floating-point map-projection parameters.
476         // That approach is generally discouraged, but not prohibited.
477         final TiffField doubleParametersField = directory.findField(GeoTiffTagConstants.EXIF_TAG_GEO_DOUBLE_PARAMS_TAG);
478         double[] doubleParameters = null;
479         if (doubleParametersField != null) {
480             doubleParameters = doubleParametersField.getDoubleArrayValue();
481         }
482 
483         // Get the ASCII field, Tag ID=34737 (0x87B1). This field
484         // is often, but not always, present.
485         final TiffField asciiParametersField = directory.findField(GeoTiffTagConstants.EXIF_TAG_GEO_ASCII_PARAMS_TAG);
486         String asciiParameters = null;
487         if (asciiParametersField != null) {
488             asciiParameters = asciiParametersField.getStringValue();
489         }
490 
491         ps.format("%nGeoKey Table%n");
492         ps.println("     key     ref     len   value/pos     name");
493         int k = 0;
494         int n = elements.length / 4;
495         for (int i = 0; i < n; i++) {
496             final int key = elements[k];
497             final int ref = elements[k + 1];
498             final int len = elements[k + 2];
499             final int vop = elements[k + 3];
500             String label = "";
501             if (ref == GeoTiffTagConstants.EXIF_TAG_GEO_ASCII_PARAMS_TAG.tag) {
502                 label = "(A)";
503             } else if (ref == GeoTiffTagConstants.EXIF_TAG_GEO_DOUBLE_PARAMS_TAG.tag) {
504                 label = "(D)";
505             }
506             for (int j = 0; j < 4; j++) {
507                 ps.format("%8d", elements[k++]);
508             }
509             ps.format("   %-3s", label);
510 
511             // The first four elements in the integer array are not
512             // actually a GeoKey, but rather an overall identifier
513             GeoKey geoKey;
514             String name;
515             String interpretation;
516             if (i == 0) {
517                 name = "~~~";
518                 interpretation = "~~~";
519             } else {
520                 geoKey = keyMap.get(key);
521                 if (geoKey == null) {
522                     name = "Unknown GeoKey";
523                     interpretation = "~~~";
524                 } else {
525                     name = geoKey.name();
526                     interpretation = interpretElements(geoKey, ref, len, vop, doubleParameters, asciiParameters);
527                 }
528             }
529 
530             ps.format(nameFormat, name);
531             ps.format("%s", interpretation);
532             ps.format("%n");
533         }
534 
535         // Note: The y coordinate of the model pixel scale is backwards.
536         // GeoTIFF images are stored from upper-left corner downward
537         // (following the conventional graphics standards). In cases
538         // where the rows in the image or raster run from north to south,
539         // one might expect that the delta-Y between rows would be
540         // a negative number. But by the GeoTIFF standard,
541         // the vertical spacing is given as a positive number.
542         final TiffField pixelScaleField = directory.findField(GeoTiffTagConstants.EXIF_TAG_MODEL_PIXEL_SCALE_TAG);
543         double[] pixelScale;
544         if (pixelScaleField == null) {
545             ps.format("%nModelPixelScale is not supplied%n");
546         } else {
547             pixelScale = pixelScaleField.getDoubleArrayValue();
548             ps.format("%nModelPixelScale%n");
549             for (final double element : pixelScale) {
550                 ps.format("   %15.10e", element);
551             }
552             ps.format("%n");
553         }
554 
555         final TiffField modelTiepointField = directory.findField(GeoTiffTagConstants.EXIF_TAG_MODEL_TIEPOINT_TAG);
556         if (modelTiepointField != null) {
557             ps.format("%nModelTiepointTag%n");
558             ps.println("           Pixel                           Model");
559 
560             final double[] tiePoints = modelTiepointField.getDoubleArrayValue();
561             n = tiePoints.length / 6;
562             for (int i = 0; i < n; i++) {
563                 ps.format("   ");
564                 for (int j = 0; j < 3; j++) {
565                     ps.format("%6.1f", tiePoints[i * 6 + j]);
566                 }
567                 ps.format("     ");
568                 for (int j = 3; j < 6; j++) {
569                     ps.format("%13.3f", tiePoints[i * 6 + j]);
570                 }
571                 ps.format("%n");
572             }
573         }
574 
575         final TiffField modelTransformField = directory.findField(GeoTiffTagConstants.EXIF_TAG_MODEL_TRANSFORMATION_TAG);
576         if (modelTransformField != null) {
577             ps.format("%nModelTransformationTag%n");
578             final double[] mtf = modelTiepointField.getDoubleArrayValue();
579             if (mtf.length >= 16) {
580                 for (int i = 0; i < 4; i++) {
581                     ps.format("   ");
582                     for (int j = 0; j < 4; j++) {
583                         ps.format("%13.3f", mtf[i * 4 + j]);
584                     }
585                     ps.format("%n");
586                 }
587             }
588         }
589     }
590 }