diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/calgray-decoded.bin b/src/UglyToad.PdfPig.Tests/Images/Files/calgray-decoded.bin new file mode 100644 index 00000000..2a65a596 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/calgray-decoded.bin differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/calgray.png b/src/UglyToad.PdfPig.Tests/Images/Files/calgray.png new file mode 100644 index 00000000..d961b04d Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/calgray.png differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/calrgb-decoded.bin b/src/UglyToad.PdfPig.Tests/Images/Files/calrgb-decoded.bin new file mode 100644 index 00000000..fbb4bb4a Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/calrgb-decoded.bin differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/calrgb.png b/src/UglyToad.PdfPig.Tests/Images/Files/calrgb.png new file mode 100644 index 00000000..4befc25b Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/calrgb.png differ diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/iccbased-decoded.bin b/src/UglyToad.PdfPig.Tests/Images/Files/iccbased-decoded.bin new file mode 100644 index 00000000..755c9e9f --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Images/Files/iccbased-decoded.bin @@ -0,0 +1 @@ +hm \ No newline at end of file diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/iccbased.png b/src/UglyToad.PdfPig.Tests/Images/Files/iccbased.png new file mode 100644 index 00000000..ccebdbee Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/iccbased.png differ diff --git a/src/UglyToad.PdfPig.Tests/Images/ImageHelpers.cs b/src/UglyToad.PdfPig.Tests/Images/ImageHelpers.cs index c4ed223c..17a2b45b 100644 --- a/src/UglyToad.PdfPig.Tests/Images/ImageHelpers.cs +++ b/src/UglyToad.PdfPig.Tests/Images/ImageHelpers.cs @@ -2,15 +2,28 @@ { using System; using System.IO; + using System.IO.Compression; using UglyToad.PdfPig.Images.Png; public static class ImageHelpers { private static readonly string FilesFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Images", "Files")); - public static byte[] LoadFileBytes(string filename) + public static byte[] LoadFileBytes(string filename, bool isCompressed = false) { - return File.ReadAllBytes(Path.Combine(FilesFolder, filename)); + var filePath = Path.Combine(FilesFolder, filename); + var memoryStream = new MemoryStream(); + if (isCompressed) + { + using (var deflateStream = new DeflateStream(File.OpenRead(filePath), CompressionMode.Decompress)) + { + deflateStream.CopyTo(memoryStream); + } + + return memoryStream.ToArray(); + } + + return File.ReadAllBytes(filePath); } public static bool ImagesAreEqual(byte[] first, byte[] second) diff --git a/src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs b/src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs index 76f5dccd..2b9340e0 100644 --- a/src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs +++ b/src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs @@ -1,5 +1,6 @@ namespace UglyToad.PdfPig.Tests.Images { + using System.Collections.Generic; using System.Linq; using UglyToad.PdfPig.Graphics.Colors; using UglyToad.PdfPig.Images.Png; @@ -150,7 +151,7 @@ var decodedBytes = ImageHelpers.LoadFileBytes("ccittfax-decoded.bin"); var image = new TestPdfImage { - ColorSpaceDetails = IndexedColorSpaceDetails.StencilBlackIs1, + ColorSpaceDetails = IndexedColorSpaceDetails.Stencil(DeviceGrayColorSpaceDetails.Instance, new[] { 1m, 0 }), DecodedBytes = decodedBytes, WidthInSamples = 1800, HeightInSamples = 3113, @@ -161,6 +162,103 @@ Assert.True(ImageHelpers.ImagesAreEqual(LoadImage("ccittfax.png"), bytes)); } + [Fact] + public void CanGeneratePngFromICCBasedImageData() + { + var decodedBytes = ImageHelpers.LoadFileBytes("iccbased-decoded.bin"); + var image = new TestPdfImage + { + ColorSpaceDetails = new ICCBasedColorSpaceDetails( + numberOfColorComponents: 3, + alternateColorSpaceDetails: DeviceRgbColorSpaceDetails.Instance, + range: new List { 0, 1, 0, 1, 0, 1 }, + metadata: null), + DecodedBytes = decodedBytes, + WidthInSamples = 1, + HeightInSamples = 1, + BitsPerComponent = 8 + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes)); + Assert.True(ImageHelpers.ImagesAreEqual(LoadImage("iccbased.png"), bytes)); + } + + [Fact] + public void AlternateColorSpaceDetailsIsCurrentlyUsedInPdfPigWhenGeneratingPngsFromICCBasedImageData() + { + var decodedBytes = ImageHelpers.LoadFileBytes("iccbased-decoded.bin"); + var iccBasedImage = new TestPdfImage + { + ColorSpaceDetails = new ICCBasedColorSpaceDetails( + numberOfColorComponents: 3, + alternateColorSpaceDetails: DeviceRgbColorSpaceDetails.Instance, + range: new List { 0, 1, 0, 1, 0, 1 }, + metadata: null), + DecodedBytes = decodedBytes, + WidthInSamples = 1, + HeightInSamples = 1, + BitsPerComponent = 8 + }; + + var deviceRGBImage = new TestPdfImage + { + ColorSpaceDetails = DeviceRgbColorSpaceDetails.Instance, + DecodedBytes = decodedBytes, + WidthInSamples = 1, + HeightInSamples = 1, + BitsPerComponent = 8 + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(iccBasedImage, out var iccPngBytes)); + Assert.True(PngFromPdfImageFactory.TryGenerate(deviceRGBImage, out var deviceRgbBytes)); + Assert.Equal(iccPngBytes, deviceRgbBytes); + + } + + [Fact] + public void CanGeneratePngFromCalRGBImageData() + { + var decodedBytes = ImageHelpers.LoadFileBytes("calrgb-decoded.bin"); + var image = new TestPdfImage + { + ColorSpaceDetails = new CalRGBColorSpaceDetails( + whitePoint: new List { 0.95043m, 1, 1.09m }, + blackPoint: null, + gamma: new List { 2.2m, 2.2m, 2.2m }, + matrix: new List { + 0.41239m, 0.21264m, 0.01933m, + 0.35758m, 0.71517m, 0.11919m, + 0.18045m, 0.07218m, 0.9504m }), + DecodedBytes = decodedBytes, + WidthInSamples = 153, + HeightInSamples = 83, + BitsPerComponent = 8 + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes)); + Assert.True(ImageHelpers.ImagesAreEqual(LoadImage("calrgb.png"), bytes)); + } + + [Fact] + public void CanGeneratePngFromCalGrayImageData() + { + var decodedBytes = ImageHelpers.LoadFileBytes("calgray-decoded.bin", isCompressed: true); + var image = new TestPdfImage + { + ColorSpaceDetails = new CalGrayColorSpaceDetails( + whitePoint: new List { 0.9505000114m, 1, 1.0889999866m }, + blackPoint: null, + gamma: 2.2000000477m), + DecodedBytes = decodedBytes, + WidthInSamples = 2480, + HeightInSamples = 1748, + BitsPerComponent = 8, + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes)); + Assert.True(ImageHelpers.ImagesAreEqual(LoadImage("calgray.png"), bytes)); + } + private static byte[] LoadImage(string name) { return ImageHelpers.LoadFileBytes(name); diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs index 60eebf12..ef41c787 100644 --- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs +++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs @@ -105,6 +105,8 @@ "UglyToad.PdfPig.Graphics.Colors.IColor", "UglyToad.PdfPig.Graphics.Colors.RGBColor", "UglyToad.PdfPig.Graphics.Colors.ColorSpaceDetails", + "UglyToad.PdfPig.Graphics.Colors.CalGrayColorSpaceDetails", + "UglyToad.PdfPig.Graphics.Colors.CalRGBColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.DeviceGrayColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.DeviceRgbColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.DeviceCmykColorSpaceDetails", diff --git a/src/UglyToad.PdfPig.Tests/Util/Matrix3x3Tests.cs b/src/UglyToad.PdfPig.Tests/Util/Matrix3x3Tests.cs new file mode 100644 index 00000000..3e1cef08 --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Util/Matrix3x3Tests.cs @@ -0,0 +1,121 @@ +namespace UglyToad.PdfPig.Tests.Util +{ + using Xunit; + using PdfPig.Util; + + public class Matrix3x3Tests + { + [Fact] + public void CanCreate() + { + var matrix = new Matrix3x3( + 1, 2, 3, + 4, 5, 6, + 7, 8, 9); + + Assert.Equal(new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, matrix); + } + + [Fact] + public void ExposesIdentityMatrix() + { + var matrix = new Matrix3x3( + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ); + + Assert.Equal(matrix, Matrix3x3.Identity); + } + + [Fact] + public void CanMultiplyWithFactor() + { + var matrix = new Matrix3x3( + 1, 2, 3, + 4, 5, 6, + 7, 8, 9); + + var result = matrix.Multiply(3); + + Assert.Equal(new double[] { 3, 6, 9, 12, 15, 18, 21, 24, 27 }, result); + } + + [Fact] + public void CanMultiplyWithVector() + { + var matrix = new Matrix3x3( + 1, 2, 3, + 4, 5, 6, + 7, 8, 9); + + var vector = (1, 2, 3); + + var product = matrix.Multiply(vector); + + Assert.Equal((14, 32, 50), product); + // Result can be verified here: + // https://www.wolframalpha.com/input/?i=%7B%7B1%2C2%2C+3%7D%2C%7B4%2C5%2C6%7D%2C%7B7%2C8%2C9%7D%7D.%7B1%2C2%2C3%7D + } + + [Fact] + public void CanMultiplyWithMatrix() + { + var matrix = new Matrix3x3( + 1, 2, 3, + 4, 5, 6, + 7, 8, 9); + + var product = matrix.Multiply(matrix); + + var expected = new Matrix3x3( + 30, 36, 42, + 66, 81, 96, + 102, 126, 150); + + Assert.Equal(expected, product); + // Result can be verified here: + // https://www.wolframalpha.com/input/?i=%7B%7B1%2C2%2C3%7D%2C%7B4%2C5%2C6%7D%2C%7B7%2C8%2C9%7D%7D.%7B%7B1%2C2%2C3%7D%2C%7B4%2C5%2C6%7D%2C%7B7%2C8%2C9%7D%7D + + } + + [Fact] + public void CanTranspose() + { + var matrix = new Matrix3x3( + 1, 2, 3, + 4, 5, 6, + 7, 8, 9); + + var transposed = matrix.Transpose(); + + Assert.Equal(new double[] { 1, 4, 7, 2, 5, 8, 3, 6, 9 }, transposed); + } + + [Fact] + public void InverseReturnsMatrixIfPossible() + { + var matrix = new Matrix3x3( + 1, 2, 3, + 0, 1, 4, + 5, 6, 0); + + var inverse = matrix.Inverse(); + + Assert.Equal(new double[] { -24, 18, 5, 20, -15, -4, -5, 4, 1 }, inverse); + Assert.Equal(Matrix3x3.Identity, matrix.Multiply(inverse)); + } + + [Fact] + public void InverseReturnsNullIfNotPossible() + { + var matrix = new Matrix3x3( + 1, 2, 3, + 4, 5, 6, + 7, 8, 9); + + var inverse = matrix.Inverse(); + Assert.Null(inverse); + } + } +} diff --git a/src/UglyToad.PdfPig.Tokens/DictionaryToken.cs b/src/UglyToad.PdfPig.Tokens/DictionaryToken.cs index f770dd14..1a56f4a5 100644 --- a/src/UglyToad.PdfPig.Tokens/DictionaryToken.cs +++ b/src/UglyToad.PdfPig.Tokens/DictionaryToken.cs @@ -102,6 +102,16 @@ /// A new with the entry created or modified. public DictionaryToken With(string key, IToken value) { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + var result = new Dictionary(Data.Count + 1); foreach (var keyValuePair in Data) @@ -114,6 +124,35 @@ return new DictionaryToken(result); } + /// + /// Creates a copy of this dictionary with the entry with the specified key removed (if it exists). + /// + /// The key of the entry to remove. + /// A new with the entry removed. + public DictionaryToken Without(NameToken key) => Without(key.Data); + + /// + /// Creates a copy of this dictionary with the entry with the specified key removed (if it exists). + /// + /// The key of the entry to remove. + /// A new with the entry removed. + public DictionaryToken Without(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var result = new Dictionary(Data.ContainsKey(key) ? Data.Count - 1 : Data.Count); + + foreach (var keyValuePair in Data.Where(x => !x.Key.Equals(key))) + { + result[keyValuePair.Key] = keyValuePair.Value; + } + + return new DictionaryToken(result); + } + /// /// Create a new . /// diff --git a/src/UglyToad.PdfPig/Content/IPdfImage.cs b/src/UglyToad.PdfPig/Content/IPdfImage.cs index 80dc2a9a..157aa482 100644 --- a/src/UglyToad.PdfPig/Content/IPdfImage.cs +++ b/src/UglyToad.PdfPig/Content/IPdfImage.cs @@ -88,7 +88,7 @@ /// /// The full dictionary for this image object. /// - public DictionaryToken ImageDictionary { get; } + DictionaryToken ImageDictionary { get; } /// /// Full details for the with any associated data. diff --git a/src/UglyToad.PdfPig/Graphics/Colors/CIEBasedColorSpaceTransformer.cs b/src/UglyToad.PdfPig/Graphics/Colors/CIEBasedColorSpaceTransformer.cs new file mode 100644 index 00000000..5f68c32f --- /dev/null +++ b/src/UglyToad.PdfPig/Graphics/Colors/CIEBasedColorSpaceTransformer.cs @@ -0,0 +1,111 @@ +namespace UglyToad.PdfPig.Graphics.Colors +{ + using System; + using UglyToad.PdfPig.Util; + + /// + /// 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 + { + private readonly RGBWorkingSpace destinationWorkingSpace; + private readonly Matrix3x3 transformationMatrix; + private readonly ChromaticAdaptation chromaticAdaptation; + + // These properties control how the color is translated from ABC to XYZ: + public Func<(double A, double B, double C), (double A, double B, double C)> DecoderABC { get; set; } = color => color; + public Func<(double L, double M, double N), (double L, double M, double N)> DecoderLMN { get; set; } = color => color; + public Matrix3x3 MatrixABC { get; set; } = Matrix3x3.Identity; + public Matrix3x3 MatrixLMN { get; set; } = Matrix3x3.Identity; + + public CIEBasedColorSpaceTransformer((double X, double Y, double Z) sourceReferenceWhite, RGBWorkingSpace destinationWorkingSpace) + { + this.destinationWorkingSpace = destinationWorkingSpace; + + // Create an adapter capable of adapting from one reference white to another + chromaticAdaptation = new ChromaticAdaptation(sourceReferenceWhite, destinationWorkingSpace.ReferenceWhite); + + // Construct the transformation matrix capable of transforming from XYZ of the source color space + // to RGB of the destination color space + var xr = destinationWorkingSpace.RedPrimary.x; + var yr = destinationWorkingSpace.RedPrimary.y; + + var xg = destinationWorkingSpace.GreenPrimary.x; + var yg = destinationWorkingSpace.GreenPrimary.y; + + var xb = destinationWorkingSpace.BluePrimary.x; + var yb = destinationWorkingSpace.BluePrimary.y; + + var Xr = xr / yr; + var Yr = 1; + var Zr = (1 - xr - yr) / yr; + + var Xg = xg / yg; + var Yg = 1; + var Zg = (1 - xg - yg) / yg; + + var Xb = xb / yb; + var Yb = 1; + var Zb = (1 - xb - yb) / yb; + + var mXYZ = new Matrix3x3( + Xr, Xg, Xb, + Yr, Yg, Yb, + Zr, Zg, Zb).Inverse(); + + var S = mXYZ.Multiply(destinationWorkingSpace.ReferenceWhite); + + var Sr = S.Item1; + var Sg = S.Item2; + var Sb = S.Item3; + + var M = new Matrix3x3( + Sr * Xr, Sg * Xg, Sb * Xb, + Sr * Yr, Sg * Yg, Sb * Yb, + Sr * Zr, Sg * Zg, Sb * Zb); + + transformationMatrix = M.Inverse(); + } + + /// + /// 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. + /// + public (double R, double G, double B) TransformToRGB((double A, double B, double C) color) + { + var xyz = TransformToXYZ(color); + + var adaptedColor = chromaticAdaptation.Transform(xyz); + var rgb = transformationMatrix.Multiply(adaptedColor); + + var gammaCorrectedR = destinationWorkingSpace.GammaCorrection(rgb.Item1); + var gammaCorrectedG = destinationWorkingSpace.GammaCorrection(rgb.Item2); + var gammaCorrectedB = destinationWorkingSpace.GammaCorrection(rgb.Item3); + + return (Clamp(gammaCorrectedR), Clamp(gammaCorrectedG), Clamp(gammaCorrectedB)); + } + + private (double X, double Y, double Z) TransformToXYZ((double A, double B, double C) color) + { + var decodedABC = DecoderABC(color); + var lmn = MatrixABC.Multiply(decodedABC); + + var decodedLMN = DecoderLMN(lmn); + var xyz = MatrixLMN.Multiply(decodedLMN); + + return xyz; + } + + private static double Clamp(double value) + { + // Force value into range [0..1] + return value < 0 ? 0 : value > 1 ? 1 : value; + } + } +} diff --git a/src/UglyToad.PdfPig/Graphics/Colors/ChromaticAdaptation.cs b/src/UglyToad.PdfPig/Graphics/Colors/ChromaticAdaptation.cs new file mode 100644 index 00000000..bb43130e --- /dev/null +++ b/src/UglyToad.PdfPig/Graphics/Colors/ChromaticAdaptation.cs @@ -0,0 +1,66 @@ +namespace UglyToad.PdfPig.Graphics.Colors +{ + using UglyToad.PdfPig.Util; + + /// + /// Encapsulates the algorithm for chromatic adaptation described here: + /// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + /// + internal class ChromaticAdaptation + { + public enum Method { XYZScaling, Bradford, VonKries }; + + private readonly Matrix3x3 adaptationMatrix; + + public ChromaticAdaptation( + (double Xws, double Yws, double Zws) sourceReferenceWhite, + (double Xwd, double Ywd, double Zwd) destinationReferenceWhite, + Method method = Method.Bradford) + { + var coneReponseDomain = GetConeResponseDomain(method); + var inverseConeResponseDomain = coneReponseDomain.Inverse(); + var (ρS, γS, βS) = coneReponseDomain.Multiply(sourceReferenceWhite); + + var (ρD, γD, βD) = coneReponseDomain.Multiply(destinationReferenceWhite); + + var scale = new Matrix3x3( + ρD / ρS, 0, 0, + 0, γD / γS, 0, + 0, 0, βD / βS); + + adaptationMatrix = inverseConeResponseDomain.Multiply(scale).Multiply(coneReponseDomain); + } + + public (double X, double Y, double Z) Transform((double X, double Y, double Z) sourceColor) + { + return adaptationMatrix.Multiply(sourceColor); + } + + private static Matrix3x3 GetConeResponseDomain(Method method) + { + switch (method) + { + case Method.XYZScaling: + return new Matrix3x3( + 1.0000000, 0.0000000, 0.0000000, + 0.0000000, 1.0000000, 0.0000000, + 0.0000000, 0.0000000, 1.0000000); + + case Method.Bradford: + return new Matrix3x3( + 0.8951000, 0.2664000, -0.1614000, + -0.7502000, 1.7135000, 0.0367000, + 0.0389000, -0.0685000, 1.0296000); + + case Method.VonKries: + return new Matrix3x3( + 0.4002400, 0.7076000, -0.0808100, + -0.2263000, 1.1653200, 0.0457000, + 0.0000000, 0.0000000, 0.9182200); + + default: + return GetConeResponseDomain(Method.Bradford); + } + } + } +} diff --git a/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs b/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs index 745151d1..3416b4e7 100644 --- a/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs +++ b/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs @@ -1,11 +1,12 @@ namespace UglyToad.PdfPig.Graphics.Colors { - using System; + using PdfPig.Core; + using System; using System.Collections.Generic; using System.Linq; - using PdfPig.Core; using Tokens; using UglyToad.PdfPig.Content; + using UglyToad.PdfPig.Util; using UglyToad.PdfPig.Util.JetBrains.Annotations; /// @@ -88,23 +89,15 @@ public class IndexedColorSpaceDetails : ColorSpaceDetails { /// - /// A color space useful for extracting stencil masks as black-and-white images. - /// Index 0 is black and index 1 is white. - /// - internal static readonly IndexedColorSpaceDetails StencilBlackIs0 - = new IndexedColorSpaceDetails(DeviceGrayColorSpaceDetails.Instance, 1, new byte[] { 0, 255 }); - - /// - /// A color space useful for extracting stencil masks as black-and-white images. - /// Index 0 is white and index 1 is black. - /// - internal static readonly IndexedColorSpaceDetails StencilBlackIs1 - = new IndexedColorSpaceDetails(DeviceGrayColorSpaceDetails.Instance, 1, new byte[] { 255, 0 }); - - internal static ColorSpaceDetails Stencil(decimal[] decode) + /// Creates a indexed color space useful for exracting 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. + /// + internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, decimal[] decode) { - return decode.Length >= 2 && decode[0] == 1 && decode[1] == 0 ? - StencilBlackIs1 : StencilBlackIs0 /* default */; + var blackIsOne = decode.Length >= 2 && decode[0] == 1 && decode[1] == 0; + return new IndexedColorSpaceDetails(colorSpaceDetails, 1, blackIsOne ? new byte[] { 255, 0 } : new byte[] { 0, 255 }); } /// @@ -188,6 +181,172 @@ AlternateColorSpaceDetails = alternateColorSpaceDetails; TintFunction = tintFunction; } + } + + /// + /// 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. + /// 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 + { + private readonly CIEBasedColorSpaceTransformer colorSpaceTransformer; + /// + /// 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; } + + /// + /// A number defining defining the gamma for the gray (A) component. Gamma must be positive and is generally + /// greater than or equal to 1. Default value: 1. + /// + public decimal Gamma { get; } + + /// + /// Create a new . + /// + public CalGrayColorSpaceDetails([NotNull] IReadOnlyList whitePoint, [CanBeNull] IReadOnlyList blackPoint, decimal? gamma) + : base(ColorSpace.CalGray) + { + WhitePoint = whitePoint ?? 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 ?? new[] { 0m, 0, 0 }.ToList(); + if (BlackPoint.Count != 3) + { + throw new ArgumentOutOfRangeException(nameof(blackPoint), blackPoint, $"Must consist of exactly three numbers, but was passed {blackPoint.Count}."); + } + + Gamma = gamma ?? 1m; + + colorSpaceTransformer = + new CIEBasedColorSpaceTransformer(((double)WhitePoint[0], (double)WhitePoint[1], (double)WhitePoint[2]), RGBWorkingSpace.sRGB) + { + DecoderABC = color => ( + Math.Pow(color.A, (double)Gamma), + Math.Pow(color.B, (double)Gamma), + Math.Pow(color.C, (double)Gamma)), + + MatrixABC = new Matrix3x3( + (double)WhitePoint[0], 0, 0, + 0, (double)WhitePoint[1], 0, + 0, 0, (double)WhitePoint[2]) + }; + } + + /// + /// Transforms the supplied A color to grayscale RGB (sRGB) using the propties of this + /// in the transformation process. + /// A represents the gray component of a calibrated gray space. The component must be in the range 0.0 to 1.0. + /// + internal RGBColor TransformToRGB(decimal colorA) + { + var colorRgb = colorSpaceTransformer.TransformToRGB(((double)colorA, (double)colorA, (double)colorA)); + return new RGBColor((decimal)colorRgb.R, (decimal)colorRgb.G, (decimal)colorRgb.B); + } + } + + /// + /// 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 class CalRGBColorSpaceDetails : ColorSpaceDetails + { + private readonly CIEBasedColorSpaceTransformer colorSpaceTransformer; + + /// + /// 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 three numbers [GR GG GB] specifying the gamma for the red, green and blue (A, B, C) components + /// of the color space. Default value: [1.0 1.0 1.0]. + /// + public IReadOnlyList Gamma { get; } + + /// + /// An array of nine numbers [XA YA ZA XB YB ZB XC YC ZC] specifying the linear interpretation of the + /// decoded A, B, C components of the color space with respect to the final XYZ representation. Default value: + /// [1 0 0 0 1 0 0 0 1]. + /// + public IReadOnlyList Matrix { get; } + + /// + /// Create a new . + /// + public CalRGBColorSpaceDetails([NotNull] IReadOnlyList whitePoint, [CanBeNull] IReadOnlyList blackPoint, [CanBeNull] IReadOnlyList gamma, [CanBeNull] IReadOnlyList matrix) + : base(ColorSpace.CalRGB) + { + WhitePoint = whitePoint ?? 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 ?? new[] { 0m, 0, 0 }.ToList(); + if (BlackPoint.Count != 3) + { + throw new ArgumentOutOfRangeException(nameof(blackPoint), blackPoint, $"Must consist of exactly three numbers, but was passed {blackPoint.Count}."); + } + + Gamma = gamma ?? new[] { 1m, 1, 1 }.ToList(); + if (Gamma.Count != 3) + { + throw new ArgumentOutOfRangeException(nameof(gamma), gamma, $"Must consist of exactly three numbers, but was passed {gamma.Count}."); + } + + Matrix = matrix ?? new[] { 1m, 0, 0, 0, 1, 0, 0, 0, 1 }.ToList(); + if (Matrix.Count != 9) + { + throw new ArgumentOutOfRangeException(nameof(matrix), matrix, $"Must consist of exactly nine numbers, but was passed {matrix.Count}."); + } + + colorSpaceTransformer = + new CIEBasedColorSpaceTransformer(((double)WhitePoint[0], (double)WhitePoint[1], (double)WhitePoint[2]), RGBWorkingSpace.sRGB) + { + DecoderABC = color => ( + Math.Pow(color.A, (double)Gamma[0]), + Math.Pow(color.B, (double)Gamma[1]), + Math.Pow(color.C, (double)Gamma[2])), + + MatrixABC = new Matrix3x3( + (double)Matrix[0], (double)Matrix[3], (double)Matrix[6], + (double)Matrix[1], (double)Matrix[4], (double)Matrix[7], + (double)Matrix[2], (double)Matrix[5], (double)Matrix[8]) + }; + } + + /// + /// Transforms the supplied ABC color to RGB (sRGB) using the propties of this + /// in the transformation process. + /// A, B and C represent red, green and blue calibrated color values in the range 0.0 to 1.0. + /// + internal RGBColor TransformToRGB((decimal A, decimal B, decimal C) colorAbc) + { + var colorRgb = colorSpaceTransformer.TransformToRGB(((double)colorAbc.A, (double)colorAbc.B, (double)colorAbc.C)); + return new RGBColor((decimal)colorRgb.R, (decimal) colorRgb.G, (decimal) colorRgb.B); + } } /// @@ -196,7 +355,7 @@ /// 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, as calculations will only be based on + /// Currently support for this color space is limited in PdfPig. Calculations will only be based on /// the color space of . /// public class ICCBasedColorSpaceDetails : ColorSpaceDetails @@ -251,12 +410,17 @@ NumberOfColorComponents = numberOfColorComponents; AlternateColorSpaceDetails = alternateColorSpaceDetails ?? - (NumberOfColorComponents == 1 ? DeviceGrayColorSpaceDetails.Instance : - NumberOfColorComponents == 3 ? DeviceRgbColorSpaceDetails.Instance : DeviceCmykColorSpaceDetails.Instance); + (NumberOfColorComponents == 1 ? (ColorSpaceDetails) DeviceGrayColorSpaceDetails.Instance : + NumberOfColorComponents == 3 ? (ColorSpaceDetails) DeviceRgbColorSpaceDetails.Instance : (ColorSpaceDetails) DeviceCmykColorSpaceDetails.Instance); BaseType = AlternateColorSpaceDetails.BaseType; Range = range ?? Enumerable.Range(0, numberOfColorComponents).Select(x => new[] { 0.0m, 1.0m }).SelectMany(x => x).ToList(); + if (Range.Count != 2 * numberOfColorComponents) + { + throw new ArgumentOutOfRangeException(nameof(range), range, + $"Must consist of exactly {2 * numberOfColorComponents } (2 x NumberOfColorComponents), but was passed {range.Count }"); + } Metadata = metadata; } } diff --git a/src/UglyToad.PdfPig/Graphics/Colors/RGBWorkingSpace.cs b/src/UglyToad.PdfPig/Graphics/Colors/RGBWorkingSpace.cs new file mode 100644 index 00000000..936deb77 --- /dev/null +++ b/src/UglyToad.PdfPig/Graphics/Colors/RGBWorkingSpace.cs @@ -0,0 +1,178 @@ +namespace UglyToad.PdfPig.Graphics.Colors +{ + using System; + + // The RGB working space specifications below were obtained from: http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html + internal class RGBWorkingSpace + { + public static readonly XYZReferenceWhite ReferenceWhites = new XYZReferenceWhite(); + + public static readonly RGBWorkingSpace AdobeRGB1998 = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D65, + RedPrimary = (0.6400, 0.3300, 0.297361), + GreenPrimary = (0.2100, 0.7100, 0.627355), + BluePrimary = (0.1500, 0.0600, 0.075285), + }; + + public static readonly RGBWorkingSpace AppleRGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(1.8), + ReferenceWhite = ReferenceWhites.D65, + RedPrimary = (0.6250, 0.3400, 0.244634), + GreenPrimary = (0.2800, 0.5950, 0.672034), + BluePrimary = (0.1550, 0.0700, 0.083332) + }; + + public static readonly RGBWorkingSpace BestRGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D50, + RedPrimary = (0.7347, 0.2653, 0.228457), + GreenPrimary = (0.2150, 0.7750, 0.737352), + BluePrimary = (0.1300, 0.0350, 0.034191) + }; + + public static readonly RGBWorkingSpace BetaRGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D50, + RedPrimary = (0.6888, 0.3112, 0.303273), + GreenPrimary = (0.1986, 0.7551, 0.663786), + BluePrimary = (0.1265, 0.0352, 0.032941) + }; + + public static readonly RGBWorkingSpace BruceRGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D65, + RedPrimary = (0.6400, 0.3300, 0.240995), + GreenPrimary = (0.2800, 0.6500, 0.683554), + BluePrimary = (0.1500, 0.0600, 0.075452) + }; + + public static readonly RGBWorkingSpace CIE_RGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.E, + RedPrimary = (0.7350, 0.2650, 0.176204), + GreenPrimary = (0.2740, 0.7170, 0.812985), + BluePrimary = (0.1670, 0.0090, 0.010811) + }; + + public static readonly RGBWorkingSpace ColorMatchRGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(1.8), + ReferenceWhite = ReferenceWhites.D50, + RedPrimary = (0.6300, 0.3400, 0.274884), + GreenPrimary = (0.2950, 0.6050, 0.658132), + BluePrimary = (0.1500, 0.0750, 0.066985) + }; + + public static readonly RGBWorkingSpace DonRGB4 = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D50, + RedPrimary = (0.6960, 0.3000, 0.278350), + GreenPrimary = (0.2150, 0.7650, 0.687970), + BluePrimary = (0.1300, 0.0350, 0.033680) + }; + + public static readonly RGBWorkingSpace EktaSpacePS5 = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D50, + RedPrimary = (0.6950, 0.3050, 0.260629), + GreenPrimary = (0.2600, 0.7000, 0.734946), + BluePrimary = (0.1100, 0.0050, 0.004425) + }; + + public static readonly RGBWorkingSpace NTSC_RGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.C, + RedPrimary = (0.6700, 0.3300, 0.298839), + GreenPrimary = (0.2100, 0.7100, 0.586811), + BluePrimary = (0.1400, 0.0800, 0.114350) + }; + + public static readonly RGBWorkingSpace PAL_SECAM_RGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D65, + RedPrimary = (0.6400, 0.3300, 0.222021), + GreenPrimary = (0.2900, 0.6000, 0.706645), + BluePrimary = (0.1500, 0.0600, 0.071334) + }; + + public static readonly RGBWorkingSpace ProPhotoRGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(1.8), + ReferenceWhite = ReferenceWhites.D50, + RedPrimary = (0.7347, 0.2653, 0.288040), + GreenPrimary = (0.1596, 0.8404, 0.711874), + BluePrimary = (0.0366, 0.0001, 0.000086) + }; + + public static readonly RGBWorkingSpace SMPTE_C_RGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D65, + RedPrimary = (0.6300, 0.3400, 0.212395), + GreenPrimary = (0.3100, 0.5950, 0.701049), + BluePrimary = (0.1550, 0.0700, 0.086556) + }; + + public static readonly RGBWorkingSpace sRGB = new RGBWorkingSpace + { + // sRGB gamma correction obtained from: http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html + GammaCorrection = val => val <= 0.0031308 ? 12.92 * val : (1.055 * Math.Pow(val, (1 / 2.4)) - 0.055), + ReferenceWhite = ReferenceWhites.D65, + RedPrimary = (0.6400, 0.3300, 0.212656), + GreenPrimary = (0.3000, 0.6000, 0.715158), + BluePrimary = (0.1500, 0.0600, 0.072186), + }; + + public static readonly RGBWorkingSpace WideGamutRGB = new RGBWorkingSpace + { + GammaCorrection = CreateGammaFunc(2.2), + ReferenceWhite = ReferenceWhites.D50, + RedPrimary = (0.7350, 0.2650, 0.258187), + GreenPrimary = (0.1150, 0.8260, 0.724938), + BluePrimary = (0.1570, 0.0180, 0.016875) + }; + + public Func GammaCorrection { get; private set; } + public (double X, double Y, double Z) ReferenceWhite { get; private set; } + public (double x, double y, double Y) RedPrimary { get; private set; } + public (double x, double y, double Y) BluePrimary { get; private set; } + public (double x, double y, double Y) GreenPrimary { get; private set; } + + private static Func CreateGammaFunc(double gamma) + { + return val => + { + var result = Math.Pow(val, 1 / gamma); + return double.IsNaN(result) ? 0 : result; + }; + } + + // The reference white values below were obtained from: http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + internal class XYZReferenceWhite + { + internal XYZReferenceWhite() {} + public readonly (double X, double Y, double Z) A = (1.09850, 1.00000, 0.35585); + public readonly (double X, double Y, double Z) B = (0.99072, 1.00000, 0.85223); + public readonly (double X, double Y, double Z) C = (0.98074, 1.00000, 1.18232); + public readonly (double X, double Y, double Z) D50 = (0.96422, 1.00000, 0.82521); + public readonly (double X, double Y, double Z) D55 = (0.95682, 1.00000, 0.92149); + public readonly (double X, double Y, double Z) D65 = (0.95047, 1.00000, 1.08883); + public readonly (double X, double Y, double Z) D75 = (0.94972, 1.00000, 1.22638); + public readonly (double X, double Y, double Z) E = (1.00000, 1.00000, 1.00000); + public readonly (double X, double Y, double Z) F2 = (0.99186, 1.00000, 0.67393); + public readonly (double X, double Y, double Z) F7 = (0.95041, 1.00000, 1.08747); + public readonly (double X, double Y, double Z) F11 = (1.00962, 1.00000, 0.64350); + } + } +} diff --git a/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs b/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs index 5f541818..76fa7cf6 100644 --- a/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs +++ b/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs @@ -5,8 +5,8 @@ using System.Linq; using Content; using Core; - using Graphics.Colors; - + using Graphics.Colors; + /// /// Utility for working with the bytes in s and converting according to their .s /// @@ -48,6 +48,19 @@ if (details is IndexedColorSpaceDetails indexed) { decoded = UnwrapIndexedColorSpaceBytes(indexed, decoded); + + // Use the base color space in potential further decoding + details = indexed.BaseColorSpaceDetails; + } + + if (details is CalRGBColorSpaceDetails calRgb) + { + decoded = TransformToRGB(calRgb, decoded); + } + + if (details is CalGrayColorSpaceDetails calGray) + { + decoded = TransformToRgbGrayScale(calGray, decoded); } return decoded.ToArray(); @@ -57,13 +70,19 @@ { switch (details) { - case DeviceGrayColorSpaceDetails: + case DeviceGrayColorSpaceDetails deviceGray: return 1; - case DeviceRgbColorSpaceDetails: + case CalGrayColorSpaceDetails calGray: + return 1; + + case DeviceRgbColorSpaceDetails deviceRgb: return 3; - case DeviceCmykColorSpaceDetails: + case CalRGBColorSpaceDetails calRgb: + return 3; + + case DeviceCmykColorSpaceDetails deviceCmyk: return 4; case IndexedColorSpaceDetails indexed: @@ -105,6 +124,34 @@ return result; } + private static IReadOnlyList TransformToRgbGrayScale(CalGrayColorSpaceDetails calGray, IReadOnlyList decoded) + { + var transformed = new List(); + for (var i = 0; i < decoded.Count; i++) + { + var component = decoded[i] / 255m; + var rgbPixel = calGray.TransformToRGB(component); + // We only need one component here + transformed.Add(ConvertToByte(rgbPixel.R)); + } + + return transformed; + } + + private static IReadOnlyList TransformToRGB(CalRGBColorSpaceDetails calRgb, IReadOnlyList decoded) + { + var transformed = new List(); + for (var i = 0; i < decoded.Count; i += 3) + { + var rgbPixel = calRgb.TransformToRGB((decoded[i] / 255m, decoded[i + 1] / 255m, decoded[i + 2] / 255m)); + transformed.Add(ConvertToByte(rgbPixel.R)); + transformed.Add(ConvertToByte(rgbPixel.G)); + transformed.Add(ConvertToByte(rgbPixel.B)); + } + + return transformed; + } + private static byte[] UnwrapIndexedColorSpaceBytes(IndexedColorSpaceDetails indexed, IReadOnlyList input) { var multiplier = 1; @@ -112,6 +159,7 @@ switch (indexed.BaseType) { case ColorSpace.DeviceRGB: + case ColorSpace.CalRGB: transformer = x => { var r = new byte[3]; @@ -139,6 +187,7 @@ multiplier = 4; break; case ColorSpace.DeviceGray: + case ColorSpace.CalGray: transformer = x => new[] { indexed.ColorTable[x] }; multiplier = 1; break; @@ -161,5 +210,11 @@ return input.ToArray(); } + + private static byte ConvertToByte(decimal componentValue) + { + var rounded = Math.Round(componentValue * 255, MidpointRounding.AwayFromZero); + return (byte)rounded; + } } } diff --git a/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs b/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs index 0a12c5b0..48de57ed 100644 --- a/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs +++ b/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs @@ -16,7 +16,8 @@ var isColorSpaceSupported = actualColorSpace == ColorSpace.DeviceGray || actualColorSpace == ColorSpace.DeviceRGB - || actualColorSpace == ColorSpace.DeviceCMYK; + || actualColorSpace == ColorSpace.DeviceCMYK || actualColorSpace == ColorSpace.CalGray + || actualColorSpace == ColorSpace.CalRGB; if (!isColorSpaceSupported || !image.TryGetBytes(out var bytesPure)) { @@ -28,7 +29,11 @@ bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure, image.BitsPerComponent, image.WidthInSamples, image.HeightInSamples); - var numberOfComponents = actualColorSpace == ColorSpace.DeviceCMYK ? 4 : actualColorSpace == ColorSpace.DeviceRGB ? 3 : 1; + var numberOfComponents = + actualColorSpace == ColorSpace.DeviceCMYK ? 4 : + actualColorSpace == ColorSpace.DeviceRGB ? 3 : + actualColorSpace == ColorSpace.CalRGB ? 3 : 1; + var is3Byte = numberOfComponents == 3; var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false); diff --git a/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs b/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs index ee578c78..5f073867 100644 --- a/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs +++ b/src/UglyToad.PdfPig/Util/ColorSpaceDetailsParser.cs @@ -79,10 +79,18 @@ if (imageDictionary.GetObjectOrDefault(NameToken.ImageMask, NameToken.Im) != null || filterProvider.GetFilters(imageDictionary, scanner).OfType().Any()) { + if (cannotRecurse) + { + return UnsupportedColorSpaceDetails.Instance; + } + + var colorSpaceDetails = GetColorSpaceDetails(colorSpace, imageDictionary.Without(NameToken.Filter).Without(NameToken.F), scanner, resourceStore, filterProvider, true); + var decodeRaw = imageDictionary.GetObjectOrDefault(NameToken.Decode, NameToken.D) as ArrayToken ?? new ArrayToken(EmptyArray.Instance); var decode = decodeRaw.Data.OfType().Select(x => x.Data).ToArray(); - return IndexedColorSpaceDetails.Stencil(decode); + + return IndexedColorSpaceDetails.Stencil(colorSpaceDetails, decode); } if (!colorSpace.HasValue) @@ -99,9 +107,98 @@ case ColorSpace.DeviceCMYK: return DeviceCmykColorSpaceDetails.Instance; case ColorSpace.CalGray: - return UnsupportedColorSpaceDetails.Instance; + { + 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.CalGray) + { + return UnsupportedColorSpaceDetails.Instance; + } + + var second = colorSpaceArray[1]; + + // WhitePoint is required + if (!DirectObjectFinder.TryGet(second, scanner, out DictionaryToken dictionaryToken) || + !dictionaryToken.TryGet(NameToken.WhitePoint, 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, out ArrayToken blackPointToken)) + { + blackPoint = blackPointToken.Data.OfType().Select(x => x.Data).ToList(); + } + + // Gamma is optional + decimal? gamma = null; + if (dictionaryToken.TryGet(NameToken.Gamma, out NumericToken gammaToken)) + { + gamma = gammaToken.Data; + } + + return new CalGrayColorSpaceDetails(whitePoint, blackPoint, gamma); + } case ColorSpace.CalRGB: - return UnsupportedColorSpaceDetails.Instance; + { + 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.CalRGB) + { + return UnsupportedColorSpaceDetails.Instance; + } + + var second = colorSpaceArray[1]; + + // WhitePoint is required + if (!DirectObjectFinder.TryGet(second, scanner, out DictionaryToken dictionaryToken) || + !dictionaryToken.TryGet(NameToken.WhitePoint, 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, out ArrayToken blackPointToken)) + { + blackPoint = blackPointToken.Data.OfType().Select(x => x.Data).ToList(); + } + + // Gamma is optional + IReadOnlyList gamma = null; + if (dictionaryToken.TryGet(NameToken.Gamma, out ArrayToken gammaToken)) + { + gamma = gammaToken.Data.OfType().Select(x => x.Data).ToList(); + } + + // Matrix is optional + IReadOnlyList matrix = null; + if (dictionaryToken.TryGet(NameToken.Matrix, out ArrayToken matrixToken)) + { + matrix = matrixToken.Data.OfType().Select(x => x.Data).ToList(); + } + + return new CalRGBColorSpaceDetails(whitePoint, blackPoint, gamma, matrix); + } case ColorSpace.Lab: return UnsupportedColorSpaceDetails.Instance; case ColorSpace.ICCBased: @@ -122,12 +219,14 @@ var second = colorSpaceArray[1]; + // N is required if (!DirectObjectFinder.TryGet(second, scanner, out StreamToken streamToken) || !streamToken.StreamDictionary.TryGet(NameToken.N, out NumericToken numeric)) { return UnsupportedColorSpaceDetails.Instance; } + // Alternate is optional ColorSpaceDetails alternateColorSpaceDetails = null; if (streamToken.StreamDictionary.TryGet(NameToken.Alternate, out NameToken alternateColorSpaceNameToken) && ColorSpaceMapper.TryMap(alternateColorSpaceNameToken, resourceStore, out var alternateColorSpace)) @@ -136,12 +235,14 @@ GetColorSpaceDetails(alternateColorSpace, imageDictionary, scanner, resourceStore, filterProvider, true); } + // Range is optional IReadOnlyList range = null; if (streamToken.StreamDictionary.TryGet(NameToken.Range, out ArrayToken arrayToken)) { range = arrayToken.Data.OfType().Select(x => x.Data).ToList(); } + // Metadata is optional XmpMetadata metadata = null; if (streamToken.StreamDictionary.TryGet(NameToken.Metadata, out StreamToken metadataStream)) { diff --git a/src/UglyToad.PdfPig/Util/Matrix3x3.cs b/src/UglyToad.PdfPig/Util/Matrix3x3.cs new file mode 100644 index 00000000..6b588bcf --- /dev/null +++ b/src/UglyToad.PdfPig/Util/Matrix3x3.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace UglyToad.PdfPig.Util +{ + internal class Matrix3x3 : IEnumerable, IEquatable + { + /// + /// The identity matrix. The result of multiplying a matrix with + /// the identity matrix is the matrix itself. + /// + public static readonly Matrix3x3 Identity = new Matrix3x3( + 1, 0, 0, + 0, 1, 0, + 0, 0, 1); + + private readonly double m11; + private readonly double m12; + private readonly double m13; + + private readonly double m21; + private readonly double m22; + private readonly double m23; + + private readonly double m31; + private readonly double m32; + private readonly double m33; + + /// + /// Creates a 3x3 matrix with the following layout: + /// + /// | m11 m12 m13 | + /// | m21 m22 m23 | + /// | m31 m32 m33 | + /// + /// + public Matrix3x3(double m11, double m12, double m13, double m21, double m22, double m23, double m31, double m32, double m33) + { + this.m11 = m11; + this.m12 = m12; + this.m13 = m13; + + this.m21 = m21; + this.m22 = m22; + this.m23 = m23; + + this.m31 = m31; + this.m32 = m32; + this.m33 = m33; + } + + public IEnumerator GetEnumerator() + { + yield return m11; + yield return m12; + yield return m13; + yield return m21; + yield return m22; + yield return m23; + yield return m31; + yield return m32; + yield return m33; + } + + /// + /// Returns a new matrix that is the inverse of this matrix (i.e. multiplying a matrix with + /// its inverse matrix yields the identity matrix). + /// + /// If an inverse matrix does not exist, null is returned. + /// + public Matrix3x3 Inverse() + { + var determinant = GetDeterminant(); + + // No inverse matrix exists when determinant is zero + if (determinant == 0) + { + return null; + } + + var transposed = Transpose(); + var minorm11 = (transposed.m22 * transposed.m33) - (transposed.m23 * transposed.m32); + var minorm12 = (transposed.m21 * transposed.m33) - (transposed.m23 * transposed.m31); + var minorm13 = (transposed.m21 * transposed.m32) - (transposed.m22 * transposed.m31); + + var minorm21 = (transposed.m12 * transposed.m33) - (transposed.m13 * transposed.m32); + var minorm22 = (transposed.m11 * transposed.m33) - (transposed.m13 * transposed.m31); + var minorm23 = (transposed.m11 * transposed.m32) - (transposed.m12 * transposed.m31); + + var minorm31 = (transposed.m12 * transposed.m23) - (transposed.m13 * transposed.m22); + var minorm32 = (transposed.m11 * transposed.m23) - (transposed.m13 * transposed.m21); + var minorm33 = (transposed.m11 * transposed.m22) - (transposed.m12 * transposed.m21); + + var adjugate = new Matrix3x3( + minorm11, -minorm12, minorm13, + -minorm21, minorm22, -minorm23, + minorm31, -minorm32, minorm33); + + return adjugate.Multiply(1 / determinant); + } + + /// + /// Returns a new matrix with each element being a mulitple of the supplied factor. + /// + public Matrix3x3 Multiply(double factor) + { + return new Matrix3x3( + m11 * factor, m12 * factor, m13 * factor, + m21 * factor, m22 * factor, m23 * factor, + m31 * factor, m32 * factor, m33 * factor); + } + + /// + /// Multiplies this matrix with the supplied 3-element vector + /// and returns a new 3-element vector as the result. + /// + public (double, double, double) Multiply((double, double, double) vector) + { + return ( + m11 * vector.Item1 + m12 * vector.Item2 + m13 * vector.Item3, + m21 * vector.Item1 + m22 * vector.Item2 + m23 * vector.Item3, + m31 * vector.Item1 + m32 * vector.Item2 + m33 * vector.Item3); + } + + /// + /// Returns a new matrix that is the 'dot product' of this matrix + /// and the supplied matrix. + /// + public Matrix3x3 Multiply(Matrix3x3 matrix) + { + return new Matrix3x3( + m11 * matrix.m11 + m12 * matrix.m21 + m13 * matrix.m31, + m11 * matrix.m12 + m12 * matrix.m22 + m13 * matrix.m32, + m11 * matrix.m13 + m12 * matrix.m23 + m13 * matrix.m33, + m21 * matrix.m11 + m22 * matrix.m21 + m23 * matrix.m31, + m21 * matrix.m12 + m22 * matrix.m22 + m23 * matrix.m32, + m21 * matrix.m13 + m22 * matrix.m23 + m23 * matrix.m33, + m31 * matrix.m11 + m32 * matrix.m21 + m33 * matrix.m31, + m31 * matrix.m12 + m32 * matrix.m22 + m33 * matrix.m32, + m31 * matrix.m13 + m32 * matrix.m23 + m33 * matrix.m33); + } + + /// + /// Returns a new matrix that is the transpose of this matrix + /// (i.e. the tranpose of a matrix, is a matrix with its rows + /// and column interchanged) + /// + public Matrix3x3 Transpose() + { + return new Matrix3x3( + m11, m21, m31, + m12, m22, m32, + m13, m23, m33); + } + + public override bool Equals(object obj) + { + var other = obj as Matrix3x3; + return Equals(other); + } + + public bool Equals(Matrix3x3 other) + { + if (other == null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return + m11 == other.m11 && + m12 == other.m12 && + m13 == other.m13 && + m21 == other.m21 && + m22 == other.m22 && + m23 == other.m23 && + m31 == other.m31 && + m32 == other.m32 && + m33 == other.m33; + } + + public override int GetHashCode() + { + return (m11, m12, m13, m21, m22, m23, m31, m32, m33).GetHashCode(); + } + + private double GetDeterminant() + { + var minorM11 = (m22 * m33) - (m23 * m32); + var minorM12 = (m21 * m33) - (m23 * m31); + var minorM13 = (m21 * m32) - (m22 * m31); + + return m11 * minorM11 - m12 * minorM12 + m13 * minorM13; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +}