Merge pull request #557 from UglyToad/functions

Implement pdf functions and add type 0, 2 and 4 function tests
This commit is contained in:
Eliot Jones 2023-03-17 18:42:41 +01:00 committed by GitHub
commit 52b99b6816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 3963 additions and 20 deletions

View File

@ -0,0 +1,90 @@
namespace UglyToad.PdfPig.Core
{
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// This class will be used to signify a range. a(min) &lt;= a* &lt;= a(max)
/// </summary>
public struct PdfRange
{
private readonly IReadOnlyList<double> rangeArray;
private readonly int startingIndex;
/// <summary>
/// Constructor with an initial range of 0..1.
/// </summary>
public PdfRange()
{
rangeArray = new double[] { 0.0, 1.0 };
startingIndex = 0;
}
/// <summary>
/// Constructor assumes a starting index of 0.
/// </summary>
/// <param name="range">The array that describes the range.</param>
public PdfRange(IEnumerable<decimal> range)
: this(range.Select(v => (double)v), 0)
{
}
/// <summary>
/// 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 <c>new PDRange(array, 1)</c>.
/// </summary>
/// <param name="range">The array that describes the index</param>
/// <param name="index">The range index into the array for the start of the range.</param>
public PdfRange(IEnumerable<decimal> range, int index)
: this(range.Select(v => (double)v), index)
{
}
/// <summary>
/// Constructor assumes a starting index of 0.
/// </summary>
/// <param name="range">The array that describes the range.</param>
public PdfRange(IEnumerable<double> range)
: this(range, 0)
{
}
/// <summary>
/// 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 <c>new PDRange(array, 1)</c>.
/// </summary>
/// <param name="range">The array that describes the index</param>
/// <param name="index">The range index into the array for the start of the range.</param>
public PdfRange(IEnumerable<double> range, int index)
{
rangeArray = range.Select(v => (double)v).ToArray();
startingIndex = index;
}
/// <summary>
/// The minimum value of the range.
/// </summary>
public double Min
{
get
{
return rangeArray[startingIndex * 2];
}
}
/// <summary>
/// The maximum value of the range.
/// </summary>
public double Max
{
get
{
return rangeArray[startingIndex * 2 + 1];
}
}
}
}

View File

@ -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, IToken>()
{
{ 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, IToken>()
{
{ 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, IToken>()
{
{ 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, IToken>()
{
{ 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, IToken>()
{
{ 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
}
}
}

View File

@ -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, IToken>()
{
{ 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
}
}
}

View File

@ -0,0 +1,11 @@
namespace UglyToad.PdfPig.Tests.Functions
{
using System;
using System.Collections.Generic;
using System.Text;
public class PdfFunctionType3Tests
{
// TODO
}
}

View File

@ -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, IToken>()
{
{ 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);
}
/// <summary>
/// Checks the <see cref="PdfFunctionType4"/>.
/// </summary>
[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]);
}
/// <summary>
/// Checks the handling of the argument order for a <see cref="PdfFunctionType4"/>.
/// </summary>
[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);
}
}
}

View File

@ -0,0 +1,482 @@
namespace UglyToad.PdfPig.Tests.Functions.Type4
{
using System;
using UglyToad.PdfPig.Functions.Type4;
using Xunit;
public class OperatorsTests
{
/// <summary>
/// Tests the "add" operator.
/// </summary>
[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);
}
/// <summary>
/// Tests the "abs" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "and" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "atan" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "ceiling" operator.
/// </summary>
[Fact]
public void Ceiling()
{
Type4Tester.Create("3.2 ceiling -4.8 ceiling 99 ceiling")
.Pop(99.0).Pop(-4.0).Pop(4.0).IsEmpty();
}
/// <summary>
/// Tests the "cos" operator.
/// </summary>
[Fact]
public void Cos()
{
Type4Tester.Create("0 cos").PopReal(1).IsEmpty();
Type4Tester.Create("90 cos").PopReal(0).IsEmpty();
}
/// <summary>
/// Tests the "cvi" operator.
/// </summary>
[Fact]
public void Cvi()
{
Type4Tester.Create("-47.8 cvi").Pop(-47).IsEmpty();
Type4Tester.Create("520.9 cvi").Pop(520).IsEmpty();
}
/// <summary>
/// Tests the "cvr" operator.
/// </summary>
[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");
}
/// <summary>
/// Tests the "div" operator.
/// </summary>
[Fact]
public void Div()
{
Type4Tester.Create("3 2 div").PopReal(1.5).IsEmpty();
Type4Tester.Create("4 2 div").PopReal(2.0).IsEmpty();
}
/// <summary>
/// Tests the "exp" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "floor" operator.
/// </summary>
[Fact]
public void Floor()
{
Type4Tester.Create("3.2 floor -4.8 floor 99 floor")
.Pop(99.0).Pop(-5.0).Pop(3.0).IsEmpty();
}
/// <summary>
/// Tests the "div" operator.
/// </summary>
[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<InvalidCastException>(() => Type4Tester.Create("4.4 2 idiv"));
}
/// <summary>
/// Tests the "ln" operator.
/// </summary>
[Fact]
public void Ln()
{
Type4Tester.Create("10 ln").PopReal(2.30259, 0.00001).IsEmpty();
Type4Tester.Create("100 ln").PopReal(4.60517, 0.00001).IsEmpty();
}
/// <summary>
/// Tests the "log" operator.
/// </summary>
[Fact]
public void Log()
{
Type4Tester.Create("10 log").PopReal(1.0).IsEmpty();
Type4Tester.Create("100 log").PopReal(2.0).IsEmpty();
}
/// <summary>
/// Tests the "mod" operator.
/// </summary>
[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<InvalidCastException>(() => Type4Tester.Create("4.4 2 mod"));
}
/// <summary>
/// Tests the "mul" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "neg" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "round" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "sin" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "sqrt" operator.
/// </summary>
[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<ArgumentException>(() => Type4Tester.Create("-4.1 sqrt"));
}
/// <summary>
/// Tests the "sub" operator.
/// </summary>
[Fact]
public void Sub()
{
Type4Tester.Create("5 2 sub -7.5 1 sub").Pop(-8.5f).Pop(3).IsEmpty();
}
/// <summary>
/// Tests the "truncate" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "bitshift" operator.
/// </summary>
[Fact]
public void Bitshift()
{
Type4Tester.Create("7 3 bitshift 142 -3 bitshift")
.Pop(17).Pop(56).IsEmpty();
}
/// <summary>
/// Tests the "eq" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "ge" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "gt" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "le" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "lt" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "ne" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "not" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "or" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "cor" operator.
/// </summary>
[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);
}
/// <summary>
/// Tests the "if" operator.
/// </summary>
[Fact]
public void If()
{
Type4Tester.Create("true { 2 1 add } if")
.Pop(3).IsEmpty();
Type4Tester.Create("false { 2 1 add } if")
.IsEmpty();
Assert.Throws<InvalidCastException>(() => Type4Tester.Create("0 { 2 1 add } if"));
}
/// <summary>
/// Tests the "ifelse" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "copy" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "dup" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "exch" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "index" operator.
/// </summary>
[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();
}
/// <summary>
/// Tests the "pop" operator.
/// </summary>
[Fact]
public void Pop()
{
Type4Tester.Create("1 pop 7 2 pop")
.Pop(7).IsEmpty();
Type4Tester.Create("1 2 3 pop pop")
.Pop(1).IsEmpty();
}
/// <summary>
/// Tests the "roll" operator.
/// </summary>
[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();
}
}
}

