I wanted to convert a PDF page into a PNG. There's a handy pdfium_dart library that does the heavy lifting and doesn't require Flutter. It is a bit low-level, but there's nothing to fear.
Get the library and initialize it:
final pdfium = getPdfium();
pdfium.FPDF_InitLibrary();
Later, we need to call FPDF_DestroyLibrary(), see below.
Load the PDF into native memory so you can pass it to the PDFium library:
final pdf = File('...');
final length = await pdf.length();
final nativeBuffer = malloc<Uint8>(length);
final dartBuffer = nativeBuffer.asTypedList(length);
final raf = await pdf.open();
await raf.readInto(dartBuffer);
await raf.close();
I want to omit a needless copy. Because the native part cannot safely access the memory of a Dart type, we need to create a native buffer and then create a view on the Dart side and use a RandomAccessFile which has a readInto method that doesn't create its own List<int> to return the data.
Next, load the document (and don't forget to free the allocated memory and to de-initialize the library):
try {
final document = pdfium.FPDF_LoadMemDocument(
nativeBuffer.cast(),
length,
nullptr,
);
if (document == nullptr) throw Exception('cannot load $pdf');
...
} finally {
malloc.free(nativeBuffer);
pdfium.FPDF_DestroyLibrary();
}
If you have the PDF accessible as a file, consider using FPDF_LoadDocument instead. There's also a FPDF_LoadCustomDocument function which takes a callback it will use to ask for chunks of memory in case you don't want to load everything into memory. PDF files can be huge. I couldn't really make use of that function, though, because that would require synchronous IO. Think for example downloading only chunks of an URL using HTTP range requests.
Next, load the first page:
try {
final page = pdfium.FPDF_LoadPage(document, 0);
if (page == nullptr) throw Exception('cannot load first page');
...
} finally {
pdfium.FPDF_CloseDocument(document);
}
Next, create a native bitmap which is then used by PDFium to reader the page. I'm scaling it down so it fits a 96x96 pixel square. Feel free to adapt the imageSize:
try {
var pageWidth = pdfium.FPDF_GetPageWidthF(page);
var pageHeight = pdfium.FPDF_GetPageHeightF(page);
if (pageWidth > pageHeight) {
pageHeight /= pageWidth;
pageWidth = 1;
} else {
pageWidth /= pageHeight;
pageHeight = 1;
}
const imageSize = 96.0;
final width = (pageWidth * imageSize).round();
final height = (pageHeight * imageSize).round();
final bitmap = pdfium.FPDFBitmap_Create(width, height, 0);
if (bitmap == nullptr) throw Exception('cannot allocate bitmap $widthĂ$height');
...
} finally {
pdfium.FPDF_ClosePage(page);
}
Just to be sure, fill the bitmap with a white background:
try {
pdfium.FPDFBitmap_FillRect(bitmap, 0, 0, width, height, 0xffffffff);
...
} finally {
pdfium.FPDFBitmap_Destroy(bitmap);
}
And last but not least, render the page:
pdfium.FPDF_RenderPageBitmap(bitmap, page, 0, 0, width, height, 0, 0);
...
There's one problem, though. PDFium returns a BGRA888 image that might have a rowStride, that is, rows of pixels might not be contiuous. If we want to pass this to the Dart image library, we need to take this into account:
final stride = pdfium.FPDFBitmap_GetStride(bitmap);
final bytes = pdfium.FPDFBitmap_GetBuffer(bitmap)
.cast<Uint8>()
.asTypedList(stride * height)
.buffer;
final image = Image.fromBytes(
width: width,
height: height,
bytes: bytes,
rowStride: stride,
order: .bgra,
);
return encodePng(image);
However, it wouldn't be that difficult to create the PNG yourself in case you don't want jet another 3rd party dependency. Writing the required PNG header is trivial, but you'd probably need to lookup how to compute the required checksum. Then use Dart's ZLibCodec, to get a ZLibEncoder and use startChunkedConversion to feed it each unstrided line of pixel, converting BGRA to RGBA by hand and prefix it with a 0 filter byte.
Finally, we need to clean up (already shown above and repeated here only for completeness):
} finally {
pdfium.FPDFBitmap_Destroy(bitmap);
}
} finally {
pdfium.FPDF_ClosePage(page);
}
} finally {
pdfium.FPDF_CloseDocument(document);
}
And⌠we're done.
We might move everything into an isolate (using Isolate.run) so PDF rendering and PNG creating is done in the background, although I'm unsure whether PDFium likes it if you initialize it multiple times concurrently. Please try and report back :)