add full color space details and transformation into full bytes for indexed

This commit is contained in:
Eliot Jones
2021-02-06 11:31:23 -04:00
parent c008340b2b
commit fad06eac27
10 changed files with 480 additions and 68 deletions

View File

@@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARGUMENTS_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_PARAMETERS_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/AddImportsToDeepestScope/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BE/@EntryIndexedValue">BE</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CIE/@EntryIndexedValue">CIE</s:String>

View File

@@ -84,6 +84,11 @@
/// </summary>
bool IsInlineImage { get; }
/// <summary>
/// Full details for the <see cref="ColorSpace"/> with any associated data.
/// </summary>
ColorSpaceDetails ColorSpaceDetails { get; }
/// <summary>
/// Get the decoded bytes of the image if applicable. For JPEG images and some other types the
/// <see cref="RawBytes"/> should be used directly.

View File

@@ -7,6 +7,7 @@
using Filters;
using Graphics.Colors;
using Graphics.Core;
using Images;
using Tokens;
using Images.Png;
@@ -51,6 +52,9 @@
/// <inheritdoc />
public IReadOnlyList<byte> RawBytes { get; }
/// <inheritdoc />
public ColorSpaceDetails ColorSpaceDetails { get; }
/// <summary>
/// Create a new <see cref="InlineImage"/>.
/// </summary>
@@ -61,7 +65,8 @@
IReadOnlyList<decimal> decode,
IReadOnlyList<byte> bytes,
IReadOnlyList<IFilter> filters,
DictionaryToken streamDictionary)
DictionaryToken streamDictionary,
ColorSpaceDetails colorSpaceDetails)
{
Bounds = bounds;
WidthInSamples = widthInSamples;
@@ -74,6 +79,7 @@
Interpolate = interpolate;
RawBytes = bytes;
ColorSpaceDetails = colorSpaceDetails;
var supportsFilters = true;
foreach (var filter in filters)
@@ -107,8 +113,6 @@
return false;
}
bytes = bytesFactory.Value;
return true;
}

View File