View File

@ -0,0 +1,42 @@
namespace UglyToad.PdfPig.Tests.Functions.Type4
{
using Xunit;
public class ParserTests
{
/// <summary>
/// Test the very basics.
/// </summary>
[Fact]
public void ParserBasics()
{
Type4Tester.Create("3 4 add 2 sub").Pop(5).IsEmpty();
}
/// <summary>
/// Test nested blocks.
/// </summary>
[Fact]
public void Nested()
{
Type4Tester.Create("true { 2 1 add } { 2 1 sub } ifelse")
.Pop(3).IsEmpty();
Type4Tester.Create("{ true }").Pop(true).IsEmpty();
}
/// <summary>
/// Tests problematic functions from PDFBOX-804.
/// </summary>
[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();
}
}
}

View File

@ -0,0 +1,124 @@
namespace UglyToad.PdfPig.Tests.Functions.Type4
{
using System;
using UglyToad.PdfPig.Functions.Type4;
using Xunit;
/// <summary>
/// Testing helper class for testing type 4 functions from the PDF specification.
/// </summary>
public sealed class Type4Tester
{
private readonly ExecutionContext context;
private Type4Tester(ExecutionContext ctxt)
{
this.context = ctxt;
}
/// <summary>
/// Creates a new instance for the given type 4 function.
/// </summary>
/// <param name="text">the text of the type 4 function</param>
/// <returns>the tester instance</returns>
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);
}
/// <summary>
/// Pops a bool value from the stack and checks it against the expected result.
/// </summary>
/// <param name="expected">the expected bool value</param>
/// <returns>this instance</returns>
public Type4Tester Pop(bool expected)
{
bool value = (bool)context.Stack.Pop();
Assert.Equal(expected, value);
return this;
}
/// <summary>
/// Pops a real value from the stack and checks it against the expected result.
/// </summary>
/// <param name="expected">the expected real value</param>
/// <returns>this instance</returns>
public Type4Tester PopReal(double expected)
{
return PopReal(expected, 0.0000001);
}
/// <summary>
/// Pops a real value from the stack and checks it against the expected result.
/// </summary>
/// <param name="expected">the expected real value</param>
/// <param name="delta">delta the allowed deviation of the value from the expected result</param>
/// <returns>this instance</returns>
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;
}
/// <summary>
/// Pops an int value from the stack and checks it against the expected result.
/// </summary>
/// <param name="expected">the expected int value</param>
/// <returns>this instance</returns>
public Type4Tester Pop(int expected)
{
int value = context.PopInt();
Assert.Equal(expected, value);
return this;
}
/// <summary>
/// Pops a numeric value from the stack and checks it against the expected result.
/// </summary>
/// <param name="expected">the expected numeric value</param>
/// <returns>this instance</returns>
public Type4Tester Pop(double expected)
{
return Pop(expected, 0.0000001);
}
/// <summary>
/// Pops a numeric value from the stack and checks it against the expected result.
/// </summary>
/// <param name="expected">the expected numeric value</param>
/// <param name="delta">the allowed deviation of the value from the expected result</param>
/// <returns>this instance</returns>
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;
}
/// <summary>
/// Checks that the stack is empty at this point.
/// </summary>
/// <returns>this instance</returns>
public Type4Tester IsEmpty()
{
Assert.Empty(context.Stack);
return this;
}
/// <summary>
/// Returns the execution context so some custom checks can be performed.
/// </summary>
/// <returns>the associated execution context</returns>
internal ExecutionContext ToExecutionContext()
{
return this.context;
}
}
}

View File

@ -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",

View File

