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