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
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ Release 4.0.0-beta-1 - 6/29/2026
false, so an out-of-the-box tika-grpc server no longer accepts per-request
configuration or runtime store mutations (TIKA-4764).

* Unified XMP extraction across containers; adds HEIF/HEIC and WebP XMP,
including Samsung/Google Motion Photo (TIKA-4775).

OTHER CHANGES

* Release artifacts are now channel-specific. Maven Central gets slim
Expand Down
2 changes: 0 additions & 2 deletions tika-bundles/tika-bundle-standard/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,6 @@
pdfbox|
pdfbox-tools|
fontbox|
jempbox|
xmpbox|
bcjmail-jdk18on|
bcprov-jdk18on|
bcpkix-jdk18on|
Expand Down
52 changes: 52 additions & 0 deletions tika-core/src/main/java/org/apache/tika/metadata/Google.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.metadata;

/**
* Properties from the Google Photos XMP namespaces used by Motion Photos and the legacy
* MicroVideo format. See
* <a href="https://developer.android.com/media/platform/motion-photo-format">the Motion Photo
* format</a>.
* <p>
* Files use the {@code Camera} (current) or {@code GCamera} (legacy) prefix, but the namespace
* URI is the same; the image XMP handler maps by URI, so these keys are stable regardless of
* prefix.
*/
public interface Google {

String CAMERA_NS = "http://ns.google.com/photos/1.0/camera/";

String CONTAINER_NS = "http://ns.google.com/photos/1.0/container/";

String ITEM_NS = "http://ns.google.com/photos/1.0/container/item/";

Property MOTION_PHOTO = Property.externalText("Camera:MotionPhoto");

Property MOTION_PHOTO_VERSION = Property.externalText("Camera:MotionPhotoVersion");

Property MOTION_PHOTO_PRESENTATION_TIMESTAMP_US =
Property.externalText("Camera:MotionPhotoPresentationTimestampUs");

Property MICRO_VIDEO = Property.externalText("Camera:MicroVideo");

Property MICRO_VIDEO_VERSION = Property.externalText("Camera:MicroVideoVersion");

Property MICRO_VIDEO_OFFSET = Property.externalText("Camera:MicroVideoOffset");

Property MICRO_VIDEO_PRESENTATION_TIMESTAMP_US =
Property.externalText("Camera:MicroVideoPresentationTimestampUs");
}
3 changes: 3 additions & 0 deletions tika-core/src/main/java/org/apache/tika/metadata/PDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public interface PDF {
*/
Property EOF_OFFSETS = Property.externalRealSeq(PDF_PREFIX + "eofOffsets");

/** Trapped flag from the XMP {@code pdf:} schema; docinfo counterpart {@link #DOC_INFO_TRAPPED}. */
Property TRAPPED = Property.internalText(PDF_PREFIX + "Trapped");

/**
* Prefix to be used for properties that record what was stored
* in the docinfo section (as opposed to XMP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public interface Photoshop {
PREFIX_PHOTOSHOP + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "ColorMode",
_COLOR_MODE_CHOICES_INDEXED);

Property ICC_PROFILE = Property.internalText(
PREFIX_PHOTOSHOP + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "ICCProfile");

Property CAPTION_WRITER = Property.internalText(
PREFIX_PHOTOSHOP + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "CaptionWriter");

Expand Down
12 changes: 12 additions & 0 deletions tika-core/src/main/java/org/apache/tika/metadata/TIFF.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,16 @@ public interface TIFF {
Property ORIGINAL_DATE = Property.internalDate("exif:DateTimeOriginal");

Property EXIF_PAGE_COUNT = Property.externalInteger("exif:PageCount");

/** Camera-body serial number. */
Property SERIAL_NUMBER = Property.internalText("aux:SerialNumber");

/** Human-readable lens description, e.g. "70-200mm f/2.8". */
Property LENS = Property.internalText("aux:Lens");

/** Lens spec: min/max focal length and aperture. */
Property LENS_INFO = Property.internalText("aux:LensInfo");

/** Numeric lens identifier. */
Property LENS_ID = Property.internalText("aux:LensID");
}
14 changes: 14 additions & 0 deletions tika-core/src/main/java/org/apache/tika/metadata/XMPMM.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ public interface XMPMM {
*/
Property HISTORY_SOFTWARE_AGENT = Property.externalTextBag(PREFIX_ + "History:SoftwareAgent");

/** Part of the resource changed by the action (XMPMM history section). */
Property HISTORY_CHANGED = Property.externalTextBag(PREFIX_ + "History:Changed");

/** Additional parameters describing the action (XMPMM history section). */
Property HISTORY_PARAMETERS = Property.externalTextBag(PREFIX_ + "History:Parameters");

/**
* Document id for the document that this document
* was derived from
Expand All @@ -104,4 +110,12 @@ public interface XMPMM {
*/
Property DERIVED_FROM_INSTANCEID = Property.externalText(PREFIX_ + "DerivedFrom:InstanceID");

/** Original document id of the source this document was derived from. */
Property DERIVED_FROM_ORIGINAL_DOCUMENTID =
Property.externalText(PREFIX_ + "DerivedFrom:OriginalDocumentID");

/** Rendition class of the source this document was derived from. */
Property DERIVED_FROM_RENDITION_CLASS =
Property.externalText(PREFIX_ + "DerivedFrom:RenditionClass");

}
70 changes: 70 additions & 0 deletions tika-core/src/main/java/org/apache/tika/metadata/XMPTIFF.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.metadata;