@ -158,7 +158,7 @@
}
try
{
{
loadedFonts[reference] = fontFactory.Get(fontObject);
}
catch
@ -168,7 +168,6 @@
throw;
}
}
}
else if (pair.Value is DictionaryToken fd)
{

View File

@ -0,0 +1,274 @@
namespace UglyToad.PdfPig.Functions
{
using System;
using System.Linq;
using UglyToad.PdfPig.Core;
using UglyToad.PdfPig.Tokens;
/// <summary>
/// This class represents a function in a PDF document.
/// </summary>
public abstract class PdfFunction
{
/// <summary>
/// The function dictionary.
/// </summary>
public DictionaryToken FunctionDictionary { get; }
/// <summary>
/// The function stream.
/// </summary>
public StreamToken FunctionStream { get; }
private ArrayToken domain;
private ArrayToken range;
private int numberOfInputValues = -1;
private int numberOfOutputValues = -1;
/// <summary>
/// This class represents a function in a PDF document.
/// </summary>
public PdfFunction(DictionaryToken function)
{
FunctionDictionary = function;
}
/// <summary>
/// This class represents a function in a PDF document.
/// </summary>
public PdfFunction(StreamToken function)
{
FunctionStream = function;
}
/// <summary>
/// Returns the function type. Possible values are:
/// <list type="bullet">
/// <item><term>0</term><description>Sampled function</description></item>
/// <item><term>2</term><description>Exponential interpolation function</description></item>
/// <item><term>3</term><description>Stitching function</description></item>
/// <item><term>4</term><description>PostScript calculator function</description></item>
/// </list>
/// </summary>
/// <returns>the function type.</returns>
public abstract FunctionTypes FunctionType { get; }
/// <summary>
/// Returns the function's dictionary. If <see cref="FunctionDictionary"/> is defined, it will be returned.
/// If not, the <see cref="FunctionStream"/>'s StreamDictionary will be returned.
/// </summary>
public DictionaryToken GetDictionary()
{
if (FunctionStream != null)
{
return FunctionStream.StreamDictionary;
}
else
{
return FunctionDictionary;
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The number of output parameters that have a range specified.</returns>
public int NumberOfOutputParameters
{
get
{
if (numberOfOutputValues == -1)
{
if (RangeValues == null)
{
numberOfOutputValues = 0;
}
else
{
numberOfOutputValues = RangeValues.Length / 2;
}
}
return numberOfOutputValues;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="n">The output parameter number to get the range for.</param>
/// <returns>The range for this component.</returns>
public PdfRange GetRangeForOutput(int n)
{
return new PdfRange(RangeValues.Data.OfType<NumericToken>().Select(t => t.Double), n);
}
/// <summary>
/// This will get the number of input parameters that
/// have a domain specified.
/// </summary>
/// <returns>The number of input parameters that have a domain specified.</returns>
public int NumberOfInputParameters
{
get
{
if (numberOfInputValues == -1)
{
ArrayToken array = GetDomainValues();
numberOfInputValues = array.Length / 2;
}
return numberOfInputValues;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="n">The parameter number to get the domain for.</param>
/// <returns>The domain range for this component.</returns>
public PdfRange GetDomainForInput(int n)
{
ArrayToken domainValues = GetDomainValues();
return new PdfRange(domainValues.Data.OfType<NumericToken>().Select(t => t.Double), n);
}
/// <summary>
/// Evaluates the function at the given input.
/// ReturnValue = f(input)
/// </summary>
/// <param name="input">The array of input values for the function.
/// In many cases will be an array of a single value, but not always.</param>
/// <returns>The of outputs the function returns based on those inputs.
/// In many cases will be an array of a single value, but not always.</returns>
public abstract double[] Eval(double[] input);
/// <summary>
/// Returns all ranges for the output values as <see cref="ArrayToken"/>. Required for type 0 and type 4 functions.
/// </summary>
/// <returns>the ranges array.</returns>
protected virtual ArrayToken RangeValues
{
get
{
if (range == null)
{
GetDictionary().TryGet(NameToken.Range, out range); // Optionnal
}
return range;
}
}
/// <summary>
/// Returns all domains for the input values as <see cref="ArrayToken"/>. Required for all function types.
/// </summary>
/// <returns>the domains array.</returns>
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;
}
/// <summary>
/// Clip the given input values to the ranges.
/// </summary>
/// <param name="inputValues">inputValues the input values</param>
/// <returns>the clipped values</returns>
protected double[] ClipToRange(double[] inputValues)
{
ArrayToken rangesArray = RangeValues;
double[] result;
if (rangesArray != null && rangesArray.Length > 0)
{
double[] rangeValues = rangesArray.Data.OfType<NumericToken>().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;
}
/// <summary>
/// Clip the given input value to the given range.
/// </summary>
/// <param name="x">x the input value</param>
/// <param name="rangeMin">the min value of the range</param>
/// <param name="rangeMax">the max value of the range</param>
/// <returns>the clipped value</returns>
protected static double ClipToRange(double x, double rangeMin, double rangeMax)
{
if (x < rangeMin)
{
return rangeMin;
}
else if (x > rangeMax)
{
return rangeMax;
}
return x;
}
/// <summary>
/// For a given value of x, interpolate calculates the y value
/// on the line defined by the two points (xRangeMin, xRangeMax)
/// and (yRangeMin, yRangeMax).
/// </summary>
/// <param name="x">the value to be interpolated value.</param>
/// <param name="xRangeMin">the min value of the x range</param>
/// <param name="xRangeMax">the max value of the x range</param>
/// <param name="yRangeMin">the min value of the y range</param>
/// <param name="yRangeMax">the max value of the y range</param>
/// <returns>the interpolated y value</returns>
protected static double Interpolate(double x, double xRangeMin, double xRangeMax, double yRangeMin, double yRangeMax)
{
return yRangeMin + ((x - xRangeMin) * (yRangeMax - yRangeMin) / (xRangeMax - xRangeMin));
}
}
/// <summary>
/// Pdf function types.
/// </summary>
public enum FunctionTypes : byte
{
/// <summary>
/// Sampled function.
/// </summary>
Sampled = 0,
/// <summary>
/// Exponential interpolation function.
/// </summary>
Exponential = 2,
/// <summary>
/// Stitching function.
/// </summary>
Stitching = 3,
/// <summary>
/// PostScript calculator function.
/// </summary>
PostScript = 4
}
}

View File

@ -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
{
/// <summary>
/// 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) ...].
/// </summary>
private ArrayToken encode;
/// <summary>
/// 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.
/// </summary>
private ArrayToken decode;
/// <summary>
/// An array of m positive integers specifying the number of samples in each
/// input dimension of the sample table.
/// </summary>
private ArrayToken size;
/// <summary>
/// The samples of the function.
/// </summary>
private int[][] samples;
/// <summary>
/// Stitching function
/// </summary>
internal PdfFunctionType0(DictionaryToken function) : base(function)
{
}
/// <summary>
/// Stitching function
/// </summary>
internal PdfFunctionType0(StreamToken function) : base(function)
{
}
public override FunctionTypes FunctionType
{
get
{
return FunctionTypes.Sampled;
}
}
/// <summary>
/// The "Size" entry, which is the number of samples in each input dimension of the sample table.
/// </summary>
public ArrayToken Size
{
get
{
if (size == null && !GetDictionary().TryGet(NameToken.Size, out size))
{
throw new ArgumentNullException(NameToken.Size);
}
return size;
}
}
/// <summary>
/// Get the number of bits that the output value will take up.
/// <para>Valid values are 1,2,4,8,12,16,24,32.</para>
/// </summary>
/// <returns>Number of bits for each output value.</returns>
public int BitsPerSample
{
get
{
if (!GetDictionary().TryGet<NumericToken>(NameToken.BitsPerSample, out var bps))
{
throw new ArgumentNullException(NameToken.BitsPerSample);
}
return bps.Int;
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>order of interpolation.</returns>
public int Order
{
get
{
if (!GetDictionary().TryGet<NumericToken>(NameToken.Order, out var order))
{
return 1;
}
return order.Int;
}
}
/// <summary>
/// Returns all encode values as <see cref="ArrayToken"/>.
/// </summary>
/// <returns>the encode array. </returns>
private ArrayToken EncodeValues
{
get
{
if (encode == null)
{
GetDictionary().TryGet<ArrayToken>(NameToken.Encode, out encode);
// the default value is [0 (size[0]-1) 0 (size[1]-1) ...]
if (encode == null)
{
var values = new List<NumericToken>();
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;
}
}
/// <summary>
/// Returns all decode values as <see cref="ArrayToken"/>.
/// </summary>
/// <returns>the decode array.</returns>
private ArrayToken DecodeValues
{
get
{
if (decode == null)
{
GetDictionary().TryGet<ArrayToken>(NameToken.Decode, out decode);
// if decode is null, the default values are the range values
if (decode == null)
{
decode = RangeValues;
}
}
return decode;
}
}
/// <summary>
/// Get the encode for the input parameter.
/// </summary>
/// <param name="paramNum">The function parameter number.</param>
/// <returns>The encode parameter range or null if none is set.</returns>
public PdfRange? GetEncodeForParameter(int paramNum)
{
ArrayToken encodeValues = EncodeValues;
if (encodeValues != null && encodeValues.Length >= paramNum * 2 + 1)
{
return new PdfRange(encodeValues.Data.OfType<NumericToken>().Select(t => t.Double), paramNum);
}
return null;
}
/// <summary>
/// Get the decode for the input parameter.
/// </summary>
/// <param name="paramNum">The function parameter number.</param>
/// <returns>The decode parameter range or null if none is set.</returns>
public PdfRange? GetDecodeForParameter(int paramNum)
{
ArrayToken decodeValues = DecodeValues;
if (decodeValues != null && decodeValues.Length >= paramNum * 2 + 1)
{
return new PdfRange(decodeValues.Data.OfType<NumericToken>().Select(t => t.Double), paramNum);
}
return null;
}
/// <summary>
/// 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.
/// <para>To understand the algorithm without recursion, see <see href="http://harmoniccode.blogspot.de/2011/04/bilinear-color-interpolation.html"/> for a bilinear interpolation
/// and <see href="https://en.wikipedia.org/wiki/Trilinear_interpolation"/> for trilinear interpolation.
/// </para>
/// </summary>
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;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="input">the input coordinates</param>
/// <param name="inputPrev">coordinate of the "ceil" point</param>
/// <param name="inputNext">coordinate of the "floor" point</param>
/// <param name="numberOfOutputValues"></param>
/// <param name="size"></param>
/// <param name="samples"></param>
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;
}
/// <summary>
/// Calculate the interpolation.
/// </summary>
/// <returns>interpolated result sample</returns>
internal double[] RInterpolate()
{
return InternalRInterpol(new int[numberOfInputValues], 0);
}
/// <summary>
/// Do a linear interpolation if the two coordinates can be known, or
/// call itself recursively twice.
/// </summary>
/// <param name="coord">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</param>
/// <param name="step">between 0 (first call) and dimension - 1</param>
/// <returns>interpolated result sample</returns>
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;
}
}
/// <summary>
/// calculate array index (structure described in p.171 PDF spec 1.7) in multiple dimensions.
/// </summary>
/// <param name="vector">with coordinates</param>
/// <returns>index in flat array</returns>
private int CalcSampleIndex(int[] vector)
{
// inspiration: http://stackoverflow.com/a/12113479/535646
// but used in reverse
double[] sizeValues = size.Data.OfType<NumericToken>().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;
}
}
/// <summary>
/// Get all sample values of this function.
/// </summary>
/// <returns>an array with all samples.</returns>
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<NumericToken>().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;
}
}
}

View File

@ -0,0 +1,138 @@
namespace UglyToad.PdfPig.Functions
{
using System;
using System.Collections.Generic;
using UglyToad.PdfPig.Tokens;
/// <summary>
/// Exponential interpolation function
/// </summary>
internal sealed class PdfFunctionType2 : PdfFunction
{
/// <summary>
/// Exponential interpolation function
/// </summary>
internal PdfFunctionType2(DictionaryToken function) : base(function)
{
if (GetDictionary().TryGet(NameToken.C0, out ArrayToken array0))
{
C0 = array0;
}
else
{
C0 = new ArrayToken(new List<IToken>());
}
if (C0.Length == 0)
{
C0 = new ArrayToken(new List<NumericToken>() { new NumericToken(0) });
}
if (GetDictionary().TryGet(NameToken.C1, out ArrayToken array1))
{
C1 = array1;
}
else
{
C1 = new ArrayToken(new List<IToken>());
}
if (C0.Length == 0)
{
C1 = new ArrayToken(new List<NumericToken>() { 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<IToken>());
}
if (C0.Length == 0)
{
C0 = new ArrayToken(new List<NumericToken>() { new NumericToken(0) });
}
if (GetDictionary().TryGet(NameToken.C1, out ArrayToken array1))
{
C1 = array1;
}
else
{
C1 = new ArrayToken(new List<IToken>());
}
if (C0.Length == 0)
{
C1 = new ArrayToken(new List<NumericToken>() { 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);
}
/// <summary>
/// The C0 values of the function, 0 if empty.
/// </summary>
public ArrayToken C0 { get; }
/// <summary>
/// The C1 values of the function, 1 if empty.
/// </summary>
public ArrayToken C1 { get; }
/// <summary>
/// The exponent of the function.
/// </summary>
public double N { get; }
public override string ToString()
{
return "FunctionType2{"
+ "C0: " + C0 + " "
+ "C1: " + C1 + " "
+ "N: " + N + "}";
}
}
}

View File

@ -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;
/// <summary>
/// Stitching function
/// </summary>
internal sealed class PdfFunctionType3 : PdfFunction
{
private ArrayToken functions;
private ArrayToken encode;
private ArrayToken bounds;
private double[] boundsValues;
/// <summary>
/// Stitching function
/// </summary>
internal PdfFunctionType3(DictionaryToken function, IReadOnlyList<PdfFunction> functionsArray)
: base(function)
{
if (functionsArray == null || functionsArray.Count == 0)
{
throw new ArgumentNullException(nameof(functionsArray));
}
this.FunctionsArray = functionsArray;
}
/// <summary>
/// Stitching function
/// </summary>
internal PdfFunctionType3(StreamToken function, IReadOnlyList<PdfFunction> 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<NumericToken>().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<PdfFunction> FunctionsArray { get; }
/// <summary>
/// Returns all functions values as <see cref="ArrayToken"/>.
/// </summary>
/// <returns>the functions array. </returns>
public ArrayToken Functions
{
get
{
if (functions == null && !GetDictionary().TryGet<ArrayToken>(NameToken.Functions, out functions))
{
throw new ArgumentNullException(NameToken.Functions);
}
return functions;
}
}
/// <summary>
/// Returns all bounds values as <see cref="ArrayToken"/>.
/// </summary>
/// <returns>the bounds array.</returns>
public ArrayToken Bounds
{
get
{
if (bounds == null && !GetDictionary().TryGet<ArrayToken>(NameToken.Bounds, out bounds))
{
throw new ArgumentNullException(NameToken.Bounds);
}
return bounds;
}
}
/// <summary>
/// Returns all encode values as <see cref="ArrayToken"/>.
/// </summary>
/// <returns>the encode array.</returns>
public ArrayToken Encode
{
get
{
if (encode == null && !GetDictionary().TryGet<ArrayToken>(NameToken.Encode, out encode))
{
throw new ArgumentNullException(NameToken.Encode);
}
return encode;
}
}
/// <summary>
/// Get the encode for the input parameter.
/// </summary>
/// <param name="n">The function parameter number.</param>
/// <returns>The encode parameter range or null if none is set.</returns>
private PdfRange GetEncodeForParameter(int n)
{
ArrayToken encodeValues = Encode;
return new PdfRange(encodeValues.Data.OfType<NumericToken>().Select(t => t.Double), n);
}
}
}

View File

@ -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;
/// <summary>
/// PostScript calculator function
/// </summary>
internal sealed class PdfFunctionType4 : PdfFunction
{
private readonly Operators operators = new Operators();
private readonly InstructionSequence instructions;
/// <summary>
/// PostScript calculator function
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,398 @@
namespace UglyToad.PdfPig.Functions.Type4
{
using System;
/// <summary>
/// Provides the arithmetic operators such as "add" and "sub".
/// </summary>
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;
}
/// <summary>
/// the "Abs" operator.
/// </summary>
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)));
}
}
}
/// <summary>
/// the "add" operator.
/// </summary>
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);
}
}
}
/// <summary>
/// the "atan" operator.
/// </summary>
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);
}
}
/// <summary>
/// the "ceiling" operator.
/// </summary>
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)));
}
}
}
/// <summary>
/// the "cos" operator.
/// </summary>
internal sealed class Cos : Operator
{
public void Execute(ExecutionContext context)
{
double angle = context.PopReal();
double cos = Math.Cos(ToRadians(angle));
context.Stack.Push(cos);
}
}
/// <summary>
/// the "cvi" operator.
/// </summary>
internal sealed class Cvi : Operator
{
public void Execute(ExecutionContext context)
{
var num = context.PopNumber();
context.Stack.Push((int)Math.Truncate(Convert.ToDouble(num)));
}
}
/// <summary>
/// the "cvr" operator.
/// </summary>
internal sealed class Cvr : Operator
{
public void Execute(ExecutionContext context)
{
var num = context.PopNumber();
context.Stack.Push(Convert.ToDouble(num));
}
}
/// <summary>
/// the "div" operator.
/// </summary>
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);
}
}
/// <summary>
/// the "exp" operator.
/// </summary>
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);
}
}
/// <summary>
/// the "floor" operator.
/// </summary>
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)));
}
}
}
/// <summary>
/// the "idiv" operator.
/// </summary>
internal sealed class IDiv : Operator
{
public void Execute(ExecutionContext context)
{
int num2 = context.PopInt();
int num1 = context.PopInt();
context.Stack.Push(num1 / num2);
}
}
/// <summary>
/// the "ln" operator.
/// </summary>
internal sealed class Ln : Operator
{
public void Execute(ExecutionContext context)
{
var num = context.PopNumber();
context.Stack.Push(Math.Log(Convert.ToDouble(num)));
}
}
/// <summary>
/// the "log" operator.
/// </summary>
internal sealed class Log : Operator
{
public void Execute(ExecutionContext context)
{
var num = context.PopNumber();
context.Stack.Push(Math.Log10(Convert.ToDouble(num)));
}
}
/// <summary>
/// the "mod" operator.
/// </summary>
internal sealed class Mod : Operator
{
public void Execute(ExecutionContext context)
{
int int2 = context.PopInt();
int int1 = context.PopInt();
context.Stack.Push(int1 % int2);
}
}
/// <summary>
/// the "mul" operator.
/// </summary>
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);
}
}
}
/// <summary>
/// the "neg" operator.
/// </summary>
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));
}
}
}
/// <summary>
/// the "round" operator.
/// </summary>
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);
}
}
}
/// <summary>
/// the "sin" operator.
/// </summary>
internal sealed class Sin : Operator
{
public void Execute(ExecutionContext context)
{
double angle = context.PopReal();
double sin = Math.Sin(ToRadians(angle));
context.Stack.Push(sin);
}
}
/// <summary>
/// the "sqrt" operator.
/// </summary>
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));
}
}
/// <summary>
/// the "sub" operator.
/// </summary>
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);
}
}
}
/// <summary>
/// the "truncate" operator.
/// </summary>
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)));
}
}
}
}
}

