diff --git a/src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs b/src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs
index 2cb82d7d..8ac61e34 100644
--- a/src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs
@@ -1,12 +1,254 @@
namespace UglyToad.PdfPig.Tests.Integration
{
+ using System;
+ using System.IO;
using System.Linq;
using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor;
+ using UglyToad.PdfPig.Graphics.Colors;
using Xunit;
public class ColorSpaceTests
{
+ private const string OutputFolder = "ColorSpaceTests";
+
+ public ColorSpaceTests()
+ {
+ Directory.CreateDirectory(OutputFolder);
+ }
+
+ [Fact]
+ public void IndexedDeviceNColorSpaceImages()
+ {
+ var path = IntegrationHelpers.GetDocumentPath("MOZILLA-3136-0.pdf");
+
+ using (var document = PdfDocument.Open(path))
+ {
+ // page 1
+ var page1 = document.GetPage(1);
+ var images1 = page1.GetImages().ToArray();
+
+ // image 12
+ var image12 = images1[12];
+ Assert.Equal(ColorSpace.Indexed, image12.ColorSpaceDetails.Type);
+ Assert.Equal(ColorSpace.DeviceN, image12.ColorSpaceDetails.BaseType);
+ Assert.True(image12.TryGetPng(out byte[] bytes1_12)); // Cyan square
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-3136-0_1_12.png"), bytes1_12);
+
+ // image 13
+ var image13 = images1[13];
+ Assert.Equal(ColorSpace.Indexed, image13.ColorSpaceDetails.Type);
+ Assert.Equal(ColorSpace.DeviceN, image13.ColorSpaceDetails.BaseType);
+ Assert.True(image13.TryGetPng(out byte[] bytes1_13)); // Cyan square
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-3136-0_1_13.png"), bytes1_13);
+ }
+ }
+
+ [Fact]
+ public void DeviceNColorSpaceImages()
+ {
+ var path = IntegrationHelpers.GetDocumentPath("DeviceN_CS_test.pdf");
+
+ using (var document = PdfDocument.Open(path))
+ {
+ // page 3
+ var page3 = document.GetPage(3);
+ var images3 = page3.GetImages().ToArray();
+
+ var image3_0 = images3[0];
+ var deviceNCs = image3_0.ColorSpaceDetails as DeviceNColorSpaceDetails;
+ Assert.NotNull(deviceNCs);
+ Assert.True(deviceNCs.AlternateColorSpaceDetails is ICCBasedColorSpaceDetails);
+ Assert.True(image3_0.TryGetPng(out byte[] bytes3_0));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "DeviceN_CS_test_3_0.png"), bytes3_0);
+
+ var image3_2 = images3[2];
+ deviceNCs = image3_2.ColorSpaceDetails as DeviceNColorSpaceDetails;
+ Assert.NotNull(deviceNCs);
+ Assert.True(deviceNCs.AlternateColorSpaceDetails is ICCBasedColorSpaceDetails);
+ Assert.True(image3_2.TryGetPng(out byte[] bytes3_2));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "DeviceN_CS_test_3_2.png"), bytes3_2);
+
+ // page 6
+ var page6 = document.GetPage(6);
+ var images6 = page6.GetImages().ToArray();
+
+ var image6_0 = images6[0];
+ deviceNCs = image6_0.ColorSpaceDetails as DeviceNColorSpaceDetails;
+ Assert.NotNull(deviceNCs);
+ Assert.True(deviceNCs.AlternateColorSpaceDetails is ICCBasedColorSpaceDetails);
+ Assert.True(image6_0.TryGetPng(out byte[] bytes6_0));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "DeviceN_CS_test_6_0.png"), bytes6_0);
+
+ var image6_1 = images6[1];
+ deviceNCs = image6_0.ColorSpaceDetails as DeviceNColorSpaceDetails;
+ Assert.NotNull(deviceNCs);
+ Assert.True(deviceNCs.AlternateColorSpaceDetails is ICCBasedColorSpaceDetails);
+ Assert.True(image6_1.TryGetPng(out byte[] bytes6_1));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "DeviceN_CS_test_6_1.png"), bytes6_1);
+
+ var image6_2 = images6[2];
+ deviceNCs = image6_2.ColorSpaceDetails as DeviceNColorSpaceDetails;
+ Assert.NotNull(deviceNCs);
+ Assert.True(deviceNCs.AlternateColorSpaceDetails is ICCBasedColorSpaceDetails);
+ Assert.True(image6_2.TryGetPng(out byte[] bytes6_2));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "DeviceN_CS_test_6_2.png"), bytes6_2);
+ }
+ }
+
+ [Fact]
+ public void SeparationColorSpaceImages()
+ {
+ var path = IntegrationHelpers.GetDocumentPath("MOZILLA-7375-0.pdf");
+
+ using (var document = PdfDocument.Open(path))
+ {
+ var page1 = document.GetPage(1);
+ var images = page1.GetImages();
+ var image1page1 = images.ElementAt(0);
+ var separationCs = image1page1.ColorSpaceDetails as SeparationColorSpaceDetails;
+ Assert.NotNull(separationCs);
+ Assert.True(separationCs.AlternateColorSpaceDetails is DeviceCmykColorSpaceDetails);
+
+ foreach (var image in images)
+ {
+ if (image.TryGetPng(out byte[] bytes))
+ {
+ // Can't check actual image processing yet because encoded not supported
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void IndexedCalRgbColorSpaceImages()
+ {
+ var path = IntegrationHelpers.GetDocumentPath("MOZILLA-10084-0.pdf");
+
+ using (var document = PdfDocument.Open(path))
+ {
+ var page1 = document.GetPage(1);
+ var images1 = page1.GetImages().ToArray();
+
+ var image0 = images1[0];
+ Assert.Equal(ColorSpace.Indexed, image0.ColorSpaceDetails.Type);
+
+ var indexedCs = image0.ColorSpaceDetails as IndexedColorSpaceDetails;
+ Assert.NotNull(indexedCs);
+ Assert.Equal(ColorSpace.CalRGB, indexedCs.BaseColorSpaceDetails.Type);
+ Assert.True(image0.TryGetPng(out byte[] bytes0));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10084-0_1_0.png"), bytes0);
+
+ var image1 = images1[1];
+ Assert.Equal(ColorSpace.Indexed, image1.ColorSpaceDetails.Type);
+ indexedCs = image1.ColorSpaceDetails as IndexedColorSpaceDetails;
+ Assert.NotNull(indexedCs);
+ Assert.Equal(ColorSpace.CalRGB, indexedCs.BaseColorSpaceDetails.Type);
+ Assert.True(image1.TryGetPng(out byte[] bytes1));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10084-0_1_1.png"), bytes1);
+ }
+ }
+
+ [Fact]
+ public void StencilIndexedIccColorSpaceImages()
+ {
+ var path = IntegrationHelpers.GetDocumentPath("MOZILLA-10225-0.pdf");
+
+ using (var document = PdfDocument.Open(path))
+ {
+ // page 1
+ var page1 = document.GetPage(2);
+ var images1 = page1.GetImages().ToArray();
+
+ var image0 = images1[0];
+ Assert.Equal(ColorSpace.Indexed, image0.ColorSpaceDetails.Type); // Icc
+ Assert.True(image0.TryGetPng(out byte[] bytes0));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10225-0_1_0.png"), bytes0);
+
+ var image1 = images1[1];
+ Assert.Equal(ColorSpace.Indexed, image1.ColorSpaceDetails.Type); // stencil
+ Assert.True(image1.TryGetPng(out byte[] bytes1));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10225-0_1_1.png"), bytes1);
+
+ // page 23
+ var page23 = document.GetPage(23);
+ var images23 = page23.GetImages().ToArray();
+
+ var image23_0 = images23[0];
+ Assert.Equal(ColorSpace.Indexed, image23_0.ColorSpaceDetails.Type);
+ Assert.True(image23_0.TryGetPng(out byte[] bytes23_0));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10225-0_23_0.png"), bytes23_0);
+
+ // page 332
+ var page332 = document.GetPage(332);
+ var images332 = page332.GetImages().ToArray();
+
+ var image332_0 = images332[0];
+ Assert.Equal(ColorSpace.ICCBased, image332_0.ColorSpaceDetails.Type);
+ Assert.True(image332_0.TryGetPng(out byte[] bytes332_0));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10225-0_332_0.png"), bytes332_0);
+
+ // page 338
+ var page338 = document.GetPage(338);
+ var images338 = page338.GetImages().ToArray();
+
+ var image338_1 = images338[1];
+ Assert.Equal(ColorSpace.Indexed, image338_1.ColorSpaceDetails.Type);
+ Assert.True(image338_1.TryGetPng(out byte[] bytes338_1));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10225-0_338_1.png"), bytes338_1);
+
+ // page 339
+ var page339 = document.GetPage(339);
+ var images339 = page339.GetImages().ToArray();
+
+ var image339_0 = images339[0];
+ Assert.Equal(ColorSpace.Indexed, image339_0.ColorSpaceDetails.Type);
+ Assert.True(image339_0.TryGetPng(out byte[] bytes339_0));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10225-0_339_0.png"), bytes339_0);
+
+ var image339_1 = images339[1];
+ Assert.Equal(ColorSpace.Indexed, image339_1.ColorSpaceDetails.Type);
+ Assert.True(image339_1.TryGetPng(out byte[] bytes339_1));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10225-0_339_1.png"), bytes339_1);
+
+ // page 341
+ var page341 = document.GetPage(341);
+ var images341 = page341.GetImages().ToArray();
+
+ var image341_0 = images341[0];
+ Assert.Equal(ColorSpace.Indexed, image341_0.ColorSpaceDetails.Type);
+ Assert.True(image341_0.TryGetPng(out byte[] bytes341_0));
+ File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-10225-0_341_0.png"), bytes341_0);
+ }
+ }
+
+ [Fact]
+ public void SeparationLabColorSpace()
+ {
+ // Test with TIKA_1552_0.pdf
+ // https://icolorpalette.com/color/pantone-289-c
+ // Pantone 289 C Color | #0C2340
+ // Rgb : rgb(12,35,64)
+ // CIE L*a*b* : 13.53, 2.89, -21.08
+
+ var path = IntegrationHelpers.GetDocumentPath("TIKA-1552-0.pdf");
+
+ using (var document = PdfDocument.Open(path))
+ {
+ var page1 = document.GetPage(1);
+
+ var background = page1.ExperimentalAccess.Paths[0];
+ Assert.True(background.IsFilled);
+
+ var (r, g, b) = background.FillColor.ToRGBValues();
+
+ // Colors picked from Acrobat reader: rgb(11, 34, 64)
+ Assert.Equal(10, ConvertToByte(r)); // Should be 11, but close enough
+ Assert.Equal(34, ConvertToByte(g));
+ Assert.Equal(64, ConvertToByte(b));
+ }
+ }
+
[Fact]
public void CanGetAllPagesImages()
{
@@ -17,12 +259,11 @@
for (int p = 0; p < document.NumberOfPages; p++)
{
var page = document.GetPage(p + 1);
- var images = page.GetImages().ToArray();
- foreach (var image in images)
+ foreach (var image in page.GetImages())
{
if (image.TryGetPng(out var png))
{
-
+ // TODO
}
}
}
@@ -65,7 +306,7 @@
for (int r = 0; r < filledColors.Length; r++)
{
var color = filledColors[r];
- Assert.Equal(PdfPig.Graphics.Colors.ColorSpace.DeviceRGB, color.ColorSpace);
+ Assert.Equal(ColorSpace.DeviceRGB, color.ColorSpace);
if (r % 2 == 0)
{
@@ -85,5 +326,11 @@
}
}
}
+
+ private static byte ConvertToByte(decimal componentValue)
+ {
+ var rounded = Math.Round(componentValue * 255, MidpointRounding.AwayFromZero);
+ return (byte)rounded;
+ }
}
}
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/DeviceN_CS_test.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/DeviceN_CS_test.pdf
new file mode 100644
index 00000000..93f1081b
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/DeviceN_CS_test.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-10084-0.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-10084-0.pdf
new file mode 100644
index 00000000..5d1fb3ad
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-10084-0.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-10225-0.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-10225-0.pdf
new file mode 100644
index 00000000..e19a699e
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-10225-0.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-3136-0.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-3136-0.pdf
new file mode 100644
index 00000000..fd25d049
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-3136-0.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-7375-0.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-7375-0.pdf
new file mode 100644
index 00000000..24a0bcc1
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/MOZILLA-7375-0.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/TIKA-1552-0.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/TIKA-1552-0.pdf
new file mode 100644
index 00000000..1e1e845f
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/TIKA-1552-0.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
index ac26fba3..53583a52 100644
--- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
+++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
@@ -122,8 +122,10 @@
"UglyToad.PdfPig.Graphics.Colors.DeviceGrayColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.DeviceRgbColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.DeviceCmykColorSpaceDetails",
+ "UglyToad.PdfPig.Graphics.Colors.DeviceNColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.ICCBasedColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.IndexedColorSpaceDetails",
+ "UglyToad.PdfPig.Graphics.Colors.LabColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.SeparationColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.UnsupportedColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Core.LineCapStyle",
diff --git a/src/UglyToad.PdfPig.Tokens/NameToken.Constants.cs b/src/UglyToad.PdfPig.Tokens/NameToken.Constants.cs
index ec8331fa..0f2c500b 100644
--- a/src/UglyToad.PdfPig.Tokens/NameToken.Constants.cs
+++ b/src/UglyToad.PdfPig.Tokens/NameToken.Constants.cs
@@ -349,6 +349,7 @@
public static readonly NameToken Metadata = new NameToken("Metadata");
public static readonly NameToken MissingWidth = new NameToken("MissingWidth");
public static readonly NameToken Mix = new NameToken("Mix");
+ public static readonly NameToken MixingHints = new NameToken("MixingHints");
public static readonly NameToken Mk = new NameToken("MK");
public static readonly NameToken Ml = new NameToken("ML");
public static readonly NameToken MmType1 = new NameToken("MMType1");
diff --git a/src/UglyToad.PdfPig/Graphics/Colors/CIEBasedColorSpaceTransformer.cs b/src/UglyToad.PdfPig/Graphics/Colors/CIEBasedColorSpaceTransformer.cs
index 5f68c32f..bc7b392b 100644
--- a/src/UglyToad.PdfPig/Graphics/Colors/CIEBasedColorSpaceTransformer.cs
+++ b/src/UglyToad.PdfPig/Graphics/Colors/CIEBasedColorSpaceTransformer.cs
@@ -5,10 +5,11 @@
///
/// Transformer for CIEBased color spaces.
- ///
+ ///
/// In addition to the PDF spec itself, the transformation implementation is based on the descriptions in:
/// https://en.wikipedia.org/wiki/SRGB#The_forward_transformation_(CIE_XYZ_to_sRGB) and
/// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
+ ///
///
internal class CIEBasedColorSpaceTransformer
{
@@ -75,7 +76,7 @@
/// Transforms the supplied ABC color to the RGB color of the
/// that was supplied to this as the destination
/// workspace.
- /// A, B and C represent red, green and blue calibrated color values in the range 0 to 1.
+ /// A, B and C represent red, green and blue calibrated color values in the range 0 to 1.
///
public (double R, double G, double B) TransformToRGB((double A, double B, double C) color)
{
diff --git a/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs b/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs
index 9fd9305a..9a4600eb 100644
--- a/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs
+++ b/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs
@@ -29,7 +29,12 @@
/// The underlying type of ColorSpace, usually equal to
/// unless .
///
- public ColorSpace BaseType { get; protected set; }
+ public ColorSpace BaseType { get; protected set; }
+
+ ///
+ /// The number of components for the underlying color space.
+ ///
+ public abstract int BaseNumberOfColorComponents { get; }
///
/// Create a new .
@@ -45,15 +50,25 @@
///
public abstract IColor GetColor(params double[] values);
+ ///
+ /// Get the color, without check and caching.
+ ///
+ internal abstract double[] Process(params double[] values);
+
///
/// Get the color that initialize the current stroking or nonstroking colour.
///
public abstract IColor GetInitializeColor();
+ ///
+ /// Transform image bytes.
+ ///
+ internal abstract IReadOnlyList Transform(IReadOnlyList decoded);
+
///
/// Convert to byte.
///
- protected static byte ConvertToByte(decimal componentValue)
+ protected static byte ConvertToByte(double componentValue)
{
var rounded = Math.Round(componentValue * 255, MidpointRounding.AwayFromZero);
return (byte)rounded;
@@ -73,10 +88,19 @@
///
public override int NumberOfColorComponents => 1;
+
+ ///
+ public override int BaseNumberOfColorComponents => NumberOfColorComponents;
private DeviceGrayColorSpaceDetails() : base(ColorSpace.DeviceGray)
{ }
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ return values;
+ }
+
///
public override IColor GetColor(params double[] values)
{
@@ -105,6 +129,12 @@
{
return GrayColor.Black;
}
+
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
+ {
+ return decoded;
+ }
}
///
@@ -120,10 +150,19 @@
///
public override int NumberOfColorComponents => 3;
+
+ ///
+ public override int BaseNumberOfColorComponents => NumberOfColorComponents;
private DeviceRgbColorSpaceDetails() : base(ColorSpace.DeviceRGB)
{ }
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ return values;
+ }
+
///
public override IColor GetColor(params double[] values)
{
@@ -151,6 +190,12 @@
public override IColor GetInitializeColor()
{
return RGBColor.Black;
+ }
+
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
+ {
+ return decoded;
}
}
@@ -166,11 +211,20 @@
///
public override int NumberOfColorComponents => 4;
+
+ ///
+ public override int BaseNumberOfColorComponents => NumberOfColorComponents;
private DeviceCmykColorSpaceDetails() : base(ColorSpace.DeviceCMYK)
{
}
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ return values;
+ }
+
///
public override IColor GetColor(params double[] values)
{
@@ -199,6 +253,12 @@
public override IColor GetInitializeColor()
{
return CMYKColor.Black;
+ }
+
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
+ {
+ return decoded;
}
}
@@ -206,12 +266,12 @@
/// An Indexed color space allows a PDF content stream to use small integers as indices into a color map or color table of arbitrary colors in some other space.
/// A PDF consumer treats each sample value as an index into the color table and uses the color value it finds there.
///
- public class IndexedColorSpaceDetails : ColorSpaceDetails
+ public sealed class IndexedColorSpaceDetails : ColorSpaceDetails
{
private readonly ConcurrentDictionary cache = new ConcurrentDictionary();
///
- /// Creates a indexed color space useful for exracting stencil masks as black-and-white images,
+ /// Creates a indexed color space useful for extracting stencil masks as black-and-white images,
/// i.e. with a color palette of two colors (black and white). If the decode parameter array is
/// [0, 1] it indicates that black is at index 0 in the color palette, whereas [1, 0] indicates
/// that the black color is at index 1.
@@ -224,6 +284,12 @@
///
public override int NumberOfColorComponents => 1;
+
+ ///
+ ///
+ /// In the case of , gets the ' BaseNumberOfColorComponents.
+ ///
+ public override int BaseNumberOfColorComponents => BaseColorSpaceDetails.BaseNumberOfColorComponents;
///
/// The base color space in which the values in the color table are to be interpreted.
@@ -254,6 +320,13 @@
BaseType = baseColorSpaceDetails.BaseType;
}
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ var csBytes = UnwrapIndexedColorSpaceBytes(new[] { (byte)values[0] });
+ return BaseColorSpaceDetails.Process(csBytes.Select(b => b / 255.0).ToArray());
+ }
+
///
public override IColor GetColor(params double[] values)
{
@@ -274,9 +347,10 @@
var multiplier = 1;
Func> transformer = null;
switch (BaseType)
- {
+ {
case ColorSpace.DeviceRGB:
- case ColorSpace.CalRGB:
+ case ColorSpace.CalRGB:
+ case ColorSpace.Lab:
transformer = x =>
{
var r = new byte[3];
@@ -288,7 +362,8 @@
return r;
};
multiplier = 3;
- break;
+ break;
+
case ColorSpace.DeviceCMYK:
transformer = x =>
{
@@ -302,11 +377,28 @@
};
multiplier = 4;
- break;
+ break;
+
case ColorSpace.DeviceGray:
- case ColorSpace.CalGray:
+ case ColorSpace.CalGray:
+ case ColorSpace.Separation:
transformer = x => new[] { ColorTable[x] };
multiplier = 1;
+ break;
+
+ case ColorSpace.DeviceN:
+ transformer = x =>
+ {
+ var r = new byte[BaseColorSpaceDetails.NumberOfColorComponents];
+ for (var i = 0; i < BaseColorSpaceDetails.NumberOfColorComponents; i++)
+ {
+ r[i] = ColorTable[x * BaseColorSpaceDetails.NumberOfColorComponents + i];
+ }
+
+ return r;
+ };
+
+ multiplier = BaseColorSpaceDetails.NumberOfColorComponents;
break;
}
@@ -335,7 +427,186 @@
// initialize the corresponding current colour to 0.
return GetColor(0);
}
- }
+
+ ///
+ ///
+ ///
+ /// Unwrap then transform using base color space details.
+ ///
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
+ {
+ var unwraped = UnwrapIndexedColorSpaceBytes(decoded);
+ return BaseColorSpaceDetails.Transform(unwraped);
+ }
+ }
+
+ ///
+ /// DeviceN colour spaces may contain an arbitrary number of colour components. They provide greater flexibility than
+ /// is possible with standard device colour spaces such as DeviceCMYK or with individual Separation colour spaces.
+ ///
+ public sealed class DeviceNColorSpaceDetails : ColorSpaceDetails
+ {
+ ///
+ ///
+ /// The 'N' in DeviceN.
+ ///
+ public override int NumberOfColorComponents { get; }
+
+ ///
+ public override int BaseNumberOfColorComponents => AlternateColorSpaceDetails.NumberOfColorComponents;
+
+ ///
+ /// Specifies name objects specifying the individual colour components. The length of the array shall
+ /// determine the number of components in the DeviceN colour space.
+ ///
+ ///
+ /// The component names shall all be different from one another, except for the name None, which may be repeated.
+ ///
+ /// The special name All, used by Separation colour spaces, shall not be used.
+ ///
+ ///
+ public IReadOnlyList Names { get; }
+
+ ///
+ /// If the colorant name associated with a DeviceN color space does not correspond to a colorant available on the device,
+ /// the application arranges for subsequent painting operations to be performed in an alternate color space.
+ /// The intended colors can be approximated by colors in a device or CIE-based color space
+ /// which are then rendered with the usual primary or process colorants.
+ ///
+ public ColorSpaceDetails AlternateColorSpaceDetails { get; }
+
+ ///
+ /// The optional attributes parameter shall be a dictionary containing additional information about the components of
+ /// colour space that conforming readers may use. Conforming readers need not use the alternateSpace and tintTransform
+ /// parameters, and may instead use custom blending algorithms, along with other information provided in the attributes
+ /// dictionary if present.
+ ///
+ public DeviceNColorSpaceAttributes? Attributes { get; }
+
+ ///
+ /// During subsequent painting operations, an application calls this function to transform a tint value into
+ /// color component values in the alternate color space.
+ /// The function is called with the tint value and must return the corresponding color component values.
+ /// That is, the number of components and the interpretation of their values depend on the .
+ ///
+ public PdfFunction TintFunction { get; }
+
+ ///
+ /// Create a new .
+ ///
+ public DeviceNColorSpaceDetails(IReadOnlyList names, ColorSpaceDetails alternateColorSpaceDetails,
+ PdfFunction tintFunction, DeviceNColorSpaceAttributes? attributes = null)
+ : base(ColorSpace.DeviceN)
+ {
+ Names = names;
+ NumberOfColorComponents = Names.Count;
+ AlternateColorSpaceDetails = alternateColorSpaceDetails;
+ Attributes = attributes;
+ TintFunction = tintFunction;
+ }
+
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ var evaled = TintFunction.Eval(values[0]);
+ return AlternateColorSpaceDetails.Process(evaled);
+ }
+
+ ///
+ public override IColor GetColor(params double[] values)
+ {
+ if (values == null || values.Length != NumberOfColorComponents)
+ {
+ throw new ArgumentException($"Invalid number of imputs, expecting {NumberOfColorComponents} but got {values.Length}", nameof(values));
+ }
+
+ // TODO - use attributes
+
+ // TODO - caching
+ var evaled = TintFunction.Eval(values);
+ return AlternateColorSpaceDetails.GetColor(evaled);
+ }
+
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
+ {
+ var transformed = new List();
+ for (var i = 0; i < decoded.Count; i += NumberOfColorComponents)
+ {
+ double[] comps = new double[NumberOfColorComponents];
+ for (int n = 0; n < NumberOfColorComponents; n++)
+ {
+ comps[n] = decoded[i + n] / 255.0;
+ }
+
+ var colors = Process(comps);
+ for (int c = 0; c < colors.Length; c++)
+ {
+ transformed.Add(ConvertToByte(colors[c]));
+ }
+ }
+
+ return transformed;
+ }
+
+ ///
+ public override IColor GetInitializeColor()
+ {
+ // When this space is set to the current colour space (using the CS or cs operators), each component
+ // shall be given an initial value of 1.0. The SCN and scn operators respectively shall set the current
+ // stroking and nonstroking colour.
+ return GetColor(Enumerable.Repeat(1.0, NumberOfColorComponents).ToArray());
+ }
+
+ ///
+ /// DeviceN Color Space Attributes.
+ ///
+ public struct DeviceNColorSpaceAttributes
+ {
+ ///
+ /// A name specifying the preferred treatment for the colour space. Values shall be DeviceN or NChannel. Default value: DeviceN.
+ ///
+ public NameToken Subtype { get; }
+
+ ///
+ /// Colorants - dictionary - Required if Subtype is NChannel and the colour space includes spot colorants; otherwise optional.
+ ///
+ public DictionaryToken Colorants { get; }
+
+ ///
+ /// Process - dictionary - Required if Subtype is NChannel and the colour space includes components of a process colour space, otherwise optional.
+ ///
+ public DictionaryToken Process { get; }
+
+ ///
+ /// MixingHints - dictionary - Optional
+ ///
+ public DictionaryToken MixingHints { get; }
+
+ ///
+ /// TODO
+ ///
+ public DeviceNColorSpaceAttributes()
+ {
+ Subtype = NameToken.Devicen;
+ Colorants = null;
+ Process = null;
+ MixingHints = null;
+ }
+
+ ///
+ /// TODO
+ ///
+ public DeviceNColorSpaceAttributes(NameToken subtype, DictionaryToken colorants, DictionaryToken process, DictionaryToken mixingHints)
+ {
+ Subtype = subtype;
+ Colorants = colorants;
+ Process = process;
+ MixingHints = mixingHints;
+ }
+ }
+ }
///
/// A Separation color space provides a means for specifying the use of additional colorants or
@@ -343,12 +614,15 @@
/// When such a space is the current color space, the current color is a single-component value, called a tint,
/// that controls the application of the given colorant or color components only.
///
- public class SeparationColorSpaceDetails : ColorSpaceDetails
+ public sealed class SeparationColorSpaceDetails : ColorSpaceDetails
{
private readonly ConcurrentDictionary cache = new ConcurrentDictionary();
///
public override int NumberOfColorComponents => 1;
+
+ ///
+ public override int BaseNumberOfColorComponents => AlternateColorSpaceDetails.NumberOfColorComponents;
///
/// Specifies the name of the colorant that this Separation color space is intended to represent.
@@ -394,6 +668,13 @@
TintFunction = tintFunction;
}
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ var evaled = TintFunction.Eval(values[0]);
+ return AlternateColorSpaceDetails.Process(evaled);
+ }
+
///
public override IColor GetColor(params double[] values)
{
@@ -411,15 +692,17 @@
});
}
- internal IReadOnlyList TransformToRGB(IReadOnlyList values)
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList values)
{
var transformed = new List();
for (var i = 0; i < values.Count; i += 3)
{
- var (r, g, b) = GetColor(values[i++] / 255.0).ToRGBValues();
- transformed.Add(ConvertToByte(r));
- transformed.Add(ConvertToByte(g));
- transformed.Add(ConvertToByte(b));
+ var colors = Process(values[i++] / 255.0);
+ for (int c = 0; c < colors.Length; c++)
+ {
+ transformed.Add(ConvertToByte(colors[c]));
+ }
}
return transformed;
@@ -439,11 +722,14 @@
/// CalGray - A CIE A color space with a single transformation.
/// A represents the gray component of a calibrated gray space. The component must be in the range 0.0 to 1.0.
///
- public class CalGrayColorSpaceDetails : ColorSpaceDetails
+ public sealed class CalGrayColorSpaceDetails : ColorSpaceDetails
{
///
public override int NumberOfColorComponents => 1;
+ ///
+ public override int BaseNumberOfColorComponents => NumberOfColorComponents;
+
private readonly CIEBasedColorSpaceTransformer colorSpaceTransformer;
///
@@ -506,24 +792,32 @@
///
private RGBColor TransformToRGB(double colorA)
{
- var colorRgb = colorSpaceTransformer.TransformToRGB((colorA, colorA, colorA));
- return new RGBColor((decimal)colorRgb.R, (decimal)colorRgb.G, (decimal)colorRgb.B);
+ var (R, G, B) = colorSpaceTransformer.TransformToRGB((colorA, colorA, colorA));
+ return new RGBColor((decimal)R, (decimal)G, (decimal)B);
}
- internal IReadOnlyList TransformToRGB(IReadOnlyList decoded)
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
{
var transformed = new List();
for (var i = 0; i < decoded.Count; i++)
{
var component = decoded[i] / 255.0;
- var rgbPixel = TransformToRGB(component);
+ var rgbPixel = Process(component);
// We only need one component here
- transformed.Add(ConvertToByte(rgbPixel.R));
+ transformed.Add(ConvertToByte(rgbPixel[0]));
}
return transformed;
}
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ var (R, _, _) = colorSpaceTransformer.TransformToRGB((values[0], values[0], values[0]));
+ return new double[] { R };
+ }
+
///
public override IColor GetColor(params double[] values)
{
@@ -552,11 +846,14 @@
/// CalRGB - A CIE ABC color space with a single transformation.
/// A, B and C represent red, green and blue color values in the range 0.0 to 1.0.
///
- public class CalRGBColorSpaceDetails : ColorSpaceDetails
+ public sealed class CalRGBColorSpaceDetails : ColorSpaceDetails
{
///
public override int NumberOfColorComponents => 3;
+ ///
+ public override int BaseNumberOfColorComponents => NumberOfColorComponents;
+
private readonly CIEBasedColorSpaceTransformer colorSpaceTransformer;
///
@@ -636,24 +933,31 @@
///
private RGBColor TransformToRGB((double A, double B, double C) colorAbc)
{
- var colorRgb = colorSpaceTransformer.TransformToRGB((colorAbc.A, colorAbc.B, colorAbc.C));
- return new RGBColor((decimal)colorRgb.R, (decimal)colorRgb.G, (decimal)colorRgb.B);
+ var (R, G, B) = colorSpaceTransformer.TransformToRGB((colorAbc.A, colorAbc.B, colorAbc.C));
+ return new RGBColor((decimal)R, (decimal)G, (decimal)B);
}
- internal IReadOnlyList TransformToRGB(IReadOnlyList decoded)
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
{
var transformed = new List();
for (var i = 0; i < decoded.Count; i += 3)
{
- var rgbPixel = TransformToRGB((decoded[i] / 255.0, decoded[i + 1] / 255.0, decoded[i + 2] / 255.0));
- transformed.Add(ConvertToByte(rgbPixel.R));
- transformed.Add(ConvertToByte(rgbPixel.G));
- transformed.Add(ConvertToByte(rgbPixel.B));
+ var rgbPixel = Process(decoded[i] / 255.0, decoded[i + 1] / 255.0, decoded[i + 2] / 255.0);
+ transformed.Add(ConvertToByte(rgbPixel[0]));
+ transformed.Add(ConvertToByte(rgbPixel[1]));
+ transformed.Add(ConvertToByte(rgbPixel[2]));
}
return transformed;
- }
+ }
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ var (R, G, B) = colorSpaceTransformer.TransformToRGB((values[0], values[1], values[2]));
+ return new double[] { R, G, B };
+ }
///
public override IColor GetColor(params double[] values)
@@ -675,6 +979,149 @@
// be substituted.)
return TransformToRGB((0, 0, 0));
}
+ }
+
+ ///
+ /// CIE (Commission Internationale de l'Éclairage) colorspace.
+ /// Specifies color related to human visual perception with the aim of producing consistent color on different output devices.
+ /// CalRGB - A CIE ABC color space with a single transformation.
+ /// A, B and C represent red, green and blue color values in the range 0.0 to 1.0.
+ ///
+ public sealed class LabColorSpaceDetails : ColorSpaceDetails
+ {
+ private readonly CIEBasedColorSpaceTransformer colorSpaceTransformer;
+
+ ///
+ public override int NumberOfColorComponents => 3;
+
+ ///
+ public override int BaseNumberOfColorComponents => NumberOfColorComponents;
+
+ ///
+ /// An array of three numbers [XW YW ZW] specifying the tristimulus value, in the CIE 1931 XYZ space of the
+ /// diffuse white point. The numbers XW and ZW shall be positive, and YW shall be equal to 1.0.
+ ///
+ public IReadOnlyList WhitePoint { get; }
+
+ ///
+ /// An array of three numbers [XB YB ZB] specifying the tristimulus value, in the CIE 1931 XYZ space of the
+ /// diffuse black point. All three numbers must be non-negative. Default value: [0.0 0.0 0.0].
+ ///
+ public IReadOnlyList BlackPoint { get; }
+
+ ///
+ /// An array of four numbers [a_min a_max b_min b_max] that shall specify the range of valid values for the a* and b* (B and C)
+ /// components of the colour space — that is, a_min ≤ a* ≤ a_max and b_min ≤ b* ≤ b_max
+ /// Component values falling outside the specified range shall be adjusted to the nearest valid value without error indication.
+ /// Default value: [−100 100 −100 100].
+ ///
+ public IReadOnlyList Matrix { get; }
+
+ ///
+ /// Create a new .
+ ///
+ public LabColorSpaceDetails([NotNull] IReadOnlyList whitePoint, [CanBeNull] IReadOnlyList blackPoint, [CanBeNull] IReadOnlyList matrix)
+ : base(ColorSpace.Lab)
+ {
+ WhitePoint = whitePoint?.Select(v => (double)v).ToArray() ?? throw new ArgumentNullException(nameof(whitePoint));
+ if (WhitePoint.Count != 3)
+ {
+ throw new ArgumentOutOfRangeException(nameof(whitePoint), whitePoint, $"Must consist of exactly three numbers, but was passed {whitePoint.Count}.");
+ }
+
+ BlackPoint = blackPoint?.Select(v => (double)v).ToArray() ?? new[] { 0.0, 0.0, 0.0 };
+ if (BlackPoint.Count != 3)
+ {
+ throw new ArgumentOutOfRangeException(nameof(blackPoint), blackPoint, $"Must consist of exactly three numbers, but was passed {blackPoint.Count}.");
+ }
+
+ Matrix = matrix?.Select(v => (double)v).ToArray() ?? new[] { -100.0, 100.0, -100.0, 100.0 };
+ if (Matrix.Count != 4)
+ {
+ throw new ArgumentOutOfRangeException(nameof(matrix), matrix, $"Must consist of exactly four numbers, but was passed {matrix.Count}.");
+ }
+
+ colorSpaceTransformer = new CIEBasedColorSpaceTransformer((WhitePoint[0], WhitePoint[1], WhitePoint[2]), RGBWorkingSpace.sRGB);
+ }
+
+ ///
+ /// Transforms the supplied ABC color to RGB (sRGB) using the properties of this
+ /// in the transformation process.
+ /// A, B and C represent the L*, a*, and b* components of a CIE 1976 L*a*b* space. The range of the first (L*)
+ /// component shall be 0 to 100; the ranges of the second and third (a* and b*) components shall be defined by
+ /// the Range entry in the colour space dictionary
+ ///
+ private RGBColor TransformToRGB((double A, double B, double C) colorAbc)
+ {
+ var rgb = Process(colorAbc.A, colorAbc.B, colorAbc.C);
+ return new RGBColor((decimal)rgb[0], (decimal)rgb[1], (decimal)rgb[2]);
+ }
+
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
+ {
+ var transformed = new List();
+ for (var i = 0; i < decoded.Count; i += 3)
+ {
+ var rgbPixel = Process(decoded[i] / 255.0, decoded[i + 1] / 255.0, decoded[i + 2] / 255.0);
+ transformed.Add(ConvertToByte(rgbPixel[0]));
+ transformed.Add(ConvertToByte(rgbPixel[1]));
+ transformed.Add(ConvertToByte(rgbPixel[2]));
+ }
+
+ return transformed;
+ }
+
+ private static double g(double x)
+ {
+ if (x > 6.0 / 29.0)
+ {
+ return x * x * x;
+ }
+ return 108.0 / 841.0 * (x - 4.0 / 29.0);
+ }
+
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ // Component Ranges: L*: [0 100]; a* and b*: [−128 127]
+ double b = PdfFunction.ClipToRange(values[1], Matrix[0], Matrix[1]);
+ double c = PdfFunction.ClipToRange(values[2], Matrix[2], Matrix[3]);
+
+ double M = (values[0] + 16.0) / 116.0;
+ double L = M + (b / 500.0);
+ double N = M - (c / 200.0);
+
+ double X = WhitePoint[0] * g(L);
+ double Y = WhitePoint[1] * g(M);
+ double Z = WhitePoint[2] * g(N);
+
+ var (R, G, B) = colorSpaceTransformer.TransformToRGB((X, Y, Z));
+ return new double[] { R, G, B };
+ }
+
+ ///
+ public override IColor GetColor(params double[] values)
+ {
+ if (values == null || values.Length != NumberOfColorComponents)
+ {
+ throw new ArgumentException($"Invalid number of imputs, expecting {NumberOfColorComponents} but got {values.Length}", nameof(values));
+ }
+
+ return TransformToRGB((values[0], values[1], values[2]));
+ }
+
+ ///
+ public override IColor GetInitializeColor()
+ {
+ // Setting the current stroking or nonstroking colour space to any CIE-based colour space shall
+ // initialize all components of the corresponding current colour to 0.0 (unless the range of valid
+ // values for a given component does not include 0.0, in which case the nearest valid value shall
+ // be substituted.)
+ double b = PdfFunction.ClipToRange(0, Matrix[0], Matrix[1]);
+ double c = PdfFunction.ClipToRange(0, Matrix[2], Matrix[3]);
+ return TransformToRGB((0, b, c));
+ }
}
///
@@ -682,11 +1129,12 @@
/// enable a page description to specify color values in a way that is related to human visual perception.
/// The goal is for the same color specification to produce consistent results on different output devices,
/// within the limitations of each device.
- ///
+ ///
/// Currently support for this color space is limited in PdfPig. Calculations will only be based on
/// the color space of .
+ ///
///
- public class ICCBasedColorSpaceDetails : ColorSpaceDetails
+ public sealed class ICCBasedColorSpaceDetails : ColorSpaceDetails
{
///
/// The number of color components in the color space described by the ICC profile data.
@@ -695,6 +1143,9 @@
///
public override int NumberOfColorComponents { get; }
+ ///
+ public override int BaseNumberOfColorComponents => NumberOfColorComponents;
+
///
/// An alternate color space that can be used in case the one specified in the stream data is not
/// supported. Non-conforming readers may use this color space. The alternate color space may be any
@@ -753,6 +1204,14 @@
Metadata = metadata;
}
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ // TODO - use ICC profile
+
+ return AlternateColorSpaceDetails.Process(values);
+ }
+
///
public override IColor GetColor(params double[] values)
{
@@ -776,13 +1235,21 @@
double v = PdfFunction.ClipToRange(0.0, (double)Range[0], (double)Range[1]);
double[] init = Enumerable.Repeat(v, NumberOfColorComponents).ToArray();
return GetColor(init);
+ }
+
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
+ {
+ // TODO - use ICC profile
+
+ return AlternateColorSpaceDetails.Transform(decoded);
}
}
///
/// A ColorSpace which the PdfPig library does not currently support. Please raise a PR if you need support for this ColorSpace.
///
- public class UnsupportedColorSpaceDetails : ColorSpaceDetails
+ public sealed class UnsupportedColorSpaceDetails : ColorSpaceDetails
{
///
/// The single instance of the .
@@ -797,23 +1264,39 @@
///
public override int NumberOfColorComponents => throw new InvalidOperationException("UnsupportedColorSpaceDetails");
- //private readonly IColor debugColor = new RGBColor(255m / 255m, 20m / 255m, 147m / 255m);
+ ///
+ ///
+ ///
+ /// Cannot be called for , will throw a .
+ ///
+ ///
+ public override int BaseNumberOfColorComponents => NumberOfColorComponents;
private UnsupportedColorSpaceDetails() : base(ColorSpace.DeviceGray)
{
}
+ ///
+ internal override double[] Process(params double[] values)
+ {
+ throw new InvalidOperationException("UnsupportedColorSpaceDetails");
+ }
+
///
public override IColor GetColor(params double[] values)
{
- //return debugColor;
throw new InvalidOperationException("UnsupportedColorSpaceDetails");
}
///
public override IColor GetInitializeColor()
{
- //return debugColor;
+ throw new InvalidOperationException("UnsupportedColorSpaceDetails");
+ }
+
+ ///
+ internal override IReadOnlyList Transform(IReadOnlyList decoded)
+ {
throw new InvalidOperationException("UnsupportedColorSpaceDetails");
}
}
diff --git a/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs b/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
index f3f9f797..94ccab2d 100644
--- a/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
+++ b/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
@@ -513,7 +513,7 @@
{
if (csArrayToken.Data[0] is NameToken firstColorSpaceName)
{
- startState.ColorSpaceContext.SetNonStrokingColorspace(csNameToken, formGroupToken);
+ startState.ColorSpaceContext.SetNonStrokingColorspace(firstColorSpaceName, formGroupToken);
}
else
{
diff --git a/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs b/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs
index 790be574..51d6b5f6 100644
--- a/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs
+++ b/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs
@@ -42,32 +42,9 @@
if (strideWidth != imageWidth)
{
decoded = RemoveStridePadding(decoded.ToArray(), strideWidth, imageWidth, imageHeight, bytesPerPixel);
- }
-
- if (details is SeparationColorSpaceDetails separation)
- {
- decoded = separation.TransformToRGB(decoded);
}
- else
- {
- // In case of indexed color space images, unwrap indices to actual pixel component values
- if (details is IndexedColorSpaceDetails indexed)
- {
- decoded = indexed.UnwrapIndexedColorSpaceBytes(decoded);
- // Use the base color space in potential further decoding
- details = indexed.BaseColorSpaceDetails;
- }
-
- if (details is CalRGBColorSpaceDetails calRgb)
- {
- decoded = calRgb.TransformToRGB(decoded);
- }
- else if (details is CalGrayColorSpaceDetails calGray)
- {
- decoded = calGray.TransformToRGB(decoded);
- }
- }
+ decoded = details.Transform(decoded);
return decoded.ToArray();
}
diff --git a/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs b/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs
index cef51859..38572183 100644
--- a/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs
+++ b/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs
@@ -10,15 +10,9 @@
{
bytes = null;
- var hasValidDetails = image.ColorSpaceDetails != null &&
- !(image.ColorSpaceDetails is UnsupportedColorSpaceDetails);
-
- var actualColorSpace = image.ColorSpaceDetails.BaseType;
+ var hasValidDetails = image.ColorSpaceDetails != null && !(image.ColorSpaceDetails is UnsupportedColorSpaceDetails);
- var isColorSpaceSupported = hasValidDetails &&
- (actualColorSpace == ColorSpace.DeviceGray || actualColorSpace == ColorSpace.DeviceRGB
- || actualColorSpace == ColorSpace.DeviceCMYK || actualColorSpace == ColorSpace.CalGray
- || actualColorSpace == ColorSpace.CalRGB);
+ var isColorSpaceSupported = hasValidDetails && image.ColorSpaceDetails.BaseType != ColorSpace.Pattern;
if (!isColorSpaceSupported || !image.TryGetBytes(out var bytesPure))
{
@@ -30,10 +24,7 @@
bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure,
image.BitsPerComponent, image.WidthInSamples, image.HeightInSamples);
- var numberOfComponents =
- actualColorSpace == ColorSpace.DeviceCMYK ? 4 :
- actualColorSpace == ColorSpace.DeviceRGB ? 3 :
- actualColorSpace == ColorSpace.CalRGB ? 3 : 1;
+ var numberOfComponents = image.ColorSpaceDetails.BaseNumberOfColorComponents;
var is3Byte = numberOfComponents == 3;
@@ -55,42 +46,56 @@
if (!isCorrectlySized)
{
return false;
- }
-
- var i = 0;
- for (var col = 0; col < image.HeightInSamples; col++)
- {
- for (var row = 0; row < image.WidthInSamples; row++)
- {
- if (actualColorSpace == ColorSpace.DeviceCMYK)
- {
- /*
- * Where CMYK in 0..1
- * R = 255 × (1-C) × (1-K)
- * G = 255 × (1-M) × (1-K)
- * B = 255 × (1-Y) × (1-K)
- */
-
- var c = (bytesPure[i++]/255d);
- var m = (bytesPure[i++]/255d);
- var y = (bytesPure[i++]/255d);
- var k = (bytesPure[i++]/255d);
- var r = (byte)(255 * (1 - c) * (1 - k));
- var g = (byte)(255 * (1 - m) * (1 - k));
- var b = (byte)(255 * (1 - y) * (1 - k));
-
- builder.SetPixel(r, g, b, row, col);
- }
- else if (is3Byte)
- {
- builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], row, col);
- }
- else
- {
- var pixel = bytesPure[i++];
- builder.SetPixel(pixel, pixel, pixel, row, col);
- }
- }
+ }
+
+ if (image.ColorSpaceDetails.BaseType == ColorSpace.DeviceCMYK || numberOfComponents == 4)
+ {
+ int i = 0;
+ for (int col = 0; col < image.HeightInSamples; col++)
+ {
+ for (int row = 0; row < image.WidthInSamples; row++)
+ {
+ /*
+ * Where CMYK in 0..1
+ * R = 255 × (1-C) × (1-K)
+ * G = 255 × (1-M) × (1-K)
+ * B = 255 × (1-Y) × (1-K)
+ */
+
+ double c = (bytesPure[i++] / 255d);
+ double m = (bytesPure[i++] / 255d);
+ double y = (bytesPure[i++] / 255d);
+ double k = (bytesPure[i++] / 255d);
+ var r = (byte)(255 * (1 - c) * (1 - k));
+ var g = (byte)(255 * (1 - m) * (1 - k));
+ var b = (byte)(255 * (1 - y) * (1 - k));
+
+ builder.SetPixel(r, g, b, row, col);
+ }
+ }
+ }
+ else if (is3Byte)
+ {
+ int i = 0;
+ for (int col = 0; col < image.HeightInSamples; col++)
+ {
+ for (int row = 0; row < image.WidthInSamples; row++)
+ {
+ builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], row, col);
+ }
+ }
+ }
+ else
+ {
+ int i = 0;
+ for (int col = 0; col < image.HeightInSamples; col++)
+ {
+ for (int row = 0; row < image.WidthInSamples; row++)
+ {
+ byte pixel = bytesPure[i++];
+ builder.SetPixel(pixel, pixel, pixel, row, col);
+ }
+ }
}
bytes = builder.Save();
diff --git a/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs b/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs
index f8e3ce20..981f5357 100644
--- a/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs
+++ b/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs
@@ -48,7 +48,8 @@
{
if (cannotRecurse)
{
- return UnsupportedColorSpaceDetails.Instance;
+ // Not sure if always Gray, it's just a single color.
+ return DeviceGrayColorSpaceDetails.Instance;
}
var colorSpaceDetails = GetColorSpaceDetails(colorSpace, imageDictionary.Without(NameToken.Filter).Without(NameToken.F), scanner, resourceStore, filterProvider, true);
@@ -166,8 +167,49 @@
return new CalRGBColorSpaceDetails(whitePoint, blackPoint, gamma, matrix);
}
- case ColorSpace.Lab:
- return UnsupportedColorSpaceDetails.Instance;
+ case ColorSpace.Lab:
+ {
+ if (!TryGetColorSpaceArray(imageDictionary, resourceStore, scanner, out var colorSpaceArray)
+ || colorSpaceArray.Length != 2)
+ {
+ return UnsupportedColorSpaceDetails.Instance;
+ }
+
+ var first = colorSpaceArray[0] as NameToken;
+
+ if (first == null || !ColorSpaceMapper.TryMap(first, resourceStore, out var innerColorSpace)
+ || innerColorSpace != ColorSpace.Lab)
+ {
+ return UnsupportedColorSpaceDetails.Instance;
+ }
+
+ var second = colorSpaceArray[1];
+
+ // WhitePoint is required
+ if (!DirectObjectFinder.TryGet(second, scanner, out DictionaryToken dictionaryToken) ||
+ !dictionaryToken.TryGet(NameToken.WhitePoint, scanner, out ArrayToken whitePointToken))
+ {
+ return UnsupportedColorSpaceDetails.Instance;
+ }
+
+ var whitePoint = whitePointToken.Data.OfType().Select(x => x.Data).ToList();
+
+ // BlackPoint is optional
+ IReadOnlyList blackPoint = null;
+ if (dictionaryToken.TryGet(NameToken.BlackPoint, scanner, out ArrayToken blackPointToken))
+ {
+ blackPoint = blackPointToken.Data.OfType().Select(x => x.Data).ToList();
+ }
+
+ // Matrix is optional
+ IReadOnlyList matrix = null;
+ if (dictionaryToken.TryGet(NameToken.Matrix, scanner, out ArrayToken matrixToken))
+ {
+ matrix = matrixToken.Data.OfType().Select(x => x.Data).ToList();
+ }
+
+ return new LabColorSpaceDetails(whitePoint, blackPoint, matrix);
+ }
case ColorSpace.ICCBased:
{
if (!TryGetColorSpaceArray(imageDictionary, resourceStore, scanner, out var colorSpaceArray)
@@ -255,14 +297,14 @@
filterProvider,
true);
}
- else if (DirectObjectFinder.TryGet(second, scanner, out ArrayToken baseColorSpaceArrayToken)
- && baseColorSpaceArrayToken.Length > 0 && baseColorSpaceArrayToken[0] is NameToken baseColorSpaceArrayNameToken
- && ColorSpaceMapper.TryMap(baseColorSpaceArrayNameToken, resourceStore, out var baseColorSpaceArrayColorSpace))
+ else if (DirectObjectFinder.TryGet(second, scanner, out ArrayToken baseColorSpaceArrayToken)
+ && baseColorSpaceArrayToken.Length > 0 && baseColorSpaceArrayToken[0] is NameToken baseColorSpaceArrayNameToken
+ && ColorSpaceMapper.TryMap(baseColorSpaceArrayNameToken, resourceStore, out var baseColorSpaceArrayColorSpace))
{
var pseudoImageDictionary = new DictionaryToken(
new Dictionary
- {
- {NameToken.ColorSpace, baseColorSpaceArrayToken}
+ {
+ { NameToken.ColorSpace, baseColorSpaceArrayToken }
});
baseDetails = GetColorSpaceDetails(
@@ -349,15 +391,15 @@
filterProvider,
true);
}
- else if (DirectObjectFinder.TryGet(colorSpaceArray[2], scanner, out ArrayToken alternateArrayToken)
- && alternateArrayToken.Length > 0
- && alternateArrayToken[0] is NameToken alternateColorSpaceNameToken
- && ColorSpaceMapper.TryMap(alternateColorSpaceNameToken, resourceStore, out var alternateArrayColorSpace))
+ else if (DirectObjectFinder.TryGet(colorSpaceArray[2], scanner, out ArrayToken alternateArrayToken)
+ && alternateArrayToken.Length > 0
+ && alternateArrayToken[0] is NameToken alternateColorSpaceNameToken
+ && ColorSpaceMapper.TryMap(alternateColorSpaceNameToken, resourceStore, out var alternateArrayColorSpace))
{
var pseudoImageDictionary = new DictionaryToken(
new Dictionary
- {
- {NameToken.ColorSpace, alternateArrayToken}
+ {
+ { NameToken.ColorSpace, alternateArrayToken }
});
alternateColorSpaceDetails = GetColorSpaceDetails(
@@ -391,8 +433,104 @@
return new SeparationColorSpaceDetails(separationNameToken, alternateColorSpaceDetails, function);
}
- case ColorSpace.DeviceN:
- return UnsupportedColorSpaceDetails.Instance;
+ case ColorSpace.DeviceN:
+ {
+ if (!TryGetColorSpaceArray(imageDictionary, resourceStore, scanner, out var colorSpaceArray)
+ || (colorSpaceArray.Length != 4 && colorSpaceArray.Length != 5))
+ {
+ // Error instead?
+ return UnsupportedColorSpaceDetails.Instance;
+ }
+
+ if (!DirectObjectFinder.TryGet(colorSpaceArray[0], scanner, out NameToken deviceNColorSpaceNameToken)
+ || !deviceNColorSpaceNameToken.Equals(NameToken.Devicen))
+ {
+ return UnsupportedColorSpaceDetails.Instance;
+ }
+
+ if (!DirectObjectFinder.TryGet(colorSpaceArray[1], scanner, out ArrayToken deviceNNamesToken))
+ {
+ return UnsupportedColorSpaceDetails.Instance;
+ }
+
+ ColorSpaceDetails alternateColorSpaceDetails;
+ if (DirectObjectFinder.TryGet(colorSpaceArray[2], scanner, out NameToken alternateNameToken)
+ && ColorSpaceMapper.TryMap(alternateNameToken, resourceStore, out var baseColorSpaceName))
+ {
+ alternateColorSpaceDetails = GetColorSpaceDetails(
+ baseColorSpaceName,
+ imageDictionary,
+ scanner,
+ resourceStore,
+ filterProvider,
+ true);
+ }
+ else if (DirectObjectFinder.TryGet(colorSpaceArray[2], scanner, out ArrayToken alternateArrayToken)
+ && alternateArrayToken.Length > 0
+ && alternateArrayToken[0] is NameToken alternateColorSpaceNameToken
+ && ColorSpaceMapper.TryMap(alternateColorSpaceNameToken, resourceStore, out var alternateArrayColorSpace))
+ {
+ var pseudoImageDictionary = new DictionaryToken(
+ new Dictionary
+ {
+ { NameToken.ColorSpace, alternateArrayToken }
+ });
+
+ alternateColorSpaceDetails = GetColorSpaceDetails(
+ alternateArrayColorSpace,
+ pseudoImageDictionary,
+ scanner,
+ resourceStore,
+ filterProvider,
+ true);
+ }
+ else
+ {
+ return UnsupportedColorSpaceDetails.Instance;
+ }
+
+ var func = colorSpaceArray[3];
+ PdfFunction tintFunc = PdfFunctionParser.Create(func, scanner, filterProvider);
+
+ if (colorSpaceArray.Length > 4 && DirectObjectFinder.TryGet(colorSpaceArray[4], scanner, out DictionaryToken deviceNAttributesToken))
+ {
+ // Optionnal
+
+ // Subtype - NameToken - Optional - Default value: DeviceN.
+ NameToken subtype = NameToken.Devicen;
+ if (deviceNAttributesToken.ContainsKey(NameToken.Subtype))
+ {
+ subtype = deviceNAttributesToken.Get(NameToken.Subtype, scanner);
+ }
+
+ // Colorants - dictionary - Required if Subtype is NChannel and the colour space includes spot colorants; otherwise optional
+ DictionaryToken colorants = null;
+ if (deviceNAttributesToken.ContainsKey(NameToken.Colorants))
+ {
+ colorants = deviceNAttributesToken.Get(NameToken.Colorants, scanner);
+ }
+
+ // Process - dictionary - Required if Subtype is NChannel and the colour space includes components of a process colour space, otherwise optional; PDF 1.6
+ DictionaryToken process = null;
+ if (deviceNAttributesToken.ContainsKey(NameToken.Process))
+ {
+ process = deviceNAttributesToken.Get(NameToken.Process, scanner);
+ }
+
+ // MixingHints - dictionary - Optional
+ DictionaryToken mixingHints = null;
+ if (deviceNAttributesToken.ContainsKey(NameToken.MixingHints))
+ {
+ mixingHints = deviceNAttributesToken.Get(NameToken.MixingHints, scanner);
+ }
+
+ var attributes = new DeviceNColorSpaceDetails.DeviceNColorSpaceAttributes(subtype, colorants, process, mixingHints);
+
+ return new DeviceNColorSpaceDetails(deviceNNamesToken.Data.OfType().ToArray(), alternateColorSpaceDetails, tintFunc, attributes);
+ }
+
+ return new DeviceNColorSpaceDetails(deviceNNamesToken.Data.OfType().ToArray(), alternateColorSpaceDetails, tintFunc);
+ }
default:
return UnsupportedColorSpaceDetails.Instance;
}
diff --git a/src/UglyToad.PdfPig/Util/PdfFunctionParser.cs b/src/UglyToad.PdfPig/Util/PdfFunctionParser.cs
index ad2b36e7..70e6bfc4 100644
--- a/src/UglyToad.PdfPig/Util/PdfFunctionParser.cs
+++ b/src/UglyToad.PdfPig/Util/PdfFunctionParser.cs
@@ -16,12 +16,12 @@
DictionaryToken functionDictionary;
StreamToken functionStream = null;
- if (function is StreamToken fs)
+ if (DirectObjectFinder.TryGet(function, scanner, out StreamToken fs))
{
functionDictionary = fs.StreamDictionary;
functionStream = new StreamToken(fs.StreamDictionary, fs.Decode(filterProvider, scanner));
}
- else if (function is DictionaryToken fd)
+ else if (DirectObjectFinder.TryGet(function, scanner, out DictionaryToken fd))
{
functionDictionary = fd;
}