Merge pull request #331 from kasperdaff/various-image-fixes

Various image related fixes and ICCBased (limited), CalGray and CalRGB color spaces support
This commit is contained in:
Eliot Jones 2021-06-01 08:14:49 -04:00 committed by GitHub
commit 14276d5d16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1423 additions and 78 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1 @@
h・m

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -2,15 +2,28 @@
{ {
using System; using System;
using System.IO; using System.IO;
using System.IO.Compression;
using UglyToad.PdfPig.Images.Png; using UglyToad.PdfPig.Images.Png;
public static class ImageHelpers public static class ImageHelpers
{ {
private static readonly string FilesFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Images", "Files")); 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) public static bool ImagesAreEqual(byte[] first, byte[] second)

View File

@ -1,5 +1,6 @@
namespace UglyToad.PdfPig.Tests.Images namespace UglyToad.PdfPig.Tests.Images
{ {
using System.Collections.Generic;
using System.Linq; using System.Linq;
using UglyToad.PdfPig.Graphics.Colors; using UglyToad.PdfPig.Graphics.Colors;
using UglyToad.PdfPig.Images.Png; using UglyToad.PdfPig.Images.Png;
@ -150,7 +151,7 @@
var decodedBytes = ImageHelpers.LoadFileBytes("ccittfax-decoded.bin"); var decodedBytes = ImageHelpers.LoadFileBytes("ccittfax-decoded.bin");
var image = new TestPdfImage var image = new TestPdfImage
{ {
ColorSpaceDetails = IndexedColorSpaceDetails.StencilBlackIs1, ColorSpaceDetails = IndexedColorSpaceDetails.Stencil(DeviceGrayColorSpaceDetails.Instance, new[] { 1m, 0 }),
DecodedBytes = decodedBytes, DecodedBytes = decodedBytes,
WidthInSamples = 1800, WidthInSamples = 1800,
HeightInSamples = 3113, HeightInSamples = 3113,
@ -161,6 +162,103 @@
Assert.True(ImageHelpers.ImagesAreEqual(LoadImage("ccittfax.png"), bytes)); 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<decimal> { 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<decimal> { 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<decimal> { 0.95043m, 1, 1.09m },
blackPoint: null,
gamma: new List<decimal> { 2.2m, 2.2m, 2.2m },
matrix: new List<decimal> {
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<decimal> { 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) private static byte[] LoadImage(string name)
{ {
return ImageHelpers.LoadFileBytes(name); return ImageHelpers.LoadFileBytes(name);

View File

@ -105,9 +105,12 @@
"UglyToad.PdfPig.Graphics.Colors.IColor", "UglyToad.PdfPig.Graphics.Colors.IColor",
"UglyToad.PdfPig.Graphics.Colors.RGBColor", "UglyToad.PdfPig.Graphics.Colors.RGBColor",
"UglyToad.PdfPig.Graphics.Colors.ColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.ColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.CalGrayColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.CalRGBColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.DeviceGrayColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.DeviceGrayColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.DeviceRgbColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.DeviceRgbColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.DeviceCmykColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.DeviceCmykColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.ICCBasedColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.IndexedColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.IndexedColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.SeparationColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.SeparationColorSpaceDetails",
"UglyToad.PdfPig.Graphics.Colors.UnsupportedColorSpaceDetails", "UglyToad.PdfPig.Graphics.Colors.UnsupportedColorSpaceDetails",

View File

@ -6,6 +6,7 @@
using UglyToad.PdfPig.Graphics.Colors; using UglyToad.PdfPig.Graphics.Colors;
using UglyToad.PdfPig.Graphics.Core; using UglyToad.PdfPig.Graphics.Core;
using UglyToad.PdfPig.Images.Png; using UglyToad.PdfPig.Images.Png;
using UglyToad.PdfPig.Tokens;
public class TestPdfImage : IPdfImage public class TestPdfImage : IPdfImage
{ {
@ -31,6 +32,8 @@
public bool IsInlineImage { get; set; } public bool IsInlineImage { get; set; }
public DictionaryToken ImageDictionary { get; set; }
public ColorSpaceDetails ColorSpaceDetails { get; set; } public ColorSpaceDetails ColorSpaceDetails { get; set; }
public IReadOnlyList<byte> DecodedBytes { get; set; } public IReadOnlyList<byte> DecodedBytes { get; set; }

View File

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

View File

@ -102,6 +102,16 @@
/// <returns>A new <see cref="DictionaryToken"/> with the entry created or modified.</returns> /// <returns>A new <see cref="DictionaryToken"/> with the entry created or modified.</returns>
public DictionaryToken With(string key, IToken value) 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<string, IToken>(Data.Count + 1); var result = new Dictionary<string, IToken>(Data.Count + 1);
foreach (var keyValuePair in Data) foreach (var keyValuePair in Data)
@ -114,6 +124,35 @@
return new DictionaryToken(result); return new DictionaryToken(result);
} }
/// <summary>
/// Creates a copy of this dictionary with the entry with the specified key removed (if it exists).
/// </summary>
/// <param name="key">The key of the entry to remove.</param>
/// <returns>A new <see cref="DictionaryToken"/> with the entry removed.</returns>
public DictionaryToken Without(NameToken key) => Without(key.Data);
/// <summary>
/// Creates a copy of this dictionary with the entry with the specified key removed (if it exists).
/// </summary>
/// <param name="key">The key of the entry to remove.</param>
/// <returns>A new <see cref="DictionaryToken"/> with the entry removed.</returns>
public DictionaryToken Without(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
var result = new Dictionary<string, IToken>(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);
}
/// <summary> /// <summary>
/// Create a new <see cref="DictionaryToken"/>. /// Create a new <see cref="DictionaryToken"/>.
/// </summary> /// </summary>

View File

@ -3,7 +3,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using Core; using Core;
using Graphics.Colors; using Graphics.Colors;
using Graphics.Core; using Graphics.Core;
using UglyToad.PdfPig.Tokens;
using XObjects; using XObjects;
/// <summary> /// <summary>
@ -84,6 +85,11 @@
/// </summary> /// </summary>
bool IsInlineImage { get; } bool IsInlineImage { get; }
/// <summary>
/// The full dictionary for this image object.
/// </summary>
DictionaryToken ImageDictionary { get; }
/// <summary> /// <summary>
/// Full details for the <see cref="ColorSpace"/> with any associated data. /// Full details for the <see cref="ColorSpace"/> with any associated data.
/// </summary> /// </summary>

View File

@ -8,8 +8,9 @@
using Graphics.Colors; using Graphics.Colors;
using Graphics.Core; using Graphics.Core;
using Tokens; using Tokens;
using Images.Png; using Images.Png;
using UglyToad.PdfPig.Util.JetBrains.Annotations;
/// <inheritdoc /> /// <inheritdoc />
/// <summary> /// <summary>
/// A small image that is completely defined directly inline within a <see cref="T:UglyToad.PdfPig.Content.Page" />'s content stream. /// A small image that is completely defined directly inline within a <see cref="T:UglyToad.PdfPig.Content.Page" />'s content stream.
@ -42,6 +43,10 @@
/// <inheritdoc /> /// <inheritdoc />
public bool IsInlineImage { get; } = true; public bool IsInlineImage { get; } = true;
/// <inheritdoc />
[NotNull]
public DictionaryToken ImageDictionary { get; }
/// <inheritdoc /> /// <inheritdoc />
public RenderingIntent RenderingIntent { get; } public RenderingIntent RenderingIntent { get; }
@ -76,6 +81,7 @@
IsImageMask = isImageMask; IsImageMask = isImageMask;
RenderingIntent = renderingIntent; RenderingIntent = renderingIntent;
Interpolate = interpolate; Interpolate = interpolate;
ImageDictionary = streamDictionary;
RawBytes = bytes; RawBytes = bytes;
ColorSpaceDetails = colorSpaceDetails; ColorSpaceDetails = colorSpaceDetails;

View File

@ -0,0 +1,111 @@
namespace UglyToad.PdfPig.Graphics.Colors
{
using System;
using UglyToad.PdfPig.Util;
/// <summary>
/// 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
/// </summary>
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();
}
/// <summary>
/// Transforms the supplied ABC color to the RGB color of the <see cref="RGBWorkingSpace"/>
/// that was supplied to this <see cref="CIEBasedColorSpaceTransformer"/> as the destination
/// workspace.
/// A, B and C represent red, green and blue calibrated color values in the range 0 to 1.
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,66 @@
namespace UglyToad.PdfPig.Graphics.Colors
{
using UglyToad.PdfPig.Util;
/// <summary>
/// Encapsulates the algorithm for chromatic adaptation described here:
/// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
/// </summary>
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);
}
}
}
}

View File

@ -1,10 +1,14 @@
namespace UglyToad.PdfPig.Graphics.Colors namespace UglyToad.PdfPig.Graphics.Colors
{ {
using System;
using System.Collections.Generic;
using PdfPig.Core; using PdfPig.Core;
using Tokens; using System;
using System.Collections.Generic;
using System.Linq;
using Tokens;
using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.Util;
using UglyToad.PdfPig.Util.JetBrains.Annotations;
/// <summary> /// <summary>
/// Contains more document-specific information about the <see cref="ColorSpace"/>. /// Contains more document-specific information about the <see cref="ColorSpace"/>.
/// </summary> /// </summary>
@ -85,23 +89,15 @@
public class IndexedColorSpaceDetails : ColorSpaceDetails public class IndexedColorSpaceDetails : ColorSpaceDetails
{ {
/// <summary> /// <summary>
/// A color space useful for extracting stencil masks as black-and-white images. /// Creates a indexed color space useful for exracting stencil masks as black-and-white images,
/// Index 0 is black and index 1 is white. /// i.e. with a color palette of two colors (black and white). If the decode parameter array is
/// </summary> /// [0, 1] it indicates that black is at index 0 in the color palette, whereas [1, 0] indicates
internal static readonly IndexedColorSpaceDetails StencilBlackIs0 /// that the black color is at index 1.
= new IndexedColorSpaceDetails(DeviceGrayColorSpaceDetails.Instance, 1, new byte[] { 0, 255 }); /// </summary>
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, decimal[] decode)
/// <summary>
/// A color space useful for extracting stencil masks as black-and-white images.
/// Index 0 is white and index 1 is black.
/// </summary>
internal static readonly IndexedColorSpaceDetails StencilBlackIs1
= new IndexedColorSpaceDetails(DeviceGrayColorSpaceDetails.Instance, 1, new byte[] { 255, 0 });
internal static ColorSpaceDetails Stencil(decimal[] decode)
{ {
return decode.Length >= 2 && decode[0] == 1 && decode[1] == 0 ? var blackIsOne = decode.Length >= 2 && decode[0] == 1 && decode[1] == 0;
StencilBlackIs1 : StencilBlackIs0 /* default */; return new IndexedColorSpaceDetails(colorSpaceDetails, 1, blackIsOne ? new byte[] { 255, 0 } : new byte[] { 0, 255 });
} }
/// <summary> /// <summary>
@ -185,6 +181,248 @@
AlternateColorSpaceDetails = alternateColorSpaceDetails; AlternateColorSpaceDetails = alternateColorSpaceDetails;
TintFunction = tintFunction; TintFunction = tintFunction;
} }
}
/// <summary>
/// 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.
/// </summary>
public class CalGrayColorSpaceDetails : ColorSpaceDetails
{
private readonly CIEBasedColorSpaceTransformer colorSpaceTransformer;
/// <summary>
/// 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.
/// </summary>
public IReadOnlyList<decimal> WhitePoint { get; }
/// <summary>
/// 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].
/// </summary>
public IReadOnlyList<decimal> BlackPoint { get; }
/// <summary>
/// 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.
/// </summary>
public decimal Gamma { get; }
/// <summary>
/// Create a new <see cref="CalGrayColorSpaceDetails"/>.
/// </summary>
public CalGrayColorSpaceDetails([NotNull] IReadOnlyList<decimal> whitePoint, [CanBeNull] IReadOnlyList<decimal> 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])
};
}
/// <summary>
/// Transforms the supplied A color to grayscale RGB (sRGB) using the propties of this
/// <see cref="CalGrayColorSpaceDetails"/> 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.
/// </summary>
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);
}
}
/// <summary>
/// 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.
/// </summary>
public class CalRGBColorSpaceDetails : ColorSpaceDetails
{
private readonly CIEBasedColorSpaceTransformer colorSpaceTransformer;
/// <summary>
/// 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.
/// </summary>
public IReadOnlyList<decimal> WhitePoint { get; }
/// <summary>
/// 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].
/// </summary>
public IReadOnlyList<decimal> BlackPoint { get; }
/// <summary>
/// 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].
/// </summary>
public IReadOnlyList<decimal> Gamma { get; }
/// <summary>
/// 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].
/// </summary>
public IReadOnlyList<decimal> Matrix { get; }
/// <summary>
/// Create a new <see cref="CalRGBColorSpaceDetails"/>.
/// </summary>
public CalRGBColorSpaceDetails([NotNull] IReadOnlyList<decimal> whitePoint, [CanBeNull] IReadOnlyList<decimal> blackPoint, [CanBeNull] IReadOnlyList<decimal> gamma, [CanBeNull] IReadOnlyList<decimal> 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])
};
}
/// <summary>
/// Transforms the supplied ABC color to RGB (sRGB) using the propties of this <see cref="CalRGBColorSpaceDetails"/>
/// in the transformation process.
/// A, B and C represent red, green and blue calibrated color values in the range 0.0 to 1.0.
/// </summary>
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);
}
}
/// <summary>
/// The ICCBased color space is one of the CIE-based color spaces supported in PDFs. These color spaces
/// 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 <see cref="AlternateColorSpaceDetails"/>.
/// </summary>
public class ICCBasedColorSpaceDetails : ColorSpaceDetails
{
/// <summary>
/// The number of color components in the color space described by the ICC profile data.
/// This numbers shall match the number of components actually in the ICC profile.
/// Valid values are 1, 3 and 4.
/// </summary>
public int NumberOfColorComponents { get; }
/// <summary>
/// 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
/// valid color space (except a Pattern color space). If this property isn't explicitly set during
/// construction, it will assume one of the color spaces, DeviceGray, DeviceRGB or DeviceCMYK depending
/// on whether the value of <see cref="NumberOfColorComponents"/> is 1, 3 or respectively.
///
/// Conversion of the source color values should not be performed when using the alternate color space.
/// Color values within the range of the ICCBased color space might not be within the range of the
/// alternate color space. In this case, the nearest values within the range of the alternate space
/// must be substituted.
/// </summary>
[NotNull]
public ColorSpaceDetails AlternateColorSpaceDetails { get; }
/// <summary>
/// A list of 2 x <see cref="NumberOfColorComponents"/> numbers [min0 max0 min1 max1 ...] that
/// specifies the minimum and maximum valid values of the corresponding color components. These
/// values must match the information in the ICC profile. Default value: [0.0 1.0 0.0 1.0 ...].
/// </summary>
[NotNull]
public IReadOnlyList<decimal> Range { get; }
/// <summary>
/// An optional metadata stream that contains metadata for the color space.
/// </summary>
[CanBeNull]
public XmpMetadata Metadata { get; }
/// <summary>
/// Create a new <see cref="ICCBasedColorSpaceDetails"/>.
/// </summary>
internal ICCBasedColorSpaceDetails(int numberOfColorComponents, [CanBeNull] ColorSpaceDetails alternateColorSpaceDetails,
[CanBeNull] IReadOnlyList<decimal> range, [CanBeNull] XmpMetadata metadata)
: base(ColorSpace.ICCBased)
{
if (numberOfColorComponents != 1 && numberOfColorComponents != 3 && numberOfColorComponents != 4)
{
throw new ArgumentOutOfRangeException(nameof(numberOfColorComponents), "must be 1, 3 or 4");
}
NumberOfColorComponents = numberOfColorComponents;
AlternateColorSpaceDetails = alternateColorSpaceDetails ??
(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;
}
} }
/// <summary> /// <summary>