View File

@ -0,0 +1,160 @@
namespace UglyToad.PdfPig.Functions.Type4
{
using System;
using System.Collections.Generic;
internal sealed class BitwiseOperators
{
private BitwiseOperators()
{
// Private constructor.
}
/// <summary>
/// Abstract base class for logical operators.
/// </summary>
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);
}
/// <summary>
/// Implements the "and" operator.
/// </summary>
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;
}
}
/// <summary>
/// Implements the "bitshift" operator.
/// </summary>
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);
}
}
}
/// <summary>
/// Implements the "false" operator.
/// </summary>
internal sealed class False : Operator
{
public void Execute(ExecutionContext context)
{
context.Stack.Push(false);
}
}
/// <summary>
/// Implements the "not" operator.
/// </summary>
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");
}
}
}
/// <summary>
/// Implements the "or" operator.
/// </summary>
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;
}
}
/// <summary>
/// Implements the "true" operator.
/// </summary>
internal sealed class True : Operator
{
public void Execute(ExecutionContext context)
{
context.Stack.Push(true);
}
}
/// <summary>
/// Implements the "xor" operator.
/// </summary>
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;
}
}
}
}

View File

@ -0,0 +1,53 @@
namespace UglyToad.PdfPig.Functions.Type4
{
using System;
using System.Collections.Generic;
/// <summary>
/// Provides the conditional operators such as "if" and "ifelse".
/// </summary>
internal sealed class ConditionalOperators
{
private ConditionalOperators()
{
// Private constructor.
}
/// <summary>
/// Implements the "if" operator.
/// </summary>
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);
}
}
}
/// <summary>
/// Implements the "ifelse" operator.
/// </summary>
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);
}
}
}
}
}

