Aller au contenu principal

Comment scanner un document en PDF dans une application Capacitor

Scannez un document en PDF dans une application Capacitor : capturez les pages avec le scanner natif, créez un PDF unique, puis enregistrez, partagez, ouvrez ou imprimez-le sur iOS et Android.

Scanner un document papier en PDF dans une application mobile Capacitor

Pour scanner un document en PDF dans une application Capacitor, ouvrez le scanner natif avec @capgo/capacitor-document-scanner, récupérez les pages JPEG qu'il renvoie, puis combinez-les en un seul PDF avec pdf-lib. À partir de là, vous écrivez le PDF avec @capacitor/filesystem et vous le transmettez au menu de partage, à un lecteur PDF ou à la boîte de dialogue d'impression du système. Ce guide décrit le flux complet avec code qui s'exécute sur Capacitor 8, sur iOS et Android.

Comment fonctionne le scan-to-PDF sur iOS et Android

Les deux plateformes incluent un scanner de documents système, vous n'avez donc pas besoin de développer vous-même la détection de contours ou la correction de perspective :

iOS Android
Scanner natif VisionKit VNDocumentCameraViewController Google ML Kit Document Scanner (Play services)
Détection de contours et recadrage Oui Oui
Filtres / nettoyage VisionKit intégré base, base_with_filter, full modes (suppression des taches et des doigts) full)
Limite de pages par session 24 De 1 à 24, par défaut 20
Prompt de permission caméra Oui, nécessite NSCameraUsageDescription Non, le scanner s'exécute dans Play services
Fonctionne sur émulateur / simulateur Non Non
Sortie du plugin pages JPEG pages JPEG

Le plugin renvoie des images, pas un PDF. C'est un choix délibéré : vous contrôlez la taille de page, la compression et le nom du fichier, et le même PDF code fonctionne aussi bien pour les photos choisies dans la galerie que pour les fichiers reçus de votre backend.

Installer les plugins

bun add @capgo/capacitor-document-scanner @capacitor/filesystem pdf-lib
bunx cap sync

Le plugin scanner suit les versions majeures de Capacitor, donc la v8 du plugin cible Capacitor 8. pdf-lib est une bibliothèque JavaScript pure sans code natif, ce qui signifie qu'elle s'exécute dans le WebView et ne nécessite aucune synchronisation.

Configuration iOS

Ajoutez une chaîne d’utilisation de l’appareil photo à ios/App/App/Info.plist. VisionKit ouvre l’appareil photo, et iOS met fin à l’exécution de l’application si la clé est manquante :

<key>NSCameraUsageDescription</key>
<string>We use the camera to scan your documents.</string>

Le package Swift du plugin cible iOS 15 ou ultérieur, ce qui correspond au minimum de Capacitor 8. N’oubliez pas que les uploads sur l’App Store exigent désormais Xcode 26, donc compilez avec cette version avant de soumettre. Si vous n’avez pas de Mac, Build Capgo peut générer le binaire iOS signé dans le cloud.

Configuration Android

Rien à ajouter à AndroidManifest.xml. Le scanner ML Kit est un module Google Play services qui se télécharge au premier usage et gère lui-même l’appareil photo, votre application ne demande donc pas CAMERA. L’appareil doit disposer de Google Play services, ce qui exclut la plupart des émulateurs et les téléphones sans Google. Prévoyez un plan B (une simple capture photo) si vous publiez sur des marchés où Play services est absent.

Scanner les pages

Commencez par une fonction qui ouvre le scanner et renvoie les chemins des pages, ou null si l’utilisateur annule :

import {
  DocumentScanner,
  ResponseType,
  ScannerMode,
  ScanDocumentResponseStatus,
} from '@capgo/capacitor-document-scanner';

export async function scanPages(): Promise<string[] | null> {
  const result = await DocumentScanner.scanDocument({
    responseType: ResponseType.ImageFilePath,
    maxNumDocuments: 20,
    letUserAdjustCrop: true,
    scannerMode: ScannerMode.Full, // Android only
    croppedImageQuality: 90,       // Android only
  });

  if (result.status === ScanDocumentResponseStatus.Cancel) {
    return null;
  }
  return result.scannedImages ?? [];
}

