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 System.Linq;
|
||||
|
||||
public class SoftMaskTests
|
||||
public class MaskTests
|
||||
{
|
||||
[Fact]
|
||||
public void PigProductionHandbook()
|
||||
@ -17,7 +17,7 @@
|
||||
var images = page.GetImages().ToArray();
|
||||
|
||||
var image1 = images[1];
|
||||
Assert.NotNull(image1.SoftMaskImage);
|
||||
Assert.NotNull(image1.MaskImage);
|
||||
Assert.True(image1.TryGetPng(out var png1));
|
||||
using (var skImage1 = SKImage.FromEncodedData(png1))
|
||||
using (var skBitmap1 = SKBitmap.FromImage(skImage1))
|
||||
@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
var image2 = images[2];
|
||||
Assert.NotNull(image2.SoftMaskImage);
|
||||
Assert.NotNull(image2.MaskImage);
|
||||
Assert.True(image2.TryGetPng(out var png2));
|
||||
using (var skImage2 = SKImage.FromEncodedData(png2))
|
||||
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.IPageFactory`1",
|
||||
"UglyToad.PdfPig.Content.IPdfImage",
|
||||
"UglyToad.PdfPig.Content.PdfImageExtensions",
|
||||
"UglyToad.PdfPig.Content.IResourceStore",
|
||||
"UglyToad.PdfPig.Content.Letter",
|
||||
"UglyToad.PdfPig.Content.MarkedContentElement",
|
||||
|
@ -37,7 +37,7 @@
|
||||
|
||||
public ReadOnlyMemory<byte> DecodedBytes { get; set; }
|
||||
|
||||
public IPdfImage? SoftMaskImage { get; }
|
||||
public IPdfImage? MaskImage { get; }
|
||||
|
||||
public bool TryGetBytesAsMemory(out ReadOnlyMemory<byte> bytes)
|
||||
{
|
||||
|
@ -95,9 +95,10 @@
|
||||
ColorSpaceDetails? ColorSpaceDetails { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Soft-mask image.
|
||||
/// The image mask.
|
||||
/// <para>Either a Soft-mask or a Stencil mask.</para>
|
||||
/// </summary>
|
||||
IPdfImage? SoftMaskImage { get; }
|
||||
IPdfImage? MaskImage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPdfImage? SoftMaskImage { get; }
|
||||
public IPdfImage? MaskImage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="InlineImage"/>.
|
||||
@ -78,6 +78,7 @@
|
||||
ColorSpaceDetails colorSpaceDetails,
|
||||
IPdfImage? softMaskImage)
|
||||
{
|
||||
IsInlineImage = true;
|
||||
Bounds = bounds;
|
||||
WidthInSamples = widthInSamples;
|
||||
HeightInSamples = heightInSamples;
|
||||
@ -114,7 +115,7 @@
|
||||
return b;
|
||||
}) : null;
|
||||
|
||||
SoftMaskImage = softMaskImage;
|
||||
MaskImage = softMaskImage;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
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>
|
||||
/// The type of the ColorSpace.
|
||||
/// </summary>
|
||||
@ -39,10 +45,11 @@
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ColorSpaceDetails"/>.
|
||||
/// </summary>
|
||||
protected internal ColorSpaceDetails(ColorSpace type)
|
||||
protected internal ColorSpaceDetails(ColorSpace type, bool isStencil = false)
|
||||
{
|
||||
Type = type;
|
||||
BaseType = type;
|
||||
IsStencil = isStencil;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -279,7 +286,7 @@
|
||||
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, double[] decode)
|
||||
{
|
||||
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/>
|
||||
@ -310,11 +317,15 @@
|
||||
/// </summary>
|
||||
public ReadOnlySpan<byte> ColorTable => colorTable;
|
||||
|
||||
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable)
|
||||
: this(baseColorSpaceDetails, hiVal, colorTable, false)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="IndexedColorSpaceDetails"/>.
|
||||
/// </summary>
|
||||
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable)
|
||||
: base(ColorSpace.Indexed)
|
||||
private IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable, bool isStencil)
|
||||
: base(ColorSpace.Indexed, isStencil)
|
||||
{
|
||||
BaseColorSpace = baseColorSpaceDetails ?? throw new ArgumentNullException(nameof(baseColorSpaceDetails));
|
||||
HiVal = hiVal;
|
||||
@ -367,56 +378,40 @@
|
||||
case ColorSpace.DeviceRGB:
|
||||
case ColorSpace.CalRGB:
|
||||
case ColorSpace.Lab:
|
||||
{
|
||||
Span<byte> result = new byte[input.Length * 3];
|
||||
var i = 0;
|
||||
foreach (var x in input)
|
||||
{
|
||||
for (var j = 0; j < 3; ++j)
|
||||
Span<byte> result = new byte[input.Length * 3];
|
||||
var i = 0;
|
||||
foreach (var x in input)
|
||||
{
|
||||
result[i++] = ColorTable[x * 3 + j];
|
||||
for (var j = 0; j < 3; ++j)
|
||||
{
|
||||
result[i++] = ColorTable[x * 3 + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
case ColorSpace.DeviceCMYK:
|
||||
{
|
||||
Span<byte> result = new byte[input.Length * 4];
|
||||
var i = 0;
|
||||
foreach (var x in input)
|
||||
{
|
||||
for (var j = 0; j < 4; ++j)
|
||||
Span<byte> result = new byte[input.Length * 4];
|
||||
var i = 0;
|
||||
foreach (var x in input)
|
||||
{
|
||||
result[i++] = ColorTable[x * 4 + j];
|
||||
for (var j = 0; j < 4; ++j)
|
||||
{
|
||||
result[i++] = ColorTable[x * 4 + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
case ColorSpace.DeviceGray:
|
||||
case ColorSpace.CalGray:
|
||||
case ColorSpace.Separation:
|
||||
{
|
||||
for (var i = 0; i < input.Length; ++i)
|
||||
{
|
||||
ref byte b = ref input[i];
|
||||
b = ColorTable[b];
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
case ColorSpace.DeviceN:
|
||||
case ColorSpace.ICCBased:
|
||||
{
|
||||
int i = 0;
|
||||
if (BaseColorSpace.NumberOfColorComponents == 1)
|
||||
{
|
||||
// In place
|
||||
for (i = 0; i < input.Length; ++i)
|
||||
for (var i = 0; i < input.Length; ++i)
|
||||
{
|
||||
ref byte b = ref input[i];
|
||||
b = ColorTable[b];
|
||||
@ -425,17 +420,33 @@
|
||||
return input;
|
||||
}
|
||||
|
||||
Span<byte> result = new byte[input.Length * BaseColorSpace.NumberOfColorComponents];
|
||||
foreach (var x in input)
|
||||
case ColorSpace.DeviceN:
|
||||
case ColorSpace.ICCBased:
|
||||
{
|
||||
for (var j = 0; j < BaseColorSpace.NumberOfColorComponents; ++j)
|
||||
int i = 0;
|
||||
if (BaseColorSpace.NumberOfColorComponents == 1)
|
||||
{
|
||||
result[i++] = ColorTable[x * BaseColorSpace.NumberOfColorComponents + j];
|
||||
}
|
||||
}
|
||||
// In place
|
||||
for (i = 0; i < input.Length; ++i)
|
||||
{
|
||||
ref byte b = ref input[i];
|
||||
b = ColorTable[b];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
Span<byte> result = new byte[input.Length * BaseColorSpace.NumberOfColorComponents];
|
||||
foreach (var x in input)
|
||||
{
|
||||
for (var j = 0; j < BaseColorSpace.NumberOfColorComponents; ++j)
|
||||
{
|
||||
result[i++] = ColorTable[x * BaseColorSpace.NumberOfColorComponents + j];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
|
@ -9,7 +9,20 @@
|
||||
{
|
||||
private static bool TryGenerateSoftMask(IPdfImage image, [NotNullWhen(true)] out ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
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))
|
||||
{
|
||||
@ -74,9 +87,24 @@
|
||||
var numberOfComponents = image.ColorSpaceDetails!.BaseNumberOfColorComponents;
|
||||
|
||||
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))
|
||||
{
|
||||
@ -98,7 +126,7 @@
|
||||
* B = 255 × (1-Y) × (1-K)
|
||||
*/
|
||||
|
||||
byte a = isSoftMask ? softMask[sm++] : byte.MaxValue;
|
||||
byte a = getAlphaChannel(sm++);
|
||||
double c = (bytesPure[i++] / 255d);
|
||||
double m = (bytesPure[i++] / 255d);
|
||||
double y = (bytesPure[i++] / 255d);
|
||||
@ -119,7 +147,7 @@
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -131,7 +159,7 @@
|
||||
{
|
||||
for (int row = 0; row < image.WidthInSamples; row++)
|
||||
{
|
||||
byte a = isSoftMask ? softMask[i] : byte.MaxValue;
|
||||
byte a = getAlphaChannel(i);
|
||||
byte pixel = bytesPure[i++];
|
||||
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.");
|
||||
}
|
||||
|
||||
var renderingIntent = xObject.DefaultRenderingIntent; // Ignored
|
||||
|
||||
XObjectContentRecord softMaskImageRecord = new XObjectContentRecord(XObjectType.Image, sMaskToken, TransformationMatrix.Identity,
|
||||
renderingIntent, DeviceGrayColorSpaceDetails.Instance);
|
||||
XObjectContentRecord softMaskImageRecord = new XObjectContentRecord(XObjectType.Image,
|
||||
sMaskToken,
|
||||
TransformationMatrix.Identity,
|
||||
xObject.DefaultRenderingIntent, // Ignored
|
||||
DeviceGrayColorSpaceDetails.Instance);
|
||||
|
||||
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);
|
||||
|
||||
@ -125,9 +143,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
var streamToken = new StreamToken(dictionary, xObject.Stream.Data);
|
||||
|
||||
var decodedBytes = supportsFilters ? new Lazy<ReadOnlyMemory<byte>>(() => streamToken.Decode(filterProvider, pdfScanner))
|
||||
var decodedBytes = supportsFilters ? new Lazy<ReadOnlyMemory<byte>>(() => xObject.Stream.Decode(filterProvider, pdfScanner))
|
||||
: null;
|
||||
|
||||
var decode = Array.Empty<double>();
|
||||
|
@ -65,7 +65,7 @@
|
||||
public ColorSpaceDetails? ColorSpaceDetails { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPdfImage? SoftMaskImage { get; }
|
||||
public IPdfImage? MaskImage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="XObjectImage"/>.
|
||||
@ -98,7 +98,7 @@
|
||||
RawMemory = rawMemory;
|
||||
ColorSpaceDetails = colorSpaceDetails;
|
||||
memoryFactory = bytes;
|
||||
SoftMaskImage = softMaskImage;
|
||||
MaskImage = softMaskImage;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
Loading…
Reference in New Issue
Block a user