mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Handle SoftMask
This commit is contained in:
41
src/UglyToad.PdfPig.Tests/Integration/SoftMaskTests.cs
Normal file
41
src/UglyToad.PdfPig.Tests/Integration/SoftMaskTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
|
||||
public ReadOnlyMemory<byte> DecodedBytes { get; set; }
|
||||
|
||||
public IPdfImage? SoftMaskImage { get; }
|
||||
|
||||
public bool TryGetBytesAsMemory(out ReadOnlyMemory<byte> bytes)
|
||||
{
|
||||
bytes = DecodedBytes;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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/>
|
||||
|
||||
65
src/UglyToad.PdfPig/Graphics/Core/BlendMode.cs
Normal file
65
src/UglyToad.PdfPig/Graphics/Core/BlendMode.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
166
src/UglyToad.PdfPig/Graphics/SoftMask.cs
Normal file
166
src/UglyToad.PdfPig/Graphics/SoftMask.cs
Normal 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 space’s 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 group’s computed alpha shall be used, disregarding its colour (see 11.5.2,
|
||||
* "Deriving a soft mask from group alpha"). Luminosity The group’s 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 space’s
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// XObject type.
|
||||
/// </summary>
|
||||
public enum XObjectType
|
||||
public enum XObjectType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Image.
|
||||
|
||||
Reference in New Issue
Block a user