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