Skip to content

Commit 4dab2ef

Browse files
committed
Add early support for Stencil masking, rename SoftMaskImage property into MaskImage and make sure IsInlineImage is true for InlineImage
1 parent 0bed135 commit 4dab2ef

File tree

11 files changed

+182
-70
lines changed

11 files changed

+182
-70
lines changed
Binary file not shown.

src/UglyToad.PdfPig.Tests/Integration/SoftMaskTests.cs renamed to src/UglyToad.PdfPig.Tests/Integration/MaskTests.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using SkiaSharp;
44
using System.Linq;
55

6-
public class SoftMaskTests
6+
public class MaskTests
77
{
88
[Fact]
99
public void PigProductionHandbook()
@@ -17,7 +17,7 @@ public void PigProductionHandbook()
1717
var images = page.GetImages().ToArray();
1818

1919
var image1 = images[1];
20-
Assert.NotNull(image1.SoftMaskImage);
20+
Assert.NotNull(image1.MaskImage);
2121
Assert.True(image1.TryGetPng(out var png1));
2222
using (var skImage1 = SKImage.FromEncodedData(png1))
2323
using (var skBitmap1 = SKBitmap.FromImage(skImage1))
@@ -27,7 +27,7 @@ public void PigProductionHandbook()
2727
}
2828

2929
var image2 = images[2];
30-
Assert.NotNull(image2.SoftMaskImage);
30+
Assert.NotNull(image2.MaskImage);
3131
Assert.True(image2.TryGetPng(out var png2));
3232
using (var skImage2 = SKImage.FromEncodedData(png2))
3333
using (var skBitmap2 = SKBitmap.FromImage(skImage2))
@@ -37,5 +37,37 @@ public void PigProductionHandbook()
3737
}
3838
}
3939
}
40+
41+
[Fact]
42+
public void MOZILLA_LINK_3264_0()
43+
{
44+
var path = IntegrationHelpers.GetDocumentPath("MOZILLA-LINK-3264-0.pdf");
45+
46+
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true, SkipMissingFonts = true }))
47+
{
48+
var page = document.GetPage(1);
49+
50+
var images = page.GetImages().ToArray();
51+
52+
var image1 = images[1];
53+
Assert.NotNull(image1.MaskImage);
54+
Assert.True(image1.TryGetPng(out var png1));
55+
using (var skImage1 = SKImage.FromEncodedData(png1))
56+
using (var skBitmap1 = SKBitmap.FromImage(skImage1))
57+
{
58+
var pixel = skBitmap1.GetPixel(0, 0);
59+
Assert.Equal(0, pixel.Alpha);
60+
}
61+
62+
page = document.GetPage(2);
63+
64+
images = page.GetImages().ToArray();
65+
66+
var image2 = images[1];
67+
Assert.NotNull(image2.MaskImage);
68+
Assert.True(image2.TryGetPng(out var png2));
69+
// TODO - Check alpha value
70+
}
71+
}
4072
}
4173
}

src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public void OnlyExposedApiIsPublic()
8080
"UglyToad.PdfPig.Content.InlineImage",
8181
"UglyToad.PdfPig.Content.IPageFactory`1",
8282
"UglyToad.PdfPig.Content.IPdfImage",
83+
"UglyToad.PdfPig.Content.PdfImageExtensions",
8384
"UglyToad.PdfPig.Content.IResourceStore",
8485
"UglyToad.PdfPig.Content.Letter",
8586
"UglyToad.PdfPig.Content.MarkedContentElement",

src/UglyToad.PdfPig.Tests/TestPdfImage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class TestPdfImage : IPdfImage
3737

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

40-
public IPdfImage? SoftMaskImage { get; }
40+
public IPdfImage? MaskImage { get; }
4141

4242
public bool TryGetBytesAsMemory(out ReadOnlyMemory<byte> bytes)
4343
{

src/UglyToad.PdfPig/Content/IPdfImage.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,10 @@ public interface IPdfImage
9595
ColorSpaceDetails? ColorSpaceDetails { get; }
9696

9797
/// <summary>
98-
/// Soft-mask image.
98+
/// The image mask.
99+
/// <para>Either a Soft-mask or a Stencil mask.</para>
99100
/// </summary>
100-
IPdfImage? SoftMaskImage { get; }
101+
IPdfImage? MaskImage { get; }
101102

102103
/// <summary>
103104
/// Get the decoded memory of the image if applicable. For JPEG images and some other types the

src/UglyToad.PdfPig/Content/InlineImage.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public class InlineImage : IPdfImage
5858
public ColorSpaceDetails ColorSpaceDetails { get; }
5959

6060
/// <inheritdoc />
61-
public IPdfImage? SoftMaskImage { get; }
61+
public IPdfImage? MaskImage { get; }
6262

6363
/// <summary>
6464
/// Create a new <see cref="InlineImage"/>.
@@ -78,6 +78,7 @@ internal InlineImage(PdfRectangle bounds,
7878
ColorSpaceDetails colorSpaceDetails,
7979
IPdfImage? softMaskImage)
8080
{
81+
IsInlineImage = true;
8182
Bounds = bounds;
8283
WidthInSamples = widthInSamples;
8384
HeightInSamples = heightInSamples;
@@ -114,7 +115,7 @@ internal InlineImage(PdfRectangle bounds,
114115
return b;
115116
}) : null;
116117

117-
SoftMaskImage = softMaskImage;
118+
MaskImage = softMaskImage;
118119
}
119120

120121
/// <inheritdoc />
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace UglyToad.PdfPig.Content
2+
{
3+
/// <summary>
4+
/// Pdf image extensions.
5+
/// </summary>
6+
public static class PdfImageExtensions
7+
{
8+
/// <summary>
9+
/// <c>true</c> if the image colors needs to be reversed based on the Decode array and color space. <c>false</c> otherwise.
10+
/// </summary>
11+
public static bool NeedsReverseDecode(this IPdfImage pdfImage)
12+
{
13+
if (pdfImage.ColorSpaceDetails?.IsStencil == true)
14+
{
15+
// Stencil color space already takes care of reversing.
16+
return false;
17+
}
18+
19+
return pdfImage.Decode.Count >= 2 && pdfImage.Decode[0] == 1 && pdfImage.Decode[1] == 0;
20+
}
21+
}
22+
}

src/UglyToad.PdfPig/Graphics/Colors/ColorSpaceDetails.cs

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
/// </summary>
1616
public abstract class ColorSpaceDetails
1717
{
18+
/// <summary>
19+
/// Is the color space a stencil indexed color space.
20+
/// <para>Stencil color spaces take care of inverting colors based on the Decode array.</para>
21+
/// </summary>
22+
public bool IsStencil { get; }
23+
1824
/// <summary>
1925
/// The type of the ColorSpace.
2026
/// </summary>
@@ -39,10 +45,11 @@ public abstract class ColorSpaceDetails
3945
/// <summary>
4046
/// Create a new <see cref="ColorSpaceDetails"/>.
4147
/// </summary>
42-
protected internal ColorSpaceDetails(ColorSpace type)
48+
protected internal ColorSpaceDetails(ColorSpace type, bool isStencil = false)
4349
{
4450
Type = type;
4551
BaseType = type;
52+
IsStencil = isStencil;
4653
}
4754

4855
/// <summary>
@@ -279,7 +286,7 @@ public sealed class IndexedColorSpaceDetails : ColorSpaceDetails
279286
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, double[] decode)
280287
{
281288
var blackIsOne = decode.Length >= 2 && decode[0] == 1 && decode[1] == 0;
282-
return new IndexedColorSpaceDetails(colorSpaceDetails, 1, blackIsOne ? [255, 0] : [0, 255]);
289+
return new IndexedColorSpaceDetails(colorSpaceDetails, 1, blackIsOne ? [255, 0] : [0, 255], true);
283290
}
284291

285292
/// <inheritdoc/>
@@ -310,11 +317,15 @@ internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, d
310317
/// </summary>
311318
public ReadOnlySpan<byte> ColorTable => colorTable;
312319

