Skip to content

Commit fd9632a

Browse files
committed
Improve masks and images (inc. JBIG) rendering
1 parent 0a3e7e5 commit fd9632a

File tree

3 files changed

+181
-77
lines changed

3 files changed

+181
-77
lines changed

UglyToad.PdfPig.Rendering.Skia/Helpers/SkiaImageExtensions.cs

Lines changed: 131 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ private static bool IsValidColorSpace(IPdfImage pdfImage)
3333
&& pdfImage.ColorSpaceDetails!.BaseType != ColorSpace.Pattern;
3434
}
3535

36-
3736
private static bool IsImageArrayCorrectlySized(IPdfImage pdfImage, ReadOnlySpan<byte> bytesPure)
3837
{
3938
var actualSize = bytesPure.Length;
@@ -52,6 +51,23 @@ private static bool IsImageArrayCorrectlySized(IPdfImage pdfImage, ReadOnlySpan<
5251
bytesPure[actualSize - 1] == ReadHelper.AsciiLineFeed);
5352
}
5453

54+
private static bool HasAlphaChannel(this IPdfImage pdfImage)
55+
{
56+
return pdfImage.MaskImage is not null || pdfImage.ImageDictionary.ContainsKey(NameToken.Mask);
57+
}
58+
59+
public static int GetRasterSize(this IPdfImage pdfImage)
60+
{
61+
int width = pdfImage.WidthInSamples;
62+
int height = pdfImage.HeightInSamples;
63+
64+
var numberOfComponents = pdfImage.ColorSpaceDetails!.BaseNumberOfColorComponents;
65+
bool isRgba = numberOfComponents > 1 || pdfImage.HasAlphaChannel();
66+
int bytesPerPixel = isRgba ? 4 : 1; // 3 (RGB) + 1 (alpha)
67+
68+
return height * width * bytesPerPixel;
69+
}
70+
5571
// https://stackoverflow.com/questions/50312937/skiasharp-tiff-support#50370515
5672
private static bool TryGenerate(this IPdfImage pdfImage, out SKImage skImage)
5773
{
@@ -65,7 +81,7 @@ private static bool TryGenerate(this IPdfImage pdfImage, out SKImage skImage)
6581
var bytesPure = imageMemory.Span;
6682
SKImage? mask = null;
6783
SKPixmap? sMaskPixmap = null;
68-
84+
6985
try
7086
{
7187
int width = pdfImage.WidthInSamples;
@@ -81,79 +97,71 @@ private static bool TryGenerate(this IPdfImage pdfImage, out SKImage skImage)
8197
return false;
8298
}
8399

84-
if (numberOfComponents == 1)
100+
bool isRgba = numberOfComponents > 1 || pdfImage.HasAlphaChannel();
101+
var colorSpace = isRgba ? SKColorType.Rgba8888 : SKColorType.Gray8;
102+
/*
103+
if (pdfImage.IsImageMask && colorSpace == SKColorType.Gray8)
85104
{
86-
if (pdfImage.MaskImage is not null)
87-
{
88-
// TODO
89-
}
90-
91-
return TryGetGray8Bitmap(width, height, bytesPure, out skImage);
105+
colorSpace = SKColorType.Alpha8;
92106
}
107+
*/
93108

94109
// We apparently need SKAlphaType.Unpremul to avoid artifacts with transparency.
95110
// For example, the logo's background in "Motor Insurance claim form.pdf" might
96111
// appear black instead of transparent at certain scales.
97112
// See https://groups.google.com/g/skia-discuss/c/sV6e3dpf4CE for related question
98-
var info = new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Unpremul);
99-
100-
// create the buffer that will hold the pixels
101-
const int bytesPerPixel = 4; // 3 (RGB) + 1 (alpha)
102-
103-
var length = (height * width * bytesPerPixel) + height;
113+
var info = new SKImageInfo(width, height, colorSpace, SKAlphaType.Unpremul);
104114

105-
var raster = new byte[length];
106-
107-
// get a pointer to the buffer, and give it to the skImage
108-
var ptr = GCHandle.Alloc(raster, GCHandleType.Pinned);
109-
using (SKPixmap pixmap = new SKPixmap(info, ptr.AddrOfPinnedObject(), info.RowBytes))
110-
{
111-
skImage = SKImage.FromPixels(pixmap, (addr, ctx) => ptr.Free());
112-
}
115+
int bytesPerPixel = isRgba ? 4 : 1; // 3 (RGB) + 1 (alpha)
113116

