Skip to content

Commit 5af64b2

Browse files
committed
Do not convert PdfPig images to Png and use SKImage instead of SKBitmap
1 parent d30e5a8 commit 5af64b2

File tree

3 files changed

+265
-23
lines changed

3 files changed

+265
-23
lines changed

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

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
using System;
1616
using SkiaSharp;
17-
using UglyToad.PdfPig.Content;
1817
using UglyToad.PdfPig.Core;
1918
using UglyToad.PdfPig.Graphics.Colors;
2019
using UglyToad.PdfPig.Graphics.Core;
@@ -338,23 +337,5 @@ public static void ApplySMask(this SKBitmap image, SKBitmap smask)
338337
scaled.Dispose();
339338
}
340339
*/
341-
342-
public static ReadOnlySpan<byte> GetImageBytes(this IPdfImage pdfImage)
343-
{
344-
// Try get png bytes
345-
if (pdfImage.TryGetPng(out byte[]? bytes) && bytes?.Length > 0)
346-
{
347-
return bytes;
348-
}
349-
350-
// Fallback to bytes
351-
if (pdfImage.TryGetBytesAsMemory(out var bytesL) && bytesL.Length > 0)
352-
{
353-
return bytesL.Span;
354-
}
355-
356-
// Fallback to raw bytes
357-
return pdfImage.RawBytes;
358-
}
359340
}
360341
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Copyright 2024 BobLd
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License").
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Runtime.InteropServices;
17+
using SkiaSharp;
18+
using UglyToad.PdfPig.Content;
19+
using UglyToad.PdfPig.Core;
20+
using UglyToad.PdfPig.Graphics.Colors;
21+
using UglyToad.PdfPig.Images;
22+
23+
namespace UglyToad.PdfPig.Rendering.Skia.Helpers
24+
{
25+
internal static class SkiaImageExtensions
26+
{
27+
// https://stackoverflow.com/questions/50312937/skiasharp-tiff-support#50370515
28+
private static bool TryGenerate(this IPdfImage image, out SKImage bitmap)
29+
{
30+
bitmap = null;
31+
32+
var hasValidDetails = image.ColorSpaceDetails != null && !(image.ColorSpaceDetails is UnsupportedColorSpaceDetails);
33+
34+
var isColorSpaceSupported = hasValidDetails && image.ColorSpaceDetails!.BaseType != ColorSpace.Pattern;
35+
36+
if (!isColorSpaceSupported || !image.TryGetBytesAsMemory(out var imageMemory))
37+
{
38+
return false;
39+
}
40+
41+
var bytesPure = imageMemory.Span;
42+
43+
try
44+
{
45+
bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails!, bytesPure,
46+
image.BitsPerComponent, image.WidthInSamples, image.HeightInSamples);
47+
48+
var numberOfComponents = image.ColorSpaceDetails!.BaseNumberOfColorComponents;
49+
50+
var is3Byte = numberOfComponents == 3;
51+
52+
var requiredSize = (image.WidthInSamples * image.HeightInSamples * numberOfComponents);
53+
54+
var actualSize = bytesPure.Length;
55+
var isCorrectlySized = bytesPure.Length == requiredSize ||
56+
// Spec, p. 37: "...error if the stream contains too much data, with the exception that
57+
// there may be an extra end-of-line marker..."
58+
(actualSize == requiredSize + 1 && bytesPure[actualSize - 1] == ReadHelper.AsciiLineFeed) ||
59+
(actualSize == requiredSize + 1 && bytesPure[actualSize - 1] == ReadHelper.AsciiCarriageReturn) ||
60+
// The combination of a CARRIAGE RETURN followed immediately by a LINE FEED is treated as one EOL marker.
61+
(actualSize == requiredSize + 2 &&
62+
bytesPure[actualSize - 2] == ReadHelper.AsciiCarriageReturn &&
63+
bytesPure[actualSize - 1] == ReadHelper.AsciiLineFeed);
64+
65+
if (!isCorrectlySized)
66+
{
67+
return false;
68+
}
69+
70+
if (numberOfComponents == 1)
71+
{
72+
return TryGetGray8Bitmap(image.WidthInSamples, image.HeightInSamples, bytesPure, out bitmap);
73+
}
74+
75+
var info = new SKImageInfo(image.WidthInSamples, image.HeightInSamples, SKColorType.Rgba8888);
76+
77+
// create the buffer that will hold the pixels
78+
bool hasAlphaChannel = true;
79+
var bpp = hasAlphaChannel ? 4 : 3;
80+
81+
var length = (image.HeightInSamples * image.WidthInSamples * bpp) + image.HeightInSamples;
82+
83+
var raster = new byte[length];
84+
85+
var builder = ImageBuilder.Create(raster, image.WidthInSamples, image.HeightInSamples, hasAlphaChannel);
86+
87+
// get a pointer to the buffer, and give it to the bitmap
88+
var ptr = GCHandle.Alloc(raster, GCHandleType.Pinned);
89+
90+
using (SKPixmap pixmap = new SKPixmap(info, ptr.AddrOfPinnedObject(), info.RowBytes))
91+
{
92+
bitmap = SKImage.FromPixels(pixmap, (addr, ctx) => ptr.Free());
93+
}
94+
95+
byte alpha = byte.MaxValue;
96+
if (image.ColorSpaceDetails.BaseType == ColorSpace.DeviceCMYK || numberOfComponents == 4)
97+
{
98+
int i = 0;
99+
for (int col = 0; col < image.HeightInSamples; col++)
100+
{
101+
for (int row = 0; row < image.WidthInSamples; row++)
102+
{
103+
/*
104+
* Where CMYK in 0..1
105+
* R = 255 × (1-C) × (1-K)
106+
* G = 255 × (1-M) × (1-K)
107+
* B = 255 × (1-Y) × (1-K)
108+
*/
109+
110+
double c = (bytesPure[i++] / 255d);
111+
double m = (bytesPure[i++] / 255d);
112+
double y = (bytesPure[i++] / 255d);
113+
double k = (bytesPure[i++] / 255d);
114+
var r = (byte)(255 * (1 - c) * (1 - k));
115+
var g = (byte)(255 * (1 - m) * (1 - k));
116+
var b = (byte)(255 * (1 - y) * (1 - k));
117+
118+
builder.SetPixel(r, g, b, alpha, row, col);
119+
}
120+
}
121+
return true;
122+
}
123+
124+
if (is3Byte)
125+
{
126+
int i = 0;
127+
for (int col = 0; col < image.HeightInSamples; col++)
128+
{
129+
for (int row = 0; row < image.WidthInSamples; row++)
130+
{
131+
builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], alpha, row, col);
132+
}
133+
}
134+
return true;
135+
}
136+
137+
throw new Exception($"Could not process image with ColorSpace={image.ColorSpaceDetails.BaseType}, numberOfComponents={numberOfComponents}.");
138+
}
139+
catch
140+
{
141+
// ignored.
142+
}
143+
144+
bitmap?.Dispose();
145+
return false;
146+
}
147+
148+
private static bool TryGetGray8Bitmap(int width, int height, ReadOnlySpan<byte> bytesPure, out SKImage? bitmap)
149+
{
150+
bitmap = null;
151+
152+
try
153+
{
154+
bitmap = SKImage.FromPixelCopy(new SKImageInfo(width, height, SKColorType.Gray8), bytesPure);
155+
return true;
156+
}
157+
catch (Exception)
158+
{
159+
// ignored.
160+
}
161+
162+
bitmap?.Dispose();
163+
return false;
164+
}
165+
166+
private sealed class ImageBuilder
167+
{
168+
private readonly byte[] rawData;
169+
private readonly bool hasAlphaChannel;
170+
private readonly int width;
171+
private readonly int height;
172+
private readonly int bytesPerPixel;
173+
174+
/// <summary>
175+
/// Create a builder for a PNG with the given width and size.
176+
/// </summary>
177+
public static ImageBuilder Create(byte[] rawData, int width, int height, bool hasAlphaChannel)
178+
{
179+
var bpp = hasAlphaChannel ? 4 : 3;
180+
181+
var length = (height * width * bpp) + height;
182+
183+
if (rawData.Length != length)
184+
{
185+
throw new ArgumentOutOfRangeException(nameof(rawData.Length), "TestBuilder.Create");
186+
}
187+
188+
return new ImageBuilder(rawData, hasAlphaChannel, width, height, bpp);
189+
}
190+
191+
private ImageBuilder(byte[] rawData, bool hasAlphaChannel, int width, int height, int bytesPerPixel)
192+
{
193+
this.rawData = rawData;
194+
this.hasAlphaChannel = hasAlphaChannel;
195+
this.width = width;
196+
this.height = height;
197+
this.bytesPerPixel = bytesPerPixel;
198+
}
199+
200+
/// <summary>
201+
/// Set the pixel value for the given column (x) and row (y).
202+
/// </summary>
203+
public void SetPixel(byte r, byte g, byte b, byte a, int x, int y)
204+
{
205+
var start = (y * (width * bytesPerPixel)) + (x * bytesPerPixel);
206+
207+
rawData[start++] = r;
208+
rawData[start++] = g;
209+
rawData[start++] = b;
210+
211+
if (hasAlphaChannel)
212+
{
213+
rawData[start] = a;
214+
}
215+
}
216+
}
217+
218+
public static SKImage GetSKImage(this IPdfImage pdfImage)
219+
{
220+
// Try get png bytes
221+
if (pdfImage.TryGenerate(out var bitmap))
222+
{
223+
return bitmap;
224+
}
225+
226+
// Fallback to bytes
227+
if (pdfImage.TryGetBytesAsMemory(out var bytesL) && bytesL.Length > 0)
228+
{
229+
try
230+
{
231+
return SKImage.FromEncodedData(bytesL.Span);
232+
}
233+
catch (Exception)
234+
{
235+
// ignore
236+
}
237+
}
238+
239+
// Fallback to raw bytes
240+
return SKImage.FromEncodedData(pdfImage.RawBytes);
241+
}
242+
243+
public static ReadOnlySpan<byte> GetImageBytes(this IPdfImage pdfImage)
244+
{
245+
// Try get png bytes
246+
if (pdfImage.TryGetPng(out byte[]? bytes) && bytes?.Length > 0)
247+
{
248+
return bytes;
249+
}
250+
251+
// Fallback to bytes
252+
if (pdfImage.TryGetBytesAsMemory(out var bytesL) && bytesL.Length > 0)
253+
{
254+
return bytesL.Span;
255+
}
256+
257+
// Fallback to raw bytes
258+
return pdfImage.RawBytes;
259+
}
260+
}
261+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ private void RenderImage(IPdfImage image)
5252
if (CurrentTransformationMatrix.A > 0 && CurrentTransformationMatrix.D > 0)
5353
{
5454
// No transformation to do
55-
using (var bitmap = SKBitmap.Decode(image.GetImageBytes()))
55+
using (var bitmap = image.GetSKImage())
5656
{
57-
_canvas.DrawBitmap(bitmap, destRect, _paintCache.GetAntialiasing());
57+
_canvas.DrawImage(bitmap, destRect, _paintCache.GetAntialiasing());
5858
}
5959
}
6060
else
@@ -63,11 +63,11 @@ private void RenderImage(IPdfImage image)
6363
Math.Sign(CurrentTransformationMatrix.A),
6464
Math.Sign(CurrentTransformationMatrix.D));
6565

66-
using (var bitmap = SKBitmap.Decode(image.GetImageBytes()))
66+
using (var bitmap = image.GetSKImage())
6767
using (new SKAutoCanvasRestore(_canvas, true))
6868
{
6969
_canvas.SetMatrix(matrix);
70-
_canvas.DrawBitmap(bitmap, matrix.MapRect(destRect), _paintCache.GetAntialiasing());
70+
_canvas.DrawImage(bitmap, matrix.MapRect(destRect), _paintCache.GetAntialiasing());
7171
}
7272
}
7373

0 commit comments

Comments
 (0)