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 Stack { get; private set; } = new Stack(); + + /// + /// Creates a new execution context. + /// + /// the operator set + public ExecutionContext(Operators operatorSet) + { + this.operators = operatorSet; + } + + internal void AddAllToStack(IEnumerable values) + { + var valuesList = values.ToList(); + valuesList.AddRange(Stack); + valuesList.Reverse(); + this.Stack = new Stack(valuesList); + } + + /// + /// Returns the operator set used by this execution context. + /// + /// the operator set + public Operators GetOperators() + { + return this.operators; + } + + /// + /// Pops a number (int or real) from the stack. If it's neither data type, a is thrown. + /// + /// the number + public object PopNumber() + { + object popped = this.Stack.Pop(); + if (popped is int || popped is double || popped is float) + { + return popped; + } + throw new InvalidCastException("The object popped is neither an integer or a real."); + } + + /// + /// Pops a value of type int from the stack. If the value is not of type int, a is thrown. + /// + /// the int value + public int PopInt() + { + object popped = Stack.Pop(); + if (popped is int poppedInt) + { + return poppedInt; + } + throw new InvalidCastException("PopInt cannot be done as the value is not integer"); + } + + /// + /// Pops a number from the stack and returns it as a real value. If the value is not of a numeric type, + /// a is thrown. + /// + /// the real value + public double PopReal() + { + return Convert.ToDouble(Stack.Pop()); + } + } +} diff --git a/src/UglyToad.PdfPig/Functions/Type4/InstructionSequence.cs b/src/UglyToad.PdfPig/Functions/Type4/InstructionSequence.cs new file mode 100644 index 00000000..dfbd393b --- /dev/null +++ b/src/UglyToad.PdfPig/Functions/Type4/InstructionSequence.cs @@ -0,0 +1,90 @@ +namespace UglyToad.PdfPig.Functions.Type4 +{ + using System; + using System.Collections.Generic; + using System.Linq; + + internal sealed class InstructionSequence + { + private readonly List instructions = new List(); + + /// + /// Add a name (ex. an operator) + /// + /// the name + public void AddName(string name) + { + this.instructions.Add(name); + } + + /// + /// Adds an int value. + /// + /// the value + public void AddInteger(int value) + { + this.instructions.Add(value); + } + + /// + /// Adds a real value. + /// + /// the value + public void AddReal(double value) + { + this.instructions.Add(value); + } + + /// + /// Adds a bool value. + /// + /// the value + public void AddBoolean(bool value) + { + this.instructions.Add(value); + } + + /// + /// Adds a proc (sub-sequence of instructions). + /// + /// the child proc + public void AddProc(InstructionSequence child) + { + this.instructions.Add(child); + } + + /// + /// Executes the instruction sequence. + /// + /// the execution context + public void Execute(ExecutionContext context) + { + foreach (object o in instructions) + { + if (o is string name) + { + Operator cmd = context.GetOperators().GetOperator(name); + if (cmd != null) + { + cmd.Execute(context); + } + else + { + throw new InvalidOperationException("Unknown operator or name: " + name); + } + } + else + { + context.Stack.Push(o); + } + } + + //Handles top-level procs that simply need to be executed + while (context.Stack.Any() && context.Stack.Peek() is InstructionSequence) + { + InstructionSequence nested = (InstructionSequence)context.Stack.Pop(); + nested.Execute(context); + } + } + } +} diff --git a/src/UglyToad.PdfPig/Functions/Type4/InstructionSequenceBuilder.cs b/src/UglyToad.PdfPig/Functions/Type4/InstructionSequenceBuilder.cs new file mode 100644 index 00000000..ec60e972 --- /dev/null +++ b/src/UglyToad.PdfPig/Functions/Type4/InstructionSequenceBuilder.cs @@ -0,0 +1,83 @@ +namespace UglyToad.PdfPig.Functions.Type4 +{ + using System.Collections.Generic; + + /// + /// Basic parser for Type 4 functions which is used to build up instruction sequences. + /// + internal sealed class InstructionSequenceBuilder : Parser.AbstractSyntaxHandler + { + private readonly InstructionSequence mainSequence = new InstructionSequence(); + private readonly Stack seqStack = new Stack(); + + private InstructionSequenceBuilder() + { + this.seqStack.Push(this.mainSequence); + } + + /// + /// Returns the instruction sequence that has been build from the syntactic elements. + /// + /// the instruction sequence + public InstructionSequence GetInstructionSequence() + { + return this.mainSequence; + } + + /// + /// Parses the given text into an instruction sequence representing a Type 4 function that can be executed. + /// + /// the Type 4 function text + /// the instruction sequence + public static InstructionSequence Parse(string text) + { + InstructionSequenceBuilder builder = new InstructionSequenceBuilder(); + Parser.Parse(text, builder); + return builder.GetInstructionSequence(); + } + + private InstructionSequence GetCurrentSequence() + { + return this.seqStack.Peek(); + } + + /// + public void Token(char[] text) + { + string val = string.Concat(text); + Token(val); + } + + public override void Token(string token) + { + if ("{".Equals(token)) + { + InstructionSequence child = new InstructionSequence(); + GetCurrentSequence().AddProc(child); + this.seqStack.Push(child); + } + else if ("}".Equals(token)) + { + this.seqStack.Pop(); + } + else + { + if (int.TryParse(token, out int tokenInt)) + { + GetCurrentSequence().AddInteger(tokenInt); + return; + } + + if (double.TryParse(token, out double tokenFloat)) + { + GetCurrentSequence().AddReal(tokenFloat); + return; + } + + //TODO Maybe implement radix numbers, such as 8#1777 or 16#FFFE + + GetCurrentSequence().AddName(token); + } + } + } +} diff --git a/src/UglyToad.PdfPig/Functions/Type4/Operator.cs b/src/UglyToad.PdfPig/Functions/Type4/Operator.cs new file mode 100644 index 00000000..35cef092 --- /dev/null +++ b/src/UglyToad.PdfPig/Functions/Type4/Operator.cs @@ -0,0 +1,14 @@ +namespace UglyToad.PdfPig.Functions.Type4 +{ + /// + /// Interface for PostScript operators.e + /// + internal interface Operator + { + /// + /// Executes the operator. The method can inspect and manipulate the stack. + /// + /// the execution context + void Execute(ExecutionContext context); + } +} diff --git a/src/UglyToad.PdfPig/Functions/Type4/Operators.cs b/src/UglyToad.PdfPig/Functions/Type4/Operators.cs new file mode 100644 index 00000000..0c47f556 --- /dev/null +++ b/src/UglyToad.PdfPig/Functions/Type4/Operators.cs @@ -0,0 +1,124 @@ +namespace UglyToad.PdfPig.Functions.Type4 +{ + using System.Collections.Generic; + + /// + /// This class provides all the supported operators. + /// + internal sealed class Operators + { + //Arithmetic operators + private static readonly Operator ABS = new ArithmeticOperators.Abs(); + private static readonly Operator ADD = new ArithmeticOperators.Add(); + private static readonly Operator ATAN = new ArithmeticOperators.Atan(); + private static readonly Operator CEILING = new ArithmeticOperators.Ceiling(); + private static readonly Operator COS = new ArithmeticOperators.Cos(); + private static readonly Operator CVI = new ArithmeticOperators.Cvi(); + private static readonly Operator CVR = new ArithmeticOperators.Cvr(); + private static readonly Operator DIV = new ArithmeticOperators.Div(); + private static readonly Operator EXP = new ArithmeticOperators.Exp(); + private static readonly Operator FLOOR = new ArithmeticOperators.Floor(); + private static readonly Operator IDIV = new ArithmeticOperators.IDiv(); + private static readonly Operator LN = new ArithmeticOperators.Ln(); + private static readonly Operator LOG = new ArithmeticOperators.Log(); + private static readonly Operator MOD = new ArithmeticOperators.Mod(); + private static readonly Operator MUL = new ArithmeticOperators.Mul(); + private static readonly Operator NEG = new ArithmeticOperators.Neg(); + private static readonly Operator ROUND = new ArithmeticOperators.Round(); + private static readonly Operator SIN = new ArithmeticOperators.Sin(); + private static readonly Operator SQRT = new ArithmeticOperators.Sqrt(); + private static readonly Operator SUB = new ArithmeticOperators.Sub(); + private static readonly Operator TRUNCATE = new ArithmeticOperators.Truncate(); + + //Relational, boolean and bitwise operators + private static readonly Operator AND = new BitwiseOperators.And(); + private static readonly Operator BITSHIFT = new BitwiseOperators.Bitshift(); + private static readonly Operator EQ = new RelationalOperators.Eq(); + private static readonly Operator FALSE = new BitwiseOperators.False(); + private static readonly Operator GE = new RelationalOperators.Ge(); + private static readonly Operator GT = new RelationalOperators.Gt(); + private static readonly Operator LE = new RelationalOperators.Le(); + private static readonly Operator LT = new RelationalOperators.Lt(); + private static readonly Operator NE = new RelationalOperators.Ne(); + private static readonly Operator NOT = new BitwiseOperators.Not(); + private static readonly Operator OR = new BitwiseOperators.Or(); + private static readonly Operator TRUE = new BitwiseOperators.True(); + private static readonly Operator XOR = new BitwiseOperators.Xor(); + + //Conditional operators + private static readonly Operator IF = new ConditionalOperators.If(); + private static readonly Operator IFELSE = new ConditionalOperators.IfElse(); + + //Stack operators + private static readonly Operator COPY = new StackOperators.Copy(); + private static readonly Operator DUP = new StackOperators.Dup(); + private static readonly Operator EXCH = new StackOperators.Exch(); + private static readonly Operator INDEX = new StackOperators.Index(); + private static readonly Operator POP = new StackOperators.Pop(); + private static readonly Operator ROLL = new StackOperators.Roll(); + + private readonly Dictionary operators = new Dictionary(); + + /// + /// Creates a new Operators object with the default set of operators. + /// + public Operators() + { + operators.Add("add", ADD); + operators.Add("abs", ABS); + operators.Add("atan", ATAN); + operators.Add("ceiling", CEILING); + operators.Add("cos", COS); + operators.Add("cvi", CVI); + operators.Add("cvr", CVR); + operators.Add("div", DIV); + operators.Add("exp", EXP); + operators.Add("floor", FLOOR); + operators.Add("idiv", IDIV); + operators.Add("ln", LN); + operators.Add("log", LOG); + operators.Add("mod", MOD); + operators.Add("mul", MUL); + operators.Add("neg", NEG); + operators.Add("round", ROUND); + operators.Add("sin", SIN); + operators.Add("sqrt", SQRT); + operators.Add("sub", SUB); + operators.Add("truncate", TRUNCATE); + + operators.Add("and", AND); + operators.Add("bitshift", BITSHIFT); + operators.Add("eq", EQ); + operators.Add("false", FALSE); + operators.Add("ge", GE); + operators.Add("gt", GT); + operators.Add("le", LE); + operators.Add("lt", LT); + operators.Add("ne", NE); + operators.Add("not", NOT); + operators.Add("or", OR); + operators.Add("true", TRUE); + operators.Add("xor", XOR); + + operators.Add("if", IF); + operators.Add("ifelse", IFELSE); + + operators.Add("copy", COPY); + operators.Add("dup", DUP); + operators.Add("exch", EXCH); + operators.Add("index", INDEX); + operators.Add("pop", POP); + operators.Add("roll", ROLL); + } + + /// + /// Returns the operator for the given operator name. + /// + /// the operator name + /// the operator (or null if there's no such operator + public Operator GetOperator(string operatorName) + { + return this.operators[operatorName]; + } + } +} diff --git a/src/UglyToad.PdfPig/Functions/Type4/Parser.cs b/src/UglyToad.PdfPig/Functions/Type4/Parser.cs new file mode 100644 index 00000000..b971965a --- /dev/null +++ b/src/UglyToad.PdfPig/Functions/Type4/Parser.cs @@ -0,0 +1,310 @@ +namespace UglyToad.PdfPig.Functions.Type4 +{ + using System.Text; + + /// + /// Parser for PDF Type 4 functions. This implements a small subset of the PostScript + /// language but is no full PostScript interpreter. + /// + internal sealed class Parser + { + /// + /// Used to indicate the parsers current state. + /// + internal enum State + { + NEWLINE, WHITESPACE, COMMENT, TOKEN + } + + private Parser() + { + //nop + } + + /// + /// Parses a Type 4 function and sends the syntactic elements to the given syntax handler. + /// + /// the text source + /// the syntax handler + public static void Parse(string input, SyntaxHandler handler) + { + Tokenizer tokenizer = new Tokenizer(input, handler); + tokenizer.Tokenize(); + } + + /// + /// This interface defines all possible syntactic elements of a Type 4 function. + /// It is called by the parser as the function is interpreted. + /// + public interface SyntaxHandler + { + /// + /// Indicates that a new line starts. + /// + /// the new line character (CR, LF, CR/LF or FF) + void NewLine(string text); + + /// + /// Called when whitespace characters are encountered. + /// + /// the whitespace text + void Whitespace(string text); + + /// + /// Called when a token is encountered. No distinction between operators and values is done here. + /// + /// the token text + void Token(string text); + + /// + /// Called for a comment. + /// + /// the comment + void Comment(string text); + } + + /// + /// Abstract base class for a . + /// + public abstract class AbstractSyntaxHandler : SyntaxHandler + { + /// + public void Comment(string text) + { + //nop + } + + /// + public void NewLine(string text) + { + //nop + } + + /// + public void Whitespace(string text) + { + //nop + } + + /// + public abstract void Token(string text); + } + + /// + /// Tokenizer for Type 4 functions. + /// + internal class Tokenizer + { + private const char NUL = '\u0000'; //NUL + private const char EOT = '\u0004'; //END OF TRANSMISSION + private const char TAB = '\u0009'; //TAB CHARACTER + private const char FF = '\u000C'; //FORM FEED + private const char CR = '\r'; //CARRIAGE RETURN + private const char LF = '\n'; //LINE FEED + private const char SPACE = '\u0020'; //SPACE + + private readonly string input; + private int index; + private readonly SyntaxHandler handler; + private State state = State.WHITESPACE; + private readonly StringBuilder buffer = new StringBuilder(); + + internal Tokenizer(string text, SyntaxHandler syntaxHandler) + { + this.input = text; + this.handler = syntaxHandler; + } + + private bool HasMore() + { + return index < input.Length; + } + + private char CurrentChar() + { + return input[index]; + } + + private char NextChar() + { + index++; + if (!HasMore()) + { + return EOT; + } + else + { + return CurrentChar(); + } + } + + private char Peek() + { + if (index < input.Length - 1) + { + return input[index + 1]; + } + else + { + return EOT; + } + } + + private State NextState() + { + char ch = CurrentChar(); + switch (ch) + { + case CR: + case LF: + case FF: //FF + state = State.NEWLINE; + break; + case NUL: + case TAB: + case SPACE: + state = State.WHITESPACE; + break; + case '%': + state = State.COMMENT; + break; + default: + state = State.TOKEN; + break; + } + return state; + } + + internal void Tokenize() + { + while (HasMore()) + { + buffer.Length = 0; + NextState(); + switch (state) + { + case State.NEWLINE: + ScanNewLine(); + break; + case State.WHITESPACE: + ScanWhitespace(); + break; + case State.COMMENT: + ScanComment(); + break; + default: + ScanToken(); + break; + } + } + } + + private void ScanNewLine() + { + System.Diagnostics.Debug.Assert(state == State.NEWLINE); + char ch = CurrentChar(); + buffer.Append(ch); + if (ch == CR && Peek() == LF) + { + //CRLF is treated as one newline + buffer.Append(NextChar()); + } + handler.NewLine(buffer.ToString()); + NextChar(); + } + + private void ScanWhitespace() + { + System.Diagnostics.Debug.Assert(state == State.WHITESPACE); + buffer.Append(CurrentChar()); + + bool loop = true; + while (HasMore() && loop) + { + char ch = NextChar(); + switch (ch) + { + case NUL: + case TAB: + case SPACE: + buffer.Append(ch); + break; + default: + loop = false; + break; + } + } + handler.Whitespace(buffer.ToString()); + } + + private void ScanComment() + { + System.Diagnostics.Debug.Assert(state == State.COMMENT); + buffer.Append(CurrentChar()); + + bool loop = true; + while (HasMore() && loop) + { + char ch = NextChar(); + switch (ch) + { + case CR: + case LF: + case FF: + loop = false; + break; + default: + buffer.Append(ch); + break; + } + } + //EOF reached + handler.Comment(buffer.ToString()); + } + + private void ScanToken() + { + System.Diagnostics.Debug.Assert(state == State.TOKEN); + char ch = CurrentChar(); + buffer.Append(ch); + switch (ch) + { + case '{': + case '}': + handler.Token(buffer.ToString()); + NextChar(); + return; + default: + //continue + break; + } + + bool loop = true; + while (HasMore() && loop) + { + ch = NextChar(); + switch (ch) + { + case NUL: + case TAB: + case SPACE: + case CR: + case LF: + case FF: + case EOT: + case '{': + case '}': + loop = false; + break; + + default: + buffer.Append(ch); + break; + } + } + //EOF reached + handler.Token(buffer.ToString()); + } + } + } +} diff --git a/src/UglyToad.PdfPig/Functions/Type4/RelationalOperators.cs b/src/UglyToad.PdfPig/Functions/Type4/RelationalOperators.cs new file mode 100644 index 00000000..f23724fe --- /dev/null +++ b/src/UglyToad.PdfPig/Functions/Type4/RelationalOperators.cs @@ -0,0 +1,118 @@ +namespace UglyToad.PdfPig.Functions.Type4 +{ + using System; + using System.Collections.Generic; + + /// + /// Provides the relational operators such as "eq" and "le". + /// + internal sealed class RelationalOperators + { + private RelationalOperators() + { + // Private constructor. + } + + /// + /// Implements the "eq" operator. + /// + internal class Eq : Operator + { + public void Execute(ExecutionContext context) + { + object op2 = context.Stack.Pop(); + object op1 = context.Stack.Pop(); + bool result = IsEqual(op1, op2); + context.Stack.Push(result); + } + + protected virtual bool IsEqual(object op1, object op2) + { + bool result; + if (op1 is double num1 && op2 is double num2) + { + result = num1.Equals(num2); + } + else + { + result = op1.Equals(op2); + } + return result; + } + } + + /// + /// Abstract base class for number comparison operators. + /// + internal abstract class AbstractNumberComparisonOperator : Operator + { + public void Execute(ExecutionContext context) + { + object op2 = context.Stack.Pop(); + object op1 = context.Stack.Pop(); + double num1 = Convert.ToDouble(op1); + double num2 = Convert.ToDouble(op2); + bool result = Compare(num1, num2); + context.Stack.Push(result); + } + + protected abstract bool Compare(double num1, double num2); + } + + /// + /// Implements the "ge" operator. + /// + internal sealed class Ge : AbstractNumberComparisonOperator + { + protected override bool Compare(double num1, double num2) + { + return num1 >= num2; + } + } + + /// + /// Implements the "gt" operator. + /// + internal sealed class Gt : AbstractNumberComparisonOperator + { + protected override bool Compare(double num1, double num2) + { + return num1 > num2; + } + } + + /// + /// Implements the "le" operator. + /// + internal sealed class Le : AbstractNumberComparisonOperator + { + protected override bool Compare(double num1, double num2) + { + return num1 <= num2; + } + } + + /// + /// Implements the "lt" operator. + /// + internal sealed class Lt : AbstractNumberComparisonOperator + { + protected override bool Compare(double num1, double num2) + { + return num1 < num2; + } + } + + /// + /// Implements the "ne" operator. + /// + internal sealed class Ne : Eq + { + protected override bool IsEqual(object op1, object op2) + { + bool result = base.IsEqual(op1, op2); + return !result; + } + } + } +} diff --git a/src/UglyToad.PdfPig/Functions/Type4/StackOperators.cs b/src/UglyToad.PdfPig/Functions/Type4/StackOperators.cs new file mode 100644 index 00000000..edcdbf8a --- /dev/null +++ b/src/UglyToad.PdfPig/Functions/Type4/StackOperators.cs @@ -0,0 +1,142 @@ +namespace UglyToad.PdfPig.Functions.Type4 +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Provides the stack operators such as "Pop" and "dup". + /// + internal sealed class StackOperators + { + private StackOperators() + { + // Private constructor. + } + + /// + /// Implements the "copy" operator. + /// + internal sealed class Copy : Operator + { + public void Execute(ExecutionContext context) + { + int n = ((int)context.Stack.Pop()); + if (n > 0) + { + int size = context.Stack.Count; + // Need to copy to a new list to avoid ConcurrentModificationException + List copy = context.Stack.ToList().GetRange(size - n - 1, n); + context.AddAllToStack(copy); + } + } + } + + /// + /// Implements the "dup" operator. + /// + internal sealed class Dup : Operator + { + public void Execute(ExecutionContext context) + { + context.Stack.Push(context.Stack.Peek()); + } + } + + /// + /// Implements the "exch" operator. + /// + internal sealed class Exch : Operator + { + public void Execute(ExecutionContext context) + { + object any2 = context.Stack.Pop(); + object any1 = context.Stack.Pop(); + context.Stack.Push(any2); + context.Stack.Push(any1); + } + } + + /// + /// Implements the "index" operator. + /// + internal sealed class Index : Operator + { + public void Execute(ExecutionContext context) + { + int n = Convert.ToInt32(context.Stack.Pop()); + if (n < 0) + { + throw new ArgumentException("rangecheck: " + n); + } + context.Stack.Push(context.Stack.ElementAt(n)); + } + } + + /// + /// Implements the "Pop" operator. + /// + internal sealed class Pop : Operator + { + public void Execute(ExecutionContext context) + { + context.Stack.Pop(); + } + } + + /// + /// Implements the "roll" operator. + /// + internal sealed class Roll : Operator + { + public void Execute(ExecutionContext context) + { + int j = (int)context.Stack.Pop(); + int n = (int)context.Stack.Pop(); + if (j == 0) + { + return; //Nothing to do + } + if (n < 0) + { + throw new ArgumentException("rangecheck: " + n); + } + + var rolled = new List(); + var moved = new List(); + if (j < 0) + { + //negative roll + int n1 = n + j; + for (int i = 0; i < n1; i++) + { + moved.Add(context.Stack.Pop()); + } + for (int i = j; i < 0; i++) + { + rolled.Add(context.Stack.Pop()); + } + + context.AddAllToStack(moved); + context.AddAllToStack(rolled); + } + else + { + //positive roll + int n1 = n - j; + for (int i = j; i > 0; i--) + { + rolled.Add(context.Stack.Pop()); + } + for (int i = 0; i < n1; i++) + { + moved.Add(context.Stack.Pop()); + } + + context.AddAllToStack(rolled); + context.AddAllToStack(moved); + } + } + } + } +} diff --git a/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs b/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs index 3416b4e7..40249297 100644 --- a/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs +++ b/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs @@ -1,11 +1,11 @@ namespace UglyToad.PdfPig.Graphics.Colors { - using PdfPig.Core; using System; using System.Collections.Generic; using System.Linq; using Tokens; using UglyToad.PdfPig.Content; + using UglyToad.PdfPig.Functions; using UglyToad.PdfPig.Util; using UglyToad.PdfPig.Util.JetBrains.Annotations; @@ -160,25 +160,25 @@ /// which are then rendered with the usual primary or process colorants. /// public ColorSpaceDetails AlternateColorSpaceDetails { get; } - + /// /// During subsequent painting operations, an application calls this function to transform a tint value into /// color component values in the alternate color space. /// The function is called with the tint value and must return the corresponding color component values. /// That is, the number of components and the interpretation of their values depend on the . - /// - public Union TintFunction { get; } + /// + public PdfFunction TintFunction { get; } /// /// Create a new . /// public SeparationColorSpaceDetails(NameToken name, - ColorSpaceDetails alternateColorSpaceDetails, - Union tintFunction) + ColorSpaceDetails alternateColorSpaceDetails, + PdfFunction tintFunction) : base(ColorSpace.Separation) { Name = name; - AlternateColorSpaceDetails = alternateColorSpaceDetails; + AlternateColorSpaceDetails = alternateColorSpaceDetails; TintFunction = tintFunction; } } diff --git a/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs b/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs index 8350e424..36e266f4 100644 --- a/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs +++ b/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs @@ -1,15 +1,16 @@ namespace UglyToad.PdfPig.Util { - using System.Collections.Generic; - using System.Linq; using Content; using Core; using Filters; using Graphics.Colors; using Parser.Parts; + using System.Collections.Generic; + using System.Linq; using Tokenization.Scanner; - using Tokens; - + using Tokens; + using UglyToad.PdfPig.Functions; + internal static class ColorSpaceMapper { private static bool TryExtendedColorSpaceNameMapping(NameToken name, out ColorSpace result) @@ -405,24 +406,24 @@ { return UnsupportedColorSpaceDetails.Instance; } - - Union functionTokensUnion; + + PdfFunction function; var func = colorSpaceArray[3]; if (DirectObjectFinder.TryGet(func, scanner, out DictionaryToken functionDictionary)) - { - functionTokensUnion = Union.One(functionDictionary); + { + function = PdfFunctionParser.Create(functionDictionary, scanner, filterProvider); } else if (DirectObjectFinder.TryGet(func, scanner, out StreamToken functionStream)) - { - functionTokensUnion = Union.Two(functionStream); + { + function = PdfFunctionParser.Create(functionStream, scanner, filterProvider); } else { return UnsupportedColorSpaceDetails.Instance; } - return new SeparationColorSpaceDetails(separationNameToken, alternateColorSpaceDetails, functionTokensUnion); + return new SeparationColorSpaceDetails(separationNameToken, alternateColorSpaceDetails, function); } case ColorSpace.DeviceN: return UnsupportedColorSpaceDetails.Instance; diff --git a/src/UglyToad.PdfPig/Util/PdfFunctionParser.cs b/src/UglyToad.PdfPig/Util/PdfFunctionParser.cs new file mode 100644 index 00000000..ad2b36e7 --- /dev/null +++ b/src/UglyToad.PdfPig/Util/PdfFunctionParser.cs @@ -0,0 +1,81 @@ +namespace UglyToad.PdfPig.Util +{ + using System; + using System.Collections.Generic; + using System.IO; + using UglyToad.PdfPig.Filters; + using UglyToad.PdfPig.Functions; + using UglyToad.PdfPig.Parser.Parts; + using UglyToad.PdfPig.Tokenization.Scanner; + using UglyToad.PdfPig.Tokens; + + internal static class PdfFunctionParser + { + public static PdfFunction Create(IToken function, IPdfTokenScanner scanner, ILookupFilterProvider filterProvider) + { + DictionaryToken functionDictionary; + StreamToken functionStream = null; + + if (function is StreamToken fs) + { + functionDictionary = fs.StreamDictionary; + functionStream = new StreamToken(fs.StreamDictionary, fs.Decode(filterProvider, scanner)); + } + else if (function is DictionaryToken fd) + { + functionDictionary = fd; + } + else + { + throw new ArgumentException(nameof(function)); + } + + int functionType = (functionDictionary.Data[NameToken.FunctionType] as NumericToken).Int; + + switch (functionType) + { + case 0: + if (functionStream == null) + { + throw new NotImplementedException("PdfFunctionType0 not stream"); + } + return new PdfFunctionType0(functionStream); + + case 2: + return new PdfFunctionType2(functionDictionary); + + case 3: + var functions = new List(); + if (functionDictionary.TryGet(NameToken.Functions, scanner, out var functionsToken)) + { + foreach (IToken token in functionsToken.Data) + { + if (DirectObjectFinder.TryGet(token, scanner, out var strTk)) + { + functions.Add(Create(strTk, scanner, filterProvider)); + } + else if (DirectObjectFinder.TryGet(token, scanner, out var dicTk)) + { + functions.Add(Create(dicTk, scanner, filterProvider)); + } + else + { + throw new ArgumentException($"Could not find function for token '{token}' inside type 3 function."); + } + } + } + return new PdfFunctionType3(functionDictionary, functions); + + case 4: + if (functionStream == null) + { + throw new NotImplementedException("PdfFunctionType0 not stream"); + } + return new PdfFunctionType4(functionStream); + + default: + throw new IOException("Error: Unknown function type " + functionType); + } + } + } +} diff --git a/src/UglyToad.PdfPig/Writer/PdfAStandard.cs b/src/UglyToad.PdfPig/Writer/PdfAStandard.cs index 50d69a3e..b41984ce 100644 --- a/src/UglyToad.PdfPig/Writer/PdfAStandard.cs +++ b/src/UglyToad.PdfPig/Writer/PdfAStandard.cs @@ -24,6 +24,14 @@ /// /// Compliance with PDF/A2-A. Level A (accessible) conformance are PDF/A2-B standards in addition to features intended to improve a document's accessibility. /// - A2A = 4 + A2A = 4, + /// + /// Compliance with PDF/A3-B. Level B (basic) conformance are PDF/A2-B standards in addition to support for embedded files + /// + A3B = 5, + /// + /// Compliance with PDF/A3-A. Level A (accessible) conformance are PDF/A3-B standards in addition to features intended to improve a document's accessibility. + /// + A3A = 6 } } \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs b/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs index a13ea14d..53467716 100644 --- a/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs +++ b/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs @@ -50,7 +50,7 @@ namespace UglyToad.PdfPig.Writer /// /// The values of the fields to include in the document information dictionary. /// - public DocumentInformationBuilder DocumentInformation { get; } = new DocumentInformationBuilder(); + public DocumentInformationBuilder DocumentInformation { get; set; } = new DocumentInformationBuilder(); /// /// The current page builders in the document and the corresponding 1 indexed page numbers. Use @@ -640,6 +640,11 @@ namespace UglyToad.PdfPig.Writer case PdfAStandard.A2A: PdfA1ARuleBuilder.Obey(catalogDictionary); break; + case PdfAStandard.A3B: + break; + case PdfAStandard.A3A: + PdfA1ARuleBuilder.Obey(catalogDictionary); + break; } } diff --git a/src/UglyToad.PdfPig/Writer/PdfMerger.cs b/src/UglyToad.PdfPig/Writer/PdfMerger.cs index 1abe0d93..21e47a32 100644 --- a/src/UglyToad.PdfPig/Writer/PdfMerger.cs +++ b/src/UglyToad.PdfPig/Writer/PdfMerger.cs @@ -19,11 +19,11 @@ /// /// Merge two PDF documents together with the pages from followed by . /// - public static byte[] Merge(string file1, string file2, IReadOnlyList file1Selection = null, IReadOnlyList file2Selection = null) + public static byte[] Merge(string file1, string file2, IReadOnlyList file1Selection = null, IReadOnlyList file2Selection = null, PdfAStandard archiveStandard = PdfAStandard.None, PdfDocumentBuilder.DocumentInformationBuilder docInfoBuilder = null) { using (var output = new MemoryStream()) { - Merge(file1, file2, output, file1Selection, file2Selection); + Merge(file1, file2, output, file1Selection, file2Selection, archiveStandard, docInfoBuilder); return output.ToArray(); } } @@ -31,7 +31,7 @@ /// /// Merge two PDF documents together with the pages from followed by into the output stream. /// - public static void Merge(string file1, string file2, Stream output, IReadOnlyList file1Selection = null, IReadOnlyList file2Selection = null) + public static void Merge(string file1, string file2, Stream output, IReadOnlyList file1Selection = null, IReadOnlyList file2Selection = null, PdfAStandard archiveStandard = PdfAStandard.None, PdfDocumentBuilder.DocumentInformationBuilder docInfoBuilder = null) { _ = file1 ?? throw new ArgumentNullException(nameof(file1)); _ = file2 ?? throw new ArgumentNullException(nameof(file2)); @@ -40,7 +40,7 @@ { using (var stream2 = File.OpenRead(file2)) { - Merge(new[] { stream1, stream2 }, output, new[] { file1Selection, file2Selection }); + Merge(new[] { stream1, stream2 }, output, new[] { file1Selection, file2Selection }, archiveStandard, docInfoBuilder); } } } @@ -49,10 +49,18 @@ /// Merge multiple PDF documents together with the pages in the order the file paths are provided. /// public static byte[] Merge(params string[] filePaths) + { + return Merge(PdfAStandard.None, null, filePaths); + } + + /// + /// Merge multiple PDF documents together with the pages in the order the file paths are provided. + /// + public static byte[] Merge(PdfAStandard archiveStandard, PdfDocumentBuilder.DocumentInformationBuilder docInfoBuilder, params string[] filePaths) { using (var output = new MemoryStream()) { - Merge(output, filePaths); + Merge(output, archiveStandard, docInfoBuilder, filePaths); return output.ToArray(); } } @@ -60,7 +68,15 @@ /// /// Merge multiple PDF documents together with the pages in the order the file paths are provided into the output stream /// - public static void Merge(Stream output, params string[] filePaths) + public static void Merge(Stream output, params string[] filePaths) + { + Merge(output, PdfAStandard.None, null, filePaths); + } + + /// + /// Merge multiple PDF documents together with the pages in the order the file paths are provided into the output stream + /// + public static void Merge(Stream output, PdfAStandard archiveStandard, PdfDocumentBuilder.DocumentInformationBuilder docInfoBuilder, params string[] filePaths) { var streams = new List(filePaths.Length); try @@ -71,7 +87,7 @@ streams.Add(File.OpenRead(filePath)); } - Merge(streams, output, null); + Merge(streams, output, null, archiveStandard, docInfoBuilder); } finally { @@ -85,13 +101,13 @@ /// /// Merge the set of PDF documents. /// - public static byte[] Merge(IReadOnlyList files, IReadOnlyList> pagesBundle = null) + public static byte[] Merge(IReadOnlyList files, IReadOnlyList> pagesBundle = null, PdfAStandard archiveStandard = PdfAStandard.None, PdfDocumentBuilder.DocumentInformationBuilder docInfoBuilder = null) { _ = files ?? throw new ArgumentNullException(nameof(files)); using (var output = new MemoryStream()) { - Merge(files.Select(f => PdfDocument.Open(f)).ToArray(), output, pagesBundle); + Merge(files.Select(f => PdfDocument.Open(f)).ToArray(), output, pagesBundle, archiveStandard, docInfoBuilder); return output.ToArray(); } } @@ -104,20 +120,28 @@ /// /// Must be writable /// + /// + /// /// - public static void Merge(IReadOnlyList streams, Stream output, IReadOnlyList> pagesBundle = null) + public static void Merge(IReadOnlyList streams, Stream output, IReadOnlyList> pagesBundle = null, PdfAStandard archiveStandard = PdfAStandard.None, PdfDocumentBuilder.DocumentInformationBuilder docInfoBuilder = null) { _ = streams ?? throw new ArgumentNullException(nameof(streams)); _ = output ?? throw new ArgumentNullException(nameof(output)); - Merge(streams.Select(f => PdfDocument.Open(f)).ToArray(), output, pagesBundle); + Merge(streams.Select(f => PdfDocument.Open(f)).ToArray(), output, pagesBundle, archiveStandard, docInfoBuilder); } - private static void Merge(IReadOnlyList files, Stream output, IReadOnlyList> pagesBundle) + private static void Merge(IReadOnlyList files, Stream output, IReadOnlyList> pagesBundle, PdfAStandard archiveStandard = PdfAStandard.None, PdfDocumentBuilder.DocumentInformationBuilder docInfoBuilder = null) { var maxVersion = files.Select(x=>x.Version).Max(); using (var document = new PdfDocumentBuilder(output, false, PdfWriterType.Default, maxVersion)) { + document.ArchiveStandard = archiveStandard; + if (docInfoBuilder != null) + { + document.IncludeDocumentInformation = true; + document.DocumentInformation = docInfoBuilder; + } foreach (var fileIndex in Enumerable.Range(0, files.Count)) { var existing = files[fileIndex]; diff --git a/src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs b/src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs index 84a924b9..a61ab83d 100644 --- a/src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs +++ b/src/UglyToad.PdfPig/Writer/Xmp/XmpWriter.cs @@ -147,6 +147,14 @@ namespace UglyToad.PdfPig.Writer.Xmp part = 2; conformance = "A"; break; + case PdfAStandard.A3A: + part = 3; + conformance = "A"; + break; + case PdfAStandard.A3B: + part = 3; + conformance = "B"; + break; default: throw new ArgumentOutOfRangeException(nameof(standard), standard, null); }