This is an automated email from the ASF dual-hosted git repository.
THausherr pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 0baf52f9bc TIKA-4861: Detect raw camera formats by content (#3099)
0baf52f9bc is described below
commit 0baf52f9bcaeb35ffe862eae0bd6d548e8cabdf5
Author: Dominik Schmidt <[email protected]>
AuthorDate: Sun Aug 30 12:58:09 2026 +0200
TIKA-4861: Detect raw camera formats by content (#3099)
* TIKA-4861 - detect raw camera formats by content: RawTiffDetector for the
TIFF-based ones, magic for RAF, RW2, MRW and ORF
* TIKA-4861 - review: mark reserve, bounded value reads, Samsung 32772,
NRW/BigTIFF/cycle tests
* TIKA-4861 - read the prefix on demand up to 1 MiB; sensor data without
PhotometricInterpretation counts as raw (Nikon Z 6, Samsung NX1)
* Update detector count in integration test
* Update TestDetectorLoading.java
---------
Co-authored-by: Tilman Hausherr <[email protected]>
---
CHANGES.txt | 9 +
.../org/apache/tika/mime/tika-mimetypes.xml | 19 +
.../tika/detect/TestContainerAwareDetector.java | 16 +
.../apache/tika/detect/TestDetectorLoading.java | 14 +-
.../java/org/apache/tika/mime/TestMimeTypes.java | 11 +
.../src/test/resources/test-documents/testTIFF.tif | Bin 0 -> 25584 bytes
.../apache/tika/detect/image/RawTiffDetector.java | 458 +++++++++++++++++++++
.../apache/tika/parser/image/RawTiffParser.java | 3 +-
.../tika/detect/image/RawTiffDetectorTest.java | 447 ++++++++++++++++++++
9 files changed, 970 insertions(+), 7 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 0f1671eea3..b8eb0cc611 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,14 @@
Release 4.1.0 - unreleased
+ * Raw camera formats are detected by content: RawTiffDetector tells
+ Nikon NEF/NRW, Pentax PEF/PTX, Sony ARW/SRF/SR2, Samsung SRW and Adobe
+ DNG from a plain TIFF by their image directory (DNGVersion, the vendor
+ Compression codes, or a CFA/LinearRaw image plus Make), and Fuji RAF,
+ Panasonic RW2, Minolta MRW and the remaining Olympus ORF byte orders
+ get magic entries. Streams without a file name used to be image/tiff.
+ image/x-raw-samsung (*.srw) is new and parsed by RawTiffParser
+ (TIKA-4861).
+
* DWGReadParser emits the drawing's THUMBNAILIMAGE as a THUMBNAIL embedded
document instead of INLINE (TIKA-4853).
diff --git
a/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
b/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
index 7750d5ae90..62a18b7840 100644
--- a/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
+++ b/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
@@ -7189,6 +7189,9 @@
<mime-type type="image/x-raw-fuji">
<_comment>Fuji raw image</_comment>
+ <magic priority="50">
+ <match value="FUJIFILMCCD-RAW" type="string" offset="0"/>
+ </magic>
<glob pattern="*.raf"/>
</mime-type>
@@ -7243,6 +7246,9 @@
<mime-type type="image/x-raw-minolta">
<_comment>Minolta raw image</_comment>
+ <magic priority="50">
+ <match value="\x00MRM" type="string" offset="0"/>
+ </magic>
<glob pattern="*.mrw"/>
</mime-type>
@@ -7255,8 +7261,11 @@
<mime-type type="image/x-raw-olympus">
<_comment>Olympus raw image</_comment>
+ <!-- IIRO and IIRS little endian, MMOR big endian -->
<magic priority="50">
<match offset="0" type="string" value="\x49\x49\x52\x4F"/>
+ <match offset="0" type="string" value="\x49\x49\x52\x53"/>
+ <match offset="0" type="string" value="\x4D\x4D\x4F\x52"/>
</magic>
<glob pattern="*.orf"/>
</mime-type>
@@ -7268,6 +7277,12 @@
<glob pattern="*.pef"/>
</mime-type>
+ <mime-type type="image/x-raw-samsung">
+ <_comment>Samsung raw image</_comment>
+ <sub-class-of type="image/tiff" />
+ <glob pattern="*.srw"/>
+ </mime-type>
+
<mime-type type="image/x-raw-sony">
<_comment>Sony raw image</_comment>
<sub-class-of type="image/tiff" />
@@ -7298,6 +7313,10 @@
<mime-type type="image/x-raw-panasonic">
<_comment>Panasonic raw image</_comment>
+ <!-- RW2: a TIFF-like header with its own magic number 0x55 -->
+ <magic priority="50">
+ <match value="IIU\x00" type="string" offset="0"/>
+ </magic>
<glob pattern="*.raw"/>
<glob pattern="*.rw2"/>
</mime-type>
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java
index aa663e5bbf..7840d8da31 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java
@@ -123,6 +123,22 @@ public class TestContainerAwareDetector extends
MultiThreadedTikaTest {
}
}
+ /**
+ * TIFF-based raw camera formats, told apart by RawTiffDetector without a
+ * file name (TIKA-4861); a plain TIFF and a CR2 (fixed signature) are
+ * untouched.
+ */
+ @Test
+ public void testRawCameraFormatsByData() throws Exception {
+ assertTypeByData("testNEF.nef", "image/x-raw-nikon");
+ assertTypeByData("testARW.arw", "image/x-raw-sony");
+ assertTypeByData("testPEF.pef", "image/x-raw-pentax");
+ assertTypeByData("testDNG.dng", "image/x-raw-adobe");
+ assertTypeByData("testDNG_bigtiff.dng", "image/x-raw-adobe");
+ assertTypeByData("testCR2.cr2", "image/x-canon-cr2");
+ assertTypeByData("testTIFF.tif", "image/tiff");
+ }
+
@Test
public void testDetectOLE2() throws Exception {
/* // Microsoft office types known by POI
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestDetectorLoading.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestDetectorLoading.java
index 517d77b71f..9b147aea6b 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestDetectorLoading.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestDetectorLoading.java
@@ -32,17 +32,19 @@ public class TestDetectorLoading {
//integration test - detectors should be sorted alphabetically by
class name
Detector detector = TikaLoader.loadDefault().loadDetectors();
List<Detector> detectors = ((CompositeDetector)
detector).getDetectors();
- assertEquals(7, detectors.size());
+ assertEquals(8, detectors.size());
// Sorted alphabetically by full class name (all are org.apache.tika.*)
assertEquals("org.apache.tika.detect.apple.BPListDetector",
detectors.get(0).getClass().getName());
assertEquals("org.apache.tika.detect.gzip.GZipSpecializationDetector",
detectors.get(1).getClass().getName());
- assertEquals("org.apache.tika.detect.microsoft.POIFSContainerDetector",
+ assertEquals("org.apache.tika.detect.image.RawTiffDetector",
detectors.get(2).getClass().getName());
- assertEquals("org.apache.tika.detect.mkv.MatroskaDetector",
detectors.get(3).getClass().getName());
- assertEquals("org.apache.tika.detect.ogg.OggDetector",
detectors.get(4).getClass().getName());
- assertEquals("org.apache.tika.detect.ole.MiscOLEDetector",
detectors.get(5).getClass().getName());
+ assertEquals("org.apache.tika.detect.microsoft.POIFSContainerDetector",
+ detectors.get(3).getClass().getName());
+ assertEquals("org.apache.tika.detect.mkv.MatroskaDetector",
detectors.get(4).getClass().getName());
+ assertEquals("org.apache.tika.detect.ogg.OggDetector",
detectors.get(5).getClass().getName());
+ assertEquals("org.apache.tika.detect.ole.MiscOLEDetector",
detectors.get(6).getClass().getName());
assertEquals("org.apache.tika.detect.zip.DefaultZipContainerDetector",
- detectors.get(6).getClass().getName());
+ detectors.get(7).getClass().getName());
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/mime/TestMimeTypes.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/mime/TestMimeTypes.java
index de2222afb6..74c2035772 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/mime/TestMimeTypes.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/mime/TestMimeTypes.java
@@ -16,6 +16,7 @@
*/
package org.apache.tika.mime;
+import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_16BE;
import static java.nio.charset.StandardCharsets.UTF_16LE;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -894,7 +895,17 @@ public class TestMimeTypes {
assertTypeByName("image/x-raw-minolta", "x.mrw");
assertTypeByName("image/x-raw-nikon", "x.nef");
assertTypeByName("image/x-raw-nikon", "x.nrw");
+ assertTypeByName("image/x-raw-samsung", "x.srw");
+ //MimeTypes alone sees a TIFF; RawTiffDetector tells the raw formats
apart,
+ //see TestContainerAwareDetector
assertTypeByData("image/tiff", "testNEF.nef");
+ //formats with a fixed signature (TIKA-4861)
+ assertTypeByData("image/x-raw-fuji", "FUJIFILMCCD-RAW
0201FF393103".getBytes(US_ASCII));
+ assertTypeByData("image/x-raw-panasonic", new byte[]{'I', 'I', 'U', 0,
0x18, 0, 0, 0});
+ assertTypeByData("image/x-raw-minolta", new byte[]{0, 'M', 'R', 'M',
0, 0, 0, 0});
+ assertTypeByData("image/x-raw-olympus", new byte[]{'I', 'I', 'R', 'O',
8, 0, 0, 0});
+ assertTypeByData("image/x-raw-olympus", new byte[]{'I', 'I', 'R', 'S',
8, 0, 0, 0});
+ assertTypeByData("image/x-raw-olympus", new byte[]{'M', 'M', 'O', 'R',
0, 0, 0, 8});
assertTypeByNameAndData("image/x-raw-nikon", "testNEF.nef");
assertTypeByData("image/tiff", "testARW.arw");
assertTypeByNameAndData("image/x-raw-sony", "testARW.arw");
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testTIFF.tif
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testTIFF.tif
new file mode 100644
index 0000000000..8f6c7abba4
Binary files /dev/null and
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testTIFF.tif
differ
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/detect/image/RawTiffDetector.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/detect/image/RawTiffDetector.java
new file mode 100644
index 0000000000..8ba5a6379a
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/detect/image/RawTiffDetector.java
@@ -0,0 +1,458 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.detect.image;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.detect.Detector;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Tells the TIFF-based raw camera formats (Nikon NEF/NRW, Pentax PEF/PTX,
+ * Sony ARW/SRF/SR2, Samsung SRW, Adobe DNG) from a plain TIFF by content,
+ * so they are recognized without a file name (TIKA-4861). They share the
+ * TIFF magic; what differs is the image directory:
+ * <ol>
+ * <li>a {@code DNGVersion} tag (0xC612) makes a DNG, whoever wrote it;</li>
+ * <li>a vendor-specific {@code Compression} value (0x0103) in any IFD
+ * names the format: 34713 Nikon, 32767 Sony, 65535 Pentax,
+ * 32770 and 32772 Samsung (this is how ExifTool identifies them);</li>
+ * <li>otherwise sensor data in any IFD marks a raw, and the {@code Make}
+ * tag (0x010F) picks the vendor: an image with
+ * {@code PhotometricInterpretation} 32803 (CFA) or 34892 (LinearRaw),
+ * or a full-resolution image (NewSubfileType 0) of one 9 to 16 bit
+ * sample without any PhotometricInterpretation, which a regular TIFF
+ * must have (Samsung NX1). This catches the uncompressed variants. A
+ * plain TIFF from the same camera has RGB data and stays
+ * {@code image/tiff}.</li>
+ * </ol>
+ * The detector reads the stream into memory as far as the directories
+ * require, up to {@link #MAX_PREFIX_LENGTH}, and walks the IFD chain and the
+ * SubIFDs (0x014A) inside that prefix; raw files keep their directories
+ * ahead of the sensor data, though behind the embedded previews (some
+ * hundred KB). A plain TIFF with its directory at the start costs a few KB.
+ * Anything it cannot decide is left to the other detectors as
+ * {@code application/octet-stream}. Canon CR2 has a fixed signature and is
+ * matched by the mime magic instead.
+ */
+@TikaComponent
+public class RawTiffDetector implements Detector {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * How far into the stream the directories may lie. The IFD tables of a
+ * raw file follow its embedded previews: 265 KB for a Nikon Z 6, 458 KB
+ * for a Samsung NX1.
+ */
+ static final int MAX_PREFIX_LENGTH = 1024 * 1024;
+
+ /**
+ * The stream is read in chunks of this size, as the directories require.
+ */
+ private static final int CHUNK_LENGTH = 64 * 1024;
+
+ private static final int TAG_NEW_SUBFILE_TYPE = 0x00FE;
+ private static final int TAG_BITS_PER_SAMPLE = 0x0102;
+ private static final int TAG_MAKE = 0x010F;
+ private static final int TAG_COMPRESSION = 0x0103;
+ private static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106;
+ private static final int TAG_SUB_IFDS = 0x014A;
+ private static final int TAG_DNG_VERSION = 0xC612;
+
+ private static final int COMPRESSION_NIKON = 34713;
+ private static final int COMPRESSION_SONY = 32767;
+ private static final int COMPRESSION_PENTAX = 65535;
+ private static final int COMPRESSION_SAMSUNG = 32770;
+ private static final int COMPRESSION_SAMSUNG_2 = 32772;
+
+ private static final int PHOTOMETRIC_CFA = 32803;
+ private static final int PHOTOMETRIC_LINEAR_RAW = 34892;
+
+ private static final int TYPE_ASCII = 2;
+ private static final int TYPE_SHORT = 3;
+ private static final int TYPE_LONG = 4;
+ private static final int TYPE_IFD = 13;
+ private static final int TYPE_LONG8 = 16;
+ private static final int TYPE_IFD8 = 18;
+
+ private static final int MAX_IFDS = 32;
+ private static final int MAX_ENTRIES_PER_IFD = 1024;
+ private static final int MAX_MAKE_LENGTH = 256;
+
+ static final MediaType NIKON = MediaType.image("x-raw-nikon");
+ static final MediaType PENTAX = MediaType.image("x-raw-pentax");
+ static final MediaType SONY = MediaType.image("x-raw-sony");
+ static final MediaType SAMSUNG = MediaType.image("x-raw-samsung");
+ static final MediaType ADOBE = MediaType.image("x-raw-adobe");
+
+ @Override
+ public MediaType detect(TikaInputStream tis, Metadata metadata,
ParseContext parseContext)
+ throws IOException {
+ if (tis == null) {
+ return MediaType.OCTET_STREAM;
+ }
+ //one more than is read: a BufferedInputStream drops the mark once the
+ //read limit is reached, and reset() in finally must always succeed
+ tis.mark(MAX_PREFIX_LENGTH + 1);
+ try {
+ byte[] header = new byte[16];
+ if (tis.readNBytes(header, 0, 16) < 16 || !isTiff(header)) {
+ return MediaType.OCTET_STREAM;
+ }
+ return detect(new Prefix(header, 16, tis, MAX_PREFIX_LENGTH));
+ } finally {
+ tis.reset();
+ }
+ }
+
+ /**
+ * The start of the file, read further from the stream as the
+ * directories require, up to a limit.
+ */
+ static final class Prefix {
+ private byte[] buf;
+ private int length;
+ private final InputStream in;
+ private final int limit;
+
+ /**
+ * @param buf the bytes read so far
+ * @param length how many of them are valid
+ * @param in where to read more from, or null if this is all
+ * @param limit how far to read at most
+ */
+ Prefix(byte[] buf, int length, InputStream in, int limit) {
+ this.buf = buf;
+ this.length = length;
+ this.in = in;
+ this.limit = limit;
+ }
+
+ /**
+ * Makes the bytes up to {@code end} (exclusive) available if the
+ * file has them and they are within the limit.
+ *
+ * @return whether {@code buf[0..end)} is valid now
+ */
+ boolean ensure(long end) throws IOException {
+ if (end <= length) {
+ return true;
+ }
+ if (in == null || end > limit) {
+ return false;
+ }
+ int wanted = (int) Math.min(limit, ((end + CHUNK_LENGTH - 1) /
CHUNK_LENGTH) * CHUNK_LENGTH);
+ if (buf.length < wanted) {
+ byte[] bigger = new byte[wanted];
+ System.arraycopy(buf, 0, bigger, 0, length);
+ buf = bigger;
+ }
+ while (length < wanted) {
+ int n = in.read(buf, length, wanted - length);
+ if (n < 0) {
+ break;
+ }
+ length += n;
+ }
+ return end <= length;
+ }
+ }
+
+ private static boolean isTiff(byte[] h) {
+ boolean bigEndian = h[0] == 'M' && h[1] == 'M';
+ boolean littleEndian = h[0] == 'I' && h[1] == 'I';
+ if (!bigEndian && !littleEndian) {
+ return false;
+ }
+ int magic = getUInt16(h, 2, bigEndian);
+ return magic == 42 || magic == 43;
+ }
+
+ /**
+ * @param buf the start of the file
+ * @param length how many bytes of it are valid
+ * @return the raw type, or {@link MediaType#OCTET_STREAM} if the prefix
+ * does not show one
+ */
+ static MediaType detect(byte[] buf, int length) {
+ try {
+ return detect(new Prefix(buf, length, null, length));
+ } catch (IOException e) {
+ //no stream, nothing to read
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static MediaType detect(Prefix prefix) throws IOException {
+ if (!prefix.ensure(8) || !isTiff(prefix.buf)) {
+ return MediaType.OCTET_STREAM;
+ }
+ byte[] buf = prefix.buf;
+ int length = prefix.length;
+ boolean bigEndian = buf[0] == 'M';
+ boolean bigTiff = getUInt16(buf, 2, bigEndian) == 43;
+ int countSize = bigTiff ? 8 : 2;
+ int entrySize = bigTiff ? 20 : 12;
+ int offsetSize = bigTiff ? 8 : 4;
+ int inlineSize = bigTiff ? 8 : 4;
+ long firstIfd;
+ if (bigTiff) {
+ if (!prefix.ensure(16) || getUInt16(buf, 4, bigEndian) != 8) {
+ return MediaType.OCTET_STREAM;
+ }
+ firstIfd = getUInt64(buf, 8, bigEndian);
+ } else {
+ firstIfd = getUInt32(buf, 4, bigEndian);
+ }
+
+ boolean dng = false;
+ MediaType byCompression = null;
+ boolean rawImage = false;
+ String make = null;
+
+ Set<Long> visited = new HashSet<>();
+ Deque<Long> toVisit = new ArrayDeque<>();
+ toVisit.add(firstIfd);
+ while (!toVisit.isEmpty() && visited.size() < MAX_IFDS) {
+ long ifdOffset = toVisit.poll();
+ if (ifdOffset <= 0 || !visited.add(ifdOffset) ||
!prefix.ensure(ifdOffset + countSize)) {
+ continue;
+ }
+ buf = prefix.buf;
+ int ifd = (int) ifdOffset;
+ long numEntries = bigTiff ? getUInt64(buf, ifd, bigEndian) :
getUInt16(buf, ifd, bigEndian);
+ if (numEntries < 0 || numEntries > MAX_ENTRIES_PER_IFD
+ || !prefix.ensure(ifd + countSize + numEntries * entrySize
+ offsetSize)) {
+ continue;
+ }
+ buf = prefix.buf;
+ length = prefix.length;
+ int entries = ifd + countSize;
+ long subfileType = -1;
+ boolean photometricPresent = false;
+ long[] bitsPerSample = new long[0];
+ for (int i = 0; i < numEntries; i++) {
+ int e = entries + i * entrySize;
+ int tag = getUInt16(buf, e, bigEndian);
+ int type = getUInt16(buf, e + 2, bigEndian);
+ long count = bigTiff ? getUInt64(buf, e + 4, bigEndian) :
getUInt32(buf, e + 4, bigEndian);
+ int valueField = e + 4 + inlineSize;
+ switch (tag) {
+ case TAG_DNG_VERSION:
+ dng = true;
+ break;
+ case TAG_SUB_IFDS:
+ //only MAX_IFDS can ever be visited: do not read more
pointers
+ for (long sub : longValues(buf, length, valueField,
bigEndian, bigTiff, type,
+ Math.min(count, MAX_IFDS))) {
+ if (toVisit.size() < MAX_IFDS) {
+ toVisit.add(sub);
+ }
+ }
+ break;
+ case TAG_COMPRESSION:
+ if (byCompression == null && count == 1) {
+ long[] v = longValues(buf, length, valueField,
bigEndian, bigTiff, type, count);
+ byCompression = v.length == 1 ?
vendorByCompression(v[0]) : null;
+ }
+ break;
+ case TAG_PHOTOMETRIC_INTERPRETATION:
+ photometricPresent = true;
+ if (count == 1) {
+ long[] v = longValues(buf, length, valueField,
bigEndian, bigTiff, type, count);
+ if (v.length == 1
+ && (v[0] == PHOTOMETRIC_CFA || v[0] ==
PHOTOMETRIC_LINEAR_RAW)) {
+ rawImage = true;
+ }
+ }
+ break;
+ case TAG_NEW_SUBFILE_TYPE:
+ if (count == 1) {
+ long[] v = longValues(buf, length, valueField,
bigEndian, bigTiff, type, count);
+ subfileType = v.length == 1 ? v[0] : -1;
+ }
+ break;
+ case TAG_BITS_PER_SAMPLE:
+ if (count <= 4) {
+ bitsPerSample = longValues(buf, length,
valueField, bigEndian, bigTiff, type, count);
+ }
+ break;
+ case TAG_MAKE:
+ if (make == null && type == TYPE_ASCII) {
+ make = asciiValue(buf, length, valueField,
bigEndian, bigTiff, count);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ //sensor data without a PhotometricInterpretation: a
full-resolution
+ //image of one deep sample, which no regular TIFF describes that
way
+ if (!photometricPresent && subfileType == 0 &&
bitsPerSample.length == 1
+ && bitsPerSample[0] >= 9 && bitsPerSample[0] <= 16) {
+ rawImage = true;
+ }
+ int follower = (int) (entries + numEntries * entrySize);
+ long next = bigTiff ? getUInt64(buf, follower, bigEndian) :
getUInt32(buf, follower, bigEndian);
+ if (toVisit.size() < MAX_IFDS) {
+ toVisit.add(next);
+ }
+ }
+
+ if (dng) {
+ return ADOBE;
+ }
+ if (byCompression != null) {
+ return byCompression;
+ }
+ if (rawImage && make != null) {
+ MediaType byMake = vendorByMake(make);
+ if (byMake != null) {
+ return byMake;
+ }
+ }
+ return MediaType.OCTET_STREAM;
+ }
+
+ private static MediaType vendorByCompression(long compression) {
+ if (compression == COMPRESSION_NIKON) {
+ return NIKON;
+ } else if (compression == COMPRESSION_SONY) {
+ return SONY;
+ } else if (compression == COMPRESSION_PENTAX) {
+ return PENTAX;
+ } else if (compression == COMPRESSION_SAMSUNG || compression ==
COMPRESSION_SAMSUNG_2) {
+ return SAMSUNG;
+ }
+ return null;
+ }
+
+ private static MediaType vendorByMake(String make) {
+ String m = make.trim().toUpperCase(Locale.ROOT);
+ if (m.startsWith("NIKON")) {
+ return NIKON;
+ } else if (m.startsWith("PENTAX") || m.startsWith("RICOH")) {
+ return PENTAX;
+ } else if (m.startsWith("SONY")) {
+ return SONY;
+ } else if (m.startsWith("SAMSUNG")) {
+ return SAMSUNG;
+ }
+ return null;
+ }
+
+ /**
+ * The values of a SHORT, LONG, IFD (and for BigTIFF LONG8, IFD8) entry,
+ * inline or at their offset; empty if the entry is another type or
+ * points outside the prefix.
+ */
+ private static long[] longValues(byte[] buf, int length, int valueField,
boolean bigEndian,
+ boolean bigTiff, int type, long count) {
+ int typeSize;
+ if (type == TYPE_SHORT) {
+ typeSize = 2;
+ } else if (type == TYPE_LONG || type == TYPE_IFD) {
+ typeSize = 4;
+ } else if (bigTiff && (type == TYPE_LONG8 || type == TYPE_IFD8)) {
+ typeSize = 8;
+ } else {
+ return new long[0];
+ }
+ if (count < 1 || count > MAX_ENTRIES_PER_IFD) {
+ return new long[0];
+ }
+ int n = (int) count;
+ long totalBytes = (long) typeSize * n;
+ int off;
+ if (totalBytes <= (bigTiff ? 8 : 4)) {
+ off = valueField;
+ } else {
+ long valueOffset = bigTiff
+ ? getUInt64(buf, valueField, bigEndian) : getUInt32(buf,
valueField, bigEndian);
+ if (valueOffset < 0 || valueOffset > length - totalBytes) {
+ return new long[0];
+ }
+ off = (int) valueOffset;
+ }
+ long[] values = new long[n];
+ for (int i = 0; i < n; i++) {
+ int p = off + i * typeSize;
+ if (typeSize == 2) {
+ values[i] = getUInt16(buf, p, bigEndian);
+ } else if (typeSize == 4) {
+ values[i] = getUInt32(buf, p, bigEndian);
+ } else {
+ values[i] = getUInt64(buf, p, bigEndian);
+ }
+ }
+ return values;
+ }
+
+ private static String asciiValue(byte[] buf, int length, int valueField,
boolean bigEndian,
+ boolean bigTiff, long count) {
+ if (count < 1 || count > MAX_MAKE_LENGTH) {
+ return null;
+ }
+ int n = (int) count;
+ int off;
+ if (n <= (bigTiff ? 8 : 4)) {
+ off = valueField;
+ } else {
+ long valueOffset = bigTiff
+ ? getUInt64(buf, valueField, bigEndian) : getUInt32(buf,
valueField, bigEndian);
+ if (valueOffset < 0 || valueOffset > length - n) {
+ return null;
+ }
+ off = (int) valueOffset;
+ }
+ int end = off;
+ while (end < off + n && buf[end] != 0) {
+ end++;
+ }
+ return new String(buf, off, end - off, StandardCharsets.US_ASCII);
+ }
+
+ private static int getUInt16(byte[] b, int off, boolean bigEndian) {
+ int a = b[off] & 0xFF;
+ int c = b[off + 1] & 0xFF;
+ return bigEndian ? (a << 8) | c : (c << 8) | a;
+ }
+
+ private static long getUInt32(byte[] b, int off, boolean bigEndian) {
+ long high = getUInt16(b, off, bigEndian);
+ long low = getUInt16(b, off + 2, bigEndian);
+ return bigEndian ? (high << 16) | low : (low << 16) | high;
+ }
+
+ private static long getUInt64(byte[] b, int off, boolean bigEndian) {
+ long high = getUInt32(b, off, bigEndian);
+ long low = getUInt32(b, off + 4, bigEndian);
+ return bigEndian ? (high << 32) | low : (low << 32) | high;
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
index f982113d56..774aa3a8a9 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
@@ -54,7 +54,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
/**
* Parser for TIFF-based camera raw images: Nikon NEF/NRW, Sony ARW/SRF/SR2,
- * Pentax PEF/PTX, Adobe DNG and Canon CR2.
+ * Pentax PEF/PTX, Samsung SRW, Adobe DNG and Canon CR2.
* <p>
* These formats are TIFF containers: metadata extraction is inherited from
* {@link TiffParser}. In addition, this parser extracts the camera-generated
@@ -89,6 +89,7 @@ public class RawTiffParser extends TiffParser {
MediaType.image("x-raw-nikon"),
MediaType.image("x-raw-sony"),
MediaType.image("x-raw-pentax"),
+ MediaType.image("x-raw-samsung"),
MediaType.image("x-raw-adobe"),
MediaType.image("x-canon-cr2"))));
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorTest.java
new file mode 100644
index 0000000000..16fa1472a1
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorTest.java
@@ -0,0 +1,447 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.detect.image;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import org.apache.tika.detect.DefaultDetector;
+import org.apache.tika.detect.Detector;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.mime.MimeTypes;
+import org.apache.tika.parser.ParseContext;
+
+public class RawTiffDetectorTest {
+
+ /**
+ * The default detector, with this module's detectors loaded through SPI.
+ */
+ private final Detector detector = new
DefaultDetector(MimeTypes.getDefaultMimeTypes());
+
+ /**
+ * testNEF_dup.nef is a synthetic file with nothing but JPEG previews in
+ * it (no Make, no vendor compression, no raw image): rightly a TIFF.
+ */
+ @ParameterizedTest
+ @CsvSource({
+ "testNEF.nef, image/x-raw-nikon",
+ "testNEF_dup.nef, image/tiff",
+ "testARW.arw, image/x-raw-sony",
+ "testPEF.pef, image/x-raw-pentax",
+ "testDNG.dng, image/x-raw-adobe",
+ "testDNG_bigtiff.dng, image/x-raw-adobe",
+ "testCR2.cr2, image/x-canon-cr2",
+ "testTIFF.tif, image/tiff",
+ "testJPEG.jpg, image/jpeg"})
+ public void testDetectionWithoutName(String file, String expected) throws
Exception {
+ try (InputStream is =
getClass().getResourceAsStream("/test-documents/" + file);
+ TikaInputStream tis = TikaInputStream.get(is)) {
+ assertEquals(expected, detector.detect(tis, new Metadata(), new
ParseContext()).toString());
+ }
+ }
+
+ /**
+ * The stream is left where it was: the parsers that follow read it from
+ * the start.
+ */
+ @Test
+ public void testStreamIsReset() throws Exception {
+ try (InputStream is =
getClass().getResourceAsStream("/test-documents/testNEF.nef");
+ TikaInputStream tis = TikaInputStream.get(is)) {
+ new RawTiffDetector().detect(tis, new Metadata(), new
ParseContext());
+ assertEquals(0, tis.getPosition());
+ assertEquals('M', tis.read());
+ }
+ }
+
+ /**
+ * A TIFF a Nikon camera wrote is not a NEF: RGB data, no vendor
+ * compression, no DNGVersion.
+ */
+ @Test
+ public void testCameraTiffStaysTiff() {
+ byte[] tiff = tiff("NIKON CORPORATION", 1, 2, false);
+ assertEquals(MediaType.OCTET_STREAM, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ /**
+ * An uncompressed raw: no vendor compression code, but a CFA image and
+ * the maker's name.
+ */
+ @Test
+ public void testUncompressedRawByMakeAndCfa() {
+ byte[] tiff = tiff("PENTAX Corporation", 1, 32803, false);
+ assertEquals(RawTiffDetector.PENTAX, RawTiffDetector.detect(tiff,
tiff.length));
+ tiff = tiff("Unknown Maker", 1, 32803, false);
+ assertEquals(MediaType.OCTET_STREAM, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ @Test
+ public void testVendorCompressionWins() {
+ byte[] tiff = tiff("NIKON CORPORATION", 34713, 2, false);
+ assertEquals(RawTiffDetector.NIKON, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ /**
+ * DNGVersion decides before anything else, also for a DNG a camera
+ * maker wrote with its own name in Make.
+ */
+ @Test
+ public void testDngVersionFirst() {
+ byte[] tiff = tiff("PENTAX", 65535, 32803, true);
+ assertEquals(RawTiffDetector.ADOBE, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ /**
+ * The NRW layout: a JPEG-compressed thumbnail in IFD0, the raw image with
+ * its CFA data in a SubIFD. The non-vendor Compression of IFD0 does not
+ * end the search.
+ */
+ @Test
+ public void testRawInSubIfd() {
+ byte[] tiff = new TiffBuilder(false)
+ .ifd(entry(0x0103, 3, 6), entry(0x010F, "NIKON CORPORATION"),
subIfds(1))
+ .ifd(entry(0x0103, 3, 1), entry(0x0106, 3, 32803))
+ .build();
+ assertEquals(RawTiffDetector.NIKON, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ /**
+ * BigTIFF: 8-byte counts, offsets and inline values, a LONG8 SubIFDs
+ * entry, and the vendor code inside the SubIFD.
+ */
+ @Test
+ public void testBigTiffWithLong8SubIfd() {
+ byte[] tiff = new TiffBuilder(true)
+ .ifd(entry(0x010F, "SONY"), subIfds(1))
+ .ifd(entry(0x0103, 3, 32767))
+ .build();
+ assertEquals(RawTiffDetector.SONY, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ @Test
+ public void testSamsungCompressionCodes() {
+ for (int code : new int[]{32770, 32772}) {
+ byte[] tiff = tiff("SAMSUNG", code, 2, false);
+ assertEquals(RawTiffDetector.SAMSUNG, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+ }
+
+ /**
+ * An IFD chain that points back at itself, and a SubIFDs array with many
+ * pointers to the same IFD: the walk ends.
+ */
+ @Test
+ public void testCyclesEnd() {
+ byte[] tiff = new TiffBuilder(false)
+ .ifd(entry(0x010F, "NIKON"), entry(0x0106, 3, 32803))
+ .nextPointsToSelf()
+ .build();
+ assertEquals(RawTiffDetector.NIKON, RawTiffDetector.detect(tiff,
tiff.length));
+
+ tiff = new TiffBuilder(false)
+ .ifd(entry(0x010F, "Unknown"), subIfds(64))
+ .ifd(entry(0x0103, 3, 1))
+ .build();
+ assertEquals(MediaType.OCTET_STREAM, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ /**
+ * The Samsung NX1 layout: the raw SubIFD has no PhotometricInterpretation
+ * and a compression code that is also PackBits, so only the shape of the
+ * image (full resolution, one 14 bit sample) and the Make say raw.
+ */
+ @Test
+ public void testDeepSingleSampleWithoutPhotometric() {
+ byte[] tiff = new TiffBuilder(false)
+ .ifd(entry(0x010F, "SAMSUNG"), subIfds(1))
+ .ifd(entry(0x00FE, 4, 0), entry(0x0102, 3, 14), entry(0x0103,
4, 32773))
+ .build();
+ assertEquals(RawTiffDetector.SAMSUNG, RawTiffDetector.detect(tiff,
tiff.length));
+ //the same image with RGB data is a TIFF
+ tiff = new TiffBuilder(false)
+ .ifd(entry(0x010F, "SAMSUNG"), subIfds(1))
+ .ifd(entry(0x00FE, 4, 0), entry(0x0102, 3, 8), entry(0x0106,
3, 2))
+ .build();
+ assertEquals(MediaType.OCTET_STREAM, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ /**
+ * Directories behind a few hundred KB of preview data are read on demand;
+ * beyond the limit they are not, and the file stays a TIFF.
+ */
+ @Test
+ public void testDirectoriesBehindPreviewData() throws Exception {
+ byte[] tiff = new TiffBuilder(false)
+ .ifd(entry(0x010F, "NIKON CORPORATION"), subIfds(1))
+ .gap(600 * 1024)
+ .ifd(entry(0x0103, 3, 34713))
+ .build();
+ try (TikaInputStream tis = TikaInputStream.get(tiff)) {
+ assertEquals(RawTiffDetector.NIKON,
+ new RawTiffDetector().detect(tis, new Metadata(), new
ParseContext()));
+ assertEquals(0, tis.getPosition());
+ }
+ tiff = new TiffBuilder(false)
+ .ifd(entry(0x010F, "NIKON CORPORATION"), subIfds(1))
+ .gap(RawTiffDetector.MAX_PREFIX_LENGTH + 1024)
+ .ifd(entry(0x0103, 3, 34713))
+ .build();
+ try (TikaInputStream tis = TikaInputStream.get(tiff)) {
+ assertEquals(MediaType.OCTET_STREAM,
+ new RawTiffDetector().detect(tis, new Metadata(), new
ParseContext()));
+ }
+ }
+
+ @Test
+ public void testTruncatedPrefixIsHarmless() {
+ byte[] tiff = tiff("NIKON CORPORATION", 34713, 32803, false);
+ for (int length = 0; length < tiff.length; length++) {
+ RawTiffDetector.detect(tiff, length);
+ }
+ }
+
+ /**
+ * A minimal little-endian TIFF: one IFD with Make, Compression,
+ * PhotometricInterpretation and, optionally, DNGVersion.
+ */
+ private static byte[] tiff(String make, int compression, int photometric,
boolean dngVersion) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] makeBytes = (make + "\0").getBytes(StandardCharsets.US_ASCII);
+ int entries = dngVersion ? 4 : 3;
+ int ifdOffset = 8;
+ int makeOffset = ifdOffset + 2 + entries * 12 + 4;
+ out.writeBytes(new byte[]{'I', 'I', 42, 0});
+ le32(out, ifdOffset);
+ le16(out, entries);
+ entry(out, 0x0103, 3, 1, compression);
+ entry(out, 0x0106, 3, 1, photometric);
+ entry(out, 0x010F, 2, makeBytes.length, makeOffset);
+ if (dngVersion) {
+ entry(out, 0xC612, 1, 4, 0x00000401);
+ }
+ le32(out, 0);
+ out.writeBytes(makeBytes);
+ return out.toByteArray();
+ }
+
+ private static void entry(ByteArrayOutputStream out, int tag, int type,
int count, int value) {
+ le16(out, tag);
+ le16(out, type);
+ le32(out, count);
+ if (type == 3 && count == 1) {
+ le16(out, value);
+ le16(out, 0);
+ } else {
+ le32(out, value);
+ }
+ }
+
+ private static Entry entry(int tag, int type, long value) {
+ return new Entry(tag, type, 1, null, value);
+ }
+
+ private static Entry entry(int tag, String ascii) {
+ byte[] bytes = (ascii + "\0").getBytes(StandardCharsets.US_ASCII);
+ return new Entry(tag, 2, bytes.length, bytes, 0);
+ }
+
+ /**
+ * A SubIFDs entry with {@code count} pointers, all to the IFD that
+ * follows the current one.
+ */
+ private static Entry subIfds(int count) {
+ return new Entry(0x014A, -1, count, null, 0);
+ }
+
+ private record Entry(int tag, int type, long count, byte[] data, long
value) {
+ }
+
+ /**
+ * Lays out IFDs one after the other, each followed by its out-of-line
+ * data; classic or BigTIFF, little endian.
+ */
+ private static final class TiffBuilder {
+ private final boolean bigTiff;
+ private final java.util.List<Entry[]> ifds = new
java.util.ArrayList<>();
+ private boolean nextPointsToSelf;
+ private final java.util.Map<Integer, Integer> gaps = new
java.util.HashMap<>();
+
+ /**
+ * Filler bytes between the last added IFD and the next one.
+ */
+ TiffBuilder gap(int bytes) {
+ gaps.put(ifds.size(), bytes);
+ return this;
+ }
+
+ TiffBuilder(boolean bigTiff) {
+ this.bigTiff = bigTiff;
+ }
+
+ TiffBuilder ifd(Entry... entries) {
+ ifds.add(entries);
+ return this;
+ }
+
+ TiffBuilder nextPointsToSelf() {
+ nextPointsToSelf = true;
+ return this;
+ }
+
+ byte[] build() {
+ int headerSize = bigTiff ? 16 : 8;
+ int countSize = bigTiff ? 8 : 2;
+ int entrySize = bigTiff ? 20 : 12;
+ int offsetSize = bigTiff ? 8 : 4;
+ int inline = bigTiff ? 8 : 4;
+ //first pass: where does each IFD start
+ long[] starts = new long[ifds.size()];
+ long pos = headerSize;
+ for (int i = 0; i < ifds.size(); i++) {
+ pos += gaps.getOrDefault(i, 0);
+ starts[i] = pos;
+ pos += countSize + (long) ifds.get(i).length * entrySize +
offsetSize;
+ for (Entry e : ifds.get(i)) {
+ pos += outOfLineSize(e, inline);
+ }
+ }
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.writeBytes(new byte[]{'I', 'I', (byte) (bigTiff ? 43 : 42),
0});
+ if (bigTiff) {
+ le16(out, 8);
+ le16(out, 0);
+ le64(out, starts[0]);
+ } else {
+ le32(out, (int) starts[0]);
+ }
+ for (int i = 0; i < ifds.size(); i++) {
+ pad(out, gaps.getOrDefault(i, 0));
+ Entry[] entries = ifds.get(i);
+ long dataPos = starts[i] + countSize + (long) entries.length *
entrySize + offsetSize;
+ if (bigTiff) {
+ le64(out, entries.length);
+ } else {
+ le16(out, entries.length);
+ }
+ ByteArrayOutputStream data = new ByteArrayOutputStream();
+ for (Entry e : entries) {
+ le16(out, e.tag());
+ if (e.tag() == 0x014A) {
+ int type = bigTiff ? 16 : 4;
+ int size = bigTiff ? 8 : 4;
+ le16(out, type);
+ count(out, e.count());
+ long target = i + 1 < starts.length ? starts[i + 1] :
0;
+ if (e.count() * size <= inline) {
+ for (int k = 0; k < e.count(); k++) {
+ offset(out, target, size);
+ }
+ pad(out, (int) (inline - e.count() * size));
+ } else {
+ offset(out, dataPos + data.size(), inline);
+ for (int k = 0; k < e.count(); k++) {
+ offset(data, target, size);
+ }
+ }
+ } else if (e.data() != null) {
+ le16(out, e.type());
+ count(out, e.count());
+ if (e.data().length <= inline) {
+ out.writeBytes(e.data());
+ pad(out, inline - e.data().length);
+ } else {
+ offset(out, dataPos + data.size(), inline);
+ data.writeBytes(e.data());
+ }
+ } else {
+ le16(out, e.type());
+ count(out, 1);
+ if (e.type() == 3) {
+ le16(out, (int) e.value());
+ pad(out, inline - 2);
+ } else {
+ le32(out, (int) e.value());
+ pad(out, inline - 4);
+ }
+ }
+ }
+ long next = nextPointsToSelf ? starts[i] : 0;
+ offset(out, next, offsetSize);
+ out.writeBytes(data.toByteArray());
+ }
+ return out.toByteArray();
+ }
+
+ private long outOfLineSize(Entry e, int inline) {
+ if (e.tag() == 0x014A) {
+ int size = bigTiff ? 8 : 4;
+ return e.count() * size <= inline ? 0 : e.count() * size;
+ }
+ if (e.data() != null) {
+ return e.data().length <= inline ? 0 : e.data().length;
+ }
+ return 0;
+ }
+
+ private void count(ByteArrayOutputStream out, long count) {
+ if (bigTiff) {
+ le64(out, count);
+ } else {
+ le32(out, (int) count);
+ }
+ }
+
+ private static void offset(ByteArrayOutputStream out, long value, int
size) {
+ if (size == 8) {
+ le64(out, value);
+ } else {
+ le32(out, (int) value);
+ }
+ }
+
+ private static void pad(ByteArrayOutputStream out, int n) {
+ for (int i = 0; i < n; i++) {
+ out.write(0);
+ }
+ }
+ }
+
+ private static void le64(ByteArrayOutputStream out, long v) {
+ le32(out, (int) (v & 0xFFFFFFFFL));
+ le32(out, (int) ((v >>> 32) & 0xFFFFFFFFL));
+ }
+
+ private static void le16(ByteArrayOutputStream out, int v) {
+ out.write(v & 0xFF);
+ out.write((v >> 8) & 0xFF);
+ }
+
+ private static void le32(ByteArrayOutputStream out, int v) {
+ le16(out, v & 0xFFFF);
+ le16(out, (v >> 16) & 0xFFFF);
+ }
+}