PdfType3Function.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.PdfObject;
import com.itextpdf.kernel.pdf.colorspace.PdfColorSpace;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * This class represents Pdf type 3 function that defines a stitching of the subdomains
 * of several 1-input functions to produce a single new 1-input function.
 *
 * <p>
 * For more info see ISO 32000-1, section 7.10.4 "Type 3 (Stitching) Functions".
 */
public class PdfType3Function extends AbstractPdfFunction<PdfDictionary> {

    private static final IPdfFunctionFactory DEFAULT_FUNCTION_FACTORY = (PdfObject pdfObject) ->
    {
        return PdfFunctionFactory.create(pdfObject);
    };
    private final IPdfFunctionFactory functionFactory;

    private List<IPdfFunction> functions;

    private double[] bounds;

    private double[] encode;


    /**
     * Instantiates a new PdfType3Function instance based on passed PdfDictionary instance.
     *
     * @param dict the function dictionary
     */
    public PdfType3Function(PdfDictionary dict) {
        this(dict, DEFAULT_FUNCTION_FACTORY);
    }

    /**
     * (see ISO-320001 Table 41).
     *
     * @param domain    the valid input domain, input will be clipped to this domain
     *                  contains a min max pair per input component
     * @param range     the valid output range, oputput will be clipped to this range
     *                  contains a min max pair per output component
     * @param functions The list of functions to stitch*
     * @param bounds    (Required) An array of k ��� 1 numbers that, in combination with Domain, shall define
     *                  the intervals to which each function from the Functions array shall apply.
     *                  Bounds elements shall be in order of increasing value, and each value shall be within
     *                  the domain defined by Domain.
     * @param encode    (Required) An array of 2 �� k numbers that, taken in pairs, shall map each subset of the domain
     *                  defined by Domain and the Bounds array to the domain of the corresponding function.
     */
    public PdfType3Function(double[] domain, double[] range,
            List<AbstractPdfFunction<? extends PdfDictionary>> functions, double[] bounds, double[] encode) {
        super(new PdfDictionary(), PdfFunctionFactory.FUNCTION_TYPE_3, domain, range);
        functionFactory = DEFAULT_FUNCTION_FACTORY;
        final PdfArray funcs = new PdfArray();
        for (final AbstractPdfFunction<? extends PdfDictionary> func : functions) {
            funcs.add(func.getPdfObject());
        }
        super.getPdfObject().put(PdfName.Functions, funcs);
        super.getPdfObject().put(PdfName.Bounds, new PdfArray(bounds));
        super.getPdfObject().put(PdfName.Encode, new PdfArray(encode));
    }

    /**
     * (see ISO-320001 Table 41).
     *
     * @param domain    the valid input domain, input will be clipped to this domain
     *                  contains a min max pair per input component
     * @param range     the valid output range, oputput will be clipped to this range
     *                  contains a min max pair per output component
     * @param functions The list of functions to stitch*
     * @param bounds    (Required) An array of k ��� 1 numbers that, in combination with Domain, shall define
     *                  the intervals to which each function from the Functions array shall apply.
     *                  Bounds elements shall be in order of increasing value, and each value shall be within
     *                  the domain defined by Domain.
     * @param encode    (Required) An array of 2 �� k numbers that, taken in pairs, shall map each subset of the domain
     *                  defined by Domain and the Bounds array to the domain of the corresponding function.
     */
    public PdfType3Function(float[] domain, float[] range,
            List<AbstractPdfFunction<? extends PdfDictionary>> functions, float[] bounds, float[] encode) {
        this(convertFloatArrayToDoubleArray(domain), convertFloatArrayToDoubleArray(range),
                functions, convertFloatArrayToDoubleArray(bounds), convertFloatArrayToDoubleArray(encode));
    }


    PdfType3Function(PdfDictionary dict, IPdfFunctionFactory functionFactory) {
        super(dict);
        this.functionFactory = functionFactory;

        final PdfArray functionsArray = dict.getAsArray(PdfName.Functions);
        functions = Collections.unmodifiableList(checkAndGetFunctions(functionsArray));

        if (super.getDomain().length < 2) {
            throw new PdfException(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_DOMAIN);
        }

        final PdfArray boundsArray = dict.getAsArray(PdfName.Bounds);
        bounds = checkAndGetBounds(boundsArray);

        final PdfArray encodeArray = dict.getAsArray(PdfName.Encode);
        encode = checkAndGetEncode(encodeArray);
    }