@@ -0,0 +1,129 @@
namespace UglyToad.PdfPig.Graphics.Colors
{
using System;
using System.Collections.Generic;
/// <summary>
/// Contains more document-specific information about the <see cref="ColorSpace"/>.
/// </summary>
public abstract class ColorSpaceDetails
{
/// <summary>
/// The type of the ColorSpace.
/// </summary>
public ColorSpace Type { get; }
/// <summary>
/// The underlying type of ColorSpace, usually equal to <see cref="Type"/>
/// unless <see cref="ColorSpace.Indexed"/>.
/// </summary>
public ColorSpace BaseType { get; protected set; }
/// <summary>
/// Create a new <see cref="ColorSpaceDetails"/>.
/// </summary>
protected ColorSpaceDetails(ColorSpace type)
{
Type = type;
BaseType = type;
}
}
/// <summary>
/// A grayscale value is represented by a single number in the range 0.0 to 1.0,
/// where 0.0 corresponds to black, 1.0 to white, and intermediate values to different gray levels.
/// </summary>
public sealed class DeviceGrayColorSpaceDetails : ColorSpaceDetails
{
/// <summary>
/// The single instance of the <see cref="DeviceGrayColorSpaceDetails"/>.
/// </summary>
public static readonly DeviceGrayColorSpaceDetails Instance = new DeviceGrayColorSpaceDetails();
private DeviceGrayColorSpaceDetails() : base(ColorSpace.DeviceGray)
{
}
}
/// <summary>
/// Color values are defined by three components representing the intensities of the additive primary colorants red, green and blue.
/// Each component is specified by a number in the range 0.0 to 1.0, where 0.0 denotes the complete absence of a primary component and 1.0 denotes maximum intensity.
/// </summary>
public sealed class DeviceRgbColorSpaceDetails : ColorSpaceDetails
{
/// <summary>
/// The single instance of the <see cref="DeviceRgbColorSpaceDetails"/>.
/// </summary>
public static readonly DeviceRgbColorSpaceDetails Instance = new DeviceRgbColorSpaceDetails();
private DeviceRgbColorSpaceDetails() : base(ColorSpace.DeviceRGB)
{
}
}
/// <summary>
/// Color values are defined by four components cyan, magenta, yellow and black
/// </summary>
public sealed class DeviceCmykColorSpaceDetails : ColorSpaceDetails
{
/// <summary>
/// The single instance of the <see cref="DeviceCmykColorSpaceDetails"/>.
/// </summary>
public static readonly DeviceCmykColorSpaceDetails Instance = new DeviceCmykColorSpaceDetails();
private DeviceCmykColorSpaceDetails() : base(ColorSpace.DeviceCMYK)
{
}
}
/// <summary>
/// An Indexed color space allows a PDF content stream to use small integers as indices into a color map or color table of arbitrary colors in some other space.
/// A PDF consumer treats each sample value as an index into the color table and uses the color value it finds there.
/// </summary>
public class IndexedColorSpaceDetails : ColorSpaceDetails
{
/// <summary>
/// The base color space in which the values in the color table are to be interpreted.
/// It can be any device or CIE-based color space or(in PDF 1.3) a Separation or DeviceN space,
/// but not a Pattern space or another Indexed space.
/// </summary>
public ColorSpaceDetails BaseColorSpaceDetails { get; }
/// <summary>
/// An integer that specifies the maximum valid index value. Can be no greater than 255.
/// </summary>
public byte HiVal { get; }
/// <summary>
/// Provides the mapping between index values and the corresponding colors in the base color space.
/// </summary>
public IReadOnlyList<byte> ColorTable { get; }
/// <summary>
/// Create a new <see cref="IndexedColorSpaceDetails"/>.
/// </summary>
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, IReadOnlyList<byte> colorTable)
: base(ColorSpace.Indexed)
{
BaseColorSpaceDetails = baseColorSpaceDetails ?? throw new ArgumentNullException(nameof(baseColorSpaceDetails));
HiVal = hiVal;
ColorTable = colorTable;
BaseType = baseColorSpaceDetails.BaseType;
}
}
/// <summary>
/// A ColorSpace which the PdfPig library does not currently support. Please raise a PR if you need support for this ColorSpace.
/// </summary>
public class UnsupportedColorSpaceDetails : ColorSpaceDetails
{
/// <summary>
/// The single instance of the <see cref="UnsupportedColorSpaceDetails"/>.
/// </summary>
public static readonly UnsupportedColorSpaceDetails Instance = new UnsupportedColorSpaceDetails();
private UnsupportedColorSpaceDetails() : base(ColorSpace.DeviceGray)
{
}
}
}

View File

@@ -10,6 +10,7 @@
using PdfPig.Core;
using Tokenization.Scanner;
using Tokens;
using Util;
internal class InlineImageBuilder
{
@@ -27,35 +28,7 @@
throw new InvalidOperationException($"Inline image builder not completely defined before calling {nameof(CreateInlineImage)}.");
}
bool TryMapColorSpace(NameToken name, out ColorSpace colorSpaceResult)
{
if (name.TryMapToColorSpace(out colorSpaceResult))
{
return true;
}
if (TryExtendedColorSpaceNameMapping(name, out colorSpaceResult))
{
return true;
}
if (!resourceStore.TryGetNamedColorSpace(name, out var colorSpaceNamedToken))
{
return false;
}
if (colorSpaceNamedToken.Name.TryMapToColorSpace(out colorSpaceResult))
{
return true;
}
if (TryExtendedColorSpaceNameMapping(colorSpaceNamedToken.Name, out colorSpaceResult))
{
return true;
}
return false;
}
var bounds = transformationMatrix.Transform(new PdfRectangle(new PdfPoint(1, 1),
new PdfPoint(0, 0)));
@@ -90,7 +63,7 @@
throw new PdfDocumentFormatException($"Invalid ColorSpace array defined for inline image: {colorSpaceArray}.");
}
if (!TryMapColorSpace(firstColorSpaceName, out var colorSpaceMapped))
if (!ColorSpaceMapper.TryMap(firstColorSpaceName, resourceStore, out var colorSpaceMapped))
{
throw new PdfDocumentFormatException($"Invalid ColorSpace defined for inline image: {firstColorSpaceName}.");
}
@@ -99,7 +72,7 @@
}
else
{
if (!TryMapColorSpace(colorSpaceName, out var colorSpaceMapped))
if (!ColorSpaceMapper.TryMap(colorSpaceName, resourceStore, out var colorSpaceMapped))
{
throw new PdfDocumentFormatException($"Invalid ColorSpace defined for inline image: {colorSpaceName}.");
}
@@ -108,6 +81,15 @@
}
}
var imgDic = new DictionaryToken(Properties ?? new Dictionary<NameToken, IToken>());
var details = ColorSpaceDetailsParser.GetColorSpaceDetails(
colorSpace,
imgDic,
tokenScanner,
resourceStore,
filterProvider);
var renderingIntent = GetByKeys<NameToken>(NameToken.Intent, null, false)?.Data?.ToRenderingIntent() ?? defaultRenderingIntent;
var filterNames = new List<NameToken>();
@@ -157,30 +139,8 @@
return new InlineImage(bounds, width, height, bitsPerComponent, isMask, renderingIntent, interpolate, colorSpace, decode, Bytes,
filters,
streamDictionary);
}
private static bool TryExtendedColorSpaceNameMapping(NameToken name, out ColorSpace result)
{
result = ColorSpace.DeviceGray;
switch (name.Data)
{
case "G":
result = ColorSpace.DeviceGray;
return true;
case "RGB":
result = ColorSpace.DeviceRGB;
return true;
case "CMYK":
result = ColorSpace.DeviceCMYK;
return true;
case "I":
result = ColorSpace.Indexed;
return true;
}
return false;
streamDictionary,
details);
}
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local

View File

@@ -0,0 +1,78 @@
namespace UglyToad.PdfPig.Images
{
using System;
using System.Collections.Generic;
using System.Linq;
using Graphics.Colors;
internal static class ColorSpaceDetailsByteConverter
{
public static byte[] Convert(ColorSpaceDetails details, IReadOnlyList<byte> decoded)
{
switch (details)
{
case IndexedColorSpaceDetails indexed:
return UnwrapIndexedColorSpaceBytes(indexed, decoded);
}
return decoded.ToArray();
}
private static byte[] UnwrapIndexedColorSpaceBytes(IndexedColorSpaceDetails indexed, IReadOnlyList<byte> input)
{
var multiplier = 1;
Func<byte, IEnumerable<byte>> transformer = null;
switch (indexed.BaseColorSpaceDetails.Type)
{
case ColorSpace.DeviceRGB:
transformer = x =>
{
var r = new byte[3];
for (var i = 0; i < 3; i++)
{
r[i] = indexed.ColorTable[x + i];
}
return r;
};
multiplier = 3;
break;
case ColorSpace.DeviceCMYK:
transformer = x =>
{
var r = new byte[4];
for (int i = 0; i < 4; i++)
{
r[i] = indexed.ColorTable[x + i];
}
return r;
};
multiplier = 4;
break;
case ColorSpace.DeviceGray:
transformer = x => new[] {indexed.ColorTable[x]};
multiplier = 1;
break;
}
if (transformer != null)
{
var result = new byte[input.Count * multiplier];
var i = 0;
foreach (var b in input)
{
foreach (var newByte in transformer(b))
{
result[i++] = newByte;
}
}
return result;
}
return input.ToArray();
}
}
}

View File

@@ -9,15 +9,23 @@
{
bytes = null;
var isColorSpaceSupported = image.ColorSpace == ColorSpace.DeviceGray || image.ColorSpace == ColorSpace.DeviceRGB;
var hasValidDetails = image.ColorSpaceDetails != null &&
!(image.ColorSpaceDetails is UnsupportedColorSpaceDetails);
var actualColorSpace = hasValidDetails ? image.ColorSpaceDetails.BaseType : image.ColorSpace;
var isColorSpaceSupported =
actualColorSpace == ColorSpace.DeviceGray || actualColorSpace == ColorSpace.DeviceRGB;
if (!isColorSpaceSupported || !image.TryGetBytes(out var bytesPure))
{
return false;
}
bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure);
try
{
var is3Byte = image.ColorSpace == ColorSpace.DeviceRGB;
var is3Byte = actualColorSpace == ColorSpace.DeviceRGB;
var multiplier = is3Byte ? 3 : 1;
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false);

View File

@@ -0,0 +1,200 @@
namespace UglyToad.PdfPig.Util
{
using System.Collections.Generic;
using Content;
using Filters;
using Graphics.Colors;
using Parser.Parts;
using Tokenization.Scanner;
using Tokens;
internal static class ColorSpaceMapper
{
private static bool TryExtendedColorSpaceNameMapping(NameToken name, out ColorSpace result)
{
result = ColorSpace.DeviceGray;
switch (name.Data)
{
case "G":
result = ColorSpace.DeviceGray;
return true;
case "RGB":
result = ColorSpace.DeviceRGB;
return true;
case "CMYK":
result = ColorSpace.DeviceCMYK;
return true;
case "I":
result = ColorSpace.Indexed;
return true;
}
return false;
}
public static bool TryMap(NameToken name, IResourceStore resourceStore, out ColorSpace colorSpaceResult)
{
if (name.TryMapToColorSpace(out colorSpaceResult))
{
return true;
}
if (TryExtendedColorSpaceNameMapping(name, out colorSpaceResult))
{
return true;
}
if (!resourceStore.TryGetNamedColorSpace(name, out var colorSpaceNamedToken))
{
return false;
}
if (colorSpaceNamedToken.Name.TryMapToColorSpace(out colorSpaceResult))
{
return true;
}
if (TryExtendedColorSpaceNameMapping(colorSpaceNamedToken.Name, out colorSpaceResult))
{
return true;
}
return false;
}
}
internal static class ColorSpaceDetailsParser
{
public static ColorSpaceDetails GetColorSpaceDetails(ColorSpace? colorSpace,
DictionaryToken imageDictionary,
IPdfTokenScanner scanner,
IResourceStore resourceStore,
IFilterProvider filterProvider,
bool cannotRecurse = false)
{
if (!colorSpace.HasValue)
{
return UnsupportedColorSpaceDetails.Instance;
}
switch (colorSpace.Value)
{
case ColorSpace.DeviceGray:
return DeviceGrayColorSpaceDetails.Instance;
case ColorSpace.DeviceRGB:
return DeviceRgbColorSpaceDetails.Instance;
case ColorSpace.DeviceCMYK:
return DeviceCmykColorSpaceDetails.Instance;
case ColorSpace.CalGray:
return UnsupportedColorSpaceDetails.Instance;
case ColorSpace.CalRGB:
return UnsupportedColorSpaceDetails.Instance;
case ColorSpace.Lab:
return UnsupportedColorSpaceDetails.Instance;
case ColorSpace.ICCBased:
return UnsupportedColorSpaceDetails.Instance;
case ColorSpace.Indexed:
{
if (cannotRecurse)
{
return UnsupportedColorSpaceDetails.Instance;
}
if (!imageDictionary.TryGet(NameToken.ColorSpace, scanner, out ArrayToken colorSpaceArray)
|| colorSpaceArray.Length != 4)
{
// Error instead?
return UnsupportedColorSpaceDetails.Instance;
}
var first = colorSpaceArray[0] as NameToken;
if (first == null || !ColorSpaceMapper.TryMap(first, resourceStore, out var innerColorSpace)
|| innerColorSpace != ColorSpace.Indexed)
{
return UnsupportedColorSpaceDetails.Instance;
}
var second = colorSpaceArray[1];
ColorSpaceDetails baseDetails;
if (DirectObjectFinder.TryGet(second, scanner, out NameToken baseColorSpaceNameToken)
&& ColorSpaceMapper.TryMap(baseColorSpaceNameToken, resourceStore, out var baseColorSpaceName))
{
baseDetails = GetColorSpaceDetails(
baseColorSpaceName,
imageDictionary,
scanner,
resourceStore,
filterProvider,
true);
}
else if (DirectObjectFinder.TryGet(second, scanner, out ArrayToken baseColorSpaceArrayToken)
&& baseColorSpaceArrayToken.Length > 0 && baseColorSpaceArrayToken[0] is NameToken baseColorSpaceArrayNameToken
&& ColorSpaceMapper.TryMap(baseColorSpaceArrayNameToken, resourceStore, out var baseColorSpaceArrayColorSpace))
{
var pseudoImageDictionary = new DictionaryToken(new Dictionary<NameToken, IToken>
{
{NameToken.ColorSpace, baseColorSpaceArrayToken}
});
baseDetails = GetColorSpaceDetails(
baseColorSpaceArrayColorSpace,
pseudoImageDictionary,
scanner,
resourceStore,
filterProvider,
true);
}
else
{
return UnsupportedColorSpaceDetails.Instance;
}
if (baseDetails is UnsupportedColorSpaceDetails)
{
return UnsupportedColorSpaceDetails.Instance;
}
var third = colorSpaceArray[2];
if (!DirectObjectFinder.TryGet(third, scanner, out NumericToken hiValNumericToken))
{
return UnsupportedColorSpaceDetails.Instance;
}
var hival = hiValNumericToken.Int;
var fourth = colorSpaceArray[3];
IReadOnlyList<byte> tableBytes;
if (DirectObjectFinder.TryGet(fourth, scanner, out HexToken tableHexToken))
{
tableBytes = tableHexToken.Bytes;
}
else if (DirectObjectFinder.TryGet(fourth, scanner, out StreamToken tableStreamToken))
{
tableBytes = tableStreamToken.Decode(filterProvider);
}
else
{
return UnsupportedColorSpaceDetails.Instance;
}
return new IndexedColorSpaceDetails(baseDetails, (byte)hival, tableBytes);
}
case ColorSpace.Pattern:
return UnsupportedColorSpaceDetails.Instance;
case ColorSpace.Separation:
return UnsupportedColorSpaceDetails.Instance;
case ColorSpace.DeviceN:
return UnsupportedColorSpaceDetails.Instance;
default:
return UnsupportedColorSpaceDetails.Instance;
}
}
}
}

