1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.imaging.formats.tiff;
18
19 import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
20 import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
21 import static org.apache.commons.imaging.common.BinaryFunctions.read8Bytes;
22 import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
23 import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
24 import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
25 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_ENTRY_MAX_VALUE_LENGTH;
26 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_ENTRY_MAX_VALUE_LENGTH_BIG;
27 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_VERSION_BIG;
28 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_VERSION_STANDARD;
29
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.nio.ByteOrder;
33 import java.util.ArrayList;
34 import java.util.List;
35
36 import org.apache.commons.imaging.FormatCompliance;
37 import org.apache.commons.imaging.ImagingException;
38 import org.apache.commons.imaging.bytesource.ByteSource;
39 import org.apache.commons.imaging.common.BinaryFileParser;
40 import org.apache.commons.imaging.common.ByteConversions;
41 import org.apache.commons.imaging.formats.jpeg.JpegConstants;
42 import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement;
43 import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
44 import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
45 import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
46 import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
47 import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDirectory;
48
49 public class TiffReader extends BinaryFileParser {
50
51 private static class Collector implements Listener {
52
53 private TiffHeader tiffHeader;
54 private final List<TiffDirectory> directories = new ArrayList<>();
55 private final List<TiffField> fields = new ArrayList<>();
56 private final boolean readThumbnails;
57
58 Collector() {
59 this(new TiffImagingParameters());
60 }
61
62 Collector(final TiffImagingParameters params) {
63 this.readThumbnails = params.isReadThumbnails();
64 }
65
66 @Override
67 public boolean addDirectory(final TiffDirectory directory) {
68 directories.add(directory);
69 return true;
70 }
71
72 @Override
73 public boolean addField(final TiffField field) {
74 fields.add(field);
75 return true;
76 }
77
78 public TiffContents getContents() {
79 return new TiffContents(tiffHeader, directories, fields);
80 }
81
82 @Override
83 public boolean readImageData() {
84 return readThumbnails;
85 }
86
87 @Override
88 public boolean readOffsetDirectories() {
89 return true;
90 }
91
92 @Override
93 public boolean setTiffHeader(final TiffHeader tiffHeader) {
94 this.tiffHeader = tiffHeader;
95 return true;
96 }
97 }
98
99 private static final class FirstDirectoryCollector extends Collector {
100 private final boolean readImageData;
101
102 FirstDirectoryCollector(final boolean readImageData) {
103 this.readImageData = readImageData;
104 }
105
106 @Override
107 public boolean addDirectory(final TiffDirectory directory) {
108 super.addDirectory(directory);
109 return false;
110 }
111
112 @Override
113 public boolean readImageData() {
114 return readImageData;
115 }
116 }
117
118 public interface Listener {
119 boolean addDirectory(TiffDirectory directory);
120
121 boolean addField(TiffField field);
122
123 boolean readImageData();
124
125 boolean readOffsetDirectories();
126
127 boolean setTiffHeader(TiffHeader tiffHeader);
128 }
129
130 private final boolean strict;
131 private boolean bigTiff;
132 private boolean standardTiff;
133 private int entryMaxValueLength;
134
135 public TiffReader(final boolean strict) {
136 this.strict = strict;
137 }
138
139 private JpegImageData getJpegRawImageData(final ByteSource byteSource, final TiffDirectory directory) throws ImagingException, IOException {
140 final ImageDataElement element = directory.getJpegRawImageDataElement();
141 final long offset = element.offset;
142 int length = element.length;
143
144 if (offset + length > byteSource.size()) {
145 length = (int) (byteSource.size() - offset);
146 }
147 final byte[] data = byteSource.getByteArray(offset, length);
148
149 if (strict && (length < 2 || ((data[data.length - 2] & 0xff) << 8 | data[data.length - 1] & 0xff) != JpegConstants.EOI_MARKER)) {
150 throw new ImagingException("JPEG EOI marker could not be found at expected location");
151 }
152 return new JpegImageData(offset, length, data);
153 }
154
155 private ByteOrder getTiffByteOrder(final int byteOrderByte) throws ImagingException {
156 if (byteOrderByte == 'I') {
157 return ByteOrder.LITTLE_ENDIAN;
158 }
159 if (byteOrderByte == 'M') {
160 return ByteOrder.BIG_ENDIAN;
161 }
162 throw new ImagingException("Invalid TIFF byte order " + (0xff & byteOrderByte));
163 }
164
165 private AbstractTiffImageData getTiffRawImageData(final ByteSource byteSource, final TiffDirectory directory) throws ImagingException, IOException {
166
167 final List<ImageDataElement> elements = directory.getTiffRawImageDataElements();
168 final AbstractTiffImageData.Data[] data = new AbstractTiffImageData.Data[elements.size()];
169
170 for (int i = 0; i < elements.size(); i++) {
171 final TiffDirectory.ImageDataElement element = elements.get(i);
172 final byte[] bytes = byteSource.getByteArray(element.offset, element.length);
173 data[i] = new AbstractTiffImageData.Data(element.offset, element.length, bytes);
174 }
175
176 if (directory.imageDataInStrips()) {
177 final TiffField rowsPerStripField = directory.findField(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP);
178
179
180
181
182 int rowsPerStrip = Integer.MAX_VALUE;
183
184 if (null != rowsPerStripField) {
185 rowsPerStrip = rowsPerStripField.getIntValue();
186 } else {
187 final TiffField imageHeight = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);
188
189
190
191
192 if (imageHeight != null) {
193 rowsPerStrip = imageHeight.getIntValue();
194 }
195
196 }
197
198 return new AbstractTiffImageData.Strips(data, rowsPerStrip);
199 }
200 final TiffField tileWidthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_WIDTH);
201 if (null == tileWidthField) {
202 throw new ImagingException("Can't find tile width field.");
203 }
204 final int tileWidth = tileWidthField.getIntValue();
205
206 final TiffField tileLengthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_LENGTH);
207 if (null == tileLengthField) {
208 throw new ImagingException("Can't find tile length field.");
209 }
210 final int tileLength = tileLengthField.getIntValue();
211
212 return new AbstractTiffImageData.Tiles(data, tileWidth, tileLength);
213 }
214
215 public void read(final ByteSource byteSource, final FormatCompliance formatCompliance, final Listener listener) throws ImagingException, IOException {
216 readDirectories(byteSource, formatCompliance, listener);
217 }
218
219 public TiffContents readContents(final ByteSource byteSource, final TiffImagingParameters params, final FormatCompliance formatCompliance)
220 throws ImagingException, IOException {
221
222 final Collector collector = new Collector(params);
223 read(byteSource, formatCompliance, collector);
224 return collector.getContents();
225 }
226
227 public TiffContents readDirectories(final ByteSource byteSource, final boolean readImageData, final FormatCompliance formatCompliance)
228 throws ImagingException, IOException {
229 final TiffImagingParameters params = new TiffImagingParameters();
230 params.setReadThumbnails(readImageData);
231 final Collector collector = new Collector(params);
232 readDirectories(byteSource, formatCompliance, collector);
233 final TiffContents contents = collector.getContents();
234 if (contents.directories.isEmpty()) {
235 throw new ImagingException("Image did not contain any directories.");
236 }
237 return contents;
238 }
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260 private void readDirectories(final ByteSource byteSource, final FormatCompliance formatCompliance, final Listener listener)
261 throws ImagingException, IOException {
262 final TiffHeader tiffHeader = readTiffHeader(byteSource);
263 if (!listener.setTiffHeader(tiffHeader)) {
264 return;
265 }
266
267 final long offset = tiffHeader.offsetToFirstIFD;
268 final int dirType = TiffDirectoryConstants.DIRECTORY_TYPE_ROOT;
269
270 final List<Number> visited = new ArrayList<>();
271 readDirectory(byteSource, offset, dirType, formatCompliance, listener, visited);
272 }
273
274 private boolean readDirectory(final ByteSource byteSource, final long directoryOffset, final int dirType, final FormatCompliance formatCompliance,
275 final Listener listener, final boolean ignoreNextDirectory, final List<Number> visited) throws ImagingException, IOException {
276
277 if (visited.contains(directoryOffset)) {
278 return false;
279 }
280 visited.add(directoryOffset);
281
282 try (InputStream is = byteSource.getInputStream()) {
283 if (directoryOffset >= byteSource.size()) {
284 return true;
285 }
286
287 skipBytes(is, directoryOffset);
288
289 final List<TiffField> fields = new ArrayList<>();
290
291 long entryCount;
292 try {
293 if (standardTiff) {
294 entryCount = read2Bytes("DirectoryEntryCount", is, "Not a Valid TIFF File", getByteOrder());
295 } else {
296 entryCount = read8Bytes("DirectoryEntryCount", is, "Not a Valid TIFF File", getByteOrder());
297 }
298 } catch (final IOException e) {
299 if (strict) {
300 throw e;
301 }
302 return true;
303 }
304
305 for (int i = 0; i < entryCount; i++) {
306 final int tag = read2Bytes("Tag", is, "Not a Valid TIFF File", getByteOrder());
307 final int type = read2Bytes("Type", is, "Not a Valid TIFF File", getByteOrder());
308 final long count;
309 final byte[] offsetBytes;
310 final long offset;
311 if (standardTiff) {
312 count = 0xFFFFffffL & read4Bytes("Count", is, "Not a Valid TIFF File", getByteOrder());
313 offsetBytes = readBytes("Offset", is, 4, "Not a Valid TIFF File");
314 offset = 0xFFFFffffL & ByteConversions.toInt(offsetBytes, getByteOrder());
315 } else {
316 count = read8Bytes("Count", is, "Not a Valid TIFF File", getByteOrder());
317 offsetBytes = readBytes("Offset", is, 8, "Not a Valid TIFF File");
318 offset = ByteConversions.toLong(offsetBytes, getByteOrder());
319 }
320
321 if (tag == 0) {
322
323
324
325
326 continue;
327 }
328
329 final AbstractFieldType abstractFieldType;
330 try {
331 abstractFieldType = AbstractFieldType.getFieldType(type);
332 } catch (final ImagingException imageReadEx) {
333
334
335
336 continue;
337 }
338 final long valueLength = count * abstractFieldType.getSize();
339 final byte[] value;
340 if (valueLength > entryMaxValueLength) {
341 if (offset < 0 || offset + valueLength > byteSource.size()) {
342 if (strict) {
343 throw new IOException("Attempt to read byte range starting from " + offset + " " + "of length " + valueLength + " "
344 + "which is outside the file's size of " + byteSource.size());
345 }
346
347 continue;
348 }
349 value = byteSource.getByteArray(offset, (int) valueLength);
350 } else {
351 value = offsetBytes;
352 }
353
354 final TiffField field = new TiffField(tag, dirType, abstractFieldType, count, offset, value, getByteOrder(), i);
355
356 fields.add(field);
357
358 if (!listener.addField(field)) {
359 return true;
360 }
361 }
362
363 final long nextDirectoryOffset = 0xFFFFffffL & read4Bytes("nextDirectoryOffset", is, "Not a Valid TIFF File", getByteOrder());
364
365 final TiffDirectory directory = new TiffDirectory(dirType, fields, directoryOffset, nextDirectoryOffset, getByteOrder());
366
367 if (listener.readImageData()) {
368 if (directory.hasTiffImageData()) {
369 final AbstractTiffImageData rawImageData = getTiffRawImageData(byteSource, directory);
370 directory.setTiffImageData(rawImageData);
371 }
372 if (directory.hasJpegImageData()) {
373 final JpegImageData rawJpegImageData = getJpegRawImageData(byteSource, directory);
374 directory.setJpegImageData(rawJpegImageData);
375 }
376 }
377
378 if (!listener.addDirectory(directory)) {
379 return true;
380 }
381
382 if (listener.readOffsetDirectories()) {
383 final TagInfoDirectory[] offsetFields = { ExifTagConstants.EXIF_TAG_EXIF_OFFSET, ExifTagConstants.EXIF_TAG_GPSINFO,
384 ExifTagConstants.EXIF_TAG_INTEROP_OFFSET };
385 final int[] directoryTypes = { TiffDirectoryConstants.DIRECTORY_TYPE_EXIF, TiffDirectoryConstants.DIRECTORY_TYPE_GPS,
386 TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY };
387 for (int i = 0; i < offsetFields.length; i++) {
388 final TagInfoDirectory offsetField = offsetFields[i];
389 final TiffField field = directory.findField(offsetField);
390 if (field != null) {
391 long subDirectoryOffset;
392 int subDirectoryType;
393 boolean subDirectoryRead = false;
394 try {
395 subDirectoryOffset = directory.getFieldValue(offsetField);
396 subDirectoryType = directoryTypes[i];
397 subDirectoryRead = readDirectory(byteSource, subDirectoryOffset, subDirectoryType, formatCompliance, listener, true, visited);
398
399 } catch (final ImagingException imageReadException) {
400 if (strict) {
401 throw imageReadException;
402 }
403 }
404 if (!subDirectoryRead) {
405 fields.remove(field);
406 }
407 }
408 }
409 }
410
411 if (!ignoreNextDirectory && directory.getNextDirectoryOffset() > 0) {
412
413 readDirectory(byteSource, directory.getNextDirectoryOffset(), dirType + 1, formatCompliance, listener, visited);
414 }
415
416 return true;
417 }
418 }
419
420 private boolean readDirectory(final ByteSource byteSource, final long offset, final int dirType, final FormatCompliance formatCompliance,
421 final Listener listener, final List<Number> visited) throws ImagingException, IOException {
422 final boolean ignoreNextDirectory = false;
423 return readDirectory(byteSource, offset, dirType, formatCompliance, listener, ignoreNextDirectory, visited);
424 }
425
426 public TiffContents readFirstDirectory(final ByteSource byteSource, final boolean readImageData, final FormatCompliance formatCompliance)
427 throws ImagingException, IOException {
428 final Collector collector = new FirstDirectoryCollector(readImageData);
429 read(byteSource, formatCompliance, collector);
430 final TiffContents contents = collector.getContents();
431 if (contents.directories.isEmpty()) {
432 throw new ImagingException("Image did not contain any directories.");
433 }
434 return contents;
435 }
436
437 private TiffHeader readTiffHeader(final ByteSource byteSource) throws ImagingException, IOException {
438 try (InputStream is = byteSource.getInputStream()) {
439 return readTiffHeader(is);
440 }
441 }
442
443 private TiffHeader readTiffHeader(final InputStream is) throws ImagingException, IOException {
444 final int byteOrder1 = readByte("BYTE_ORDER_1", is, "Not a Valid TIFF File");
445 final int byteOrder2 = readByte("BYTE_ORDER_2", is, "Not a Valid TIFF File");
446 if (byteOrder1 != byteOrder2) {
447 throw new ImagingException("Byte Order bytes don't match (" + byteOrder1 + ", " + byteOrder2 + ").");
448 }
449
450 final ByteOrder byteOrder = getTiffByteOrder(byteOrder1);
451 setByteOrder(byteOrder);
452
453
454
455
456
457
458 final long offsetToFirstIFD;
459 final int tiffVersion = read2Bytes("tiffVersion", is, "Not a Valid TIFF File", getByteOrder());
460 if (tiffVersion == TIFF_VERSION_STANDARD) {
461 bigTiff = false;
462 standardTiff = true;
463 entryMaxValueLength = TIFF_ENTRY_MAX_VALUE_LENGTH;
464 offsetToFirstIFD = 0xFFFFffffL & read4Bytes("offsetToFirstIFD", is, "Not a Valid TIFF File", getByteOrder());
465 } else if (tiffVersion == TIFF_VERSION_BIG) {
466 bigTiff = true;
467 standardTiff = false;
468 entryMaxValueLength = TIFF_ENTRY_MAX_VALUE_LENGTH_BIG;
469 final int byteSize = read2Bytes("bytesizeOfOffset", is, "Not a Valid TIFF File", getByteOrder());
470 final int expectedZero = read2Bytes("expectedZero", is, "Not a Valid TIFF File", getByteOrder());
471 if (byteSize != 8 || expectedZero != 0) {
472 throw new ImagingException("Misformed Big-TIFF header: " + tiffVersion);
473 }
474 offsetToFirstIFD = read8Bytes("offsetToFirstIFD", is, "Not a Valid TIFF File", getByteOrder());
475 } else {
476 throw new ImagingException("Unknown TIFF Version: " + tiffVersion);
477 }
478
479 skipBytes(is, offsetToFirstIFD - 8, "Not a Valid TIFF File: couldn't find IFDs");
480
481 return new TiffHeader(byteOrder, tiffVersion, offsetToFirstIFD, bigTiff);
482 }
483 }