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; }