DEV Community

Robin for Capawesome

Posted on Originally published at capawesome.io

How to Scan a Document to PDF in a Capacitor App

Originally published on the Capawesome blog.

A signed contract that comes out of a scanner as a folder of JPEGs is hard to email, print or file, so most apps want one PDF instead. The Capacitor Document Scanner plugin returns that PDF next to the individual page images when you pass generatePdf: true to scanDocument(...). This guide shows how to scan a document to PDF in a Capacitor app and what to do with the file afterwards, from opening it in a native viewer to printing it, sharing it and keeping it past the next app launch. The scanner and the Capacitor Printer plugin are part of Capawesome Insiders; the PDF viewer, the file opener and the official Share and Filesystem plugins are free.

Ship a fix in one command with Capawesome Cloud Live Updates, no store review

Key takeaways:

  • scanDocument({ generatePdf: true }) returns one combined PDF in pdf and still returns every page as a JPEG in scannedImages, from the same scan.
  • On Android, Google's ML Kit generates the PDF; on iOS, VisionKit returns only page images, and the plugin composes an image-only PDF from them.
  • The pdf value is a file URL in the app's cache that the PDF Viewer, Printer, File Opener and @capacitor/share plugins accept without conversion.
  • pageLimit defaults to 10 and caps the PDF as well, so raise it for longer documents; on iOS, extra pages are dropped after scanning.
  • The Capacitor Document Scanner plugin clears its cache files when it loads on the next app launch, so copy the PDF to Directory.Data in the same flow.

One PDF per scan

To turn scanned pages into a single PDF in Capacitor, call scanDocument(...) of the Capacitor Document Scanner plugin with generatePdf: true. The result then holds one combined PDF of all scanned pages in pdf, and the pages themselves as JPEG files in scannedImages. The plugin is part of Capawesome Insiders. To install it, please refer to the Installation section in the plugin documentation, and do the same for each companion plugin below. Announcing the Capacitor Document Scanner Plugin covers setup and the Android scanner options, so this guide starts at the scan call:

import { DocumentScanner } from '@capawesome-team/capacitor-document-scanner';

const scanToPdf = async () => {
  try {
    const { pdf, scannedImages } = await DocumentScanner.scanDocument({
      generatePdf: true,
    });
    return { pdf, scannedImages };
  } catch (error) {
    if ((error as { code?: string }).code === 'SCAN_CANCELED') {
      return null;
    }
    throw error;
  }
};
Enter fullscreen mode Exit fullscreen mode

When the user backs out of the scanner, the promise rejects with the SCAN_CANCELED error code. That is a normal outcome, so the function returns null instead of throwing. pdf is typed string | null because it is only set when generatePdf is true; the snippets in the following sections take it as a string.

The two platforms produce the PDF differently. On Android, ML Kit generates it. Google's Android guide states that the scanner can return both PDF and JPEG files for a scan, depending on the formats an app requests with setResultFormats, and it recommends requesting only the formats you need, since generating document files takes time and processing power. The scanner itself is a Google Play services module rather than part of your APK. Google lists its app size impact as a "~300KB download size increase", and it only runs on devices with Play services, which is what isAvailable() checks on Android.

On iOS, Apple's VNDocumentCameraViewController returns page images and no PDF. Apple leaves the export to the app: "With the collection of scanned images, your app can create a digital version of the physical document and export the scanned images to PDF." The plugin does that export with one PDF page per scanned page, and the result is an image-only PDF without a text layer.

Page limit and quality

The pageLimit option caps the PDF along with the images. It defaults to 10, so a 14-page contract never fits into one PDF with default options. Raise the limit for long documents:

import { DocumentScanner } from '@capawesome-team/capacitor-document-scanner';

const scanContract = async () => {
  const { pdf } = await DocumentScanner.scanDocument({
    generatePdf: true,
    pageLimit: 30,
  });
  return pdf;
};
Enter fullscreen mode Exit fullscreen mode

On Android, ML Kit enforces the limit inside the scanner UI. VisionKit has no public API to stop the scanner after a set number of pages, so on iOS the plugin truncates the result after scanning, and the pages past the limit are missing from the PDF as well. Set pageLimit above the longest document your users handle, or mention the limit in your own UI before the scanner opens.

The imageQuality option does not shrink the PDF. It re-encodes only the JPEG pages and leaves the PDF untouched on both platforms, and the plugin has no option for the PDF's page size, resolution or compression. Lower imageQuality when you store or upload the JPEGs, not to make pdf smaller.

Open the PDF in-app

The Capacitor PDF Viewer plugin shows the scanned PDF in a fullscreen native viewer with a toolbar, paging and pinch-to-zoom, without the user leaving your app. It is free. Pass the scanner's pdf value as path to open(...):

import { PdfViewer } from '@capawesome/capacitor-pdf-viewer';

const openScan = async (pdf: string) => {
  await PdfViewer.open({
    path: pdf,
    title: 'Rental contract',
    showShareButton: true,
  });
};
Enter fullscreen mode Exit fullscreen mode

Always pass a title. The viewer falls back to the file name, and the scanner names its files with a generated ID, not with anything a user would recognize. showShareButton adds a share button to the viewer toolbar on Android and iOS; on Android, it depends on the same file provider setup as the Share plugin (see Share or hand off).

The viewer renders differently per platform. On iOS, the plugin uses Apple's PDFKit and needs no configuration. On Android, it uses the android-pdf-viewer library, which bundles the Pdfium native libraries and adds about 10 to 16 MB (uncompressed, across all ABIs) to your app. Published as an Android App Bundle, each device downloads only the libraries for its own ABI. The Android viewer does not support text selection.

