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