320+
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable)
321+
: this(baseColorSpaceDetails, hiVal, colorTable, false)
322+
{ }
323+
313324
/// <summary>
314325
/// Create a new <see cref="IndexedColorSpaceDetails"/>.
315326
/// </summary>
316-
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable)
317-
: base(ColorSpace.Indexed)
327+
private IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable, bool isStencil)
328+
: base(ColorSpace.Indexed, isStencil)
318329
{
319330
BaseColorSpace = baseColorSpaceDetails ?? throw new ArgumentNullException(nameof(baseColorSpaceDetails));
320331
HiVal = hiVal;
@@ -367,56 +378,40 @@ internal Span<byte> UnwrapIndexedColorSpaceBytes(Span<byte> input)
367378
case ColorSpace.DeviceRGB:
368379
case ColorSpace.CalRGB:
369380
case ColorSpace.Lab:
370-
{
371-
Span<byte> result = new byte[input.Length * 3];
372-
var i = 0;
373-
foreach (var x in input)
374381
{
375-
for (var j = 0; j < 3; ++j)
382+
Span<byte> result = new byte[input.Length * 3];
383+
var i = 0;
384+
foreach (var x in input)
376385
{
377-
result[i++] = ColorTable[x * 3 + j];
386+
for (var j = 0; j < 3; ++j)
387+
{
388+
result[i++] = ColorTable[x * 3 + j];
389+
}
378390
}
379-
}
380391

381-
return result;
382-
}
392+
return result;
393+
}
383394

384395
case ColorSpace.DeviceCMYK:
385-
{
386-
Span<byte> result = new byte[input.Length * 4];
387-
var i = 0;
388-
foreach (var x in input)
389396
{
390-
for (var j = 0; j < 4; ++j)
397+
Span<byte> result = new byte[input.Length * 4];
398+
var i = 0;
399+
foreach (var x in input)
391400
{
392-
result[i++] = ColorTable[x * 4 + j];
401+
for (var j = 0; j < 4; ++j)
402+
{
403+
result[i++] = ColorTable[x * 4 + j];
404+
}
393405
}
394-
}
395406

396-
return result;
397-
}
407+
return result;
408+
}
398409

399410
case ColorSpace.DeviceGray:
400411
case ColorSpace.CalGray:
401412
case ColorSpace.Separation:
402-
{
403-
for (var i = 0; i < input.Length; ++i)
404413
{
405-
ref byte b = ref input[i];
406-
b = ColorTable[b];
407-
}
408-
409-
return input;
410-
}
411-
412-
case ColorSpace.DeviceN:
413-
case ColorSpace.ICCBased:
414-
{
415-
int i = 0;
416-
if (BaseColorSpace.NumberOfColorComponents == 1)
417-
{
418-
// In place
419-
for (i = 0; i < input.Length; ++i)
414+
for (var i = 0; i < input.Length; ++i)
420415
{
421416
ref byte b = ref input[i];
422417
b = ColorTable[b];
@@ -425,17 +420,33 @@ internal Span<byte> UnwrapIndexedColorSpaceBytes(Span<byte> input)
425420
return input;
426421
}
427422

428-
Span<byte> result = new byte[input.Length * BaseColorSpace.NumberOfColorComponents];
429-
foreach (var x in input)
423+
case ColorSpace.DeviceN:
424+
case ColorSpace.ICCBased:
430425
{
431-
for (var j = 0; j < BaseColorSpace.NumberOfColorComponents; ++j)
426+
int i = 0;
427+
if (BaseColorSpace.NumberOfColorComponents == 1)
432428
{
433-
result[i++] = ColorTable[x * BaseColorSpace.NumberOfColorComponents + j];
429+
// In place
430+
for (i = 0; i < input.Length; ++i)
431+
{
432+
ref byte b = ref input[i];
433+
b = ColorTable[b];
434+
}
435+
436+
return input;
434437
}
435-
}
436438

437-
return result;
438-
}
439+
Span<byte> result = new byte[input.Length * BaseColorSpace.NumberOfColorComponents];
440+
foreach (var x in input)
441+
{
442+
for (var j = 0; j < BaseColorSpace.NumberOfColorComponents; ++j)
443+
{
444+
result[i++] = ColorTable[x * BaseColorSpace.NumberOfColorComponents + j];
445+
}
446+
}
447+
448+
return result;
449+
}
439450
}
440451

