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