View File

@@ -1,6 +1,4 @@
using UglyToad.PdfPig.Parser.Parts;
namespace UglyToad.PdfPig.XObjects
namespace UglyToad.PdfPig.XObjects
{
using System;
using System.Collections.Generic;
@@ -11,8 +9,10 @@ namespace UglyToad.PdfPig.XObjects
using Graphics;
using Graphics.Colors;
using Graphics.Core;
using Parser.Parts;
using Tokenization.Scanner;
using Tokens;
using Util;
internal static class XObjectFactory
{
@@ -137,10 +137,27 @@ namespace UglyToad.PdfPig.XObjects
}
}
return new XObjectImage(bounds, width, height, bitsPerComponent, colorSpace, isJpxDecode, isImageMask, intent, interpolate, decode,
dictionary, xObject.Stream.Data, decodedBytes);
var details = ColorSpaceDetailsParser.GetColorSpaceDetails(colorSpace, dictionary, pdfScanner, resourceStore, filterProvider);
return new XObjectImage(
bounds,
width,
height,
bitsPerComponent,
colorSpace,
isJpxDecode,
isImageMask,
intent,
interpolate,
decode,
dictionary,
xObject.Stream.Data,
decodedBytes,
details);
}
private static bool TryMapColorSpace(NameToken name, IResourceStore resourceStore, out ColorSpace colorSpaceResult)
{
if (name.TryMapToColorSpace(out colorSpaceResult))

View File

@@ -6,6 +6,7 @@
using Core;
using Graphics.Colors;
using Graphics.Core;
using Images;
using Images.Png;
using Tokens;
using Util.JetBrains.Annotations;
@@ -65,10 +66,16 @@
/// <inheritdoc />
public IReadOnlyList<byte> RawBytes { get; }
/// <inheritdoc />
public ColorSpaceDetails ColorSpaceDetails { get; }
/// <summary>
/// Creates a new <see cref="XObjectImage"/>.
/// </summary>
internal XObjectImage(PdfRectangle bounds, int widthInSamples, int heightInSamples, int bitsPerComponent,
internal XObjectImage(PdfRectangle bounds,
int widthInSamples,
int heightInSamples,
int bitsPerComponent,
ColorSpace? colorSpace,
bool isJpxEncoded,
bool isImageMask,
@@ -77,7 +84,8 @@
IReadOnlyList<decimal> decode,
DictionaryToken imageDictionary,
IReadOnlyList<byte> rawBytes,
Lazy<IReadOnlyList<byte>> bytes)
Lazy<IReadOnlyList<byte>> bytes,
ColorSpaceDetails colorSpaceDetails)
{
Bounds = bounds;
WidthInSamples = widthInSamples;
@@ -91,6 +99,7 @@
Decode = decode;
ImageDictionary = imageDictionary ?? throw new ArgumentNullException(nameof(imageDictionary));
RawBytes = rawBytes;
ColorSpaceDetails = colorSpaceDetails;
bytesFactory = bytes;
}