Skip to content

Commit 236ebee

Browse files
committed
Handle SoftMask
1 parent 74d61bd commit 236ebee

File tree

14 files changed

+578
-135
lines changed

14 files changed

+578
-135
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace UglyToad.PdfPig.Tests.Integration
2+
{
3+
using SkiaSharp;
4+
using System.Linq;
5+
6+
public class SoftMaskTests
7+
{
8+
[Fact]
9+
public void PigProductionHandbook()
10+
{
11+
var path = IntegrationHelpers.GetDocumentPath("Pig Production Handbook.pdf");
12+
13+
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true, SkipMissingFonts = true }))
14+
{
15+
var page = document.GetPage(1);
16+
17+
var images = page.GetImages().ToArray();
18+
19+
var image1 = images[1];
20+
Assert.NotNull(image1.SoftMaskImage);
21+
Assert.True(image1.TryGetPng(out var png1));
22+
using (var skImage1 = SKImage.FromEncodedData(png1))
23+
using (var skBitmap1 = SKBitmap.FromImage(skImage1))
24+
{
25+
var pixel = skBitmap1.GetPixel(0, 0);
26+
Assert.Equal(0, pixel.Alpha);
27+
}
28+
29+
var image2 = images[2];
30+
Assert.NotNull(image2.SoftMaskImage);
31+
Assert.True(image2.TryGetPng(out var png2));
32+
using (var skImage2 = SKImage.FromEncodedData(png2))
33+
using (var skBitmap2 = SKBitmap.FromImage(skImage2))
34+
{
35+
var pixel = skBitmap2.GetPixel(0, 0);
36+
Assert.Equal(0, pixel.Alpha);
37+
}
38+
}
39+
}
40+
}
41+
}

src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public void OnlyExposedApiIsPublic()
161161
"UglyToad.PdfPig.Graphics.Colors.LatticeFormGouraudShading",
162162
"UglyToad.PdfPig.Graphics.Colors.CoonsPatchMeshesShading",
163163
"UglyToad.PdfPig.Graphics.Colors.TensorProductPatchMeshesShading",
164+
"UglyToad.PdfPig.Graphics.Core.BlendMode",
164165
"UglyToad.PdfPig.Graphics.Core.LineCapStyle",
165166
"UglyToad.PdfPig.Graphics.Core.LineDashPattern",
166167
"UglyToad.PdfPig.Graphics.Core.LineJoinStyle",
@@ -245,6 +246,8 @@ public void OnlyExposedApiIsPublic()
245246
"UglyToad.PdfPig.Graphics.Operations.TextState.Type3SetGlyphWidth",
246247
"UglyToad.PdfPig.Graphics.Operations.TextState.Type3SetGlyphWidthAndBoundingBox",
247248
"UglyToad.PdfPig.Graphics.PerformantRectangleTransformer",
249+
"UglyToad.PdfPig.Graphics.SoftMask",
250+
"UglyToad.PdfPig.Graphics.SoftMaskType",
248251
"UglyToad.PdfPig.Graphics.TextMatrices",
249252
"UglyToad.PdfPig.Graphics.XObjectContentRecord",
250253
"UglyToad.PdfPig.Images.ColorSpaceDetailsByteConverter",

src/UglyToad.PdfPig.Tests/TestPdfImage.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class TestPdfImage : IPdfImage
3737

3838
public ReadOnlyMemory<byte> DecodedBytes { get; set; }
3939

