mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-06-28 15:04:04 +08:00
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:
commit
14276d5d16
BIN
src/UglyToad.PdfPig.Tests/Images/Files/calgray-decoded.bin
Normal file
BIN
src/UglyToad.PdfPig.Tests/Images/Files/calgray-decoded.bin
Normal file
Binary file not shown.
BIN
src/UglyToad.PdfPig.Tests/Images/Files/calgray.png
Normal file
BIN
src/UglyToad.PdfPig.Tests/Images/Files/calgray.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
BIN
src/UglyToad.PdfPig.Tests/Images/Files/calrgb-decoded.bin
Normal file
BIN
src/UglyToad.PdfPig.Tests/Images/Files/calrgb-decoded.bin
Normal file
Binary file not shown.
BIN
src/UglyToad.PdfPig.Tests/Images/Files/calrgb.png
Normal file
BIN
src/UglyToad.PdfPig.Tests/Images/Files/calrgb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1 @@
|
||||
h・m
|
BIN
src/UglyToad.PdfPig.Tests/Images/Files/iccbased.png
Normal file
BIN
src/UglyToad.PdfPig.Tests/Images/Files/iccbased.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 B |
@ -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)
|
||||
|
@ -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<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)
|
||||
{
|
||||
return ImageHelpers.LoadFileBytes(name);
|
||||
|
@ -105,9 +105,12 @@
|
||||
"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",
|
||||
"UglyToad.PdfPig.Graphics.Colors.ICCBasedColorSpaceDetails",
|
||||
"UglyToad.PdfPig.Graphics.Colors.IndexedColorSpaceDetails",
|
||||
"UglyToad.PdfPig.Graphics.Colors.SeparationColorSpaceDetails",
|
||||
"UglyToad.PdfPig.Graphics.Colors.UnsupportedColorSpaceDetails",
|
||||
|
@ -6,6 +6,7 @@
|
||||
using UglyToad.PdfPig.Graphics.Colors;
|
||||
using UglyToad.PdfPig.Graphics.Core;
|
||||
using UglyToad.PdfPig.Images.Png;
|
||||
using UglyToad.PdfPig.Tokens;
|
||||
|
||||
public class TestPdfImage : IPdfImage
|
||||
{
|
||||
@ -31,6 +32,8 @@
|
||||
|
||||
public bool IsInlineImage { get; set; }
|
||||
|
||||
public DictionaryToken ImageDictionary { get; set; }
|
||||
|
||||
public ColorSpaceDetails ColorSpaceDetails { get; set; }
|
||||
|
||||
public IReadOnlyList<byte> DecodedBytes { get; set; }
|
||||
|
121
src/UglyToad.PdfPig.Tests/Util/Matrix3x3Tests.cs
Normal file
121
src/UglyToad.PdfPig.Tests/Util/Matrix3x3Tests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -102,6 +102,16 @@
|
||||
/// <returns>A new <see cref="DictionaryToken"/> with the entry created or modified.</returns>
|
||||
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);
|
||||
|
||||
foreach (var keyValuePair in Data)
|
||||
@ -114,6 +124,35 @@
|
||||
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>
|
||||
/// Create a new <see cref="DictionaryToken"/>.
|
||||
/// </summary>
|
||||
|
@ -3,7 +3,8 @@
|
||||
using System.Collections.Generic;
|
||||
using Core;
|
||||
using Graphics.Colors;
|
||||
using Graphics.Core;
|
||||
using Graphics.Core;
|
||||
using UglyToad.PdfPig.Tokens;
|
||||
using XObjects;
|
||||
|
||||
/// <summary>
|
||||
@ -84,6 +85,11 @@
|
||||
/// </summary>
|
||||
bool IsInlineImage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full dictionary for this image object.
|
||||
/// </summary>
|
||||
DictionaryToken ImageDictionary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Full details for the <see cref="ColorSpace"/> with any associated data.
|
||||
/// </summary>
|
||||
|
@ -8,8 +8,9 @@
|
||||
using Graphics.Colors;
|
||||
using Graphics.Core;
|
||||
using Tokens;
|
||||
using Images.Png;
|
||||
|
||||
using Images.Png;
|
||||
using UglyToad.PdfPig.Util.JetBrains.Annotations;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// 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 />
|
||||
public bool IsInlineImage { get; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
[NotNull]
|
||||
public DictionaryToken ImageDictionary { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RenderingIntent RenderingIntent { get; }
|
||||
|
||||
@ -76,6 +81,7 @@
|
||||
IsImageMask = isImageMask;
|
||||
RenderingIntent = renderingIntent;
|
||||
Interpolate = interpolate;
|
||||
ImageDictionary = streamDictionary;
|
||||
|
||||
RawBytes = bytes;
|
||||
ColorSpaceDetails = colorSpaceDetails;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
66
src/UglyToad.PdfPig/Graphics/Colors/ChromaticAdaptation.cs
Normal file
66
src/UglyToad.PdfPig/Graphics/Colors/ChromaticAdaptation.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
namespace UglyToad.PdfPig.Graphics.Colors
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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>
|
||||
/// Contains more document-specific information about the <see cref="ColorSpace"/>.
|
||||
/// </summary>
|
||||
@ -85,23 +89,15 @@
|
||||
public class IndexedColorSpaceDetails : ColorSpaceDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// A color space useful for extracting stencil masks as black-and-white images.
|
||||
/// Index 0 is black and index 1 is white.
|
||||
/// </summary>
|
||||
internal static readonly IndexedColorSpaceDetails StencilBlackIs0
|
||||
= new IndexedColorSpaceDetails(DeviceGrayColorSpaceDetails.Instance, 1, new byte[] { 0, 255 });
|
||||
|
||||
/// <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)
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -185,6 +181,248 @@
|
||||
AlternateColorSpaceDetails = alternateColorSpaceDetails;
|
||||
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>
|
||||
|
178
src/UglyToad.PdfPig/Graphics/Colors/RGBWorkingSpace.cs
Normal file
178
src/UglyToad.PdfPig/Graphics/Colors/RGBWorkingSpace.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,8 +5,8 @@
|
||||
using System.Linq;
|
||||
using Content;
|
||||
using Core;
|
||||
using Graphics.Colors;
|
||||
|
||||
using Graphics.Colors;
|
||||
|
||||
/// <summary>
|
||||
/// Utility for working with the bytes in <see cref="IPdfImage"/>s and converting according to their <see cref="ColorSpaceDetails"/>.s
|
||||
/// </summary>
|
||||
@ -28,48 +28,92 @@
|
||||
if (details == null)
|
||||
{
|
||||
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:
|
||||
if (bitsPerComponent != 8)
|
||||
{
|
||||
// To ease unwrapping further below the indices are unpacked to occupy a single byte each
|
||||
decoded = UnpackIndices(decoded, bitsPerComponent);
|
||||
|
||||
// Remove padding bytes when the stride width differs from the image width
|
||||
var stride = (imageWidth * bitsPerComponent + 7) / 8;
|
||||
var strideWidth = stride * (8 / bitsPerComponent);
|
||||
if (strideWidth != imageWidth)
|
||||
{
|
||||
decoded = RemoveStridePadding(decoded.ToArray(), strideWidth, imageWidth, imageHeight);
|
||||
}
|
||||
}
|
||||
|
||||
return UnwrapIndexedColorSpaceBytes(indexed, decoded);
|
||||
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();
|
||||
}
|
||||
|
||||
private static byte[] UnpackIndices(IReadOnlyList<byte> input, int bitsPerComponent)
|
||||
{
|
||||
IEnumerable<byte> Unpack(byte b)
|
||||
private static int GetBytesPerPixel(ColorSpaceDetails details)
|
||||
{
|
||||
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
|
||||
for (int i = 8 - bitsPerComponent; i >= 0; i -= bitsPerComponent)
|
||||
{
|
||||
yield return (byte)((b >> i) & ((int)Math.Pow(2, bitsPerComponent) - 1));
|
||||
}
|
||||
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++)
|
||||
{
|
||||
int sourceIndex = y * strideWidth;
|
||||
@ -80,13 +124,42 @@
|
||||
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)
|
||||
{
|
||||
var multiplier = 1;
|
||||
Func<byte, IEnumerable<byte>> transformer = null;
|
||||
switch (indexed.BaseColorSpaceDetails.Type)
|
||||
switch (indexed.BaseType)
|
||||
{
|
||||
case ColorSpace.DeviceRGB:
|
||||
case ColorSpace.CalRGB:
|
||||
transformer = x =>
|
||||
{
|
||||
var r = new byte[3];
|
||||
@ -114,6 +187,7 @@
|
||||
multiplier = 4;
|
||||
break;
|
||||
case ColorSpace.DeviceGray:
|
||||
case ColorSpace.CalGray:
|
||||
transformer = x => new[] { indexed.ColorTable[x] };
|
||||
multiplier = 1;
|
||||
break;
|
||||
@ -136,5 +210,11 @@
|
||||
|
||||
return input.ToArray();
|
||||
}
|
||||
|
||||
private static byte ConvertToByte(decimal componentValue)
|
||||
{
|
||||
var rounded = Math.Round(componentValue * 255, MidpointRounding.AwayFromZero);
|
||||
return (byte)rounded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{
|
||||
using Content;
|
||||
using Graphics.Colors;
|
||||
using UglyToad.PdfPig.Core;
|
||||
|
||||
internal static class PngFromPdfImageFactory
|
||||
{
|
||||
@ -15,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))
|
||||
{
|
||||
@ -27,12 +29,27 @@
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -64,6 +64,7 @@
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static class ColorSpaceDetailsParser
|
||||
@ -78,10 +79,18 @@
|
||||
if (imageDictionary.GetObjectOrDefault(NameToken.ImageMask, NameToken.Im) != null ||
|
||||
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
|
||||
?? new ArrayToken(EmptyArray<IToken>.Instance);
|
||||
var decode = decodeRaw.Data.OfType<NumericToken>().Select(x => x.Data).ToArray();
|
||||
return IndexedColorSpaceDetails.Stencil(decode);
|
||||
|
||||
return IndexedColorSpaceDetails.Stencil(colorSpaceDetails, decode);
|
||||
}
|
||||
|
||||
if (!colorSpace.HasValue)
|
||||
@ -98,22 +107,159 @@
|
||||
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, 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:
|
||||
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:
|
||||
return UnsupportedColorSpaceDetails.Instance;
|
||||
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:
|
||||
{
|
||||
if (cannotRecurse)
|
||||
{
|
||||
return UnsupportedColorSpaceDetails.Instance;
|
||||
}
|
||||
|
||||
if (!imageDictionary.TryGet(NameToken.ColorSpace, scanner, out ArrayToken colorSpaceArray)
|
||||
|| colorSpaceArray.Length != 4)
|
||||
}
|
||||
|
||||
if (!TryGetColorSpaceArray(imageDictionary, resourceStore, scanner, out var colorSpaceArray)
|
||||
|| colorSpaceArray.Length != 4)
|
||||
{
|
||||
// Error instead?
|
||||
return UnsupportedColorSpaceDetails.Instance;
|
||||
@ -122,7 +268,7 @@
|
||||
var first = colorSpaceArray[0] as NameToken;
|
||||
|
||||
if (first == null || !ColorSpaceMapper.TryMap(first, resourceStore, out var innerColorSpace)
|
||||
|| innerColorSpace != ColorSpace.Indexed)
|
||||
|| innerColorSpace != ColorSpace.Indexed)
|
||||
{
|
||||
return UnsupportedColorSpaceDetails.Instance;
|
||||
}
|
||||
@ -206,8 +352,8 @@
|
||||
return UnsupportedColorSpaceDetails.Instance;
|
||||
case ColorSpace.Separation:
|
||||
{
|
||||
if (!imageDictionary.TryGet(NameToken.ColorSpace, scanner, out ArrayToken colorSpaceArray)
|
||||
|| colorSpaceArray.Length != 4)
|
||||
if (!TryGetColorSpaceArray(imageDictionary, resourceStore, scanner, out var colorSpaceArray)
|
||||
|| colorSpaceArray.Length != 4)
|
||||
{
|
||||
// Error instead?
|
||||
return UnsupportedColorSpaceDetails.Instance;
|
||||
@ -282,7 +428,23 @@
|
||||
return UnsupportedColorSpaceDetails.Instance;
|
||||
default:
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
206
src/UglyToad.PdfPig/Util/Matrix3x3.cs
Normal file
206
src/UglyToad.PdfPig/Util/Matrix3x3.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@
|
||||
using Core;
|
||||
using Graphics.Colors;
|
||||
using Graphics.Core;
|
||||
using Images;
|
||||
using Images.Png;
|
||||
using Tokens;
|
||||
using Util.JetBrains.Annotations;
|
||||
@ -57,9 +56,7 @@
|
||||
/// <inheritdoc />
|
||||
public bool IsInlineImage { get; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The full dictionary for this Image XObject.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[NotNull]
|
||||
public DictionaryToken ImageDictionary { get; }
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user