WebPXmpTest.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 static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.xml.sax.helpers.DefaultHandler;

import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;

/**
 * WebP stores XMP in a RIFF {@code "XMP "} chunk. Tika parses it with the shared
 * {@link org.apache.tika.parser.xmp.XmpExtractor}; regression guard for TIKA where the
 * WebP path dropped XMP entirely (no in-repo WebP carries an XMP packet).
 */
public class WebPXmpTest {

    private static final String XMP =
            "<?xpacket begin=\"���\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>"
            + "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">"
            + "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">"
            + "<rdf:Description rdf:about=\"\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">"
            + "<dc:title><rdf:Alt><rdf:li xml:lang=\"x-default\">WebP XMP Title</rdf:li>"
            + "</rdf:Alt></dc:title>"
            + "<dc:creator><rdf:Seq><rdf:li>Jane Photographer</rdf:li></rdf:Seq></dc:creator>"
            + "</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end=\"w\"?>";

    private static void chunk(ByteArrayOutputStream riff, String fourCC, byte[] data) {
        riff.writeBytes(fourCC.getBytes(US_ASCII));
        riff.writeBytes(le(data.length));
        riff.writeBytes(data);
        if ((data.length & 1) == 1) {
            riff.write(0);   // RIFF pads chunks to even length
        }
    }

    private static byte[] le(int v) {
        return new byte[]{(byte) v, (byte) (v >>> 8), (byte) (v >>> 16), (byte) (v >>> 24)};
    }

    private static byte[] webpWithXmp() {
        ByteArrayOutputStream body = new ByteArrayOutputStream();
        body.writeBytes("WEBP".getBytes(US_ASCII));
        // VP8X extended-format header with the XMP flag (0x04) set
        chunk(body, "VP8X", new byte[]{0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0});
        chunk(body, "VP8 ", new byte[16]);   // dummy bitstream; content is irrelevant to metadata
        chunk(body, "XMP ", XMP.getBytes(UTF_8));
        ByteArrayOutputStream riff = new ByteArrayOutputStream();
        riff.writeBytes("RIFF".getBytes(US_ASCII));
        riff.writeBytes(le(body.size()));
        riff.writeBytes(body.toByteArray());
        return riff.toByteArray();
    }

    @Test
    public void testWebpXmpIsExtracted(@TempDir Path tmp) throws Exception {
        Path file = tmp.resolve("xmp.webp");
        Files.write(file, webpWithXmp());

        Metadata metadata = new Metadata();
        metadata.set(Metadata.CONTENT_TYPE, "image/webp");
        try (TikaInputStream tis = TikaInputStream.get(file)) {
            new WebPParser().parse(tis, new DefaultHandler(), metadata, new ParseContext());
        }

        assertEquals("WebP XMP Title", metadata.get(TikaCoreProperties.TITLE));
        assertEquals("WebP XMP Title", metadata.get("dc:title"));
        assertEquals("Jane Photographer", metadata.get(TikaCoreProperties.CREATOR));
        assertEquals("Jane Photographer", metadata.get("dc:creator"));
    }

    /** A malformed XMP packet is recorded, not thrown: the rest of the parse still succeeds. */
    @Test
    public void testMalformedXmpIsNonFatal(@TempDir Path tmp) throws Exception {
        ByteArrayOutputStream body = new ByteArrayOutputStream();
        body.write("WEBP".getBytes(US_ASCII));
        chunk(body, "VP8X", new byte[]{0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0});
        chunk(body, "VP8 ", new byte[16]);
        chunk(body, "XMP ", "<x:xmpmeta><rdf:RDF><dc:title>unclosed".getBytes(UTF_8));
        ByteArrayOutputStream riff = new ByteArrayOutputStream();
        riff.write("RIFF".getBytes(US_ASCII));
        riff.writeBytes(le(body.size()));
        riff.writeBytes(body.toByteArray());
        Path file = tmp.resolve("bad.webp");
        Files.write(file, riff.toByteArray());

        Metadata metadata = new Metadata();
        metadata.set(Metadata.CONTENT_TYPE, "image/webp");
        try (TikaInputStream tis = TikaInputStream.get(file)) {
            new WebPParser().parse(tis, new DefaultHandler(), metadata, new ParseContext());
        }
        assertNull(metadata.get(TikaCoreProperties.TITLE));
        assertNotNull(metadata.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
    }
}