PagesConfigTest.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.pages;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.Test;

import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.ParseRecord;
import org.apache.tika.parser.inference.InputKind;
import org.apache.tika.renderer.ImageType;
import org.apache.tika.renderer.RenderSettings;

public class PagesConfigTest {

    @Test
    public void testDefaultsAreComplete() {
        PagesConfig d = PagesConfig.defaults();
        assertEquals(TextPolicy.AUTO, d.getText());
        assertEquals(300, d.getRender().getDpi());
        assertEquals(-1, d.getOcr().getMaxPages());
        assertEquals(10, d.getOcr().getAuto().getTotalCharsPerPage());
        assertEquals(List.of(InputKind.TEXT), d.getInference());
        assertFalse(d.getEmit().getEnabled());
        assertTrue(d.emitsSameImage());
    }

    @Test
    public void testLayersFoldPerField() throws Exception {
        PagesConfig deployment = new PagesConfig();
        RenderSettings render = new RenderSettings();
        render.setDpi(200);
        deployment.setRender(render);
        deployment.setText(TextPolicy.EXTRACT);

        PagesConfig parser = new PagesConfig();
        PagesConfig.Emit emit = new PagesConfig.Emit();
        emit.setEnabled(true);
        emit.setMaxPages(1);
        RenderSettings emitted = new RenderSettings();
        emitted.setImageType(ImageType.RGB);
        emit.setRender(emitted);
        parser.setEmit(emit);

        PagesConfig request = new PagesConfig();
        PagesConfig.Emit requestEmit = new PagesConfig.Emit();
        RenderSettings requestEmitted = new RenderSettings();
        requestEmitted.setMaxWidth(256);
        requestEmit.setRender(requestEmitted);
        request.setEmit(requestEmit);

        ParseContext context = new ParseContext();
        context.set(PagesConfig.class, deployment);
        PagesConfig effective = PagesConfig.resolve(context, parser, request);

        assertEquals(TextPolicy.EXTRACT, effective.getText());
        assertEquals(200, effective.getRender().getDpi());
        assertTrue(effective.getEmit().getEnabled());
        assertEquals(1, effective.getEmit().getMaxPages());
        RenderSettings out = effective.emittedRender();
        // nested overlays accumulate: the parser's imageType and the request's box both hold
        assertEquals(ImageType.RGB, out.getImageType());
        assertEquals(256, out.getMaxWidth());
        assertEquals(200, out.getDpi());
        assertFalse(effective.emitsSameImage());
        // the layers are untouched
        assertNull(parser.getText());
        assertNull(request.getRender());
        assertEquals(TextPolicy.EXTRACT, deployment.getText());
    }

    @Test
    public void testNoContextAndNoOverlaysIsDefaults() throws Exception {
        PagesConfig effective = PagesConfig.resolve(new ParseContext());
        assertEquals(TextPolicy.AUTO, effective.getText());
        assertEquals(300, effective.getRender().getDpi());
        PagesConfig withNull = PagesConfig.resolve(null, (PagesConfig) null);
        assertEquals(TextPolicy.AUTO, withNull.getText());
    }

