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