Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
Release 4.1.0 - ???

* RawTiffParser extracts the camera-generated JPEG previews embedded in
TIFF-based raw images (Nikon NEF/NRW, Sony ARW/SRF/SR2, Pentax PEF/PTX,
Adobe DNG and Canon CR2, including BigTIFF DNG containers) as thumbnail
embedded documents. image/x-raw-{nikon,sony,pentax,adobe} are now
sub-classes of image/tiff, so a named NEF/ARW/PEF/DNG that used to detect
as image/tiff (TiffParser, metadata only) now detects as image/x-raw-* and
emits thumbnail-N.jpg attachments in /rmeta and /unpack; CR2 keeps its
detection but also gains the attachments. Disable via
"raw-tiff-parser": {"extractPreviews": false} (TIKA-4824).


Release 4.0.0 - 8/18/2026

This section is the complete delta from 3.x. It includes everything first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7164,6 +7164,7 @@
<mime-type type="image/x-raw-adobe">
<acronym>DNG</acronym>
<_comment>Adobe Digital Negative</_comment>
<sub-class-of type="image/tiff" />
<glob pattern="*.dng"/>
</mime-type>

Expand Down Expand Up @@ -7233,6 +7234,7 @@

<mime-type type="image/x-raw-nikon">
<_comment>Nikon raw image</_comment>
<sub-class-of type="image/tiff" />
<glob pattern="*.nef"/>
<glob pattern="*.nrw"/>
</mime-type>
Expand All @@ -7247,12 +7249,14 @@

<mime-type type="image/x-raw-pentax">
<_comment>Pentax raw image</_comment>
<sub-class-of type="image/tiff" />
<glob pattern="*.ptx"/>
<glob pattern="*.pef"/>
</mime-type>

