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