114117
Func<int, int, byte, byte, byte, byte> getAlphaChannel = (_, _, _, _, _) => byte.MaxValue;
115-
if (pdfImage.MaskImage?.TryGenerate(out mask) == true)
118+
if (pdfImage.MaskImage is not null)
116119
{
117-
if (!skImage.Info.Rect.Equals(mask!.Info.Rect))
120+
if (pdfImage.MaskImage.TryGenerate(out mask))
118121
{
119-
// Resize
120-
var maskInfo = new SKImageInfo(skImage.Info.Width, skImage.Info.Height, SKColorType.Gray8, SKAlphaType.Unpremul);
121-
var maskRaster = new byte[skImage.Info.Width * skImage.Info.Height];
122-
var ptrMask = GCHandle.Alloc(maskRaster, GCHandleType.Pinned);
123-
sMaskPixmap = new SKPixmap(maskInfo, ptrMask.AddrOfPinnedObject(), maskInfo.RowBytes);
124-
if (!mask.ScalePixels(sMaskPixmap, SKFilterQuality.High))
122+
if (!info.Rect.Equals(mask!.Info.Rect))
125123
{
126-
// TODO - Error
124+
// Resize
125+
var maskInfo = new SKImageInfo(info.Width, info.Height, mask!.Info.ColorType, mask!.Info.AlphaType);
126+
var maskRasterResize = new byte[info.Width * info.Height];
127+
var ptrMask = GCHandle.Alloc(maskRasterResize, GCHandleType.Pinned);
128+
sMaskPixmap = new SKPixmap(maskInfo, ptrMask.AddrOfPinnedObject(), maskInfo.RowBytes);
129+
if (!mask.ScalePixels(sMaskPixmap, SKFilterQuality.High))
130+
{
131+
// TODO - Error
132+
}
133+
134+
mask.Dispose();
135+
136+
mask = SKImage.FromPixels(sMaskPixmap, (addr, ctx) => ptrMask.Free());
137+
}
138+
else
139+
{
140+
sMaskPixmap = mask.PeekPixels();
127141
}
128142

129-
mask.Dispose();
130-
mask = SKImage.FromPixels(sMaskPixmap, (addr, ctx) => ptrMask.Free());
131-
}
132-
else
133-
{
134-
sMaskPixmap = mask.PeekPixels();
135-
}
136-
137-
if (!sMaskPixmap.GetPixelSpan().IsEmpty)
138-
{
139-
//getAlphaChannel = (row, col, _, _, _) => sMaskPixmap.GetPixelSpan()[(row * width) + col];
140-
141-
if (pdfImage.MaskImage.NeedsReverseDecode())
143+
if (!sMaskPixmap.GetPixelSpan().IsEmpty)
142144
{
145+
// TODO - It is very unclear why we need this logic of reversing or not here for IsImageMask
146+
if (pdfImage.MaskImage.IsImageMask)
147+
{
148+
// We reverse pixel color
149+
getAlphaChannel = (row, col, _, _, _) => (byte)~sMaskPixmap.GetPixelSpan()[(row * width) + col];
150+
}
151+
else
152+
{
153+
// This is a NameToken.Smask - we do not reverse pixel color
154+
getAlphaChannel = (row, col, _, _, _) => sMaskPixmap.GetPixelSpan()[(row * width) + col];
155+
}
156+
157+
// Examples of docs that are decode inverse
143158
// MOZILLA-LINK-3264-0.pdf
144159
// MOZILLA-LINK-4246-2.pdf
145160
// MOZILLA-LINK-4293-0.pdf
146161
// MOZILLA-LINK-4314-0.pdf
147162
// MOZILLA-LINK-3758-0.pdf
148163

149-
// Wrong: MOZILLA-LINK-4379-0.pd
150-
151-
getAlphaChannel = (row, col, _, _, _) =>
152-
Convert.ToByte(255 - sMaskPixmap.GetPixelSpan()[(row * width) + col]);
153-
}
154-
else
155-
{
156-
getAlphaChannel = (row, col, _, _, _) => sMaskPixmap.GetPixelSpan()[(row * width) + col];
164+
// Wrong: MOZILLA-LINK-4379-0.pdf
157165
}
158166
}
159167
}
@@ -200,6 +208,10 @@ private static bool TryGenerate(this IPdfImage pdfImage, out SKImage skImage)
200208
gMax = range[4];
201209
bMax = range[5];
202210
}
211+
else if (numberOfComponents == 1)
212+
{
213+
throw new NotImplementedException("Mask with numberOfComponents == 1.");
214+
}
203215