<mime-type type="image/x-raw-sony">
<_comment>Sony raw image</_comment>
<sub-class-of type="image/tiff" />
<glob pattern="*.arw"/>
<glob pattern="*.srf"/>
<glob pattern="*.sr2"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,19 @@ public void testRawDetection() throws Exception {
assertTypeByName("image/x-raw-minolta", "x.mrw");
assertTypeByName("image/x-raw-nikon", "x.nef");
assertTypeByName("image/x-raw-nikon", "x.nrw");
assertTypeByData("image/tiff", "testNEF.nef");
assertTypeByNameAndData("image/x-raw-nikon", "testNEF.nef");
assertTypeByData("image/tiff", "testARW.arw");
assertTypeByNameAndData("image/x-raw-sony", "testARW.arw");
assertTypeByData("image/tiff", "testPEF.pef");
assertTypeByNameAndData("image/x-raw-pentax", "testPEF.pef");
assertTypeByData("image/tiff", "testDNG.dng");
assertTypeByNameAndData("image/x-raw-adobe", "testDNG.dng");
//BigTIFF has no image/tiff data magic here (TiffParser can't read BigTIFF yet),
//so a BigTIFF DNG resolves by name+data via the glob, not by data alone
assertTypeByNameAndData("image/x-raw-adobe", "testDNG_bigtiff.dng");
assertTypeByData("image/x-canon-cr2", "testCR2.cr2");
assertTypeByNameAndData("image/x-canon-cr2", "testCR2.cr2");
assertTypeByName("image/x-raw-olympus", "x.orf");
assertTypeByName("image/x-raw-pentax", "x.ptx");
assertTypeByName("image/x-raw-pentax", "x.pef");
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.parser.image;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.xml.sax.helpers.DefaultHandler;

import org.apache.tika.TikaTest;
import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TIFF;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;

public class RawTiffParserTest extends TikaTest {

private List<Metadata> parseByName(String fileName) throws Exception {
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, fileName);
return getRecursiveMetadata(fileName, metadata);
}

private void assertPreview(Metadata preview, int index, int width, int height) {
assertEquals("image/jpeg", preview.get(HttpHeaders.CONTENT_TYPE));
assertEquals("thumbnail-" + index + ".jpg",
preview.get(TikaCoreProperties.RESOURCE_NAME_KEY));
assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(),
preview.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
assertEquals(Integer.toString(width), preview.get(TIFF.IMAGE_WIDTH));
assertEquals(Integer.toString(height), preview.get(TIFF.IMAGE_LENGTH));
}

@Test
public void testNEF() throws Exception {
List<Metadata> metadataList = parseByName("testNEF.nef");
assertEquals(2, metadataList.size());

Metadata container = metadataList.get(0);
assertEquals("image/x-raw-nikon", container.get(HttpHeaders.CONTENT_TYPE));
assertEquals("NIKON CORPORATION", container.get(TIFF.EQUIPMENT_MAKE));
assertEquals("NIKON D3000", container.get(TIFF.EQUIPMENT_MODEL));

assertPreview(metadataList.get(1), 0, 64, 48);
}

@Test
public void testDuplicatePreviewOffsetsDeduplicated() throws Exception {
//IFD0 and IFD1 both point their JPEGInterchangeFormat at the same region;
//it is extracted once, not per referencing IFD
List<Metadata> metadataList = parseByName("testNEF_dup.nef");
assertEquals(2, metadataList.size());
assertPreview(metadataList.get(1), 0, 64, 48);
}

@Test
public void testARW() throws Exception {
List<Metadata> metadataList = parseByName("testARW.arw");
assertEquals(3, metadataList.size());

Metadata container = metadataList.get(0);
assertEquals("image/x-raw-sony", container.get(HttpHeaders.CONTENT_TYPE));
assertEquals("SONY", container.get(TIFF.EQUIPMENT_MAKE));
assertEquals("NEX-6", container.get(TIFF.EQUIPMENT_MODEL));

//full-size preview from IFD0, then the thumbnail from IFD1
assertPreview(metadataList.get(1), 0, 64, 48);
assertPreview(metadataList.get(2), 1, 32, 24);
}

@Test
public void testPEF() throws Exception {
List<Metadata> metadataList = parseByName("testPEF.pef");
assertEquals(3, metadataList.size());

Metadata container = metadataList.get(0);
assertEquals("image/x-raw-pentax", container.get(HttpHeaders.CONTENT_TYPE));
assertEquals("PENTAX K-7", container.get(TIFF.EQUIPMENT_MODEL));

//thumbnail from IFD1, then the full-size preview from IFD2
assertPreview(metadataList.get(1), 0, 32, 24);
assertPreview(metadataList.get(2), 1, 64, 48);
}

@Test
public void testDNG() throws Exception {
List<Metadata> metadataList = parseByName("testDNG.dng");
//the raw strip in SubIFD0 also starts with a JPEG SOI marker
//(lossless JPEG) and must not be extracted
assertEquals(2, metadataList.size());

Metadata container = metadataList.get(0);
assertEquals("image/x-raw-adobe", container.get(HttpHeaders.CONTENT_TYPE));
assertEquals("PENTAX K-x", container.get(TIFF.EQUIPMENT_MODEL));

assertPreview(metadataList.get(1), 0, 64, 48);
}

@Test
public void testCR2() throws Exception {
List<Metadata> metadataList = parseByName("testCR2.cr2");
//the lossless-JPEG raw strip in the last IFD must not be extracted
assertEquals(3, metadataList.size());

Metadata container = metadataList.get(0);
assertEquals("image/x-canon-cr2", container.get(HttpHeaders.CONTENT_TYPE));
assertEquals("Canon EOS 7D", container.get(TIFF.EQUIPMENT_MODEL));

//full-size preview stored as a strip in IFD0, thumbnail from IFD1
assertPreview(metadataList.get(1), 0, 64, 48);
assertPreview(metadataList.get(2), 1, 32, 24);
}

@Test
public void testParsedByRawTiffParser() throws Exception {
List<Metadata> metadataList = parseByName("testNEF.nef");
List<String> parsedBy =
Arrays.asList(metadataList.get(0).getValues(TikaCoreProperties.TIKA_PARSED_BY));
assertContains(RawTiffParser.class.getName(), parsedBy);
}

@Test
public void testBigTiffDNG() throws Exception {
//DNG allows BigTIFF containers since spec version 1.7
List<Metadata> metadataList = parseByName("testDNG_bigtiff.dng");
assertEquals(2, metadataList.size());

Metadata container = metadataList.get(0);
assertEquals("image/x-raw-adobe", container.get(HttpHeaders.CONTENT_TYPE));
assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));

