Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/UglyToad.PdfPig.Tests/Integration/SoftMaskTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
3 changes: 3 additions & 0 deletions src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ public void OnlyExposedApiIsPublic()
"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",
Expand Down Expand Up @@ -245,6 +246,8 @@ public void OnlyExposedApiIsPublic()
"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",
Expand Down
2 changes: 2 additions & 0 deletions src/UglyToad.PdfPig.Tests/TestPdfImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class TestPdfImage : IPdfImage

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

public IPdfImage? SoftMaskImage { get; }

public bool TryGetBytesAsMemory(out ReadOnlyMemory<byte> bytes)
{
bytes = DecodedBytes;
Expand Down
5 changes: 5 additions & 0 deletions src/UglyToad.PdfPig/Content/IPdfImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ public interface IPdfImage
/// </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.
Expand Down
18 changes: 14 additions & 4 deletions src/UglyToad.PdfPig/Content/InlineImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,28 @@ public class InlineImage : IPdfImage
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,
ReadOnlyMemory<byte> rawMemory,
ILookupFilterProvider filterProvider,
IReadOnlyList<NameToken> filterNames,
DictionaryToken streamDictionary,
ColorSpaceDetails colorSpaceDetails)
ColorSpaceDetails colorSpaceDetails,
IPdfImage? softMaskImage)
{
Bounds = bounds;
WidthInSamples = widthInSamples;
Expand Down Expand Up @@ -104,7 +112,9 @@ internal InlineImage(PdfRectangle bounds, int widthInSamples, int heightInSample
}

return b;
}) : null;
}) : null;

SoftMaskImage = softMaskImage;
}

/// <inheritdoc />
Expand Down
72 changes: 56 additions & 16 deletions src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,22 +495,19 @@ protected virtual void ProcessFormXObject(StreamToken formStream, NameToken xObj
$"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;

/* 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

/* 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.
*/
// 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.
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.
startState.AlphaConstantNonStroking = 1.0;
startState.AlphaConstantStroking = 1.0;

Expand Down Expand Up @@ -765,6 +762,49 @@ public virtual void SetNamedGraphicsState(NameToken stateName)
// (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/>
Expand Down
65 changes: 65 additions & 0 deletions src/UglyToad.PdfPig/Graphics/Core/BlendMode.cs
Original file line number Diff line number Diff line change
@@ -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
};
}
}
}
17 changes: 16 additions & 1 deletion src/UglyToad.PdfPig/Graphics/CurrentGraphicsState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,18 @@ public class CurrentGraphicsState : IDeepCloneable<CurrentGraphicsState>
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>
Expand All @@ -95,6 +103,11 @@ public class CurrentGraphicsState : IDeepCloneable<CurrentGraphicsState>
/// </summary>
public IColor CurrentNonStrokingColor { get; set; }

/// <summary>
/// The current blend mode.
/// </summary>
public BlendMode BlendMode { get; set; } = BlendMode.Normal;

#region Device Dependent

/// <summary>
Expand Down Expand Up @@ -151,6 +164,8 @@ public CurrentGraphicsState DeepClone()
CurrentNonStrokingColor = CurrentNonStrokingColor,
CurrentClippingPath = CurrentClippingPath,
ColorSpaceContext = ColorSpaceContext?.DeepClone(),
BlendMode = BlendMode,
SoftMask = SoftMask
};
}
}
Expand Down
35 changes: 31 additions & 4 deletions src/UglyToad.PdfPig/Graphics/InlineImageBuilder.cs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -49,9 +53,34 @@ internal InlineImage CreateInlineImage(
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);
Expand All @@ -74,8 +103,6 @@ internal InlineImage CreateInlineImage(
}
}

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;
Expand Down Expand Up @@ -106,7 +133,7 @@ internal InlineImage CreateInlineImage(

return new InlineImage(bounds, width, height, bitsPerComponent,
isMask, renderingIntent, interpolate, decode, Bytes,
filterProvider, filterNames, imgDic, details);
filterProvider, filterNames, imgDic, details, softMaskImage);
}

#nullable disable
Expand Down
Loading
Loading