Een PDF pagina als achtergrond in Flutter

In mijn huidige Flutter Web project vroegen gebruikers om een pagina uit een PDF document als achtergrondplaatje te kunnen gebruiken. Omdat het vrij grote bouwtekeningen betreft, en onze applicatie een eigen oplossing heeft voor navigatie over de tekening, voldeden geen van de beschikbare packages. Dit is hoe ik het toch heb opgelost.

Het goede nieuws was dat er een aantal Flutter packages zijn die complexe PDF documenten betrouwbaar kunnen laden en als widget op het scherm toveren. Helaas gaan al deze packages er vanuit dat de gebruiker door een volledig document of toch minstens over een hele pagina wil navigeren. In ons geval hebben we alleen het plaatje op een door ons bepaalde schaal nodig, dus de standaard oplossingen waren voor ons allemaal te geavanceerd.

Nadere inspectie van de verschillende packages leverde op dat ze onder de motorkap verrassende overeenkomsten hebben, en voor de rendering in Flutter Web allemaal terug vallen op PDF.js. We hebben na wat inspectie van de broncode voor het PDFX package gekozen, omdat de implementatie een nettere indruk gaf dan andere vergelijkbare packages.

Het gebruik van PDFX is vrij eenvoudig, en er is niet meer dan onderstaande code nodig om een specifieke pagina van een document te lezen::

import 'package:pdfx/pdfx.dart';

final pdfDocument = await Pdfdocument.openData(bytes);
final pdfPage = await document.getPage(pageNr);

Na wat zoeken bleek PdfPage een manier te hebben om een (facade voor een) Flutter Texture te kunnen produceren, die vervolgens te gebruiken is om met de PDF rendering engine van het platform te koppelen:

import 'package:pdfx/src/viewer/wrappers/pdf_texture.dart';

final pdfPageTexture = await pdfPage.createTexture();
final pdfTexture = PdfTexture(textureId: pdfPageTexture.id);

Deze pdfTexture is een widget dat rechtstreeks gebruikt kan worden in een build() functie om de inhoud van de pagina te tonen. Het vervelende hierbij is dat PdfTexture een verborgen implementation class is, die niet in de normale PDFX package import beschikbaar is. Maar het lijkt niet plausibel dat dit deze code snel zal wijzigen, dus het is een berekend risico om deze import toch te gebruiken.

Vervolgens kan de inhoud van de texture worden gewijzigd door via de parameters van de pdfPageTexture.updateRect(...) functie aan te geven welk stuk van de pagina in welke resolutie getoond moet worden. De low-level engine produceert vervolgens vanzelf asynchroon een nieuw plaatje, wat door de PdfTexture automatisch getoond wordt. Afhankelijk van de complexiteit van de pagina kan dit echter in de PDF.js implementatie voor Flutter Web honderden milliseconden duren.

Omdat gebruikers in onze applicatie kunnen in- en uitzoomen, is deze vertraging bij het renderen een probleem. Zonder tijdige feedback kun je immers niet meer zien waar je je op de pagina bevinden. Om dat op te lossen, maak ik gebruik van de mogelijkheid in PDFX om een (relatief) lage resolutie plaatje van de pagina als afbeelding te renderen:

final pdfPageImage = await pdfPage.render(width: width, height: height);
final image = Image.memory(image.bytes, fit: BoxFit.fill);

Dit image is een standaard Flutter Image widget dat door middel van een Positioned widget binnen een Stack als tijdelijke plaatsvervanger van de gedetailleerde PdfTexture gebruikt kan worden. Dit blijkt in praktijk een bruikbaar compromis met een hoge performance op te leveren, omdat het renderen van afbeeldingen in een browser vloeiend werkt.

Mijn conclusie uit deze ervaring is dat Flutter ondanks de multi-platform abstracties (zelfs in een web omgeving) voldoende low-level blijft om externe packages naar de hand te zetten.