40+
public IPdfImage? SoftMaskImage { get; }
41+
4042
public bool TryGetBytesAsMemory(out ReadOnlyMemory<byte> bytes)
4143
{
4244
bytes = DecodedBytes;

src/UglyToad.PdfPig/Content/IPdfImage.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ public interface IPdfImage
9494
/// </summary>
9595
ColorSpaceDetails? ColorSpaceDetails { get; }
9696

97+
/// <summary>
98+
/// Soft-mask image.
99+
/// </summary>
100+
IPdfImage? SoftMaskImage { get; }
101+
97102
/// <summary>
98103
/// Get the decoded memory of the image if applicable. For JPEG images and some other types the
99104
/// <see cref="RawMemory"/> should be used directly.

src/UglyToad.PdfPig/Content/InlineImage.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,28 @@ public class InlineImage : IPdfImage
5555
public ReadOnlySpan<byte> RawBytes => RawMemory.Span;
5656

5757
/// <inheritdoc />
58-
public ColorSpaceDetails ColorSpaceDetails { get; }
58+
public ColorSpaceDetails ColorSpaceDetails { get; }
59+
60+
/// <inheritdoc />
61+
public IPdfImage? SoftMaskImage { get; }
5962

6063
/// <summary>
6164
/// Create a new <see cref="InlineImage"/>.
6265
/// </summary>
63-
internal InlineImage(PdfRectangle bounds, int widthInSamples, int heightInSamples, int bitsPerComponent, bool isImageMask,
66+
internal InlineImage(PdfRectangle bounds,
67+
int widthInSamples,
68+
int heightInSamples,
69+
int bitsPerComponent,
70+
bool isImageMask,
6471
RenderingIntent renderingIntent,
6572
bool interpolate,
6673
IReadOnlyList<double> decode,
6774
ReadOnlyMemory<byte> rawMemory,
6875
ILookupFilterProvider filterProvider,
6976
IReadOnlyList<NameToken> filterNames,
7077
DictionaryToken streamDictionary,
71-
ColorSpaceDetails colorSpaceDetails)
78+
ColorSpaceDetails colorSpaceDetails,
79+
IPdfImage? softMaskImage)
7280
{
7381
Bounds = bounds;
7482
WidthInSamples = widthInSamples;
@@ -104,7 +112,9 @@ internal InlineImage(PdfRectangle bounds, int widthInSamples, int heightInSample
104112
}
105113

106114
return b;
107-
}) : null;
115+
}) : null;
116+
117+
SoftMaskImage = softMaskImage;
108118
}
109119

110120
/// <inheritdoc />

src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -495,22 +495,19 @@ protected virtual void ProcessFormXObject(StreamToken formStream, NameToken xObj
495495
$"Invalid Transparency Group XObject, '{NameToken.S}' token is not set or not equal to '{NameToken.Transparency}'.");
496496
}
497497

498-
/* blend mode
499-
* A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
500-
* transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: Normal.
501-
*/
502-
//startState.BlendMode = BlendMode.Normal;
503-
504-
/* soft mask
505-
* A conforming reader shall implicitly reset this parameter implicitly reset to its initial value at the beginning
506-
* of execution of a transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: None.
507-
*/
508-
// TODO
509-
510-
/* alpha constant
511-
* A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
512-
* transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: 1.0.
513-
*/
498+
// Blend mode
499+
// A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
500+
// transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: Normal.
501+
startState.BlendMode = BlendMode.Normal;
502+
503+
// Soft mask
504+
// A conforming reader shall implicitly reset this parameter implicitly reset to its initial value at the beginning
505+
// of execution of a transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: None.
506+
startState.SoftMask = null;
507+
508+
// Alpha constant
509+
// A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
510+
// transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: 1.0.
514511
startState.AlphaConstantNonStroking = 1.0;
515512
startState.AlphaConstantStroking = 1.0;
516513

@@ -765,6 +762,49 @@ public virtual void SetNamedGraphicsState(NameToken stateName)
765762
// (see Section 6.5.4, “Automatic Stroke Adjustment”).
766763
currentGraphicsState.StrokeAdjustment = saToken.Data;
767764
}
765+
766+
// (PDF 1.4, array is deprecated in PDF 2.0) The current blend mode that shall be
767+
// used in the transparent imaging model (see 11.3.5, "Blend mode"). A PDF reader
768+
// shall implicitly reset this parameter to its initial value at the (array is
769+
// deprecated beginning of execution of a transparency group XObject
770+
// (see 11.6.6, in PDF 2.0) "Transparency group XObjects"). The value shall be
771+
// either a name object, designating one of the standard blend modes listed in
772+
// "Table 134 — Standard separable blend modes" and "Table 135 — Standard
773+
// non-separable blend modes" in 11.3.5, "Blend mode", or an array of such names.
774+
// In the latter case, the PDF reader shall use the first blend mode in the array
775+
// that it recognises (or Normal if it recognises none of them).
776+
//
777+
// Initial value: Normal.
778+
if (state.TryGet(NameToken.Bm, PdfScanner, out NameToken? bmToken))
779+
{
780+
currentGraphicsState.BlendMode = bmToken.Data.ToBlendMode() ?? BlendMode.Normal;
781+
}
782+
else if (state.TryGet(NameToken.Bm, PdfScanner, out ArrayToken? bmArrayToken))
783+
{
784+
// The PDF reader shall use the first blend mode in the array that it
785+
// recognises (or Normal if it recognises none of them).
786+
787+
currentGraphicsState.BlendMode = BlendMode.Normal;
788+
789+
foreach (var token in bmArrayToken.Data.OfType<NameToken>())
790+
{
791+
var bm = token.Data.ToBlendMode();
792+
if (bm.HasValue)
793+
{
794+
currentGraphicsState.BlendMode = bm.Value;
795+
break;
796+
}
797+
}
798+
}
799+
800+
if (state.TryGet(NameToken.Smask, PdfScanner, out NameToken? smToken) && smToken.Equals(NameToken.None))
801+
{
802+
currentGraphicsState.SoftMask = null;
803+
}
804+
else if (state.TryGet(NameToken.Smask, PdfScanner, out DictionaryToken? smDictToken))
805+
{
806+
currentGraphicsState.SoftMask = SoftMask.Parse(smDictToken, PdfScanner, FilterProvider);
807+
}
768808
}
769809

