Handle SoftMask

This commit is contained in:
BobLd
2024-09-06 21:17:36 +01:00
parent 74d61bd985
commit 5fb36d452f
14 changed files with 578 additions and 135 deletions

View File

@@ -0,0 +1,41 @@
namespace UglyToad.PdfPig.Tests.Integration
{
using SkiaSharp;
using System.Linq;
public class SoftMaskTests
{
[Fact]
public void PigProductionHandbook()
{
var path = IntegrationHelpers.GetDocumentPath("Pig Production Handbook.pdf");
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true, SkipMissingFonts = true }))
{
var page = document.GetPage(1);
var images = page.GetImages().ToArray();
var image1 = images[1];
Assert.NotNull(image1.SoftMaskImage);
Assert.True(image1.TryGetPng(out var png1));
using (var skImage1 = SKImage.FromEncodedData(png1))
using (var skBitmap1 = SKBitmap.FromImage(skImage1))
{
var pixel = skBitmap1.GetPixel(0, 0);
Assert.Equal(0, pixel.Alpha);
}
var image2 = images[2];
Assert.NotNull(image2.SoftMaskImage);
Assert.True(image2.TryGetPng(out var png2));
using (var skImage2 = SKImage.FromEncodedData(png2))
using (var skBitmap2 = SKBitmap.FromImage(skImage2))
{
var pixel = skBitmap2.GetPixel(0, 0);
Assert.Equal(0, pixel.Alpha);
}
}
}
}
}

View File

@@ -161,6 +161,7 @@
"UglyToad.PdfPig.Graphics.Colors.LatticeFormGouraudShading",
"UglyToad.PdfPig.Graphics.Colors.CoonsPatchMeshesShading",
"UglyToad.PdfPig.Graphics.Colors.TensorProductPatchMeshesShading",
"UglyToad.PdfPig.Graphics.Core.BlendMode",
"UglyToad.PdfPig.Graphics.Core.LineCapStyle",
"UglyToad.PdfPig.Graphics.Core.LineDashPattern",
"UglyToad.PdfPig.Graphics.Core.LineJoinStyle",
@@ -245,6 +246,8 @@
"UglyToad.PdfPig.Graphics.Operations.TextState.Type3SetGlyphWidth",
"UglyToad.PdfPig.Graphics.Operations.TextState.Type3SetGlyphWidthAndBoundingBox",
"UglyToad.PdfPig.Graphics.PerformantRectangleTransformer",
"UglyToad.PdfPig.Graphics.SoftMask",
"UglyToad.PdfPig.Graphics.SoftMaskType",
"UglyToad.PdfPig.Graphics.TextMatrices",
"UglyToad.PdfPig.Graphics.XObjectContentRecord",
"UglyToad.PdfPig.Images.ColorSpaceDetailsByteConverter",

View File

@@ -37,6 +37,8 @@
public ReadOnlyMemory<byte> DecodedBytes { get; set; }
public IPdfImage? SoftMaskImage { get; }
public bool TryGetBytesAsMemory(out ReadOnlyMemory<byte> bytes)
{
bytes = DecodedBytes;

View File

@@ -94,6 +94,11 @@
/// </summary>
ColorSpaceDetails? ColorSpaceDetails { get; }
/// <summary>
/// Soft-mask image.
/// </summary>
IPdfImage? SoftMaskImage { get; }
/// <summary>
/// Get the decoded memory of the image if applicable. For JPEG images and some other types the
/// <see cref="RawMemory"/> should be used directly.

View File

@@ -55,12 +55,19 @@
public ReadOnlySpan<byte> RawBytes => RawMemory.Span;
/// <inheritdoc />
public ColorSpaceDetails ColorSpaceDetails { get; }
public ColorSpaceDetails ColorSpaceDetails { get; }
/// <inheritdoc />
public IPdfImage? SoftMaskImage { get; }
/// <summary>
/// Create a new <see cref="InlineImage"/>.
/// </summary>
internal InlineImage(PdfRectangle bounds, int widthInSamples, int heightInSamples, int bitsPerComponent, bool isImageMask,
internal InlineImage(PdfRectangle bounds,
int widthInSamples,
int heightInSamples,
int bitsPerComponent,
bool isImageMask,
RenderingIntent renderingIntent,
bool interpolate,
IReadOnlyList<double> decode,
@@ -68,7 +75,8 @@
ILookupFilterProvider filterProvider,
IReadOnlyList<NameToken> filterNames,
DictionaryToken streamDictionary,
ColorSpaceDetails colorSpaceDetails)
ColorSpaceDetails colorSpaceDetails,
IPdfImage? softMaskImage)
{
Bounds = bounds;
WidthInSamples = widthInSamples;
@@ -104,7 +112,9 @@
}
return b;
}) : null;
}) : null;
SoftMaskImage = softMaskImage;
}
/// <inheritdoc />