View File

@ -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;
/// <summary>
/// The stack used by this execution context.
/// </summary>
public Stack<object> Stack { get; private set; } = new Stack<object>();
/// <summary>
/// Creates a new execution context.
/// </summary>
/// <param name="operatorSet">the operator set</param>
public ExecutionContext(Operators operatorSet)
{
this.operators = operatorSet;
}
internal void AddAllToStack(IEnumerable<object> values)
{
var valuesList = values.ToList();
valuesList.AddRange(Stack);
valuesList.Reverse();
this.Stack = new Stack<object>(valuesList);
}
/// <summary>
/// Returns the operator set used by this execution context.
/// </summary>
/// <returns>the operator set</returns>
public Operators GetOperators()
{
return this.operators;
}
/// <summary>
/// Pops a number (int or real) from the stack. If it's neither data type, a <see cref="InvalidCastException"/> is thrown.
/// </summary>
/// <returns>the number</returns>
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.");
}
/// <summary>
/// Pops a value of type int from the stack. If the value is not of type int, a <see cref="InvalidCastException"/> is thrown.
/// </summary>
/// <returns>the int value</returns>
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");
}
/// <summary>
/// Pops a number from the stack and returns it as a real value. If the value is not of a numeric type,
/// a <see cref="InvalidCastException"/> is thrown.
/// </summary>
/// <returns>the real value</returns>
public double PopReal()
{
return Convert.ToDouble(Stack.Pop());
}
}
}