Quelques options méritent votre attention :

  • responseType: ImageFilePath (par défaut) renvoie des chemins de fichiers, Base64 renvoie des chaînes base64. Privilégiez les chemins de fichiers pour les documents multi-pages. Les chaînes base64 de 20 pages haute résolution peuvent atteindre des dizaines de Mo et chaque octet traverse le pont Capacitor.
  • maxNumDocuments: définissez-le sur 1 pour les flux à page unique comme les reçus ou les cartes d'identité.
  • reviewCapturedDocument: affiche chaque page capturée avant de continuer, ce qui est utile quand les utilisateurs scannent des documents importants.
  • brightness et contrast: post-traitement appliqué aux images. Un contraste de 1.2 vers 1.5 aide souvent avec les reçus décolorés.
  • scannerMode (Android) : Full ajoute un nettoyage basé sur l'IA, Base est plus rapide et conserve l'image intacte.

L'emplacement des fichiers varie selon la plateforme. Sur Android, les pages vont dans le répertoire cache de l'app. Sur iOS, le plugin les écrit dans le répertoire Documents de l'app. Aucun de ces emplacements n'est idéal pour le long terme, donc le flux ci-dessous génère le PDF, le stocke où vous le souhaitez, puis supprime les images des pages.

Combiner les pages en un seul PDF

pdf-lib peut intégrer des JPEG directement sans les réencoder, afin que la taille du PDF reste proche de la somme des tailles des pages. Lire chaque page via Capacitor.convertFileSrc, qui transforme un chemin natif en une URL que la WebView peut récupérer :

import { Capacitor } from '@capacitor/core';
import { PDFDocument } from 'pdf-lib';

const A4 = { width: 595.28, height: 841.89 }; // points

export async function pagesToPdf(paths: string[]): Promise<string> {
  const pdf = await PDFDocument.create();
  pdf.setTitle('Scanned document');
  pdf.setCreator('My Capacitor App');

  for (const path of paths) {
    const bytes = await fetch(Capacitor.convertFileSrc(path)).then((r) => r.arrayBuffer());
    const image = await pdf.embedJpg(bytes);

    // Fit the image inside an A4 page, keeping its aspect ratio
    const scale = Math.min(A4.width / image.width, A4.height / image.height);
    const width = image.width * scale;
    const height = image.height * scale;

    const page = pdf.addPage([A4.width, A4.height]);
    page.drawImage(image, {
      x: (A4.width - width) / 2,
      y: (A4.height - height) / 2,
      width,
      height,
    });
  }

  // Base64 without the data URI prefix, ready for Filesystem.writeFile
  return pdf.saveAsBase64();
}

Si vous préférez des pages qui correspondent exactement au papier scanné, remplacez la logique A4 par pdf.addPage([image.width, image.height]) et dessinez l'image en taille réelle. Les reçus rendent mieux ainsi, car ils sont longs et étroits.

Alternative : générer le PDF nativement

Si votre application génère déjà des PDF à partir de HTML (factures, rapports), vous pouvez réutiliser ce chemin avec @capgo/capacitor-pdf-generator. Il génère une chaîne HTML via le moteur d'impression de la plateforme et renvoie du base64 :

import { PdfGenerator } from '@capgo/capacitor-pdf-generator';
import { Capacitor } from '@capacitor/core';

const html = `
  <html><body style="margin:0">
    ${paths
      .map((p) => `<img src="${Capacitor.convertFileSrc(p)}" style="width:100%;page-break-after:always" />`)
      .join('')}
  </body></html>`;

const result = await PdfGenerator.fromData({
  data: html,
  documentSize: 'A4',
  orientation: 'portrait',
  type: 'base64',
});

if (result.type === 'base64') {
  // result.base64 contains the PDF
}

C'est pratique quand vous voulez une page de garde, un titre ou un pied de page avec une date sur chaque page. Pour du pur image-vers-PDF, pdf-lib vous offre un meilleur contrôle sur la géométrie des pages et la compression d'image.