View File

@@ -495,22 +495,19 @@
$"Invalid Transparency Group XObject, '{NameToken.S}' token is not set or not equal to '{NameToken.Transparency}'.");
}
/* blend mode
* A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
* transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: Normal.
*/
//startState.BlendMode = BlendMode.Normal;
// Blend mode
// A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
// transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: Normal.
startState.BlendMode = BlendMode.Normal;
/* soft mask
* A conforming reader shall implicitly reset this parameter implicitly reset to its initial value at the beginning
* of execution of a transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: None.
*/
// TODO
// Soft mask
// A conforming reader shall implicitly reset this parameter implicitly reset to its initial value at the beginning
// of execution of a transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: None.
startState.SoftMask = null;
/* alpha constant
* A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
* transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: 1.0.
*/
// Alpha constant
// A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
// transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: 1.0.
startState.AlphaConstantNonStroking = 1.0;
startState.AlphaConstantStroking = 1.0;
@@ -765,6 +762,49 @@
// (see Section 6.5.4, “Automatic Stroke Adjustment”).
currentGraphicsState.StrokeAdjustment = saToken.Data;
}
// (PDF 1.4, array is deprecated in PDF 2.0) The current blend mode that shall be
// used in the transparent imaging model (see 11.3.5, "Blend mode"). A PDF reader
// shall implicitly reset this parameter to its initial value at the (array is
// deprecated beginning of execution of a transparency group XObject
// (see 11.6.6, in PDF 2.0) "Transparency group XObjects"). The value shall be
// either a name object, designating one of the standard blend modes listed in
// "Table 134 — Standard separable blend modes" and "Table 135 — Standard
// non-separable blend modes" in 11.3.5, "Blend mode", or an array of such names.
// In the latter case, the PDF reader shall use the first blend mode in the array
// that it recognises (or Normal if it recognises none of them).
//
// Initial value: Normal.
if (state.TryGet(NameToken.Bm, PdfScanner, out NameToken? bmToken))
{
currentGraphicsState.BlendMode = bmToken.Data.ToBlendMode() ?? BlendMode.Normal;
}
else if (state.TryGet(NameToken.Bm, PdfScanner, out ArrayToken? bmArrayToken))
{
// The PDF reader shall use the first blend mode in the array that it
// recognises (or Normal if it recognises none of them).
currentGraphicsState.BlendMode = BlendMode.Normal;
foreach (var token in bmArrayToken.Data.OfType<NameToken>())
{
var bm = token.Data.ToBlendMode();
if (bm.HasValue)
{
currentGraphicsState.BlendMode = bm.Value;
break;
}
}
}
if (state.TryGet(NameToken.Smask, PdfScanner, out NameToken? smToken) && smToken.Equals(NameToken.None))
{
currentGraphicsState.SoftMask = null;
}
else if (state.TryGet(NameToken.Smask, PdfScanner, out DictionaryToken? smDictToken))
{
currentGraphicsState.SoftMask = SoftMask.Parse(smDictToken, PdfScanner, FilterProvider);
}
}
/// <inheritdoc/>

View File