Print the PDF

The Capacitor Printer plugin prints the scanned PDF with printPdf(...), which presents the platform's printing UI on Android and iOS. Like the scanner, it is a Capawesome Insiders plugin.

import { Printer } from '@capawesome-team/capacitor-printer';

const printScan = async (pdf: string) => {
  await Printer.printPdf({
    name: 'Rental contract',
    path: pdf,
  });
};
Enter fullscreen mode Exit fullscreen mode

name sets the name of the print job and defaults to Document, so pass the same label you gave the viewer. The Printer plugin can print the scanned images as well, for example through printFile(...), when the user needs a single page rather than the whole document. Exploring the Capacitor Printer API covers the remaining print methods, from base64 data to HTML.

Share or hand off

The free @capacitor/share plugin from the Capacitor team opens the platform's share sheet, so the user can send the PDF to another app, such as a mail client or a messenger. Its files option takes an array of file:// URLs on Android and iOS, and the scanner's pdf value is such a URL:

import { Share } from '@capacitor/share';

const shareScan = async (pdf: string) => {
  await Share.share({
    title: 'Rental contract',
    files: [pdf],
  });
};
Enter fullscreen mode Exit fullscreen mode

title becomes the subject when the user shares to email. If the user closes the sheet without picking an app, share() rejects with the message Share canceled, which deserves the same handling as a canceled scan.

Sharing the scanned PDF on Android needs no configuration. The Share documentation gives the reason:

> By default, Capacitor apps only allow to share files from caches folder. To make other Android folders shareable, they have to be added in android/app/src/main/res/xml/file_paths.xml file.

The scanner writes to that cache folder, and the file_paths.xml of a default Capacitor app already declares it with a `` entry. The PDF Viewer share button and the File Opener plugin pass files to other apps through the same file provider, so they work on the cached PDF without changes too.

To open the PDF in another app rather than send it, use the free Capacitor File Opener plugin. openFile(...) determines the MIME type on its own:

`typescript
import { FileOpener } from '@capawesome-team/capacitor-file-opener';

const openInDefaultApp = async (pdf: string) => {
await FileOpener.openFile({ path: pdf });
};
`

On iOS, the call shows a Quick Look preview inside your app, with the system share button. On Android, it opens the PDF in the user's default PDF app, or shows the app chooser if no default is set. Compared with the PDF Viewer plugin, File Opener leaves the choice of viewer to the platform, while the PDF Viewer plugin lets you set the title and the initial page and listen for page changes.

Keep the PDF

The Capacitor Document Scanner plugin writes the PDF to the app's cache directory and deletes it again on the next app launch. From the plugin's FAQ:

> Stale files created by the plugin are cleaned up automatically when the plugin is loaded. Copy the files to a persistent location if you need to keep them.

The official @capacitor/filesystem plugin, which is free, copies the PDF to a persistent directory:

`typescript
import { Directory, Filesystem } from '@capacitor/filesystem';

const keepScan = async (pdf: string) => {
const { uri } = await Filesystem.copy({
from: pdf,
to: 'contract.pdf',
toDirectory: Directory.Data,
});
return uri;
};
`

Without a directory option, the Filesystem plugin reads from as a full file:// path, and toDirectory places the copy in Directory.Data, which is the app's files directory on Android and the Documents directory on iOS. Files there are deleted when the app is uninstalled, not on the next launch. copy(...) resolves with the uri of the copy, which you store with the document record. Give every document its own file name, since a fixed name like contract.pdf only fits one.

The Capacitor File Manager plugin, an Insiders plugin, does the same with copyFile(...), which copies natively with constant memory usage and takes file:// URIs throughout its API. Android Scoped Storage in Capacitor Apps, Explained compares the directories an Android app can write to without a permission. On iOS, the Filesystem documentation lists two Info.plist keys, UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace, that make the app's files appear in the Files app when set to YES.

Moving the PDF out of the cache has one consequence on Android. The app's files directory is not in Capacitor's default file_paths.xml, so the Share plugin, the File Opener plugin and the PDF Viewer share button cannot pass the copy to another app until you declare it. Add a ` entry to android/app/src/main/res/xml/file_paths.xml`:







Enter fullscreen mode Exit fullscreen mode

The first two entries are the Capacitor defaults, and `` adds the app's files directory.

If the scan goes to a backend instead of staying on the device, the Capacitor File Transfer plugin, also an Insiders plugin, uploads files in the background; Announcing the Capacitor File Transfer Plugin walks through it.

FAQ

How do I turn scanned pages into a single PDF in Capacitor?

Call scanDocument(...) of the Capacitor Document Scanner plugin with generatePdf: true. The result contains one combined PDF in pdf and the page images in scannedImages. ML Kit generates the PDF on Android, the plugin composes it from the VisionKit page images on iOS, and it holds at most pageLimit pages (default 10).

Is the scanned PDF searchable?

The Capacitor Document Scanner plugin runs no OCR, and on iOS the PDF has no text layer. If you need the text of a document, run processImage(...) of the free Capacitor ML Kit Text Recognition plugin on the JPEG pages in scannedImages.

Does scan-to-PDF work on the web?

No. scanDocument(...) rejects with an unimplemented error on the web. The PDF Viewer plugin has no web implementation either, since browsers display a PDF in an `

Top comments (2)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.