204216
getAlphaChannel = (_, _, r, g, b) =>
205217
{
@@ -214,7 +226,23 @@ private static bool TryGenerate(this IPdfImage pdfImage, out SKImage skImage)
214226
};
215227
}
216228

217-
if (pdfImage.ColorSpaceDetails.BaseType == ColorSpace.DeviceCMYK || numberOfComponents == 4)
229+
// create the buffer that will hold the pixels
230+
byte[] raster = new byte[height * width * bytesPerPixel];
231+
Span<byte> rasterSpan = raster;
232+
233+
// get a pointer to the buffer, and give it to the skImage
234+
var ptr = GCHandle.Alloc(raster, GCHandleType.Pinned);
235+
using (SKPixmap pixmap = new SKPixmap(info, ptr.AddrOfPinnedObject(), info.RowBytes))
236+
{
237+
skImage = SKImage.FromPixels(pixmap, (addr, ctx) =>
238+
{
239+
ptr.Free();
240+
raster = null;
241+
System.Diagnostics.Debug.WriteLine("ptr.Free()");
242+
});
243+
}
244+
245+
if (numberOfComponents == 4)
218246
{
219247
int i = 0;
220248
for (int row = 0; row < height; ++row)
@@ -225,10 +253,10 @@ private static bool TryGenerate(this IPdfImage pdfImage, out SKImage skImage)
225253
out byte r, out byte g, out byte b);
226254

227255
var start = (row * (width * bytesPerPixel)) + (col * bytesPerPixel);
228-
raster[start++] = r;
229-
raster[start++] = g;
230-
raster[start++] = b;
231-
raster[start] = getAlphaChannel(row, col, r, g, b);
256+
rasterSpan[start] = r;
257+
rasterSpan[start + 1] = g;
258+
rasterSpan[start + 2] = b;
259+
rasterSpan[start + 3] = getAlphaChannel(row, col, r, g, b);
232260
}
233261
}
234262

@@ -247,11 +275,52 @@ private static bool TryGenerate(this IPdfImage pdfImage, out SKImage skImage)
247275
byte b = bytesPure[i++];
248276