/**
* Metadata keys for values that derive strictly from the XMP {@code tiff:} / {@code exif:}
* schemas, mirroring {@link TIFF}. Tika folds these into the canonical {@link TIFF} keys, where
* the same value can also arrive from binary EXIF; these parallel keys preserve provenance so a
* consumer can distinguish an XMP-sourced value from a binary one.
* <p>
* Same pattern as {@link XMPDC}. Deliberately not implemented by {@link Metadata}; reference it
* directly.
*/
public interface XMPTIFF {

String PREFIX_TIFF = "xmp" + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "tiff";
String PREFIX_EXIF = "xmp" + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "exif";

Property EQUIPMENT_MAKE = Property.internalText(
PREFIX_TIFF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "Make");

Property EQUIPMENT_MODEL = Property.internalText(
PREFIX_TIFF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "Model");

Property SOFTWARE = Property.internalText(
PREFIX_TIFF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "Software");

Property IMAGE_WIDTH = Property.internalInteger(
PREFIX_TIFF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "ImageWidth");

Property IMAGE_LENGTH = Property.internalInteger(
PREFIX_TIFF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "ImageLength");

Property BITS_PER_SAMPLE = Property.internalIntegerSequence(
PREFIX_TIFF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "BitsPerSample");

Property SAMPLES_PER_PIXEL = Property.internalInteger(
PREFIX_TIFF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "SamplesPerPixel");

Property ORIENTATION = Property.internalText(
PREFIX_TIFF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "Orientation");

Property ORIGINAL_DATE = Property.internalDate(
PREFIX_EXIF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "DateTimeOriginal");

Property ISO_SPEED_RATINGS = Property.internalIntegerSequence(
PREFIX_EXIF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "IsoSpeedRatings");

/** Valid image width; folded into canonical {@link TIFF#IMAGE_WIDTH}. */
Property PIXEL_X_DIMENSION = Property.internalInteger(
PREFIX_EXIF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "PixelXDimension");

/** Valid image height; folded into canonical {@link TIFF#IMAGE_LENGTH}. */
Property PIXEL_Y_DIMENSION = Property.internalInteger(
PREFIX_EXIF + TikaCoreProperties.NAMESPACE_PREFIX_DELIMITER + "PixelYDimension");
}
1 change: 0 additions & 1 deletion tika-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,6 @@
<jbig2.version>3.0.5</jbig2.version>
<jcommander.version>1.82</jcommander.version>
<jdom2.version>2.0.6.1</jdom2.version>
<jempbox.version>1.8.17</jempbox.version>
<jetty.version>12.1.11</jetty.version>
<jetty.http2.version>12.1.11</jetty.http2.version>
<jhighlight.version>2.0.0</jhighlight.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ void extractMetadata(InputStream stream, ContentHandler contentHandler, Metadata
metadataExtractor.parseRawExif(stream, extensionLength, true);
break;
case EXTENSION_TAG_XMP:
handleXMP(stream, extensionLength, metadataExtractor);
handleXMP(stream, extensionLength, metadata, parseContext);
break;
default:
IOUtils.skipFully(stream, extensionLength);
Expand All @@ -186,7 +186,8 @@ public int getMaxRecordLength() {
return this.maxRecordLength;
}

protected void handleXMP(InputStream stream, int xmpLength, ImageMetadataExtractor extractor)
protected void handleXMP(InputStream stream, int xmpLength, Metadata metadata,
ParseContext context)
throws IOException, TikaException, SAXException {
if (xmpLength < 0) {
throw new TikaException("xmp length must be >= 0");
Expand All @@ -199,6 +200,6 @@ protected void handleXMP(InputStream stream, int xmpLength, ImageMetadataExtract
}
byte[] xmp = new byte[xmpLength];
IOUtils.readFully(stream, xmp);
extractor.parseRawXMP(xmp);
ImageXmp.extractRaw(xmp, metadata, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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 java.nio.charset.StandardCharsets.US_ASCII;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.drew.lang.SequentialByteArrayReader;
import com.drew.lang.SequentialReader;

/**
* Reassembles a large XMP packet split across JPEG APP1 segments ("Extended XMP", XMP Part 3).
* Chunk stitching is borrowed almost verbatim from metadata-extractor's {@code XmpReader}
* (Apache 2.0, Drew Noakes); we find the GUID by scanning the standard packet since Tika parses
* the XMP itself.
*/
final class ExtendedXmp {

private static final String XMP_JPEG_PREAMBLE = "http://ns.adobe.com/xap/1.0/\u0000";
private static final String XMP_EXTENSION_JPEG_PREAMBLE =
"http://ns.adobe.com/xmp/extension/\u0000";
private static final int EXTENDED_XMP_GUID_LENGTH = 32;
private static final int EXTENDED_XMP_INT_LENGTH = 4;
// Tika: cap the declared length so a hostile chunk can't drive a huge allocation (not upstream).
private static final int MAX_EXTENDED = 64 * 1024 * 1024;
// allow any run of attribute/element syntax (=, quotes, '>', whitespace, newlines) before the GUID
private static final Pattern HAS_EXTENDED =
Pattern.compile("HasExtendedXMP[\"'>=\\s]*([A-Fa-f0-9]{32})");

private ExtendedXmp() {
}

/** XMP packets (standard + reassembled extended, if any) from a JPEG's APP1 segment payloads. */
static List<byte[]> assemble(Iterable<byte[]> segments) {
// adapted from metadata-extractor XmpReader.readJpegSegments
final int preambleLength = XMP_JPEG_PREAMBLE.length();
final int extensionPreambleLength = XMP_EXTENSION_JPEG_PREAMBLE.length();
String guid = null;
byte[] standard = null;
byte[] extendedBuffer = null;

for (byte[] segmentBytes : segments) {
if (segmentBytes.length >= preambleLength && XMP_JPEG_PREAMBLE
.equalsIgnoreCase(new String(segmentBytes, 0, preambleLength, US_ASCII))) {
standard = new byte[segmentBytes.length - preambleLength];
System.arraycopy(segmentBytes, preambleLength, standard, 0, standard.length);
guid = findGuid(standard);
extendedBuffer = null; // a new standard packet starts a fresh extended assembly
} else if (guid != null && segmentBytes.length >= extensionPreambleLength
&& XMP_EXTENSION_JPEG_PREAMBLE.equalsIgnoreCase(
new String(segmentBytes, 0, extensionPreambleLength, US_ASCII))) {
extendedBuffer = processExtendedXMPChunk(segmentBytes, guid, extendedBuffer);
}
}

List<byte[]> packets = new ArrayList<>();
if (standard != null) {
packets.add(standard);
}
if (extendedBuffer != null) {
packets.add(extendedBuffer);
}
return packets;
}

private static String findGuid(byte[] standard) {
Matcher m = HAS_EXTENDED.matcher(new String(standard, US_ASCII));
return m.find() ? m.group(1) : null;
}

// Borrowed almost verbatim from metadata-extractor XmpReader.processExtendedXMPChunk (Apache 2.0).
private static byte[] processExtendedXMPChunk(byte[] segmentBytes, String extendedXMPGUID,
byte[] extendedXMPBuffer) {
final int extensionPreambleLength = XMP_EXTENSION_JPEG_PREAMBLE.length();
final int segmentLength = segmentBytes.length;
final int totalOffset = extensionPreambleLength + EXTENDED_XMP_GUID_LENGTH
+ EXTENDED_XMP_INT_LENGTH + EXTENDED_XMP_INT_LENGTH;
if (segmentLength >= totalOffset) {
try {
final SequentialReader reader = new SequentialByteArrayReader(segmentBytes);
reader.skip(extensionPreambleLength);
final String segmentGUID = reader.getString(EXTENDED_XMP_GUID_LENGTH);
if (extendedXMPGUID.equals(segmentGUID)) {
final int fullLength = (int) reader.getUInt32();
final int chunkOffset = (int) reader.getUInt32();
if (extendedXMPBuffer == null) {
if (fullLength <= 0 || fullLength > MAX_EXTENDED) { // Tika OOM guard
return null;
}
extendedXMPBuffer = new byte[fullLength];
}
if (extendedXMPBuffer.length == fullLength) {
int copyLength = segmentLength - totalOffset;
if (chunkOffset >= 0 && chunkOffset <= extendedXMPBuffer.length - copyLength) {
System.arraycopy(segmentBytes, totalOffset, extendedXMPBuffer, chunkOffset,
copyLength);
}
}
}
} catch (IOException ex) {
// best effort: keep whatever chunks were already assembled
}
}
return extendedXMPBuffer;
}
}
Loading
Loading