Enregistrer le PDF

Écrivez la sortie base64 avec @capacitor/filesystem. Choisissez le répertoire selon la durée de vie souhaitée du fichier :

Répertoire Android iOS Utilisez-le pour
Directory.Cache Cache de l’app, peut être vidé par l’OS Library/Caches, peut être vidé par l’OS PDF que vous uploadez ou partagez aussitôt
Directory.Data Fichiers privés de l’app, supprimés à la désinstallation cible Documents dossier, sauvegardé sur iCloud PDF gérés par votre app et réaffichés plus tard
Directory.LibraryNoCloud Fichiers privés de l’app Library/NoCloud, non sauvegardés PDF volumineux que vous pouvez retélécharger

Sur iOS, Directory.Data et Directory.Documents pointent vers le même dossier. Ajoutez UIFileSharingEnabled et LSSupportsOpeningDocumentsInPlace vers Info.plist si les utilisateurs doivent voir ces PDF dans l'app Fichiers.

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

export async function savePdf(base64: string, name: string) {
  const fileName = `${name.replace(/[^a-z0-9-_]/gi, '_')}.pdf`;
  const { uri } = await Filesystem.writeFile({
    path: `scans/${fileName}`,
    data: base64,
    directory: Directory.Data,
    recursive: true,
  });
  return uri; // file:// URI you can share, open, or print
}

export async function deletePages(paths: string[]) {
  await Promise.all(
    paths.map((path) => Filesystem.deleteFile({ path }).catch(() => undefined)),
  );
}

Sur les plateformes natives, writeFile traite une chaîne sans encoding option comme du base64 et écrit les octets binaires. Ne passez pas Encoding.UTF8 pour les PDF, sinon le fichier sera corrompu.

Si l'utilisateur doit pouvoir trouver le PDF dans son dossier Téléchargements sur Android, utilisez @capgo/capacitor-file-sharerde save() méthode, qui écrit dans MediaStore Downloads sans aucun droit de stockage sur Android 10+. Sur iOS, le même appel ouvre la feuille de partage, permettant à l’utilisateur de choisir « Enregistrer dans Fichiers ».

Assembler l'ensemble du flux

export async function scanToPdf(title: string) {
  const pages = await scanPages();
  if (!pages || pages.length === 0) return null;

  try {
    const base64 = await pagesToPdf(pages);
    return await savePdf(base64, title);
  } finally {
    await deletePages(pages);
  }
}

Associez-l’à un bouton et affichez un spinner pendant que pagesToPdf s’exécute. Sur un smartphone Android milieu de gamme, 10 pages prennent généralement moins de quelques secondes, car les données JPEG sont copiées, pas décodées.

Partager, ouvrir ou imprimer le PDF

Partager via le menu de partage système

import { FileSharer } from '@capgo/capacitor-file-sharer';

await FileSharer.share({
  filename: 'contract.pdf',
  path: pdfUri,
  contentType: 'application/pdf',
  subject: 'Signed contract',
});

@capgo/capacitor-file-sharer copie le fichier dans son propre dossier de cache et le sert via son propre FileProvider sur Android, vous n’avez donc pas besoin de modifier file_paths.xml, même si le PDF se trouve dans Directory.Data. Si vous utilisez le plugin officiel @capacitor/share à la place, son files fonctionne par défaut uniquement pour le dossier de cache sur Android. Les autres dossiers doivent être ajoutés à android/app/src/main/res/xml/file_paths.xml.

Ouvrir dans l'app PDF de l'utilisateur

Pour un aperçu rapide, l’outil d’ouverture de fichiers de la communauté transmet le fichier à Quick Look sur iOS ou au visionneuse PDF par défaut sur Android :

bun add @capacitor-community/file-opener
bunx cap sync
import { FileOpener } from '@capacitor-community/file-opener';

await FileOpener.open({
  filePath: pdfUri,
  contentType: 'application/pdf',
});

Imprimer

@capgo/capacitor-printer ouvre UIPrintInteractionController sur iOS et PrintManager sur Android :

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

await Printer.printPdf({
  name: 'Signed contract',
  path: pdfUri,
});

