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);
+    }
+}

Reply via email to