    /**
     * (Required) An array of k 1-input functions that shall make up the stitching function.
     * The output dimensionality of all functions shall be the same, and compatible with the value
     * of Range if Range is present.
     *
     * <p>
     * (see ISO-320001 Table 41)
     *
     * @return the list of functions
     */
    public Collection<IPdfFunction> getFunctions() {
        return functions;
    }

    /**
     * (Required) An array of k 1-input functions that shall make up the stitching function.
     * The output dimensionality of all functions shall be the same, and compatible with the value
     * of Range if Range is present.
     *
     * <p>
     * (see ISO-320001 Table 41)
     *
     * @param value the list of functions
     */
    public void setFunctions(Iterable<AbstractPdfFunction<? extends PdfDictionary>> value) {
        final PdfArray pdfFunctions = new PdfArray();
        for (final AbstractPdfFunction<? extends PdfDictionary> f : value) {
            pdfFunctions.add(f.getPdfObject().getIndirectReference());
        }
        getPdfObject().put(PdfName.Functions, pdfFunctions);
    }

    /**
     * An array of k ��� 1 numbers that, in combination with Domain, shall define
     * the intervals to which each function from the Functions array shall apply.
     * Bounds elements shall be in order of increasing value, and each value shall be within
     * the domain defined by Domain.
     *
     * <p>
     * (see ISO-320001 Table 41)
     *
     * @return the bounds
     */
    public double[] getBounds() {
        return bounds;
    }

    /**
     * (Required) An array of k ��� 1 numbers that, in combination with Domain, shall define
     * the intervals to which each function from the Functions array shall apply.
     * Bounds elements shall be in order of increasing value, and each value shall be within
     * the domain defined by Domain.
     *
     * <p>
     * (see ISO-320001 Table 41)
     *
     * @param value the new set of bounds
     */

    public void setBounds(double[] value) {
        bounds = Arrays.copyOf(value, value.length);
    }

    /**
     * An array of 2 �� k numbers that, taken in pairs, shall map each subset of the domain defined
     * by Domain and the Bounds array to the domain of the corresponding function.
     *
     * <p>
     * (see ISO-320001 Table 41)
     *
     * @return the encode values
     */
    public double[] getEncode() {
        return getPdfObject().getAsArray(PdfName.Encode).toDoubleArray();
    }

    /**
     * (Required) An array of 2 �� k numbers that, taken in pairs, shall map each subset of the domain defined
     * by Domain and the Bounds array to the domain of the corresponding function.
     *
     * <p>
     * (see ISO-320001 Table 41)
     *
     * @param value the new set of encodings
     */
    public void setEncode(double[] value) {
        getPdfObject().put(PdfName.Encode, new PdfArray(value));
    }

    @Override
    public boolean checkCompatibilityWithColorSpace(PdfColorSpace alternateSpace) {
        return false;
    }

    /**
     * Gets output size of function.
     *
     * <p>
     * If Range field is absent, the output size of functions will be returned.
     *
     * @return output size of function
     */
    @Override
    public int getOutputSize() {
        return getRange() == null ? functions.get(0).getOutputSize() : getRange().length / 2;
    }

    @Override
    public double[] calculate(double[] input) {
        if (input == null || input.length != 1) {
            throw new PdfException(KernelExceptionMessageConstant.INVALID_INPUT_FOR_TYPE_3_FUNCTION);
        }
        double[] clipped = clipInput(input);
        double x = clipped[0];
        final int subdomain = calculateSubdomain(x);
        final double[] subdomainBorders = getSubdomainBorders(subdomain);
        x = mapValueFromActualRangeToExpected(x, subdomainBorders[0], subdomainBorders[1], encode[subdomain * 2],
                encode[(subdomain * 2) + 1]);

        final double[] output = functions.get(subdomain).calculate(new double[] {x});
        return clipOutput(output);
    }

