This is an automated email from the ASF dual-hosted git repository.

tballison 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 fecd5966ab TIKA-4980: Extract previews from Olympus ORF, Panasonic 
RW2, Epson ERF, Kodak KDC and Leaf MOS (#3370)
fecd5966ab is described below

commit fecd5966ab48369ba368a1ff2bc216e3c13f314f
Author: Dominik Schmidt <[email protected]>
AuthorDate: Tue Oct 20 20:53:27 2026 +0200

    TIKA-4980: Extract previews from Olympus ORF, Panasonic RW2, Epson ERF, 
Kodak KDC and Leaf MOS (#3370)
    
    Generated-by: Claude Code
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 CHANGES.txt                                        |   8 +
 docs/modules/ROOT/partials/supported-formats.adoc  |   5 +
 .../org/apache/tika/mime/tika-mimetypes.xml        |   3 +
 .../apache/tika/detect/image/RawTiffDetector.java  |   9 +-
 .../tika/parser/image/ImageMetadataExtractor.java  |  14 +-
 .../apache/tika/parser/image/RawTiffParser.java    | 528 +++++++++++++++++++--
 .../tika/detect/image/RawTiffDetectorTest.java     |  36 ++
 .../tika/parser/image/RawTiffParserTest.java       | 166 +++++++
 .../src/test/resources/test-documents/testERF.erf  | Bin 0 -> 537 bytes
 .../resources/test-documents/testERF_nopreview.erf | Bin 0 -> 166 bytes
 .../src/test/resources/test-documents/testKDC.kdc  | Bin 0 -> 467 bytes
 .../src/test/resources/test-documents/testMOS.mos  | Bin 0 -> 564 bytes
 .../src/test/resources/test-documents/testORF.orf  | Bin 0 -> 890 bytes
 .../resources/test-documents/testORF_bigendian.orf | Bin 0 -> 866 bytes
 .../resources/test-documents/testORF_omsystem.orf  | Bin 0 -> 912 bytes
 .../src/test/resources/test-documents/testRW2.rw2  | Bin 0 -> 467 bytes
 16 files changed, 724 insertions(+), 45 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 2402b3fb0f..fed348fbf5 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,13 @@
 Release 4.2.0 - unreleased
 
+   * Previews are extracted from Olympus ORF (also with the OM SYSTEM 
MakerNote), Panasonic
+     RW2, Epson ERF, Kodak KDC and Leaf MOS. Epson ERF is detected by content, 
and
+     image/x-raw-epson, image/x-raw-kodak and image/x-raw-leaf are sub-classes 
of
+     image/tiff (TIKA-4980).
+
+   * ImageMetadataExtractor skips a tag whose metadata-extractor descriptor 
throws instead of
+     failing the parse; this applies to every image format metadata-extractor 
reads (TIKA-4980).
+
    * A JsonConfig's raw text is read only through ConfigDeserializer or
      PluginJson.read(JsonConfig, Class), which apply the request restrictions 
and
      @OperatorOnly; forbidden-apis rejects JsonConfig.json() elsewhere in 
Tika's main code.
diff --git a/docs/modules/ROOT/partials/supported-formats.adoc 
b/docs/modules/ROOT/partials/supported-formats.adoc
index 9239661486..393914373f 100644
--- a/docs/modules/ROOT/partials/supported-formats.adoc
+++ b/docs/modules/ROOT/partials/supported-formats.adoc
@@ -136,7 +136,12 @@
 * `org.apache.tika.parser.image.` 
link:{tika-javadoc-url}/org/apache/tika/parser/image/RawTiffParser.html[RawTiffParser]
 ** `image/x-canon-cr2`
 ** `image/x-raw-adobe`
+** `image/x-raw-epson`
+** `image/x-raw-kodak`
+** `image/x-raw-leaf`
 ** `image/x-raw-nikon`
+** `image/x-raw-olympus`
+** `image/x-raw-panasonic`
 ** `image/x-raw-pentax`
 ** `image/x-raw-samsung`
 ** `image/x-raw-sony`
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 d59422dbca..652923e86b 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
@@ -7304,6 +7304,7 @@
 
   <mime-type type="image/x-raw-kodak">
     <_comment>Kodak raw image</_comment>
+    <sub-class-of type="image/tiff" />
     <glob pattern="*.k25"/>
     <glob pattern="*.kdc"/>
     <glob pattern="*.dcs"/>
@@ -7364,6 +7365,7 @@
 
   <mime-type type="image/x-raw-epson">
     <_comment>Epson raw image</_comment>
+    <sub-class-of type="image/tiff" />
     <glob pattern="*.erf"/>
   </mime-type>
 
@@ -7374,6 +7376,7 @@
 
   <mime-type type="image/x-raw-leaf">
     <_comment>Leaf raw image</_comment>
+    <sub-class-of type="image/tiff" />
     <glob pattern="*.mos"/>
   </mime-type>
 
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
index 4521287a77..3f5673b65a 100644
--- 
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
@@ -34,14 +34,16 @@ 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,
+ * Sony ARW/SRF/SR2, Samsung SRW, Epson ERF, 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>
+ *       32770 and 32772 Samsung (this is how ExifTool identifies
+ *       them). Epson's own 32769 is not among them: it is a published TIFF
+ *       value other writers use, so an ERF is told by its Make;</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),
@@ -109,6 +111,7 @@ public class RawTiffDetector implements Detector {
     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 EPSON = MediaType.image("x-raw-epson");
     static final MediaType ADOBE = MediaType.image("x-raw-adobe");
 
     @Override
@@ -368,6 +371,8 @@ public class RawTiffDetector implements Detector {
             return SONY;
         } else if (m.startsWith("SAMSUNG")) {
             return SAMSUNG;
+        } else if (m.startsWith("SEIKO EPSON") || m.startsWith("EPSON")) {
+            return EPSON;
         }
         return null;
     }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataExtractor.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataExtractor.java
index 74be1c0378..984f7483dc 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataExtractor.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataExtractor.java
@@ -395,9 +395,17 @@ public class ImageMetadataExtractor {
                     if (!includeIccCurvesAndLuts && isIccCurveOrLut(iccType)) {
                         continue;
                     }
-                    String description = iccType == ICC_TYPE_CURV ?
-                            
formatIccCurve(directory.getByteArray(tag.getTagType())) :
-                            tag.getDescription();
+                    String description;
+                    try {
+                        description = iccType == ICC_TYPE_CURV ?
+                                
formatIccCurve(directory.getByteArray(tag.getTagType())) :
+                                tag.getDescription();
+                    } catch (ClassCastException | IndexOutOfBoundsException e) 
{
+                        //a descriptor reads its tag as the type and length it 
expects,
+                        //and throws when the file stored another: one tag 
nobody can
+                        //describe is no reason to drop the rest of the 
directory
+                        continue;
+                    }
                     if (description != null) {
                         String value = description.trim();
                         if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
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 49c382662f..6763048ed2 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
@@ -16,10 +16,14 @@
  */
 package org.apache.tika.parser.image;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.RandomAccessFile;
+import java.io.SequenceInputStream;
 import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -54,7 +58,8 @@ import org.apache.tika.sax.XHTMLContentHandler;
 
 /**
  * Parser for TIFF-based camera raw images: Nikon NEF/NRW, Sony ARW/SRF/SR2,
- * Pentax PEF/PTX, Samsung SRW, Adobe DNG and Canon CR2.
+ * Pentax PEF/PTX, Samsung SRW, Epson ERF, Kodak KDC, Leaf MOS, Adobe DNG,
+ * Canon CR2, Olympus ORF and Panasonic RW2.
  * <p>
  * These formats are TIFF containers: metadata extraction is inherited from
  * {@link TiffParser}. In addition, this parser extracts the camera-generated
@@ -62,7 +67,17 @@ import org.apache.tika.sax.XHTMLContentHandler;
  * {@link EmbeddedDocumentExtractor}. Previews are referenced from the IFD
  * chain or from SubIFDs, either via the JPEGInterchangeFormat/
  * JPEGInterchangeFormatLength tags or as a single JPEG-compressed strip
- * (DNG, CR2). Strips holding raw sensor data are also JPEG-encoded in some
+ * (DNG, CR2). Four formats keep theirs elsewhere. A Panasonic RW2 uses the
+ * private JpgFromRaw tag. An Olympus ORF uses its MakerNote, whose own IFD
+ * holds the camera thumbnail and the CameraSettings sub-IFD that points at
+ * the preview. An Epson ERF uses its MakerNote too, under a tag of its own,
+ * at an offset counted from the start of the file and behind a byte the
+ * camera writes over the JPEG's first one, which is put back on the way out.
+ * A Leaf MOS uses a vendor block of named sections, one of which holds the
+ * JPEG. ORF and RW2 carry a magic number of their own; ERF, KDC and MOS are
+ * plain TIFFs, and a KDC names its preview the way a TIFF does.
+ * <p>
+ * Strips holding raw sensor data are also JPEG-encoded in some
  * formats (lossless JPEG in CR2 and DNG), so strip candidates are only
  * accepted for displayable images: PhotometricInterpretation RGB or YCbCr,
  * or 8 bits per sample when PhotometricInterpretation is absent (CR2).
@@ -90,10 +105,16 @@ public class RawTiffParser extends TiffParser {
                     MediaType.image("x-raw-sony"),
                     MediaType.image("x-raw-pentax"),
                     MediaType.image("x-raw-samsung"),
+                    MediaType.image("x-raw-epson"),
+                    MediaType.image("x-raw-kodak"),
+                    MediaType.image("x-raw-leaf"),
                     MediaType.image("x-raw-adobe"),
+                    MediaType.image("x-raw-olympus"),
+                    MediaType.image("x-raw-panasonic"),
                     MediaType.image("x-canon-cr2"))));
 
     private static final String JPEG_MIME = "image/jpeg";
+    private static final byte[] JPEG_SOI_FIRST_BYTE = {(byte) 0xFF};
 
     private static final int TAG_BITS_PER_SAMPLE = 0x0102;
     private static final int TAG_COMPRESSION = 0x0103;
@@ -103,6 +124,55 @@ public class RawTiffParser extends TiffParser {
     private static final int TAG_SUB_IFDS = 0x014A;
     private static final int TAG_JPEG_INTERCHANGE_FORMAT = 0x0201;
     private static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 0x0202;
+    private static final int TAG_EXIF_IFD = 0x8769;
+    private static final int TAG_MAKER_NOTE = 0x927C;
+    //Panasonic keeps the whole preview JPEG in a private tag of IFD0
+    private static final int TAG_PANASONIC_JPG_FROM_RAW = 0x002E;
+    //inside an Olympus MakerNote: the camera thumbnail, and the sub-IFD
+    //that points at the preview
+    private static final int TAG_OLYMPUS_THUMBNAIL = 0x0100;
+    private static final int TAG_OLYMPUS_CAMERA_SETTINGS = 0x2020;
+    private static final int TAG_OLYMPUS_PREVIEW_START = 0x0101;
+    private static final int TAG_OLYMPUS_PREVIEW_LENGTH = 0x0102;
+    //ExifTool knows it as PreviewImage, in ERFs and in JPEGs from some Epsons
+    private static final int TAG_EPSON_PREVIEW = 0x0280;
+    //Leaf keeps its own data in one tag, a chain of named blocks
+    private static final int TAG_LEAF_DATA = 0x8606;
+    private static final byte[] LEAF_BLOCK_MAGIC =
+            "PKTS".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] LEAF_PREVIEW_NAME =
+            "JPEG_preview_data".getBytes(StandardCharsets.US_ASCII);
+    //magic and version, then a padded name, then the payload's length
+    private static final int LEAF_BLOCK_NAME_OFFSET = 8;
+    private static final int LEAF_BLOCK_NAME_LENGTH = 40;
+    private static final int LEAF_BLOCK_HEADER_LENGTH =
+            LEAF_BLOCK_NAME_OFFSET + LEAF_BLOCK_NAME_LENGTH + 4;
+    //a file holds a dozen of them; a crafted one must not be walked forever
+    private static final int MAX_LEAF_BLOCKS = 64;
+
+    private static final int MAGIC_TIFF = 42;
+    private static final int MAGIC_BIG_TIFF = 43;
+    //tika-mimetypes.xml matches the same signatures: "IIRO" and "MMOR" read
+    //as 0x4F52, "IIRS" as 0x5352, and an RW2 starts with "IIU\0"
+    private static final int MAGIC_OLYMPUS_RO = 0x4F52;
+    private static final int MAGIC_OLYMPUS_RS = 0x5352;
+    private static final int MAGIC_PANASONIC = 0x0055;
+
+    private static final byte[] OLYMPUS_MAKER_NOTE_NAME =
+            "OLYMPUS\u0000".getBytes(StandardCharsets.US_ASCII);
+    //OM Digital Solutions bodies (OM-1, OM-3, OM-5, TG-7) write a longer name
+    //and the same layout behind it, what ExifTool calls MakerNoteOlympus3
+    private static final byte[] OM_SYSTEM_MAKER_NOTE_NAME =
+            "OM SYSTEM\u0000\u0000\u0000".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] EPSON_MAKER_NOTE_NAME =
+            "EPSON\u0000".getBytes(StandardCharsets.US_ASCII);
+    //the name and two bytes of version, then the IFD, whose offsets count
+    //from the start of the file and not from the MakerNote
+    private static final int EPSON_MAKER_NOTE_HEADER_LENGTH = 8;
+    //the name, the MakerNote's own byte order marker and its version, then
+    //its IFD starts
+    private static final int OLYMPUS_MAKER_NOTE_HEADER_LENGTH = 12;
+    private static final int OM_SYSTEM_MAKER_NOTE_HEADER_LENGTH = 16;
 
     private static final int COMPRESSION_OLD_JPEG = 6;
     private static final int COMPRESSION_JPEG = 7;
@@ -110,6 +180,16 @@ public class RawTiffParser extends TiffParser {
     private static final int PHOTOMETRIC_YCBCR = 6;
 
     private static final int MAX_IFDS = 32;
+    private static final int CLASSIC_ENTRY_SIZE = 12;
+    private static final int CLASSIC_INLINE_SIZE = 4;
+    private static final int BIG_TIFF_INLINE_SIZE = 8;
+    private static final int TYPE_BYTE = 1;
+    private static final int TYPE_ASCII = 2;
+    private static final int TYPE_UNDEFINED = 7;
+    private static final int BIG_TIFF_ENTRY_SIZE = 20;
+    //a crafted file must not send the reader chasing dozens of MakerNotes,
+    //so the looking is what is counted, not the finding
+    private static final int MAX_MAKER_NOTE_LOOKUPS = 4;
     private static final int MAX_ENTRIES_PER_IFD = 1024;
     // at most MAX_IFDS are ever processed; cap the pending queue so a crafted
     // file packed with SubIFD pointers cannot grow it without bound
@@ -144,7 +224,16 @@ public class RawTiffParser extends TiffParser {
     @Override
     public void parse(TikaInputStream tis, ContentHandler handler, Metadata 
metadata,
                       ParseContext context) throws IOException, SAXException, 
TikaException {
-        if (isBigTiff(tis)) {
+        int magic = readMagic(tis);
+        if (!isKnownMagic(magic)) {
+            //only a file name sent it here (*.raw is shared with other
+            //formats): nothing to read, as when no parser claimed the type
+            XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, 
metadata, context);
+            xhtml.startDocument();
+            xhtml.endDocument();
+            return;
+        }
+        if (magic == MAGIC_BIG_TIFF) {
             //metadata-extractor cannot read BigTIFF containers yet:
             //scan for XMP, but skip EXIF metadata extraction
             tis.getFile();
@@ -223,12 +312,16 @@ public class RawTiffParser extends TiffParser {
                 }
                 //stream the preview region instead of loading it onto the 
heap;
                 //close-shield the shared channel so parseEmbedded cannot 
close it
-                channel.position(preview.offset());
+                channel.position(preview.streamOffset());
                 BoundedInputStream bounded = BoundedInputStream.builder()
                         
.setInputStream(CloseShieldInputStream.wrap(Channels.newInputStream(channel)))
-                        .setMaxCount(preview.length())
+                        .setMaxCount(preview.streamLength())
                         .get();
-                try (TikaInputStream previewStream = 
TikaInputStream.get(bounded)) {
+                InputStream bytes = preview.soiPatched()
+                        ? new SequenceInputStream(new 
ByteArrayInputStream(JPEG_SOI_FIRST_BYTE),
+                        bounded)
+                        : bounded;
+                try (TikaInputStream previewStream = 
TikaInputStream.get(bytes)) {
                     extractor.parseEmbedded(previewStream, xhtml, 
previewMetadata, context, true);
                 }
             }
@@ -257,29 +350,31 @@ public class RawTiffParser extends TiffParser {
             throw new TiffStructureException("not a TIFF byte order marker");
         }
         int magic = readUInt16(raf, bigEndian);
-        boolean bigTiff;
-        if (magic == 42) {
-            bigTiff = false;
-        } else if (magic == 43) {
+        boolean bigTiff = false;
+        //ORF and RW2 are classic TIFF layouts behind their own magic number,
+        //and keep their previews in places only they use
+        boolean olympus = magic == MAGIC_OLYMPUS_RO || magic == 
MAGIC_OLYMPUS_RS;
+        boolean panasonic = magic == MAGIC_PANASONIC;
+        if (magic == MAGIC_BIG_TIFF) {
             //BigTIFF: 8-byte offset size, then a constant 0
             bigTiff = true;
             if (readUInt16(raf, bigEndian) != 8 || readUInt16(raf, bigEndian) 
!= 0) {
                 throw new TiffStructureException("unsupported BigTIFF header");
             }
-        } else {
+        } else if (magic != MAGIC_TIFF && !olympus && !panasonic) {
             throw new TiffStructureException("bad TIFF magic number");
         }
         int countSize = bigTiff ? 8 : 2;
-        int entrySize = bigTiff ? 20 : 12;
+        int entrySize = bigTiff ? BIG_TIFF_ENTRY_SIZE : CLASSIC_ENTRY_SIZE;
         int offsetSize = bigTiff ? 8 : 4;
 
-        List<Preview> previews = new ArrayList<>();
-        //distinct IFDs can reference the same JPEG region; extract each 
region once
-        Set<Long> previewOffsets = new HashSet<>();
-        long totalPreviewBytes = 0;
-        long maxTotalPreviewBytes = config.getMaxTotalPreviewBytes();
+        PreviewCollector collector = new 
PreviewCollector(config.getMaxTotalPreviewBytes());
         Set<Long> visited = new HashSet<>();
         Deque<Long> toVisit = new ArrayDeque<>();
+        List<Long> makerNotes = new ArrayList<>();
+        int makerNoteLookups = 0;
+        long leafData = -1;
+        long leafDataLength = -1;
         toVisit.add(readOffset(raf, bigEndian, bigTiff));
 
         while (!toVisit.isEmpty() && visited.size() < MAX_IFDS) {
@@ -303,9 +398,11 @@ public class RawTiffParser extends TiffParser {
             byte[] table = new byte[(int) (numEntries * entrySize + 
offsetSize)];
             raf.seek(ifdOffset + countSize);
             raf.readFully(table);
-            int inlineSize = bigTiff ? 8 : 4;
+            int inlineSize = bigTiff ? BIG_TIFF_INLINE_SIZE : 
CLASSIC_INLINE_SIZE;
             long jpegOffset = -1;
             long jpegLength = -1;
+            long panasonicOffset = -1;
+            long panasonicLength = -1;
             long compression = -1;
             long photometric = -1;
             long[] bitsPerSample = new long[0];
@@ -313,8 +410,8 @@ public class RawTiffParser extends TiffParser {
             long[] stripByteCounts = new long[0];
             for (int i = 0; i < numEntries; i++) {
                 int e = i * entrySize;
-                int tag = getUInt16(table, e, bigEndian);
-                int type = getUInt16(table, e + 2, bigEndian);
+                int tag = entryTag(table, e, bigEndian);
+                int type = entryType(table, e, bigEndian);
                 long valueCount = bigTiff
                         ? getUInt64(table, e + 4, bigEndian) : 
getUInt32(table, e + 4, bigEndian);
                 int valueField = e + 4 + inlineSize;
@@ -341,34 +438,360 @@ public class RawTiffParser extends TiffParser {
                     stripOffsets = readLongValues(raf, table, valueField, 
bigEndian, bigTiff, type, valueCount);
                 } else if (tag == TAG_STRIP_BYTE_COUNTS) {
                     stripByteCounts = readLongValues(raf, table, valueField, 
bigEndian, bigTiff, type, valueCount);
+                } else if (panasonic && tag == TAG_PANASONIC_JPG_FROM_RAW
+                        && isBlob(type, valueCount, inlineSize)) {
+                    panasonicOffset = getUInt32(table, valueField, bigEndian);
+                    panasonicLength = valueCount;
+                } else if (tag == TAG_LEAF_DATA && leafData < 0 && valueCount 
> inlineSize
+                        && (type == TYPE_ASCII || isBlob(type, valueCount, 
inlineSize))) {
+                    //Leaf types the block as ASCII, which it is not
+                    leafData = getUInt32(table, valueField, bigEndian);
+                    leafDataLength = valueCount;
+                } else if (tag == TAG_EXIF_IFD && valueCount == 1 && !bigTiff
+                        && makerNoteLookups < MAX_MAKER_NOTE_LOOKUPS) {
+                    for (long exifIfd :
+                            readLongValues(raf, table, valueField, bigEndian, 
bigTiff, type, valueCount)) {
+                        makerNoteLookups++;
+                        long makerNote = readMakerNoteOffset(raf, fileLength, 
exifIfd, bigEndian);
+                        if (makerNote > 0) {
+                            makerNotes.add(makerNote);
+                        }
+                    }
                 }
             }
             int followerField = (int) (numEntries * entrySize);
             enqueue(toVisit, bigTiff
                     ? getUInt64(table, followerField, bigEndian) : 
getUInt32(table, followerField, bigEndian));
 
+            if (panasonicOffset > 0) {
+                //an RW2 keeps its preview here and writes placeholders into 
the
+                //strip tags, so its own tag answers before either of them
+                jpegOffset = panasonicOffset;
+                jpegLength = panasonicLength;
+            }
             if (jpegOffset < 0 && isDisplayableJpegStrip(compression, 
photometric, bitsPerSample,
                     stripOffsets, stripByteCounts)) {
                 jpegOffset = stripOffsets[0];
                 jpegLength = stripByteCounts[0];
             }
-            if (jpegOffset > 0 && jpegLength > 4 &&
-                    jpegLength <= config.getMaxPreviewLengthBytes() &&
-                    jpegOffset <= fileLength - jpegLength) {
-                raf.seek(jpegOffset);
-                //require the JPEG SOI marker
-                if (raf.read() == 0xFF && raf.read() == 0xD8 && 
previewOffsets.add(jpegOffset)) {
-                    //bound the aggregate extraction work; overlapping offsets
-                    //could otherwise sum to many times the file's own size
-                    if (totalPreviewBytes > maxTotalPreviewBytes - jpegLength) 
{
-                        break;
-                    }
-                    totalPreviewBytes += jpegLength;
-                    previews.add(new Preview(jpegOffset, jpegLength));
+            Preview candidate = new Preview(jpegOffset, jpegLength);
+            if (isPreviewAt(raf, fileLength, candidate, config)) {
+                collector.add(candidate);
+            }
+            if (collector.isFull()) {
+                break;
+            }
+        }
+
+        for (Preview candidate : readLeafPreviews(raf, fileLength, leafData, 
leafDataLength,
+                bigEndian)) {
+            if (isPreviewAt(raf, fileLength, candidate, config)) {
+                collector.add(candidate);
+            }
+            if (collector.isFull()) {
+                break;
+            }
+        }
+
+        for (long makerNote : makerNotes) {
+            if (collector.isFull()) {
+                break;
+            }
+            for (Preview candidate : readMakerNotePreviews(raf, fileLength, 
makerNote, bigEndian)) {
+                if (isPreviewAt(raf, fileLength, candidate, config)) {
+                    collector.add(candidate);
+                }
+                if (collector.isFull()) {
+                    break;
                 }
             }
         }
-        return previews;
+        return collector.previews();
+    }
+
+    /**
+     * Whether a preview really lies at the offset: inside the file, within the
+     * per-preview limit, and starting with the JPEG SOI marker.
+     */
+    private boolean isPreviewAt(RandomAccessFile raf, long fileLength, Preview 
preview,
+                                RawTiffParserConfig config) throws IOException 
{
+        if (preview.offset() <= 0 || preview.length() <= 4
+                || preview.length() > config.getMaxPreviewLengthBytes()
+                || preview.offset() > fileLength - preview.length()) {
+            return false;
+        }
+        raf.seek(preview.offset());
+        int first = raf.read();
+        int second = raf.read();
+
+        return (preview.soiPatched() || first == 0xFF) && second == 0xD8;
+    }
+
+    /**
+     * The previews found so far, and the budget they share.
+     */
+    private static final class PreviewCollector {
+        private final List<Preview> previews = new ArrayList<>();
+        //distinct IFDs can reference the same JPEG region; extract each 
region once
+        private final Set<Long> offsets = new HashSet<>();
+        private final long maxTotalBytes;
+        private long totalBytes;
+        private boolean full;
+
+        PreviewCollector(long maxTotalBytes) {
+            this.maxTotalBytes = maxTotalBytes;
+        }
+
+        /**
+         * The aggregate limit bounds the extraction work: overlapping offsets
+         * could otherwise sum to many times the file's own size, and reaching
+         * it ends the walk.
+         */
+        void add(Preview preview) {
+            if (!offsets.add(preview.offset())) {
+                return;
+            }
+            if (preview.length() > maxTotalBytes - totalBytes) {
+                full = true;
+                return;
+            }
+            totalBytes += preview.length();
+            previews.add(preview);
+        }
+
+        boolean isFull() {
+            return full;
+        }
+
+        List<Preview> previews() {
+            return previews;
+        }
+    }
+
+    /**
+     * Where the MakerNote of an Exif IFD begins, or -1 when it has none.
+     */
+    private long readMakerNoteOffset(RandomAccessFile raf, long fileLength, 
long exifIfd,
+                                     boolean bigEndian) throws IOException {
+        byte[] table = readIfdEntries(raf, fileLength, exifIfd, bigEndian);
+        if (table == null) {
+            return -1;
+        }
+        for (int e = 0; e + CLASSIC_ENTRY_SIZE <= table.length; e += 
CLASSIC_ENTRY_SIZE) {
+            if (entryTag(table, e, bigEndian) == TAG_MAKER_NOTE
+                    && isBlob(entryType(table, e, bigEndian), 
classicEntryCount(table, e, bigEndian),
+                    CLASSIC_INLINE_SIZE)) {
+                return classicEntryValue(table, e, bigEndian);
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * The JPEG a Leaf MOS keeps in its data block: a chain of blocks, each a
+     * magic and a version, a padded name and the length of what follows. The
+     * one named JPEG_preview_data holds the preview, stored as it is.
+     */
+    private List<Preview> readLeafPreviews(RandomAccessFile raf, long 
fileLength, long block,
+                                           long blockLength, boolean bigEndian)
+            throws IOException {
+        List<Preview> candidates = new ArrayList<>();
+        if (block <= 0 || blockLength <= 0 || block > fileLength - 
blockLength) {
+            return candidates;
+        }
+        long end = block + blockLength;
+        long at = block;
+        byte[] header = new byte[LEAF_BLOCK_HEADER_LENGTH];
+        for (int n = 0; n < MAX_LEAF_BLOCKS && at <= end - 
LEAF_BLOCK_HEADER_LENGTH; n++) {
+            raf.seek(at);
+            raf.readFully(header);
+            if (!startsWith(header, LEAF_BLOCK_MAGIC)) {
+                break;
+            }
+            long length = getUInt32(header, LEAF_BLOCK_HEADER_LENGTH - 4, 
bigEndian);
+            long payload = at + LEAF_BLOCK_HEADER_LENGTH;
+            if (length > end - payload) {
+                break;
+            }
+            if (Arrays.equals(header, LEAF_BLOCK_NAME_OFFSET,
+                    LEAF_BLOCK_NAME_OFFSET + LEAF_PREVIEW_NAME.length,
+                    LEAF_PREVIEW_NAME, 0, LEAF_PREVIEW_NAME.length)) {
+                candidates.add(new Preview(payload, length));
+            }
+            at = payload + length;
+        }
+
+        return candidates;
+    }
+
+    /**
+     * The previews a MakerNote points at, by the name it begins with.
+     */
+    private List<Preview> readMakerNotePreviews(RandomAccessFile raf, long 
fileLength,
+                                                long makerNote, boolean 
bigEndian)
+            throws IOException {
+        if (makerNote <= 0 || makerNote > fileLength - 
EPSON_MAKER_NOTE_HEADER_LENGTH) {
+            return new ArrayList<>();
+        }
+        //as much of a header as the file holds: the three names need 8, 12 
and 16
+        byte[] header = new byte[OM_SYSTEM_MAKER_NOTE_HEADER_LENGTH];
+        raf.seek(makerNote);
+        raf.readFully(header, 0, (int) Math.min(header.length, fileLength - 
makerNote));
+        if (startsWith(header, OLYMPUS_MAKER_NOTE_NAME)) {
+            return readOlympusMakerNotePreviews(raf, fileLength, makerNote, 
header,
+                    OLYMPUS_MAKER_NOTE_NAME, OLYMPUS_MAKER_NOTE_HEADER_LENGTH);
+        }
+        if (startsWith(header, OM_SYSTEM_MAKER_NOTE_NAME)) {
+            return readOlympusMakerNotePreviews(raf, fileLength, makerNote, 
header,
+                    OM_SYSTEM_MAKER_NOTE_NAME, 
OM_SYSTEM_MAKER_NOTE_HEADER_LENGTH);
+        }
+        if (startsWith(header, EPSON_MAKER_NOTE_NAME)) {
+            return readEpsonMakerNotePreviews(raf, fileLength, makerNote, 
bigEndian);
+        }
+
+        return new ArrayList<>();
+    }
+
+    private static boolean startsWith(byte[] header, byte[] name) {
+        return Arrays.equals(header, 0, name.length, name, 0, name.length);
+    }
+
+    /**
+     * The preview an Epson ERF keeps in its MakerNote (0x0280). Its offset
+     * counts from the start of the file, and the camera writes a byte of its
+     * own where the JPEG's first one belongs, so the preview is marked for
+     * the SOI to be put back when its bytes are handed on.
+     */
+    private List<Preview> readEpsonMakerNotePreviews(RandomAccessFile raf, 
long fileLength,
+                                                     long makerNote, boolean 
bigEndian)
+            throws IOException {
+        List<Preview> candidates = new ArrayList<>();
+        byte[] table = readIfdEntries(raf, fileLength,
+                makerNote + EPSON_MAKER_NOTE_HEADER_LENGTH, bigEndian);
+        if (table == null) {
+            return candidates;
+        }
+        for (int e = 0; e + CLASSIC_ENTRY_SIZE <= table.length; e += 
CLASSIC_ENTRY_SIZE) {
+            long count = classicEntryCount(table, e, bigEndian);
+            if (entryTag(table, e, bigEndian) == TAG_EPSON_PREVIEW
+                    && isBlob(entryType(table, e, bigEndian), count, 
CLASSIC_INLINE_SIZE)) {
+                candidates.add(new Preview(classicEntryValue(table, e, 
bigEndian), count, true));
+            }
+        }
+
+        return candidates;
+    }
+
+    /**
+     * The JPEGs an Olympus MakerNote points at: the camera's thumbnail
+     * (0x0100) and the preview that the CameraSettings sub-IFD (0x2020)
+     * names with PreviewImageStart and PreviewImageLength. Every offset
+     * inside the MakerNote is relative to its first byte. The "OLYMPUS\0"
+     * and "OM SYSTEM\0\0\0" headers are read, which differ in the name's
+     * length only; the older "OLYMP\0" one counts from elsewhere and is left
+     * to the next reader.
+     */
+    private List<Preview> readOlympusMakerNotePreviews(RandomAccessFile raf, 
long fileLength,
+                                                       long makerNote, byte[] 
header,
+                                                       byte[] name, int 
headerLength)
+            throws IOException {
+        List<Preview> candidates = new ArrayList<>();
+        //the MakerNote carries its own byte order marker, which a big endian
+        //file does not have to share
+        boolean bigEndian;
+        int order = name.length;
+        if (header[order] == 'I' && header[order + 1] == 'I') {
+            bigEndian = false;
+        } else if (header[order] == 'M' && header[order + 1] == 'M') {
+            bigEndian = true;
+        } else {
+            return candidates;
+        }
+        byte[] table = readIfdEntries(raf, fileLength, makerNote + 
headerLength, bigEndian);
+        if (table == null) {
+            return candidates;
+        }
+        long settings = -1;
+        for (int e = 0; e + CLASSIC_ENTRY_SIZE <= table.length; e += 
CLASSIC_ENTRY_SIZE) {
+            int tag = entryTag(table, e, bigEndian);
+            long count = classicEntryCount(table, e, bigEndian);
+            long value = classicEntryValue(table, e, bigEndian);
+            if (tag == TAG_OLYMPUS_THUMBNAIL
+                    && isBlob(entryType(table, e, bigEndian), count, 
CLASSIC_INLINE_SIZE)) {
+                candidates.add(new Preview(makerNote + value, count));
+            } else if (tag == TAG_OLYMPUS_CAMERA_SETTINGS && count == 1) {
+                settings = makerNote + value;
+            }
+        }
+        byte[] settingsTable = readIfdEntries(raf, fileLength, settings, 
bigEndian);
+        if (settingsTable == null) {
+            return candidates;
+        }
+        long start = -1;
+        long length = -1;
+        for (int e = 0; e + CLASSIC_ENTRY_SIZE <= settingsTable.length; e += 
CLASSIC_ENTRY_SIZE) {
+            int tag = entryTag(settingsTable, e, bigEndian);
+            if (classicEntryCount(settingsTable, e, bigEndian) != 1) {
+                continue;
+            }
+            if (tag == TAG_OLYMPUS_PREVIEW_START) {
+                start = classicEntryValue(settingsTable, e, bigEndian);
+            } else if (tag == TAG_OLYMPUS_PREVIEW_LENGTH) {
+                length = classicEntryValue(settingsTable, e, bigEndian);
+            }
+        }
+        if (start >= 0 && length > 0) {
+            candidates.add(new Preview(makerNote + start, length));
+        }
+        return candidates;
+    }
+
+    /**
+     * The fields of an IFD entry. The tag and the type sit where they do in a
+     * BigTIFF too; the other two are the classic layout's, as their names say.
+     */
+    private static int entryTag(byte[] entries, int e, boolean bigEndian) {
+        return getUInt16(entries, e, bigEndian);
+    }
+
+    private static int entryType(byte[] entries, int e, boolean bigEndian) {
+        return getUInt16(entries, e + 2, bigEndian);
+    }
+
+    private static long classicEntryCount(byte[] entries, int e, boolean 
bigEndian) {
+        return getUInt32(entries, e + 4, bigEndian);
+    }
+
+    private static long classicEntryValue(byte[] entries, int e, boolean 
bigEndian) {
+        return getUInt32(entries, e + 8, bigEndian);
+    }
+
+    /**
+     * Whether the entry holds its bytes elsewhere, which is what a tag naming
+     * an image is: BYTE or UNDEFINED, and too long for the value field.
+     */
+    private static boolean isBlob(int type, long valueCount, int inlineSize) {
+        return (type == TYPE_BYTE || type == TYPE_UNDEFINED) && valueCount > 
inlineSize;
+    }
+
+    /**
+     * The entries of a classic IFD at the offset, without the pointer that
+     * follows them; null when the file holds no such table there.
+     */
+    private byte[] readIfdEntries(RandomAccessFile raf, long fileLength, long 
offset,
+                                  boolean bigEndian) throws IOException {
+        if (offset <= 0 || offset > fileLength - 2) {
+            return null;
+        }
+        raf.seek(offset);
+        int numEntries = readUInt16(raf, bigEndian);
+        if (numEntries < 1 || numEntries > MAX_ENTRIES_PER_IFD
+                || offset + 2 + (long) numEntries * CLASSIC_ENTRY_SIZE > 
fileLength) {
+            return null;
+        }
+        byte[] table = new byte[numEntries * CLASSIC_ENTRY_SIZE];
+        raf.readFully(table);
+        return table;
     }
 
     /**
@@ -404,25 +827,34 @@ public class RawTiffParser extends TiffParser {
         return true;
     }
 
-    private boolean isBigTiff(TikaInputStream tis) throws IOException {
+    /**
+     * The magic number after the byte order marker, or -1 when the stream
+     * does not start with one.
+     */
+    private static int readMagic(TikaInputStream tis) throws IOException {
         tis.mark(4);
         try {
             byte[] header = new byte[4];
             if (IOUtils.read(tis, header) < 4) {
-                return false;
+                return -1;
             }
             if (header[0] == 'I' && header[1] == 'I') {
-                return header[2] == 0x2B && header[3] == 0;
+                return getUInt16(header, 2, false);
             }
             if (header[0] == 'M' && header[1] == 'M') {
-                return header[2] == 0 && header[3] == 0x2B;
+                return getUInt16(header, 2, true);
             }
-            return false;
+            return -1;
         } finally {
             tis.reset();
         }
     }
 
+    private static boolean isKnownMagic(int magic) {
+        return magic == MAGIC_TIFF || magic == MAGIC_BIG_TIFF || magic == 
MAGIC_OLYMPUS_RO
+                || magic == MAGIC_OLYMPUS_RS || magic == MAGIC_PANASONIC;
+    }
+
     private static void enqueue(Deque<Long> toVisit, long offset) {
         if (toVisit.size() < MAX_PENDING_IFDS) {
             toVisit.add(offset);
@@ -527,7 +959,23 @@ public class RawTiffParser extends TiffParser {
         return bigEndian ? (high << 32) | low : (low << 32) | high;
     }
 
-    private record Preview(long offset, long length) {
+    /**
+     * A JPEG inside the file. A patched one begins with a byte the camera
+     * wrote over the SOI, which is restored when it is handed on.
+     */
+    private record Preview(long offset, long length, boolean soiPatched) {
+        Preview(long offset, long length) {
+            this(offset, length, false);
+        }
+
+        long streamOffset() {
+            return soiPatched ? offset + 1 : offset;
+        }
+
+        long streamLength() {
+            return soiPatched ? length - 1 : length;
+        }
+
     }
 
     private static class TiffStructureException extends TikaException {
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
index 58cf342305..6bd56fd945 100644
--- 
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
@@ -17,6 +17,7 @@
 package org.apache.tika.detect.image;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
@@ -55,6 +56,12 @@ public class RawTiffDetectorTest {
             "testDNG.dng, image/x-raw-adobe",
             "testDNG_bigtiff.dng, image/x-raw-adobe",
             "testCR2.cr2, image/x-canon-cr2",
+            "testERF.erf, image/x-raw-epson",
+            "testERF_nopreview.erf, image/x-raw-epson",
+            "testORF.orf, image/x-raw-olympus",
+            "testORF_bigendian.orf, image/x-raw-olympus",
+            "testORF_omsystem.orf, image/x-raw-olympus",
+            "testRW2.rw2, image/x-raw-panasonic",
             "testTIFF.tif, image/tiff",
             "testJPEG.jpg, image/jpeg"})
     public void testDetectionWithoutName(String file, String expected) throws 
Exception {
@@ -100,6 +107,16 @@ public class RawTiffDetectorTest {
         assertEquals(MediaType.OCTET_STREAM, RawTiffDetector.detect(tiff, 
tiff.length));
     }
 
+    /**
+     * A declared type can only refine what was detected, so the registry has
+     * to know an ERF for a TIFF.
+     */
+    @Test
+    public void testEpsonIsATiff() throws Exception {
+        assertTrue(MimeTypes.getDefaultMimeTypes().getMediaTypeRegistry()
+                .isSpecializationOf(RawTiffDetector.EPSON, 
MediaType.image("tiff")));
+    }
+
     @Test
     public void testVendorCompressionWins() {
         byte[] tiff = tiff("NIKON CORPORATION", 34713, 2, false);
@@ -130,6 +147,25 @@ public class RawTiffDetectorTest {
         assertEquals(RawTiffDetector.NIKON, RawTiffDetector.detect(tiff, 
tiff.length));
     }
 
+    /**
+     * The ERF layout: an uncompressed RGB thumbnail in IFD0 carrying the
+     * Make, the CFA image in a SubIFD. Epson's compression code is a
+     * published TIFF value other writers use, so the Make decides, whether
+     * the sensor image is compressed or not.
+     */
+    @Test
+    public void testEpsonRaw() {
+        byte[] tiff = new TiffBuilder(false)
+                .ifd(entry(0x0103, 3, 1), entry(0x010F, "SEIKO EPSON CORP."), 
subIfds(1))
+                .ifd(entry(0x0103, 3, 32769), entry(0x0106, 3, 32803))
+                .build();
+        assertEquals(RawTiffDetector.EPSON, RawTiffDetector.detect(tiff, 
tiff.length));
+
+        byte[] uncompressed = tiff("SEIKO EPSON CORP.", 1, 32803, false);
+        assertEquals(RawTiffDetector.EPSON,
+                RawTiffDetector.detect(uncompressed, uncompressed.length));
+    }
+
     /**
      * BigTIFF: 8-byte counts, offsets and inline values, a LONG8 SubIFDs
      * entry, and the vendor code inside the SubIFD.
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/RawTiffParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/RawTiffParserTest.java
index a658d8622e..23f5c9f189 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/RawTiffParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/RawTiffParserTest.java
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Random;
 
 import org.junit.jupiter.api.Test;
 import org.xml.sax.helpers.DefaultHandler;
@@ -151,6 +152,171 @@ public class RawTiffParserTest extends TikaTest {
         assertThumbnail(metadataList.get(1), 64, 48);
     }
 
+    /**
+     * An ERF reaches this parser because the detector tells it by its Make
+     * (Epson's compression code is a published TIFF value other writers use)
+     * and the registry makes image/x-raw-epson a TIFF. Its
+     * preview hangs in the MakerNote under 0x0280, at an offset counted from
+     * the start of the file, and begins with a byte the camera wrote over
+     * the SOI: it only parses as a JPEG once that byte is put back.
+     */
+    @Test
+    public void testERF() throws Exception {
+        List<Metadata> metadataList = parseByName("testERF.erf");
+        assertEquals(2, metadataList.size());
+
+        Metadata container = metadataList.get(0);
+        assertEquals("image/x-raw-epson", 
container.get(HttpHeaders.CONTENT_TYPE));
+        assertEquals("SEIKO EPSON CORP.", container.get(TIFF.EQUIPMENT_MAKE));
+        
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
+
+        assertThumbnail(metadataList.get(1), 64, 48);
+    }
+
+    /**
+     * ORF keeps its previews in the MakerNote: the preview the CameraSettings
+     * sub-IFD points at, and the camera's own thumbnail.
+     */
+    @Test
+    public void testORF() throws Exception {
+        List<Metadata> metadataList = parseByName("testORF.orf");
+        assertEquals(3, metadataList.size());
+
+        Metadata container = metadataList.get(0);
+        assertEquals("image/x-raw-olympus", 
container.get(HttpHeaders.CONTENT_TYPE));
+        assertEquals("OLYMPUS CORPORATION", 
container.get(TIFF.EQUIPMENT_MAKE));
+        
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
+
+        assertThumbnail(metadataList.get(1), 64, 48);
+        assertInlinePreview(metadataList.get(2), 0, 32, 24);
+    }
+
+    /**
+     * A MakerNote carries its own byte order marker and does not have to
+     * share the file's: this one is a big endian "MMOR" whose note says
+     * "OLYMPUS\0II".
+     */
+    @Test
+    public void testORFBigEndian() throws Exception {
+        List<Metadata> metadataList = parseByName("testORF_bigendian.orf");
+        assertEquals(3, metadataList.size());
+
+        Metadata container = metadataList.get(0);
+        assertEquals("image/x-raw-olympus", 
container.get(HttpHeaders.CONTENT_TYPE));
+        
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
+
+        assertThumbnail(metadataList.get(1), 64, 48);
+        assertInlinePreview(metadataList.get(2), 0, 32, 24);
+    }
+
+    /**
+     * OM Digital Solutions bodies keep the Olympus layout behind a longer
+     * name: "OM SYSTEM\0\0\0", then the byte order marker and the version,
+     * and the IFD at 16, with every offset still counted from the MakerNote.
+     */
+    @Test
+    public void testORFOMSystem() throws Exception {
+        List<Metadata> metadataList = parseByName("testORF_omsystem.orf");
+        assertEquals(3, metadataList.size());
+
+        Metadata container = metadataList.get(0);
+        assertEquals("image/x-raw-olympus", 
container.get(HttpHeaders.CONTENT_TYPE));
+        assertEquals("OM Digital Solutions", 
container.get(TIFF.EQUIPMENT_MAKE));
+        assertEquals("OM-3", container.get(TIFF.EQUIPMENT_MODEL));
+        
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
+
+        assertThumbnail(metadataList.get(1), 64, 48);
+        assertInlinePreview(metadataList.get(2), 0, 32, 24);
+    }
+
+    /**
+     * RW2 keeps its preview in the private JpgFromRaw tag and writes
+     * placeholders into the strip tags, which otherwise read as a JPEG strip
+     * of no length: the private tag has to answer first.
+     */
+    @Test
+    public void testRW2() throws Exception {
+        List<Metadata> metadataList = parseByName("testRW2.rw2");
+        assertEquals(2, metadataList.size());
+
+        Metadata container = metadataList.get(0);
+        assertEquals("image/x-raw-panasonic", 
container.get(HttpHeaders.CONTENT_TYPE));
+        
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
+
+        assertThumbnail(metadataList.get(1), 64, 48);
+    }
+
+    /**
+     * A KDC points at its preview with the standard tags, so it only had to
+     * reach this parser. Its name is what types it: nothing in the file says
+     * raw to the detector.
+     */
+    @Test
+    public void testKDC() throws Exception {
+        List<Metadata> metadataList = parseByName("testKDC.kdc");
+        assertEquals(2, metadataList.size());
+
+        Metadata container = metadataList.get(0);
+        assertEquals("image/x-raw-kodak", 
container.get(HttpHeaders.CONTENT_TYPE));
+        assertEquals("EASTMAN KODAK COMPANY", 
container.get(TIFF.EQUIPMENT_MAKE));
+        
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
+
+        assertThumbnail(metadataList.get(1), 64, 48);
+    }
+
+    /**
+     * A MOS keeps its preview in the Leaf data block, in the section named
+     * JPEG_preview_data. The tag is typed ASCII, which the block is not.
+     */
+    @Test
+    public void testMOS() throws Exception {
+        List<Metadata> metadataList = parseByName("testMOS.mos");
+        assertEquals(2, metadataList.size());
+
+        Metadata container = metadataList.get(0);
+        assertEquals("image/x-raw-leaf", 
container.get(HttpHeaders.CONTENT_TYPE));
+        
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
+
+        assertThumbnail(metadataList.get(1), 64, 48);
+    }
+
+    /**
+     * A raw with nothing to hand on is typed and read all the same: this one
+     * carries a MakerNote that names no preview, and no thumbnail is an
+     * answer, not a failure.
+     */
+    @Test
+    public void testERFWithoutPreview() throws Exception {
+        List<Metadata> metadataList = parseByName("testERF_nopreview.erf");
+        assertEquals(1, metadataList.size());
+
+        Metadata container = metadataList.get(0);
+        assertEquals("image/x-raw-epson", 
container.get(HttpHeaders.CONTENT_TYPE));
+        assertEquals("SEIKO EPSON CORP.", container.get(TIFF.EQUIPMENT_MAKE));
+        
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
+    }
+
+    /**
+     * *.raw is also a disk image, PCM audio and more: when only the name
+     * says Panasonic, the parser reads nothing instead of failing.
+     */
+    @Test
+    public void testNonTiffByNameOnly() throws Exception {
+        byte[] bytes = new byte[4096];
+        new Random(42).nextBytes(bytes);
+        bytes[0] = 0;
+        Metadata metadata = new Metadata();
+        metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "disk.raw");
+        try (TikaInputStream tis = TikaInputStream.get(bytes)) {
+            List<Metadata> metadataList =
+                    getRecursiveMetadata(tis, metadata, new ParseContext(), 
false);
+            assertEquals(1, metadataList.size());
+            assertEquals("image/x-raw-panasonic",
+                    metadataList.get(0).get(HttpHeaders.CONTENT_TYPE));
+            
assertNull(metadataList.get(0).get(TikaCoreProperties.CONTAINER_EXCEPTION));
+        }
+    }
+
     @Test
     public void testCR2() throws Exception {
         List<Metadata> metadataList = parseByName("testCR2.cr2");
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testERF.erf
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testERF.erf
new file mode 100644
index 0000000000..a138b3b9a2
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testERF.erf
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testERF_nopreview.erf
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testERF_nopreview.erf
new file mode 100644
index 0000000000..ec22083ffb
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testERF_nopreview.erf
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testKDC.kdc
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testKDC.kdc
new file mode 100644
index 0000000000..6c93df5ec0
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testKDC.kdc
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testMOS.mos
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testMOS.mos
new file mode 100644
index 0000000000..42db8c019c
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testMOS.mos
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF.orf
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF.orf
new file mode 100644
index 0000000000..4ab0e58e89
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF.orf
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF_bigendian.orf
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF_bigendian.orf
new file mode 100644
index 0000000000..452faaf904
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF_bigendian.orf
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF_omsystem.orf
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF_omsystem.orf
new file mode 100644
index 0000000000..0c88be6e33
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testORF_omsystem.orf
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testRW2.rw2
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testRW2.rw2
new file mode 100644
index 0000000000..538596bbd5
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/test-documents/testRW2.rw2
 differ

Reply via email to