View File

@ -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<object> instructions = new List<object>();
/// <summary>
/// Add a name (ex. an operator)
/// </summary>
/// <param name="name">the name</param>
public void AddName(string name)
{
this.instructions.Add(name);
}
/// <summary>
/// Adds an int value.
/// </summary>
/// <param name="value">the value</param>
public void AddInteger(int value)
{
this.instructions.Add(value);
}
/// <summary>
/// Adds a real value.
/// </summary>
/// <param name="value">the value</param>
public void AddReal(double value)
{
this.instructions.Add(value);
}
/// <summary>
/// Adds a bool value.
/// </summary>
/// <param name="value">the value</param>
public void AddBoolean(bool value)
{
this.instructions.Add(value);
}
/// <summary>
/// Adds a proc (sub-sequence of instructions).
/// </summary>
/// <param name="child">the child proc</param>
public void AddProc(InstructionSequence child)
{
this.instructions.Add(child);
}
/// <summary>
/// Executes the instruction sequence.
/// </summary>
/// <param name="context">the execution context</param>
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);
}
}
}
}

View File

@ -0,0 +1,83 @@
namespace UglyToad.PdfPig.Functions.Type4
{
using System.Collections.Generic;
/// <summary>
/// Basic parser for Type 4 functions which is used to build up instruction sequences.
/// </summary>
internal sealed class InstructionSequenceBuilder : Parser.AbstractSyntaxHandler
{
private readonly InstructionSequence mainSequence = new InstructionSequence();
private readonly Stack<InstructionSequence> seqStack = new Stack<InstructionSequence>();
private InstructionSequenceBuilder()
{
this.seqStack.Push(this.mainSequence);
}
/// <summary>
/// Returns the instruction sequence that has been build from the syntactic elements.
/// </summary>
/// <returns>the instruction sequence</returns>
public InstructionSequence GetInstructionSequence()
{
return this.mainSequence;
}
/// <summary>
/// Parses the given text into an instruction sequence representing a Type 4 function that can be executed.
/// </summary>
/// <param name="text">the Type 4 function text</param>
/// <returns>the instruction sequence</returns>
public static InstructionSequence Parse(string text)
{
InstructionSequenceBuilder builder = new InstructionSequenceBuilder();
Parser.Parse(text, builder);
return builder.GetInstructionSequence();
}
private InstructionSequence GetCurrentSequence()
{
return this.seqStack.Peek();
}
/// <inheritdoc/>
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);
}
}
}
}

