diff --git a/src/UglyToad.PdfPig.Core/PdfRange.cs b/src/UglyToad.PdfPig.Core/PdfRange.cs
new file mode 100644
index 00000000..471d4e95
--- /dev/null
+++ b/src/UglyToad.PdfPig.Core/PdfRange.cs
@@ -0,0 +1,90 @@
+namespace UglyToad.PdfPig.Core
+{
+ using System.Collections.Generic;
+ using System.Linq;
+
+ ///
+ /// This class will be used to signify a range. a(min) <= a* <= a(max)
+ ///
+ public struct PdfRange
+ {
+ private readonly IReadOnlyList rangeArray;
+ private readonly int startingIndex;
+
+ ///
+ /// Constructor with an initial range of 0..1.
+ ///
+ public PdfRange()
+ {
+ rangeArray = new double[] { 0.0, 1.0 };
+ startingIndex = 0;
+ }
+
+ ///
+ /// Constructor assumes a starting index of 0.
+ ///
+ /// The array that describes the range.
+ public PdfRange(IEnumerable range)
+ : this(range.Select(v => (double)v), 0)
+ {
+ }
+
+ ///
+ /// Constructor with an index into an array. Because some arrays specify
+ /// multiple ranges ie [0, 1, 0, 2, 2, 3]. It is convenient for this
+ /// class to take an index into an array. So if you want this range to
+ /// represent 0, 2 in the above example then you would say new PDRange(array, 1).
+ ///
+ /// The array that describes the index
+ /// The range index into the array for the start of the range.
+ public PdfRange(IEnumerable range, int index)
+ : this(range.Select(v => (double)v), index)
+ {
+ }
+
+ ///
+ /// Constructor assumes a starting index of 0.
+ ///
+ /// The array that describes the range.
+ public PdfRange(IEnumerable range)
+ : this(range, 0)
+ {
+ }
+
+ ///
+ /// Constructor with an index into an array. Because some arrays specify
+ /// multiple ranges ie [0, 1, 0, 2, 2, 3]. It is convenient for this
+ /// class to take an index into an array. So if you want this range to
+ /// represent 0, 2 in the above example then you would say new PDRange(array, 1).
+ ///
+ /// The array that describes the index
+ /// The range index into the array for the start of the range.
+ public PdfRange(IEnumerable range, int index)
+ {
+ rangeArray = range.Select(v => (double)v).ToArray();
+ startingIndex = index;
+ }
+
+ ///
+ /// The minimum value of the range.
+ ///
+ public double Min
+ {
+ get
+ {
+ return rangeArray[startingIndex * 2];
+ }
+ }
+
+ ///
+ /// The maximum value of the range.
+ ///
+ public double Max
+ {
+ get
+ {
+ return rangeArray[startingIndex * 2 + 1];
+ }
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType0Tests.cs b/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType0Tests.cs
new file mode 100644
index 00000000..05cbe559
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType0Tests.cs
@@ -0,0 +1,200 @@
+namespace UglyToad.PdfPig.Tests.Functions
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using UglyToad.PdfPig.Functions;
+ using UglyToad.PdfPig.Tokens;
+ using Xunit;
+
+ public class PdfFunctionType0Tests
+ {
+ private static ArrayToken GetArrayToken(params double[] data)
+ {
+ return new ArrayToken(data.Select(v => new NumericToken((decimal)v)).ToArray());
+ }
+
+ [Fact]
+ public void TIKA_1228_0()
+ {
+ DictionaryToken dictionaryToken = new DictionaryToken(new Dictionary()
+ {
+ { NameToken.FunctionType, new NumericToken(0) },
+ { NameToken.Domain, GetArrayToken(0, 1) },
+ { NameToken.Range, GetArrayToken(0, 1, 0, 1, 0, 1, 0, 1) },
+
+ { NameToken.BitsPerSample, new NumericToken(8) },
+ { NameToken.Decode, GetArrayToken(0, 1, 0, 1, 0, 1, 0, 1) },
+ { NameToken.Encode, GetArrayToken(0, 254) },
+ { NameToken.Size, GetArrayToken(255) }
+ });
+
+ byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 15, 0, 0, 0, 16, 0, 0, 0, 17, 0, 0, 0, 18, 0, 0, 0, 19, 0, 0, 0, 20, 0, 0, 0, 21, 0, 0, 0, 22, 0, 0, 0, 23, 0, 0, 0, 24, 0, 0, 0, 25, 0, 0, 0, 26, 0, 0, 0, 27, 0, 0, 0, 28, 0, 0, 0, 29, 0, 0, 0, 30, 0, 0, 0, 31, 0, 0, 0, 32, 0, 0, 0, 33, 0, 0, 0, 34, 0, 0, 0, 35, 0, 0, 0, 36, 0, 0, 0, 37, 0, 0, 0, 38, 0, 0, 0, 39, 0, 0, 0, 40, 0, 0, 0, 41, 0, 0, 0, 42, 0, 0, 0, 43, 0, 0, 0, 44, 0, 0, 0, 45, 0, 0, 0, 46, 0, 0, 0, 47, 0, 0, 0, 48, 0, 0, 0, 49, 0, 0, 0, 50, 0, 0, 0, 51, 0, 0, 0, 52, 0, 0, 0, 53, 0, 0, 0, 54, 0, 0, 0, 55, 0, 0, 0, 56, 0, 0, 0, 57, 0, 0, 0, 58, 0, 0, 0, 59, 0, 0, 0, 60, 0, 0, 0, 61, 0, 0, 0, 62, 0, 0, 0, 63, 0, 0, 0, 64, 0, 0, 0, 65, 0, 0, 0, 66, 0, 0, 0, 67, 0, 0, 0, 68, 0, 0, 0, 69, 0, 0, 0, 70, 0, 0, 0, 71, 0, 0, 0, 72, 0, 0, 0, 73, 0, 0, 0, 74, 0, 0, 0, 75, 0, 0, 0, 76, 0, 0, 0, 77, 0, 0, 0, 78, 0, 0, 0, 79, 0, 0, 0, 80, 0, 0, 0, 81, 0, 0, 0, 82, 0, 0, 0, 83, 0, 0, 0, 84, 0, 0, 0, 85, 0, 0, 0, 86, 0, 0, 0, 87, 0, 0, 0, 88, 0, 0, 0, 89, 0, 0, 0, 90, 0, 0, 0, 91, 0, 0, 0, 92, 0, 0, 0, 93, 0, 0, 0, 94, 0, 0, 0, 95, 0, 0, 0, 96, 0, 0, 0, 97, 0, 0, 0, 98, 0, 0, 0, 99, 0, 0, 0, 100, 0, 0, 0, 101, 0, 0, 0, 102, 0, 0, 0, 103, 0, 0, 0, 104, 0, 0, 0, 105, 0, 0, 0, 106, 0, 0, 0, 107, 0, 0, 0, 108, 0, 0, 0, 109, 0, 0, 0, 110, 0, 0, 0, 111, 0, 0, 0, 112, 0, 0, 0, 113, 0, 0, 0, 114, 0, 0, 0, 115, 0, 0, 0, 116, 0, 0, 0, 117, 0, 0, 0, 118, 0, 0, 0, 119, 0, 0, 0, 120, 0, 0, 0, 121, 0, 0, 0, 122, 0, 0, 0, 123, 0, 0, 0, 124, 0, 0, 0, 125, 0, 0, 0, 126, 0, 0, 0, 128, 0, 0, 0, 129, 0, 0, 0, 130, 0, 0, 0, 131, 0, 0, 0, 132, 0, 0, 0, 133, 0, 0, 0, 134, 0, 0, 0, 135, 0, 0, 0, 136, 0, 0, 0, 137, 0, 0, 0, 138, 0, 0, 0, 139, 0, 0, 0, 140, 0, 0, 0, 141, 0, 0, 0, 142, 0, 0, 0, 143, 0, 0, 0, 144, 0, 0, 0, 145, 0, 0, 0, 146, 0, 0, 0, 147, 0, 0, 0, 148, 0, 0, 0, 149, 0, 0, 0, 150, 0, 0, 0, 151, 0, 0, 0, 152, 0, 0, 0, 153, 0, 0, 0, 154, 0, 0, 0, 155, 0, 0, 0, 156, 0, 0, 0, 157, 0, 0, 0, 158, 0, 0, 0, 159, 0, 0, 0, 160, 0, 0, 0, 161, 0, 0, 0, 162, 0, 0, 0, 163, 0, 0, 0, 164, 0, 0, 0, 165, 0, 0, 0, 166, 0, 0, 0, 167, 0, 0, 0, 168, 0, 0, 0, 169, 0, 0, 0, 170, 0, 0, 0, 171, 0, 0, 0, 172, 0, 0, 0, 173, 0, 0, 0, 174, 0, 0, 0, 175, 0, 0, 0, 176, 0, 0, 0, 177, 0, 0, 0, 178, 0, 0, 0, 179, 0, 0, 0, 180, 0, 0, 0, 181, 0, 0, 0, 182, 0, 0, 0, 183, 0, 0, 0, 184, 0, 0, 0, 185, 0, 0, 0, 186, 0, 0, 0, 187, 0, 0, 0, 188, 0, 0, 0, 189, 0, 0, 0, 190, 0, 0, 0, 191, 0, 0, 0, 192, 0, 0, 0, 193, 0, 0, 0, 194, 0, 0, 0, 195, 0, 0, 0, 196, 0, 0, 0, 197, 0, 0, 0, 198, 0, 0, 0, 199, 0, 0, 0, 200, 0, 0, 0, 201, 0, 0, 0, 202, 0, 0, 0, 203, 0, 0, 0, 204, 0, 0, 0, 205, 0, 0, 0, 206, 0, 0, 0, 207, 0, 0, 0, 208, 0, 0, 0, 209, 0, 0, 0, 210, 0, 0, 0, 211, 0, 0, 0, 212, 0, 0, 0, 213, 0, 0, 0, 214, 0, 0, 0, 215, 0, 0, 0, 216, 0, 0, 0, 217, 0, 0, 0, 218, 0, 0, 0, 219, 0, 0, 0, 220, 0, 0, 0, 221, 0, 0, 0, 222, 0, 0, 0, 223, 0, 0, 0, 224, 0, 0, 0, 225, 0, 0, 0, 226, 0, 0, 0, 227, 0, 0, 0, 228, 0, 0, 0, 229, 0, 0, 0, 230, 0, 0, 0, 231, 0, 0, 0, 232, 0, 0, 0, 233, 0, 0, 0, 234, 0, 0, 0, 235, 0, 0, 0, 236, 0, 0, 0, 237, 0, 0, 0, 238, 0, 0, 0, 239, 0, 0, 0, 240, 0, 0, 0, 241, 0, 0, 0, 242, 0, 0, 0, 243, 0, 0, 0, 244, 0, 0, 0, 245, 0, 0, 0, 246, 0, 0, 0, 247, 0, 0, 0, 248, 0, 0, 0, 249, 0, 0, 0, 250, 0, 0, 0, 251, 0, 0, 0, 252, 0, 0, 0, 253, 0, 0, 0, 254, 0, 0, 0, 255 };
+
+ StreamToken function = new StreamToken(dictionaryToken, data);
+
+ var function0 = new PdfFunctionType0(function);
+ var result = function0.Eval(new double[] { 0 });
+ Assert.Equal(4, result.Length);
+ result = function0.Eval(new double[] { 0.5 });
+ Assert.Equal(4, result.Length);
+ result = function0.Eval(new double[] { 1 });
+ Assert.Equal(4, result.Length);
+ result = function0.Eval(new double[] { 0.2 });
+ Assert.Equal(4, result.Length);
+ }
+
+ [Fact]
+ public void Simple16()
+ {
+ DictionaryToken dictionaryToken = new DictionaryToken(new Dictionary()
+ {
+ { NameToken.FunctionType, new NumericToken(0) },
+ { NameToken.Domain, GetArrayToken(0, 1) },
+ { NameToken.Range, GetArrayToken(0, 1) },
+
+ { NameToken.BitsPerSample, new NumericToken(16) },
+ { NameToken.Size, GetArrayToken(5) }
+ });
+
+ byte[] data = new ushort[] { 0, 8192, 16384, 32768, 65535 }.SelectMany(v => BitConverter.GetBytes(v)).ToArray();
+
+ StreamToken function = new StreamToken(dictionaryToken, data);
+
+ var function0 = new PdfFunctionType0(function);
+ var result = function0.Eval(new double[] { 0.00 });
+ Assert.Single(result);
+ Assert.Equal(0.0, result[0], 3);
+
+ result = function0.Eval(new double[] { 0.25 });
+ Assert.Single(result);
+ Assert.Equal(0.125, result[0], 3);
+
+ result = function0.Eval(new double[] { 0.50 });
+ Assert.Single(result);
+ Assert.Equal(0.25, result[0], 2);
+
+ result = function0.Eval(new double[] { 0.75 });
+ Assert.Single(result);
+ Assert.Equal(0.50, result[0], 2);
+
+ result = function0.Eval(new double[] { 1.0 });
+ Assert.Single(result);
+ Assert.Equal(1.00, result[0], 2);
+ }
+
+ [Fact]
+ public void Simple8()
+ {
+ DictionaryToken dictionaryToken = new DictionaryToken(new Dictionary()
+ {
+ { NameToken.FunctionType, new NumericToken(0) },
+ { NameToken.Domain, GetArrayToken(0, 1) },
+ { NameToken.Range, GetArrayToken(0, 1) },
+
+ { NameToken.BitsPerSample, new NumericToken(8) },
+ { NameToken.Size, GetArrayToken(5) }
+ });
+
+ byte[] data = new byte[] { 0, 32, 64, 128, 255 };
+
+ StreamToken function = new StreamToken(dictionaryToken, data);
+
+ var function0 = new PdfFunctionType0(function);
+ var result = function0.Eval(new double[] { 0.00 });
+ Assert.Single(result);
+ Assert.Equal(0.0, result[0], 3);
+
+ result = function0.Eval(new double[] { 0.25 });
+ Assert.Single(result);
+ Assert.Equal(0.125, result[0], 3);
+
+ result = function0.Eval(new double[] { 0.50 });
+ Assert.Single(result);
+ Assert.Equal(0.25, result[0], 2);
+
+ result = function0.Eval(new double[] { 0.75 });
+ Assert.Single(result);
+ Assert.Equal(0.50, result[0], 2);
+
+ result = function0.Eval(new double[] { 1.0 });
+ Assert.Single(result);
+ Assert.Equal(1.00, result[0], 2);
+ }
+
+ [Fact]
+ public void RgbColorSpace()
+ {
+ DictionaryToken dictionaryToken = new DictionaryToken(new Dictionary()
+ {
+ { NameToken.FunctionType, new NumericToken(0) },
+ { NameToken.Domain, GetArrayToken(0, 1, 0, 1) },
+ { NameToken.Range, GetArrayToken(0, 1, 0, 1, 0, 1) },
+
+ { NameToken.BitsPerSample, new NumericToken(8) },
+ { NameToken.Size, GetArrayToken(2, 2) }
+ });
+
+ byte[] data = new byte[] { 255, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255 };
+
+ StreamToken function = new StreamToken(dictionaryToken, data);
+
+ var function0 = new PdfFunctionType0(function);
+ var result = function0.Eval(new double[] { 0, 0 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 1, 1, 0 }, result); // yellow
+
+ result = function0.Eval(new double[] { 1, 0 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 0, 0, 0 }, result); // black
+
+ result = function0.Eval(new double[] { 0, 1 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 1, 0, 0 }, result); // red
+
+ result = function0.Eval(new double[] { 1, 1 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 0, 0, 1 }, result); // blue
+
+ result = function0.Eval(new double[] { 0.5, 0.5 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 0.5, 0.25, 0.25 }, result); // Mid point
+ }
+
+ [Fact]
+ public void RedBlueGradient()
+ {
+ DictionaryToken dictionaryToken = new DictionaryToken(new Dictionary()
+ {
+ { NameToken.FunctionType, new NumericToken(0) },
+ { NameToken.Domain, GetArrayToken(0, 1) },
+ { NameToken.Range, GetArrayToken(0, 1, 0, 1, 0, 1) },
+
+ { NameToken.BitsPerSample, new NumericToken(8) },
+ { NameToken.Size, GetArrayToken(2) }
+ });
+
+ byte[] data = new byte[] { 255, 0, 0, 0, 0, 255 };
+
+ StreamToken function = new StreamToken(dictionaryToken, data);
+
+ var function0 = new PdfFunctionType0(function);
+
+ var result = function0.Eval(new double[] { 0 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 1, 0, 0 }, result); // red
+
+ result = function0.Eval(new double[] { 1 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 0, 0, 1 }, result); // blue
+
+ result = function0.Eval(new double[] { 0.5 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 0.5, 0.0, 0.5 }, result); // Mid point
+
+ result = function0.Eval(new double[] { 0.3333 });
+ Assert.Equal(3, result.Length);
+ Assert.Equal(new double[] { 0.6667, 0.0, 0.3333 }, result); // 1/3 point
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType2Tests.cs b/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType2Tests.cs
new file mode 100644
index 00000000..96a6d814
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType2Tests.cs
@@ -0,0 +1,169 @@
+namespace UglyToad.PdfPig.Tests.Functions
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using UglyToad.PdfPig.Functions;
+ using UglyToad.PdfPig.Tokens;
+ using Xunit;
+
+ public class PdfFunctionType2Tests
+ {
+ private PdfFunctionType2 CreateFunction(double[] domain, double[] range, double[] c0, double[] c1, double n)
+ {
+ DictionaryToken dictionaryToken = new DictionaryToken(new Dictionary()
+ {
+ { NameToken.FunctionType, new NumericToken(4) },
+ { NameToken.Domain, new ArrayToken(domain.Select(v => new NumericToken((decimal)v)).ToArray()) },
+ { NameToken.Range, new ArrayToken(range.Select(v => new NumericToken((decimal)v)).ToArray()) },
+
+ { NameToken.C0, new ArrayToken(c0.Select(v => new NumericToken((decimal)v)).ToArray()) },
+ { NameToken.C1, new ArrayToken(c1.Select(v => new NumericToken((decimal)v)).ToArray()) },
+ { NameToken.N, new NumericToken((decimal)n) },
+ });
+
+ return new PdfFunctionType2(dictionaryToken);
+ }
+
+ [Fact]
+ public void Simple()
+ {
+ PdfFunctionType2 function = CreateFunction(
+ new double[] { -1.0, 1.0, -1.0, 1.0 },
+ new double[] { -1.0, 1.0 },
+ new double[] { 0.0 },
+ new double[] { 1.0 },
+ 1);
+
+ Assert.Equal(FunctionTypes.Exponential, function.FunctionType);
+
+ double[] input = new double[] { -0.7 };
+ double[] output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(-0.7, output[0], 4);
+
+ input = new double[] { 0.7 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(0.7, output[0], 4);
+
+ input = new double[] { -0.5 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(-0.5, output[0], 4);
+
+ input = new double[] { 0.5 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(0.5, output[0], 4);
+
+ input = new double[] { 0 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(0, output[0], 4);
+
+ input = new double[] { 1 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(1, output[0], 4);
+
+ input = new double[] { -1 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(-1, output[0], 4);
+ }
+
+ [Fact]
+ public void SimpleClip()
+ {
+ PdfFunctionType2 function = CreateFunction(
+ new double[] { -1.0, 1.0, -1.0, 1.0 },
+ new double[] { -1.0, 1.0 },
+ new double[] { 0.0 },
+ new double[] { 1.0 },
+ 1);
+
+ double[] input = new double[] { -15 };
+ double[] output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(-1, output[0], 4);
+
+ input = new double[] { 15 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(1, output[0], 4);
+ }
+
+ [Fact]
+ public void N2()
+ {
+ PdfFunctionType2 function = CreateFunction(
+ new double[] { -1.0, 1.0, -1.0, 1.0 },
+ new double[] { -10.0, 10.0 },
+ new double[] { 0.0 },
+ new double[] { 1.0 },
+ 2);
+
+ double[] input = new double[] { 1.12 };
+ double[] output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(1.2544, output[0], 4);
+
+ input = new double[] { -1.35 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(1.82250, output[0], 4);
+
+ input = new double[] { 5 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(10, output[0], 4); // clip
+ }
+
+ [Fact]
+ public void N3()
+ {
+ PdfFunctionType2 function = CreateFunction(
+ new double[] { -1.0, 1.0, -1.0, 1.0 },
+ new double[] { -10.0, 10.0 },
+ new double[] { 4.0 },
+ new double[] { 9.53 },
+ 3);
+
+ double[] input = new double[] { 1.0 };
+ double[] output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(9.53, output[0], 4);
+
+ input = new double[] { -1.236 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(-6.44192, output[0], 4);
+ }
+
+ [Fact]
+ public void NSqrt()
+ {
+ PdfFunctionType2 function = CreateFunction(
+ new double[] { -1.0, 1.0, -1.0, 1.0 },
+ new double[] { -10.0, 10.0 },
+ new double[] { 2.589 },
+ new double[] { 10.58 },
+ 0.5);
+
+ double[] input = new double[] { 0.5 };
+ double[] output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(8.23949, output[0], 4);
+
+ input = new double[] { 0.78 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.Equal(9.64646, output[0], 4);
+
+ input = new double[] { -0.78 };
+ output = function.Eval(input);
+ Assert.Single(output);
+ Assert.True(double.IsNaN(output[0])); // negative input with sqrt
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType3Tests.cs b/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType3Tests.cs
new file mode 100644
index 00000000..70e9aaff
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType3Tests.cs
@@ -0,0 +1,11 @@
+namespace UglyToad.PdfPig.Tests.Functions
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ public class PdfFunctionType3Tests
+ {
+ // TODO
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType4Tests.cs b/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType4Tests.cs
new file mode 100644
index 00000000..ee49a0fc
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Functions/PdfFunctionType4Tests.cs
@@ -0,0 +1,96 @@
+namespace UglyToad.PdfPig.Tests.Functions
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using UglyToad.PdfPig.Functions;
+ using UglyToad.PdfPig.Tokens;
+ using Xunit;
+
+ public class PdfFunctionType4Tests
+ {
+ private PdfFunctionType4 CreateFunction(string function, double[] domain, double[] range)
+ {
+ DictionaryToken dictionaryToken = new DictionaryToken(new Dictionary()
+ {
+ { NameToken.FunctionType, new NumericToken(4) },
+ { NameToken.Domain, new ArrayToken(domain.Select(v => new NumericToken((decimal)v)).ToArray()) },
+ { NameToken.Range, new ArrayToken(range.Select(v => new NumericToken((decimal)v)).ToArray()) },
+ });
+
+ var data = Encoding.ASCII.GetBytes(function); // OtherEncodings.Iso88591.GetBytes(function);
+ StreamToken stream = new StreamToken(dictionaryToken, data);
+
+ return new PdfFunctionType4(stream);
+ }
+
+ ///
+ /// Checks the .
+ ///
+ [Fact]
+ public void FunctionSimple()
+ {
+ const string functionText = "{ add }";
+ //Simply adds the two arguments and returns the result
+
+ PdfFunctionType4 function = CreateFunction(functionText,
+ new double[] { -1.0, 1.0, -1.0, 1.0 },
+ new double[] { -1.0, 1.0 });
+
+ Assert.Equal(FunctionTypes.PostScript, function.FunctionType);
+
+ double[] input = new double[] { 0.8, 0.1 };
+ double[] output = function.Eval(input);
+
+ Assert.Single(output);
+ Assert.Equal(0.9, output[0], 4);
+
+ input = new double[] { 0.8, 0.3 }; //results in 1.1f being outside Range
+ output = function.Eval(input);
+
+ Assert.Single(output);
+ Assert.Equal(1, output[0]);
+
+ input = new double[] { 0.8, 1.2 }; //input argument outside Dimension
+ output = function.Eval(input);
+
+ Assert.Single(output);
+ Assert.Equal(1, output[0]);
+ }
+
+ ///
+ /// Checks the handling of the argument order for a .
+ ///
+ [Fact]
+ public void FunctionArgumentOrder()
+ {
+ const string functionText = "{ pop }";
+ // pops an argument (2nd) and returns the next argument (1st)
+
+ PdfFunctionType4 function = CreateFunction(functionText,
+ new double[] { -1.0, 1.0, -1.0, 1.0 },
+ new double[] { -1.0, 1.0 });
+
+ double[] input = new double[] { -0.7, 0.0 };
+ double[] output = function.Eval(input);
+
+ Assert.Single(output);
+ Assert.Equal(-0.7, output[0], 4);
+ }
+
+ [Fact]
+ public void Advanced()
+ {
+ const string functionText = "{ dup 0.0 mul 1 exch sub 2 index 1.0 mul 1 exch sub mul 1 exch sub 3 1 roll dup 0.75 mul 1 exch sub 2 index 0.723 mul 1 exch sub mul 1 exch sub 3 1 roll dup 0.9 mul 1 exch sub 2 index 0.0 mul 1 exch sub mul 1 exch sub 3 1 roll dup 0.0 mul 1 exch sub 2 index 0.02 mul 1 exch sub mul 1 exch sub 3 1 roll pop pop }";
+
+ PdfFunctionType4 function = CreateFunction(functionText,
+ new double[] { 0, 1, 0, 1 },
+ new double[] { 0, 1, 0, 1, 0, 1, 0, 1 });
+
+ double[] input = new double[] { 1.0, 1.0 };
+ double[] output = function.Eval(input);
+
+ Assert.Equal(4, output.Length);
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/Functions/Type4/OperatorsTests.cs b/src/UglyToad.PdfPig.Tests/Functions/Type4/OperatorsTests.cs
new file mode 100644
index 00000000..46660877
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Functions/Type4/OperatorsTests.cs
@@ -0,0 +1,482 @@
+namespace UglyToad.PdfPig.Tests.Functions.Type4
+{
+ using System;
+ using UglyToad.PdfPig.Functions.Type4;
+ using Xunit;
+
+ public class OperatorsTests
+ {
+ ///
+ /// Tests the "add" operator.
+ ///
+ [Fact]
+ public void Add()
+ {
+ Type4Tester.Create("5 6 add").Pop(11).IsEmpty();
+
+ Type4Tester.Create("5 0.23 add").Pop(5.23).IsEmpty();
+
+ const int bigValue = int.MaxValue - 2;
+ ExecutionContext context = Type4Tester.Create($"{bigValue} {bigValue} add").ToExecutionContext();
+ double floatResult = Convert.ToDouble(context.Stack.Pop());
+ Assert.Equal((long)2 * (long)int.MaxValue - (long)4, floatResult, 1);
+
+ Assert.Empty(context.Stack);
+ }
+
+ ///
+ /// Tests the "abs" operator.
+ ///
+ [Fact]
+ public void Abs()
+ {
+ Type4Tester.Create("-3 abs 2.1 abs -2.1 abs -7.5 abs")
+ .Pop(7.5).Pop(2.1).Pop(2.1).Pop(3).IsEmpty();
+ }
+
+ ///
+ /// Tests the "and" operator.
+ ///
+ [Fact]
+ public void And()
+ {
+ Type4Tester.Create("true true and true false and")
+ .Pop(false).Pop(true).IsEmpty();
+
+ Type4Tester.Create("99 1 and 52 7 and")
+ .Pop(4).Pop(1).IsEmpty();
+ }
+
+ ///
+ /// Tests the "atan" operator.
+ ///
+ [Fact]
+ public void Atan()
+ {
+ Type4Tester.Create("0 1 atan").Pop(0.0).IsEmpty();
+ Type4Tester.Create("1 0 atan").Pop(90.0).IsEmpty();
+ Type4Tester.Create("-100 0 atan").Pop(270.0).IsEmpty();
+ Type4Tester.Create("4 4 atan").Pop(45.0).IsEmpty();
+ }
+
+ ///
+ /// Tests the "ceiling" operator.
+ ///
+ [Fact]
+ public void Ceiling()
+ {
+ Type4Tester.Create("3.2 ceiling -4.8 ceiling 99 ceiling")
+ .Pop(99.0).Pop(-4.0).Pop(4.0).IsEmpty();
+ }
+
+ ///
+ /// Tests the "cos" operator.
+ ///
+ [Fact]
+ public void Cos()
+ {
+ Type4Tester.Create("0 cos").PopReal(1).IsEmpty();
+ Type4Tester.Create("90 cos").PopReal(0).IsEmpty();
+ }
+
+ ///
+ /// Tests the "cvi" operator.
+ ///
+ [Fact]
+ public void Cvi()
+ {
+ Type4Tester.Create("-47.8 cvi").Pop(-47).IsEmpty();
+ Type4Tester.Create("520.9 cvi").Pop(520).IsEmpty();
+ }
+
+ ///
+ /// Tests the "cvr" operator.
+ ///
+ [Fact]
+ public void Cvr()
+ {
+ Type4Tester.Create("-47.8 cvr").PopReal(-47.8).IsEmpty();
+ Type4Tester.Create("520.9 cvr").PopReal(520.9).IsEmpty();
+ Type4Tester.Create("77 cvr").PopReal(77).IsEmpty();
+
+ //Check that the data types are really right
+ ExecutionContext context = Type4Tester.Create("77 77 cvr").ToExecutionContext();
+ Assert.True(context.Stack.Pop() is double, "Expected a real as the result of 'cvr'");
+ Assert.True(context.Stack.Pop() is int, "Expected an int from an int literal");
+ }
+
+ ///
+ /// Tests the "div" operator.
+ ///
+ [Fact]
+ public void Div()
+ {
+ Type4Tester.Create("3 2 div").PopReal(1.5).IsEmpty();
+ Type4Tester.Create("4 2 div").PopReal(2.0).IsEmpty();
+ }
+
+ ///
+ /// Tests the "exp" operator.
+ ///
+ [Fact]
+ public void Exp()
+ {
+ Type4Tester.Create("9 0.5 exp").PopReal(3.0).IsEmpty();
+ Type4Tester.Create("-9 -1 exp").PopReal(-0.111111, 0.000001).IsEmpty();
+ }
+
+ ///
+ /// Tests the "floor" operator.
+ ///
+ [Fact]
+ public void Floor()
+ {
+ Type4Tester.Create("3.2 floor -4.8 floor 99 floor")
+ .Pop(99.0).Pop(-5.0).Pop(3.0).IsEmpty();
+ }
+
+ ///
+ /// Tests the "div" operator.
+ ///
+ [Fact]
+ public void IDiv()
+ {
+ Type4Tester.Create("3 2 idiv").Pop(1).IsEmpty();
+ Type4Tester.Create("4 2 idiv").Pop(2).IsEmpty();
+ Type4Tester.Create("-5 2 idiv").Pop(-2).IsEmpty();
+
+ Assert.Throws(() => Type4Tester.Create("4.4 2 idiv"));
+ }
+
+ ///
+ /// Tests the "ln" operator.
+ ///
+ [Fact]
+ public void Ln()
+ {
+ Type4Tester.Create("10 ln").PopReal(2.30259, 0.00001).IsEmpty();
+ Type4Tester.Create("100 ln").PopReal(4.60517, 0.00001).IsEmpty();
+ }
+
+ ///
+ /// Tests the "log" operator.
+ ///
+ [Fact]
+ public void Log()
+ {
+ Type4Tester.Create("10 log").PopReal(1.0).IsEmpty();
+ Type4Tester.Create("100 log").PopReal(2.0).IsEmpty();
+ }
+
+ ///
+ /// Tests the "mod" operator.
+ ///
+ [Fact]
+ public void Mod()
+ {
+ Type4Tester.Create("5 3 mod").Pop(2).IsEmpty();
+ Type4Tester.Create("5 2 mod").Pop(1).IsEmpty();
+ Type4Tester.Create("-5 3 mod").Pop(-2).IsEmpty();
+
+ Assert.Throws(() => Type4Tester.Create("4.4 2 mod"));
+ }
+
+ ///
+ /// Tests the "mul" operator.
+ ///
+ [Fact]
+ public void Mul()
+ {
+ Type4Tester.Create("1 2 mul").Pop(2).IsEmpty();
+ Type4Tester.Create("1.5 2 mul").PopReal(3.0).IsEmpty();
+ Type4Tester.Create("1.5 2.1 mul").PopReal(3.15, 0.001).IsEmpty();
+ Type4Tester.Create($"{(int.MaxValue - 3)} 2 mul") //int overflow -> real
+ .PopReal(2L * (int.MaxValue - 3), 0.001).IsEmpty();
+ }
+
+ ///
+ /// Tests the "neg" operator.
+ ///
+ [Fact]
+ public void Neg()
+ {
+ Type4Tester.Create("4.5 neg").PopReal(-4.5).IsEmpty();
+ Type4Tester.Create("-3 neg").Pop(3).IsEmpty();
+
+ //Border cases
+ Type4Tester.Create((int.MinValue + 1) + " neg").Pop(int.MaxValue).IsEmpty();
+ Type4Tester.Create(int.MinValue + " neg").PopReal(-(double)int.MinValue).IsEmpty();
+ }
+
+ ///
+ /// Tests the "round" operator.
+ ///
+ [Fact]
+ public void Round()
+ {
+ Type4Tester.Create("3.2 round").PopReal(3.0).IsEmpty();
+ Type4Tester.Create("6.5 round").PopReal(7.0).IsEmpty();
+ Type4Tester.Create("-4.8 round").PopReal(-5.0).IsEmpty();
+ Type4Tester.Create("-6.5 round").PopReal(-6.0).IsEmpty();
+ Type4Tester.Create("99 round").Pop(99).IsEmpty();
+ }
+
+ ///
+ /// Tests the "sin" operator.
+ ///
+ [Fact]
+ public void Sin()
+ {
+ Type4Tester.Create("0 sin").PopReal(0).IsEmpty();
+ Type4Tester.Create("90 sin").PopReal(1).IsEmpty();
+ Type4Tester.Create("-90.0 sin").PopReal(-1).IsEmpty();
+ }
+
+ ///
+ /// Tests the "sqrt" operator.
+ ///
+ [Fact]
+ public void Sqrt()
+ {
+ Type4Tester.Create("0 sqrt").PopReal(0).IsEmpty();
+ Type4Tester.Create("1 sqrt").PopReal(1).IsEmpty();
+ Type4Tester.Create("4 sqrt").PopReal(2).IsEmpty();
+ Type4Tester.Create("4.4 sqrt").PopReal(2.097617, 0.000001).IsEmpty();
+ Assert.Throws(() => Type4Tester.Create("-4.1 sqrt"));
+ }
+
+ ///
+ /// Tests the "sub" operator.
+ ///
+ [Fact]
+ public void Sub()
+ {
+ Type4Tester.Create("5 2 sub -7.5 1 sub").Pop(-8.5f).Pop(3).IsEmpty();
+ }
+
+ ///
+ /// Tests the "truncate" operator.
+ ///
+ [Fact]
+ public void Truncate()
+ {
+ Type4Tester.Create("3.2 truncate").PopReal(3.0).IsEmpty();
+ Type4Tester.Create("-4.8 truncate").PopReal(-4.0).IsEmpty();
+ Type4Tester.Create("99 truncate").Pop(99).IsEmpty();
+ }
+
+ ///
+ /// Tests the "bitshift" operator.
+ ///
+ [Fact]
+ public void Bitshift()
+ {
+ Type4Tester.Create("7 3 bitshift 142 -3 bitshift")
+ .Pop(17).Pop(56).IsEmpty();
+ }
+
+ ///
+ /// Tests the "eq" operator.
+ ///
+ [Fact]
+ public void Eq()
+ {
+ Type4Tester.Create("7 7 eq 7 6 eq 7 -7 eq true true eq false true eq 7.7 7.7 eq")
+ .Pop(true).Pop(false).Pop(true).Pop(false).Pop(false).Pop(true).IsEmpty();
+ }
+
+ ///
+ /// Tests the "ge" operator.
+ ///
+ [Fact]
+ public void Ge()
+ {
+ Type4Tester.Create("5 7 ge 7 5 ge 7 7 ge -1 2 ge")
+ .Pop(false).Pop(true).Pop(true).Pop(false).IsEmpty();
+ }
+
+ ///
+ /// Tests the "gt" operator.
+ ///
+ [Fact]
+ public void Gt()
+ {
+ Type4Tester.Create("5 7 gt 7 5 gt 7 7 gt -1 2 gt")
+ .Pop(false).Pop(false).Pop(true).Pop(false).IsEmpty();
+ }
+
+ ///
+ /// Tests the "le" operator.
+ ///
+ [Fact]
+ public void Le()
+ {
+ Type4Tester.Create("5 7 le 7 5 le 7 7 le -1 2 le")
+ .Pop(true).Pop(true).Pop(false).Pop(true).IsEmpty();
+ }
+
+ ///
+ /// Tests the "lt" operator.
+ ///
+ [Fact]
+ public void Lt()
+ {
+ Type4Tester.Create("5 7 lt 7 5 lt 7 7 lt -1 2 lt")
+ .Pop(true).Pop(false).Pop(false).Pop(true).IsEmpty();
+ }
+
+ ///
+ /// Tests the "ne" operator.
+ ///
+ [Fact]
+ public void Ne()
+ {
+ Type4Tester.Create("7 7 ne 7 6 ne 7 -7 ne true true ne false true ne 7.7 7.7 ne")
+ .Pop(false).Pop(true).Pop(false).Pop(true).Pop(true).Pop(false).IsEmpty();
+ }
+
+ ///
+ /// Tests the "not" operator.
+ ///
+ [Fact]
+ public void Not()
+ {
+ Type4Tester.Create("true not false not")
+ .Pop(true).Pop(false).IsEmpty();
+
+ Type4Tester.Create("52 not -37 not")
+ .Pop(37).Pop(-52).IsEmpty();
+ }
+
+ ///
+ /// Tests the "or" operator.
+ ///
+ [Fact]
+ public void Or()
+ {
+ Type4Tester.Create("true true or true false or false false or")
+ .Pop(false).Pop(true).Pop(true).IsEmpty();
+
+ Type4Tester.Create("17 5 or 1 1 or")
+ .Pop(1).Pop(21).IsEmpty();
+ }
+
+ ///
+ /// Tests the "cor" operator.
+ ///
+ [Fact]
+ public void Xor()
+ {
+ Type4Tester.Create("true true xor true false xor false false xor")
+ .Pop(false).Pop(true).Pop(false).IsEmpty();
+
+ Type4Tester.Create("7 3 xor 12 3 or")
+ .Pop(15).Pop(4);
+ }
+
+ ///
+ /// Tests the "if" operator.
+ ///
+ [Fact]
+ public void If()
+ {
+ Type4Tester.Create("true { 2 1 add } if")
+ .Pop(3).IsEmpty();
+
+ Type4Tester.Create("false { 2 1 add } if")
+ .IsEmpty();
+
+ Assert.Throws(() => Type4Tester.Create("0 { 2 1 add } if"));
+ }
+
+ ///
+ /// Tests the "ifelse" operator.
+ ///
+ [Fact]
+ public void IfElse()
+ {
+ Type4Tester.Create("true { 2 1 add } { 2 1 sub } ifelse")
+ .Pop(3).IsEmpty();
+
+ Type4Tester.Create("false { 2 1 add } { 2 1 sub } ifelse")
+ .Pop(1).IsEmpty();
+ }
+
+ ///
+ /// Tests the "copy" operator.
+ ///
+ [Fact]
+ public void Copy()
+ {
+ Type4Tester.Create("true 1 2 3 3 copy")
+ .Pop(3).Pop(2).Pop(1)
+ .Pop(3).Pop(2).Pop(1)
+ .Pop(true)
+ .IsEmpty();
+ }
+
+ ///
+ /// Tests the "dup" operator.
+ ///
+ [Fact]
+ public void Dup()
+ {
+ Type4Tester.Create("true 1 2 dup")
+ .Pop(2).Pop(2).Pop(1)
+ .Pop(true)
+ .IsEmpty();
+ Type4Tester.Create("true dup")
+ .Pop(true).Pop(true).IsEmpty();
+ }
+
+ ///
+ /// Tests the "exch" operator.
+ ///
+ [Fact]
+ public void Exch()
+ {
+ Type4Tester.Create("true 1 exch")
+ .Pop(true).Pop(1).IsEmpty();
+ Type4Tester.Create("1 2.5 exch")
+ .Pop(1).Pop(2.5).IsEmpty();
+ }
+
+ ///
+ /// Tests the "index" operator.
+ ///
+ [Fact]
+ public void Index()
+ {
+ Type4Tester.Create("1 2 3 4 0 index")
+ .Pop(4).Pop(4).Pop(3).Pop(2).Pop(1).IsEmpty();
+ Type4Tester.Create("1 2 3 4 3 index")
+ .Pop(1).Pop(4).Pop(3).Pop(2).Pop(1).IsEmpty();
+ }
+
+ ///
+ /// Tests the "pop" operator.
+ ///
+ [Fact]
+ public void Pop()
+ {
+ Type4Tester.Create("1 pop 7 2 pop")
+ .Pop(7).IsEmpty();
+ Type4Tester.Create("1 2 3 pop pop")
+ .Pop(1).IsEmpty();
+ }
+
+ ///
+ /// Tests the "roll" operator.
+ ///
+ [Fact]
+ public void Roll()
+ {
+ Type4Tester.Create("1 2 3 4 5 5 -2 roll")
+ .Pop(2).Pop(1).Pop(5).Pop(4).Pop(3).IsEmpty();
+ Type4Tester.Create("1 2 3 4 5 5 2 roll")
+ .Pop(3).Pop(2).Pop(1).Pop(5).Pop(4).IsEmpty();
+ Type4Tester.Create("1 2 3 3 0 roll")
+ .Pop(3).Pop(2).Pop(1).IsEmpty();
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/Functions/Type4/ParserTests.cs b/src/UglyToad.PdfPig.Tests/Functions/Type4/ParserTests.cs
new file mode 100644
index 00000000..ff85310f
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Functions/Type4/ParserTests.cs
@@ -0,0 +1,42 @@
+namespace UglyToad.PdfPig.Tests.Functions.Type4
+{
+ using Xunit;
+
+ public class ParserTests
+ {
+ ///
+ /// Test the very basics.
+ ///
+ [Fact]
+ public void ParserBasics()
+ {
+ Type4Tester.Create("3 4 add 2 sub").Pop(5).IsEmpty();
+ }
+
+ ///
+ /// Test nested blocks.
+ ///
+ [Fact]
+ public void Nested()
+ {
+ Type4Tester.Create("true { 2 1 add } { 2 1 sub } ifelse")
+ .Pop(3).IsEmpty();
+
+ Type4Tester.Create("{ true }").Pop(true).IsEmpty();
+ }
+
+ ///
+ /// Tests problematic functions from PDFBOX-804.
+ ///
+ [Fact]
+ public void Jira804()
+ {
+ //This is an example of a tint to CMYK function
+ //Problems here were:
+ //1. no whitespace between "mul" and "}" (token was detected as "mul}")
+ //2. line breaks cause endless loops
+ Type4Tester.Create("1 {dup dup .72 mul exch 0 exch .38 mul}\n")
+ .Pop(0.38f).Pop(0f).Pop(0.72f).Pop(1.0f).IsEmpty();
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/Functions/Type4/Type4Tester.cs b/src/UglyToad.PdfPig.Tests/Functions/Type4/Type4Tester.cs
new file mode 100644
index 00000000..bb61b746
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Functions/Type4/Type4Tester.cs
@@ -0,0 +1,124 @@
+namespace UglyToad.PdfPig.Tests.Functions.Type4
+{
+ using System;
+ using UglyToad.PdfPig.Functions.Type4;
+ using Xunit;
+
+ ///
+ /// Testing helper class for testing type 4 functions from the PDF specification.
+ ///
+ public sealed class Type4Tester
+ {
+ private readonly ExecutionContext context;
+
+ private Type4Tester(ExecutionContext ctxt)
+ {
+ this.context = ctxt;
+ }
+
+ ///
+ /// Creates a new instance for the given type 4 function.
+ ///
+ /// the text of the type 4 function
+ /// the tester instance
+ public static Type4Tester Create(string text)
+ {
+ InstructionSequence instructions = InstructionSequenceBuilder.Parse(text.Trim());
+
+ ExecutionContext context = new ExecutionContext(new Operators());
+ instructions.Execute(context);
+ return new Type4Tester(context);
+ }
+
+ ///
+ /// Pops a bool value from the stack and checks it against the expected result.
+ ///
+ /// the expected bool value
+ /// this instance
+ public Type4Tester Pop(bool expected)
+ {
+ bool value = (bool)context.Stack.Pop();
+ Assert.Equal(expected, value);
+ return this;
+ }
+
+ ///
+ /// Pops a real value from the stack and checks it against the expected result.
+ ///
+ /// the expected real value
+ /// this instance
+ public Type4Tester PopReal(double expected)
+ {
+ return PopReal(expected, 0.0000001);
+ }
+
+ ///
+ /// Pops a real value from the stack and checks it against the expected result.
+ ///
+ /// the expected real value
+ /// delta the allowed deviation of the value from the expected result
+ /// this instance
+ public Type4Tester PopReal(double expected, double delta)
+ {
+ double value = Convert.ToDouble(context.Stack.Pop());
+ DoubleComparer doubleComparer = new DoubleComparer(delta);
+ Assert.True(doubleComparer.Equals(expected, value));//expected, value, delta);
+ return this;
+ }
+
+ ///
+ /// Pops an int value from the stack and checks it against the expected result.
+ ///
+ /// the expected int value
+ /// this instance
+ public Type4Tester Pop(int expected)
+ {
+ int value = context.PopInt();
+ Assert.Equal(expected, value);
+ return this;
+ }
+
+ ///
+ /// Pops a numeric value from the stack and checks it against the expected result.
+ ///
+ /// the expected numeric value
+ /// this instance
+ public Type4Tester Pop(double expected)
+ {
+ return Pop(expected, 0.0000001);
+ }
+
+ ///
+ /// Pops a numeric value from the stack and checks it against the expected result.
+ ///
+ /// the expected numeric value
+ /// the allowed deviation of the value from the expected result
+ /// this instance
+ public Type4Tester Pop(double expected, double delta)
+ {
+ object value = context.PopNumber();
+ DoubleComparer doubleComparer = new DoubleComparer(delta);
+ Assert.True(doubleComparer.Equals(expected, Convert.ToDouble(value)));
+ return this;
+ }
+
+ ///
+ /// Checks that the stack is empty at this point.
+ ///
+ /// this instance
+ public Type4Tester IsEmpty()
+ {
+ Assert.Empty(context.Stack);
+ return this;
+ }
+
+ ///
+ /// Returns the execution context so some custom checks can be performed.
+ ///
+ /// the associated execution context
+ internal ExecutionContext ToExecutionContext()
+ {
+ return this.context;
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
index f110d4b5..e96e60a0 100644
--- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
+++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
@@ -90,6 +90,8 @@
"UglyToad.PdfPig.Filters.DefaultFilterProvider",
"UglyToad.PdfPig.Filters.IFilter",
"UglyToad.PdfPig.Filters.IFilterProvider",
+ "UglyToad.PdfPig.Functions.FunctionTypes",
+ "UglyToad.PdfPig.Functions.PdfFunction",
"UglyToad.PdfPig.PdfFonts.DescriptorFontFile",
"UglyToad.PdfPig.PdfFonts.FontDescriptor",
"UglyToad.PdfPig.PdfFonts.FontDescriptorFlags",
diff --git a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs
index 83d7fcb2..5fe76ae3 100644
--- a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs
@@ -636,6 +636,8 @@
[InlineData(PdfAStandard.A1A)]
[InlineData(PdfAStandard.A2B)]
[InlineData(PdfAStandard.A2A)]
+ [InlineData(PdfAStandard.A3B)]
+ [InlineData(PdfAStandard.A3A)]
public void CanGeneratePdfAFile(PdfAStandard standard)
{
var builder = new PdfDocumentBuilder
diff --git a/src/UglyToad.PdfPig/Content/ResourceStore.cs b/src/UglyToad.PdfPig/Content/ResourceStore.cs
index fa6e146e..17756229 100644
--- a/src/UglyToad.PdfPig/Content/ResourceStore.cs
+++ b/src/UglyToad.PdfPig/Content/ResourceStore.cs
@@ -158,7 +158,7 @@
}
try
- {
+ {
loadedFonts[reference] = fontFactory.Get(fontObject);
}
catch
@@ -168,7 +168,6 @@
throw;
}
}
-
}
else if (pair.Value is DictionaryToken fd)
{
diff --git a/src/UglyToad.PdfPig/Functions/PdfFunction.cs b/src/UglyToad.PdfPig/Functions/PdfFunction.cs
new file mode 100644
index 00000000..3fcc91e0
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/PdfFunction.cs
@@ -0,0 +1,274 @@
+namespace UglyToad.PdfPig.Functions
+{
+ using System;
+ using System.Linq;
+ using UglyToad.PdfPig.Core;
+ using UglyToad.PdfPig.Tokens;
+
+ ///
+ /// This class represents a function in a PDF document.
+ ///
+ public abstract class PdfFunction
+ {
+ ///
+ /// The function dictionary.
+ ///
+ public DictionaryToken FunctionDictionary { get; }
+
+ ///
+ /// The function stream.
+ ///
+ public StreamToken FunctionStream { get; }
+
+ private ArrayToken domain;
+ private ArrayToken range;
+ private int numberOfInputValues = -1;
+ private int numberOfOutputValues = -1;
+
+ ///
+ /// This class represents a function in a PDF document.
+ ///
+ public PdfFunction(DictionaryToken function)
+ {
+ FunctionDictionary = function;
+ }
+
+ ///
+ /// This class represents a function in a PDF document.
+ ///
+ public PdfFunction(StreamToken function)
+ {
+ FunctionStream = function;
+ }
+
+ ///
+ /// Returns the function type. Possible values are:
+ ///
+ /// - 0Sampled function
+ /// - 2Exponential interpolation function
+ /// - 3Stitching function
+ /// - 4PostScript calculator function
+ ///
+ ///
+ /// the function type.
+ public abstract FunctionTypes FunctionType { get; }
+
+ ///
+ /// Returns the function's dictionary. If is defined, it will be returned.
+ /// If not, the 's StreamDictionary will be returned.
+ ///
+ public DictionaryToken GetDictionary()
+ {
+ if (FunctionStream != null)
+ {
+ return FunctionStream.StreamDictionary;
+ }
+ else
+ {
+ return FunctionDictionary;
+ }
+ }
+
+ ///
+ /// This will get the number of output parameters that
+ /// have a range specified. A range for output parameters
+ /// is optional so this may return zero for a function
+ /// that does have output parameters, this will simply return the
+ /// number that have the range specified.
+ ///
+ /// The number of output parameters that have a range specified.
+ public int NumberOfOutputParameters
+ {
+ get
+ {
+ if (numberOfOutputValues == -1)
+ {
+ if (RangeValues == null)
+ {
+ numberOfOutputValues = 0;
+ }
+ else
+ {
+ numberOfOutputValues = RangeValues.Length / 2;
+ }
+ }
+ return numberOfOutputValues;
+ }
+ }
+
+ ///
+ /// This will get the range for a certain output parameters. This is will never
+ /// return null. If it is not present then the range 0 to 0 will
+ /// be returned.
+ ///
+ /// The output parameter number to get the range for.
+ /// The range for this component.
+ public PdfRange GetRangeForOutput(int n)
+ {
+ return new PdfRange(RangeValues.Data.OfType().Select(t => t.Double), n);
+ }
+
+ ///
+ /// This will get the number of input parameters that
+ /// have a domain specified.
+ ///
+ /// The number of input parameters that have a domain specified.
+ public int NumberOfInputParameters
+ {
+ get
+ {
+ if (numberOfInputValues == -1)
+ {
+ ArrayToken array = GetDomainValues();
+ numberOfInputValues = array.Length / 2;
+ }
+ return numberOfInputValues;
+ }
+ }
+
+ ///
+ /// This will get the range for a certain input parameter. This is will never
+ /// return null. If it is not present then the range 0 to 0 will
+ /// be returned.
+ ///
+ /// The parameter number to get the domain for.
+ /// The domain range for this component.
+ public PdfRange GetDomainForInput(int n)
+ {
+ ArrayToken domainValues = GetDomainValues();
+ return new PdfRange(domainValues.Data.OfType().Select(t => t.Double), n);
+ }
+
+ ///
+ /// Evaluates the function at the given input.
+ /// ReturnValue = f(input)
+ ///
+ /// The array of input values for the function.
+ /// In many cases will be an array of a single value, but not always.
+ /// The of outputs the function returns based on those inputs.
+ /// In many cases will be an array of a single value, but not always.
+ public abstract double[] Eval(double[] input);
+
+ ///
+ /// Returns all ranges for the output values as . Required for type 0 and type 4 functions.
+ ///
+ /// the ranges array.
+ protected virtual ArrayToken RangeValues
+ {
+ get
+ {
+ if (range == null)
+ {
+ GetDictionary().TryGet(NameToken.Range, out range); // Optionnal
+ }
+ return range;
+ }
+ }
+
+ ///
+ /// Returns all domains for the input values as . Required for all function types.
+ ///
+ /// the domains array.
+ private ArrayToken GetDomainValues()
+ {
+ if (domain == null)
+ {
+ if (!GetDictionary().TryGet(NameToken.Domain, out ArrayToken domainToken))
+ {
+ throw new ArgumentException("Could not retrieve Domain.");
+ }
+ domain = domainToken;
+ }
+ return domain;
+ }
+
+ ///
+ /// Clip the given input values to the ranges.
+ ///
+ /// inputValues the input values
+ /// the clipped values
+ protected double[] ClipToRange(double[] inputValues)
+ {
+ ArrayToken rangesArray = RangeValues;
+ double[] result;
+ if (rangesArray != null && rangesArray.Length > 0)
+ {
+ double[] rangeValues = rangesArray.Data.OfType().Select(t => t.Double).ToArray();
+ int numberOfRanges = rangeValues.Length / 2;
+ result = new double[numberOfRanges];
+ for (int i = 0; i < numberOfRanges; i++)
+ {
+ int index = i << 1;
+ result[i] = ClipToRange(inputValues[i], rangeValues[index], rangeValues[index + 1]);
+ }
+ }
+ else
+ {
+ result = inputValues;
+ }
+ return result;
+ }
+
+ ///
+ /// Clip the given input value to the given range.
+ ///
+ /// x the input value
+ /// the min value of the range
+ /// the max value of the range
+ /// the clipped value
+ protected static double ClipToRange(double x, double rangeMin, double rangeMax)
+ {
+ if (x < rangeMin)
+ {
+ return rangeMin;
+ }
+ else if (x > rangeMax)
+ {
+ return rangeMax;
+ }
+ return x;
+ }
+
+ ///
+ /// For a given value of x, interpolate calculates the y value
+ /// on the line defined by the two points (xRangeMin, xRangeMax)
+ /// and (yRangeMin, yRangeMax).
+ ///
+ /// the value to be interpolated value.
+ /// the min value of the x range
+ /// the max value of the x range
+ /// the min value of the y range
+ /// the max value of the y range
+ /// the interpolated y value
+ protected static double Interpolate(double x, double xRangeMin, double xRangeMax, double yRangeMin, double yRangeMax)
+ {
+ return yRangeMin + ((x - xRangeMin) * (yRangeMax - yRangeMin) / (xRangeMax - xRangeMin));
+ }
+ }
+
+ ///
+ /// Pdf function types.
+ ///
+ public enum FunctionTypes : byte
+ {
+ ///
+ /// Sampled function.
+ ///
+ Sampled = 0,
+
+ ///
+ /// Exponential interpolation function.
+ ///
+ Exponential = 2,
+
+ ///
+ /// Stitching function.
+ ///
+ Stitching = 3,
+
+ ///
+ /// PostScript calculator function.
+ ///
+ PostScript = 4
+ }
+}
diff --git a/src/UglyToad.PdfPig/Functions/PdfFunctionType0.cs b/src/UglyToad.PdfPig/Functions/PdfFunctionType0.cs
new file mode 100644
index 00000000..1a0dc8d4
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/PdfFunctionType0.cs
@@ -0,0 +1,418 @@
+namespace UglyToad.PdfPig.Functions
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using UglyToad.PdfPig.Core;
+ using UglyToad.PdfPig.Tokens;
+
+ internal sealed class PdfFunctionType0 : PdfFunction
+ {
+ ///
+ /// An array of 2 x m numbers specifying the linear mapping of input values
+ /// into the domain of the function's sample table. Default value: [ 0 (Size0
+ /// - 1) 0 (Size1 - 1) ...].
+ ///
+ private ArrayToken encode;
+
+ ///
+ /// An array of 2 x n numbers specifying the linear mapping of sample values
+ /// into the range appropriate for the function's output values. Default
+ /// value: same as the value of Range.
+ ///
+ private ArrayToken decode;
+
+ ///
+ /// An array of m positive integers specifying the number of samples in each
+ /// input dimension of the sample table.
+ ///
+ private ArrayToken size;
+
+ ///
+ /// The samples of the function.
+ ///
+ private int[][] samples;
+
+ ///
+ /// Stitching function
+ ///
+ internal PdfFunctionType0(DictionaryToken function) : base(function)
+ {
+ }
+
+ ///
+ /// Stitching function
+ ///
+ internal PdfFunctionType0(StreamToken function) : base(function)
+ {
+ }
+
+ public override FunctionTypes FunctionType
+ {
+ get
+ {
+ return FunctionTypes.Sampled;
+ }
+ }
+
+ ///
+ /// The "Size" entry, which is the number of samples in each input dimension of the sample table.
+ ///
+ public ArrayToken Size
+ {
+ get
+ {
+ if (size == null && !GetDictionary().TryGet(NameToken.Size, out size))
+ {
+ throw new ArgumentNullException(NameToken.Size);
+ }
+ return size;
+ }
+ }
+
+ ///
+ /// Get the number of bits that the output value will take up.
+ /// Valid values are 1,2,4,8,12,16,24,32.
+ ///
+ /// Number of bits for each output value.
+ public int BitsPerSample
+ {
+ get
+ {
+ if (!GetDictionary().TryGet(NameToken.BitsPerSample, out var bps))
+ {
+ throw new ArgumentNullException(NameToken.BitsPerSample);
+ }
+ return bps.Int;
+ }
+ }
+
+ ///
+ /// Get the order of interpolation between samples. Valid values are 1 and 3,
+ /// specifying linear and cubic spline interpolation, respectively. Default
+ /// is 1. See p.170 in PDF spec 1.7.
+ ///
+ /// order of interpolation.
+ public int Order
+ {
+ get
+ {
+ if (!GetDictionary().TryGet(NameToken.Order, out var order))
+ {
+ return 1;
+ }
+ return order.Int;
+ }
+ }
+
+ ///
+ /// Returns all encode values as .
+ ///
+ /// the encode array.
+ private ArrayToken EncodeValues
+ {
+ get
+ {
+ if (encode == null)
+ {
+ GetDictionary().TryGet(NameToken.Encode, out encode);
+
+ // the default value is [0 (size[0]-1) 0 (size[1]-1) ...]
+ if (encode == null)
+ {
+ var values = new List();
+ ArrayToken sizeValues = Size;
+ int sizeValuesSize = sizeValues.Length;
+ for (int i = 0; i < sizeValuesSize; i++)
+ {
+ values.Add(new NumericToken(0));
+ values.Add(new NumericToken((sizeValues[i] as NumericToken).Int - 1L));
+ }
+ encode = new ArrayToken(values);
+ }
+ }
+ return encode;
+ }
+ }
+
+ ///
+ /// Returns all decode values as .
+ ///
+ /// the decode array.
+ private ArrayToken DecodeValues
+ {
+ get
+ {
+ if (decode == null)
+ {
+ GetDictionary().TryGet(NameToken.Decode, out decode);
+
+ // if decode is null, the default values are the range values
+ if (decode == null)
+ {
+ decode = RangeValues;
+ }
+ }
+ return decode;
+ }
+ }
+
+ ///
+ /// Get the encode for the input parameter.
+ ///
+ /// The function parameter number.
+ /// The encode parameter range or null if none is set.
+ public PdfRange? GetEncodeForParameter(int paramNum)
+ {
+ ArrayToken encodeValues = EncodeValues;
+ if (encodeValues != null && encodeValues.Length >= paramNum * 2 + 1)
+ {
+ return new PdfRange(encodeValues.Data.OfType().Select(t => t.Double), paramNum);
+ }
+ return null;
+ }
+
+ ///
+ /// Get the decode for the input parameter.
+ ///
+ /// The function parameter number.
+ /// The decode parameter range or null if none is set.
+ public PdfRange? GetDecodeForParameter(int paramNum)
+ {
+ ArrayToken decodeValues = DecodeValues;
+ if (decodeValues != null && decodeValues.Length >= paramNum * 2 + 1)
+ {
+ return new PdfRange(decodeValues.Data.OfType().Select(t => t.Double), paramNum);
+ }
+ return null;
+ }
+
+ ///
+ /// Inner class do to an interpolation in the Nth dimension by comparing the
+ /// content size of N-1 dimensional objects.This is done with the help of
+ /// recursive calls.
+ /// To understand the algorithm without recursion, see for a bilinear interpolation
+ /// and for trilinear interpolation.
+ ///
+ ///
+ internal class RInterpol
+ {
+ // coordinate that is to be interpolated
+ private readonly double[] in_;
+ // coordinate of the "ceil" point
+ private readonly int[] inPrev;
+ // coordinate of the "floor" point
+ private readonly int[] inNext;
+ private readonly int numberOfInputValues;
+ private readonly int numberOfOutputValues;
+ private readonly ArrayToken size;
+
+ private readonly int[][] samples;
+
+ ///
+ /// Constructor.
+ ///
+ /// the input coordinates
+ /// coordinate of the "ceil" point
+ /// coordinate of the "floor" point
+ ///
+ ///
+ ///
+ internal RInterpol(double[] input, int[] inputPrev, int[] inputNext, int numberOfOutputValues, ArrayToken size, int[][] samples)
+ {
+ in_ = input;
+ inPrev = inputPrev;
+ inNext = inputNext;
+ numberOfInputValues = input.Length;
+ this.numberOfOutputValues = numberOfOutputValues;
+ this.size = size;
+ this.samples = samples;
+ }
+
+ ///
+ /// Calculate the interpolation.
+ ///
+ /// interpolated result sample
+ internal double[] RInterpolate()
+ {
+ return InternalRInterpol(new int[numberOfInputValues], 0);
+ }
+
+ ///
+ /// Do a linear interpolation if the two coordinates can be known, or
+ /// call itself recursively twice.
+ ///
+ /// partially set coordinate (not set from step
+ /// upwards); gets fully filled in the last call ("leaf"), where it is
+ /// used to get the correct sample
+ /// between 0 (first call) and dimension - 1
+ /// interpolated result sample
+ private double[] InternalRInterpol(int[] coord, int step)
+ {
+ double[] resultSample = new double[numberOfOutputValues];
+ if (step == in_.Length - 1)
+ {
+ // leaf
+ if (inPrev[step] == inNext[step])
+ {
+ coord[step] = inPrev[step];
+ int[] tmpSample = samples[CalcSampleIndex(coord)];
+ for (int i = 0; i < numberOfOutputValues; ++i)
+ {
+ resultSample[i] = tmpSample[i];
+ }
+ return resultSample;
+ }
+ coord[step] = inPrev[step];
+ int[] sample1 = samples[CalcSampleIndex(coord)];
+ coord[step] = inNext[step];
+ int[] sample2 = samples[CalcSampleIndex(coord)];
+ for (int i = 0; i < numberOfOutputValues; ++i)
+ {
+ resultSample[i] = Interpolate(in_[step], inPrev[step], inNext[step], sample1[i], sample2[i]);
+ }
+ return resultSample;
+ }
+ else
+ {
+ // branch
+ if (inPrev[step] == inNext[step])
+ {
+ coord[step] = inPrev[step];
+ return InternalRInterpol(coord, step + 1);
+ }
+ coord[step] = inPrev[step];
+ double[] sample1 = InternalRInterpol(coord, step + 1);
+ coord[step] = inNext[step];
+ double[] sample2 = InternalRInterpol(coord, step + 1);
+ for (int i = 0; i < numberOfOutputValues; ++i)
+ {
+ resultSample[i] = Interpolate(in_[step], inPrev[step], inNext[step], sample1[i], sample2[i]);
+ }
+ return resultSample;
+ }
+ }
+
+ ///
+ /// calculate array index (structure described in p.171 PDF spec 1.7) in multiple dimensions.
+ ///
+ /// with coordinates
+ /// index in flat array
+ private int CalcSampleIndex(int[] vector)
+ {
+ // inspiration: http://stackoverflow.com/a/12113479/535646
+ // but used in reverse
+ double[] sizeValues = size.Data.OfType().Select(t => t.Double).ToArray();
+ int index = 0;
+ int sizeProduct = 1;
+ int dimension = vector.Length;
+ for (int i = dimension - 2; i >= 0; --i)
+ {
+ sizeProduct = (int)(sizeProduct * sizeValues[i]);
+ }
+ for (int i = dimension - 1; i >= 0; --i)
+ {
+ index += sizeProduct * vector[i];
+ if (i - 1 >= 0)
+ {
+ sizeProduct = (int)(sizeProduct / sizeValues[i - 1]);
+ }
+ }
+ return index;
+ }
+ }
+
+ ///
+ /// Get all sample values of this function.
+ ///
+ /// an array with all samples.
+ private int[][] GetSamples()
+ {
+ if (samples == null)
+ {
+ int arraySize = 1;
+ int nIn = NumberOfInputParameters;
+ int nOut = NumberOfOutputParameters;
+ ArrayToken sizes = Size;
+ for (int i = 0; i < nIn; i++)
+ {
+ arraySize *= (sizes[i] as NumericToken).Int;
+ }
+ samples = new int[arraySize][];
+ int bitsPerSample = BitsPerSample;
+
+ // PDF spec 1.7 p.171:
+ // Each sample value is represented as a sequence of BitsPerSample bits.
+ // Successive values are adjacent in the bit stream; there is no padding at byte boundaries.
+ var bits = new BitArray(FunctionStream.Data.ToArray());
+
+ System.Diagnostics.Debug.Assert(bits.Length == arraySize * nOut * bitsPerSample);
+
+ for (int i = 0; i < arraySize; i++)
+ {
+ samples[i] = new int[nOut];
+ for (int k = 0; k < nOut; k++)
+ {
+ long accum = 0L;
+ for (int l = bitsPerSample - 1; l >= 0; l--)
+ {
+ accum <<= 1;
+ accum |= bits[i * nOut * bitsPerSample + (k * bitsPerSample) + l] ? (uint)1 : 0;
+ }
+
+ // TODO will this cast work properly for 32 bitsPerSample or should we use long[]?
+ samples[i][k] = (int)accum;
+ }
+ }
+ }
+
+ return samples;
+ }
+
+ public override double[] Eval(double[] input)
+ {
+ //This involves linear interpolation based on a set of sample points.
+ //Theoretically it's not that difficult ... see section 3.9.1 of the PDF Reference.
+
+ double[] sizeValues = Size.Data.OfType().Select(t => t.Double).ToArray();
+ int bitsPerSample = BitsPerSample;
+ double maxSample = Math.Pow(2, bitsPerSample) - 1.0;
+ int numberOfInputValues = input.Length;
+ int numberOfOutputValues = NumberOfOutputParameters;
+
+ int[] inputPrev = new int[numberOfInputValues];
+ int[] inputNext = new int[numberOfInputValues];
+ input = input.ToArray(); // PDFBOX-4461
+
+ for (int i = 0; i < numberOfInputValues; i++)
+ {
+ PdfRange domain = GetDomainForInput(i);
+ PdfRange? encodeValues = GetEncodeForParameter(i);
+ input[i] = ClipToRange(input[i], domain.Min, domain.Max);
+ input[i] = Interpolate(input[i], domain.Min, domain.Max,
+ encodeValues.Value.Min, encodeValues.Value.Max);
+ input[i] = ClipToRange(input[i], 0, sizeValues[i] - 1);
+ inputPrev[i] = (int)Math.Floor(input[i]);
+ inputNext[i] = (int)Math.Ceiling(input[i]);
+ }
+
+ double[] outputValues = new RInterpol(input, inputPrev, inputNext, numberOfOutputValues, Size, GetSamples()).RInterpolate();
+
+ for (int i = 0; i < numberOfOutputValues; i++)
+ {
+ PdfRange range = GetRangeForOutput(i);
+ PdfRange? decodeValues = GetDecodeForParameter(i);
+ if (!decodeValues.HasValue)
+ {
+ throw new IOException("Range missing in function /Decode entry");
+ }
+ outputValues[i] = Interpolate(outputValues[i], 0, maxSample, decodeValues.Value.Min, decodeValues.Value.Max);
+ outputValues[i] = ClipToRange(outputValues[i], range.Min, range.Max);
+ }
+
+ return outputValues;
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Functions/PdfFunctionType2.cs b/src/UglyToad.PdfPig/Functions/PdfFunctionType2.cs
new file mode 100644
index 00000000..11f2110f
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/PdfFunctionType2.cs
@@ -0,0 +1,138 @@
+namespace UglyToad.PdfPig.Functions
+{
+ using System;
+ using System.Collections.Generic;
+ using UglyToad.PdfPig.Tokens;
+
+ ///
+ /// Exponential interpolation function
+ ///
+ internal sealed class PdfFunctionType2 : PdfFunction
+ {
+ ///
+ /// Exponential interpolation function
+ ///
+ internal PdfFunctionType2(DictionaryToken function) : base(function)
+ {
+ if (GetDictionary().TryGet(NameToken.C0, out ArrayToken array0))
+ {
+ C0 = array0;
+ }
+ else
+ {
+ C0 = new ArrayToken(new List());
+ }
+ if (C0.Length == 0)
+ {
+ C0 = new ArrayToken(new List() { new NumericToken(0) });
+ }
+
+ if (GetDictionary().TryGet(NameToken.C1, out ArrayToken array1))
+ {
+ C1 = array1;
+ }
+ else
+ {
+ C1 = new ArrayToken(new List());
+ }
+ if (C0.Length == 0)
+ {
+ C1 = new ArrayToken(new List() { new NumericToken(1) });
+ }
+
+ if (GetDictionary().TryGet(NameToken.N, out NumericToken exp))
+ {
+ N = exp.Double;
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal PdfFunctionType2(StreamToken function) : base(function)
+ {
+ if (GetDictionary().TryGet(NameToken.C0, out ArrayToken array0))
+ {
+ C0 = array0;
+ }
+ else
+ {
+ C0 = new ArrayToken(new List());
+ }
+ if (C0.Length == 0)
+ {
+ C0 = new ArrayToken(new List() { new NumericToken(0) });
+ }
+
+ if (GetDictionary().TryGet(NameToken.C1, out ArrayToken array1))
+ {
+ C1 = array1;
+ }
+ else
+ {
+ C1 = new ArrayToken(new List());
+ }
+ if (C0.Length == 0)
+ {
+ C1 = new ArrayToken(new List() { new NumericToken(1) });
+ }
+
+ if (GetDictionary().TryGet(NameToken.N, out NumericToken exp))
+ {
+ N = exp.Double;
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override FunctionTypes FunctionType
+ {
+ get
+ {
+ return FunctionTypes.Exponential;
+ }
+ }
+
+ public override double[] Eval(double[] input)
+ {
+ // exponential interpolation
+ double xToN = Math.Pow(input[0], N); // x^exponent
+
+ double[] result = new double[Math.Min(C0.Length, C1.Length)];
+ for (int j = 0; j < result.Length; j++)
+ {
+ double c0j = ((NumericToken)C0[j]).Double;
+ double c1j = ((NumericToken)C1[j]).Double;
+ result[j] = c0j + xToN * (c1j - c0j);
+ }
+
+ return ClipToRange(result);
+ }
+
+ ///
+ /// The C0 values of the function, 0 if empty.
+ ///
+ public ArrayToken C0 { get; }
+
+ ///
+ /// The C1 values of the function, 1 if empty.
+ ///
+ public ArrayToken C1 { get; }
+
+ ///
+ /// The exponent of the function.
+ ///
+ public double N { get; }
+
+ public override string ToString()
+ {
+ return "FunctionType2{"
+ + "C0: " + C0 + " "
+ + "C1: " + C1 + " "
+ + "N: " + N + "}";
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Functions/PdfFunctionType3.cs b/src/UglyToad.PdfPig/Functions/PdfFunctionType3.cs
new file mode 100644
index 00000000..34e76419
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/PdfFunctionType3.cs
@@ -0,0 +1,172 @@
+namespace UglyToad.PdfPig.Functions
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using UglyToad.PdfPig.Core;
+ using UglyToad.PdfPig.Tokens;
+
+ ///
+ /// Stitching function
+ ///
+ internal sealed class PdfFunctionType3 : PdfFunction
+ {
+ private ArrayToken functions;
+ private ArrayToken encode;
+ private ArrayToken bounds;
+ private double[] boundsValues;
+
+ ///
+ /// Stitching function
+ ///
+ internal PdfFunctionType3(DictionaryToken function, IReadOnlyList functionsArray)
+ : base(function)
+ {
+ if (functionsArray == null || functionsArray.Count == 0)
+ {
+ throw new ArgumentNullException(nameof(functionsArray));
+ }
+ this.FunctionsArray = functionsArray;
+ }
+
+ ///
+ /// Stitching function
+ ///
+ internal PdfFunctionType3(StreamToken function, IReadOnlyList functionsArray)
+ : base(function)
+ {
+ if (functionsArray == null || functionsArray.Count == 0)
+ {
+ throw new ArgumentNullException(nameof(functionsArray));
+ }
+ this.FunctionsArray = functionsArray;
+ }
+
+ public override FunctionTypes FunctionType
+ {
+ get
+ {
+ return FunctionTypes.Stitching;
+ }
+ }
+
+ public override double[] Eval(double[] input)
+ {
+ // This function is known as a "stitching" function. Based on the input, it decides which child function to call.
+ // All functions in the array are 1-value-input functions
+ // See PDF Reference section 3.9.3.
+ PdfFunction function = null;
+ double x = input[0];
+ PdfRange domain = GetDomainForInput(0);
+ // clip input value to domain
+ x = ClipToRange(x, domain.Min, domain.Max);
+
+ if (FunctionsArray.Count == 1)
+ {
+ // This doesn't make sense but it may happen...
+ function = FunctionsArray[0];
+ PdfRange encRange = GetEncodeForParameter(0);
+ x = Interpolate(x, domain.Min, domain.Max, encRange.Min, encRange.Max);
+ }
+ else
+ {
+ if (boundsValues == null)
+ {
+ boundsValues = Bounds.Data.OfType().Select(t => t.Double).ToArray();
+ }
+
+ int boundsSize = boundsValues.Length;
+ // create a combined array containing the domain and the bounds values
+ // domain.min, bounds[0], bounds[1], ...., bounds[boundsSize-1], domain.max
+ double[] partitionValues = new double[boundsSize + 2];
+ int partitionValuesSize = partitionValues.Length;
+ partitionValues[0] = domain.Min;
+ partitionValues[partitionValuesSize - 1] = domain.Max;
+ Array.Copy(boundsValues, 0, partitionValues, 1, boundsSize); // System.arraycopy(boundsValues, 0, partitionValues, 1, boundsSize);
+ // find the partition
+ for (int i = 0; i < partitionValuesSize - 1; i++)
+ {
+ if (x >= partitionValues[i] &&
+ (x < partitionValues[i + 1] || (i == partitionValuesSize - 2 && x == partitionValues[i + 1])))
+ {
+ function = FunctionsArray[i];
+ PdfRange encRange = GetEncodeForParameter(i);
+ x = Interpolate(x, partitionValues[i], partitionValues[i + 1], encRange.Min, encRange.Max);
+ break;
+ }
+ }
+ }
+ if (function == null)
+ {
+ throw new IOException("partition not found in type 3 function");
+ }
+ double[] functionValues = new double[] { x };
+ // calculate the output values using the chosen function
+ double[] functionResult = function.Eval(functionValues);
+ // clip to range if available
+ return ClipToRange(functionResult);
+ }
+
+ public IReadOnlyList FunctionsArray { get; }
+
+ ///
+ /// Returns all functions values as .
+ ///
+ /// the functions array.
+ public ArrayToken Functions
+ {
+ get
+ {
+ if (functions == null && !GetDictionary().TryGet(NameToken.Functions, out functions))
+ {
+ throw new ArgumentNullException(NameToken.Functions);
+ }
+ return functions;
+ }
+ }
+
+ ///
+ /// Returns all bounds values as .
+ ///
+ /// the bounds array.
+ public ArrayToken Bounds
+ {
+ get
+ {
+ if (bounds == null && !GetDictionary().TryGet(NameToken.Bounds, out bounds))
+ {
+ throw new ArgumentNullException(NameToken.Bounds);
+ }
+ return bounds;
+ }
+ }
+
+ ///
+ /// Returns all encode values as .
+ ///
+ /// the encode array.
+ public ArrayToken Encode
+ {
+ get
+ {
+ if (encode == null && !GetDictionary().TryGet(NameToken.Encode, out encode))
+ {
+ throw new ArgumentNullException(NameToken.Encode);
+ }
+ return encode;
+ }
+ }
+
+ ///
+ /// Get the encode for the input parameter.
+ ///
+ /// The function parameter number.
+ /// The encode parameter range or null if none is set.
+ private PdfRange GetEncodeForParameter(int n)
+ {
+ ArrayToken encodeValues = Encode;
+ return new PdfRange(encodeValues.Data.OfType().Select(t => t.Double), n);
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Functions/PdfFunctionType4.cs b/src/UglyToad.PdfPig/Functions/PdfFunctionType4.cs
new file mode 100644
index 00000000..986fd1dd
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/PdfFunctionType4.cs
@@ -0,0 +1,71 @@
+namespace UglyToad.PdfPig.Functions
+{
+ using System;
+ using System.Linq;
+ using UglyToad.PdfPig.Core;
+ using UglyToad.PdfPig.Functions.Type4;
+ using UglyToad.PdfPig.Tokens;
+
+ ///
+ /// PostScript calculator function
+ ///
+ internal sealed class PdfFunctionType4 : PdfFunction
+ {
+ private readonly Operators operators = new Operators();
+ private readonly InstructionSequence instructions;
+
+ ///
+ /// PostScript calculator function
+ ///
+ internal PdfFunctionType4(StreamToken function) : base(function)
+ {
+ byte[] bytes = FunctionStream.Data.ToArray();
+ string str = OtherEncodings.Iso88591.GetString(bytes);
+ this.instructions = InstructionSequenceBuilder.Parse(str);
+ }
+
+ public override FunctionTypes FunctionType
+ {
+ get
+ {
+ return FunctionTypes.PostScript;
+ }
+ }
+
+ public override double[] Eval(double[] input)
+ {
+ //Setup the input values
+ ExecutionContext context = new ExecutionContext(operators);
+ for (int i = 0; i < input.Length; i++)
+ {
+ PdfRange domain = GetDomainForInput(i);
+ double value = ClipToRange(input[i], domain.Min, domain.Max);
+ context.Stack.Push(value);
+ }
+
+ //Execute the type 4 function.
+ instructions.Execute(context);
+
+ //Extract the output values
+ int numberOfOutputValues = NumberOfOutputParameters;
+ int numberOfActualOutputValues = context.Stack.Count;
+ if (numberOfActualOutputValues < numberOfOutputValues)
+ {
+ throw new ArgumentOutOfRangeException("The type 4 function returned "
+ + numberOfActualOutputValues
+ + " values but the Range entry indicates that "
+ + numberOfOutputValues + " values be returned.");
+ }
+ double[] outputValues = new double[numberOfOutputValues];
+ for (int i = numberOfOutputValues - 1; i >= 0; i--)
+ {
+ PdfRange range = GetRangeForOutput(i);
+ outputValues[i] = context.PopReal();
+ outputValues[i] = ClipToRange(outputValues[i], range.Min, range.Max);
+ }
+
+ //Return the resulting array
+ return outputValues;
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Functions/Type4/ArithmeticOperators.cs b/src/UglyToad.PdfPig/Functions/Type4/ArithmeticOperators.cs
new file mode 100644
index 00000000..234af09b
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/Type4/ArithmeticOperators.cs
@@ -0,0 +1,398 @@
+namespace UglyToad.PdfPig.Functions.Type4
+{
+ using System;
+
+ ///
+ /// Provides the arithmetic operators such as "add" and "sub".
+ ///
+ internal sealed class ArithmeticOperators
+ {
+ private ArithmeticOperators()
+ {
+ // Private constructor.
+ }
+
+ private static double ToRadians(double val)
+ {
+ return (Math.PI / 180.0) * val;
+ }
+
+ private static double ToDegrees(double val)
+ {
+ return (180.0 / Math.PI) * val;
+ }
+
+ ///
+ /// the "Abs" operator.
+ ///
+ internal sealed class Abs : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ if (num is int numi)
+ {
+ context.Stack.Push(Math.Abs(numi));
+ }
+ else
+ {
+ context.Stack.Push(Math.Abs(Convert.ToDouble(num)));
+ }
+ }
+ }
+
+ ///
+ /// the "add" operator.
+ ///
+ internal sealed class Add : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num2 = context.PopNumber();
+ var num1 = context.PopNumber();
+ if (num1 is int num1i && num2 is int num2i)
+ {
+ long sum = (long)num1i + (long)num2i; // Keep both cast here
+ if (sum < int.MinValue || sum > int.MaxValue)
+ {
+ context.Stack.Push((double)sum);
+ }
+ else
+ {
+ context.Stack.Push((int)sum);
+ }
+ }
+ else
+ {
+ double sum = Convert.ToDouble(num1) + Convert.ToDouble(num2);
+ context.Stack.Push(sum);
+ }
+ }
+ }
+
+ ///
+ /// the "atan" operator.
+ ///
+ internal sealed class Atan : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ double den = context.PopReal();
+ double num = context.PopReal();
+ double atan = Math.Atan2(num, den);
+ atan = ToDegrees(atan) % 360;
+ if (atan < 0)
+ {
+ atan += 360;
+ }
+ context.Stack.Push(atan);
+ }
+ }
+
+ ///
+ /// the "ceiling" operator.
+ ///
+ internal sealed class Ceiling : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ if (num is int numi)
+ {
+ context.Stack.Push(numi);
+ }
+ else
+ {
+ context.Stack.Push(Math.Ceiling(Convert.ToDouble(num)));
+ }
+ }
+ }
+
+ ///
+ /// the "cos" operator.
+ ///
+ internal sealed class Cos : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ double angle = context.PopReal();
+ double cos = Math.Cos(ToRadians(angle));
+ context.Stack.Push(cos);
+ }
+ }
+
+ ///
+ /// the "cvi" operator.
+ ///
+ internal sealed class Cvi : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ context.Stack.Push((int)Math.Truncate(Convert.ToDouble(num)));
+ }
+ }
+
+ ///
+ /// the "cvr" operator.
+ ///
+ internal sealed class Cvr : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ context.Stack.Push(Convert.ToDouble(num));
+ }
+ }
+
+ ///
+ /// the "div" operator.
+ ///
+ internal sealed class Div : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ double num2 = Convert.ToDouble(context.PopNumber());
+ double num1 = Convert.ToDouble(context.PopNumber());
+ context.Stack.Push(num1 / num2);
+ }
+ }
+
+ ///
+ /// the "exp" operator.
+ ///
+ internal sealed class Exp : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ double exp = Convert.ToDouble(context.PopNumber());
+ double base_ = Convert.ToDouble(context.PopNumber());
+ double value = Math.Pow(base_, exp);
+ context.Stack.Push(value);
+ }
+ }
+
+ ///
+ /// the "floor" operator.
+ ///
+ internal sealed class Floor : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ if (num is int numi)
+ {
+ context.Stack.Push(numi);
+ }
+ else
+ {
+ context.Stack.Push(Math.Floor(Convert.ToDouble(num)));
+ }
+ }
+ }
+
+ ///
+ /// the "idiv" operator.
+ ///
+ internal sealed class IDiv : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ int num2 = context.PopInt();
+ int num1 = context.PopInt();
+ context.Stack.Push(num1 / num2);
+ }
+ }
+
+ ///
+ /// the "ln" operator.
+ ///
+ internal sealed class Ln : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ context.Stack.Push(Math.Log(Convert.ToDouble(num)));
+ }
+ }
+
+ ///
+ /// the "log" operator.
+ ///
+ internal sealed class Log : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ context.Stack.Push(Math.Log10(Convert.ToDouble(num)));
+ }
+ }
+
+ ///
+ /// the "mod" operator.
+ ///
+ internal sealed class Mod : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ int int2 = context.PopInt();
+ int int1 = context.PopInt();
+ context.Stack.Push(int1 % int2);
+ }
+ }
+
+ ///
+ /// the "mul" operator.
+ ///
+ internal sealed class Mul : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num2 = context.PopNumber();
+ var num1 = context.PopNumber();
+ if (num1 is int num1i && num2 is int num2i)
+ {
+ long result = (long)num1i * (long)num2i; // Keep both cast here
+ if (result >= int.MinValue && result <= int.MaxValue)
+ {
+ context.Stack.Push((int)result);
+ }
+ else
+ {
+ context.Stack.Push((double)result);
+ }
+ }
+ else
+ {
+ double result = Convert.ToDouble(num1) * Convert.ToDouble(num2);
+ context.Stack.Push(result);
+ }
+ }
+ }
+
+ ///
+ /// the "neg" operator.
+ ///
+ internal sealed class Neg : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ if (num is int v)
+ {
+ if (v == int.MinValue)
+ {
+ context.Stack.Push(-Convert.ToDouble(v));
+ }
+ else
+ {
+ context.Stack.Push(-v);
+ }
+ }
+ else
+ {
+ context.Stack.Push(-Convert.ToDouble(num));
+ }
+ }
+ }
+
+ ///
+ /// the "round" operator.
+ ///
+ internal sealed class Round : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ if (num is int numi)
+ {
+ context.Stack.Push(numi);
+ }
+ else
+ {
+ double value = Convert.ToDouble(num);
+ // The way java works...
+ double roundedValue = value < 0 ? Math.Round(value) : Math.Round(value, MidpointRounding.AwayFromZero);
+ context.Stack.Push(roundedValue);
+ }
+ }
+ }
+
+ ///
+ /// the "sin" operator.
+ ///
+ internal sealed class Sin : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ double angle = context.PopReal();
+ double sin = Math.Sin(ToRadians(angle));
+ context.Stack.Push(sin);
+ }
+ }
+
+ ///
+ /// the "sqrt" operator.
+ ///
+ internal sealed class Sqrt : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ double num = context.PopReal();
+ if (num < 0)
+ {
+ throw new ArgumentException("argument must be nonnegative");
+ }
+ context.Stack.Push(Math.Sqrt(num));
+ }
+ }
+
+ ///
+ /// the "sub" operator.
+ ///
+ internal sealed class Sub : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num2 = context.PopNumber();
+ var num1 = context.PopNumber();
+ if (num1 is int num1i && num2 is int num2i)
+ {
+ long result = (long)num1i - (long)num2i; // Keep both cast here
+ if (result < int.MinValue || result > int.MaxValue)
+ {
+ context.Stack.Push((double)result);
+ }
+ else
+ {
+ context.Stack.Push((int)result);
+ }
+ }
+ else
+ {
+ double result = Convert.ToDouble(num1) - Convert.ToDouble(num2);
+ context.Stack.Push(result);
+ }
+ }
+ }
+
+ ///
+ /// the "truncate" operator.
+ ///
+ internal sealed class Truncate : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ var num = context.PopNumber();
+ if (num is int numi)
+ {
+ context.Stack.Push(numi);
+ }
+ else
+ {
+ context.Stack.Push(Math.Truncate(Convert.ToDouble(num)));
+ }
+ }
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Functions/Type4/BitwiseOperators.cs b/src/UglyToad.PdfPig/Functions/Type4/BitwiseOperators.cs
new file mode 100644
index 00000000..08f58c07
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/Type4/BitwiseOperators.cs
@@ -0,0 +1,160 @@
+namespace UglyToad.PdfPig.Functions.Type4
+{
+ using System;
+ using System.Collections.Generic;
+
+ internal sealed class BitwiseOperators
+ {
+ private BitwiseOperators()
+ {
+ // Private constructor.
+ }
+
+ ///
+ /// Abstract base class for logical operators.
+ ///
+ internal abstract class AbstractLogicalOperator : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ object op2 = context.Stack.Pop();
+ object op1 = context.Stack.Pop();
+ if (op1 is bool bool1 && op2 is bool bool2)
+ {
+ bool result = ApplyForBoolean(bool1, bool2);
+ context.Stack.Push(result);
+ }
+ else if (op1 is int int1 && op2 is int int2)
+ {
+ int result = ApplyForInt(int1, int2);
+ context.Stack.Push(result);
+ }
+ else
+ {
+ throw new InvalidCastException("Operands must be bool/bool or int/int");
+ }
+ }
+
+ protected abstract bool ApplyForBoolean(bool bool1, bool bool2);
+
+ protected abstract int ApplyForInt(int int1, int int2);
+ }
+
+ ///
+ /// Implements the "and" operator.
+ ///
+ internal sealed class And : AbstractLogicalOperator
+ {
+ protected override bool ApplyForBoolean(bool bool1, bool bool2)
+ {
+ return bool1 && bool2;
+ }
+
+ protected override int ApplyForInt(int int1, int int2)
+ {
+ return int1 & int2;
+ }
+ }
+
+ ///
+ /// Implements the "bitshift" operator.
+ ///
+ internal sealed class Bitshift : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ int shift = Convert.ToInt32(context.Stack.Pop());
+ int int1 = Convert.ToInt32(context.Stack.Pop());
+ if (shift < 0)
+ {
+ int result = int1 >> Math.Abs(shift);
+ context.Stack.Push(result);
+ }
+ else
+ {
+ int result = int1 << shift;
+ context.Stack.Push(result);
+ }
+ }
+ }
+
+ ///
+ /// Implements the "false" operator.
+ ///
+ internal sealed class False : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ context.Stack.Push(false);
+ }
+ }
+
+ ///
+ /// Implements the "not" operator.
+ ///
+ internal sealed class Not : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ object op1 = context.Stack.Pop();
+ if (op1 is bool bool1)
+ {
+ bool result = !bool1;
+ context.Stack.Push(result);
+ }
+ else if (op1 is int int1)
+ {
+ int result = -int1;
+ context.Stack.Push(result);
+ }
+ else
+ {
+ throw new InvalidCastException("Operand must be bool or int");
+ }
+ }
+ }
+
+ ///
+ /// Implements the "or" operator.
+ ///
+ internal sealed class Or : AbstractLogicalOperator
+ {
+ protected override bool ApplyForBoolean(bool bool1, bool bool2)
+ {
+ return bool1 || bool2;
+ }
+
+ protected override int ApplyForInt(int int1, int int2)
+ {
+ return int1 | int2;
+ }
+ }
+
+ ///
+ /// Implements the "true" operator.
+ ///
+ internal sealed class True : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ context.Stack.Push(true);
+ }
+ }
+
+ ///
+ /// Implements the "xor" operator.
+ ///
+ internal sealed class Xor : AbstractLogicalOperator
+ {
+ protected override bool ApplyForBoolean(bool bool1, bool bool2)
+ {
+ return bool1 ^ bool2;
+ }
+
+ protected override int ApplyForInt(int int1, int int2)
+ {
+ return int1 ^ int2;
+ }
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Functions/Type4/ConditionalOperators.cs b/src/UglyToad.PdfPig/Functions/Type4/ConditionalOperators.cs
new file mode 100644
index 00000000..106c4ae6
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/Type4/ConditionalOperators.cs
@@ -0,0 +1,53 @@
+namespace UglyToad.PdfPig.Functions.Type4
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Provides the conditional operators such as "if" and "ifelse".
+ ///
+ internal sealed class ConditionalOperators
+ {
+ private ConditionalOperators()
+ {
+ // Private constructor.
+ }
+
+ ///
+ /// Implements the "if" operator.
+ ///
+ internal sealed class If : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ InstructionSequence proc = (InstructionSequence)context.Stack.Pop();
+ bool condition = (bool)context.Stack.Pop();
+ if (condition)
+ {
+ proc.Execute(context);
+ }
+ }
+ }
+
+ ///
+ /// Implements the "ifelse" operator.
+ ///
+ internal sealed class IfElse : Operator
+ {
+ public void Execute(ExecutionContext context)
+ {
+ InstructionSequence proc2 = (InstructionSequence)context.Stack.Pop();
+ InstructionSequence proc1 = (InstructionSequence)context.Stack.Pop();
+ bool condition = Convert.ToBoolean(context.Stack.Pop());
+ if (condition)
+ {
+ proc1.Execute(context);
+ }
+ else
+ {
+ proc2.Execute(context);
+ }
+ }
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Functions/Type4/ExecutionContext.cs b/src/UglyToad.PdfPig/Functions/Type4/ExecutionContext.cs
new file mode 100644
index 00000000..79d89e28
--- /dev/null
+++ b/src/UglyToad.PdfPig/Functions/Type4/ExecutionContext.cs
@@ -0,0 +1,81 @@
+namespace UglyToad.PdfPig.Functions.Type4
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ internal sealed class ExecutionContext
+ {
+ private readonly Operators operators;
+
+ ///
+ /// The stack used by this execution context.
+ ///
+ public Stack