770810
/// <inheritdoc/>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
namespace UglyToad.PdfPig.Graphics.Core
2+
{
3+
/// <summary>
4+
/// The blend mode.
5+
/// </summary>
6+
public enum BlendMode : byte
7+
{
8+
// 11.3.5.2 Separable blend modes
9+
10+
/// <summary>
11+
/// Default.
12+
/// <para>Same as Compatible.</para>
13+
/// </summary>
14+
Normal = 0,
15+
Multiply = 1,
16+
Screen = 2,
17+
Darken = 3,
18+
Lighten = 4,
19+
ColorDodge = 5,
20+
ColorBurn = 6,
21+
HardLight = 7,
22+
SoftLight = 8,
23+
Overlay = 9,
24+
Difference = 10,
25+
Exclusion = 11,
26+
27+
// 11.3.5.3 Non-separable blend modes
28+
Hue = 12,
29+
Saturation = 13,
30+
Color = 14,
31+
Luminosity = 15
32+
}
33+
34+
internal static class BlendModeExtensions
35+
{
36+
public static BlendMode? ToBlendMode(this string s)
37+
{
38+
return s switch
39+
{
40+
// 11.3.5.2 Separable blend modes
41+
"Normal" => BlendMode.Normal,
42+
"Compatible" => BlendMode.Normal,
43+
"Multiply" => BlendMode.Multiply,
44+
"Screen" => BlendMode.Screen,
45+
"Darken" => BlendMode.Darken,
46+
"Lighten" => BlendMode.Lighten,
47+
"ColorDodge" => BlendMode.ColorDodge,
48+
"ColorBurn" => BlendMode.ColorBurn,
49+
"HardLight" => BlendMode.HardLight,
50+
"SoftLight" => BlendMode.SoftLight,
51+
"Overlay" => BlendMode.Overlay,
52+
"Difference" => BlendMode.Difference,
53+
"Exclusion" => BlendMode.Exclusion,
54+
55+
// 11.3.5.3 Non-separable blend modes
56+
"Hue" => BlendMode.Hue,
57+
"Saturation" => BlendMode.Saturation,
58+
"Color" => BlendMode.Color,
59+
"Luminosity" => BlendMode.Luminosity,
60+
61+
_ => null
62+
};
63+
}
64+
}
65+
}

src/UglyToad.PdfPig/Graphics/CurrentGraphicsState.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,18 @@ public class CurrentGraphicsState : IDeepCloneable<CurrentGraphicsState>
7171
public double AlphaConstantNonStroking { get; set; } = 1;
7272

7373
/// <summary>
74-
/// Should soft mask and alpha constant values be interpreted as shape (<see langword="true"/>) or opacity (<see langword="false"/>) values?
74+
/// Should soft mask and alpha constant values be interpreted as shape
75+
/// (<see langword="true"/>) or opacity (<see langword="false"/>) values?
7576
/// </summary>
7677
public bool AlphaSource { get; set; } = false;
7778

79+
/// <summary>
80+
/// A soft-mask dictionary specifying the mask shape or mask opacity values
81+
/// that shall be used in the transparent imaging model, or the name None if
82+
/// no such mask is specified
83+
/// </summary>
84+
public SoftMask SoftMask { get; set; }
85+
7886
/// <summary>
7987
/// Maps positions from user coordinates to device coordinates.
8088
/// </summary>
@@ -95,6 +103,11 @@ public class CurrentGraphicsState : IDeepCloneable<CurrentGraphicsState>
95103
/// </summary>
96104
public IColor CurrentNonStrokingColor { get; set; }
97105

