[
https://issues.apache.org/jira/browse/TIKA-4824?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18105772#comment-18105772
]
ASF GitHub Bot commented on TIKA-4824:
--------------------------------------
Copilot commented on code in PR #3037:
URL: https://github.com/apache/tika/pull/3037#discussion_r3809984521
##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java:
##########
@@ -0,0 +1,396 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.BoundedInputStream;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+/**
+ * Parser for TIFF-based camera raw images: Nikon NEF/NRW, Sony ARW/SRF/SR2,
+ * Pentax PEF/PTX, Adobe DNG and Canon CR2.
+ * <p>
+ * These formats are TIFF containers: metadata extraction is inherited from
+ * {@link TiffParser}. In addition, this parser extracts the camera-generated
+ * JPEG preview images embedded in the raw file and hands them to the
+ * {@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
+ * 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).
+ */
+@TikaComponent
+public class RawTiffParser extends TiffParser {
+
+ /**
+ * Serial version UID
+ */
+ private static final long serialVersionUID = 5385105345533384662L;
+
+ private static final Set<MediaType> SUPPORTED_TYPES =
Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(
+ MediaType.image("x-raw-nikon"),
+ MediaType.image("x-raw-sony"),
+ MediaType.image("x-raw-pentax"),
+ MediaType.image("x-raw-adobe"),
+ MediaType.image("x-canon-cr2"))));
+
+ private static final String JPEG_MIME = "image/jpeg";
+
+ private static final int TAG_BITS_PER_SAMPLE = 0x0102;
+ private static final int TAG_COMPRESSION = 0x0103;
+ private static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106;
+ private static final int TAG_STRIP_OFFSETS = 0x0111;
+ private static final int TAG_STRIP_BYTE_COUNTS = 0x0117;
+ 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 COMPRESSION_OLD_JPEG = 6;
+ private static final int COMPRESSION_JPEG = 7;
+ private static final int PHOTOMETRIC_RGB = 2;
+ private static final int PHOTOMETRIC_YCBCR = 6;
+
+ private static final int MAX_IFDS = 32;
+ 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
+ private static final int MAX_PENDING_IFDS = 1024;
+ //previews are camera-generated JPEGs, tens of MB is already generous
+ private static final long DEFAULT_MAX_PREVIEW_LENGTH_BYTES = 100 * 1024 *
1024;
+
+ private final RawTiffParserConfig defaultConfig;
+
+ public RawTiffParser() {
+ this(new RawTiffParserConfig());
+ }
+
+ public RawTiffParser(RawTiffParserConfig config) {
+ this.defaultConfig = config;
+ }
+
+ public RawTiffParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig,
RawTiffParserConfig.class));
+ }
+
+ @Override
+ public Set<MediaType> getSupportedTypes(ParseContext context) {
+ return SUPPORTED_TYPES;
+ }
+
+ @Override
+ public void parse(TikaInputStream tis, ContentHandler handler, Metadata
metadata,
+ ParseContext context) throws IOException, SAXException,
TikaException {
+ extractMetadata(tis, handler, metadata, context);
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata,
context);
+ xhtml.startDocument();
+ if (defaultConfig.isExtractPreviews()) {
+ extractPreviews(tis, xhtml, metadata, context);
+ }
+ xhtml.endDocument();
+ }
+
+ private void extractPreviews(TikaInputStream tis, XHTMLContentHandler
xhtml, Metadata metadata,
+ ParseContext context) throws IOException,
SAXException {
+ List<Preview> previews;
+ try (RandomAccessFile raf = new RandomAccessFile(tis.getFile(), "r")) {
+ previews = locateJpegPreviews(raf);
+ } catch (TiffStructureException e) {
+ EmbeddedDocumentUtil.recordException(e, metadata);
+ return;
+ }
+ if (previews.isEmpty()) {
+ return;
+ }
+ EmbeddedDocumentExtractor extractor =
+ EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+ int count = 0;
+ for (Preview preview : previews) {
+ Metadata previewMetadata = Metadata.newInstance(context);
+ previewMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+
TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString());
+ previewMetadata.set(HttpHeaders.CONTENT_TYPE, JPEG_MIME);
+ EmbeddedDocumentUtil.setGeneratedResourceName(previewMetadata,
+ EmbeddedDocumentUtil.EmbeddedResourcePrefix.THUMBNAIL,
count, JPEG_MIME);
+ count++;
+ if (!extractor.shouldParseEmbedded(previewMetadata, context)) {
+ continue;
+ }
+ //stream the preview region instead of loading it onto the heap
+ try (InputStream fileStream = Files.newInputStream(tis.getPath()))
{
+ IOUtils.skipFully(fileStream, preview.offset());
+ BoundedInputStream bounded = BoundedInputStream.builder()
+ .setInputStream(fileStream)
+ .setMaxCount(preview.length())
+ .get();
+ try (TikaInputStream previewStream =
TikaInputStream.get(bounded)) {
+ extractor.parseEmbedded(previewStream, xhtml,
previewMetadata, context, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Walks the TIFF IFD chain and any SubIFDs (traversal bounded by
+ * {@link #MAX_IFDS}) and returns the embedded JPEG previews.
+ */
+ private List<Preview> locateJpegPreviews(RandomAccessFile raf)
+ throws IOException, TiffStructureException {
+ long fileLength = raf.length();
+ if (fileLength < 8) {
+ throw new TiffStructureException("file too short for a TIFF
header");
+ }
+ raf.seek(0);
+ int b0 = raf.read();
+ int b1 = raf.read();
+ boolean bigEndian;
+ if (b0 == 'M' && b1 == 'M') {
+ bigEndian = true;
+ } else if (b0 == 'I' && b1 == 'I') {
+ bigEndian = false;
+ } else {
+ throw new TiffStructureException("not a TIFF byte order marker");
+ }
+ if (readUInt16(raf, bigEndian) != 42) {
+ throw new TiffStructureException("bad TIFF magic number");
+ }
Review Comment:
RawTiffParser’s preview locator rejects valid BigTIFF headers (magic 43 /
0x002B) by throwing a TiffStructureException. This is observable for CR2
because tika-mimetypes.xml explicitly matches `MM\x00\x2b` (BigTIFF) for
image/x-canon-cr2, so RawTiffParser can be selected for a BigTIFF CR2 but will
always record an exception and skip preview extraction. At minimum, treat
BigTIFF as a supported header and return no previews without recording an
exception (or ideally add full BigTIFF IFD parsing).
> Extract embedded JPEG previews from TIFF-based raw images (NEF, ARW, PEF,
> DNG, CR2)
> -----------------------------------------------------------------------------------
>
> Key: TIKA-4824
> URL: https://issues.apache.org/jira/browse/TIKA-4824
> Project: Tika
> Issue Type: New Feature
> Reporter: Dominik Schmidt
> Priority: Major
>
> TIFF-based camera raw files embed camera-generated JPEG previews, often at
> full resolution. Tika currently gives no access to them: the x-raw-* types
> are glob-only (not sub-classes of image/tiff), no parser claims them, and
> nothing extracts the preview bytes. For consumers that cannot decode raw
> sensor data, the embedded JPEG is the only practical way to render a raw file
> (analogous to TIKA-4801 for audio cover art).
> The previews are stored in two ways:
> - Nikon NEF/NRW, Sony ARW/SRF/SR2, Pentax PEF/PTX:
> JPEGInterchangeFormat/-Length tags in the IFD chain or in SubIFDs
> - Adobe DNG, Canon CR2: a single JPEG-compressed strip (Compression 6/7)
> Proposal:
> - New RawTiffParser in tika-parser-image-module claiming
> image/x-raw-{nikon,sony,pentax,adobe} and image/x-canon-cr2: TIFF/EXIF/XMP
> metadata via TiffParser, walks the IFD chain plus SubIFDs and emits each
> preview through the EmbeddedDocumentExtractor (image/jpeg, THUMBNAIL);
> configurable via "raw-tiff-parser": {"extractPreviews": false}
> - Strip candidates need a safety rule: raw sensor data in CR2/DNG is lossless
> JPEG and also starts with an SOI marker. Strips are only accepted for
> displayable images (PhotometricInterpretation RGB/YCbCr, or 8 bits per sample
> when it is absent, as in CR2's preview IFD)
> - Make image/x-raw-{nikon,sony,pentax,adobe} sub-classes of image/tiff so
> name+data detection resolves them (they have no reliable magic; data-only
> detection stays image/tiff)
> - Synthetic ~2KB test fixtures mirroring the real IFD layouts, unit and
> detection tests
> Evaluated and excluded: Epson ERF (preview lives in the MakerNote), Olympus
> ORF and Panasonic RW2 (non-standard TIFF magic), Fuji RAF (not TIFF), Canon
> CR3 (ISO-BMFF). Verified against CC0 samples from raw.pixls.us (NEX-6,
> DSC-R1, K-7, EOS 7D, K-x and GR DNGs) plus real Nikon D80/D3000 files.
> PR to follow.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)