View File

@ -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<double, double> 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<double, double> 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);
}
}
}

View File

@ -5,8 +5,8 @@
using System.Linq; using System.Linq;
using Content; using Content;
using Core; using Core;
using Graphics.Colors; using Graphics.Colors;
/// <summary> /// <summary>
/// Utility for working with the bytes in <see cref="IPdfImage"/>s and converting according to their <see cref="ColorSpaceDetails"/>.s /// Utility for working with the bytes in <see cref="IPdfImage"/>s and converting according to their <see cref="ColorSpaceDetails"/>.s
/// </summary> /// </summary>
@ -28,48 +28,92 @@
if (details == null) if (details == null)
{ {
return decoded.ToArray(); return decoded.ToArray();
}
if (bitsPerComponent != 8)
{
// Unpack components such that they occupy one byte each
decoded = UnpackComponents(decoded, bitsPerComponent);
}
// Remove padding bytes when the stride width differs from the image width
var bytesPerPixel = details is IndexedColorSpaceDetails ? 1 : GetBytesPerPixel(details);
var strideWidth = decoded.Count / imageHeight / bytesPerPixel;
if (strideWidth != imageWidth)
{
decoded = RemoveStridePadding(decoded.ToArray(), strideWidth, imageWidth, imageHeight, bytesPerPixel);
} }
switch (details) // In case of indexed color space images, unwrap indices to actual pixel component values
if (details is IndexedColorSpaceDetails indexed)
{ {
case IndexedColorSpaceDetails indexed: decoded = UnwrapIndexedColorSpaceBytes(indexed, decoded);
if (bitsPerComponent != 8)
{ // Use the base color space in potential further decoding
// To ease unwrapping further below the indices are unpacked to occupy a single byte each details = indexed.BaseColorSpaceDetails;
decoded = UnpackIndices(decoded, bitsPerComponent); }
// Remove padding bytes when the stride width differs from the image width if (details is CalRGBColorSpaceDetails calRgb)
var stride = (imageWidth * bitsPerComponent + 7) / 8; {
var strideWidth = stride * (8 / bitsPerComponent); decoded = TransformToRGB(calRgb, decoded);
if (strideWidth != imageWidth) }
{
decoded = RemoveStridePadding(decoded.ToArray(), strideWidth, imageWidth, imageHeight); if (details is CalGrayColorSpaceDetails calGray)
} {
} decoded = TransformToRgbGrayScale(calGray, decoded);
return UnwrapIndexedColorSpaceBytes(indexed, decoded);
} }
return decoded.ToArray(); return decoded.ToArray();
} }
private static byte[] UnpackIndices(IReadOnlyList<byte> input, int bitsPerComponent) private static int GetBytesPerPixel(ColorSpaceDetails details)
{ {
IEnumerable<byte> Unpack(byte b) switch (details)
{
case DeviceGrayColorSpaceDetails deviceGray:
return 1;
case CalGrayColorSpaceDetails calGray:
return 1;
case DeviceRgbColorSpaceDetails deviceRgb:
return 3;
case CalRGBColorSpaceDetails calRgb:
return 3;
case DeviceCmykColorSpaceDetails deviceCmyk:
return 4;
case IndexedColorSpaceDetails indexed:
return GetBytesPerPixel(indexed.BaseColorSpaceDetails);
case ICCBasedColorSpaceDetails iccBased:
// Currently PdfPig only supports the 'Alternate' color space of ICCBasedColorSpaceDetails
return GetBytesPerPixel(iccBased.AlternateColorSpaceDetails);
default:
return 1;
}
}
private static byte[] UnpackComponents(IReadOnlyList<byte> input, int bitsPerComponent)
{
IEnumerable<byte> Unpack(byte b)
{
// Enumerate bits in bitsPerComponent-sized chunks from MSB to LSB, masking on the appropriate bits
for (int i = 8 - bitsPerComponent; i >= 0; i -= bitsPerComponent)
{ {
// Enumerate bits in bitsPerComponent-sized chunks from MSB to LSB, masking on the appropriate bits yield return (byte)((b >> i) & ((int)Math.Pow(2, bitsPerComponent) - 1));
for (int i = 8 - bitsPerComponent; i >= 0; i -= bitsPerComponent)
{
yield return (byte)((b >> i) & ((int)Math.Pow(2, bitsPerComponent) - 1));
}
} }
}
return input.SelectMany(b => Unpack(b)).ToArray();
return input.SelectMany(b => Unpack(b)).ToArray();
} }
private static byte[] RemoveStridePadding(byte[] input, int strideWidth, int imageWidth, int imageHeight) private static byte[] RemoveStridePadding(byte[] input, int strideWidth, int imageWidth, int imageHeight, int multiplier)
{ {
var result = new byte[imageWidth * imageHeight]; var result = new byte[imageWidth * imageHeight * multiplier];
for (int y = 0; y < imageHeight; y++) for (int y = 0; y < imageHeight; y++)
{ {
int sourceIndex = y * strideWidth; int sourceIndex = y * strideWidth;
@ -80,13 +124,42 @@
return result; return result;
} }
private static IReadOnlyList<byte> TransformToRgbGrayScale(CalGrayColorSpaceDetails calGray, IReadOnlyList<byte> decoded)
{
var transformed = new List<byte>();
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<byte> TransformToRGB(CalRGBColorSpaceDetails calRgb, IReadOnlyList<byte> decoded)
{
var transformed = new List<byte>();
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<byte> input) private static byte[] UnwrapIndexedColorSpaceBytes(IndexedColorSpaceDetails indexed, IReadOnlyList<byte> input)
{ {
var multiplier = 1; var multiplier = 1;
Func<byte, IEnumerable<byte>> transformer = null; Func<byte, IEnumerable<byte>> transformer = null;
switch (indexed.BaseColorSpaceDetails.Type) switch (indexed.BaseType)
{ {
case ColorSpace.DeviceRGB: case ColorSpace.DeviceRGB:
case ColorSpace.CalRGB:
transformer = x => transformer = x =>
{ {
var r = new byte[3]; var r = new byte[3];
@ -114,6 +187,7 @@
multiplier = 4; multiplier = 4;
break; break;
case ColorSpace.DeviceGray: case ColorSpace.DeviceGray:
case ColorSpace.CalGray:
transformer = x => new[] { indexed.ColorTable[x] }; transformer = x => new[] { indexed.ColorTable[x] };
multiplier = 1; multiplier = 1;
break; break;
@ -136,5 +210,11 @@
return input.ToArray(); return input.ToArray();
} }
private static byte ConvertToByte(decimal componentValue)
{
var rounded = Math.Round(componentValue * 255, MidpointRounding.AwayFromZero);
return (byte)rounded;
}
} }
} }

View File

@ -2,6 +2,7 @@
{ {
using Content; using Content;
using Graphics.Colors; using Graphics.Colors;
using UglyToad.PdfPig.Core;
internal static class PngFromPdfImageFactory internal static class PngFromPdfImageFactory
{ {
@ -15,7 +16,8 @@
var isColorSpaceSupported = var isColorSpaceSupported =
actualColorSpace == ColorSpace.DeviceGray || actualColorSpace == ColorSpace.DeviceRGB 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)) if (!isColorSpaceSupported || !image.TryGetBytes(out var bytesPure))
{ {
@ -27,12 +29,27 @@
bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure, bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure,
image.BitsPerComponent, image.WidthInSamples, image.HeightInSamples); 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 is3Byte = numberOfComponents == 3;
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false); var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false);
var isCorrectlySized = bytesPure.Count == (image.WidthInSamples * image.HeightInSamples * numberOfComponents); var requiredSize = (image.WidthInSamples * image.HeightInSamples * numberOfComponents);
var actualSize = bytesPure.Count;
var isCorrectlySized = bytesPure.Count == requiredSize ||
// Spec, p. 37: "...error if the stream contains too much data, with the exception that
// there may be an extra end-of-line marker..."
(actualSize == requiredSize + 1 && bytesPure[actualSize - 1] == ReadHelper.AsciiLineFeed) ||
(actualSize == requiredSize + 1 && bytesPure[actualSize - 1] == ReadHelper.AsciiCarriageReturn) ||
// The combination of a CARRIAGE RETURN followed immediately by a LINE FEED is treated as one EOL marker.
(actualSize == requiredSize + 2 &&
bytesPure[actualSize - 2] == ReadHelper.AsciiCarriageReturn &&
bytesPure[actualSize - 1] == ReadHelper.AsciiLineFeed);
if (!isCorrectlySized) if (!isCorrectlySized)
{ {

View File

@ -64,6 +64,7 @@
return false; return false;
} }
} }
internal static class ColorSpaceDetailsParser internal static class ColorSpaceDetailsParser
@ -78,10 +79,18 @@
if (imageDictionary.GetObjectOrDefault(NameToken.ImageMask, NameToken.Im) != null || if (imageDictionary.GetObjectOrDefault(NameToken.ImageMask, NameToken.Im) != null ||
filterProvider.GetFilters(imageDictionary, scanner).OfType<CcittFaxDecodeFilter>().Any()) filterProvider.GetFilters(imageDictionary, scanner).OfType<CcittFaxDecodeFilter>().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 var decodeRaw = imageDictionary.GetObjectOrDefault(NameToken.Decode, NameToken.D) as ArrayToken
?? new ArrayToken(EmptyArray<IToken>.Instance); ?? new ArrayToken(EmptyArray<IToken>.Instance);
var decode = decodeRaw.Data.OfType<NumericToken>().Select(x => x.Data).ToArray(); var decode = decodeRaw.Data.OfType<NumericToken>().Select(x => x.Data).ToArray();
return IndexedColorSpaceDetails.Stencil(decode);
return IndexedColorSpaceDetails.Stencil(colorSpaceDetails, decode);
} }
if (!colorSpace.HasValue) if (!colorSpace.HasValue)
@ -98,22 +107,159 @@
case ColorSpace.DeviceCMYK: case ColorSpace.DeviceCMYK:
return DeviceCmykColorSpaceDetails.Instance; return DeviceCmykColorSpaceDetails.Instance;
case ColorSpace.CalGray: 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, scanner, out ArrayToken whitePointToken))
{
return UnsupportedColorSpaceDetails.Instance;
}
var whitePoint = whitePointToken.Data.OfType<NumericToken>().Select(x => x.Data).ToList();
// BlackPoint is optional
IReadOnlyList<decimal> blackPoint = null;
if (dictionaryToken.TryGet(NameToken.BlackPoint, scanner, out ArrayToken blackPointToken))
{
blackPoint = blackPointToken.Data.OfType<NumericToken>().Select(x => x.Data).ToList();
}
// Gamma is optional
decimal? gamma = null;
if (dictionaryToken.TryGet(NameToken.Gamma, scanner, out NumericToken gammaToken))
{
gamma = gammaToken.Data;
}
return new CalGrayColorSpaceDetails(whitePoint, blackPoint, gamma);
}
case ColorSpace.CalRGB: 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, scanner, out ArrayToken whitePointToken))
{
return UnsupportedColorSpaceDetails.Instance;
}
var whitePoint = whitePointToken.Data.OfType<NumericToken>().Select(x => x.Data).ToList();
// BlackPoint is optional
IReadOnlyList<decimal> blackPoint = null;
if (dictionaryToken.TryGet(NameToken.BlackPoint, scanner, out ArrayToken blackPointToken))
{
blackPoint = blackPointToken.Data.OfType<NumericToken>().Select(x => x.Data).ToList();
}
// Gamma is optional
IReadOnlyList<decimal> gamma = null;
if (dictionaryToken.TryGet(NameToken.Gamma, scanner, out ArrayToken gammaToken))
{
gamma = gammaToken.Data.OfType<NumericToken>().Select(x => x.Data).ToList();
}
// Matrix is optional
IReadOnlyList<decimal> matrix = null;
if (dictionaryToken.TryGet(NameToken.Matrix, scanner, out ArrayToken matrixToken))
{
matrix = matrixToken.Data.OfType<NumericToken>().Select(x => x.Data).ToList();
}
return new CalRGBColorSpaceDetails(whitePoint, blackPoint, gamma, matrix);
}
case ColorSpace.Lab: case ColorSpace.Lab:
return UnsupportedColorSpaceDetails.Instance; return UnsupportedColorSpaceDetails.Instance;
case ColorSpace.ICCBased: case ColorSpace.ICCBased:
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.ICCBased)
{
return UnsupportedColorSpaceDetails.Instance;
}
var second = colorSpaceArray[1];
// N is required
if (!DirectObjectFinder.TryGet(second, scanner, out StreamToken streamToken) ||
!streamToken.StreamDictionary.TryGet(NameToken.N, scanner, 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))
{
alternateColorSpaceDetails =
GetColorSpaceDetails(alternateColorSpace, imageDictionary, scanner, resourceStore, filterProvider, true);
}
// Range is optional
IReadOnlyList<decimal> range = null;
if (streamToken.StreamDictionary.TryGet(NameToken.Range, scanner, out ArrayToken arrayToken))
{
range = arrayToken.Data.OfType<NumericToken>().Select(x => x.Data).ToList();
}
// Metadata is optional
XmpMetadata metadata = null;
if (streamToken.StreamDictionary.TryGet(NameToken.Metadata, scanner, out StreamToken metadataStream))
{
metadata = new XmpMetadata(metadataStream, filterProvider, scanner);
}
return new ICCBasedColorSpaceDetails(numeric.Int, alternateColorSpaceDetails, range, metadata);
}
case ColorSpace.Indexed: case ColorSpace.Indexed:
{ {
if (cannotRecurse) if (cannotRecurse)
{ {
return UnsupportedColorSpaceDetails.Instance; return UnsupportedColorSpaceDetails.Instance;
} }
if (!imageDictionary.TryGet(NameToken.ColorSpace, scanner, out ArrayToken colorSpaceArray) if (!TryGetColorSpaceArray(imageDictionary, resourceStore, scanner, out var colorSpaceArray)
|| colorSpaceArray.Length != 4) || colorSpaceArray.Length != 4)
{ {
// Error instead? // Error instead?
return UnsupportedColorSpaceDetails.Instance; return UnsupportedColorSpaceDetails.Instance;
@ -122,7 +268,7 @@
var first = colorSpaceArray[0] as NameToken; var first = colorSpaceArray[0] as NameToken;
if (first == null || !ColorSpaceMapper.TryMap(first, resourceStore, out var innerColorSpace) if (first == null || !ColorSpaceMapper.TryMap(first, resourceStore, out var innerColorSpace)
|| innerColorSpace != ColorSpace.Indexed) || innerColorSpace != ColorSpace.Indexed)
{ {
return UnsupportedColorSpaceDetails.Instance; return UnsupportedColorSpaceDetails.Instance;
} }
@ -206,8 +352,8 @@
return UnsupportedColorSpaceDetails.Instance; return UnsupportedColorSpaceDetails.Instance;
case ColorSpace.Separation: case ColorSpace.Separation:
{ {
if (!imageDictionary.TryGet(NameToken.ColorSpace, scanner, out ArrayToken colorSpaceArray) if (!TryGetColorSpaceArray(imageDictionary, resourceStore, scanner, out var colorSpaceArray)
|| colorSpaceArray.Length != 4) || colorSpaceArray.Length != 4)
{ {
// Error instead? // Error instead?
return UnsupportedColorSpaceDetails.Instance; return UnsupportedColorSpaceDetails.Instance;
@ -282,7 +428,23 @@
return UnsupportedColorSpaceDetails.Instance; return UnsupportedColorSpaceDetails.Instance;
default: default:
return UnsupportedColorSpaceDetails.Instance; return UnsupportedColorSpaceDetails.Instance;
} }
} }
private static bool TryGetColorSpaceArray(DictionaryToken imageDictionary, IResourceStore resourceStore,
IPdfTokenScanner scanner,
out ArrayToken colorSpaceArray)
{
var colorSpace = imageDictionary.GetObjectOrDefault(NameToken.ColorSpace, NameToken.Cs);
if (!DirectObjectFinder.TryGet(colorSpace, scanner, out colorSpaceArray)
&& DirectObjectFinder.TryGet(colorSpace, scanner, out NameToken colorSpaceName) &&
resourceStore.TryGetNamedColorSpace(colorSpaceName, out var colorSpaceNamedToken))
{
colorSpaceArray = colorSpaceNamedToken.Data as ArrayToken;
}
return colorSpaceArray != null;
}
} }
} }