@@ -0,0 +1,65 @@
namespace UglyToad.PdfPig.Graphics.Core
{
/// <summary>
/// The blend mode.
/// </summary>
public enum BlendMode : byte
{
// 11.3.5.2 Separable blend modes
/// <summary>
/// Default.
/// <para>Same as Compatible.</para>
/// </summary>
Normal = 0,
Multiply = 1,
Screen = 2,
Darken = 3,
Lighten = 4,
ColorDodge = 5,
ColorBurn = 6,
HardLight = 7,
SoftLight = 8,
Overlay = 9,
Difference = 10,
Exclusion = 11,
// 11.3.5.3 Non-separable blend modes
Hue = 12,
Saturation = 13,
Color = 14,
Luminosity = 15
}
internal static class BlendModeExtensions
{
public static BlendMode? ToBlendMode(this string s)
{
return s switch
{
// 11.3.5.2 Separable blend modes
"Normal" => BlendMode.Normal,
"Compatible" => BlendMode.Normal,
"Multiply" => BlendMode.Multiply,
"Screen" => BlendMode.Screen,
"Darken" => BlendMode.Darken,
"Lighten" => BlendMode.Lighten,
"ColorDodge" => BlendMode.ColorDodge,
"ColorBurn" => BlendMode.ColorBurn,
"HardLight" => BlendMode.HardLight,
"SoftLight" => BlendMode.SoftLight,
"Overlay" => BlendMode.Overlay,
"Difference" => BlendMode.Difference,
"Exclusion" => BlendMode.Exclusion,
// 11.3.5.3 Non-separable blend modes
"Hue" => BlendMode.Hue,
"Saturation" => BlendMode.Saturation,
"Color" => BlendMode.Color,
"Luminosity" => BlendMode.Luminosity,
_ => null
};
}
}
}

View File

@@ -71,10 +71,18 @@ namespace UglyToad.PdfPig.Graphics
public double AlphaConstantNonStroking { get; set; } = 1;
/// <summary>
/// Should soft mask and alpha constant values be interpreted as shape (<see langword="true"/>) or opacity (<see langword="false"/>) values?
/// Should soft mask and alpha constant values be interpreted as shape
/// (<see langword="true"/>) or opacity (<see langword="false"/>) values?
/// </summary>
public bool AlphaSource { get; set; } = false;
/// <summary>
/// A soft-mask dictionary specifying the mask shape or mask opacity values
/// that shall be used in the transparent imaging model, or the name None if
/// no such mask is specified.
/// </summary>
public SoftMask SoftMask { get; set; }
/// <summary>
/// Maps positions from user coordinates to device coordinates.
/// </summary>
@@ -95,6 +103,11 @@ namespace UglyToad.PdfPig.Graphics
/// </summary>
public IColor CurrentNonStrokingColor { get; set; }
/// <summary>
/// The current blend mode.
/// </summary>
public BlendMode BlendMode { get; set; } = BlendMode.Normal;
#region Device Dependent
/// <summary>
@@ -151,6 +164,8 @@ namespace UglyToad.PdfPig.Graphics
CurrentNonStrokingColor = CurrentNonStrokingColor,
CurrentClippingPath = CurrentClippingPath,
ColorSpaceContext = ColorSpaceContext?.DeepClone(),
BlendMode = BlendMode,
SoftMask = SoftMask
};
}
}

View File

@@ -1,14 +1,18 @@
namespace UglyToad.PdfPig.Graphics
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Content;
using Core;
using Filters;
using PdfPig.Core;
using Tokenization.Scanner;
using Tokens;
using UglyToad.PdfPig.Graphics.Colors;
using UglyToad.PdfPig.XObjects;
/// <summary>
/// Inline Image Builder.
@@ -49,9 +53,34 @@
var isMask = maskToken?.Data == true;
var bitsPerComponent = GetByKeys<NumericToken>(NameToken.BitsPerComponent, NameToken.Bpc, !isMask)?.Int ?? 1;
NameToken? colorSpaceName = null;
var imgDic = new DictionaryToken(Properties ?? new Dictionary<NameToken, IToken>());
XObjectImage? softMaskImage = null;
if (imgDic.TryGet(NameToken.Smask, tokenScanner, out StreamToken? sMaskToken))
{
if (!sMaskToken.StreamDictionary.TryGet(NameToken.Subtype, out NameToken softMaskSubType) || !softMaskSubType.Equals(NameToken.Image))
{
throw new Exception("The SMask dictionary does not contain a 'Subtype' entry, or its value is not 'Image'.");
}
if (!sMaskToken.StreamDictionary.TryGet(NameToken.ColorSpace, out NameToken softMaskColorSpace) || !softMaskColorSpace.Equals(NameToken.Devicegray))
{
throw new Exception("The SMask dictionary does not contain a 'ColorSpace' entry, or its value is not 'Devicegray'.");
}
if (sMaskToken.StreamDictionary.ContainsKey(NameToken.Mask) || sMaskToken.StreamDictionary.ContainsKey(NameToken.Smask))
{
throw new Exception("The SMask dictionary contains a 'Mask' or 'Smask' entry.");
}
XObjectContentRecord softMaskImageRecord = new XObjectContentRecord(XObjectType.Image, sMaskToken, TransformationMatrix.Identity,
defaultRenderingIntent, DeviceGrayColorSpaceDetails.Instance);
softMaskImage = XObjectFactory.ReadImage(softMaskImageRecord, tokenScanner, filterProvider, resourceStore);
}
if (!isMask)
{
colorSpaceName = GetByKeys<NameToken>(NameToken.ColorSpace, NameToken.Cs, false);
@@ -74,8 +103,6 @@
}
}
var imgDic = new DictionaryToken(Properties ?? new Dictionary<NameToken, IToken>());
var details = resourceStore.GetColorSpaceDetails(colorSpaceName, imgDic);
var renderingIntent = GetByKeys<NameToken>(NameToken.Intent, null, false)?.Data?.ToRenderingIntent() ?? defaultRenderingIntent;
@@ -106,7 +133,7 @@
return new InlineImage(bounds, width, height, bitsPerComponent,
isMask, renderingIntent, interpolate, decode, Bytes,
filterProvider, filterNames, imgDic, details);
filterProvider, filterNames, imgDic, details, softMaskImage);
}
#nullable disable

View File

@@ -0,0 +1,166 @@
namespace UglyToad.PdfPig.Graphics
{
using System;
using System.Linq;
using UglyToad.PdfPig.Filters;
using UglyToad.PdfPig.Functions;
using UglyToad.PdfPig.Tokenization.Scanner;
using UglyToad.PdfPig.Tokens;
using UglyToad.PdfPig.Util;
/// <summary>
/// Soft Mask.
/// </summary>
public sealed class SoftMask
{
/// <summary>
/// (Required) A subtype specifying the method that shall be used in deriving the mask
/// values from the transparency group specified by the G entry:
/// <list type="bullet">
/// <item>Alpha - The group's computed alpha shall be used, disregarding its colour.</item>
/// <item>Luminosity - The group's computed colour shall be converted to a single-component luminosity value.</item>
/// </list>
/// </summary>
public SoftMaskType Subtype { get; private set; }
/// <summary>
/// (Required) A transparency group XObject that shall be used as the source of alpha
/// or colour values for deriving the mask. If the subtype S is Luminosity, the group
/// attributes dictionary shall contain a CS entry defining the colour space in which
/// the compositing computation is to be performed.
/// </summary>
public StreamToken TransparencyGroup { get; private set; }
/// <summary>
/// (Optional) An array of component values specifying the colour that shall be used
/// as the backdrop against which to composite the transparency group XObject G.
/// This entry shall be consulted only if the subtype S is Luminosity.
/// The array shall consist of n numbers, where n is the number of components in the
/// colour space specified by the CS entry in the group attributes dictionary.
/// <para>
/// Default value: the colour spaces initial value, representing black.
/// </para>
/// </summary>
public double[]? BC { get; private set; }
/// <summary>
/// (Optional) A function object (see 7.10, "Functions") specifying the transfer
/// function that shall be used in deriving the mask values.The function shall
/// accept one input, the computed group alpha or luminosity (depending on the
/// value of the subtype S), and shall return one output, the resulting mask
/// value.The input shall be in the range 0.0 to 1.0. The computed output shall
/// be in the range 0.0 to 1.0; if it falls outside this range, it shall be forced
/// to the nearest valid value.The name Identity may be specified in place of a
/// function object to designate the identity function.
/// <para>
/// Default value: Identity.
/// </para>
/// </summary>
public PdfFunction? TransferFunction { get; private set; }
internal static SoftMask Parse(DictionaryToken dictionaryToken, IPdfTokenScanner pdfTokenScanner, ILookupFilterProvider filterProvider)
{
if (dictionaryToken == null)
{
throw new ArgumentNullException(nameof(dictionaryToken));
}
var softMask = new SoftMask();
if (!dictionaryToken.TryGet(NameToken.S, pdfTokenScanner, out NameToken? s))
{
/*
* (Required) A subtype specifying the method that shall be used in deriving
* the mask values from the transparency group specified by the G entry: Alpha
* The groups computed alpha shall be used, disregarding its colour (see 11.5.2,
* "Deriving a soft mask from group alpha"). Luminosity The groups computed
* colour shall be converted to a single-component luminosity value (see 11.5.3,
* "Deriving a soft mask from group luminosity").
*/
throw new Exception($"Missing soft-mask dictionary '{NameToken.S}' entry.");
}
if (s.Equals(NameToken.Luminosity))
{
softMask.Subtype = SoftMaskType.Luminosity;
}
else if (s.Equals(NameToken.Alpha))
{
softMask.Subtype = SoftMaskType.Alpha;
}
else
{
throw new Exception($"Invalid soft-mask Subtype '{s}' entry.");
}
if (!dictionaryToken.TryGet(NameToken.G, pdfTokenScanner, out StreamToken g))
{
/*
* (Required) A transparency group XObject (see 11.6.6, "Transparency group
* XObjects") that shall be used as the source of alpha or colour values for
* deriving the mask. If the subtype S is Luminosity, the group attributes
* dictionary shall contain a CS entry defining the colour space in which
* the compositing computation is to be performed.
*/
throw new Exception($"Missing soft-mask dictionary '{NameToken.G}' entry.");
}
softMask.TransparencyGroup = g;
if (dictionaryToken.TryGet(NameToken.Bc, pdfTokenScanner, out ArrayToken bc))
{
/*
* (Optional) An array of component values specifying the colour that shall
* be used as the backdrop against which to composite the transparency group
* XObject G. This entry shall be consulted only if the subtype S is Luminosity.
* The array shall consist of n numbers, where n is the number of components in
* the colour space specified by the CS entry in the group attributes dictionary
* (see 11.6.6, "Transparency group XObjects"). Default value: the colour spaces
* initial value, representing black.
*/
softMask.BC = bc.Data.OfType<NumericToken>().Select(x => x.Data).ToArray();
}
if (dictionaryToken.TryGet(NameToken.Tr, pdfTokenScanner, out NameToken trName))
{
/*
* (Optional) A function object (see 7.10, "Functions") specifying the transfer
* function that shall be used in deriving the mask values. The function shall
* accept one input, the computed group alpha or luminosity (depending on the
* value of the subtype S), and shall return one output, the resulting mask
* value. The input shall be in the range 0.0 to 1.0. The computed output shall
* be in the range 0.0 to 1.0; if it falls outside this range, it shall be forced
* to the nearest valid value. The name Identity may be specified in place of a
* function object to designate the identity function. Default value: Identity
*/
if (!trName.Equals(NameToken.Identity))
{
throw new Exception($"Invalid transfer function name '{trName}' entry, should be '{NameToken.Identity}'.");
}
}
else if (dictionaryToken.TryGet(NameToken.Tr, pdfTokenScanner, out IToken? trFunction))
{
softMask.TransferFunction = PdfFunctionParser.Create(trFunction, pdfTokenScanner, filterProvider);
}
return softMask;
}
}
/// <summary>
/// The soft mask type.
/// <para>Alpha or Luminosity.</para>
/// </summary>
public enum SoftMaskType : byte
{
/// <summary>
/// Alpha - The group's computed alpha shall be used, disregarding its colour.
/// </summary>
Alpha = 0,
/// <summary>
/// Luminosity - The group's computed colour shall be converted to a single-component luminosity value.
/// </summary>
Luminosity = 1
}
}

View File

@@ -6,7 +6,51 @@
using UglyToad.PdfPig.Core;
internal static class PngFromPdfImageFactory
{
{
private static bool TryGenerateSoftMask(IPdfImage image, [NotNullWhen(true)] out ReadOnlySpan<byte> bytes)
{
bytes = ReadOnlySpan<byte>.Empty;
if (!image.TryGetBytesAsMemory(out var imageMemory))
{
return false;
}
try
{
bytes = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails!,
imageMemory.Span,
image.BitsPerComponent,
image.WidthInSamples,
image.HeightInSamples);
return IsCorrectlySized(image, bytes);
}
catch (Exception)
{
// ignored.
}
return false;
}
private static bool IsCorrectlySized(IPdfImage image, ReadOnlySpan<byte> bytesPure)
{
var numberOfComponents = image.ColorSpaceDetails!.BaseNumberOfColorComponents;
var requiredSize = (image.WidthInSamples * image.HeightInSamples * numberOfComponents);
var actualSize = bytesPure.Length;
return bytesPure.Length == 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);
}
public static bool TryGenerate(IPdfImage image, [NotNullWhen(true)] out byte[]? bytes)
{
bytes = null;
@@ -29,24 +73,12 @@
var numberOfComponents = image.ColorSpaceDetails!.BaseNumberOfColorComponents;
var is3Byte = numberOfComponents == 3;
ReadOnlySpan<byte> softMask = null;
bool isSoftMask = image.SoftMaskImage is not null && TryGenerateSoftMask(image.SoftMaskImage, out softMask);
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false);
var requiredSize = (image.WidthInSamples * image.HeightInSamples * numberOfComponents);
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, isSoftMask);
var actualSize = bytesPure.Length;
var isCorrectlySized = bytesPure.Length == requiredSize ||
// Spec, p. 37: "...error if the stream contains too much data, with the exception that
// there may be an extra end-of-line marker..."
(actualSize == requiredSize + 1 && bytesPure[actualSize - 1] == ReadHelper.AsciiLineFeed) ||
(actualSize == requiredSize + 1 && bytesPure[actualSize - 1] == ReadHelper.AsciiCarriageReturn) ||
// The combination of a CARRIAGE RETURN followed immediately by a LINE FEED is treated as one EOL marker.
(actualSize == requiredSize + 2 &&
bytesPure[actualSize - 2] == ReadHelper.AsciiCarriageReturn &&
bytesPure[actualSize - 1] == ReadHelper.AsciiLineFeed);
if (!isCorrectlySized)
if (!IsCorrectlySized(image, bytesPure))
{
return false;
}
@@ -54,6 +86,7 @@
if (image.ColorSpaceDetails.BaseType == ColorSpace.DeviceCMYK || numberOfComponents == 4)
{
int i = 0;
int sm = 0;
for (int col = 0; col < image.HeightInSamples; col++)
{
for (int row = 0; row < image.WidthInSamples; row++)
@@ -65,6 +98,7 @@
* B = 255 × (1-Y) × (1-K)
*/
byte a = isSoftMask ? softMask[sm++] : byte.MaxValue;
double c = (bytesPure[i++] / 255d);
double m = (bytesPure[i++] / 255d);
double y = (bytesPure[i++] / 255d);
@@ -73,18 +107,20 @@
var g = (byte)(255 * (1 - m) * (1 - k));
var b = (byte)(255 * (1 - y) * (1 - k));
builder.SetPixel(r, g, b, row, col);
builder.SetPixel(new Pixel(r, g, b, a, false), row, col);
}
}
}
else if (is3Byte)
else if (numberOfComponents == 3)
{
int i = 0;
int sm = 0;
for (int col = 0; col < image.HeightInSamples; col++)
{
for (int row = 0; row < image.WidthInSamples; row++)
{
builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], row, col);
byte a = isSoftMask ? softMask[sm++] : byte.MaxValue;
builder.SetPixel(new Pixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], a, false), row, col);
}
}
}
@@ -95,8 +131,9 @@
{
for (int row = 0; row < image.WidthInSamples; row++)
{
byte a = isSoftMask ? softMask[i] : byte.MaxValue;
byte pixel = bytesPure[i++];
builder.SetPixel(pixel, pixel, pixel, row, col);
builder.SetPixel(new Pixel(pixel, pixel, pixel, a, false), row, col);
}
}
}

View File

