Edit a PDF in Flutter
By: David Gonzalez
Tags: Pdf, Android, Edit, iOS, Flutter

For one of my Flutter application, I needed to generate a pdf file containing user information. It consisted in inserting some information (image, text, and signature) into a template pdf file, and finally save modifications as a new pdf file.

My searches from pub package website leads me to this conclusion: there is no Flutter library for pdf edition. After some searches on internet, I found a couple of libraries that give a solution for editing a pdf, PSPDFKit and Pdftron, but they are not free.

My goal does not involve to edit again an already modified pdf file. So I build a custom solution, I like to call it the “Spare wheel” solution, for programmatically edit a pdf.

Solution Explanation

The idea is very simple: it consists in reading the pdf file as images (one image per page), and then create a pdf, having as background one of the retrieved images.

I know, this is not the best solution, because it will lower the rendered quality. For example, the user will be able to see blurred text when the file is zoomed. But that solution comes with a cheap cost (time, money, stress) for an acceptable result.

During my searches, I found two pub package that I will have to combine:

pdf_image_renderer: library dedicated to transform pdf files into a list of images. Friendly (short) api, no documentation but at least good example.

pdf: the most known pdf related library for Flutter, focusing on pdf creation. This package contains a lot of ‘widgets’ well documented, that copy flutter SDK. So very straightforward to use, if you are vigilant on where widgets comes from. For example, this library define widgets Center, Text, Container, et cetera…that you have to differentiate from corresponding widgets provided by Flutter sdk.

Added to these two libraries, we will need path_provider, in order to save files in a public folder. Also, I found that we cannot load pdf from our application assets (warning is also written in applcation logs, when I tried to read from assets).

It will be a “piece of cake” !

The cake

Conception

There is two steps in the conception: first decode pdf file, providing a list of images, and then create the pdf to render, where each page will be a stack of PdfWidgets, and the bottom most one will be the image coming from the initial pdf.

Retrieve pdf data as a list of images

First, I import pdf_image_renderer as pdfRender; I use prefix to avoid confusion between the two libraries. Then, I developed the PdfRawImage class, that will actually hold all needed information about the retrieved images: raw data as unsigned integer list, and the size of the page. I will use raw data to draw an Image (widget of pdf package), and use size to set the same dimensions to Page instances (of pdf package).

import 'package:pdf_image_renderer/pdf_image_renderer.dart' as pdfRender;



