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