assertPreview(metadataList.get(1), 0, 64, 48);
}

@Test
public void testEmptyBigTiffWithoutException() throws Exception {
//a BigTIFF file without any previews must parse quietly,
//without recording an exception
byte[] bigTiff = new byte[]{
'M', 'M', 0, 0x2B, 0, 8, 0, 0,
0, 0, 0, 0, 0, 0, 0, 16, //offset of IFD0
0, 0, 0, 0, 0, 0, 0, 0, //IFD0: zero entries
0, 0, 0, 0, 0, 0, 0, 0 //no next IFD
};
Metadata metadata = new Metadata();
metadata.set(HttpHeaders.CONTENT_TYPE, "image/x-raw-adobe");
try (TikaInputStream tis = TikaInputStream.get(bigTiff)) {
new RawTiffParser().parse(tis, new DefaultHandler(), metadata, new ParseContext());
}
assertNull(metadata.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
}

@Test
public void testMaxPreviewLength() throws Exception {
Parser parser = TikaLoader
.load(getConfigPath(RawTiffParserTest.class,
"tika-config-raw-preview-max-length.json"))
.loadParsers();
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "testNEF.nef");
metadata.set(HttpHeaders.CONTENT_TYPE, "image/x-raw-nikon");
List<Metadata> metadataList =
getRecursiveMetadata("testNEF.nef", parser, metadata, new ParseContext(), false);

//the preview is larger than the configured limit and is skipped
assertEquals(1, metadataList.size());
}

@Test
public void testMaxTotalPreviewBytes() throws Exception {
//testARW has two previews (676 + 644 bytes); a 700-byte total budget
//admits only the first, bounding aggregate extraction work
Parser parser = TikaLoader
.load(getConfigPath(RawTiffParserTest.class,
"tika-config-raw-total-preview-budget.json"))
.loadParsers();
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "testARW.arw");
metadata.set(HttpHeaders.CONTENT_TYPE, "image/x-raw-sony");
List<Metadata> metadataList =
getRecursiveMetadata("testARW.arw", parser, metadata, new ParseContext(), false);

assertEquals(2, metadataList.size());
assertPreview(metadataList.get(1), 0, 64, 48);
}

@Test
public void testExtractPreviewsDisabled() throws Exception {
Parser parser = TikaLoader
.load(getConfigPath(RawTiffParserTest.class, "tika-config-raw-previews-off.json"))
.loadParsers();
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "testNEF.nef");
metadata.set(HttpHeaders.CONTENT_TYPE, "image/x-raw-nikon");
List<Metadata> metadataList =
getRecursiveMetadata("testNEF.nef", parser, metadata, new ParseContext(), false);

assertEquals(1, metadataList.size());
assertEquals("NIKON CORPORATION", metadataList.get(0).get(TIFF.EQUIPMENT_MAKE));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"parsers": [
"default-parser",
{
"raw-tiff-parser": {
"maxPreviewLengthBytes": 100
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"parsers": [
"default-parser",
{
"raw-tiff-parser": {
"extractPreviews": false
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"parsers": [
"default-parser",
{
"raw-tiff-parser": {
"maxTotalPreviewBytes": 700
}
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading