PdfType3FunctionTest.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2025 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.pdf.function;

import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.test.ExtendedITextTest;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;

@Tag("UnitTest")
public class PdfType3FunctionTest extends ExtendedITextTest {
    private final static double EPSILON = 10e-6;

    @Test
    public void constructorNullFunctionsTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.remove(PdfName.Functions);
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_FUNCTIONS, ex.getMessage());
    }

    @Test
    public void constructorZeroSizeOfFunctionsTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.put(PdfName.Functions, new PdfArray());
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_FUNCTIONS, ex.getMessage());
    }

    @Test
    public void constructorDifferentOutputSizeOfFunctionsTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.getAsArray(PdfName.Functions).getAsDictionary(0).put(PdfName.Range, new PdfArray(new double[] {-100, 100, -100, 100}));
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_FUNCTIONS_OUTPUT, ex.getMessage());
    }

    @Test
    public void constructorDifferentOutputSizeFuncWithRangeTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.put(PdfName.Range, new PdfArray(new double[] {-100, 100, -100, 100}));
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_FUNCTIONS_OUTPUT, ex.getMessage());
    }

    @Test
    public void constructorInvalidInputSizeOfFuncTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        IPdfFunctionFactory customFactory = (dict) -> new CustomPdfFunction((PdfDictionary)dict, 2, 1);
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func, customFactory));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_FUNCTIONS_INPUT, ex.getMessage());
    }

    @Test
    public void constructorIgnoreNotDictFunctionsTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.getAsArray(PdfName.Functions).add(new PdfNumber(1));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);
        Assertions.assertEquals(2, type3Function.getFunctions().size());
    }

    @Test
    public void constructorInvalidFunctionTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.getAsArray(PdfName.Functions).getAsDictionary(0).remove(PdfName.N);
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_2_FUNCTION_N, ex.getMessage());
    }

    @Test
    public void constructorNullBoundsTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.remove(PdfName.Bounds);
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_BOUNDS, ex.getMessage());
    }

    @Test
    public void constructorInvalidSizeOfBoundsTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.put(PdfName.Bounds, new PdfArray());
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_BOUNDS, ex.getMessage());
    }

    @Test
    public void constructorInvalidBoundsLessThanDomainTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.getAsArray(PdfName.Bounds).remove(0);
        type3Func.getAsArray(PdfName.Bounds).add(new PdfNumber(-1));
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_BOUNDS, ex.getMessage());
    }

    @Test
    public void constructorInvalidBoundsMoreThanDomainTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.getAsArray(PdfName.Bounds).remove(0);
        type3Func.getAsArray(PdfName.Bounds).add(new PdfNumber(3));
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_BOUNDS, ex.getMessage());
    }

    @Test
    public void constructorInvalidBoundsLessThanPreviousTest() {
        PdfDictionary type3Func = new PdfDictionary();
        type3Func.put(PdfName.FunctionType, new PdfNumber(3));

        PdfArray domain = new PdfArray(new int[] {0, 1});
        type3Func.put(PdfName.Domain, domain);

        PdfArray functions = new PdfArray(PdfFunctionUtil.createMinimalPdfType2FunctionDict());
        functions.add(PdfFunctionUtil.createMinimalPdfType2FunctionDict());
        functions.add(PdfFunctionUtil.createMinimalPdfType2FunctionDict());
        type3Func.put(PdfName.Functions, functions);

        type3Func.put(PdfName.Bounds, new PdfArray(new double[] {1, 0.5}));

        type3Func.put(PdfName.Encode, new PdfArray(new double[] {0, 1, 0, 1, 0, 1}));

        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_BOUNDS, ex.getMessage());
    }

    @Test
    public void constructorNullEncodeTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.remove(PdfName.Encode);
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_ENCODE, ex.getMessage());
    }

    @Test
    public void constructorInvalidSizeOfEncodeTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.put(PdfName.Encode, new PdfArray());
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_ENCODE, ex.getMessage());
    }

    @Test
    public void constructorInvalidDomainTest() {
        PdfDictionary type3Func = createMinimalPdfType3FunctionDict();
        type3Func.put(PdfName.Domain, new PdfArray(new double[] {1}));
        Exception ex = Assertions.assertThrows(PdfException.class, () -> new PdfType3Function(type3Func));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_DOMAIN, ex.getMessage());
    }

    @Test
    public void getOutputSizeNullRangeTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        IPdfFunctionFactory customFactory = (dict) -> new CustomPdfFunction((PdfDictionary)dict, 1, 7);
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict, customFactory);
        Assertions.assertEquals(7, type3Function.getOutputSize());
    }

    @Test
    public void getEncodeTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.put(PdfName.Encode, new PdfArray(new double[] {0, 0.37, -1, 0}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        Assertions.assertArrayEquals(new double[] {0, 0.37, -1, 0}, type3Function.getEncode(), EPSILON);
    }

    @Test
    public void getBoundsTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.put(PdfName.Bounds, new PdfArray(new double[] {0.789}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        Assertions.assertArrayEquals(new double[] {0.789}, type3Function.getBounds(), EPSILON);
    }

    @Test
    public void calculateInvalid2NumberInputTest() {
        PdfType3Function type3Func = new PdfType3Function(createMinimalPdfType3FunctionDict());

        Exception ex = Assertions.assertThrows(PdfException.class, () -> type3Func.calculate(new double[] {0, 1}));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_INPUT_FOR_TYPE_3_FUNCTION, ex.getMessage());
    }

    @Test
    public void calculateInvalidNullInputTest() {
        PdfType3Function type3Func = new PdfType3Function(createMinimalPdfType3FunctionDict());

        Exception ex = Assertions.assertThrows(PdfException.class, () -> type3Func.calculate(null));
        Assertions.assertEquals(KernelExceptionMessageConstant.INVALID_INPUT_FOR_TYPE_3_FUNCTION, ex.getMessage());
    }

    @Test
    public void calculateInputClipTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {-5});
        // input value was clipped to 0 from -5
        Assertions.assertArrayEquals(new double[] {0}, output, EPSILON);
    }

    @Test
    public void calculateDomainOnePointIntervalTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.put(PdfName.Bounds, new PdfArray());
        type3FuncDict.getAsArray(PdfName.Functions).remove(1);
        type3FuncDict.put(PdfName.Domain, new PdfArray(new double[] {0.5, 0.5}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {7});
        Assertions.assertArrayEquals(new double[] {0}, output, EPSILON);
    }

    @Test
    public void calculateInputClipByFuncTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.getAsArray(PdfName.Functions).getAsDictionary(0).put(PdfName.Domain, new PdfArray(new double[] {2, 3}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {0.1});
        // input value 0.1 was passed to first function with domain [2, 3], so value was clipped to 2 from 0.1
        Assertions.assertArrayEquals(new double[] {4}, output, EPSILON);
    }

    @Test
    public void calculateInputValueEqualBoundsTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.getAsArray(PdfName.Functions).getAsDictionary(1).put(PdfName.C0, new PdfArray(new double[] {-3}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {0.5});
        // Input value 0.5 was passed to second function.
        // Subdomain is [0.5, 1], encode is [0, 1], so value 0.5 was encoded to 0.
        Assertions.assertArrayEquals(new double[] {-3}, output, EPSILON);
    }

    @Test
    public void calculateInputValueNotEqualBoundsTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {0.53});
        // Input value 0.53 was passed to second function.
        // Subdomain is [0.5, 1], encode is [0, 1], so value 0.53 was encoded to 0.06.
        Assertions.assertArrayEquals(new double[] {0.06}, output, EPSILON);
    }

    @Test
    public void calculateInputValueEqualDomainTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {1});
        Assertions.assertArrayEquals(new double[] {1}, output, EPSILON);
    }

    @Test
    public void calculateWith3FunctionsTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();

        PdfDictionary minimalType2Func = PdfFunctionUtil.createMinimalPdfType2FunctionDict();
        minimalType2Func.put(PdfName.N, new PdfNumber(3));
        type3FuncDict.getAsArray(PdfName.Functions).add(1, minimalType2Func);
        type3FuncDict.put(PdfName.Encode, new PdfArray(new double[] {0, 1, 0, 1, 0, 1}));
        type3FuncDict.put(PdfName.Bounds, new PdfArray(new double[] {0.5, 0.7}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {0.52});
        // Input value 0.52 was passed to second function.
        // Subdomain is [0.5, 0.7], encode is [0, 1], so value 0.52 was encoded to 0.1.
        Assertions.assertArrayEquals(new double[] {0.001}, output, EPSILON);
    }

    @Test
    public void calculateReverseEncodingTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.put(PdfName.Encode, new PdfArray(new double[] {0, 1, 1, 0}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        Assertions.assertArrayEquals(new double[] {0, 1, 1, 0}, type3Function.getEncode(), EPSILON);

        double[] output = type3Function.calculate(new double[] {1});
        // Input value 1 was passed to second function.
        // Subdomain is [0.5, 1], encode is [1, 0], so value 1 was encoded to 0.
        Assertions.assertArrayEquals(new double[] {0}, output, EPSILON);
    }

    @Test
    public void calculateOneFunctionTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.put(PdfName.Bounds, new PdfArray());
        type3FuncDict.getAsArray(PdfName.Functions).remove(1);
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {0.6});
        Assertions.assertArrayEquals(new double[] {0.36}, output, EPSILON);
    }

    @Test
    public void calculateBoundsEqualLeftDomainTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.getAsArray(PdfName.Functions).getAsDictionary(0).put(PdfName.C0, new PdfArray(new double[] {-3}));
        type3FuncDict.getAsArray(PdfName.Functions).getAsDictionary(1).put(PdfName.C1, new PdfArray(new double[] {5}));
        type3FuncDict.put(PdfName.Bounds, new PdfArray(new double[] {0}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {0});
        // first function was used
        Assertions.assertArrayEquals(new double[] {-3}, output, EPSILON);

        output = type3Function.calculate(new double[] {0.1});
        // second function was used
        Assertions.assertArrayEquals(new double[] {0.5}, output, EPSILON);

        output = type3Function.calculate(new double[] {1});
        // second function was used
        Assertions.assertArrayEquals(new double[] {5}, output, EPSILON);
    }

    @Test
    public void calculateBoundsEqualRightDomainTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.getAsArray(PdfName.Functions).getAsDictionary(0).put(PdfName.C1, new PdfArray(new double[] {-3}));
        type3FuncDict.getAsArray(PdfName.Functions).getAsDictionary(1).put(PdfName.C0, new PdfArray(new double[] {5}));
        type3FuncDict.put(PdfName.Bounds, new PdfArray(new double[] {1}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {0});
        // first function was used
        Assertions.assertArrayEquals(new double[] {0}, output, EPSILON);

        output = type3Function.calculate(new double[] {0.1});
        // first function was used
        Assertions.assertArrayEquals(new double[] {-0.03}, output, EPSILON);

        output = type3Function.calculate(new double[] {1});
        // second function was used
        Assertions.assertArrayEquals(new double[] {5}, output, EPSILON);
    }

    @Test
    public void calculateBoundsEqualLeftDomainWith3FuncTest() {
        PdfDictionary type3FuncDict = createMinimalPdfType3FunctionDict();
        type3FuncDict.getAsArray(PdfName.Functions).getAsDictionary(0).put(PdfName.C0, new PdfArray(new double[] {-3}));
        type3FuncDict.getAsArray(PdfName.Functions).getAsDictionary(1).put(PdfName.C1, new PdfArray(new double[] {5}));
        PdfDictionary minimalType2Func = PdfFunctionUtil.createMinimalPdfType2FunctionDict();
        minimalType2Func.put(PdfName.N, new PdfNumber(1));
        minimalType2Func.put(PdfName.C1, new PdfArray(new double[] {-2}));
        type3FuncDict.getAsArray(PdfName.Functions).add(minimalType2Func);
        type3FuncDict.put(PdfName.Bounds, new PdfArray(new double[] {0, 0.5}));
        type3FuncDict.put(PdfName.Encode, new PdfArray(new double[] {0, 1, 0, 1, 0, 1}));
        PdfType3Function type3Function = new PdfType3Function(type3FuncDict);

        double[] output = type3Function.calculate(new double[] {0});
        // first function was used
        Assertions.assertArrayEquals(new double[] {-3}, output, EPSILON);

        output = type3Function.calculate(new double[] {0.1});
        // second function was used
        Assertions.assertArrayEquals(new double[] {1}, output, EPSILON);

        output = type3Function.calculate(new double[] {0.6});
        // third function was used
        Assertions.assertArrayEquals(new double[] {-0.4}, output, EPSILON);

        output = type3Function.calculate(new double[] {1});
        // third function was used
        Assertions.assertArrayEquals(new double[] {-2}, output, EPSILON);
    }

    private static PdfDictionary createMinimalPdfType3FunctionDict() {
        PdfDictionary type3Func = new PdfDictionary();
        type3Func.put(PdfName.FunctionType, new PdfNumber(3));

        PdfArray domain = new PdfArray(new int[] {0, 1});
        type3Func.put(PdfName.Domain, domain);

        PdfArray functions = new PdfArray(PdfFunctionUtil.createMinimalPdfType2FunctionDict());
        PdfDictionary minimalType2Func = PdfFunctionUtil.createMinimalPdfType2FunctionDict();
        minimalType2Func.put(PdfName.N, new PdfNumber(1));
        functions.add(minimalType2Func);
        type3Func.put(PdfName.Functions, functions);

        type3Func.put(PdfName.Bounds, new PdfArray(new double[] {0.5}));

        type3Func.put(PdfName.Encode, new PdfArray(new double[] {0, 1, 0, 1}));

        return type3Func;
    }

    private static class CustomPdfFunction extends AbstractPdfFunction<PdfDictionary> {
        private final int inputSize;
        private final int outputSize;

        protected CustomPdfFunction(PdfDictionary pdfObject, int inputSize, int outputSize) {
            super(pdfObject);
            this.inputSize = inputSize;
            this.outputSize = outputSize;
        }

        @Override
        public int getInputSize() {
            return inputSize;
        }

        @Override
        public int getOutputSize() {
            return outputSize;
        }

        @Override
        public double[] calculate(double[] input) {
            return new double[0];
        }

        @Override
        protected boolean isWrappedObjectMustBeIndirect() {
            return false;
        }
    }
}