249277
var start = (row * (width * bytesPerPixel)) + (col * bytesPerPixel);
250-
raster[start++] = r;
251-
raster[start++] = g;
252-
raster[start++] = b;
253-
raster[start] = getAlphaChannel(row, col, r, g, b);
278+
rasterSpan[start] = r;
279+
rasterSpan[start + 1] = g;
280+
rasterSpan[start + 2] = b;
281+
rasterSpan[start + 3] = getAlphaChannel(row, col, r, g, b);
282+
}
283+
}
284+
285+
return true;
286+
}
287+
288+
if (numberOfComponents == 1)
289+
{
290+
if (isRgba)
291+
{
292+
// Handle gray scale image as RGBA because we have an alpha channel
293+
int i = 0;
294+
for (int row = 0; row < height; ++row)
295+
{
296+
for (int col = 0; col < width; ++col)
297+
{
298+
byte g = bytesPure[i++];
299+
300+
var start = (row * (width * bytesPerPixel)) + (col * bytesPerPixel);
301+
rasterSpan[start] = g;
302+
rasterSpan[start + 1] = g;
303+
rasterSpan[start + 2] = g;
304+
rasterSpan[start + 3] = getAlphaChannel(row, col, g, g, g);
305+
}
254306
}
307+
308+
return true;
309+
}
310+
311+
if (pdfImage.NeedsReverseDecode())
312+
{
313+
for (int i = 0; i < bytesPure.Length; ++i)
314+
{
315+
rasterSpan[i] = (byte)~bytesPure[i];
316+
}
317+
318+
return true;
319+
}
320+
321+
for (int i = 0; i < bytesPure.Length; ++i)
322+
{
323+
rasterSpan[i] = bytesPure[i];
255324
}
256325

257326
return true;
@@ -312,7 +381,6 @@ private static bool TryGetGray8Bitmap(int width, int height, ReadOnlySpan<byte>
312381

313382
public static SKImage GetSKImage(this IPdfImage pdfImage)
314383
{
315-
// Try get png bytes
316384
if (pdfImage.TryGenerate(out var bitmap))
317385
{
318386
return bitmap;

UglyToad.PdfPig.Rendering.Skia/SkiaStreamProcessor.Image.cs

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Runtime.InteropServices;
17+
using System.Threading.Tasks;
1618
using SkiaSharp;
1719
using UglyToad.PdfPig.Content;
1820
using UglyToad.PdfPig.Graphics;
@@ -71,28 +73,62 @@ private void RenderImage(IPdfImage image)
7173
else
7274
{
7375
// Draw image mask
74-
var colour = GetCurrentState().CurrentNonStrokingColor.ToSKColor(1);
75-
76-
byte refByte = image.NeedsReverseDecode() ? byte.MaxValue : byte.MinValue;
76+
SKColor colour = GetCurrentState().CurrentNonStrokingColor.ToSKColor(GetCurrentState().AlphaConstantNonStroking);
77+
78+
/*
79+
var maskShader = SKShader.CreateImage(skImage, SKShaderTileMode.Clamp, SKShaderTileMode.Clamp, SKMatrix.CreateScale(destRect.Width / skImage.Width, destRect.Height / skImage.Height));
7780
81+
using var paint = new SKPaint
82+
{
83+
Color = colour,
84+
Shader = maskShader
85+
};
86+
_canvas.DrawRect(destRect, paint);
87+
*/
88+
89+
byte r = colour.Red;
90+
byte g = colour.Green;
91+
byte b = colour.Blue;
92+
7893
using (var skImagePixels = skImage.PeekPixels())
79-
using (var alphaMask = new SKBitmap(skImage.Width, skImage.Height, SKColorType.Bgra8888, SKAlphaType.Premul))
8094
{
81-
var span = skImagePixels.GetPixelSpan();
95+
var raster = new byte[skImage.Width * skImage.Height * 4]; // RGBA
8296

83-
for (int y = 0; y < skImage.Height; y++)
97+
Span<byte> span = skImagePixels.GetPixelSpan<byte>();
98+
Span<byte> rasterSpan = raster;
99+
100+
int i = 0;
101+
for (int row = 0; row < skImage.Height; ++row)
84102
{
85-
for (int x = 0; x < skImage.Width; x++)
103+
for (int col = 0; col < skImage.Width; ++col)
86104
{
87-
byte pixel = span[(y * skImage.Width) + x];
88-
if (pixel == refByte)
105+
byte pixel = span[(row * skImage.Width) + col];
106+
if (pixel == byte.MinValue)
89107
{
90-
alphaMask.SetPixel(x, y, colour);
108+
var start = (row * (skImage.Width * 4)) + (col * 4);
109+
rasterSpan[start] = r;
110+
rasterSpan[start + 1] = g;
111+
rasterSpan[start + 2] = b;
112+
rasterSpan[start + 3] = byte.MaxValue;
91113
}
92114
}
93115
}
94116

95-
_canvas.DrawBitmap(alphaMask, destRect, _paintCache.GetAntialiasing());
117+
var info = new SKImageInfo(skImage.Width, skImage.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
118+
119+
// get a pointer to the buffer, and give it to the skImage
120+
var ptr = GCHandle.Alloc(raster, GCHandleType.Pinned);
121+
122+
using (SKPixmap pixmap = new SKPixmap(info, ptr.AddrOfPinnedObject(), info.RowBytes))
123+
using (SKImage skImage2 = SKImage.FromPixels(pixmap, (addr, ctx) =>
124+
{
125+
ptr.Free();
126+
raster = null;
127+
System.Diagnostics.Debug.WriteLine("ptr.Free()");
128+
}))
129+
{
130+
_canvas.DrawImage(skImage2, destRect, _paintCache.GetAntialiasing());
131+
}
96132
}
97133
}
98134
}

UglyToad.PdfPig.Rendering.Skia/UglyToad.PdfPig.Rendering.Skia.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
55
<LangVersion>12</LangVersion>
6-
<Version>0.1.11.1-alpha012</Version>
6+
<Version>0.1.11.1-alpha013</Version>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
</PropertyGroup>
99

@@ -41,9 +41,9 @@
4141
</ItemGroup>
4242

4343
<ItemGroup>
44-
<PackageReference Include="PdfPig" Version="0.1.11-alpha-20250602-89abf" />
44+
<PackageReference Include="PdfPig" Version="0.1.11-alpha-20250719-6a064" />
4545
<PackageReference Include="PdfPig.Filters.Dct.JpegLibrary" Version="0.1.11-alpha001" />
46-
<PackageReference Include="PdfPig.Filters.Jbig2.PdfboxJbig2" Version="0.1.11-alpha001" />
46+
<PackageReference Include="PdfPig.Filters.Jbig2.PdfboxJbig2" Version="0.1.11-alpha003" />
4747
<PackageReference Include="PdfPig.Filters.Jpx.OpenJpeg" Version="0.1.11-alpha001" />
4848
<PackageReference Include="SkiaSharp" Version="2.88.9" />
4949
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.9" />

0 commit comments

Comments
 (0)