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.formats.tiff.constants.TiffConstants.ENTRY_MAX_VALUE_LENGTH;
20 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.ENTRY_MAX_VALUE_LENGTH_BIG;
21 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.VERSION_BIG;
22 import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.VERSION_STANDARD;
23
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.nio.ByteOrder;
27 import java.util.ArrayList;
28 import java.util.List;
29
30 import org.apache.commons.imaging.FormatCompliance;
31 import org.apache.commons.imaging.ImagingException;
32 import org.apache.commons.imaging.bytesource.ByteSource;
33 import org.apache.commons.imaging.common.BinaryFileParser;
34 import org.apache.commons.imaging.common.BinaryFunctions;
35 import org.apache.commons.imaging.common.ByteConversions;
36 import org.apache.commons.imaging.formats.jpeg.JpegConstants;
37 import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement;
38 import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
39 import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
40 import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
41 import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
42 import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDirectory;
43
44 public class TiffReader extends BinaryFileParser {
45
46 private static class Collector implements Listener {
47
48 private TiffHeader tiffHeader;
49 private final List<TiffDirectory> directories = new ArrayList<>();
50 private final List<TiffField> fields = new ArrayList<>();
51 private final boolean readThumbnails;
52
53 Collector() {
54 this(new TiffImagingParameters());
55 }
56
57 Collector(final TiffImagingParameters params) {
58 this.readThumbnails = params.isReadThumbnails();
59 }
60
61 @Override
62 public boolean addDirectory(final TiffDirectory directory) {
63 directories.add(directory);
64 return true;
65 }
66
67 @Override
68 public boolean addField(final TiffField field) {
69 fields.add(field);
70 return true;
71 }
72
73 public TiffContents getContents() {
74 return new TiffContents(tiffHeader, directories, fields);
75 }
76
77 @Override
78 public boolean readImageData() {
79 return readThumbnails;
80 }
81
82 @Override
83 public boolean readOffsetDirectories() {
84 return true;
85 }
86
87 @Override
88 public boolean setTiffHeader(final TiffHeader tiffHeader) {
89 this.tiffHeader = tiffHeader;
90 return true;
91 }
92 }
93
94 private static final class FirstDirectoryCollector extends Collector {
95 private final boolean readImageData;
96
97 FirstDirectoryCollector(final boolean readImageData) {
98 this.readImageData = readImageData;
99 }
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
139 if (offset + length > byteSource.size()) {
140 length = (int) (byteSource.size() - offset);
141 }
142 final byte[] data = byteSource.getByteArray(offset, length);
143
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;
153 }
154 if (byteOrderByte == 'M') {
155 return ByteOrder.BIG_ENDIAN;
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
175
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
185
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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
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
318
319
320
321 continue;
322 }
323
324 final AbstractFieldType abstractFieldType;
325 try {
326 abstractFieldType = AbstractFieldType.getFieldType(type);
327 } catch (final ImagingException imageReadEx) {
328
329
330
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
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
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
449
450
451
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 }