... class _PdfFileHandler { static Future<List<PdfRawImage>> loadPdf(String path) async {

var file = pdfRender.PdfImageRendererPdf(path: path);

await file.open();

var count = await file.getPageCount();

var images = List<PdfRawImage>();

for (var i = 0; i < count; i++) {

var size = await file.getPageSize(pageIndex: i);

var rawImage = await file.renderPage(

background: Colors.transparent,

x: 0,

y: 0,

width: size.width,

height: size.height,

scale: 1.0,

pageIndex: i,

);

images.add(PdfRawImage(

data: rawImage,

size: Size(size.width.toDouble(), size.height.toDouble()),

));

}

return images;

}

As you can see, this is a very simple development, where we used a for loop to retrieve pages data one by one. For each page, we render an image of the exact page size, and then store the data (raw image and size) into a PdfRawImage instance. Note that no scale is applied when rendering the page, in order to preserve quality as much as possible.

Added to that, I developed another method to read pdf packaged into the application.

static Future<File> getFileFromAssets(String filename) async {

assert(filename != null);

final byteData = await rootBundle.load(filename);

var name = filename.split(Platform.pathSeparator).last;

var absoluteName = '${(await getLibraryDirectory()).path}/$name';

final file = File(absoluteName);



await file.writeAsBytes(byteData.buffer.asUint8List());



return file;

}

Well, if we cannot read pdf file from the application assets, then make a copy of it in an internal application directory :D

Me realizing that we cannot load pdf from asset file directly

We load the file thanks to the rootBundle (part of the Flutter sdk), then write all retrieved data into another directory (you should avoid public/external directories, as you only copy the pdf to modify). Consider it as a temporary file.

Create the pdf file to render

In that part, we will build our pdf file, adding extra elements to it (modifications).

Pdf library comes with two entry point: pdf/pdf.dart, mostly defining data, and pdf/widgets.dart, containing all the widgets. I prefixed them respectively as ‘pdf’ and ‘pdfWidgets’, to avoid confusion (and errors).

To create a pdf document, first you will have to create a pdfWidgets.Document object, then add pdfWidgets.Page instances to it. We will define a page as a Stack containing, as first element, one of the image retrieved earlier.

final PdfRawImage _background;

final List<pdfWidgets.Widget> _stackedItems; pdfWidgets.Page build(pdfWidgets.Document document) {

final format = pdf.PdfPageFormat(_background.size.width, _background.size.height);

return pdfWidgets.Page(

pageFormat: format,

orientation: pdfWidgets.PageOrientation.portrait,

build: (context) {

return pdfWidgets.Stack(

children: [

pdfWidgets.Image(pdf.PdfImage.file(

document.document,

bytes: _background.data,

)),

..._stackedItems,

],

);

});

}

As you can observe, we define the pdfWidgets.Page to be the same size as the retrieved image, named in that example ‘_background’.

Me thinking that i will have to prefix all pdf widget classes if I want to use it into my StatefulWidget

Warning: pdf library copy the name of a lot of Flutter sdk classes. In that example, ‘context’ is not a BuildContext, but a pdfWidget.Context instance !

Saving a document is simple: use the pdfWidget.Document.save method to write pdf objects into an Uint8List. Then save this raw data into a file (using File.writeAsBytes method, as we saw it when copying the template from the application internal assets).

class _PdfFileHandler { ... static Future<File> save(pdfWidgets.Document document, String filename, {String directory}) async {

final dir = directory ?? (await getApplicationDocumentsDirectory()).path;

final file = File('$dir${Platform.pathSeparator}$filename');

return file.writeAsBytes(document.save());

}

Architecture choices

There is something a little bit annoying in that process: we have to add at the same time the “background” (image coming from the pdf to modify) and the widgets we want to insert. The reason underneath is that we cannot add a widget to a pdfWidgets.Page instance. So to avoid that problem, we can design wrappers of a pdfWidgets.Document and a pdfWidgets.Page that will hold background images and page size for us.

class PdfMutablePage {

final PdfRawImage _background;

final List<pdfWidgets.Widget> _stackedItems;



PdfMutablePage({PdfRawImage background})

: _background = background,

_stackedItems = [];



void add({pdfWidgets.Widget item}) {

_stackedItems.add(item);

}



Size get size => _background.size;



pdfWidgets.Page build(pdfWidgets.Document document) {

...

}

}

As shown previously, the build method is there to actually build the pdfWidgets.Page instance, used by another class or our own to provide more flexibility to the pdfWidgets.Document.

class PdfMutableDocument {

String _filePath;

final List<PdfMutablePage> _pages;



PdfMutableDocument._({List<PdfMutablePage> pages, String filePath})

: _pages = pages ?? [],

_filePath = filePath;



static Future<PdfMutableDocument> asset(String assetName) async {

var copy = await _PdfFileHandler.getFileFromAssets(assetName);

final rawImages = await _PdfFileHandler.loadPdf(copy.path);

final pages = rawImages.map((raw) => PdfMutablePage(background: raw)).toList();

return PdfMutableDocument._(pages: pages, filePath: copy.uri.pathSegments.last);

}



void addPage(PdfMutablePage page) => _pages.add(page);



PdfMutablePage getPage(int index) => _pages[index];



pdfWidgets.Document build() {

var doc = pdfWidgets.Document();

_pages.forEach((page) => doc.addPage(page.build(doc)));

return doc;

}



Future<void> save({String filename}) async {

_PdfFileHandler.save(build(), filename ?? _filePath);

}

}

In that design, as we build the pages and document at the very last moment (in save method), we can easily apply modifications to the content of the pdf .

When you call the ‘save’ method, the pdf document is actually created and saved

The main points of that basic architecture are :

Technical process (image from the pdf to edit as page background) is hidden to the developer. He/She doesn’t have to worry about that.

Pages are automatically sized

You can add element after having created your page, which were not the case in pdf library

having created your page, which were not the case in library As modifications will be draw in a Stack, you can position them freely, or use Center, Align, … (there is no limitation in this aspect)

But as mentioned at the beginning, the architecture has some limitations :

You cannot retrieve information from the pdf to edit : you won’t be able to search for a specific text

The quality of the edited pdf will be degraded compare to the original version; consequently editing several times a same pdf file is not recommended.

As a result, here is a little example demonstrating the efficiency of that architecture:

void _edit() async {

PdfMutableDocument doc = await PdfMutableDocument.asset("assets/test.pdf");

_editDocument(doc);

await doc.save(filename: "modified.pdf");

print("PDF Edition Done");

}



void _editDocument(PdfMutableDocument document) {

var page = document.getPage(0);

page.add(item: pdfWidget.Positioned(

left: 0.0,

top: 0.0,

child: pdfWidget.Text("COUCOU",

style: pdfWidget.TextStyle(fontSize: 32, color: pdf.PdfColors.red)),

));

var centeredText = pdfWidget.Center(

child: pdfWidget.Text(

"CENTERED TEXT",

style: pdfWidget.TextStyle(

fontSize: 40,

color: pdf.PdfColors.green,

background: pdfWidget.BoxDecoration(color: pdf.PdfColors.amber)),

));

page.add(item: centeredText);

}

Loading and saving a pdf file is achieved in two lines of code. Pdf edition allow us to use all the pdf widgets: you can either design the whole modifications once (in one big pdf widget), or in several little modifications as in the above example.

Me, after having spent the entire afternoon for providing this solution

And as always in my tutorial, you can have access to the full code source