001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.formats.tiff;
018
019import java.awt.image.BufferedImage;
020import java.io.IOException;
021import java.nio.ByteOrder;
022import java.util.ArrayList;
023import java.util.List;
024
025import org.apache.commons.imaging.ImagingException;
026import org.apache.commons.imaging.common.GenericImageMetadata;
027import org.apache.commons.imaging.common.RationalNumber;
028import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
029import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
030import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType;
031import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
032import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
033import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii;
034import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoByte;
035import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDoubles;
036import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoFloats;
037import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoGpsText;
038import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLongs;
039import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRationals;
040import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSBytes;
041import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSLongs;
042import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSRationals;
043import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoSShorts;
044import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShorts;
045import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoXpString;
046import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
047import org.apache.commons.imaging.formats.tiff.write.TiffOutputField;
048import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
049
050public class TiffImageMetadata extends GenericImageMetadata {
051    public static class Directory extends GenericImageMetadata implements ImageMetadataItem {
052        // private BufferedImage thumbnail;
053
054        public final int type;
055
056        private final TiffDirectory directory;
057        private final ByteOrder byteOrder;
058
059        public Directory(final ByteOrder byteOrder, final TiffDirectory directory) {
060            this.type = directory.type;
061            this.directory = directory;
062            this.byteOrder = byteOrder;
063        }
064
065        public void add(final TiffField entry) {
066            add(new TiffMetadataItem(entry));
067        }
068
069        public TiffField findField(final TagInfo tagInfo) throws ImagingException {
070            return directory.findField(tagInfo);
071        }
072
073        public List<TiffField> getAllFields() {
074            return directory.getDirectoryEntries();
075        }
076
077        public JpegImageData getJpegImageData() {
078            return directory.getJpegImageData();
079        }
080
081        public TiffOutputDirectory getOutputDirectory(final ByteOrder byteOrder) throws ImagingException {
082            try {
083                final TiffOutputDirectory dstDir = new TiffOutputDirectory(type, byteOrder);
084
085                final List<? extends ImageMetadataItem> entries = getItems();
086                for (final ImageMetadataItem entry : entries) {
087                    final TiffMetadataItem item = (TiffMetadataItem) entry;
088                    final TiffField srcField = item.getTiffField();
089
090                    if (null != dstDir.findField(srcField.getTag())) {
091                        // ignore duplicate tags in a directory.
092                        continue;
093                    }
094                    if (srcField.getTagInfo().isOffset()) {
095                        // ignore offset fields.
096                        continue;
097                    }
098
099                    final TagInfo tagInfo = srcField.getTagInfo();
100                    final AbstractFieldType abstractFieldType = srcField.getFieldType();
101                    // byte[] bytes = srcField.fieldType.getRawBytes(srcField);
102
103                    // Debug.debug("tagInfo", tagInfo);
104
105                    final Object value = srcField.getValue();
106
107                    // Debug.debug("value", Debug.getType(value));
108
109                    final byte[] bytes = tagInfo.encodeValue(abstractFieldType, value, byteOrder);
110
111                    // if (tagInfo.isUnknown())
112                    // Debug.debug(
113                    // "\t" + "unknown tag(0x"
114                    // + Integer.toHexString(srcField.tag)
115                    // + ") bytes", bytes);
116
117                    final int count = bytes.length / abstractFieldType.getSize();
118                    final TiffOutputField dstField = new TiffOutputField(srcField.getTag(), tagInfo, abstractFieldType, count, bytes);
119                    dstField.setSortHint(srcField.getSortHint());
120                    dstDir.add(dstField);
121                }
122
123                dstDir.setTiffImageData(getTiffImageData());
124                dstDir.setJpegImageData(getJpegImageData());
125
126                return dstDir;
127            } catch (final ImagingException e) {
128                throw new ImagingException(e.getMessage(), e);
129            }
130        }
131
132        public BufferedImage getThumbnail() throws ImagingException, IOException {
133            return directory.getTiffImage(byteOrder);
134        }
135
136        public AbstractTiffImageData getTiffImageData() {
137            return directory.getTiffImageData();
138        }
139
140        @Override
141        public String toString(final String prefix) {
142            return (prefix != null ? prefix : "") + directory.description() + ": " //
143                    + (getTiffImageData() != null ? " (tiffImageData)" : "") //
144                    + (getJpegImageData() != null ? " (jpegImageData)" : "") //
145                    + "\n" + super.toString(prefix) + "\n";
146        }
147
148    }
149
150    public static class GpsInfo {
151        public final String latitudeRef;
152        public final String longitudeRef;
153
154        public final RationalNumber latitudeDegrees;
155        public final RationalNumber latitudeMinutes;
156        public final RationalNumber latitudeSeconds;
157        public final RationalNumber longitudeDegrees;
158        public final RationalNumber longitudeMinutes;
159        public final RationalNumber longitudeSeconds;
160
161        public GpsInfo(final String latitudeRef, final String longitudeRef, final RationalNumber latitudeDegrees, final RationalNumber latitudeMinutes,
162                final RationalNumber latitudeSeconds, final RationalNumber longitudeDegrees, final RationalNumber longitudeMinutes,
163                final RationalNumber longitudeSeconds) {
164            this.latitudeRef = latitudeRef;
165            this.longitudeRef = longitudeRef;
166            this.latitudeDegrees = latitudeDegrees;
167            this.latitudeMinutes = latitudeMinutes;
168            this.latitudeSeconds = latitudeSeconds;
169            this.longitudeDegrees = longitudeDegrees;
170            this.longitudeMinutes = longitudeMinutes;
171            this.longitudeSeconds = longitudeSeconds;
172        }
173
174        public double getLatitudeAsDegreesNorth() throws ImagingException {
175            final double result = latitudeDegrees.doubleValue() + latitudeMinutes.doubleValue() / 60.0 + latitudeSeconds.doubleValue() / 3600.0;
176
177            if (latitudeRef.trim().equalsIgnoreCase("n")) {
178                return result;
179            }
180            if (latitudeRef.trim().equalsIgnoreCase("s")) {
181                return -result;
182            }
183            throw new ImagingException("Unknown latitude ref: \"" + latitudeRef + "\"");
184        }
185
186        public double getLongitudeAsDegreesEast() throws ImagingException {
187            final double result = longitudeDegrees.doubleValue() + longitudeMinutes.doubleValue() / 60.0 + longitudeSeconds.doubleValue() / 3600.0;
188
189            if (longitudeRef.trim().equalsIgnoreCase("e")) {
190                return result;
191            }
192            if (longitudeRef.trim().equalsIgnoreCase("w")) {
193                return -result;
194            }
195            throw new ImagingException("Unknown longitude ref: \"" + longitudeRef + "\"");
196        }
197
198        @Override
199        public String toString() {
200            // This will format the gps info like so:
201            //
202            // latitude: 8 degrees, 40 minutes, 42.2 seconds S
203            // longitude: 115 degrees, 26 minutes, 21.8 seconds E
204
205            return "[GPS. Latitude: " + latitudeDegrees.toDisplayString() + " degrees, " + latitudeMinutes.toDisplayString() + " minutes, "
206                    + latitudeSeconds.toDisplayString() + " seconds " + latitudeRef + ", Longitude: " + longitudeDegrees.toDisplayString() + " degrees, "
207                    + longitudeMinutes.toDisplayString() + " minutes, " + longitudeSeconds.toDisplayString() + " seconds " + longitudeRef + ']';
208        }
209
210    }
211
212    public static class TiffMetadataItem extends GenericImageMetadataItem {
213        private final TiffField entry;
214
215        public TiffMetadataItem(final TiffField entry) {
216            // super(entry.getTagName() + " (" + entry.getFieldTypeName() + ")",
217            super(entry.getTagName(), entry.getValueDescription());
218            this.entry = entry;
219        }
220
221        public TiffField getTiffField() {
222            return entry;
223        }
224
225    }
226
227    public final TiffContents contents;
228
229    public TiffImageMetadata(final TiffContents contents) {
230        this.contents = contents;
231    }
232
233    public TiffDirectory findDirectory(final int directoryType) {
234        final List<? extends ImageMetadataItem> directories = getDirectories();
235        for (final ImageMetadataItem directory1 : directories) {
236            final Directory directory = (Directory) directory1;
237            if (directory.type == directoryType) {
238                return directory.directory;
239            }
240        }
241        return null;
242    }
243
244    public TiffField findField(final TagInfo tagInfo) throws ImagingException {
245        return findField(tagInfo, false);
246    }
247
248    public TiffField findField(final TagInfo tagInfo, final boolean exactDirectoryMatch) throws ImagingException {
249        // Please keep this method in sync with TiffField's getTag()
250        final Integer tagCount = TiffTags.getTagCount(tagInfo.tag);
251        final int tagsMatching = tagCount == null ? 0 : tagCount;
252
253        final List<? extends ImageMetadataItem> directories = getDirectories();
254        if (exactDirectoryMatch || tagInfo.directoryType != TiffDirectoryType.EXIF_DIRECTORY_UNKNOWN) {
255            for (final ImageMetadataItem directory1 : directories) {
256                final Directory directory = (Directory) directory1;
257                if (directory.type == tagInfo.directoryType.directoryType) {
258                    final TiffField field = directory.findField(tagInfo);
259                    if (field != null) {
260                        return field;
261                    }
262                }
263            }
264            if (exactDirectoryMatch || tagsMatching > 1) {
265                return null;
266            }
267            for (final ImageMetadataItem directory1 : directories) {
268                final Directory directory = (Directory) directory1;
269                if (tagInfo.directoryType.isImageDirectory() && directory.type >= 0 || !tagInfo.directoryType.isImageDirectory() && directory.type < 0) {
270                    final TiffField field = directory.findField(tagInfo);
271                    if (field != null) {
272                        return field;
273                    }
274                }
275            }
276        }
277
278        for (final ImageMetadataItem directory1 : directories) {
279            final Directory directory = (Directory) directory1;
280            final TiffField field = directory.findField(tagInfo);
281            if (field != null) {
282                return field;
283            }
284        }
285
286        return null;
287    }
288
289    public List<TiffField> getAllFields() {
290        final List<TiffField> result = new ArrayList<>();
291        final List<? extends ImageMetadataItem> directories = getDirectories();
292        for (final ImageMetadataItem directory1 : directories) {
293            final Directory directory = (Directory) directory1;
294            result.addAll(directory.getAllFields());
295        }
296        return result;
297    }
298
299    public List<? extends ImageMetadataItem> getDirectories() {
300        return super.getItems();
301    }
302
303    public Object getFieldValue(final TagInfo tag) throws ImagingException {
304        final TiffField field = findField(tag);
305        if (field == null) {
306            return null;
307        }
308        return field.getValue();
309    }
310
311    public String[] getFieldValue(final TagInfoAscii tag) throws ImagingException {
312        final TiffField field = findField(tag);
313        if (field == null) {
314            return null;
315        }
316        if (!tag.dataTypes.contains(field.getFieldType())) {
317            return null;
318        }
319        final byte[] bytes = field.getByteArrayValue();
320        return tag.getValue(field.getByteOrder(), bytes);
321    }
322
323    public byte[] getFieldValue(final TagInfoByte tag) throws ImagingException {
324        final TiffField field = findField(tag);
325        if (field == null) {
326            return null;
327        }
328        if (!tag.dataTypes.contains(field.getFieldType())) {
329            return null;
330        }
331        return field.getByteArrayValue();
332    }
333
334    public double[] getFieldValue(final TagInfoDoubles tag) throws ImagingException {
335        final TiffField field = findField(tag);
336        if (field == null) {
337            return null;
338        }
339        if (!tag.dataTypes.contains(field.getFieldType())) {
340            return null;
341        }
342        final byte[] bytes = field.getByteArrayValue();
343        return tag.getValue(field.getByteOrder(), bytes);
344    }
345
346    public float[] getFieldValue(final TagInfoFloats tag) throws ImagingException {
347        final TiffField field = findField(tag);
348        if (field == null) {
349            return null;
350        }
351        if (!tag.dataTypes.contains(field.getFieldType())) {
352            return null;
353        }
354        final byte[] bytes = field.getByteArrayValue();
355        return tag.getValue(field.getByteOrder(), bytes);
356    }
357
358    public String getFieldValue(final TagInfoGpsText tag) throws ImagingException {
359        final TiffField field = findField(tag);
360        if (field == null) {
361            return null;
362        }
363        return tag.getValue(field);
364    }
365
366    public int[] getFieldValue(final TagInfoLongs tag) throws ImagingException {
367        final TiffField field = findField(tag);
368        if (field == null) {
369            return null;
370        }
371        if (!tag.dataTypes.contains(field.getFieldType())) {
372            return null;
373        }
374        final byte[] bytes = field.getByteArrayValue();
375        return tag.getValue(field.getByteOrder(), bytes);
376    }
377
378    public RationalNumber[] getFieldValue(final TagInfoRationals tag) throws ImagingException {
379        final TiffField field = findField(tag);
380        if (field == null) {
381            return null;
382        }
383        if (!tag.dataTypes.contains(field.getFieldType())) {
384            return null;
385        }
386        final byte[] bytes = field.getByteArrayValue();
387        return tag.getValue(field.getByteOrder(), bytes);
388    }
389
390    public byte[] getFieldValue(final TagInfoSBytes tag) throws ImagingException {
391        final TiffField field = findField(tag);
392        if (field == null) {
393            return null;
394        }
395        if (!tag.dataTypes.contains(field.getFieldType())) {
396            return null;
397        }
398        return field.getByteArrayValue();
399    }
400
401    public short[] getFieldValue(final TagInfoShorts tag) throws ImagingException {
402        final TiffField field = findField(tag);
403        if (field == null) {
404            return null;
405        }
406        if (!tag.dataTypes.contains(field.getFieldType())) {
407            return null;
408        }
409        final byte[] bytes = field.getByteArrayValue();
410        return tag.getValue(field.getByteOrder(), bytes);
411    }
412
413    public int[] getFieldValue(final TagInfoSLongs tag) throws ImagingException {
414        final TiffField field = findField(tag);
415        if (field == null) {
416            return null;
417        }
418        if (!tag.dataTypes.contains(field.getFieldType())) {
419            return null;
420        }
421        final byte[] bytes = field.getByteArrayValue();
422        return tag.getValue(field.getByteOrder(), bytes);
423    }
424
425    public RationalNumber[] getFieldValue(final TagInfoSRationals tag) throws ImagingException {
426        final TiffField field = findField(tag);
427        if (field == null) {
428            return null;
429        }
430        if (!tag.dataTypes.contains(field.getFieldType())) {
431            return null;
432        }
433        final byte[] bytes = field.getByteArrayValue();
434        return tag.getValue(field.getByteOrder(), bytes);
435    }
436
437    public short[] getFieldValue(final TagInfoSShorts tag) throws ImagingException {
438        final TiffField field = findField(tag);
439        if (field == null) {
440            return null;
441        }
442        if (!tag.dataTypes.contains(field.getFieldType())) {
443            return null;
444        }
445        final byte[] bytes = field.getByteArrayValue();
446        return tag.getValue(field.getByteOrder(), bytes);
447    }
448
449    public String getFieldValue(final TagInfoXpString tag) throws ImagingException {
450        final TiffField field = findField(tag);
451        if (field == null) {
452            return null;
453        }
454        return tag.getValue(field);
455    }
456
457    public GpsInfo getGpsInfo() throws ImagingException {
458        final TiffDirectory gpsDirectory = findDirectory(TiffDirectoryConstants.DIRECTORY_TYPE_GPS);
459        if (null == gpsDirectory) {
460            return null;
461        }
462
463        // more specific example of how to access GPS values.
464        final TiffField latitudeRefField = gpsDirectory.findField(GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF);
465        final TiffField latitudeField = gpsDirectory.findField(GpsTagConstants.GPS_TAG_GPS_LATITUDE);
466        final TiffField longitudeRefField = gpsDirectory.findField(GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF);
467        final TiffField longitudeField = gpsDirectory.findField(GpsTagConstants.GPS_TAG_GPS_LONGITUDE);
468
469        if (latitudeRefField == null || latitudeField == null || longitudeRefField == null || longitudeField == null) {
470            return null;
471        }
472
473        // all of these values are strings.
474        final String latitudeRef = latitudeRefField.getStringValue();
475        final RationalNumber[] latitude = (RationalNumber[]) latitudeField.getValue();
476        final String longitudeRef = longitudeRefField.getStringValue();
477        final RationalNumber[] longitude = (RationalNumber[]) longitudeField.getValue();
478
479        if (latitude.length != 3 || longitude.length != 3) {
480            throw new ImagingException("Expected three values for latitude and longitude.");
481        }
482
483        final RationalNumber latitudeDegrees = latitude[0];
484        final RationalNumber latitudeMinutes = latitude[1];
485        final RationalNumber latitudeSeconds = latitude[2];
486
487        final RationalNumber longitudeDegrees = longitude[0];
488        final RationalNumber longitudeMinutes = longitude[1];
489        final RationalNumber longitudeSeconds = longitude[2];
490
491        return new GpsInfo(latitudeRef, longitudeRef, latitudeDegrees, latitudeMinutes, latitudeSeconds, longitudeDegrees, longitudeMinutes, longitudeSeconds);
492    }
493
494    @Override
495    public List<? extends ImageMetadataItem> getItems() {
496        final List<ImageMetadataItem> result = new ArrayList<>();
497
498        final List<? extends ImageMetadataItem> items = super.getItems();
499        for (final ImageMetadataItem item : items) {
500            final Directory dir = (Directory) item;
501            result.addAll(dir.getItems());
502        }
503
504        return result;
505    }
506
507    public TiffOutputSet getOutputSet() throws ImagingException {
508        final ByteOrder byteOrder = contents.header.byteOrder;
509        final TiffOutputSet result = new TiffOutputSet(byteOrder);
510
511        final List<? extends ImageMetadataItem> srcDirs = getDirectories();
512        for (final ImageMetadataItem srcDir1 : srcDirs) {
513            final Directory srcDir = (Directory) srcDir1;
514
515            if (null != result.findDirectory(srcDir.type)) {
516                // Certain cameras right directories more than once.
517                // This is a bug.
518                // Ignore second directory of a given type.
519                continue;
520            }
521
522            final TiffOutputDirectory outputDirectory = srcDir.getOutputDirectory(byteOrder);
523            result.addDirectory(outputDirectory);
524        }
525
526        return result;
527    }
528
529}