ImageXmp.java
/*
* 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.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.imaging.jpeg.JpegSegmentData;
import com.drew.imaging.jpeg.JpegSegmentReader;
import com.drew.imaging.jpeg.JpegSegmentType;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.xml.sax.SAXException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.xmp.XMPPacketScanner;
import org.apache.tika.parser.xmp.XmpExtractor;
/**
* Image XMP via the shared {@link XmpExtractor}. XMP is auxiliary: a bad packet is recorded and
* swallowed so it never fails the image parse (matches the PDF module).
*/
final class ImageXmp {
private ImageXmp() {
}
/**
* Scan an {@code <?xpacket?>}-wrapped packet out of a stream (TIFF/JXL/PSD/HEIF) and parse
* it. Returns true if a packet was found, so callers can fall back to another locator.
*/
static boolean scanAndExtract(InputStream stream, Metadata metadata, ParseContext context) {
try {
UnsynchronizedByteArrayOutputStream out =
UnsynchronizedByteArrayOutputStream.builder().get();
if (new XMPPacketScanner().parse(stream, out)) {
try (InputStream packet = out.toInputStream()) {
new XmpExtractor().extract(packet, metadata, context);
}
return true;
}
} catch (SecurityException e) {
throw e;
} catch (IOException | SAXException | TikaException | RuntimeException e) {
EmbeddedDocumentUtil.recordException(e, metadata);
}
return false;
}
/** Parse an already-isolated raw XMP packet (BPG). */
static void extractRaw(byte[] xmp, Metadata metadata, ParseContext context) {
try {
new XmpExtractor().extract(xmp, metadata, context);
} catch (SecurityException e) {
throw e;
} catch (IOException | SAXException | TikaException | RuntimeException e) {
EmbeddedDocumentUtil.recordException(e, metadata);
}
}
/** JPEG: read APP1 segments, reassemble Extended XMP, parse each resulting packet. */
static void extractJpeg(File file, Metadata metadata, ParseContext context) {
try {
Iterable<byte[]> app1;
try {
JpegSegmentData data = JpegSegmentReader.readSegments(file,
Collections.singletonList(JpegSegmentType.APP1));
app1 = data.getSegments(JpegSegmentType.APP1);
} catch (JpegProcessingException e) {
return; // not a decodable jpeg segment structure
}
if (app1 == null) {
return;
}
for (byte[] packet : ExtendedXmp.assemble(app1)) {
new XmpExtractor().extract(packet, metadata, context);
}
} catch (SecurityException e) {
throw e;
} catch (IOException | SAXException | TikaException | RuntimeException e) {
EmbeddedDocumentUtil.recordException(e, metadata);
}
}
/** WebP: pull the raw packet out of the RIFF {@code "XMP "} chunk and parse it. */
static void extractWebp(File file, Metadata metadata, ParseContext context) {
try {
byte[] xmp = readRiffChunk(file, "XMP ");
if (xmp != null) {
new XmpExtractor().extract(xmp, metadata, context);
}
} catch (SecurityException e) {
throw e;
} catch (IOException | SAXException | TikaException | RuntimeException e) {
EmbeddedDocumentUtil.recordException(e, metadata);
}
}
// 64 MB: guards against a corrupt chunk size triggering a huge allocation.
private static final long MAX_CHUNK = 64L * 1024 * 1024;
/** Return the payload of the first top-level RIFF chunk with the given FourCC, or null. */
private static byte[] readRiffChunk(File file, String fourCC) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
byte[] head = new byte[12];
if (IOUtils.read(in, head, 0, 12) < 12 || head[0] != 'R' || head[1] != 'I' ||
head[2] != 'F' || head[3] != 'F' || head[8] != 'W' || head[9] != 'E' ||
head[10] != 'B' || head[11] != 'P') {
return null;
}
byte[] ch = new byte[8];
while (IOUtils.read(in, ch, 0, 8) == 8) {
long size = (ch[4] & 0xffL) | (ch[5] & 0xffL) << 8 |
(ch[6] & 0xffL) << 16 | (ch[7] & 0xffL) << 24;
if (fourCC.equals(new String(ch, 0, 4, StandardCharsets.US_ASCII))) {
if (size > MAX_CHUNK) {
return null; // target chunk too large to allocate
}
byte[] data = new byte[(int) size];
return IOUtils.read(in, data, 0, data.length) == data.length ? data : null;
}
// a large foreign chunk before "XMP " must be skipped, not abort the scan
IOUtils.skipFully(in, size + (size & 1L)); // RIFF pads chunks to even length
}
}
return null;
}
}