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 static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes; 020import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes; 021import static org.apache.commons.imaging.common.BinaryFunctions.readByte; 022import static org.apache.commons.imaging.common.BinaryFunctions.readBytes; 023import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes; 024import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_ENTRY_MAX_VALUE_LENGTH; 025 026import java.io.IOException; 027import java.io.InputStream; 028import java.nio.ByteOrder; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.List; 032import java.util.Map; 033 034import org.apache.commons.imaging.FormatCompliance; 035import org.apache.commons.imaging.ImageReadException; 036import org.apache.commons.imaging.ImagingConstants; 037import org.apache.commons.imaging.common.BinaryFileParser; 038import org.apache.commons.imaging.common.ByteConversions; 039import org.apache.commons.imaging.common.bytesource.ByteSource; 040import org.apache.commons.imaging.common.bytesource.ByteSourceFile; 041import org.apache.commons.imaging.formats.jpeg.JpegConstants; 042import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement; 043import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants; 044import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants; 045import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; 046import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType; 047import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDirectory; 048 049public class TiffReader extends BinaryFileParser { 050 051 private final boolean strict; 052 053 public TiffReader(final boolean strict) { 054 this.strict = strict; 055 } 056 057 private TiffHeader readTiffHeader(final ByteSource byteSource) throws ImageReadException, IOException { 058 try (InputStream is = byteSource.getInputStream()) { 059 return readTiffHeader(is); 060 } 061 } 062 063 private ByteOrder getTiffByteOrder(final int byteOrderByte) throws ImageReadException { 064 if (byteOrderByte == 'I') { 065 return ByteOrder.LITTLE_ENDIAN; // Intel 066 } else if (byteOrderByte == 'M') { 067 return ByteOrder.BIG_ENDIAN; // Motorola 068 } else { 069 throw new ImageReadException("Invalid TIFF byte order " + (0xff & byteOrderByte)); 070 } 071 } 072 073 private TiffHeader readTiffHeader(final InputStream is) throws ImageReadException, IOException { 074 final int byteOrder1 = readByte("BYTE_ORDER_1", is, "Not a Valid TIFF File"); 075 final int byteOrder2 = readByte("BYTE_ORDER_2", is, "Not a Valid TIFF File"); 076 if (byteOrder1 != byteOrder2) { 077 throw new ImageReadException("Byte Order bytes don't match (" + byteOrder1 + ", " + byteOrder2 + ")."); 078 } 079 080 final ByteOrder byteOrder = getTiffByteOrder(byteOrder1); 081 setByteOrder(byteOrder); 082 083 final int tiffVersion = read2Bytes("tiffVersion", is, "Not a Valid TIFF File", getByteOrder()); 084 if (tiffVersion != 42) { 085 throw new ImageReadException("Unknown Tiff Version: " + tiffVersion); 086 } 087 088 final long offsetToFirstIFD = 089 0xFFFFffffL & read4Bytes("offsetToFirstIFD", is, "Not a Valid TIFF File", getByteOrder()); 090 091 skipBytes(is, offsetToFirstIFD - 8, "Not a Valid TIFF File: couldn't find IFDs"); 092 093 return new TiffHeader(byteOrder, tiffVersion, offsetToFirstIFD); 094 } 095 096 private void readDirectories(final ByteSource byteSource, 097 final FormatCompliance formatCompliance, final Listener listener) 098 throws ImageReadException, IOException { 099 final TiffHeader tiffHeader = readTiffHeader(byteSource); 100 if (!listener.setTiffHeader(tiffHeader)) { 101 return; 102 } 103 104 final long offset = tiffHeader.offsetToFirstIFD; 105 final int dirType = TiffDirectoryConstants.DIRECTORY_TYPE_ROOT; 106 107 final List<Number> visited = new ArrayList<>(); 108 readDirectory(byteSource, offset, dirType, formatCompliance, listener, visited); 109 } 110 111 private boolean readDirectory(final ByteSource byteSource, final long offset, 112 final int dirType, final FormatCompliance formatCompliance, final Listener listener, 113 final List<Number> visited) throws ImageReadException, IOException { 114 final boolean ignoreNextDirectory = false; 115 return readDirectory(byteSource, offset, dirType, formatCompliance, 116 listener, ignoreNextDirectory, visited); 117 } 118 119 private boolean readDirectory(final ByteSource byteSource, final long directoryOffset, 120 final int dirType, final FormatCompliance formatCompliance, final Listener listener, 121 final boolean ignoreNextDirectory, final List<Number> visited) 122 throws ImageReadException, IOException { 123 124 if (visited.contains(directoryOffset)) { 125 return false; 126 } 127 visited.add(directoryOffset); 128 129 try (InputStream is = byteSource.getInputStream()) { 130 if (directoryOffset >= byteSource.getLength()) { 131 return true; 132 } 133 134 skipBytes(is, directoryOffset); 135 136 final List<TiffField> fields = new ArrayList<>(); 137 138 int entryCount; 139 try { 140 entryCount = read2Bytes("DirectoryEntryCount", is, "Not a Valid TIFF File", getByteOrder()); 141 } catch (final IOException e) { 142 if (strict) { 143 throw e; 144 } 145 return true; 146 } 147 148 for (int i = 0; i < entryCount; i++) { 149 final int tag = read2Bytes("Tag", is, "Not a Valid TIFF File", getByteOrder()); 150 final int type = read2Bytes("Type", is, "Not a Valid TIFF File", getByteOrder()); 151 final long count = 0xFFFFffffL & read4Bytes("Count", is, "Not a Valid TIFF File", getByteOrder()); 152 final byte[] offsetBytes = readBytes("Offset", is, 4, "Not a Valid TIFF File"); 153 final long offset = 0xFFFFffffL & ByteConversions.toInt(offsetBytes, getByteOrder()); 154 155 if (tag == 0) { 156 // skip invalid fields. 157 // These are seen very rarely, but can have invalid value 158 // lengths, 159 // which can cause OOM problems. 160 continue; 161 } 162 163 final FieldType fieldType; 164 try { 165 fieldType = FieldType.getFieldType(type); 166 } catch (final ImageReadException imageReadEx) { 167 // skip over unknown fields types, since we 168 // can't calculate their size without 169 // knowing their type 170 continue; 171 } 172 final long valueLength = count * fieldType.getSize(); 173 final byte[] value; 174 if (valueLength > TIFF_ENTRY_MAX_VALUE_LENGTH) { 175 if ((offset < 0) || (offset + valueLength) > byteSource.getLength()) { 176 if (strict) { 177 throw new IOException( 178 "Attempt to read byte range starting from " + offset + " " 179 + "of length " + valueLength + " " 180 + "which is outside the file's size of " 181 + byteSource.getLength()); 182 } else { 183 // corrupt field, ignore it 184 continue; 185 } 186 } 187 value = byteSource.getBlock(offset, (int) valueLength); 188 } else { 189 value = offsetBytes; 190 } 191 192 final TiffField field = new TiffField(tag, dirType, fieldType, count, 193 offset, value, getByteOrder(), i); 194 195 fields.add(field); 196 197 if (!listener.addField(field)) { 198 return true; 199 } 200 } 201 202 final long nextDirectoryOffset = 0xFFFFffffL & read4Bytes("nextDirectoryOffset", is, 203 "Not a Valid TIFF File", getByteOrder()); 204 205 final TiffDirectory directory = new TiffDirectory( 206 dirType, 207 fields, 208 directoryOffset, 209 nextDirectoryOffset, 210 getByteOrder()); 211 212 if (listener.readImageData()) { 213 if (directory.hasTiffImageData()) { 214 final TiffImageData rawImageData = getTiffRawImageData( 215 byteSource, directory); 216 directory.setTiffImageData(rawImageData); 217 } 218 if (directory.hasJpegImageData()) { 219 final JpegImageData rawJpegImageData = getJpegRawImageData( 220 byteSource, directory); 221 directory.setJpegImageData(rawJpegImageData); 222 } 223 } 224 225 if (!listener.addDirectory(directory)) { 226 return true; 227 } 228 229 if (listener.readOffsetDirectories()) { 230 final TagInfoDirectory[] offsetFields = { 231 ExifTagConstants.EXIF_TAG_EXIF_OFFSET, 232 ExifTagConstants.EXIF_TAG_GPSINFO, 233 ExifTagConstants.EXIF_TAG_INTEROP_OFFSET 234 }; 235 final int[] directoryTypes = { 236 TiffDirectoryConstants.DIRECTORY_TYPE_EXIF, 237 TiffDirectoryConstants.DIRECTORY_TYPE_GPS, 238 TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY 239 }; 240 for (int i = 0; i < offsetFields.length; i++) { 241 final TagInfoDirectory offsetField = offsetFields[i]; 242 final TiffField field = directory.findField(offsetField); 243 if (field != null) { 244 long subDirectoryOffset; 245 int subDirectoryType; 246 boolean subDirectoryRead = false; 247 try { 248 subDirectoryOffset = directory.getFieldValue(offsetField); 249 subDirectoryType = directoryTypes[i]; 250 subDirectoryRead = readDirectory(byteSource, 251 subDirectoryOffset, subDirectoryType, 252 formatCompliance, listener, true, visited); 253 254 } catch (final ImageReadException imageReadException) { 255 if (strict) { 256 throw imageReadException; 257 } 258 } 259 if (!subDirectoryRead) { 260 fields.remove(field); 261 } 262 } 263 } 264 } 265 266 if (!ignoreNextDirectory && directory.nextDirectoryOffset > 0) { 267 // Debug.debug("next dir", directory.nextDirectoryOffset ); 268 readDirectory(byteSource, directory.nextDirectoryOffset, 269 dirType + 1, formatCompliance, listener, visited); 270 } 271 272 return true; 273 } 274 } 275 276 public interface Listener { 277 boolean setTiffHeader(TiffHeader tiffHeader); 278 279 boolean addDirectory(TiffDirectory directory); 280 281 boolean addField(TiffField field); 282 283 boolean readImageData(); 284 285 boolean readOffsetDirectories(); 286 } 287 288 private static class Collector implements Listener { 289 private TiffHeader tiffHeader; 290 private final List<TiffDirectory> directories = new ArrayList<>(); 291 private final List<TiffField> fields = new ArrayList<>(); 292 private final boolean readThumbnails; 293 294 Collector() { 295 this(null); 296 } 297 298 Collector(final Map<String, Object> params) { 299 boolean tmpReadThumbnails = true; 300 if (params != null && params.containsKey(ImagingConstants.PARAM_KEY_READ_THUMBNAILS)) { 301 tmpReadThumbnails = Boolean.TRUE.equals(params.get(ImagingConstants.PARAM_KEY_READ_THUMBNAILS)); 302 } 303 this.readThumbnails = tmpReadThumbnails; 304 } 305 306 @Override 307 public boolean setTiffHeader(final TiffHeader tiffHeader) { 308 this.tiffHeader = tiffHeader; 309 return true; 310 } 311 312 @Override 313 public boolean addDirectory(final TiffDirectory directory) { 314 directories.add(directory); 315 return true; 316 } 317 318 @Override 319 public boolean addField(final TiffField field) { 320 fields.add(field); 321 return true; 322 } 323 324 @Override 325 public boolean readImageData() { 326 return readThumbnails; 327 } 328 329 @Override 330 public boolean readOffsetDirectories() { 331 return true; 332 } 333 334 public TiffContents getContents() { 335 return new TiffContents(tiffHeader, directories, fields); 336 } 337 } 338 339 private static class FirstDirectoryCollector extends Collector { 340 private final boolean readImageData; 341 342 FirstDirectoryCollector(final boolean readImageData) { 343 this.readImageData = readImageData; 344 } 345 346 @Override 347 public boolean addDirectory(final TiffDirectory directory) { 348 super.addDirectory(directory); 349 return false; 350 } 351 352 @Override 353 public boolean readImageData() { 354 return readImageData; 355 } 356 } 357 358// NOT USED 359// private static class DirectoryCollector extends Collector { 360// private final boolean readImageData; 361// 362// public DirectoryCollector(final boolean readImageData) { 363// this.readImageData = readImageData; 364// } 365// 366// @Override 367// public boolean addDirectory(final TiffDirectory directory) { 368// super.addDirectory(directory); 369// return false; 370// } 371// 372// @Override 373// public boolean readImageData() { 374// return readImageData; 375// } 376// } 377 378 public TiffContents readFirstDirectory(final ByteSource byteSource, final Map<String, Object> params, 379 final boolean readImageData, final FormatCompliance formatCompliance) 380 throws ImageReadException, IOException { 381 final Collector collector = new FirstDirectoryCollector(readImageData); 382 read(byteSource, params, formatCompliance, collector); 383 final TiffContents contents = collector.getContents(); 384 if (contents.directories.isEmpty()) { 385 throw new ImageReadException( 386 "Image did not contain any directories."); 387 } 388 return contents; 389 } 390 391 public TiffContents readDirectories(final ByteSource byteSource, 392 final boolean readImageData, final FormatCompliance formatCompliance) 393 throws ImageReadException, IOException { 394 Map<String, Object> params = Collections.singletonMap( 395 ImagingConstants.PARAM_KEY_READ_THUMBNAILS, readImageData); 396 final Collector collector = new Collector(params); 397 readDirectories(byteSource, formatCompliance, collector); 398 final TiffContents contents = collector.getContents(); 399 if (contents.directories.isEmpty()) { 400 throw new ImageReadException( 401 "Image did not contain any directories."); 402 } 403 return contents; 404 } 405 406 public TiffContents readContents(final ByteSource byteSource, final Map<String, Object> params, 407 final FormatCompliance formatCompliance) throws ImageReadException, 408 IOException { 409 410 final Collector collector = new Collector(params); 411 read(byteSource, params, formatCompliance, collector); 412 return collector.getContents(); 413 } 414 415 public void read(final ByteSource byteSource, final Map<String, Object> params, 416 final FormatCompliance formatCompliance, final Listener listener) 417 throws ImageReadException, IOException { 418 // TiffContents contents = 419 readDirectories(byteSource, formatCompliance, listener); 420 } 421 422 private TiffImageData getTiffRawImageData(final ByteSource byteSource, 423 final TiffDirectory directory) throws ImageReadException, IOException { 424 425 final List<ImageDataElement> elements = directory.getTiffRawImageDataElements(); 426 final TiffImageData.Data[] data = new TiffImageData.Data[elements.size()]; 427 428 if (byteSource instanceof ByteSourceFile) { 429 final ByteSourceFile bsf = (ByteSourceFile) byteSource; 430 for (int i = 0; i < elements.size(); i++) { 431 final TiffDirectory.ImageDataElement element = elements.get(i); 432 data[i] = new TiffImageData.ByteSourceData(element.offset, 433 element.length, bsf); 434 } 435 } else { 436 for (int i = 0; i < elements.size(); i++) { 437 final TiffDirectory.ImageDataElement element = elements.get(i); 438 final byte[] bytes = byteSource.getBlock(element.offset, element.length); 439 data[i] = new TiffImageData.Data(element.offset, element.length, bytes); 440 } 441 } 442 443 if (directory.imageDataInStrips()) { 444 final TiffField rowsPerStripField = directory.findField(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP); 445 /* 446 * Default value of rowsperstrip is assumed to be infinity 447 * http://www.awaresystems.be/imaging/tiff/tifftags/rowsperstrip.html 448 */ 449 int rowsPerStrip = Integer.MAX_VALUE; 450 451 if (null != rowsPerStripField) { 452 rowsPerStrip = rowsPerStripField.getIntValue(); 453 } else { 454 final TiffField imageHeight = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH); 455 /** 456 * if rows per strip not present then rowsPerStrip is equal to 457 * imageLength or an infinity value; 458 */ 459 if (imageHeight != null) { 460 rowsPerStrip = imageHeight.getIntValue(); 461 } 462 463 } 464 465 return new TiffImageData.Strips(data, rowsPerStrip); 466 } else { 467 final TiffField tileWidthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_WIDTH); 468 if (null == tileWidthField) { 469 throw new ImageReadException("Can't find tile width field."); 470 } 471 final int tileWidth = tileWidthField.getIntValue(); 472 473 final TiffField tileLengthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_LENGTH); 474 if (null == tileLengthField) { 475 throw new ImageReadException("Can't find tile length field."); 476 } 477 final int tileLength = tileLengthField.getIntValue(); 478 479 return new TiffImageData.Tiles(data, tileWidth, tileLength); 480 } 481 } 482 483 private JpegImageData getJpegRawImageData(final ByteSource byteSource, 484 final TiffDirectory directory) throws ImageReadException, IOException { 485 final ImageDataElement element = directory.getJpegRawImageDataElement(); 486 final long offset = element.offset; 487 int length = element.length; 488 // In case the length is not correct, adjust it and check if the last read byte actually is the end of the image 489 if (offset + length > byteSource.getLength()) { 490 length = (int) (byteSource.getLength() - offset); 491 } 492 final byte[] data = byteSource.getBlock(offset, length); 493 // check if the last read byte is actually the end of the image data 494 if (strict && 495 (length < 2 || 496 (((data[data.length - 2] & 0xff) << 8) | (data[data.length - 1] & 0xff)) != JpegConstants.EOI_MARKER)) { 497 throw new ImageReadException("JPEG EOI marker could not be found at expected location"); 498 } 499 return new JpegImageData(offset, length, data); 500 } 501 502}