441452
return input;

src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,20 @@ internal static class PngFromPdfImageFactory
99
{
1010
private static bool TryGenerateSoftMask(IPdfImage image, [NotNullWhen(true)] out ReadOnlySpan<byte> bytes)
1111
{
12-
bytes = ReadOnlySpan<byte>.Empty;
12+
bytes = ReadOnlySpan<byte>.Empty;
13+
14+
if (image.MaskImage is null)
15+
{
16+
return false;
17+
}
18+
19+
// Because we cannot resize images directly in PdfPig, we only
20+
// apply the mask if it has the same size as the image
21+
if (image.HeightInSamples != image.MaskImage.HeightInSamples ||
22+
image.WidthInSamples != image.MaskImage.WidthInSamples)
23+
{
24+
return false;
25+
}
1326

1427
if (!image.TryGetBytesAsMemory(out var imageMemory))
1528
{
@@ -74,9 +87,24 @@ public static bool TryGenerate(IPdfImage image, [NotNullWhen(true)] out byte[]?
7487
var numberOfComponents = image.ColorSpaceDetails!.BaseNumberOfColorComponents;
7588

7689
ReadOnlySpan<byte> softMask = null;
77-
bool isSoftMask = image.SoftMaskImage is not null && TryGenerateSoftMask(image.SoftMaskImage, out softMask);
78-
79-
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, isSoftMask);
90+
91+
bool hasMask = TryGenerateSoftMask(image, out softMask);
92+
Func<int, byte> getAlphaChannel = _ => byte.MaxValue;
93+
94+
if (hasMask)
95+
{
96+
byte[] softMaskBytes = softMask.ToArray();
97+
if (image.MaskImage!.NeedsReverseDecode())
98+
{
99+
getAlphaChannel = i => Convert.ToByte(255 - softMaskBytes[i]);
100+
}
101+
else
102+
{
103+
getAlphaChannel = i => softMaskBytes[i];
104+
}
105+
}
106+
107+
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, hasMask);
80108

81109
if (!IsCorrectlySized(image, bytesPure))
82110
{
@@ -98,7 +126,7 @@ public static bool TryGenerate(IPdfImage image, [NotNullWhen(true)] out byte[]?
98126
* B = 255 × (1-Y) × (1-K)
99127
*/
100128

101-
byte a = isSoftMask ? softMask[sm++] : byte.MaxValue;
129+
byte a = getAlphaChannel(sm++);
102130
double c = (bytesPure[i++] / 255d);
103131
double m = (bytesPure[i++] / 255d);
104132
double y = (bytesPure[i++] / 255d);
@@ -119,7 +147,7 @@ public static bool TryGenerate(IPdfImage image, [NotNullWhen(true)] out byte[]?
119147
{
120148
for (int row = 0; row < image.WidthInSamples; row++)
121149
{
122-
byte a = isSoftMask ? softMask[sm++] : byte.MaxValue;
150+
byte a = getAlphaChannel(sm++);
123151
builder.SetPixel(new Pixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], a, false), row, col);
124152
}
125153
}
@@ -131,7 +159,7 @@ public static bool TryGenerate(IPdfImage image, [NotNullWhen(true)] out byte[]?
131159
{
132160
for (int row = 0; row < image.WidthInSamples; row++)
133161
{
134-
byte a = isSoftMask ? softMask[i] : byte.MaxValue;
162+
byte a = getAlphaChannel(i);
135163
byte pixel = bytesPure[i++];
136164
builder.SetPixel(new Pixel(pixel, pixel, pixel, a, false), row, col);
137165
}

0 commit comments

Comments
 (0)