View File

@ -0,0 +1,14 @@
namespace UglyToad.PdfPig.Functions.Type4
{
/// <summary>
/// Interface for PostScript operators.e
/// </summary>
internal interface Operator
{
/// <summary>
/// Executes the operator. The method can inspect and manipulate the stack.
/// </summary>
/// <param name="context">the execution context</param>
void Execute(ExecutionContext context);
}
}

View File

@ -0,0 +1,124 @@
namespace UglyToad.PdfPig.Functions.Type4
{
using System.Collections.Generic;
/// <summary>
/// This class provides all the supported operators.
/// </summary>
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<string, Operator> operators = new Dictionary<string, Operator>();
/// <summary>
/// Creates a new Operators object with the default set of operators.
/// </summary>
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);
}
/// <summary>
/// Returns the operator for the given operator name.
/// </summary>
/// <param name="operatorName">the operator name</param>
/// <returns>the operator (or null if there's no such operator</returns>
public Operator GetOperator(string operatorName)
{
return this.operators[operatorName];
}
}
}

View File

@ -0,0 +1,310 @@
namespace UglyToad.PdfPig.Functions.Type4
{
using System.Text;
/// <summary>
/// Parser for PDF Type 4 functions. This implements a small subset of the PostScript
/// language but is no full PostScript interpreter.
/// </summary>
internal sealed class Parser
{
/// <summary>
/// Used to indicate the parsers current state.
/// </summary>
internal enum State
{
NEWLINE, WHITESPACE, COMMENT, TOKEN
}
private Parser()
{
//nop
}
/// <summary>
/// Parses a Type 4 function and sends the syntactic elements to the given syntax handler.
/// </summary>
/// <param name="input">the text source</param>
/// <param name="handler">the syntax handler</param>
public static void Parse(string input, SyntaxHandler handler)
{
Tokenizer tokenizer = new Tokenizer(input, handler);
tokenizer.Tokenize();
}
/// <summary>
/// This interface defines all possible syntactic elements of a Type 4 function.
/// It is called by the parser as the function is interpreted.
/// </summary>
public interface SyntaxHandler
{
/// <summary>
/// Indicates that a new line starts.
/// </summary>
/// <param name="text">the new line character (CR, LF, CR/LF or FF)</param>
void NewLine(string text);
/// <summary>
/// Called when whitespace characters are encountered.
/// </summary>
/// <param name="text">the whitespace text</param>
void Whitespace(string text);
/// <summary>
/// Called when a token is encountered. No distinction between operators and values is done here.
/// </summary>
/// <param name="text">the token text</param>
void Token(string text);
/// <summary>
/// Called for a comment.
/// </summary>
/// <param name="text">the comment</param>
void Comment(string text);
}
/// <summary>
/// Abstract base class for a <see cref="SyntaxHandler"/>.
/// </summary>
public abstract class AbstractSyntaxHandler : SyntaxHandler
{
/// <inheritdoc/>
public void Comment(string text)
{
//nop
}
/// <inheritdoc/>
public void NewLine(string text)
{
//nop
}
/// <inheritdoc/>
public void Whitespace(string text)
{
//nop
}
/// <inheritdoc/>
public abstract void Token(string text);
}
/// <summary>
/// Tokenizer for Type 4 functions.
/// </summary>
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());
}
}
}
}