    private int calculateSubdomain(double inputValue) {
        if (bounds.length > 0) {
            if (areThreeDoubleEqual(bounds[0], getDomain()[0], inputValue)) {
                return 0;
            }
            if (areThreeDoubleEqual(bounds[bounds.length - 1], getDomain()[1], inputValue)) {
                return bounds.length;
            }
        }

        for (int i = 0; i < bounds.length; i++) {
            if (inputValue < bounds[i]) {
                return i;
            }
        }
        return bounds.length;
    }

    private double[] getSubdomainBorders(int subdomain) {
        if (bounds.length == 0) {
            return getDomain();
        }
        if (subdomain == 0) {
            return new double[] {getDomain()[0], bounds[0]};
        } else if (subdomain == bounds.length) {
            return new double[] {bounds[bounds.length - 1], getDomain()[1]};
        } else {
            return new double[] {bounds[subdomain - 1], bounds[subdomain]};
        }
    }

    private List<IPdfFunction> checkAndGetFunctions(PdfArray functionsArray) {
        if (functionsArray == null || functionsArray.size() == 0) {
            throw new PdfException(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_FUNCTIONS);
        }

        Integer tempOutputSize = null;
        if (getRange()!= null)
        {
            tempOutputSize = getOutputSize();
        }
        final List<IPdfFunction> tempFunctions = new ArrayList<>();
        for (PdfObject funcObj : functionsArray) {
            if (!(funcObj instanceof PdfDictionary)) {
                continue;
            }
            final PdfDictionary funcDict = (PdfDictionary) funcObj;
            final IPdfFunction tempFunc = functionFactory.create(funcDict);
            if (tempOutputSize == null) {
                tempOutputSize = tempFunc.getOutputSize();
            }
            if (tempOutputSize != tempFunc.getOutputSize()) {
                throw new PdfException(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_FUNCTIONS_OUTPUT);
            }
            if (tempFunc.getInputSize() != 1) {
                throw new PdfException(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_FUNCTIONS_INPUT);
            }

            tempFunctions.add(tempFunc);
        }
        return tempFunctions;
    }

    private double[] checkAndGetBounds(PdfArray boundsArray) {
        if (boundsArray == null || boundsArray.size() != (functions.size() - 1)) {
            throw new PdfException(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_BOUNDS);
        }
        final double[] bounds = boundsArray.toDoubleArray();

        boolean areBoundsInvalid = false;
        for (int i = 0; i < bounds.length; i++) {
            areBoundsInvalid |= i == 0 ? bounds[i] < getDomain()[0] : bounds[i] <= getDomain()[0];
            areBoundsInvalid |= i == bounds.length - 1 ? getDomain()[1] < bounds[i] : getDomain()[1] <= bounds[i];
            areBoundsInvalid |= (i != 0 && bounds[i] <= bounds[i - 1]);
        }
        if (areBoundsInvalid) {
            throw new PdfException(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_BOUNDS);
        }
        return bounds;
    }

    private double[] checkAndGetEncode(PdfArray encodeArray) {
        if (encodeArray == null || encodeArray.size() < (functions.size() * 2)) {
            throw new PdfException(KernelExceptionMessageConstant.INVALID_TYPE_3_FUNCTION_NULL_ENCODE);
        }
        return encodeArray.toDoubleArray();
    }

    /**
     * Maps passed value from actual range to expected range.
     *
     * @param value  the value to map
     * @param aStart the start of actual range
     * @param aEnd   the end of actual range
     * @param eStart the start of expected range
     * @param eEnd   the end of expected range
     *
     * @return the mapped value
     */
    private static double mapValueFromActualRangeToExpected(double value, double aStart, double aEnd, double eStart,
            double eEnd) {

        // Present ranges [start, end] as [0, ...RangeLength].
        final double actualRangeLength = aEnd - aStart;
        if (actualRangeLength == 0) {
            return eStart;
        }
        final double expectedRangeLength = eEnd - eStart;

        // New input value = value - actual.start.
        final double x = value - aStart;
        final double y = (expectedRangeLength / actualRangeLength) * x;

        // Map y from range [0, expectedRangeLength] to [eStart, eEnd].
        return eStart + y;
    }

    private static boolean areThreeDoubleEqual(double first, double second, double third) {
        return Double.compare(first, second) == 0 && Double.compare(second, third) == 0;
    }
}