From af7aefb0966633dfa7733c7cc2f07786d3710ef3 Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Mon, 24 Apr 2023 18:14:02 +0100 Subject: [PATCH] Tests - not correct - to remove --- .../Integration/ColorSpaceTests.cs | 50 ++- .../Graphics/Colors/ColorSpaceDetails.cs | 53 ++- .../Graphics/Colors/ICC/IccHelper.cs | 19 +- .../Graphics/Colors/ICC/IccProfile.cs | 375 ++++++++++-------- 4 files changed, 311 insertions(+), 186 deletions(-) diff --git a/src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs b/src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs index 8de9a3d2..bdff1416 100644 --- a/src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs +++ b/src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs @@ -17,13 +17,13 @@ Directory.CreateDirectory(OutputFolder); } + [Fact] public void IccXyzTest() { var path = IntegrationHelpers.GetSpecificTestDocumentPath("xyztest.pdf"); using (var document = PdfDocument.Open(path)) { - // page 1 var page1 = document.GetPage(1); var paths = page1.ExperimentalAccess.Paths .OrderBy(p => p.GetBoundingRectangle().Value.Centroid.X) @@ -80,6 +80,52 @@ } } + [Fact] + public void IccLabProfile8bitLUT() + { + var path = IntegrationHelpers.GetSpecificTestDocumentPath("icc-lab-8bit.pdf"); + using (var document = PdfDocument.Open(path)) + { + var page1 = document.GetPage(1); + var paths = page1.ExperimentalAccess.Paths + .OrderBy(p => p.GetBoundingRectangle().Value.Centroid.X) + .ThenBy(p => p.GetBoundingRectangle().Value.Centroid.Y) + .ToArray(); + + var grouped = page1.ExperimentalAccess.Paths + .GroupBy(p => p.GetBoundingRectangle().Value.Centroid.X) + .ToDictionary(kvp => kvp.Key, kvp => kvp); + + /* + * If all the profiles were rendered correctly, for the 3 "Lab" files we'd expect to see: + * stripe 1: lab(0 0 0) - black + * stripe 2: lab(100% 0 0) - white + * stripe 3: lab(75% 0 50) - mustard (#CEB759) + * stripe 4: lab(75% 50 0) - pink (#FF92BB) + */ + + var black = grouped[30].Single().FillColor; + var white = grouped[60].Single().FillColor; + var mustard = grouped[90].Single().FillColor; // rgb(206,183,89) + var pink = grouped[120].Single().FillColor; // rgb(255,146,187) + + AssertColor(206, 183, 89, mustard); + AssertColor(255, 146, 187, pink); + } + } + + private void AssertColor(byte r, byte g, byte b, IColor color) + { + var rgb = color.ToRGBValues(); + var rAct = ConvertToByte(rgb.r); + var gAct = ConvertToByte(rgb.g); + var bAct = ConvertToByte(rgb.b); + + Assert.Equal(r, rAct); + Assert.Equal(g, gAct); + Assert.Equal(b, bAct); + } + [Fact] public void IccLabProfileMatrixv4() { @@ -422,7 +468,7 @@ private static byte ConvertToByte(decimal componentValue) { - var rounded = Math.Round(componentValue * 255, MidpointRounding.AwayFromZero); + var rounded = Math.Round(componentValue * 255m, MidpointRounding.AwayFromZero); return (byte)rounded; } } diff --git a/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs b/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs index dcefe74a..0821ffc2 100644 --- a/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs +++ b/src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs @@ -1161,7 +1161,7 @@ return transformed; } - private static double g(double x) + internal static double g(double x) { if (x > 6.0 / 29.0) { @@ -1355,7 +1355,7 @@ return new double[] { 0, 0, 0 }; // Black RGB } - if (Profile.TryProcess(values, out double[] xyz) && xyz.Length == 3) + if (Profile.TryProcessToPcs(values, null, out double[] xyz) && xyz.Length == 3) // No rendering intent for now { double x = xyz[0]; double y = xyz[1]; @@ -1395,7 +1395,44 @@ return RGBColor.Black; } - if (Profile.TryProcess(values, out double[] xyz) && xyz.Length == 3) + if (values.Any(x => x > 1.0)) + { + //values[0] /= 100.0; + //values[1] = (values[1] + 127.0) / 255.0; + //values[2] = (values[2] + 127.0) / 255.0; + + for (int i = 0; i < values.Length; i++) + { + values[i] /= 100.0; + } + } + + if (Profile.Header.ColourSpace == IccColourSpaceType.CIELABorPCSLAB) + { + //values[0] /= 100.0; + //values[1] = (values[1] + 127.0) / 256.0; + //values[2] = (values[2] + 127.0) / 256.0; + + // Component Ranges: L*: [0 100]; a* and b*: [−128 127] + //double b = PdfFunction.ClipToRange(values[1], -128.0, 127.0); + //double c = PdfFunction.ClipToRange(values[2], -128.0, 127.0); + + //double M = (values[0] + 16.0) / 116.0; + //double L = M + (b / 500.0); + //double N = M - (c / 200.0); + + //IccXyz referenceWhite = Profile.Header.nCIEXYZ; + + //values[0] = LabColorSpaceDetails.g(L) * referenceWhite.X; // X + //values[1] = LabColorSpaceDetails.g(M) * referenceWhite.Y; // Y + //values[2] = LabColorSpaceDetails.g(N) * referenceWhite.Z; // Z + + //var labColor = labColorSpaceDetails.GetColor(values[0], values[1], values[2]); + //values[0] = + } + + + if (Profile.TryProcessToPcs(values, null, out double[] xyz) && xyz.Length == 3) // No rendering intent for now { double x = xyz[0]; double y = xyz[1]; @@ -1408,7 +1445,7 @@ } else { - return labColorSpaceDetails.GetColor(x, y, z); + return labColorSpaceDetails.GetColor(x * 100.0, y * 255.0 - 127.0, z * 255.0 - 127.0); } } } @@ -1437,7 +1474,8 @@ int outputSize = (int)(decoded.Count * outputCount / (double)NumberOfColorComponents); var transformed = new byte[outputSize]; - Parallel.For(0, decoded.Count / NumberOfColorComponents, i => + //Parallel.For(0, decoded.Count / NumberOfColorComponents, i => + for (int i = 0; i < decoded.Count / NumberOfColorComponents; i++) { double[] comps = new double[NumberOfColorComponents]; for (int n = 0; n < NumberOfColorComponents; n++) @@ -1450,7 +1488,8 @@ { transformed[i * outputCount + c] = ConvertToByte(colors[c]); } - }); + } + //); return transformed; } @@ -1512,4 +1551,4 @@ throw new InvalidOperationException("UnsupportedColorSpaceDetails"); } } -} +} diff --git a/src/UglyToad.PdfPig/Graphics/Colors/ICC/IccHelper.cs b/src/UglyToad.PdfPig/Graphics/Colors/ICC/IccHelper.cs index 64c3a45c..869e223b 100644 --- a/src/UglyToad.PdfPig/Graphics/Colors/ICC/IccHelper.cs +++ b/src/UglyToad.PdfPig/Graphics/Colors/ICC/IccHelper.cs @@ -29,7 +29,13 @@ namespace IccProfileNet index += w; } - return clut[(int)index]; + int indexInt = (int)index; + if (indexInt > clut.Length - 1) + { + //indexInt = clut.Length - 1; + } + + return clut[indexInt]; } internal static double[] Lookup(double[] input, double[][] clut, int clutGridPoints) @@ -40,10 +46,17 @@ namespace IccProfileNet double index = 0; for (int i = 0; i < input.Length; i++) { - index += input[i] * Math.Pow(clutGridPoints, input.Length - i); + double pow = Math.Pow(clutGridPoints, input.Length - i); + index += input[i] * pow; } - return clut[(int)index]; + int indexInt = (int)index; + if (indexInt > clut.Length - 1) + { + //indexInt = indexInt % clut.Length; + } + + return clut[indexInt]; } /// diff --git a/src/UglyToad.PdfPig/Graphics/Colors/ICC/IccProfile.cs b/src/UglyToad.PdfPig/Graphics/Colors/ICC/IccProfile.cs index b93d06ec..f6f36c02 100644 --- a/src/UglyToad.PdfPig/Graphics/Colors/ICC/IccProfile.cs +++ b/src/UglyToad.PdfPig/Graphics/Colors/ICC/IccProfile.cs @@ -144,209 +144,236 @@ namespace IccProfileNet } } - public bool TryProcessOld(double[] input, out double[] output) + private bool TryProcessGrayTRC(double[] input, out double[] output) { - throw new NotImplementedException($"Unsuported ICC profile v{Header.VersionMajor}"); + // 8.3.4 Monochrome Input profiles + // 8.4.4 Monochrome Display profiles + // 8.5.3 Monochrome Output profiles + if (Tags.TryGetValue(IccTags.GrayTRCTag, out var kTrcTag) && kTrcTag is IccBaseCurveType kTrc) + { + double v = kTrc.Process(input.Single()); + output = new double[] { v, v, v }; + return true; + } - //switch (Header.VersionMajor) - //{ - // case 2: - // return TryProcess2(input, out output); - - // case 4: - // return TryProcess4(input, out output); - - // default: - // throw new NotImplementedException($"Unsuported ICC profile v{Header.VersionMajor}"); - //} + output = null; + return false; } - public bool TryProcess(double[] input, out double[] output) + private bool TryProcessTRCMatrix(double[] input, out double[] output) { - try + // 8.3.3 Three-component matrix-based Input profiles + // 8.4.3 Three-component matrix-based Display profiles + // See p197 of Wiley book + if (Tags.TryGetValue(IccTags.RedMatrixColumnTag, out var rmcTag) && rmcTag is IccXyzType rmc && + Tags.TryGetValue(IccTags.GreenMatrixColumnTag, out var gmcTag) && gmcTag is IccXyzType gmc && + Tags.TryGetValue(IccTags.BlueMatrixColumnTag, out var bmcTag) && bmcTag is IccXyzType bmc && + Tags.TryGetValue(IccTags.RedTRCTag, out var rTrcTag) && rTrcTag is IccBaseCurveType rTrc && + Tags.TryGetValue(IccTags.GreenTRCTag, out var gTrcTag) && gTrcTag is IccBaseCurveType gTrc && + Tags.TryGetValue(IccTags.BlueTRCTag, out var bTrcTag) && bTrcTag is IccBaseCurveType bTrc) { + // Optional + // Tags.TryGetValue(IccTags.ChromaticAdaptationTag, out var caTag) && caTag is IccS15Fixed16ArrayType ca + + double channel1 = input[0]; + double channel2 = input[1]; + double channel3 = input[2]; + + double lR = rTrc.Process(channel1); + double lG = gTrc.Process(channel2); + double lB = bTrc.Process(channel3); + + double cX = (rmc.Xyz.X * lR) + (gmc.Xyz.X * lG) + (bmc.Xyz.X * lB); + double cY = (rmc.Xyz.Y * lR) + (gmc.Xyz.Y * lG) + (bmc.Xyz.Y * lB); + double cZ = (rmc.Xyz.Z * lR) + (gmc.Xyz.Z * lG) + (bmc.Xyz.Z * lB); + + output = new double[] { cX, cY, cZ }; + return true; + } + + output = null; + return false; + } + + /// + /// Process from Device space to PCS. + /// + /// A to B. + /// + /// + public bool TryProcessToPcs(double[] input, IccRenderingIntent? renderingIntent, out double[] output) + { + // See Table 25 — Profile type/profile tag and defined rendering intents + + //try + //{ + if (TryProcessGrayTRC(input, out output)) + { + return true; + } + + if (TryProcessTRCMatrix(input, out output)) + { + return true; + } + + if (renderingIntent == null) + { + // use profile rendering intent + renderingIntent = Header.RenderingIntent; + } + switch (Header.ProfileClass) { case IccProfileClass.Input: - { - // 8.3 Input profiles - // 8.3.2 N-component LUT-based Input profiles - if (Tags.TryGetValue(IccTags.AToB0Tag, out var lutAB0TagI) && - lutAB0TagI is IccLutABType lutAb0I) - { - //output = lutAB.Process(input, Header); - //return true; - } - - // 8.3.3 Three-component matrix-based Input profiles - if (Tags.TryGetValue(IccTags.RedMatrixColumnTag, out var rmcTagI) && - Tags.TryGetValue(IccTags.RedTRCTag, out var rTrcTagI)) - { - // Check other taggs - } - - // 8.3.4 Monochrome Input profiles - if (Tags.TryGetValue(IccTags.GrayTRCTag, out var kTrcTag) && kTrcTag is IccBaseCurveType kTrc) - { - double v = kTrc.Process(input.Single()); - output = new double[] { v, v, v }; - return true; - } - break; - } - case IccProfileClass.Display: - { - // 8.4 Display profiles - // 8.4.2 N-Component LUT-based Display profilesS - if (Tags.TryGetValue(IccTags.AToB0Tag, out var lutAB0TagD) && lutAB0TagD is IccLutABType lutAb0D && - Tags.TryGetValue(IccTags.BToA0Tag, out var lutBA0TagD) && lutBA0TagD is IccLutABType lutBa0D) - { - output = lutBa0D.Process(input, Header); - return true; - } - - // 8.4.3 Three-component matrix-based Display profiles - // See p197 of Wiley book - if (Tags.TryGetValue(IccTags.RedMatrixColumnTag, out var rmcTag) && rmcTag is IccXyzType rmc && - Tags.TryGetValue(IccTags.GreenMatrixColumnTag, out var gmcTag) && gmcTag is IccXyzType gmc && - Tags.TryGetValue(IccTags.BlueMatrixColumnTag, out var bmcTag) && bmcTag is IccXyzType bmc && - - Tags.TryGetValue(IccTags.RedTRCTag, out var rTrcTag) && rTrcTag is IccBaseCurveType rTrc && - Tags.TryGetValue(IccTags.GreenTRCTag, out var gTrcTag) && gTrcTag is IccBaseCurveType gTrc && - Tags.TryGetValue(IccTags.BlueTRCTag, out var bTrcTag) && bTrcTag is IccBaseCurveType bTrc) - { - // Optional - // Tags.TryGetValue(IccTags.ChromaticAdaptationTag, out var caTag) && caTag is IccS15Fixed16ArrayType ca - - double channel1 = input[0]; - double channel2 = input[1]; - double channel3 = input[2]; - - double lR = rTrc.Process(channel1); - double lG = gTrc.Process(channel2); - double lB = bTrc.Process(channel3); - - double cX = rmc.Xyz.X * lR + gmc.Xyz.X * lG + bmc.Xyz.X * lB; - double cY = rmc.Xyz.Y * lR + gmc.Xyz.Y * lG + bmc.Xyz.Y * lB; - double cZ = rmc.Xyz.Z * lR + gmc.Xyz.Z * lG + bmc.Xyz.Z * lB; - - output = new double[] { cX, cY, cZ }; - return true; - } - - // 8.4.4 Monochrome Display profiles - if (Tags.TryGetValue(IccTags.GrayTRCTag, out var kTrcTag) && kTrcTag is IccBaseCurveType kTrc) - { - double v = kTrc.Process(input.Single()); - output = new double[] { v, v, v }; - return true; - } - break; - } - case IccProfileClass.Output: - { - // 8.5.2 N-component LUT-based Output profiles - if (Tags.TryGetValue(IccTags.AToB0Tag, out var lutAB0Tag) && lutAB0Tag is IIccClutType lutAb0 && - Tags.TryGetValue(IccTags.AToB1Tag, out var lutAB1Tag) && lutAB1Tag is IIccClutType lutAb1 && - Tags.TryGetValue(IccTags.AToB2Tag, out var lutAB2Tag) && lutAB2Tag is IIccClutType lutAb2 && - - - Tags.TryGetValue(IccTags.BToA0Tag, out var lutBA0Tag) && lutBA0Tag is IIccClutType lutBa0 && - Tags.TryGetValue(IccTags.BToA1Tag, out var lutBA1Tag) && lutBA1Tag is IIccClutType lutBa1 && - Tags.TryGetValue(IccTags.BToA2Tag, out var lutBA2Tag) && lutBA2Tag is IIccClutType lutBa2 && - - Tags.TryGetValue(IccTags.GamutTag, out var gamutTag)) - { - // Optional?? - // Tags.TryGetValue(IccTags.ColorantTableTag, out var colorantTableTag) - switch (Header.RenderingIntent) - { - case IccRenderingIntent.Perceptual: - output = lutAb0.Process(input, Header); - return true; - - case IccRenderingIntent.MediaRelativeColorimetric: - output = lutAb1.Process(input, Header); - return true; - - case IccRenderingIntent.Saturation: - output = lutAb2.Process(input, Header); - return true; - } - } - - // 8.5.3 Monochrome Output profiles - if (Tags.TryGetValue(IccTags.GrayTRCTag, out var tag)) - { - - } - break; - } - - case IccProfileClass.DeviceLink: - { - // TODO - break; - } - case IccProfileClass.ColorSpace: { - // 8.7 ColorSpace profile - if (Tags.TryGetValue(IccTags.AToB0Tag, out var lutAB0Tag) && - Tags.TryGetValue(IccTags.BToA0Tag, out var lutBA0Tag)) + // 8.3.2 N-component LUT-based Input profiles + // 8.4.2 N-Component LUT-based Display profiles + // 8.5.2 N-component LUT-based Output profiles + + if (renderingIntent == IccRenderingIntent.Perceptual && Tags.TryGetValue(IccTags.AToB0Tag, out var lutAB0Tag) + && lutAB0Tag is IIccClutType lutAb0) { - // TODO - check - if (lutAB0Tag is IccLutABType lutAb0) - { - output = lutAb0.Process(input, Header); - return true; - } - else if (lutAB0Tag is IccBaseLutType lutAb0bis) - { - output = lutAb0bis.Process(input, Header); - return true; - } - else - { - throw new Exception(""); - } + output = lutAb0.Process(input, Header); + return true; + } + else if (renderingIntent == IccRenderingIntent.MediaRelativeColorimetric && Tags.TryGetValue(IccTags.AToB1Tag, out var lutAB1Tag) + && lutAB1Tag is IIccClutType lutAb1) + { + output = lutAb1.Process(input, Header); + return true; + } + else if (renderingIntent == IccRenderingIntent.Saturation && Tags.TryGetValue(IccTags.AToB2Tag, out var lutAB2Tag) + && lutAB2Tag is IIccClutType lutAb2) + { + output = lutAb2.Process(input, Header); + return true; + } + else if (Tags.TryGetValue(IccTags.AToB0Tag, out var lutAB0TagDefault) && lutAB0TagDefault is IIccClutType lutAb0Default) + { + output = lutAb0Default.Process(input, Header); + return true; } break; } case IccProfileClass.Abstract: + case IccProfileClass.DeviceLink: + case IccProfileClass.NamedColor: // undefined actually { - if (Tags.TryGetValue(IccTags.AToB0Tag, out var lutAB0Tag)) + // TODO - use IIccClutType instead? + if (Tags.TryGetValue(IccTags.AToB0Tag, out var lutAB0Tag) && lutAB0Tag is IIccClutType lutAb0) { - // TODO - check - if (lutAB0Tag is IccLutABType lutAb0) - { - output = lutAb0.Process(input, Header); - return true; - } - else if (lutAB0Tag is IccBaseLutType lutAb0bis) - { - output = lutAb0bis.Process(input, Header); - return true; - } - else - { - throw new Exception(""); - } + output = lutAb0.Process(input, Header); + return true; } break; } - case IccProfileClass.NamedColor: + default: + throw new ArgumentOutOfRangeException("TODO"); + } + //} + //catch (Exception ex) + //{ + // // Ignore + // System.Diagnostics.Debug.WriteLine(ex); + // output = null; + // return false; + //} + + output = null; + return false; + } + + /// + /// Process from PCS to Device space. + /// + /// B to A. + /// + /// + public bool TryProcessFromPcs(double[] input, IccRenderingIntent? renderingIntent, out double[] output) + { + // TODO + + // See Table 25 — Profile type/profile tag and defined rendering intents + + try + { + if (TryProcessGrayTRC(input, out output)) // TODO - Need inversion + { + return true; + } + + if (TryProcessTRCMatrix(input, out output)) // TODO - Need inversion + { + return true; + } + + if (renderingIntent == null) + { + // use profile rendering intent + renderingIntent = Header.RenderingIntent; + } + + switch (Header.ProfileClass) + { + case IccProfileClass.Input: + case IccProfileClass.Display: + case IccProfileClass.Output: + case IccProfileClass.ColorSpace: { - // TODO + // 8.3.2 N-component LUT-based Input profiles + // 8.4.2 N-Component LUT-based Display profiles + // 8.5.2 N-component LUT-based Output profiles + if (Tags.TryGetValue(IccTags.BToA0Tag, out var lutBA0Tag) && lutBA0Tag is IIccClutType lutBa0 && + Tags.TryGetValue(IccTags.BToA1Tag, out var lutBA1Tag) && lutBA1Tag is IIccClutType lutBa1 && + Tags.TryGetValue(IccTags.BToA2Tag, out var lutBA2Tag) && lutBA2Tag is IIccClutType lutBa2) + { + // Optional?? + // Tags.TryGetValue(IccTags.GamutTag, out var gamutTag) + // Tags.TryGetValue(IccTags.ColorantTableTag, out var colorantTableTag) + + switch (renderingIntent) + { + case IccRenderingIntent.Perceptual: + output = lutBa0.Process(input, Header); + return true; + + case IccRenderingIntent.MediaRelativeColorimetric: + output = lutBa1.Process(input, Header); + return true; + + case IccRenderingIntent.Saturation: + output = lutBa2.Process(input, Header); + return true; + + default: + output = lutBa0.Process(input, Header); + return true; + } + } + + break; + } + + case IccProfileClass.Abstract: + case IccProfileClass.DeviceLink: + case IccProfileClass.NamedColor: // undefined actually + { + if (Tags.TryGetValue(IccTags.BToA0Tag, out var lutBA0Tag) && lutBA0Tag is IccLutABType lutBa0) + { + output = lutBa0.Process(input, Header); + return true; + } break; } default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException("TODO"); } } catch (Exception) @@ -366,4 +393,4 @@ namespace IccProfileNet return $"ICC Profile v{Header}"; } } -} \ No newline at end of file +}