View File

@ -0,0 +1,118 @@
namespace UglyToad.PdfPig.Functions.Type4
{
using System;
using System.Collections.Generic;
/// <summary>
/// Provides the relational operators such as "eq" and "le".
/// </summary>
internal sealed class RelationalOperators
{
private RelationalOperators()
{
// Private constructor.
}
/// <summary>
/// Implements the "eq" operator.
/// </summary>
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;
}
}
/// <summary>
/// Abstract base class for number comparison operators.
/// </summary>
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);
}
/// <summary>
/// Implements the "ge" operator.
/// </summary>
internal sealed class Ge : AbstractNumberComparisonOperator
{
protected override bool Compare(double num1, double num2)
{
return num1 >= num2;
}
}
/// <summary>
/// Implements the "gt" operator.
/// </summary>
internal sealed class Gt : AbstractNumberComparisonOperator
{
protected override bool Compare(double num1, double num2)
{
return num1 > num2;
}
}
/// <summary>
/// Implements the "le" operator.
/// </summary>
internal sealed class Le : AbstractNumberComparisonOperator
{
protected override bool Compare(double num1, double num2)
{
return num1 <= num2;
}
}
/// <summary>
/// Implements the "lt" operator.
/// </summary>
internal sealed class Lt : AbstractNumberComparisonOperator
{
protected override bool Compare(double num1, double num2)
{
return num1 < num2;
}
}
/// <summary>
/// Implements the "ne" operator.
/// </summary>
internal sealed class Ne : Eq
{
protected override bool IsEqual(object op1, object op2)
{
bool result = base.IsEqual(op1, op2);
return !result;
}
}
}
}

View File

@ -0,0 +1,142 @@
namespace UglyToad.PdfPig.Functions.Type4
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Provides the stack operators such as "Pop" and "dup".
/// </summary>
internal sealed class StackOperators
{
private StackOperators()
{
// Private constructor.
}
/// <summary>
/// Implements the "copy" operator.
/// </summary>
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<object> copy = context.Stack.ToList().GetRange(size - n - 1, n);
context.AddAllToStack(copy);
}
}
}
/// <summary>
/// Implements the "dup" operator.
/// </summary>
internal sealed class Dup : Operator
{
public void Execute(ExecutionContext context)
{
context.Stack.Push(context.Stack.Peek());
}
}
/// <summary>
/// Implements the "exch" operator.
/// </summary>
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);
}
}
/// <summary>
/// Implements the "index" operator.
/// </summary>
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));
}
}
/// <summary>
/// Implements the "Pop" operator.
/// </summary>
internal sealed class Pop : Operator
{
public void Execute(ExecutionContext context)
{
context.Stack.Pop();
}
}
/// <summary>
/// Implements the "roll" operator.
/// </summary>
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<object>();
var moved = new List<object>();
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);
}
}
}
}
}

View File

@ -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.
/// </summary>
public ColorSpaceDetails AlternateColorSpaceDetails { get; }
/// <summary>
/// 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 <see cref="AlternateColorSpaceDetails"/>.
/// </summary>
public Union<DictionaryToken, StreamToken> TintFunction { get; }
/// </summary>
public PdfFunction TintFunction { get; }
/// <summary>
/// Create a new <see cref="SeparationColorSpaceDetails"/>.
/// </summary>
public SeparationColorSpaceDetails(NameToken name,
ColorSpaceDetails alternateColorSpaceDetails,
Union<DictionaryToken, StreamToken> tintFunction)
ColorSpaceDetails alternateColorSpaceDetails,
PdfFunction tintFunction)
: base(ColorSpace.Separation)
{
Name = name;
AlternateColorSpaceDetails = alternateColorSpaceDetails;
AlternateColorSpaceDetails = alternateColorSpaceDetails;
TintFunction = tintFunction;
}
}

View File

@ -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<DictionaryToken, StreamToken> functionTokensUnion;
PdfFunction function;
var func = colorSpaceArray[3];
if (DirectObjectFinder.TryGet(func, scanner, out DictionaryToken functionDictionary))
{
functionTokensUnion = Union<DictionaryToken, StreamToken>.One(functionDictionary);
{
function = PdfFunctionParser.Create(functionDictionary, scanner, filterProvider);
}
else if (DirectObjectFinder.TryGet(func, scanner, out StreamToken functionStream))
{
functionTokensUnion = Union<DictionaryToken, StreamToken>.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;

View File

@ -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<PdfFunction>();
if (functionDictionary.TryGet<ArrayToken>(NameToken.Functions, scanner, out var functionsToken))
{
foreach (IToken token in functionsToken.Data)
{
if (DirectObjectFinder.TryGet<StreamToken>(token, scanner, out var strTk))
{
functions.Add(Create(strTk, scanner, filterProvider));
}
else if (DirectObjectFinder.TryGet<DictionaryToken>(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);
}
}
}
}