1 /* Copyright (C) 2005-2011 Fabio Riccardi */
2 
3 package com.lightcrafts.image.types;
4 
5 import java.awt.color.ICC_Profile;
6 import java.awt.image.RenderedImage;
7 import java.awt.image.renderable.ParameterBlock;
8 import java.awt.*;
9 import java.io.*;
10 import java.nio.BufferUnderflowException;
11 import java.nio.ByteBuffer;
12 import java.util.ArrayList;
13 
14 import com.lightcrafts.mediax.jai.PlanarImage;
15 import com.lightcrafts.mediax.jai.JAI;
16 import com.lightcrafts.mediax.jai.ImageLayout;
17 
18 import org.w3c.dom.Document;
19 
20 import com.lightcrafts.image.*;
21 import com.lightcrafts.image.export.*;
22 import com.lightcrafts.image.libs.LCImageLibException;
23 import com.lightcrafts.image.libs.LCTIFFReader;
24 import com.lightcrafts.image.libs.LCTIFFWriter;
25 import com.lightcrafts.image.metadata.*;
26 import com.lightcrafts.image.metadata.values.ByteMetaValue;
27 import com.lightcrafts.image.metadata.values.ImageMetaValue;
28 import com.lightcrafts.image.metadata.values.UndefinedMetaValue;
29 import com.lightcrafts.jai.JAIContext;
30 import com.lightcrafts.jai.opimage.CachedImage;
31 import com.lightcrafts.utils.bytebuffer.ByteBufferUtil;
32 import com.lightcrafts.utils.ColorProfileInfo;
33 import com.lightcrafts.utils.file.FileUtil;
34 import com.lightcrafts.utils.UserCanceledException;
35 import com.lightcrafts.utils.thread.ProgressThread;
36 import com.lightcrafts.utils.xml.XmlNode;
37 import com.lightcrafts.utils.xml.XMLException;
38 import com.lightcrafts.utils.xml.XMLUtil;
39 import com.lightcrafts.utils.xml.XmlDocument;
40 
41 import static com.lightcrafts.image.metadata.TIFFTags.*;
42 import static com.lightcrafts.image.types.AdobeConstants.*;
43 import static com.lightcrafts.image.types.TIFFConstants.TIFF_COMPRESSION_LZW;
44 import static com.lightcrafts.image.types.TIFFConstants.TIFF_COMPRESSION_NONE;
45 
46 /**
47  * A <code>TIFFImageType</code> is-an {@link ImageType} for TIFF images.
48  *
49  * @author Paul J. Lucas [paul@lightcrafts.com]
50  */
51 public class TIFFImageType extends ImageType implements TrueImageTypeProvider {
52 
53     ////////// public /////////////////////////////////////////////////////////
54 
55     /** The singleton instance of <code>TIFFImageType</code>. */
56     public static final TIFFImageType INSTANCE = new TIFFImageType();
57 
58     /**
59      * <code>ExportOptions</code> are {@link ImageFileExportOptions} for TIFF
60      * images.
61      */
62     public static class ExportOptions extends ImageFileExportOptions {
63 
64         ////////// public /////////////////////////////////////////////////////
65 
66         public final BitsPerChannelOption   bitsPerChannel;
67         public final LZWCompressionOption   lzwCompression;
68         public final MultilayerOption       multilayer;
69 
70         /**
71          * Construct an <code>ExportOptions</code>.
72          */
ExportOptions()73         public ExportOptions() {
74             this( INSTANCE );
75         }
76 
77         /**
78          * {@inheritDoc}
79          */
readFrom( ImageExportOptionReader r )80         public void readFrom( ImageExportOptionReader r ) throws IOException {
81             super.readFrom( r );
82             bitsPerChannel.readFrom( r );
83             lzwCompression.readFrom( r );
84             multilayer.readFrom( r );
85         }
86 
87         /**
88          * {@inheritDoc}
89          */
writeTo( ImageExportOptionWriter w )90         public void writeTo( ImageExportOptionWriter w ) throws IOException {
91             super.writeTo( w );
92             bitsPerChannel.writeTo( w );
93             lzwCompression.writeTo( w );
94             multilayer.writeTo( w );
95         }
96 
97         ////////// protected //////////////////////////////////////////////////
98 
99         /**
100          * Construct an <code>ExportOptions</code>.
101          *
102          * @param instance The singleton instance of {@link ImageType} that
103          * this <code>ExportOptions</code> is for.
104          */
ExportOptions( ImageType instance )105         protected ExportOptions( ImageType instance ) {
106             super( instance );
107             bitsPerChannel = new BitsPerChannelOption( 8, this );
108             lzwCompression = new LZWCompressionOption( false, this );
109             multilayer     = new MultilayerOption( false, this );
110         }
111 
112         /**
113          * @deprecated
114          */
save( XmlNode node )115         protected void save( XmlNode node ) {
116             super.save( node );
117             bitsPerChannel.save( node );
118             lzwCompression.save( node );
119             multilayer.save( node );
120         }
121 
122         /**
123          * @deprecated
124          */
restore( XmlNode node )125         protected void restore( XmlNode node ) throws XMLException {
126             super.restore( node );
127             bitsPerChannel.restore( node );
128             lzwCompression.restore( node );
129             if (node.hasChild( multilayer.getName() )) {
130                 multilayer.restore( node );
131             }
132         }
133     }
134 
135     /**
136      * Checks whether the application can export to TIFF images.
137      *
138      * @return Always returns <code>true</code>.
139      */
canExport()140     public boolean canExport() {
141         return true;
142     }
143 
144     /**
145      * {@inheritDoc}
146      */
getDimension( ImageInfo imageInfo )147     public Dimension getDimension( ImageInfo imageInfo )
148         throws BadImageFileException, IOException, UnknownImageTypeException
149     {
150         final ImageMetadata metadata = imageInfo.getMetadata();
151         final ImageMetadataDirectory tiffDir =
152             metadata.getDirectoryFor( TIFFDirectory.class );
153         if ( tiffDir == null )
154             return null;
155         final ImageMetaValue width = tiffDir.getValue( TIFF_IMAGE_WIDTH );
156         final ImageMetaValue height = tiffDir.getValue( TIFF_IMAGE_LENGTH );
157         return width != null && height != null ?
158             new Dimension( width.getIntValue(), height.getIntValue() ) : null;
159     }
160 
161     /**
162      * {@inheritDoc}
163      */
getExtensions()164     public String[] getExtensions() {
165         return EXTENSIONS;
166     }
167 
168     /**
169      * {@inheritDoc}
170      */
getICCProfile( ImageInfo imageInfo )171     public ICC_Profile getICCProfile( ImageInfo imageInfo )
172         throws BadImageFileException, ColorProfileException, IOException,
173                UnknownImageTypeException
174     {
175         final ImageMetaValue v = imageInfo.getMetadata().getValue(
176             TIFFDirectory.class, TIFF_ICC_PROFILE
177         );
178         if ( v != null ) {
179             final byte[] iccData = ((UndefinedMetaValue)v).getUndefinedValue();
180             try {
181                 return ICC_Profile.getInstance( iccData );
182             }
183             catch ( IllegalArgumentException e ) {
184                 throw new BadColorProfileException(
185                     imageInfo.getFile().getName()
186                 );
187             }
188         }
189         return null;
190     }
191 
192     /**
193      * {@inheritDoc}
194      */
getImage( ImageInfo imageInfo, ProgressThread thread )195     public PlanarImage getImage( ImageInfo imageInfo, ProgressThread thread )
196         throws BadImageFileException, UserCanceledException, UnsupportedEncodingException
197     {
198         return getImage( imageInfo, thread, false );
199     }
200 
201     /**
202      * Gets the actual image data of an image.
203      *
204      * @param imageInfo The {@link ImageInfo} to get the actual image from.
205      * @param thread The thread doing the getting.
206      * @param read2nd If <code>true</code>, read the second TIFF image (if
207      * present).
208      * @return Returns said image data.
209      */
getImage( ImageInfo imageInfo, ProgressThread thread, boolean read2nd )210     public static PlanarImage getImage( ImageInfo imageInfo,
211                                         ProgressThread thread,
212                                         boolean read2nd )
213         throws BadImageFileException, UserCanceledException, UnsupportedEncodingException
214     {
215         try {
216             final String fileName = imageInfo.getFile().getAbsolutePath();
217             final PlanarImage image;
218 
219             if (true) {
220                 final LCTIFFReader reader =
221                     new LCTIFFReader( fileName, read2nd );
222                 image = reader.getImage( thread );
223 
224                 assert image instanceof CachedImage
225                         && image.getTileWidth() == JAIContext.TILE_WIDTH
226                         && image.getTileHeight() == JAIContext.TILE_HEIGHT;
227             } else {
228                 final PlanarImage tiffImage =
229                     new LCTIFFReader.TIFFImage( fileName );
230                 if (tiffImage.getTileWidth() != JAIContext.TILE_WIDTH ||
231                     tiffImage.getTileHeight() != JAIContext.TILE_HEIGHT) {
232                     final RenderingHints formatHints = new RenderingHints(
233                         JAI.KEY_IMAGE_LAYOUT,
234                         new ImageLayout(
235                             0, 0, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
236                             tiffImage.getSampleModel(),
237                             tiffImage.getColorModel()
238                         )
239                     );
240                     final ParameterBlock pb = new ParameterBlock();
241                     pb.addSource(tiffImage);
242                     pb.add(tiffImage.getSampleModel().getDataType());
243                     image = JAI.create("Format", pb, formatHints);
244                     image.setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);
245                 } else
246                     image = tiffImage;
247             }
248 
249             return image;
250         }
251         catch ( LCImageLibException e ) {
252             throw new BadImageFileException( imageInfo.getFile(), e );
253         }
254     }
255 
256     /**
257      * Gets the image specified by the JPEGInterchangeFormat tag, if any.
258      *
259      * @param imageInfo The {@link ImageInfo} to get the actual preview image
260      * from.
261      * @param maxWidth The maximum width of the image to get, rescaling if
262      * necessary.  A value of 0 means don't scale.
263      * @param maxHeight The maximum height of the image to get, rescaling if
264      * necessary.  A value of 0 means don't scale.
265      * @return Returns said image data or <code>null</code> if none.
266      */
getJPEGInterchangeImage( ImageInfo imageInfo, int maxWidth, int maxHeight )267     public static RenderedImage getJPEGInterchangeImage( ImageInfo imageInfo,
268                                                          int maxWidth,
269                                                          int maxHeight )
270         throws BadImageFileException, IOException, UnknownImageTypeException
271     {
272         final ImageMetadata metadata = imageInfo.getMetadata();
273         final ImageMetadataDirectory dir =
274             metadata.getDirectoryFor( TIFFDirectory.class );
275         if ( dir == null )
276             return null;
277         return JPEGImageType.getImageFromBuffer(
278             imageInfo.getByteBuffer(),
279             dir.getValue( TIFF_JPEG_INTERCHANGE_FORMAT ), 0,
280             dir.getValue( TIFF_JPEG_INTERCHANGE_FORMAT_LENGTH ),
281             maxWidth, maxHeight
282         );
283     }
284 
285     /**
286      * {@inheritDoc}
287      */
getName()288     public String getName() {
289         return "TIFF";
290     }
291 
292     /**
293      * Gets the actual thumbnail image.  We normally don't get a thumbnail
294      * image for TIFF files, but we have to handle a special case for files
295      * generated by Photoshop that can embed a thumbnail image as part of the
296      * value of the {@link TIFFTags#TIFF_PHOTOSHOP_IMAGE_RESOURCES} tag.
297      *
298      * @param imageInfo The {@link ImageInfo} to get the actual preview image
299      * from.
300      * @return Returns said image data.
301      */
getThumbnailImage( ImageInfo imageInfo )302     public RenderedImage getThumbnailImage( ImageInfo imageInfo )
303         throws BadImageFileException, IOException, UnknownImageTypeException
304     {
305         return getPhotoshopThumbnail( imageInfo );
306     }
307 
308     /**
309      * {@inheritDoc}
310      */
getTrueImageTypeOf( ImageInfo imageInfo )311     public ImageType getTrueImageTypeOf( ImageInfo imageInfo )
312         throws BadImageFileException, IOException
313     {
314         //
315         // Some camera vendors, in their infinite wisdom, decided, for some of
316         // their cameras, to create raw files with a .TIF extension.  Since the
317         // file isn't really a TIFF file, the ImageType returned by
318         // ImageType.determineTypeByExtensionOf() is wrong.  If we were to try
319         // to read one of these raw files as a TIFF, reading the full-sized
320         // image doesn't work (you get the thumbnail instead).
321         //
322         // We therefore have to probe the image file to determine whether it's
323         // really one of these raw image files.
324         //
325         for ( TrueImageTypeProvider p : m_rawImageProbes ) {
326             final ImageType t = p.getTrueImageTypeOf( imageInfo );
327             if ( t != null )
328                 return t;
329         }
330 
331         try {
332             final Document lznDoc = getLZNDocumentImpl( imageInfo );
333             if ( lznDoc != null ) {
334                 final XmlDocument xmlDoc =
335                     new XmlDocument( lznDoc.getDocumentElement() );
336                 // The original image may be in the same file,
337                 // or referenced through a path pointer:
338                 final XmlNode root = xmlDoc.getRoot();
339                 // (tag copied from ui.editor.Document)
340                 final XmlNode imageNode = root.getChild( "Image" );
341                 // (tag written in export())
342                 if ( imageNode.hasAttribute( "self" ) )
343                     return MultipageTIFFImageType.INSTANCE;
344                 else
345                     return SidecarTIFFImageType.INSTANCE;
346             }
347         }
348         catch ( UnknownImageTypeException e ) {
349             // should never happen at this stage
350         }
351         return null;
352     }
353 
354     /**
355      * {@inheritDoc}
356      */
getXMP( ImageInfo imageInfo )357     public Document getXMP( ImageInfo imageInfo )
358         throws BadImageFileException, IOException, UnknownImageTypeException
359     {
360         return getXMP( imageInfo, TIFFDirectory.class );
361     }
362 
363     /**
364      * {@inheritDoc}
365      */
newExportOptions()366     public ExportOptions newExportOptions() {
367         return new ExportOptions();
368     }
369 
370     /**
371      * {@inheritDoc}
372      */
putImage( ImageInfo imageInfo, PlanarImage image, ImageExportOptions options, Document lznDoc, ProgressThread thread )373     public void putImage( ImageInfo imageInfo, PlanarImage image,
374                           ImageExportOptions options, Document lznDoc,
375                           ProgressThread thread ) throws IOException {
376         final ExportOptions tiffOptions = (ExportOptions)options;
377         final File exportFile = options.getExportFile();
378         File tempFile = null;
379 
380         ImageMetadata metadata;
381         try {
382             metadata = imageInfo.getMetadata();
383         }
384         catch ( BadImageFileException e ) {
385             metadata = new ImageMetadata( this );
386         }
387         catch ( UnknownImageTypeException e ) {
388             metadata = new ImageMetadata( this );
389         }
390 
391         final LCTIFFWriter writer;
392         try {
393             if ( tiffOptions.multilayer.getValue() ) {
394                 File originalFile = imageInfo.getFile();
395                 if ( exportFile.equals( originalFile ) ) {
396                     tempFile = File.createTempFile( "LightZone", "tif" );
397                     FileUtil.copyFile( originalFile, tempFile );
398                     originalFile = tempFile;
399                 }
400                 writer = new LCTIFFWriter(
401                     exportFile.getAbsolutePath(),
402                     originalFile.getAbsolutePath(),
403                     tiffOptions.resizeWidth.getValue(),
404                     tiffOptions.resizeHeight.getValue(),
405                     tiffOptions.resolution.getValue(),
406                     tiffOptions.resolutionUnit.getValue()
407                 );
408             } else {
409                 writer = new LCTIFFWriter(
410                     options.getExportFile().getAbsolutePath(),
411                     tiffOptions.resizeWidth.getValue(),
412                     tiffOptions.resizeHeight.getValue(),
413                     tiffOptions.resolution.getValue(),
414                     tiffOptions.resolutionUnit.getValue()
415                 );
416             }
417 
418             writer.setIntField(
419                 TIFF_COMPRESSION,
420                 tiffOptions.lzwCompression.getValue() ?
421                     TIFF_COMPRESSION_LZW : TIFF_COMPRESSION_NONE
422             );
423 
424             ICC_Profile profile = ColorProfileInfo.getExportICCProfileFor(
425                 tiffOptions.colorProfile.getValue()
426             );
427             if ( profile == null )
428                 profile = JAIContext.sRGBExportColorProfile;
429             writer.setICCProfile( profile );
430 
431             if ( lznDoc != null ) {
432                 final byte[] buf = XMLUtil.encodeDocument( lznDoc, false );
433                 writer.setByteField( TIFF_LIGHTZONE, buf );
434             }
435 
436             writer.putMetadata( metadata );
437             writer.putImageStriped( image, thread );
438             // TODO: allow users to write tiled TIFFs if they want
439             // writer.putImageTiled( image, thread );
440             writer.dispose();
441         }
442         catch ( LCImageLibException e ) {
443             final IOException ioe = new IOException( "TIFF export failed" );
444             ioe.initCause( e );
445             throw ioe;
446         }
447         finally {
448             if ( tempFile != null )
449                 tempFile.delete();
450         }
451     }
452 
453     /**
454      * Reads all the metadata for a given TIFF image.
455      *
456      * @param imageInfo The image to read the metadata from.
457      */
readMetadata( ImageInfo imageInfo )458     public void readMetadata( ImageInfo imageInfo )
459         throws BadImageFileException, IOException, UnknownImageTypeException
460     {
461         final TIFFMetadataReader reader = new TIFFMetadataReader( imageInfo );
462         final ImageMetadata metadata = reader.readMetadata();
463         final Document xmpDoc = getXMP( imageInfo );
464         if ( xmpDoc != null ) {
465             final ImageMetadata xmpMetadata =
466                 XMPMetadataReader.readFrom( xmpDoc );
467             metadata.mergeFrom( xmpMetadata );
468         }
469     }
470 
471     /**
472      * Writes the metadata for TIFF files to an XMP sidecar file.
473      *
474      * @param imageInfo The image to write the metadata for.
475      */
writeMetadata( ImageInfo imageInfo )476     public void writeMetadata( ImageInfo imageInfo )
477         throws BadImageFileException, IOException, UnknownImageTypeException
478     {
479         final File xmpFile = new File( imageInfo.getXMPFilename() );
480         final ImageMetadata metadata = imageInfo.getCurrentMetadata();
481         XMPMetadataWriter.mergeInto( metadata, xmpFile );
482         metadata.clearEdited();
483     }
484 
485     ////////// package ////////////////////////////////////////////////////////
486 
487     /**
488      * Gets the XMP document from the XMP packet tag.
489      *
490      * @param imageInfo The TIFF or DNG image to get the XMP document from.
491      * @param dirClass The {@link Class} of the {@link ImageMetadataDirectory}
492      * to get the XMP packet from.
493      * @return Returns said {@link Document} or <code>null</code> if none.
494      */
getXMP( ImageInfo imageInfo, Class<? extends ImageMetadataDirectory> dirClass )495     static Document getXMP(
496         ImageInfo imageInfo, Class<? extends ImageMetadataDirectory> dirClass
497     )
498         throws BadImageFileException, IOException, UnknownImageTypeException
499     {
500         final ImageMetadata metadata = imageInfo.getMetadata();
501         final ImageMetaValue xmpValue =
502             metadata.getValue( dirClass, TIFF_XMP_PACKET );
503         if ( xmpValue == null )
504             return null;
505         final byte[] xmpBytes = XMPUtil.getXMPDataFrom( xmpValue );
506         if ( xmpBytes == null )
507             return null;
508         return XMLUtil.readDocumentFrom( new ByteArrayInputStream( xmpBytes ) );
509     }
510 
511     ////////// protected //////////////////////////////////////////////////////
512 
513     /**
514      * Construct a <code>TIFFImageType</code>.
515      */
TIFFImageType()516     protected TIFFImageType() {
517         // do nothing
518     }
519 
520     /**
521      * Gets the LightZone document (if any) from the given TIFF image.
522      *
523      * @param imageInfo The image to get the LightZone document from.
524      * @return Returns said {@link Document} or <code>null</code> if none.
525      */
getLZNDocumentImpl( ImageInfo imageInfo )526     protected static Document getLZNDocumentImpl( ImageInfo imageInfo )
527         throws BadImageFileException, IOException, UnknownImageTypeException
528     {
529         final ImageMetadata metadata = imageInfo.getMetadata();
530         final ImageMetaValue lznValue =
531             metadata.getValue( TIFFDirectory.class, TIFF_LIGHTZONE );
532         if ( lznValue != null ) {
533             final byte[] buf = ((ByteMetaValue)lznValue).getByteValues();
534             final InputStream in = new ByteArrayInputStream( buf );
535             return XMLUtil.readDocumentFrom( in );
536         }
537         //
538         // For backwards compatibility, check for LightZone data inside XMP
539         // metadata.
540         //
541         final ImageMetaValue xmpValue =
542             metadata.getValue( TIFFDirectory.class, TIFF_XMP_PACKET );
543         final byte[] xmp = XMPUtil.getXMPDataFrom( xmpValue );
544         if ( xmp != null ) {
545             final InputStream in = new ByteArrayInputStream( xmp );
546             final Document xmpDoc = XMLUtil.readDocumentFrom( in );
547             final Document lznDoc = XMPUtil.getLZNDocumentFrom( xmpDoc );
548             if ( lznDoc != null )
549                 return lznDoc;
550         }
551         return null;
552     }
553 
554     ////////// private ////////////////////////////////////////////////////////
555 
556     /**
557      * Gets the thumbnail image from the thumbnail resource block from the
558      * value of the {@link TIFFTags#TIFF_PHOTOSHOP_IMAGE_RESOURCES} tag.
559      *
560      * @param imageInfo The {@link ImageInfo} to get the actual thumbnail image
561      * from.
562      * @return Returns said image data.
563      * @see <i>Photoshop CS File Formats Specification</i>, Adobe Systems,
564      * Incorporated, October 2003.
565      */
getPhotoshopThumbnail( ImageInfo imageInfo )566     private static RenderedImage getPhotoshopThumbnail( ImageInfo imageInfo )
567         throws BadImageFileException, IOException, UnknownImageTypeException
568     {
569         final ImageMetadata metadata = imageInfo.getMetadata();
570         final ImageMetaValue photoshop = metadata.getValue(
571             TIFFDirectory.class, TIFF_PHOTOSHOP_IMAGE_RESOURCES
572         );
573         if ( photoshop == null )
574             return null;
575         final byte[] bytes = ((ByteMetaValue)photoshop).getByteValues();
576         ByteBuffer buf = ByteBuffer.wrap( bytes );
577 
578         //
579         // Go through all the Image Resource Blocks looking for the ones for
580         // the thumbnail.
581         //
582         int blockSize;
583         try {
584             while ( true ) {
585                 final String blockSig =
586                     ByteBufferUtil.getString( buf, 4, "ASCII" );
587                 if ( !blockSig.equals( PHOTOSHOP_CREATOR_CODE ) )
588                     return null;
589                 final int blockID = ByteBufferUtil.getUnsignedShort( buf );
590                 final int blockNameLen = ByteBufferUtil.getUnsignedByte( buf );
591                 ByteBufferUtil.skipBytes( buf, blockNameLen );
592                 if ( (blockNameLen + 1) % 2 == 1 )
593                     ByteBufferUtil.skipBytes( buf, 1 );
594                 blockSize = buf.getInt();
595                 if ( blockID == PHOTOSHOP_5_THUMBNAIL_RESOURCE_ID )
596                     break;
597                 if ( blockSize % 2 == 1 ) // must be made even
598                     ++blockSize;
599                 ByteBufferUtil.skipBytes( buf, blockSize );
600             }
601         }
602         catch ( BufferUnderflowException e ) {
603             return null;
604         }
605         catch ( IllegalArgumentException e ) {
606             return null;
607         }
608 
609         // Wrap just the thumbnail resource block.
610         buf = ByteBuffer.wrap( bytes, buf.position(), blockSize );
611 
612         if ( buf.getInt() != 1 )        // format: should always = 1
613             return null;
614         buf.getInt();                   // skip width
615         buf.getInt();                   // skip height
616         buf.getInt();                   // skip scanline size
617         buf.getInt();                   // skip decompressed memory size
618         final int jpegSize = buf.getInt();
619         buf.getShort();                 // skip bits per pixel
620         buf.getShort();                 // skip number of planes
621 
622         // Finally, the buffer is positioned at the JPEG image data.
623         return JPEGImageType.getImageFromBuffer(
624             bytes, buf.position(), jpegSize, null, 0, 0
625         );
626     }
627 
628     /**
629      * All the possible filename extensions for TIFF files.  All must be lower
630      * case and the preferred one must be first.
631      */
632     private static final String EXTENSIONS[] = {
633         "tif", "tiff", "iiq"
634     };
635 
636     /**
637      * The global static list of all the {@link TrueImageTypeProvider}s that
638      * are used to probe raw image types.
639      */
640     private static final ArrayList<TrueImageTypeProvider> m_rawImageProbes =
641         new ArrayList<TrueImageTypeProvider>();
642 
643     static {
644         // TODO: is there a better way to do this?
645         m_rawImageProbes.add( CanonTIFFRawImageProbe.INSTANCE );
646         m_rawImageProbes.add( PhaseOneTIFFRawImageProbe.INSTANCE );
647     }
648 }
649 /* vim:set et sw=4 ts=4: */
650