View File

@ -0,0 +1,206 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace UglyToad.PdfPig.Util
{
internal class Matrix3x3 : IEnumerable<double>, IEquatable<Matrix3x3>
{
/// <summary>
/// The identity matrix. The result of multiplying a matrix with
/// the identity matrix is the matrix itself.
/// </summary>
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;
/// <summary>
/// Creates a 3x3 matrix with the following layout:
///
/// | m11 m12 m13 |
/// | m21 m22 m23 |
/// | m31 m32 m33 |
///
/// </summary>
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<double> 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;
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Returns a new matrix with each element being a mulitple of the supplied factor.
/// </summary>
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);
}
/// <summary>
/// Multiplies this matrix with the supplied 3-element vector
/// and returns a new 3-element vector as the result.
/// </summary>
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);
}
/// <summary>
/// Returns a new matrix that is the 'dot product' of this matrix
/// and the supplied matrix.
/// </summary>
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);
}
/// <summary>
/// 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)
/// </summary>
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();
}
}
}

View File

@ -6,7 +6,6 @@
using Core; using Core;
using Graphics.Colors; using Graphics.Colors;
using Graphics.Core; using Graphics.Core;
using Images;
using Images.Png; using Images.Png;
using Tokens; using Tokens;
using Util.JetBrains.Annotations; using Util.JetBrains.Annotations;
@ -57,9 +56,7 @@
/// <inheritdoc /> /// <inheritdoc />
public bool IsInlineImage { get; } = false; public bool IsInlineImage { get; } = false;
/// <summary> /// <inheritdoc />
/// The full dictionary for this Image XObject.
/// </summary>
[NotNull] [NotNull]
public DictionaryToken ImageDictionary { get; } public DictionaryToken ImageDictionary { get; }