[ 
https://issues.apache.org/jira/browse/TIKA-4869?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18110723#comment-18110723
 ] 

ASF GitHub Bot commented on TIKA-4869:
--------------------------------------

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


##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/MotionPhoto.java:
##########
@@ -0,0 +1,299 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+
+import org.xml.sax.SAXException;
+
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Google;
+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.EmbeddedContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+/**
+ * The video of a motion photo, appended after the image and described by the
+ * XMP the image parsers already extract (TIKA-4869):
+ * <ul>
+ *   <li>a Motion Photo lists its parts in {@code Container:Directory}: the
+ *       primary image first, the video last with nothing after it, each with
+ *       an {@code Item:Length};</li>
+ *   <li>the older MicroVideo gives {@code Camera:MicroVideoOffset}, the
+ *       number of bytes from the end of the file to the start of the 
video.</li>
+ * </ul>
+ * Either way the video ends at the end of the file, so its start follows from
+ * its length. What is found there is typed by content: the declared
+ * {@code Item:Mime} is not used as a detection hint, because a hint would
+ * make a wrong length pass as a video, and nothing is emitted when detection
+ * recognizes nothing.
+ * <p>
+ * The same holds for HEIC motion photos, whose video sits in a trailing
+ * {@code mpvd} box; its 8 byte header is the primary item's padding, so the
+ * video still ends at the end of the file.
+ */
+final class MotionPhoto {
+
+    /**
+     * The name the video is emitted under, with the extension of whatever it
+     * turns out to be.
+     */
+    private static final String NAME = "motion-photo";
+
+    private static final String ITEM = "]/Container:Item/";
+    private static final String DIRECTORY = "xmp-raw:Container:Directory[";
+
+    /**
+     * The {@code Item:Semantic} of the video.
+     */
+    private static final String MOTION_PHOTO = "MotionPhoto";
+
+    /**
+     * A directory holds a handful of items; this only bounds the walk.
+     */
+    private static final int MAX_ITEMS = 64;
+
+    /**
+     * Enough of the video for the detectors to recognize it.
+     */
+    private static final int DETECTION_PREFIX = 8 * 1024;
+
+    /**
+     * The branch below an emitted trailer, which is not searched for a trailer
+     * of its own: what is appended to an image may be an image again, and a
+     * crafted file can nest that as deep as it likes.
+     */
+    private static final class Nested {
+    }
+
+    private static final Nested NESTED = new Nested();
+
+    private MotionPhoto() {
+    }
+
+    /**
+     * Emits the trailer as an embedded document, or nothing when the image
+     * declares none, when the declared length does not fit the file, or when
+     * the bytes there are not recognized. The last two are what sharing a
+     * motion photo out of a gallery leaves behind, a common enough thing that
+     * it is not worth an exception on a file that is otherwise fine; the XMP
+     * that promised the video is in the metadata for a client to see.
+     */
+    static void extract(TikaInputStream tis, Metadata metadata, 
XHTMLContentHandler xhtml,
+                        ParseContext context) throws IOException, SAXException 
{
+        if (context.get(Nested.class) != null) {
+            return;
+        }
+        Declaration declared = declaration(metadata);
+        if (declared == null) {
+            return;
+        }
+        //a length the file cannot hold is settled from what the stream already
+        //knows, before an image gets spilled to disk on the strength of it
+        if (tis.hasLength() && declared.length >= tis.getLength()) {
+            return;
+        }
+        Trailer trailer = locate(tis, declared, context);
+        if (trailer == null) {
+            return;
+        }
+        Metadata trailerMetadata = Metadata.newInstance(context);
+        //the name has to be set before the parse, and the declaration names 
the
+        //format the file was written with, which detection cannot always tell
+        //apart: an MP4 with the isom brand types as quicktime (TIKA-3646), and
+        //the MicroVideo format declares nothing at all. Where the two disagree
+        //about the kind of file it is, the bytes win.
+        boolean fromDetection = declared.mime != null
+                && !declared.mime.startsWith(trailer.type.getType() + "/");
+        String extension = declared.mime == null ? "" : EmbeddedDocumentUtil
+                .getExtensionForMediaType(
+                        fromDetection ? trailer.type.toString() : 
declared.mime);
+        trailerMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, NAME + 
extension);
+        if (fromDetection && !extension.isEmpty()) {
+            
trailerMetadata.set(TikaCoreProperties.RESOURCE_NAME_EXTENSION_INFERRED, true);
+        }
+        trailerMetadata.set(HttpHeaders.CONTENT_TYPE, trailer.type.toString());
+        trailerMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+                TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.name());
+        EmbeddedDocumentExtractor extractor =
+                EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+        if (!extractor.shouldParseEmbedded(trailerMetadata, context)) {
+            return;
+        }
+        InputStream bytes = open(trailer);
+        if (bytes == null) {
+            return;
+        }
+        context.set(Nested.class, NESTED);
+        try (TikaInputStream embedded = TikaInputStream.get(bytes)) {
+            extractor.parseEmbedded(embedded, new 
EmbeddedContentHandler(xhtml), trailerMetadata,
+                    context, true);
+        } finally {
+            context.set(Nested.class, null);
+        }
+    }
+
+    /**
+     * What the declaration points at: where the bytes start and what they turn
+     * out to be, or null when they are not there, are not recognized, or
+     * cannot be read. An image that parsed is not failed over a trailer that
+     * is out of reach.
+     */
+    private static Trailer locate(TikaInputStream tis, Declaration declared,
+                                  ParseContext context) {
+        try {
+            Path file = tis.getPath();
+            long start = Files.size(file) - declared.length;
+            if (start <= 0) {
+                return null;
+            }
+            MediaType type = detect(file, start, context);
+            if (type == null || MediaType.OCTET_STREAM.equals(type)) {
+                return null;
+            }

Review Comment:
   `tis.getPath()` can be `null` for non-file-backed streams (or depending on 
how the `TikaInputStream` was constructed). If it’s null here, 
`Files.size(file)` will throw a `NullPointerException` and fail the whole image 
parse. Handle this explicitly (e.g., return `null` when `file == null`, or 
materialize/spool to a temp file before calling `Files.size(...)`).



##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-xmp-commons/src/main/java/org/apache/tika/parser/xmp/XmpSaxFlattener.java:
##########
@@ -77,6 +79,26 @@ static boolean isContainer(String u, String l) {
             return RDF.equals(u) && (l.equals("Bag") || l.equals("Seq") || 
l.equals("Alt"));
         }
 
+        /**
+         * The prefix a namespace is normally written with, for the few whose
+         * raw keys are read elsewhere in Tika. An XMP prefix is the writer's
+         * own choice (ISO 16684-1) and rewriting a packet is enough to change
+         * it, so a key built from the document's prefix moves with it: a
+         * motion photo that has been through exiftool lists its parts under
+         * GContainer:Directory rather than Container:Directory.
+         */
+        static final Map<String, String> CANONICAL_PREFIX =
+                Map.of(Google.CONTAINER_NS, "Container", Google.ITEM_NS, 
"Item");
+
+        /**
+         * The name a path segment is keyed under: the document's own qName,
+         * except for the namespaces above.
+         */
+        static String canonical(String uri, String qName, String localName) {
+            String prefix = CANONICAL_PREFIX.get(uri);
+            return prefix == null ? qName : prefix + ":" + localName;

Review Comment:
   If `localName` is empty (which can happen depending on SAX parser 
configuration/behavior), this will produce malformed path segments like 
`Container:` / `Item:` and break lookups that rely on these keys. Safer 
behavior is to fall back to `qName` when `localName` is null/empty, even when 
the namespace URI is one of the canonicalized ones.





> Emit the video of a motion photo as an embedded document
> --------------------------------------------------------
>
>                 Key: TIKA-4869
>                 URL: https://issues.apache.org/jira/browse/TIKA-4869
>             Project: Tika
>          Issue Type: Improvement
>            Reporter: Dominik Schmidt
>            Priority: Major
>
> Tika exposes the Motion Photo and MicroVideo XMP of Google/Android motion 
> photos, but not the video itself, although both formats say where it is and 
> every byte of it is already in the file.
> The MP4 is appended after the JPEG:
> - Motion Photo (Camera:MotionPhoto): Container:Directory lists the items in 
> file order, the primary image first, the rest tightly packed after it, each 
> with an Item:Length and an optional Item:Padding. The video starts at 
> fileLength minus the lengths of the items from the video to the end; in 
> practice it is the last one, also in Ultra HDR files, where the spec puts the 
> gain map before it.
> - MicroVideo (the older Camera:MicroVideo / GCamera): Camera:MicroVideoOffset 
> is the number of bytes from the end of the file, so the video starts at 
> fileLength minus that value.
> Proposal: the image parsers emit those bytes as an embedded document, 
> ATTACHMENT, named motion-photo.<ext>. What is at the computed offset is 
> detected by content, without the declared Item:Mime as a hint (a hint would 
> make a wrong length pass as video/mp4 anyway), and nothing is emitted when 
> detection finds no type: a wrong or hostile length then yields no embedded 
> document rather than a bogus one. Extraction follows the usual embedded 
> document limits; a client that does not want the video filters by type as 
> usual.
> The same computation covers HEIC and AVIF motion photos, whose XMP goes 
> through the same extractor: there the video sits in an mpvd box, but that box 
> is last and its 8 byte header is the primary item's Padding, so the video 
> still ends at the end of the file. Putting the extraction in the shared image 
> parser base rather than in JpegParser therefore covers them too, untested for 
> lack of a sample file.
> Apple Live Photos are a different thing and need nothing here: their video is 
> a separate .MOV, paired by a content identifier, so a HEIC alone carries no 
> video.
> The existing test files (testJPEG_MotionPhoto.jpg, testJPEG_MicroVideo.jpg) 
> declare a video in their XMP but do not contain one; new fixtures with a 
> small real MP4 appended are needed.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to