    @Test
    public void testEmitApplies() {
        PagesConfig.Emit emit = PagesConfig.Emit.defaults();
        ParseContext context = new ParseContext();
        Metadata top = new Metadata();
        assertFalse(emit.applies(top, context));
        emit.setEnabled(true);
        assertTrue(emit.applies(top, context));
        assertTrue(emit.applies(top, null), "no context is the top level");
        emit.setResourceTypes(Set.of("THUMBNAIL"));
        assertFalse(emit.applies(top, context));
        Metadata thumbnail = new Metadata();
        thumbnail.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, "THUMBNAIL");
        assertTrue(emit.applies(thumbnail, context));
        assertTrue(emit.withinBudget(50));
        emit.setMaxPages(1);
        assertTrue(emit.withinBudget(1));
        assertFalse(emit.withinBudget(2));
    }

    /** maxDepth reads the extractor's embedding depth: 0 is the top-level document only. */
    @Test
    public void testEmitMaxDepth() {
        PagesConfig.Emit emit = PagesConfig.Emit.defaults();
        emit.setEnabled(true);
        ParseContext context = new ParseContext();
        ParseRecord record = ParseRecord.newInstance(context);
        context.set(ParseRecord.class, record);
        Metadata attachment = new Metadata();
        record.enterEmbedded();
        assertTrue(emit.applies(attachment, context), "-1 is any depth");
        emit.setMaxDepth(0);
        assertFalse(emit.applies(attachment, context), "an attachment is depth 1");
        record.exitEmbedded();
        assertTrue(emit.applies(new Metadata(), context), "the top-level document is depth 0");
        emit.setMaxDepth(1);
        record.enterEmbedded();
        assertTrue(emit.applies(attachment, context));
        record.enterEmbedded();
        assertFalse(emit.applies(attachment, context), "depth 2");
        assertThrows(IllegalArgumentException.class, () -> emit.setMaxDepth(-2));
    }

    /** An overlay applied to a config never shares objects with the config it came from. */
    @Test
    public void testOverlayIsCopied() {
        PagesConfig mine = new PagesConfig();
        mine.ocr().auto().setTotalCharsPerPage(5);
        mine.emit().render().setDpi(20);
        PagesConfig applied = new PagesConfig().over(mine);
        mine.ocr().auto().setTotalCharsPerPage(99);
        mine.emit().render().setDpi(99);
        assertEquals(5, applied.getOcr().getAuto().getTotalCharsPerPage());
        assertEquals(20, applied.getEmit().getRender().getDpi());
    }

    @Test
    public void testValidation() {
        PagesConfig config = new PagesConfig();
        assertThrows(IllegalArgumentException.class,
                () -> config.setInference(List.of(InputKind.IMAGES)));
        PagesConfig.Emit emit = new PagesConfig.Emit();
        assertThrows(IllegalArgumentException.class, () -> emit.setResourceTypes(Set.of("THUMB")));
        assertThrows(IllegalArgumentException.class, () -> emit.setMaxPages(0));
        PagesConfig.Ocr ocr = new PagesConfig.Ocr();
        assertThrows(IllegalArgumentException.class, () -> ocr.setMaxPages(-3));
    }

    private static ParseContext requestFor(String componentName, PagesConfig operatorTop) {
        ParseContext context = new ParseContext();
        // the parser's block came over the wire
        context.setJsonConfig(componentName, "{}", false);
        context.set(PagesConfig.OperatorPages.class, new PagesConfig.OperatorPages(operatorTop));
        return context;
    }

    @Test
    public void testRequestMayNotRaiseRenderLimits() throws Exception {
        PagesConfig request = new PagesConfig();
        request.render().setDpi(1200);
        request.render().setMaxImagePixels((long) RenderSettings.NO_LIMIT);
        request.emit().render().setDpi(2400);

        PagesConfig pages = PagesConfig.resolve(requestFor("x-parser", null), "x-parser",
                null, request);

        assertEquals(300, pages.getRender().getDpi());
        assertEquals(100_000_000L, pages.getRender().getMaxImagePixels());
        assertEquals(300, pages.emittedRender().getDpi());
    }

    @Test
    public void testOperatorLayersAreTheCeiling() throws Exception {
        PagesConfig operatorTop = new PagesConfig();
        operatorTop.render().setDpi(150);
        // this parser's operator overlay allows more than the top-level block
        PagesConfig operatorOverlay = new PagesConfig();
        operatorOverlay.render().setDpi(600);
        PagesConfig request = new PagesConfig();
        request.render().setDpi(1200);

        PagesConfig pages = PagesConfig.resolve(requestFor("x-parser", operatorTop), "x-parser",
                operatorOverlay, request);
        assertEquals(600, pages.getRender().getDpi());

        request.render().setDpi(96);
        pages = PagesConfig.resolve(requestFor("x-parser", operatorTop), "x-parser",
                operatorOverlay, request);
        assertEquals(96, pages.getRender().getDpi());
    }

    @Test
    public void testNoClampOutsideTheWorker() throws Exception {
        ParseContext context = new ParseContext();
        context.setJsonConfig("x-parser", "{}", false);
        PagesConfig request = new PagesConfig();
        request.render().setDpi(1200);

        assertEquals(1200, PagesConfig.resolve(context, "x-parser", null, request)
                .getRender().getDpi());
    }
}