| 1 | /*
|
|---|
| 2 | * Copyright 2002-2012 Drew Noakes
|
|---|
| 3 | *
|
|---|
| 4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
|---|
| 5 | * you may not use this file except in compliance with the License.
|
|---|
| 6 | * You may obtain a copy of the License at
|
|---|
| 7 | *
|
|---|
| 8 | * http://www.apache.org/licenses/LICENSE-2.0
|
|---|
| 9 | *
|
|---|
| 10 | * Unless required by applicable law or agreed to in writing, software
|
|---|
| 11 | * distributed under the License is distributed on an "AS IS" BASIS,
|
|---|
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|---|
| 13 | * See the License for the specific language governing permissions and
|
|---|
| 14 | * limitations under the License.
|
|---|
| 15 | *
|
|---|
| 16 | * More information about this project is available at:
|
|---|
| 17 | *
|
|---|
| 18 | * http://drewnoakes.com/code/exif/
|
|---|
| 19 | * http://code.google.com/p/metadata-extractor/
|
|---|
| 20 | */
|
|---|
| 21 | package com.drew.metadata.exif;
|
|---|
| 22 |
|
|---|
| 23 | import java.util.HashSet;
|
|---|
| 24 | import java.util.Set;
|
|---|
| 25 |
|
|---|
| 26 | import com.drew.lang.BufferBoundsException;
|
|---|
| 27 | import com.drew.lang.BufferReader;
|
|---|
| 28 | import com.drew.lang.Rational;
|
|---|
| 29 | import com.drew.lang.annotations.NotNull;
|
|---|
| 30 | import com.drew.metadata.Directory;
|
|---|
| 31 | import com.drew.metadata.Metadata;
|
|---|
| 32 | import com.drew.metadata.MetadataReader;
|
|---|
| 33 |
|
|---|
| 34 | /**
|
|---|
| 35 | * Decodes Exif binary data, populating a {@link Metadata} object with tag values in {@link ExifSubIFDDirectory},
|
|---|
| 36 | * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera makernote directories.
|
|---|
| 37 | *
|
|---|
| 38 | * @author Drew Noakes http://drewnoakes.com
|
|---|
| 39 | */
|
|---|
| 40 | public class ExifReader implements MetadataReader
|
|---|
| 41 | {
|
|---|
| 42 | // TODO extract a reusable TiffReader from this class with hooks for special tag handling and subdir following
|
|---|
| 43 |
|
|---|
| 44 | /** The number of bytes used per format descriptor. */
|
|---|
| 45 | @NotNull
|
|---|
| 46 | private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 };
|
|---|
| 47 |
|
|---|
| 48 | /** The number of formats known. */
|
|---|
| 49 | private static final int MAX_FORMAT_CODE = 12;
|
|---|
| 50 |
|
|---|
| 51 | // Format types
|
|---|
| 52 | // TODO use an enum for these?
|
|---|
| 53 | /** An 8-bit unsigned integer. */
|
|---|
| 54 | private static final int FMT_BYTE = 1;
|
|---|
| 55 | /** A fixed-length character string. */
|
|---|
| 56 | private static final int FMT_STRING = 2;
|
|---|
| 57 | /** An unsigned 16-bit integer. */
|
|---|
| 58 | private static final int FMT_USHORT = 3;
|
|---|
| 59 | /** An unsigned 32-bit integer. */
|
|---|
| 60 | private static final int FMT_ULONG = 4;
|
|---|
| 61 | private static final int FMT_URATIONAL = 5;
|
|---|
| 62 | /** An 8-bit signed integer. */
|
|---|
| 63 | private static final int FMT_SBYTE = 6;
|
|---|
| 64 | private static final int FMT_UNDEFINED = 7;
|
|---|
| 65 | /** A signed 16-bit integer. */
|
|---|
| 66 | private static final int FMT_SSHORT = 8;
|
|---|
| 67 | /** A signed 32-bit integer. */
|
|---|
| 68 | private static final int FMT_SLONG = 9;
|
|---|
| 69 | private static final int FMT_SRATIONAL = 10;
|
|---|
| 70 | /** A 32-bit floating point number. */
|
|---|
| 71 | private static final int FMT_SINGLE = 11;
|
|---|
| 72 | /** A 64-bit floating point number. */
|
|---|
| 73 | private static final int FMT_DOUBLE = 12;
|
|---|
| 74 |
|
|---|
| 75 | /** This tag is a pointer to the Exif SubIFD. */
|
|---|
| 76 | public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769;
|
|---|
| 77 | /** This tag is a pointer to the Exif Interop IFD. */
|
|---|
| 78 | public static final int TAG_INTEROP_OFFSET = 0xA005;
|
|---|
| 79 | /** This tag is a pointer to the Exif GPS IFD. */
|
|---|
| 80 | public static final int TAG_GPS_INFO_OFFSET = 0x8825;
|
|---|
| 81 | /** This tag is a pointer to the Exif Makernote IFD. */
|
|---|
| 82 | public static final int TAG_MAKER_NOTE_OFFSET = 0x927C;
|
|---|
| 83 |
|
|---|
| 84 | public static final int TIFF_HEADER_START_OFFSET = 6;
|
|---|
| 85 |
|
|---|
| 86 | /**
|
|---|
| 87 | * Performs the Exif data extraction, adding found values to the specified
|
|---|
| 88 | * instance of <code>Metadata</code>.
|
|---|
| 89 | *
|
|---|
| 90 | * @param reader The buffer reader from which Exif data should be read.
|
|---|
| 91 | * @param metadata The Metadata object into which extracted values should be merged.
|
|---|
| 92 | */
|
|---|
| 93 | public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata)
|
|---|
| 94 | {
|
|---|
| 95 | final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
|
|---|
| 96 |
|
|---|
| 97 | // check for the header length
|
|---|
| 98 | if (reader.getLength() <= 14) {
|
|---|
| 99 | directory.addError("Exif data segment must contain at least 14 bytes");
|
|---|
| 100 | return;
|
|---|
| 101 | }
|
|---|
| 102 |
|
|---|
| 103 | // check for the header preamble
|
|---|
| 104 | try {
|
|---|
| 105 | if (!reader.getString(0, 6).equals("Exif\0\0")) {
|
|---|
| 106 | directory.addError("Exif data segment doesn't begin with 'Exif'");
|
|---|
| 107 | return;
|
|---|
| 108 | }
|
|---|
| 109 |
|
|---|
| 110 | extractIFD(metadata, metadata.getOrCreateDirectory(ExifIFD0Directory.class), TIFF_HEADER_START_OFFSET, reader);
|
|---|
| 111 | } catch (BufferBoundsException e) {
|
|---|
| 112 | directory.addError("Exif data segment ended prematurely");
|
|---|
| 113 | }
|
|---|
| 114 | }
|
|---|
| 115 |
|
|---|
| 116 | /**
|
|---|
| 117 | * Performs the Exif data extraction on a TIFF/RAW, adding found values to the specified
|
|---|
| 118 | * instance of <code>Metadata</code>.
|
|---|
| 119 | *
|
|---|
| 120 | * @param reader The BufferReader from which TIFF data should be read.
|
|---|
| 121 | * @param metadata The Metadata object into which extracted values should be merged.
|
|---|
| 122 | */
|
|---|
| 123 | public void extractTiff(@NotNull BufferReader reader, @NotNull Metadata metadata)
|
|---|
| 124 | {
|
|---|
| 125 | final ExifIFD0Directory directory = metadata.getOrCreateDirectory(ExifIFD0Directory.class);
|
|---|
| 126 |
|
|---|
| 127 | try {
|
|---|
| 128 | extractIFD(metadata, directory, 0, reader);
|
|---|
| 129 | } catch (BufferBoundsException e) {
|
|---|
| 130 | directory.addError("Exif data segment ended prematurely");
|
|---|
| 131 | }
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | private void extractIFD(@NotNull Metadata metadata, @NotNull final ExifIFD0Directory directory, int tiffHeaderOffset, @NotNull BufferReader reader) throws BufferBoundsException
|
|---|
| 135 | {
|
|---|
| 136 | // this should be either "MM" or "II"
|
|---|
| 137 | String byteOrderIdentifier = reader.getString(tiffHeaderOffset, 2);
|
|---|
| 138 |
|
|---|
| 139 | if ("MM".equals(byteOrderIdentifier)) {
|
|---|
| 140 | reader.setMotorolaByteOrder(true);
|
|---|
| 141 | } else if ("II".equals(byteOrderIdentifier)) {
|
|---|
| 142 | reader.setMotorolaByteOrder(false);
|
|---|
| 143 | } else {
|
|---|
| 144 | directory.addError("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);
|
|---|
| 145 | return;
|
|---|
| 146 | }
|
|---|
| 147 |
|
|---|
| 148 | // Check the next two values for correctness.
|
|---|
| 149 | final int tiffMarker = reader.getUInt16(2 + tiffHeaderOffset);
|
|---|
| 150 |
|
|---|
| 151 | final int standardTiffMarker = 0x002A;
|
|---|
| 152 | final int olympusRawTiffMarker = 0x4F52; // for ORF files
|
|---|
| 153 | final int panasonicRawTiffMarker = 0x0055; // for RW2 files
|
|---|
| 154 |
|
|---|
| 155 | if (tiffMarker != standardTiffMarker && tiffMarker != olympusRawTiffMarker && tiffMarker != panasonicRawTiffMarker) {
|
|---|
| 156 | directory.addError("Unexpected TIFF marker after byte order identifier: 0x" + Integer.toHexString(tiffMarker));
|
|---|
| 157 | return;
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | int firstDirectoryOffset = reader.getInt32(4 + tiffHeaderOffset) + tiffHeaderOffset;
|
|---|
| 161 |
|
|---|
| 162 | // David Ekholm sent a digital camera image that has this problem
|
|---|
| 163 | if (firstDirectoryOffset >= reader.getLength() - 1) {
|
|---|
| 164 | directory.addError("First exif directory offset is beyond end of Exif data segment");
|
|---|
| 165 | // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case
|
|---|
| 166 | firstDirectoryOffset = 14;
|
|---|
| 167 | }
|
|---|
| 168 |
|
|---|
| 169 | Set<Integer> processedDirectoryOffsets = new HashSet<Integer>();
|
|---|
| 170 |
|
|---|
| 171 | processDirectory(directory, processedDirectoryOffsets, firstDirectoryOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 172 |
|
|---|
| 173 | // after the extraction process, if we have the correct tags, we may be able to store thumbnail information
|
|---|
| 174 | ExifThumbnailDirectory thumbnailDirectory = metadata.getDirectory(ExifThumbnailDirectory.class);
|
|---|
| 175 | if (thumbnailDirectory!=null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) {
|
|---|
| 176 | Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
|
|---|
| 177 | Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
|
|---|
| 178 | if (offset != null && length != null) {
|
|---|
| 179 | try {
|
|---|
| 180 | byte[] thumbnailData = reader.getBytes(tiffHeaderOffset + offset, length);
|
|---|
| 181 | thumbnailDirectory.setThumbnailData(thumbnailData);
|
|---|
| 182 | } catch (BufferBoundsException ex) {
|
|---|
| 183 | directory.addError("Invalid thumbnail data specification: " + ex.getMessage());
|
|---|
| 184 | }
|
|---|
| 185 | }
|
|---|
| 186 | }
|
|---|
| 187 | }
|
|---|
| 188 |
|
|---|
| 189 | /**
|
|---|
| 190 | * Process one of the nested Tiff IFD directories.
|
|---|
| 191 | * <p/>
|
|---|
| 192 | * Header
|
|---|
| 193 | * 2 bytes: number of tags
|
|---|
| 194 | * <p/>
|
|---|
| 195 | * Then for each tag
|
|---|
| 196 | * 2 bytes: tag type
|
|---|
| 197 | * 2 bytes: format code
|
|---|
| 198 | * 4 bytes: component count
|
|---|
| 199 | */
|
|---|
| 200 | private void processDirectory(@NotNull Directory directory, @NotNull Set<Integer> processedDirectoryOffsets, int dirStartOffset, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull final BufferReader reader) throws BufferBoundsException
|
|---|
| 201 | {
|
|---|
| 202 | // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist
|
|---|
| 203 | if (processedDirectoryOffsets.contains(Integer.valueOf(dirStartOffset)))
|
|---|
| 204 | return;
|
|---|
| 205 |
|
|---|
| 206 | // remember that we've visited this directory so that we don't visit it again later
|
|---|
| 207 | processedDirectoryOffsets.add(dirStartOffset);
|
|---|
| 208 |
|
|---|
| 209 | if (dirStartOffset >= reader.getLength() || dirStartOffset < 0) {
|
|---|
| 210 | directory.addError("Ignored directory marked to start outside data segment");
|
|---|
| 211 | return;
|
|---|
| 212 | }
|
|---|
| 213 |
|
|---|
| 214 | // First two bytes in the IFD are the number of tags in this directory
|
|---|
| 215 | int dirTagCount = reader.getUInt16(dirStartOffset);
|
|---|
| 216 |
|
|---|
| 217 | int dirLength = (2 + (12 * dirTagCount) + 4);
|
|---|
| 218 | if (dirLength + dirStartOffset > reader.getLength()) {
|
|---|
| 219 | directory.addError("Illegally sized directory");
|
|---|
| 220 | return;
|
|---|
| 221 | }
|
|---|
| 222 |
|
|---|
| 223 | // Handle each tag in this directory
|
|---|
| 224 | for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) {
|
|---|
| 225 | final int tagOffset = calculateTagOffset(dirStartOffset, tagNumber);
|
|---|
| 226 |
|
|---|
| 227 | // 2 bytes for the tag type
|
|---|
| 228 | final int tagType = reader.getUInt16(tagOffset);
|
|---|
| 229 |
|
|---|
| 230 | // 2 bytes for the format code
|
|---|
| 231 | final int formatCode = reader.getUInt16(tagOffset + 2);
|
|---|
| 232 | if (formatCode < 1 || formatCode > MAX_FORMAT_CODE) {
|
|---|
| 233 | // This error suggests that we are processing at an incorrect index and will generate
|
|---|
| 234 | // rubbish until we go out of bounds (which may be a while). Exit now.
|
|---|
| 235 | directory.addError("Invalid TIFF tag format code: " + formatCode);
|
|---|
| 236 | continue; // JOSM patch to fix #9030
|
|---|
| 237 | }
|
|---|
| 238 |
|
|---|
| 239 | // 4 bytes dictate the number of components in this tag's data
|
|---|
| 240 | final int componentCount = reader.getInt32(tagOffset + 4);
|
|---|
| 241 | if (componentCount < 0) {
|
|---|
| 242 | directory.addError("Negative TIFF tag component count");
|
|---|
| 243 | continue;
|
|---|
| 244 | }
|
|---|
| 245 | // each component may have more than one byte... calculate the total number of bytes
|
|---|
| 246 | final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode];
|
|---|
| 247 | final int tagValueOffset;
|
|---|
| 248 | if (byteCount > 4) {
|
|---|
| 249 | // If it's bigger than 4 bytes, the dir entry contains an offset.
|
|---|
| 250 | // dirEntryOffset must be passed, as some makernote implementations (e.g. FujiFilm) incorrectly use an
|
|---|
| 251 | // offset relative to the start of the makernote itself, not the TIFF segment.
|
|---|
| 252 | final int offsetVal = reader.getInt32(tagOffset + 8);
|
|---|
| 253 | if (offsetVal + byteCount > reader.getLength()) {
|
|---|
| 254 | // Bogus pointer offset and / or byteCount value
|
|---|
| 255 | directory.addError("Illegal TIFF tag pointer offset");
|
|---|
| 256 | continue;
|
|---|
| 257 | }
|
|---|
| 258 | tagValueOffset = tiffHeaderOffset + offsetVal;
|
|---|
| 259 | } else {
|
|---|
| 260 | // 4 bytes or less and value is in the dir entry itself
|
|---|
| 261 | tagValueOffset = tagOffset + 8;
|
|---|
| 262 | }
|
|---|
| 263 |
|
|---|
| 264 | if (tagValueOffset < 0 || tagValueOffset > reader.getLength()) {
|
|---|
| 265 | directory.addError("Illegal TIFF tag pointer offset");
|
|---|
| 266 | continue;
|
|---|
| 267 | }
|
|---|
| 268 |
|
|---|
| 269 | // Check that this tag isn't going to allocate outside the bounds of the data array.
|
|---|
| 270 | // This addresses an uncommon OutOfMemoryError.
|
|---|
| 271 | if (byteCount < 0 || tagValueOffset + byteCount > reader.getLength()) {
|
|---|
| 272 | directory.addError("Illegal number of bytes: " + byteCount);
|
|---|
| 273 | continue;
|
|---|
| 274 | }
|
|---|
| 275 |
|
|---|
| 276 | switch (tagType) {
|
|---|
| 277 | case TAG_EXIF_SUB_IFD_OFFSET: {
|
|---|
| 278 | final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
|
|---|
| 279 | processDirectory(metadata.getOrCreateDirectory(ExifSubIFDDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 280 | continue;
|
|---|
| 281 | }
|
|---|
| 282 | case TAG_INTEROP_OFFSET: {
|
|---|
| 283 | final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
|
|---|
| 284 | processDirectory(metadata.getOrCreateDirectory(ExifInteropDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 285 | continue;
|
|---|
| 286 | }
|
|---|
| 287 | case TAG_GPS_INFO_OFFSET: {
|
|---|
| 288 | final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
|
|---|
| 289 | processDirectory(metadata.getOrCreateDirectory(GpsDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 290 | continue;
|
|---|
| 291 | }
|
|---|
| 292 | case TAG_MAKER_NOTE_OFFSET: {
|
|---|
| 293 | processMakerNote(tagValueOffset, processedDirectoryOffsets, tiffHeaderOffset, metadata, reader);
|
|---|
| 294 | continue;
|
|---|
| 295 | }
|
|---|
| 296 | default: {
|
|---|
| 297 | processTag(directory, tagType, tagValueOffset, componentCount, formatCode, reader);
|
|---|
| 298 | break;
|
|---|
| 299 | }
|
|---|
| 300 | }
|
|---|
| 301 | }
|
|---|
| 302 |
|
|---|
| 303 | // at the end of each IFD is an optional link to the next IFD
|
|---|
| 304 | final int finalTagOffset = calculateTagOffset(dirStartOffset, dirTagCount);
|
|---|
| 305 | int nextDirectoryOffset = reader.getInt32(finalTagOffset);
|
|---|
| 306 | if (nextDirectoryOffset != 0) {
|
|---|
| 307 | nextDirectoryOffset += tiffHeaderOffset;
|
|---|
| 308 | if (nextDirectoryOffset >= reader.getLength()) {
|
|---|
| 309 | // Last 4 bytes of IFD reference another IFD with an address that is out of bounds
|
|---|
| 310 | // Note this could have been caused by jhead 1.3 cropping too much
|
|---|
| 311 | return;
|
|---|
| 312 | } else if (nextDirectoryOffset < dirStartOffset) {
|
|---|
| 313 | // Last 4 bytes of IFD reference another IFD with an address that is before the start of this directory
|
|---|
| 314 | return;
|
|---|
| 315 | }
|
|---|
| 316 | // TODO in Exif, the only known 'follower' IFD is the thumbnail one, however this may not be the case
|
|---|
| 317 | final ExifThumbnailDirectory nextDirectory = metadata.getOrCreateDirectory(ExifThumbnailDirectory.class);
|
|---|
| 318 | processDirectory(nextDirectory, processedDirectoryOffsets, nextDirectoryOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 319 | }
|
|---|
| 320 | }
|
|---|
| 321 |
|
|---|
| 322 | private void processMakerNote(int subdirOffset, @NotNull Set<Integer> processedDirectoryOffsets, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull BufferReader reader) throws BufferBoundsException
|
|---|
| 323 | {
|
|---|
| 324 | // Determine the camera model and makernote format
|
|---|
| 325 | Directory ifd0Directory = metadata.getDirectory(ExifIFD0Directory.class);
|
|---|
| 326 |
|
|---|
| 327 | if (ifd0Directory==null)
|
|---|
| 328 | return;
|
|---|
| 329 |
|
|---|
| 330 | String cameraModel = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);
|
|---|
| 331 |
|
|---|
| 332 | //final String firstTwoChars = reader.getString(subdirOffset, 2);
|
|---|
| 333 | final String firstThreeChars = reader.getString(subdirOffset, 3);
|
|---|
| 334 | final String firstFourChars = reader.getString(subdirOffset, 4);
|
|---|
| 335 | final String firstFiveChars = reader.getString(subdirOffset, 5);
|
|---|
| 336 | final String firstSixChars = reader.getString(subdirOffset, 6);
|
|---|
| 337 | final String firstSevenChars = reader.getString(subdirOffset, 7);
|
|---|
| 338 | final String firstEightChars = reader.getString(subdirOffset, 8);
|
|---|
| 339 | final String firstTwelveChars = reader.getString(subdirOffset, 12);
|
|---|
| 340 |
|
|---|
| 341 | if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {
|
|---|
| 342 | // Olympus Makernote
|
|---|
| 343 | // Epson and Agfa use Olympus maker note standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/
|
|---|
| 344 | processDirectory(metadata.getOrCreateDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset, metadata, reader);
|
|---|
| 345 | } else if (cameraModel != null && cameraModel.trim().toUpperCase().startsWith("NIKON")) {
|
|---|
| 346 | if ("Nikon".equals(firstFiveChars)) {
|
|---|
| 347 | /* There are two scenarios here:
|
|---|
| 348 | * Type 1: **
|
|---|
| 349 | * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
|
|---|
| 350 | * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
|
|---|
| 351 | * Type 3: **
|
|---|
| 352 | * :0000: 4E 69 6B 6F 6E 00 02 00-00 00 4D 4D 00 2A 00 00 Nikon....MM.*...
|
|---|
| 353 | * :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200
|
|---|
| 354 | */
|
|---|
| 355 | switch (reader.getUInt8(subdirOffset + 6)) {
|
|---|
| 356 | case 1:
|
|---|
| 357 | processDirectory(metadata.getOrCreateDirectory(NikonType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset, metadata, reader);
|
|---|
| 358 | break;
|
|---|
| 359 | case 2:
|
|---|
| 360 | processDirectory(metadata.getOrCreateDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 18, subdirOffset + 10, metadata, reader);
|
|---|
| 361 | break;
|
|---|
| 362 | default:
|
|---|
| 363 | ifd0Directory.addError("Unsupported Nikon makernote data ignored.");
|
|---|
| 364 | break;
|
|---|
| 365 | }
|
|---|
| 366 | } else {
|
|---|
| 367 | // The IFD begins with the first MakerNote byte (no ASCII name). This occurs with CoolPix 775, E990 and D1 models.
|
|---|
| 368 | processDirectory(metadata.getOrCreateDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 369 | }
|
|---|
| 370 | } else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars)) {
|
|---|
| 371 | processDirectory(metadata.getOrCreateDirectory(SonyType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset, metadata, reader);
|
|---|
| 372 | } else if ("SIGMA\u0000\u0000\u0000".equals(firstEightChars) || "FOVEON\u0000\u0000".equals(firstEightChars)) {
|
|---|
| 373 | processDirectory(metadata.getOrCreateDirectory(SigmaMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 10, tiffHeaderOffset, metadata, reader);
|
|---|
| 374 | } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) {
|
|---|
| 375 | // force MM for this directory
|
|---|
| 376 | boolean isMotorola = reader.isMotorolaByteOrder();
|
|---|
| 377 | reader.setMotorolaByteOrder(true);
|
|---|
| 378 | // skip 12 byte header + 2 for "MM" + 6
|
|---|
| 379 | processDirectory(metadata.getOrCreateDirectory(SonyType6MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset, metadata, reader);
|
|---|
| 380 | reader.setMotorolaByteOrder(isMotorola);
|
|---|
| 381 | } else if ("KDK".equals(firstThreeChars)) {
|
|---|
| 382 | processDirectory(metadata.getOrCreateDirectory(KodakMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset, metadata, reader);
|
|---|
| 383 | } else if ("Canon".equalsIgnoreCase(cameraModel)) {
|
|---|
| 384 | processDirectory(metadata.getOrCreateDirectory(CanonMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 385 | } else if (cameraModel != null && cameraModel.toUpperCase().startsWith("CASIO")) {
|
|---|
| 386 | if ("QVC\u0000\u0000\u0000".equals(firstSixChars))
|
|---|
| 387 | processDirectory(metadata.getOrCreateDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, tiffHeaderOffset, metadata, reader);
|
|---|
| 388 | else
|
|---|
| 389 | processDirectory(metadata.getOrCreateDirectory(CasioType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 390 | } else if ("FUJIFILM".equals(firstEightChars) || "Fujifilm".equalsIgnoreCase(cameraModel)) {
|
|---|
| 391 | boolean byteOrderBefore = reader.isMotorolaByteOrder();
|
|---|
| 392 | // bug in fujifilm makernote ifd means we temporarily use Intel byte ordering
|
|---|
| 393 | reader.setMotorolaByteOrder(false);
|
|---|
| 394 | // the 4 bytes after "FUJIFILM" in the makernote point to the start of the makernote
|
|---|
| 395 | // IFD, though the offset is relative to the start of the makernote, not the TIFF
|
|---|
| 396 | // header (like everywhere else)
|
|---|
| 397 | int ifdStart = subdirOffset + reader.getInt32(subdirOffset + 8);
|
|---|
| 398 | processDirectory(metadata.getOrCreateDirectory(FujifilmMakernoteDirectory.class), processedDirectoryOffsets, ifdStart, tiffHeaderOffset, metadata, reader);
|
|---|
| 399 | reader.setMotorolaByteOrder(byteOrderBefore);
|
|---|
| 400 | } else if (cameraModel != null && cameraModel.toUpperCase().startsWith("MINOLTA")) {
|
|---|
| 401 | // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote
|
|---|
| 402 | // area that commences immediately.
|
|---|
| 403 | processDirectory(metadata.getOrCreateDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
|
|---|
| 404 | } else if ("KYOCERA".equals(firstSevenChars)) {
|
|---|
| 405 | // http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html
|
|---|
| 406 | processDirectory(metadata.getOrCreateDirectory(KyoceraMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 22, tiffHeaderOffset, metadata, reader);
|
|---|
| 407 | } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(subdirOffset, 12))) {
|
|---|
| 408 | // NON-Standard TIFF IFD Data using Panasonic Tags. There is no Next-IFD pointer after the IFD
|
|---|
| 409 | // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment
|
|---|
| 410 | // more information here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html
|
|---|
| 411 | processDirectory(metadata.getOrCreateDirectory(PanasonicMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset, metadata, reader);
|
|---|
| 412 | } else if ("AOC\u0000".equals(firstFourChars)) {
|
|---|
| 413 | // NON-Standard TIFF IFD Data using Casio Type 2 Tags
|
|---|
| 414 | // IFD has no Next-IFD pointer at end of IFD, and
|
|---|
| 415 | // Offsets are relative to the start of the current IFD tag, not the TIFF header
|
|---|
| 416 | // Observed for:
|
|---|
| 417 | // - Pentax ist D
|
|---|
| 418 | processDirectory(metadata.getOrCreateDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, subdirOffset, metadata, reader);
|
|---|
| 419 | } else if (cameraModel != null && (cameraModel.toUpperCase().startsWith("PENTAX") || cameraModel.toUpperCase().startsWith("ASAHI"))) {
|
|---|
| 420 | // NON-Standard TIFF IFD Data using Pentax Tags
|
|---|
| 421 | // IFD has no Next-IFD pointer at end of IFD, and
|
|---|
| 422 | // Offsets are relative to the start of the current IFD tag, not the TIFF header
|
|---|
| 423 | // Observed for:
|
|---|
| 424 | // - PENTAX Optio 330
|
|---|
| 425 | // - PENTAX Optio 430
|
|---|
| 426 | processDirectory(metadata.getOrCreateDirectory(PentaxMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, subdirOffset, metadata, reader);
|
|---|
| 427 | // } else if ("KC".equals(firstTwoChars) || "MINOL".equals(firstFiveChars) || "MLY".equals(firstThreeChars) || "+M+M+M+M".equals(firstEightChars)) {
|
|---|
| 428 | // // This Konica data is not understood. Header identified in accordance with information at this site:
|
|---|
| 429 | // // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html
|
|---|
| 430 | // // TODO add support for minolta/konica cameras
|
|---|
| 431 | // exifDirectory.addError("Unsupported Konica/Minolta data ignored.");
|
|---|
| 432 | } else {
|
|---|
| 433 | // TODO how to store makernote data when it's not from a supported camera model?
|
|---|
| 434 | // this is difficult as the starting offset is not known. we could look for it...
|
|---|
| 435 | }
|
|---|
| 436 | }
|
|---|
| 437 |
|
|---|
| 438 | private void processTag(@NotNull Directory directory, int tagType, int tagValueOffset, int componentCount, int formatCode, @NotNull final BufferReader reader) throws BufferBoundsException
|
|---|
| 439 | {
|
|---|
| 440 | // Directory simply stores raw values
|
|---|
| 441 | // The display side uses a Descriptor class per directory to turn the raw values into 'pretty' descriptions
|
|---|
| 442 | switch (formatCode) {
|
|---|
| 443 | case FMT_UNDEFINED:
|
|---|
| 444 | // this includes exif user comments
|
|---|
| 445 | directory.setByteArray(tagType, reader.getBytes(tagValueOffset, componentCount));
|
|---|
| 446 | break;
|
|---|
| 447 | case FMT_STRING:
|
|---|
| 448 | String string = reader.getNullTerminatedString(tagValueOffset, componentCount);
|
|---|
| 449 | directory.setString(tagType, string);
|
|---|
| 450 | /*
|
|---|
| 451 | // special handling for certain known tags, proposed by Yuri Binev but left out for now,
|
|---|
| 452 | // as it gives the false impression that the image was captured in the same timezone
|
|---|
| 453 | // in which the string is parsed
|
|---|
| 454 | if (tagType==ExifSubIFDDirectory.TAG_DATETIME ||
|
|---|
| 455 | tagType==ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL ||
|
|---|
| 456 | tagType==ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED) {
|
|---|
| 457 | String[] datePatterns = {
|
|---|
| 458 | "yyyy:MM:dd HH:mm:ss",
|
|---|
| 459 | "yyyy:MM:dd HH:mm",
|
|---|
| 460 | "yyyy-MM-dd HH:mm:ss",
|
|---|
| 461 | "yyyy-MM-dd HH:mm"};
|
|---|
| 462 | for (String datePattern : datePatterns) {
|
|---|
| 463 | try {
|
|---|
| 464 | DateFormat parser = new SimpleDateFormat(datePattern);
|
|---|
| 465 | Date date = parser.parse(string);
|
|---|
| 466 | directory.setDate(tagType, date);
|
|---|
| 467 | break;
|
|---|
| 468 | } catch (ParseException ex) {
|
|---|
| 469 | // simply try the next pattern
|
|---|
| 470 | }
|
|---|
| 471 | }
|
|---|
| 472 | }
|
|---|
| 473 | */
|
|---|
| 474 | break;
|
|---|
| 475 | case FMT_SRATIONAL:
|
|---|
| 476 | if (componentCount == 1) {
|
|---|
| 477 | directory.setRational(tagType, new Rational(reader.getInt32(tagValueOffset), reader.getInt32(tagValueOffset + 4)));
|
|---|
| 478 | } else if (componentCount > 1) {
|
|---|
| 479 | Rational[] rationals = new Rational[componentCount];
|
|---|
| 480 | for (int i = 0; i < componentCount; i++)
|
|---|
| 481 | rationals[i] = new Rational(reader.getInt32(tagValueOffset + (8 * i)), reader.getInt32(tagValueOffset + 4 + (8 * i)));
|
|---|
| 482 | directory.setRationalArray(tagType, rationals);
|
|---|
| 483 | }
|
|---|
| 484 | break;
|
|---|
| 485 | case FMT_URATIONAL:
|
|---|
| 486 | if (componentCount == 1) {
|
|---|
| 487 | directory.setRational(tagType, new Rational(reader.getUInt32(tagValueOffset), reader.getUInt32(tagValueOffset + 4)));
|
|---|
| 488 | } else if (componentCount > 1) {
|
|---|
| 489 | Rational[] rationals = new Rational[componentCount];
|
|---|
| 490 | for (int i = 0; i < componentCount; i++)
|
|---|
| 491 | rationals[i] = new Rational(reader.getUInt32(tagValueOffset + (8 * i)), reader.getUInt32(tagValueOffset + 4 + (8 * i)));
|
|---|
| 492 | directory.setRationalArray(tagType, rationals);
|
|---|
| 493 | }
|
|---|
| 494 | break;
|
|---|
| 495 | case FMT_SINGLE:
|
|---|
| 496 | if (componentCount == 1) {
|
|---|
| 497 | directory.setFloat(tagType, reader.getFloat32(tagValueOffset));
|
|---|
| 498 | } else {
|
|---|
| 499 | float[] floats = new float[componentCount];
|
|---|
| 500 | for (int i = 0; i < componentCount; i++)
|
|---|
| 501 | floats[i] = reader.getFloat32(tagValueOffset + (i * 4));
|
|---|
| 502 | directory.setFloatArray(tagType, floats);
|
|---|
| 503 | }
|
|---|
| 504 | break;
|
|---|
| 505 | case FMT_DOUBLE:
|
|---|
| 506 | if (componentCount == 1) {
|
|---|
| 507 | directory.setDouble(tagType, reader.getDouble64(tagValueOffset));
|
|---|
| 508 | } else {
|
|---|
| 509 | double[] doubles = new double[componentCount];
|
|---|
| 510 | for (int i = 0; i < componentCount; i++)
|
|---|
| 511 | doubles[i] = reader.getDouble64(tagValueOffset + (i * 4));
|
|---|
| 512 | directory.setDoubleArray(tagType, doubles);
|
|---|
| 513 | }
|
|---|
| 514 | break;
|
|---|
| 515 |
|
|---|
| 516 | //
|
|---|
| 517 | // Note that all integral types are stored as int32 internally (the largest supported by TIFF)
|
|---|
| 518 | //
|
|---|
| 519 |
|
|---|
| 520 | case FMT_SBYTE:
|
|---|
| 521 | if (componentCount == 1) {
|
|---|
| 522 | directory.setInt(tagType, reader.getInt8(tagValueOffset));
|
|---|
| 523 | } else {
|
|---|
| 524 | int[] bytes = new int[componentCount];
|
|---|
| 525 | for (int i = 0; i < componentCount; i++)
|
|---|
| 526 | bytes[i] = reader.getInt8(tagValueOffset + i);
|
|---|
| 527 | directory.setIntArray(tagType, bytes);
|
|---|
| 528 | }
|
|---|
| 529 | break;
|
|---|
| 530 | case FMT_BYTE:
|
|---|
| 531 | if (componentCount == 1) {
|
|---|
| 532 | directory.setInt(tagType, reader.getUInt8(tagValueOffset));
|
|---|
| 533 | } else {
|
|---|
| 534 | int[] bytes = new int[componentCount];
|
|---|
| 535 | for (int i = 0; i < componentCount; i++)
|
|---|
| 536 | bytes[i] = reader.getUInt8(tagValueOffset + i);
|
|---|
| 537 | directory.setIntArray(tagType, bytes);
|
|---|
| 538 | }
|
|---|
| 539 | break;
|
|---|
| 540 | case FMT_USHORT:
|
|---|
| 541 | if (componentCount == 1) {
|
|---|
| 542 | int i = reader.getUInt16(tagValueOffset);
|
|---|
| 543 | directory.setInt(tagType, i);
|
|---|
| 544 | } else {
|
|---|
| 545 | int[] ints = new int[componentCount];
|
|---|
| 546 | for (int i = 0; i < componentCount; i++)
|
|---|
| 547 | ints[i] = reader.getUInt16(tagValueOffset + (i * 2));
|
|---|
| 548 | directory.setIntArray(tagType, ints);
|
|---|
| 549 | }
|
|---|
| 550 | break;
|
|---|
| 551 | case FMT_SSHORT:
|
|---|
| 552 | if (componentCount == 1) {
|
|---|
| 553 | int i = reader.getInt16(tagValueOffset);
|
|---|
| 554 | directory.setInt(tagType, i);
|
|---|
| 555 | } else {
|
|---|
| 556 | int[] ints = new int[componentCount];
|
|---|
| 557 | for (int i = 0; i < componentCount; i++)
|
|---|
| 558 | ints[i] = reader.getInt16(tagValueOffset + (i * 2));
|
|---|
| 559 | directory.setIntArray(tagType, ints);
|
|---|
| 560 | }
|
|---|
| 561 | break;
|
|---|
| 562 | case FMT_SLONG:
|
|---|
| 563 | case FMT_ULONG:
|
|---|
| 564 | // NOTE 'long' in this case means 32 bit, not 64
|
|---|
| 565 | if (componentCount == 1) {
|
|---|
| 566 | int i = reader.getInt32(tagValueOffset);
|
|---|
| 567 | directory.setInt(tagType, i);
|
|---|
| 568 | } else {
|
|---|
| 569 | int[] ints = new int[componentCount];
|
|---|
| 570 | for (int i = 0; i < componentCount; i++)
|
|---|
| 571 | ints[i] = reader.getInt32(tagValueOffset + (i * 4));
|
|---|
| 572 | directory.setIntArray(tagType, ints);
|
|---|
| 573 | }
|
|---|
| 574 | break;
|
|---|
| 575 | default:
|
|---|
| 576 | directory.addError("Unknown format code " + formatCode + " for tag " + tagType);
|
|---|
| 577 | }
|
|---|
| 578 | }
|
|---|
| 579 |
|
|---|
| 580 | /**
|
|---|
| 581 | * Determine the offset at which a given InteropArray entry begins within the specified IFD.
|
|---|
| 582 | *
|
|---|
| 583 | * @param dirStartOffset the offset at which the IFD starts
|
|---|
| 584 | * @param entryNumber the zero-based entry number
|
|---|
| 585 | */
|
|---|
| 586 | private int calculateTagOffset(int dirStartOffset, int entryNumber)
|
|---|
| 587 | {
|
|---|
| 588 | // add 2 bytes for the tag count
|
|---|
| 589 | // each entry is 12 bytes, so we skip 12 * the number seen so far
|
|---|
| 590 | return dirStartOffset + 2 + (12 * entryNumber);
|
|---|
| 591 | }
|
|---|
| 592 | }
|
|---|