Le name becomes the print job name and the default file name if the user picks “Save as PDF” in the print dialog.

Envoyer le PDF

Pour envoyer le PDF à votre backend, relisez-l’en tant que Blob et publiez-l’avec fetch, ou utilisez un uploader natif pour les gros fichiers afin que l’upload se poursuive lorsque l’app passe en arrière-plan :

import { Uploader } from '@capgo/capacitor-uploader';

const { id } = await Uploader.startUpload({
  filePath: pdfUri,
  serverUrl: 'https://api.example.com/documents',
  method: 'POST',
  headers: { Authorization: `Bearer ${token}` },
  mimeType: 'application/pdf',
});

Consultez le plugin uploader Capacitor pour les événements de progression et les uploads multipart.

Rendre les PDF scannés consultables

Image-only PDFs cannot be searched or copied from. If users expect to search their scans:

  1. Exécutez l’OCR sur chaque image de page avant de la supprimer. Les options sur l’appareil incluent ML Kit Text Recognition sur Android et le framework Vision sur iOS, tous deux accessibles via des plugins Capacitor de la communauté, ou un service OCR côté serveur.
  2. Stockez le texte reconnu dans votre base de données à côté du chemin du PDF et effectuez la recherche là-bas.
  3. Si le PDF lui-même doit contenir du texte, générez-le côté serveur avec un outil OCR qui écrit une couche de texte invisible, comme OCRmyPDF.

Pour la plupart des applications, l’étape 2 suffit et évite d’expédier un second modèle volumineux dans l’application.

Troubleshooting

« Le scan de documents n’est pas pris en charge sur les émulateurs Android »: c’est prévu. Utilisez un appareil physique avec les services Google Play.

L'application crash sur iOS dès l'ouverture du scanner: NSCameraUsageDescription manque dans Info.plist, ou l'utilisateur a refusé l'accès à la caméra auparavant. Vérifiez Réglages > Confidentialité > Appareil photo.

embedJpg renvoie « SOI not found »: les octets ne sont pas ceux d'un JPEG. Cela arrive si vous avez passé une data URL base64 là où pdf-lib attendait des octets bruts, ou si vous avez récupéré un chemin qui renvoie une page d'erreur HTML. Loguez Capacitor.convertFileSrc(path) et ouvrez-le dans l'inspecteur Safari ou Chrome.

Le PDF est énorme: réduisez croppedImageQuality (Android) ou re-compressez les pages avec @capgo/capacitor-file-compressor avant l'intégration. Une qualité de 70 à 80 est généralement lisible pour des documents texte.

Les pages apparaissent pivotées: pivotez dans pdf-lib avec page.setRotation(degrees(90)) ou laissez l'utilisateur corriger la rotation sur l'écran de revue du scanner avec reviewCapturedDocument: true.

Le partage échoue sur Android avec FileUriExposedException ou « Failed to find configured root »: vous avez transmis un chemin hors des dossiers déclarés dans file_paths.xml vers @capacitor/share. Utilisez le plugin file sharer ou copiez le PDF dans Directory.Cache d'abord.

Old page images fill up storage on iOS: le scanner écrit des JPEG dans le répertoire Documents. Supprimez-les après la création du PDF, comme indiqué dans scanToPdf.

Déployer des mises à jour du flux de scan

Most of this feature lives in TypeScript: the PDF layout, file naming, upload logic, and UI. Changes to those parts do not need a new store release. With les live updates Capgo you can ship a fix to the PDF layout or upload endpoint to every user the same day, while native changes (adding the plugin, editing Info.plist) passent toujours par un build classique. Le docs scanner de documents liste toutes les options si vous devez ajuster davantage le scanner.

Live updates pour les apps Capacitor

Lorsqu'un bug de la couche web est en ligne, déployez le correctif via Capgo au lieu d'attendre des jours l'approbation des stores. Les utilisateurs reçoivent la mise à jour en arrière-plan, tandis que les changements natifs suivent le circuit de validation habituel.

Les derniers articles du blog

Capgo vous offre les meilleures informations pour créer une application mobile vraiment professionnelle.