mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-06-28 15:30:17 +08:00
Add early support for Stencil masking, rename SoftMaskImage property into MaskImage and make sure IsInlineImage is true for InlineImage
This commit is contained in:
parent
0bed135bad
commit
4dab2ef239
Binary file not shown.
@ -3,7 +3,7 @@
|
|||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
public class SoftMaskTests
|
public class MaskTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PigProductionHandbook()
|
public void PigProductionHandbook()
|
||||||
@ -17,7 +17,7 @@
|
|||||||
var images = page.GetImages().ToArray();
|
var images = page.GetImages().ToArray();
|
||||||
|
|
||||||
var image1 = images[1];
|
var image1 = images[1];
|
||||||
Assert.NotNull(image1.SoftMaskImage);
|
Assert.NotNull(image1.MaskImage);
|
||||||
Assert.True(image1.TryGetPng(out var png1));
|
Assert.True(image1.TryGetPng(out var png1));
|
||||||
using (var skImage1 = SKImage.FromEncodedData(png1))
|
using (var skImage1 = SKImage.FromEncodedData(png1))
|
||||||
using (var skBitmap1 = SKBitmap.FromImage(skImage1))
|
using (var skBitmap1 = SKBitmap.FromImage(skImage1))
|
||||||
@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var image2 = images[2];
|
var image2 = images[2];
|
||||||
Assert.NotNull(image2.SoftMaskImage);
|
Assert.NotNull(image2.MaskImage);
|
||||||
Assert.True(image2.TryGetPng(out var png2));
|
Assert.True(image2.TryGetPng(out var png2));
|
||||||
using (var skImage2 = SKImage.FromEncodedData(png2))
|
using (var skImage2 = SKImage.FromEncodedData(png2))
|
||||||
using (var skBitmap2 = SKBitmap.FromImage(skImage2))
|
using (var skBitmap2 = SKBitmap.FromImage(skImage2))
|
||||||
@ -37,5 +37,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MOZILLA_LINK_3264_0()
|
||||||
|
{
|
||||||
|
var path = IntegrationHelpers.GetDocumentPath("MOZILLA-LINK-3264-0.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.MaskImage);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
page = document.GetPage(2);
|
||||||
|
|
||||||
|
images = page.GetImages().ToArray();
|
||||||
|
|
||||||
|
var image2 = images[1];
|
||||||
|
Assert.NotNull(image2.MaskImage);
|
||||||
|
Assert.True(image2.TryGetPng(out var png2));
|
||||||
|
// TODO - Check alpha value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -80,6 +80,7 @@
|
|||||||
"UglyToad.PdfPig.Content.InlineImage",
|
"UglyToad.PdfPig.Content.InlineImage",
|
||||||
"UglyToad.PdfPig.Content.IPageFactory`1",
|
"UglyToad.PdfPig.Content.IPageFactory`1",
|
||||||
"UglyToad.PdfPig.Content.IPdfImage",
|
"UglyToad.PdfPig.Content.IPdfImage",
|
||||||
|
"UglyToad.PdfPig.Content.PdfImageExtensions",
|
||||||
"UglyToad.PdfPig.Content.IResourceStore",
|
"UglyToad.PdfPig.Content.IResourceStore",
|
||||||
"UglyToad.PdfPig.Content.Letter",
|
"UglyToad.PdfPig.Content.Letter",
|
||||||
"UglyToad.PdfPig.Content.MarkedContentElement",
|
"UglyToad.PdfPig.Content.MarkedContentElement",
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
public ReadOnlyMemory<byte> DecodedBytes { get; set; }
|
public ReadOnlyMemory<byte> DecodedBytes { get; set; }
|
||||||
|
|
||||||
public IPdfImage? SoftMaskImage { get; }
|
public IPdfImage? MaskImage { get; }
|
||||||
|
|
||||||
public bool TryGetBytesAsMemory(out ReadOnlyMemory<byte> bytes)
|
public bool TryGetBytesAsMemory(out ReadOnlyMemory<byte> bytes)
|
||||||
{
|
{
|
||||||
|
@ -95,9 +95,10 @@
|
|||||||
ColorSpaceDetails? ColorSpaceDetails { get; }
|
ColorSpaceDetails? ColorSpaceDetails { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Soft-mask image.
|
/// The image mask.
|
||||||
|
/// <para>Either a Soft-mask or a Stencil mask.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IPdfImage? SoftMaskImage { get; }
|
IPdfImage? MaskImage { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the decoded memory of the image if applicable. For JPEG images and some other types the
|
/// Get the decoded memory of the image if applicable. For JPEG images and some other types the
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
public ColorSpaceDetails ColorSpaceDetails { get; }
|
public ColorSpaceDetails ColorSpaceDetails { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IPdfImage? SoftMaskImage { get; }
|
public IPdfImage? MaskImage { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="InlineImage"/>.
|
/// Create a new <see cref="InlineImage"/>.
|
||||||
@ -78,6 +78,7 @@
|
|||||||
ColorSpaceDetails colorSpaceDetails,
|
ColorSpaceDetails colorSpaceDetails,
|
||||||
IPdfImage? softMaskImage)
|
IPdfImage? softMaskImage)
|
||||||
{
|
{
|
||||||
|
IsInlineImage = true;
|
||||||
Bounds = bounds;
|
Bounds = bounds;
|
||||||
WidthInSamples = widthInSamples;
|
WidthInSamples = widthInSamples;
|
||||||
HeightInSamples = heightInSamples;
|
HeightInSamples = heightInSamples;
|
||||||
@ -114,7 +115,7 @@
|
|||||||
return b;
|
return b;
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
SoftMaskImage = softMaskImage;
|
MaskImage = softMaskImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
22
src/UglyToad.PdfPig/Content/PdfImageExtensions.cs
Normal file
22
src/UglyToad.PdfPig/Content/PdfImageExtensions.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
namespace UglyToad.PdfPig.Content
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pdf image extensions.
|
||||||
|
/// </summary>
|
||||||
|
public static class PdfImageExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <c>true</c> if the image colors needs to be reversed based on the Decode array and color space. <c>false</c> otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public static bool NeedsReverseDecode(this IPdfImage pdfImage)
|
||||||
|
{
|
||||||
|
if (pdfImage.ColorSpaceDetails?.IsStencil == true)
|
||||||
|
{
|
||||||
|
// Stencil color space already takes care of reversing.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfImage.Decode.Count >= 2 && pdfImage.Decode[0] == 1 && pdfImage.Decode[1] == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,12 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ColorSpaceDetails
|
public abstract class ColorSpaceDetails
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Is the color space a stencil indexed color space.
|
||||||
|
/// <para>Stencil color spaces take care of inverting colors based on the Decode array.</para>
|
||||||
|
/// </summary>
|
||||||
|
public bool IsStencil { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type of the ColorSpace.
|
/// The type of the ColorSpace.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -39,10 +45,11 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="ColorSpaceDetails"/>.
|
/// Create a new <see cref="ColorSpaceDetails"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ColorSpaceDetails(ColorSpace type)
|
protected internal ColorSpaceDetails(ColorSpace type, bool isStencil = false)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
BaseType = type;
|
BaseType = type;
|
||||||
|
IsStencil = isStencil;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -279,7 +286,7 @@
|
|||||||
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, double[] decode)
|
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, double[] decode)
|
||||||
{
|
{
|
||||||
var blackIsOne = decode.Length >= 2 && decode[0] == 1 && decode[1] == 0;
|
var blackIsOne = decode.Length >= 2 && decode[0] == 1 && decode[1] == 0;
|
||||||
return new IndexedColorSpaceDetails(colorSpaceDetails, 1, blackIsOne ? [255, 0] : [0, 255]);
|
return new IndexedColorSpaceDetails(colorSpaceDetails, 1, blackIsOne ? [255, 0] : [0, 255], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -310,11 +317,15 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlySpan<byte> ColorTable => colorTable;
|
public ReadOnlySpan<byte> ColorTable => colorTable;
|
||||||
|
|
||||||
|
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable)
|
||||||
|
: this(baseColorSpaceDetails, hiVal, colorTable, false)
|
||||||
|
{ }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="IndexedColorSpaceDetails"/>.
|
/// Create a new <see cref="IndexedColorSpaceDetails"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable)
|
private IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable, bool isStencil)
|
||||||
: base(ColorSpace.Indexed)
|
: base(ColorSpace.Indexed, isStencil)
|
||||||
{
|
{
|
||||||
BaseColorSpace = baseColorSpaceDetails ?? throw new ArgumentNullException(nameof(baseColorSpaceDetails));
|
BaseColorSpace = baseColorSpaceDetails ?? throw new ArgumentNullException(nameof(baseColorSpaceDetails));
|
||||||
HiVal = hiVal;
|
HiVal = hiVal;
|
||||||
|
@ -11,6 +11,19 @@
|
|||||||
{
|
{
|
||||||
bytes = ReadOnlySpan<byte>.Empty;
|
bytes = ReadOnlySpan<byte>.Empty;
|
||||||
|
|
||||||
|
if (image.MaskImage is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because we cannot resize images directly in PdfPig, we only
|
||||||
|
// apply the mask if it has the same size as the image
|
||||||
|
if (image.HeightInSamples != image.MaskImage.HeightInSamples ||
|
||||||
|
image.WidthInSamples != image.MaskImage.WidthInSamples)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!image.TryGetBytesAsMemory(out var imageMemory))
|
if (!image.TryGetBytesAsMemory(out var imageMemory))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@ -74,9 +87,24 @@
|
|||||||
var numberOfComponents = image.ColorSpaceDetails!.BaseNumberOfColorComponents;
|
var numberOfComponents = image.ColorSpaceDetails!.BaseNumberOfColorComponents;
|
||||||
|
|
||||||
ReadOnlySpan<byte> softMask = null;
|
ReadOnlySpan<byte> softMask = null;
|
||||||
bool isSoftMask = image.SoftMaskImage is not null && TryGenerateSoftMask(image.SoftMaskImage, out softMask);
|
|
||||||
|
|
||||||
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, isSoftMask);
|
bool hasMask = TryGenerateSoftMask(image, out softMask);
|
||||||
|
Func<int, byte> getAlphaChannel = _ => byte.MaxValue;
|
||||||
|
|
||||||
|
if (hasMask)
|
||||||
|
{
|
||||||
|
byte[] softMaskBytes = softMask.ToArray();
|
||||||
|
if (image.MaskImage!.NeedsReverseDecode())
|
||||||
|
{
|
||||||
|
getAlphaChannel = i => Convert.ToByte(255 - softMaskBytes[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
getAlphaChannel = i => softMaskBytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, hasMask);
|
||||||
|
|
||||||
if (!IsCorrectlySized(image, bytesPure))
|
if (!IsCorrectlySized(image, bytesPure))
|
||||||
{
|
{
|
||||||
@ -98,7 +126,7 @@
|
|||||||
* B = 255 × (1-Y) × (1-K)
|
* B = 255 × (1-Y) × (1-K)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
byte a = isSoftMask ? softMask[sm++] : byte.MaxValue;
|
byte a = getAlphaChannel(sm++);
|
||||||
double c = (bytesPure[i++] / 255d);
|
double c = (bytesPure[i++] / 255d);
|
||||||
double m = (bytesPure[i++] / 255d);
|
double m = (bytesPure[i++] / 255d);
|
||||||
double y = (bytesPure[i++] / 255d);
|
double y = (bytesPure[i++] / 255d);
|
||||||
@ -119,7 +147,7 @@
|
|||||||
{
|
{
|
||||||
for (int row = 0; row < image.WidthInSamples; row++)
|
for (int row = 0; row < image.WidthInSamples; row++)
|
||||||
{
|
{
|
||||||
byte a = isSoftMask ? softMask[sm++] : byte.MaxValue;
|
byte a = getAlphaChannel(sm++);
|
||||||
builder.SetPixel(new Pixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], a, false), row, col);
|
builder.SetPixel(new Pixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], a, false), row, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +159,7 @@
|
|||||||
{
|
{
|
||||||
for (int row = 0; row < image.WidthInSamples; row++)
|
for (int row = 0; row < image.WidthInSamples; row++)
|
||||||
{
|
{
|
||||||
byte a = isSoftMask ? softMask[i] : byte.MaxValue;
|
byte a = getAlphaChannel(i);
|
||||||
byte pixel = bytesPure[i++];
|
byte pixel = bytesPure[i++];
|
||||||
builder.SetPixel(new Pixel(pixel, pixel, pixel, a, false), row, col);
|
builder.SetPixel(new Pixel(pixel, pixel, pixel, a, false), row, col);
|
||||||
}
|
}
|
||||||
|
@ -63,13 +63,31 @@
|
|||||||
throw new Exception("The SMask dictionary contains a 'Mask' or 'Smask' entry.");
|
throw new Exception("The SMask dictionary contains a 'Mask' or 'Smask' entry.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var renderingIntent = xObject.DefaultRenderingIntent; // Ignored
|
XObjectContentRecord softMaskImageRecord = new XObjectContentRecord(XObjectType.Image,
|
||||||
|
sMaskToken,
|
||||||
XObjectContentRecord softMaskImageRecord = new XObjectContentRecord(XObjectType.Image, sMaskToken, TransformationMatrix.Identity,
|
TransformationMatrix.Identity,
|
||||||
renderingIntent, DeviceGrayColorSpaceDetails.Instance);
|
xObject.DefaultRenderingIntent, // Ignored
|
||||||
|
DeviceGrayColorSpaceDetails.Instance);
|
||||||
|
|
||||||
softMaskImage = ReadImage(softMaskImageRecord, pdfScanner, filterProvider, resourceStore);
|
softMaskImage = ReadImage(softMaskImageRecord, pdfScanner, filterProvider, resourceStore);
|
||||||
}
|
}
|
||||||
|
else if (dictionary.TryGet(NameToken.Mask, out StreamToken maskStream))
|
||||||
|
{
|
||||||
|
if (maskStream.StreamDictionary.ContainsKey(NameToken.ColorSpace))
|
||||||
|
{
|
||||||
|
throw new Exception("The SMask dictionary contains a 'ColorSpace'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stencil masking
|
||||||
|
XObjectContentRecord maskImageRecord = new XObjectContentRecord(XObjectType.Image,
|
||||||
|
maskStream,
|
||||||
|
TransformationMatrix.Identity,
|
||||||
|
xObject.DefaultRenderingIntent,
|
||||||
|
null);
|
||||||
|
|
||||||
|
softMaskImage = ReadImage(maskImageRecord, pdfScanner, filterProvider, resourceStore);
|
||||||
|
System.Diagnostics.Debug.Assert(softMaskImage.ColorSpaceDetails?.IsStencil == true);
|
||||||
|
}
|
||||||
|
|
||||||
var isJpxDecode = dictionary.TryGet(NameToken.Filter, out NameToken filterName) && filterName.Equals(NameToken.JpxDecode);
|
var isJpxDecode = dictionary.TryGet(NameToken.Filter, out NameToken filterName) && filterName.Equals(NameToken.JpxDecode);
|
||||||
|
|
||||||
@ -125,9 +143,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var streamToken = new StreamToken(dictionary, xObject.Stream.Data);
|
var decodedBytes = supportsFilters ? new Lazy<ReadOnlyMemory<byte>>(() => xObject.Stream.Decode(filterProvider, pdfScanner))
|
||||||
|
|
||||||
var decodedBytes = supportsFilters ? new Lazy<ReadOnlyMemory<byte>>(() => streamToken.Decode(filterProvider, pdfScanner))
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var decode = Array.Empty<double>();
|
var decode = Array.Empty<double>();
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
public ColorSpaceDetails? ColorSpaceDetails { get; }
|
public ColorSpaceDetails? ColorSpaceDetails { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IPdfImage? SoftMaskImage { get; }
|
public IPdfImage? MaskImage { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="XObjectImage"/>.
|
/// Creates a new <see cref="XObjectImage"/>.
|
||||||
@ -98,7 +98,7 @@
|
|||||||
RawMemory = rawMemory;
|
RawMemory = rawMemory;
|
||||||
ColorSpaceDetails = colorSpaceDetails;
|
ColorSpaceDetails = colorSpaceDetails;
|
||||||
memoryFactory = bytes;
|
memoryFactory = bytes;
|
||||||
SoftMaskImage = softMaskImage;
|
MaskImage = softMaskImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
Loading…
Reference in New Issue
Block a user