106+
/// <summary>
107+
/// The current blend mode.
108+
/// </summary>
109+
public BlendMode BlendMode { get; set; } = BlendMode.Normal;
110+
98111
#region Device Dependent
99112

100113
/// <summary>
@@ -151,6 +164,8 @@ public CurrentGraphicsState DeepClone()
151164
CurrentNonStrokingColor = CurrentNonStrokingColor,
152165
CurrentClippingPath = CurrentClippingPath,
153166
ColorSpaceContext = ColorSpaceContext?.DeepClone(),
167+
BlendMode = BlendMode,
168+
SoftMask = SoftMask
154169
};
155170
}
156171
}

src/UglyToad.PdfPig/Graphics/InlineImageBuilder.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
namespace UglyToad.PdfPig.Graphics
22
{
33
using System;
4+
using System.Collections;
45
using System.Collections.Generic;
56
using System.Linq;
7+
using System.Xml.Linq;
68
using Content;
79
using Core;
810
using Filters;
911
using PdfPig.Core;
1012
using Tokenization.Scanner;
1113
using Tokens;
14+
using UglyToad.PdfPig.Graphics.Colors;
15+
using UglyToad.PdfPig.XObjects;
1216

1317
/// <summary>
1418
/// Inline Image Builder.
@@ -49,9 +53,34 @@ internal InlineImage CreateInlineImage(
4953
var isMask = maskToken?.Data == true;
5054

5155
var bitsPerComponent = GetByKeys<NumericToken>(NameToken.BitsPerComponent, NameToken.Bpc, !isMask)?.Int ?? 1;
52-
5356
NameToken? colorSpaceName = null;
5457

58+
var imgDic = new DictionaryToken(Properties ?? new Dictionary<NameToken, IToken>());
59+
60+
XObjectImage? softMaskImage = null;
61+
if (imgDic.TryGet(NameToken.Smask, tokenScanner, out StreamToken? sMaskToken))
62+
{
63+
if (!sMaskToken.StreamDictionary.TryGet(NameToken.Subtype, out NameToken softMaskSubType) || !softMaskSubType.Equals(NameToken.Image))
64+
{
65+
throw new Exception("The SMask dictionary does not contain a 'Subtype' entry, or its value is not 'Image'.");
66+
}
67+
68+
if (!sMaskToken.StreamDictionary.TryGet(NameToken.ColorSpace, out NameToken softMaskColorSpace) || !softMaskColorSpace.Equals(NameToken.Devicegray))
69+
{
70+
throw new Exception("The SMask dictionary does not contain a 'ColorSpace' entry, or its value is not 'Devicegray'.");
71+
}
72+
73+
if (sMaskToken.StreamDictionary.ContainsKey(NameToken.Mask) || sMaskToken.StreamDictionary.ContainsKey(NameToken.Smask))
74+
{
75+
throw new Exception("The SMask dictionary contains a 'Mask' or 'Smask' entry.");
76+
}
77+
78+
XObjectContentRecord softMaskImageRecord = new XObjectContentRecord(XObjectType.Image, sMaskToken, TransformationMatrix.Identity,
79+
defaultRenderingIntent, DeviceGrayColorSpaceDetails.Instance);
80+
81+
softMaskImage = XObjectFactory.ReadImage(softMaskImageRecord, tokenScanner, filterProvider, resourceStore);
82+
}
83+
5584
if (!isMask)
5685
{
5786
colorSpaceName = GetByKeys<NameToken>(NameToken.ColorSpace, NameToken.Cs, false);
@@ -74,8 +103,6 @@ internal InlineImage CreateInlineImage(
74103
}
75104
}
76105

77-
var imgDic = new DictionaryToken(Properties ?? new Dictionary<NameToken, IToken>());
78-
79106
var details = resourceStore.GetColorSpaceDetails(colorSpaceName, imgDic);
80107

81108
var renderingIntent = GetByKeys<NameToken>(NameToken.Intent, null, false)?.Data?.ToRenderingIntent() ?? defaultRenderingIntent;
@@ -106,7 +133,7 @@ internal InlineImage CreateInlineImage(
106133

107134
return new InlineImage(bounds, width, height, bitsPerComponent,
108135
isMask, renderingIntent, interpolate, decode, Bytes,
109-
filterProvider, filterNames, imgDic, details);
136+
filterProvider, filterNames, imgDic, details, softMaskImage);
110137
}
111138

112139
#nullable disable

0 commit comments

Comments
 (0)