@@ -1,55 +1,81 @@
namespace UglyToad.PdfPig.XObjects
{
using System;
using System.Linq;
using Content;
using Core;
using Filters;
using Graphics;
using Graphics.Colors;
using Graphics.Core;
namespace UglyToad.PdfPig.XObjects
{
using System;
using System.Linq;
using Content;
using Core;
using Filters;
using Graphics;
using Graphics.Colors;
using Graphics.Core;
using Images;
using Tokenization.Scanner;
using Tokenization.Scanner;
using Tokens;
using Util;
/// <summary>
/// External Object (XObject) factory.
/// </summary>
public static class XObjectFactory
/// <summary>
/// External Object (XObject) factory.
/// </summary>
public static class XObjectFactory
{
/// <summary>
/// Read the XObject image.
/// </summary>
/// <summary>
/// Read the XObject image.
/// </summary>
public static XObjectImage ReadImage(XObjectContentRecord xObject,
IPdfTokenScanner pdfScanner,
ILookupFilterProvider filterProvider,
IResourceStore resourceStore)
{
if (xObject is null)
{
throw new ArgumentNullException(nameof(xObject));
}
if (xObject.Type != XObjectType.Image)
{
throw new InvalidOperationException($"Cannot create an image from an XObject with type: {xObject.Type}.");
ILookupFilterProvider filterProvider,
IResourceStore resourceStore)
{
if (xObject is null)
{
throw new ArgumentNullException(nameof(xObject));
}
if (xObject.Type != XObjectType.Image)
{
throw new InvalidOperationException($"Cannot create an image from an XObject with type: {xObject.Type}.");
}
var dictionary = xObject.Stream.StreamDictionary.Resolve(pdfScanner);
var bounds = xObject.AppliedTransformation.Transform(new PdfRectangle(new PdfPoint(0, 0), new PdfPoint(1, 1)));
var bounds = xObject.AppliedTransformation.Transform(new PdfRectangle(new PdfPoint(0, 0), new PdfPoint(1, 1)));
var width = dictionary.GetInt(NameToken.Width);
var height = dictionary.GetInt(NameToken.Height);
var isImageMask = dictionary.TryGet(NameToken.ImageMask, out BooleanToken isMaskToken) && isMaskToken.Data;
var isJpxDecode = dictionary.TryGet(NameToken.Filter, out NameToken filterName) && filterName.Equals(NameToken.JpxDecode);
XObjectImage? softMaskImage = null;
if (dictionary.TryGet(NameToken.Smask, pdfScanner, out StreamToken? sMaskToken))
{
if (!sMaskToken.StreamDictionary.TryGet(NameToken.Subtype, out NameToken softMaskSubType) || !softMaskSubType.Equals(NameToken.Image))
{
throw new Exception("The SMask dictionary does not contain a 'Subtype' entry, or its value is not 'Image'.");
}
if (!sMaskToken.StreamDictionary.TryGet(NameToken.ColorSpace, out NameToken softMaskColorSpace) || !softMaskColorSpace.Equals(NameToken.Devicegray))
{
throw new Exception("The SMask dictionary does not contain a 'ColorSpace' entry, or its value is not 'Devicegray'.");
}
if (sMaskToken.StreamDictionary.ContainsKey(NameToken.Mask) || sMaskToken.StreamDictionary.ContainsKey(NameToken.Smask))
{
throw new Exception("The SMask dictionary contains a 'Mask' or 'Smask' entry.");
}
var renderingIntent = xObject.DefaultRenderingIntent; // Ignored
XObjectContentRecord softMaskImageRecord = new XObjectContentRecord(XObjectType.Image, sMaskToken, TransformationMatrix.Identity,
renderingIntent, DeviceGrayColorSpaceDetails.Instance);
softMaskImage = ReadImage(softMaskImageRecord, pdfScanner, filterProvider, resourceStore);
}
var isJpxDecode = dictionary.TryGet(NameToken.Filter, out NameToken filterName) && filterName.Equals(NameToken.JpxDecode);
int bitsPerComponent;
if (isImageMask)
{
{
bitsPerComponent = 1;
}
else
@@ -74,17 +100,17 @@
{
throw new PdfDocumentFormatException($"No bits per component defined for image: {dictionary}.");
}
bitsPerComponent = bitsPerComponentToken.Int;
}
}
var intent = xObject.DefaultRenderingIntent;
if (dictionary.TryGet(NameToken.Intent, out NameToken renderingIntentToken))
{
intent = renderingIntentToken.Data.ToRenderingIntent();
}
}
var intent = xObject.DefaultRenderingIntent;
if (dictionary.TryGet(NameToken.Intent, out NameToken renderingIntentToken))
{
intent = renderingIntentToken.Data.ToRenderingIntent();
}
var interpolate = dictionary.TryGet(NameToken.Interpolate, out BooleanToken? interpolateToken)
&& interpolateToken.Data;
@@ -99,55 +125,56 @@
}
}
var streamToken = new StreamToken(dictionary, xObject.Stream.Data);
var decodedBytes = supportsFilters ? new Lazy<ReadOnlyMemory<byte>>(() => streamToken.Decode(filterProvider, pdfScanner))
: null;
var decode = Array.Empty<double>();
if (dictionary.TryGet(NameToken.Decode, out ArrayToken decodeArrayToken))
{
decode = decodeArrayToken.Data.OfType<NumericToken>()
.Select(x => x.Double)
.ToArray();
}
var streamToken = new StreamToken(dictionary, xObject.Stream.Data);
ColorSpaceDetails? details = null;
if (!isImageMask)
{
var decodedBytes = supportsFilters ? new Lazy<ReadOnlyMemory<byte>>(() => streamToken.Decode(filterProvider, pdfScanner))
: null;
var decode = Array.Empty<double>();
if (dictionary.TryGet(NameToken.Decode, out ArrayToken decodeArrayToken))
{
decode = decodeArrayToken.Data.OfType<NumericToken>()
.Select(x => x.Double)
.ToArray();
}
ColorSpaceDetails? details = null;
if (!isImageMask)
{
if (dictionary.TryGet(NameToken.ColorSpace, out NameToken? colorSpaceNameToken))
{
details = resourceStore.GetColorSpaceDetails(colorSpaceNameToken, dictionary);
}
details = resourceStore.GetColorSpaceDetails(colorSpaceNameToken, dictionary);
}
else if (dictionary.TryGet(NameToken.ColorSpace, out ArrayToken? colorSpaceArrayToken)
&& colorSpaceArrayToken.Length > 0 && colorSpaceArrayToken.Data[0] is NameToken firstColorSpaceName)
&& colorSpaceArrayToken.Length > 0 && colorSpaceArrayToken.Data[0] is NameToken firstColorSpaceName)
{
details = resourceStore.GetColorSpaceDetails(firstColorSpaceName, dictionary);
}
else if (!isJpxDecode)
{
details = xObject.DefaultColorSpace;
}
}
else if (!isJpxDecode)
{
details = xObject.DefaultColorSpace;
}
}
else
{
details = resourceStore.GetColorSpaceDetails(null, dictionary);
}
return new XObjectImage(
bounds,
width,
height,
bitsPerComponent,
isJpxDecode,
isImageMask,
intent,
interpolate,
decode,
dictionary,
xObject.Stream.Data,
decodedBytes,
details);
}
}
}
}
return new XObjectImage(
bounds,
width,
height,
bitsPerComponent,
isJpxDecode,
isImageMask,
intent,
interpolate,
decode,
dictionary,
xObject.Stream.Data,
decodedBytes,
details,
softMaskImage);
}
}
}

View File

@@ -64,6 +64,9 @@
/// <inheritdoc />
public ColorSpaceDetails? ColorSpaceDetails { get; }
/// <inheritdoc />
public IPdfImage? SoftMaskImage { get; }
/// <summary>
/// Creates a new <see cref="XObjectImage"/>.
/// </summary>
@@ -79,7 +82,8 @@
DictionaryToken imageDictionary,
ReadOnlyMemory<byte> rawMemory,
Lazy<ReadOnlyMemory<byte>>? bytes,
ColorSpaceDetails? colorSpaceDetails)
ColorSpaceDetails? colorSpaceDetails,
IPdfImage? softMaskImage)
{
Bounds = bounds;
WidthInSamples = widthInSamples;
@@ -94,6 +98,7 @@
RawMemory = rawMemory;
ColorSpaceDetails = colorSpaceDetails;
memoryFactory = bytes;
SoftMaskImage = softMaskImage;
}
/// <inheritdoc />

View File

@@ -3,7 +3,7 @@
/// <summary>
/// XObject type.
/// </summary>
public enum XObjectType
public enum XObjectType : byte
{
/// <summary>
/// Image.