Skip to content

Commit 721e4e3

Browse files
Feature/availability loader and lazy page sizes (#185)
* Fix opening a document when a single page cannot be resolved PdfDocument measured every page in its constructor, so one page pdfium could not resolve failed the whole document, even for callers that only wanted the page count or a different page. PageSizes is now measured per page on first read and the count comes from FPDF_GetPageCount, which resolves no pages. GetPageSizes still measures every page and still reports an unreadable one. * Reach pages of linearized documents through FPDFAvail Where walking /Pages dead-ends, pages present in the file report as missing. FPDFAvail_* fills page_list_ from the linearization hint tables instead, so GetPageDictionary resolves them without traversing: on a 3467 page document the walk reaches 46 pages and FPDFAvail all of them. PdfFile reopens that way once per document, only after a page has failed and only when FPDFAvail_IsLinearized does not say no, so documents that work today keep their path and a failed reopen leaves the usual page error. Also stops PdfException.CreateException(SUCCESS) being dereferenced with "!" when pdfium refuses a page without recording an error. * Use the availability API for all documents; add unit tests for Stream related issues * API cleanup and restricted support of >4 GiB PDF files * Add tests for the 4 GiB PDF file size limit --------- Co-authored-by: David Sungaila <david.sungaila@arcor.de>
1 parent 0d693e9 commit 721e4e3

19 files changed

Lines changed: 1215 additions & 145 deletions

src/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<PackageVersion Include="Microsoft.Maui.Controls" Version="10.0.90" />
2020
<PackageVersion Include="Microsoft.Maui.Controls.Compatibility" Version="10.0.90" />
2121
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
22+
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.298" />
2223
<PackageVersion Include="MSTest.TestAdapter" Version="4.3.2" />
2324
<PackageVersion Include="MSTest.TestFramework" Version="4.3.2" />
2425
<PackageVersion Include="PatrickJahr.Blazor.FileHandling" Version="1.0.0" />

src/PDFtoImage/Conversion.Stream.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Runtime.CompilerServices;
99
using System.Threading;
10+
using System.Threading.Tasks;
1011

1112
namespace PDFtoImage
1213
{
@@ -91,10 +92,10 @@ public static IEnumerable<SKBitmap> ToImages(Stream pdfStream, IEnumerable<int>
9192

9293
var pageCount = pdfDocument.PageSizes.Count;
9394

94-
if (validatedPages.Any(p => p >= pageCount))
95+
if (validatedPages.Any(p => p < 0 || p >= pageCount))
9596
throw new ArgumentOutOfRangeException(nameof(pages), $"The page numbers must be between 0 and {pageCount - 1}. The PDF has {pageCount} pages in total.");
9697

97-
foreach (var bitmap in ToImagesImpl(pdfStream, leaveOpen, password, options, validatedPages))
98+
foreach (var bitmap in ToImagesImpl(pdfDocument, options, validatedPages))
9899
{
99100
yield return bitmap;
100101
}
@@ -259,7 +260,7 @@ public static IEnumerable<SKBitmap> ToImages(Stream pdfStream, Range pages, bool
259260

260261
var pageNumbers = Enumerable.Range(offset, length);
261262

262-
foreach (var bitmap in ToImagesImpl(pdfStream, leaveOpen, password, options, pageNumbers))
263+
foreach (var bitmap in ToImagesImpl(pdfDocument, options, pageNumbers))
263264
{
264265
yield return bitmap;
265266
}
@@ -282,7 +283,7 @@ public static async IAsyncEnumerable<SKBitmap> ToImagesAsync(Stream pdfStream, R
282283
throw new ArgumentNullException(nameof(pdfStream));
283284

284285
// Stream -> Internals.PdfDocument
285-
using var pdfDocument = PdfDocument.Load(pdfStream, password, !leaveOpen);
286+
using var pdfDocument = await Task.Run(() => PdfDocument.Load(pdfStream, password, !leaveOpen), cancellationToken);
286287

287288
var pageCount = pdfDocument.PageSizes.Count;
288289
var (offset, length) = pages.GetOffsetAndLength(pageCount);
@@ -292,7 +293,7 @@ public static async IAsyncEnumerable<SKBitmap> ToImagesAsync(Stream pdfStream, R
292293

293294
var pageNumbers = Enumerable.Range(offset, length);
294295

295-
await foreach (var bitmap in ToImagesImplAsync(pdfStream, leaveOpen, password, options, pageNumbers, cancellationToken))
296+
await foreach (var bitmap in ToImagesImplAsync(pdfDocument, options, pageNumbers, cancellationToken))
296297
{
297298
yield return bitmap;
298299
}
@@ -319,14 +320,14 @@ public static async IAsyncEnumerable<SKBitmap> ToImagesAsync(Stream pdfStream, I
319320
var validatedPages = pages.ToArray();
320321

321322
// Stream -> Internals.PdfDocument
322-
using var pdfDocument = PdfDocument.Load(pdfStream, password, !leaveOpen);
323+
using var pdfDocument = await Task.Run(() => PdfDocument.Load(pdfStream, password, !leaveOpen), cancellationToken);
323324

324325
var pageCount = pdfDocument.PageSizes.Count;
325326

326-
if (validatedPages.Any(p => p >= pageCount))
327+
if (validatedPages.Any(p => p < 0 || p >= pageCount))
327328
throw new ArgumentOutOfRangeException(nameof(pages), $"The page numbers must be between 0 and {pageCount - 1}. The PDF has {pageCount} pages in total.");
328329

329-
await foreach (var bitmap in ToImagesImplAsync(pdfStream, leaveOpen, password, options, validatedPages, cancellationToken))
330+
await foreach (var bitmap in ToImagesImplAsync(pdfDocument, options, validatedPages, cancellationToken))
330331
{
331332
yield return bitmap;
332333
}

src/PDFtoImage/Conversion.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,20 @@ internal static async IAsyncEnumerable<SKBitmap> ToImagesImplAsync(Stream pdfStr
6262
if (pdfStream == null)
6363
throw new ArgumentNullException(nameof(pdfStream));
6464

65-
if (options == default)
66-
options = new();
67-
6865
// Stream -> Internals.PdfDocument
6966
using var pdfDocument = await Task.Run(() => PdfDocument.Load(pdfStream, password, !leaveOpen), cancellationToken);
7067

68+
await foreach (var bitmap in ToImagesImplAsync(pdfDocument, options, pages, cancellationToken))
69+
{
70+
yield return bitmap;
71+
}
72+
}
73+
74+
internal static async IAsyncEnumerable<SKBitmap> ToImagesImplAsync(PdfDocument pdfDocument, RenderOptions options, IEnumerable<int>? pages, [EnumeratorCancellation] CancellationToken cancellationToken = default)
75+
{
76+
if (options == default)
77+
options = new();
78+
7179
pages ??= Enumerable.Range(0, pdfDocument.PageSizes.Count);
7280

7381
foreach (var page in pages)

src/PDFtoImage/Internals/NativeMethods.DllImport.cs

Lines changed: 86 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,31 @@ namespace PDFtoImage.Internals
1010
{
1111
internal static partial class NativeMethods
1212
{
13-
public static void SetFormFieldHighlightColor(IntPtr hHandle, int fieldType, uint color)
13+
public static bool Bitmap_FillRect(IntPtr bitmapHandle, int left, int top, int width, int height, uint color)
1414
{
1515
lock (LockString)
1616
{
17-
Imports.FPDF_SetFormFieldHighlightColor(hHandle, fieldType, color);
17+
return Imports.FPDFBitmap_FillRect(bitmapHandle, left, top, width, height, color) != 0;
1818
}
1919
}
2020

21-
public static bool Bitmap_FillRect(IntPtr bitmapHandle, int left, int top, int width, int height, uint color)
21+
public static bool GetPageSizeByIndex(IntPtr document, int page_index, out double width, out double height)
2222
{
2323
lock (LockString)
2424
{
25-
return Imports.FPDFBitmap_FillRect(bitmapHandle, left, top, width, height, color) != 0;
25+
return Imports.FPDF_GetPageSizeByIndex(document, page_index, out width, out height) != 0;
2626
}
2727
}
2828

29-
public static bool GetPageSizeByIndex(IntPtr document, int page_index, out double width, out double height)
29+
public static FPDF_ERR GetLastError()
3030
{
3131
lock (LockString)
3232
{
33-
return Imports.FPDF_GetPageSizeByIndex(document, page_index, out width, out height) != 0;
33+
return (FPDF_ERR)Imports.FPDF_GetLastError();
3434
}
3535
}
3636

37-
/// <summary>
38-
/// Opens a document using a .NET Stream. Allows opening huge
39-
/// PDFs without loading them into memory first.
40-
/// </summary>
41-
/// <param name="input">The input Stream. Don't dispose prior to closing the pdf.</param>
42-
/// <param name="password">Password, if the PDF is protected. Can be null.</param>
43-
/// <param name="id">Retrieves an IntPtr to the COM object for the Stream. The caller must release this with Marshal.Release prior to Disposing the Stream.</param>
44-
/// <returns>An IntPtr to the FPDF_DOCUMENT object.</returns>
45-
public unsafe static IntPtr LoadCustomDocument(Stream input, string? password, int id)
37+
private unsafe static IntPtr CreateAvailFileAccessState(Stream input, int id)
4638
{
4739
#if BROWSER
4840
delegate* unmanaged[Cdecl]<IntPtr, uint, IntPtr, uint, int> getBlock = &FPDF_GetBlock;
@@ -52,38 +44,71 @@ public unsafe static IntPtr LoadCustomDocument(Stream input, string? password, i
5244
var access = new FPDF_FILEACCESS((uint)input.Length, getBlock, (IntPtr)id);
5345
#endif
5446

55-
var size = Marshal.SizeOf<FPDF_FILEACCESS>();
56-
var ptr = Marshal.AllocHGlobal(size);
57-
Marshal.StructureToPtr(access, ptr, false);
58-
59-
byte[]? passwordBytes = password != null
60-
? Encoding.UTF8.GetBytes(password + '\0')
61-
: null;
47+
var fileAccessState = Marshal.AllocHGlobal(Marshal.SizeOf<FPDF_FILEACCESS>());
6248

6349
try
6450
{
65-
fixed (byte* passwordPointer = passwordBytes)
66-
{
67-
lock (LockString)
68-
{
69-
return Imports.FPDF_LoadCustomDocument(ptr, (IntPtr)passwordPointer);
70-
}
71-
}
51+
Marshal.StructureToPtr(access, fileAccessState, false);
52+
return fileAccessState;
7253
}
73-
finally
54+
catch
7455
{
75-
Marshal.FreeHGlobal(ptr);
56+
Marshal.FreeHGlobal(fileAccessState);
57+
throw;
7658
}
7759
}
7860

79-
public static FPDF_ERR GetLastError()
61+
private unsafe static IntPtr Avail_GetDocumentCore(IntPtr avail, string? password)
8062
{
81-
lock (LockString)
63+
byte[]? passwordBytes = password != null
64+
? Encoding.UTF8.GetBytes(password + '\0')
65+
: null;
66+
67+
fixed (byte* passwordPointer = passwordBytes)
8268
{
83-
return (FPDF_ERR)Imports.FPDF_GetLastError();
69+
return Imports.FPDFAvail_GetDocument(avail, (IntPtr)passwordPointer);
8470
}
8571
}
8672

73+
private unsafe static IntPtr GetIsDataAvailCallbackPointer()
74+
{
75+
#if BROWSER
76+
delegate* unmanaged[Cdecl]<IntPtr, UIntPtr, UIntPtr, int> callback = &FX_IsDataAvail;
77+
return (IntPtr)callback;
78+
#else
79+
return Marshal.GetFunctionPointerForDelegate(_isDataAvailDelegate);
80+
#endif
81+
}
82+
83+
private unsafe static IntPtr GetAddSegmentCallbackPointer()
84+
{
85+
#if BROWSER
86+
delegate* unmanaged[Cdecl]<IntPtr, UIntPtr, UIntPtr, void> callback = &FX_AddSegment;
87+
return (IntPtr)callback;
88+
#else
89+
return Marshal.GetFunctionPointerForDelegate(_addSegmentDelegate);
90+
#endif
91+
}
92+
93+
// PDFtoImage gives PDFium a complete seekable stream, matching pdfium_test's local-file
94+
// availability provider: all requested byte ranges are reported as present and download
95+
// hints are intentionally ignored.
96+
#if BROWSER
97+
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
98+
#else
99+
// needed for Unity IL2CPP compilation
100+
[AOT.MonoPInvokeCallback(typeof(FX_IsDataAvailDelegate))]
101+
#endif
102+
private static int FX_IsDataAvail(IntPtr param, UIntPtr offset, UIntPtr size) => 1;
103+
104+
#if BROWSER
105+
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
106+
#else
107+
// needed for Unity IL2CPP compilation
108+
[AOT.MonoPInvokeCallback(typeof(FX_AddSegmentDelegate))]
109+
#endif
110+
private static void FX_AddSegment(IntPtr param, UIntPtr offset, UIntPtr size) { }
111+
87112
#if BROWSER
88113
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
89114
#else
@@ -168,12 +193,6 @@ private static partial class Imports
168193
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
169194
public static extern int FPDF_GetPageCount(IntPtr document);
170195

171-
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
172-
public static extern void FPDF_SetFormFieldHighlightColor(IntPtr hHandle, int fieldType, uint color);
173-
174-
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
175-
public static extern void FPDF_SetFormFieldHighlightAlpha(IntPtr hHandle, byte alpha);
176-
177196
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
178197
public static extern IntPtr FPDF_LoadPage(IntPtr document, int page_index);
179198

@@ -216,9 +235,24 @@ private static partial class Imports
216235
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
217236
public static extern void FPDF_RemoveFormFieldHighlight(IntPtr form);
218237

238+
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
239+
public static extern IntPtr FPDFAvail_Create(IntPtr file_avail, IntPtr file);
240+
241+
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
242+
public static extern void FPDFAvail_Destroy(IntPtr avail);
243+
244+
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
245+
public static extern int FPDFAvail_IsDocAvail(IntPtr avail, IntPtr hints);
246+
247+
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
248+
public static extern int FPDFAvail_IsPageAvail(IntPtr avail, int page_index, IntPtr hints);
249+
250+
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
251+
public static extern int FPDFAvail_IsFormAvail(IntPtr avail, IntPtr hints);
252+
219253
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
220254
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA2101")]
221-
public static extern IntPtr FPDF_LoadCustomDocument(IntPtr access, IntPtr password);
255+
public static extern IntPtr FPDFAvail_GetDocument(IntPtr avail, IntPtr password);
222256

223257
[DllImport("pdfium", CallingConvention = CallingConvention.Cdecl)]
224258
public static extern IntPtr FPDFDOC_InitFormFillEnvironment(IntPtr document, IntPtr formInfo);
@@ -232,8 +266,18 @@ private static partial class Imports
232266
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
233267
private delegate int FPDF_GetBlockDelegate(IntPtr param, uint position, IntPtr buffer, uint size);
234268

269+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
270+
private delegate int FX_IsDataAvailDelegate(IntPtr param, UIntPtr offset, UIntPtr size);
271+
272+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
273+
private delegate void FX_AddSegmentDelegate(IntPtr param, UIntPtr offset, UIntPtr size);
274+
235275
#if !BROWSER
236276
private static readonly FPDF_GetBlockDelegate _getBlockDelegate = FPDF_GetBlock;
277+
278+
private static readonly FX_IsDataAvailDelegate _isDataAvailDelegate = FX_IsDataAvail;
279+
280+
private static readonly FX_AddSegmentDelegate _addSegmentDelegate = FX_AddSegment;
237281
#endif
238282

239283
[StructLayout(LayoutKind.Sequential)]
@@ -243,6 +287,7 @@ public readonly struct FPDF_FILEACCESS(uint m_FileLen, IntPtr m_GetBlock, IntPtr
243287
private readonly IntPtr m_GetBlock = m_GetBlock;
244288
private readonly IntPtr m_Param = m_Param;
245289
}
290+
246291
}
247292
}
248293
#endif

0 commit comments

Comments
 (0)