Copilot commented on code in PR #3037:
URL: https://github.com/apache/tika/pull/3037#discussion_r3803499821


##########
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,362 @@
+/*
+ * 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.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+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;
+    //previews are camera-generated JPEGs, tens of MB is already generous
+    private static final long MAX_PREVIEW_LENGTH_BYTES = 100 * 1024 * 1024;
+
+    private RawTiffParserConfig defaultConfig = new RawTiffParserConfig();
+
+    public RawTiffParser() {
+    }
+
+    public RawTiffParser(RawTiffParserConfig config) {
+        this.defaultConfig = config;
+    }

Review Comment:
   `defaultConfig` is mutable, non-final, and is assigned directly from the 
caller-provided `RawTiffParserConfig`. If the caller retains and mutates that 
config instance while parses are running, behavior can change mid-parse and 
introduce thread-safety issues. Consider copying the needed values into an 
immutable/final field (e.g., store a final boolean), or defensively copying the 
config object in the constructor.



##########
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,362 @@
+/*
+ * 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.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+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;
+    //previews are camera-generated JPEGs, tens of MB is already generous
+    private static final long MAX_PREVIEW_LENGTH_BYTES = 100 * 1024 * 1024;
+
+    private RawTiffParserConfig defaultConfig = new RawTiffParserConfig();
+
+    public RawTiffParser() {
+    }
+
+    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 {
+        tis.getFile();
+        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<long[]> previews;
+        try (RandomAccessFile raf = new RandomAccessFile(tis.getFile(), "r")) {
+            previews = locateJpegPreviews(raf);
+            if (previews.isEmpty()) {
+                return;
+            }
+            EmbeddedDocumentExtractor extractor =
+                    EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+            int count = 0;
+            for (long[] 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;
+                }
+                byte[] data = new byte[(int) preview[1]];
+                raf.seek(preview[0]);
+                raf.readFully(data);
+                try (TikaInputStream previewStream = 
TikaInputStream.get(data)) {
+                    extractor.parseEmbedded(previewStream, xhtml, 
previewMetadata, context, true);
+                }

Review Comment:
   This reads each preview fully into a byte[] before parsing. Since previews 
can be large (up to 100MB by current cap) and multiple previews can exist per 
file, this can create significant heap pressure and increase OOM risk under 
concurrent parsing. Prefer streaming from the underlying file with a 
bounded/offset-limited stream (e.g., seek to the offset and wrap a stream 
limited to `jpegLength`) so extraction does not require loading the entire 
preview into memory.



##########
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,362 @@
+/*
+ * 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.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+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;
+    //previews are camera-generated JPEGs, tens of MB is already generous
+    private static final long MAX_PREVIEW_LENGTH_BYTES = 100 * 1024 * 1024;
+
+    private RawTiffParserConfig defaultConfig = new RawTiffParserConfig();
+
+    public RawTiffParser() {
+    }
+
+    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 {
+        tis.getFile();
+        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<long[]> previews;
+        try (RandomAccessFile raf = new RandomAccessFile(tis.getFile(), "r")) {
+            previews = locateJpegPreviews(raf);
+            if (previews.isEmpty()) {
+                return;
+            }
+            EmbeddedDocumentExtractor extractor =
+                    EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+            int count = 0;
+            for (long[] 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;
+                }
+                byte[] data = new byte[(int) preview[1]];
+                raf.seek(preview[0]);
+                raf.readFully(data);
+                try (TikaInputStream previewStream = 
TikaInputStream.get(data)) {
+                    extractor.parseEmbedded(previewStream, xhtml, 
previewMetadata, context, true);
+                }
+            }
+        } catch (TiffStructureException e) {
+            EmbeddedDocumentUtil.recordException(e, metadata);
+        }
+    }
+
+    /**
+     * Walks the TIFF IFD chain plus one level of SubIFDs and returns
+     * {offset, length} pairs of embedded JPEG previews.
+     */
+    private List<long[]> 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");
+        }
+
+        List<long[]> previews = new ArrayList<>();
+        Set<Long> visited = new HashSet<>();
+        List<Long> toVisit = new ArrayList<>();
+        toVisit.add(readUInt32(raf, bigEndian));
+
+        while (!toVisit.isEmpty() && visited.size() < MAX_IFDS) {
+            long ifdOffset = toVisit.remove(0);

Review Comment:
   `toVisit.remove(0)` on an `ArrayList` is O(n) due to shifting elements. Even 
though `MAX_IFDS` is currently small, switching `toVisit` to a queue structure 
(e.g., `ArrayDeque`) avoids the quadratic pattern and makes the intent (FIFO 
traversal) clearer.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to