AbstractPdfType0FunctionTest.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.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.test.ExtendedITextTest;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;
@Tag("IntegrationTest")
public abstract class AbstractPdfType0FunctionTest extends ExtendedITextTest {
protected static final double DELTA = 1e-12;
private final int order;
protected AbstractPdfType0FunctionTest(int order) {
this.order = order;
}
@Test
public void testConstantFunctions() {
// f(x, y, z) = (1, 2, 3)
double[] expected = {-1, 0, 1};
PdfStream stream = new PdfStream(new byte[] {0, 0, 0});
stream.put(PdfName.Domain, new PdfArray(new double[] {0, 0, 0, 0, 0, 0}));
stream.put(PdfName.Size, new PdfArray(new int[] {2, 1, 3}));
stream.put(PdfName.Range, new PdfArray(
new double[] {expected[0], expected[0], expected[1], expected[1], expected[2], expected[2]}));
stream.put(PdfName.BitsPerSample, new PdfNumber(1));
PdfType0Function pdfFunction = new PdfType0Function(stream);
double[] actual = pdfFunction.calculate(new double[] {0, 10, -99});
Assertions.assertArrayEquals(expected, actual, DELTA);
}
@Test
public void testLinearFunctions() {
// f(x) = (x, 3x, 2-x, 2x-1) : [-1, 2] -> [-1,2]x[-3,6]x[0,3]x[-3,3]
Function<Double, List<Double>> function = x -> Arrays.asList(x, 3 * x, 2 - x, 2 * x - 1);
double[] domain = {-1, 2};
int[] size = {2};
double[] range = {-1, 2, -3, 6, 0, 3, -3, 3};
int bitsPerSample = 1;
byte[] samples = {0x2d};
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
null, null, bitsPerSample, samples);
double[] arguments = {-1.0, -0.67, -0.33, 0.0, 0.33, 0.67, 1.0, 1.33, 1.67, 2.0};
for (double argument : arguments) {
List<Double> fRes = function.apply(argument);
Stream<Double> stream = fRes.stream();
double[] expected = stream.mapToDouble(x -> x).toArray();
double[] actual = pdfFunction.calculate(new double[] {argument});
Assertions.assertArrayEquals(expected, actual, DELTA);
}
}
@Test
public void testLinearFunctionsWithEncoding() {
// f(x) = (x, 3x, 2-x, 2x-1) : [-1, 2] -> [-1,2]x[-3,6]x[0,3]x[-3,3]
Function<Double, List<Double>> function = x -> Arrays.asList(x, 3 * x, 2 - x, 2 * x - 1);
double[] domain = {-1, 2};
int[] size = {4};
double[] range = {-1, 2, -3, 6, 0, 3, -3, 3};
int[] encode = {1, 2};
int bitsPerSample = 1;
byte[] samples = {(byte) 0xf2, (byte) 0xd0};
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
encode, null, bitsPerSample, samples);
double[] arguments = {-1.0, -0.67, -0.33, 0.0, 0.33, 0.67, 1.0, 1.33, 1.67, 2.0};
for (double argument : arguments) {
double[] expected = function.apply(argument).stream().mapToDouble(x -> x).toArray();
double[] actual = pdfFunction.calculate(new double[] {argument});
Assertions.assertArrayEquals(expected, actual, DELTA);
}
}
@Test
public void testLinearFunctionsDim3() {
// f(x, y, z) = (x+y, x-y, x+y+z) : [0,1]x[0,1]x[0,1] -> [0,2]x[-1,1]x[0,3]
Function<List<Double>, List<Double>> function = x -> Arrays
.asList(x.get(0) + x.get(1), x.get(0) - x.get(1), x.get(0) + x.get(1) + x.get(2));
double[] domain = {0, 1, 0, 1, 0, 1};
int[] size = {2, 2, 2};
double[] range = {0, 2, -1, 1, 0, 3};
double[] decode = {0, 3, -1, 2, 0, 3};
int bitsPerSample = 2;
byte[] samples = {0x11, (byte) 0x94, 0x66, 0x15, (byte) 0xa4, (byte) 0xa7};
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
null, decode, bitsPerSample, samples);
List<List<Double>> arguments = Arrays.asList(
Arrays.asList(0.0, 0.0, 0.0),
Arrays.asList(1.0, 1.0, 1.0),
Arrays.asList(0.05, 0.95, 0.35),
Arrays.asList(0.15, 0.01, 0.88),
Arrays.asList(0.99, 0.99, 0.5),
Arrays.asList(0.98, 0.1, 0.01)
);
for (List<Double> argument : arguments) {
double[] expected = function.apply(argument).stream().mapToDouble(d -> d).toArray();
double[] actual = pdfFunction.calculate(argument.stream().mapToDouble(d -> d).toArray());
Assertions.assertArrayEquals(expected, actual, DELTA);
}
}
protected void testPolynomials(double[] expectedDelta) {
// f(x) = (x^4, 1 - x + x^3, 1 - x^2) : [-1,1] -> [0,1]x[0.5,1.5]x[0,1]
Function<Double, List<Double>> function = x -> {
double x2 = x * x;
return Arrays.asList(x2 * x2, 1 - x + x * x2, 1 - x2);
};
double[] domain = {-1, 1};
int[] size = {5};
double[] range = {0, 1, 0.5, 1.5, 0, 1};
double[] decode = {0, 15.9375, 0, 31.875, 0, 63.75};
int bitsPerSample = 8;
byte[] samples = {16, 8, 0, 1, 11, 3, 0, 8, 4, 1, 5, 3, 16, 8, 0};
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
null, decode, bitsPerSample, samples);
double[] arguments = {-1.0, -0.67, -0.33, 0.0, 0.33, 0.67, 1.0};
for (double argument : arguments) {
double[] expected = function.apply(argument).stream().mapToDouble(x -> x).toArray();
double[] actual = pdfFunction.calculate(new double[] {argument});
for (int i = 0; i < expectedDelta.length; ++i) {
Assertions.assertEquals(expected[i], actual[i], expectedDelta[i]);
}
}
}
protected void testPolynomialsWithEncoding(double[] expectedDelta) {
// f(x) = (x^4, 1 - x + x^3, 1 - x^2) : [-1,1] -> [0,1]x[0.5,1.5]x[0,1]
Function<Double, List<Double>> function = x -> {
double x2 = x * x;
return Arrays.asList(x2 * x2, 1 - x + x * x2, 1 - x2);
};
double[] domain = {-1, 1};
int[] size = {10};
double[] range = {0, 1, 0.5, 1.5, 0, 1};
int[] encode = {3, 7};
double[] decode = {0, 15.9375, 0, 31.875, 0, 63.75};
int bitsPerSample = 8;
byte[] samples = {
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
(byte) 0xff,
16, 8, 0, 1, 11, 3, 0, 8, 4, 1, 5, 3, 16, 8, 0,
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
(byte) 0xff
};
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
encode, decode, bitsPerSample, samples);
double[] arguments = {-1.0, -0.67, -0.33, 0.0, 0.33, 0.67, 1.0};
for (double argument : arguments) {
double[] expected = function.apply(argument).stream().mapToDouble(x -> x).toArray();
double[] actual = pdfFunction.calculate(new double[] {argument});
for (int i = 0; i < expectedDelta.length; ++i) {
Assertions.assertEquals(expected[i], actual[i], expectedDelta[i]);
}
}
}
protected void testPolynomialsDim2(double[] expectedDelta) {
// f(x, y) = (x^2+y, x+xy); [0, 1]x[0, 1] -> [0, 2]x[0,2]
Function<List<Double>, List<Double>> function = x -> Arrays
.asList(x.get(0) * x.get(0) + x.get(1), x.get(0) + x.get(0) * x.get(1));
double[] domain = {0, 1, 0, 1};
int[] size = {6, 2};
double[] range = {0, 2, 0, 2};
double[] decode = {0, 10.2, 0, 51};
int bitsPerSample = 8;
byte[] samples = {0, 0, 1, 1, 4, 2, 9, 3, 16, 4, 25, 5, 25, 0, 26, 2, 29, 4, 34, 6, 41, 8, 50, 10};
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
null, decode, bitsPerSample, samples);
List<List<Double>> arguments = Arrays.asList(
Arrays.asList(0.0, 0.0),
Arrays.asList(1.0, 1.0),
Arrays.asList(0.05, 0.99),
Arrays.asList(0.9, 0.55),
Arrays.asList(0.35, 0.11),
Arrays.asList(0.5, 0.99)
);
for (List<Double> argument : arguments) {
double[] expected = function.apply(argument).stream().mapToDouble(d -> d).toArray();
double[] actual = pdfFunction.calculate(argument.stream().mapToDouble(d -> d).toArray());
for (int i = 0; i < expectedDelta.length; ++i) {
Assertions.assertEquals(expected[i], actual[i], expectedDelta[i]);
}
}
}
protected void testPolynomialsDim2WithEncoding(double[] expectedDelta) {
// f(x, y) = (x^2+y, x+xy); [0, 1]x[0, 1] -> [0, 2]x[0,2]
Function<List<Double>, List<Double>> function = x -> Arrays
.asList(x.get(0) * x.get(0) + x.get(1), x.get(0) + x.get(0) * x.get(1));
double[] domain = {0, 1, 0, 1};
int[] size = {10, 5};
double[] range = {0, 2, 0, 2};
int[] encode = {2, 7, 3, 4};
double[] decode = {0, 10.2, 0, 51};
int bitsPerSample = 8;
byte[] samples = {
0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9, 0,
0, 1, 1, 1, 2, 1, 3, 1, 4, 1, 5, 1, 6, 1, 7, 1, 8, 1, 9, 1,
0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5, 2, 6, 2, 7, 2, 8, 2, 9, 2,
0, 3, 1, 3, 0, 0, 1, 1, 4, 2, 9, 3, 16, 4, 25, 5, 8, 3, 9, 3,
0, 4, 1, 4, 25, 0, 26, 2, 29, 4, 34, 6, 41, 8, 50, 10, 8, 4, 9, 4
};
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
encode, decode, bitsPerSample, samples);
List<List<Double>> arguments = Arrays.asList(
Arrays.asList(0.0, 0.0),
Arrays.asList(1.0, 1.0),
Arrays.asList(0.05, 0.99),
Arrays.asList(0.9, 0.55),
Arrays.asList(0.35, 0.11),
Arrays.asList(0.5, 0.99)
);
for (List<Double> argument : arguments) {
double[] expected = function.apply(argument).stream().mapToDouble(d -> d).toArray();
double[] actual = pdfFunction.calculate(argument.stream().mapToDouble(d -> d).toArray());
for (int i = 0; i < expectedDelta.length; ++i) {
Assertions.assertEquals(expected[i], actual[i], expectedDelta[i]);
}
}
}
protected void testSinus(double delta) {
// f(x) = sin(x) : [0, 180] -> [0, 1]
Function<Double, Double> function = x -> Math.sin(Math.toRadians(x));
double[] domain = {0, 180};
int[] size = {21};
double[] range = {0, 1};
int bitsPerSample = 32;
byte[] samples = generate1Dim32BitSamples(function, size[0], domain, range);
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
null, null, bitsPerSample, samples);
for (int i = 0; i <= 180; ++i) {
double expected = function.apply((double) i);
double actual = pdfFunction.calculate(new double[] {i})[0];
Assertions.assertEquals(expected, actual, delta);
}
}
protected void testExponent(double delta) {
// f(x) = e^x : [0, 2] -> [1, e^2]
Function<Double, Double> function = (Double val) -> Math.exp(val);
double[] domain = {0, 2};
int[] size = {9};
double[] range = {1, function.apply(domain[1])};
int bitsPerSample = 32;
byte[] samples = generate1Dim32BitSamples(function, size[0], domain, range);
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
null, null, bitsPerSample, samples);
double[] arguments = new double[21];
for (int i = 0; i < 21; i++) {
arguments[i] = i / 10;
}
//double[] arguments = DoubleStream.iterate(0, x -> x + 0.1).limit(21).toArray();
for (double argument : arguments) {
double expected = function.apply(argument);
double actual = pdfFunction.calculate(new double[] {argument})[0];
Assertions.assertEquals(expected, actual, delta);
}
}
protected void testLogarithm(double delta) {
// f(x) = ln(x) : [1,10] -> [0,ln(10)]
Function<Double, Double> function = (Double val) -> Math.log(val);
double[] domain = {1, 10};
int[] size = {10};
double[] range = {0, function.apply(domain[1])};
int bitsPerSample = 32;
byte[] samples = generate1Dim32BitSamples(function, size[0], domain, range);
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
null, null, bitsPerSample, samples);
double[] arguments = new double[37];
for (int i = 0; i < 37; i++) {
arguments[i] = 1 + i / 4;
}
//double[] arguments = DoubleStream.iterate(1, x -> x + 0.25).limit(37).toArray();
for (double argument : arguments) {
double expected = function.apply(argument);
double actual = pdfFunction.calculate(new double[] {argument})[0];
Assertions.assertEquals(expected, actual, delta);
}
}
protected void testGeneralInterpolation(double delta) {
// f(x, y) = exp(xy) / (1 + y^2) : [0,1]x[0,1] - > [0.5, e]
Function<List<Double>, Double> function = x ->
Math.exp(x.get(0) * x.get(1)) / (1 + x.get(1) * x.get(1));
double[] domain = {0, 1, 0, 1};
int[] size = {5, 5};
double[] range = {0.5, Math.E};
int bitsPerSample = 32;
byte[] samples = generate2Dim32BitSamples(function, size, domain, range);
PdfType0Function pdfFunction = new PdfType0Function(domain, size, range, order,
null, null, bitsPerSample, samples);
for (double x = 0; x < 1.01; x += 0.03) {
for (double y = 0; y < 1.01; y += 0.03) {
double expected = function.apply(Arrays.asList(x, y));
double actual = pdfFunction.calculate(new double[] {x, y})[0];
Assertions.assertEquals(expected, actual, delta);
}
}
}
private byte[] generate1Dim32BitSamples(Function<Double, Double> function, int size, double[] domain,
double[] range) {
byte[] samples = new byte[size << 2];
int pos = 0;
for (int i = 0; i < size; ++i) {
double value = function.apply(domain[0] + i * (domain[1] - domain[0]) / (size - 1));
long sampleValue = (long) Math.round(0xffffffffL * ((value - range[0]) / (range[1] - range[0])));
for (int k = 24; k >= 0; k -= 8) {
samples[pos++] = (byte) (((sampleValue) >> k) & 0xff);
}
}
return samples;
}
private byte[] generate2Dim32BitSamples(Function<List<Double>, Double> function, int[] size, double[] domain,
double[] range) {
byte[] samples = new byte[(size[0] * size[1]) << 2];
int pos = 0;
for (int i = 0; i < size[1]; ++i) {
for (int j = 0; j < size[0]; ++j) {
double value = function.apply(Arrays.asList(
domain[0] + j * (domain[1] - domain[0]) / (size[0] - 1),
domain[0] + i * (domain[1] - domain[0]) / (size[1] - 1)
));
long sampleValue = (long) Math.round(0xffffffffL * ((value - range[0]) / (range[1] - range[0])));
for (int k = 24; k >= 0; k -= 8) {
samples[pos++] = (byte) (((sampleValue) >> k) & 0xff);
}
}
}
return samples;
}
}