Um in einer Capacitor-App ein Dokument als PDF zu scannen, öffnest du den nativen Scanner mit @capgo/capacitor-document-scanner, nimm die zurückgegebenen JPEG-Seiten und kombiniere sie zu einem PDF mit pdf-lib. Danach schreibst du das PDF mit @capacitor/filesystem und übergibst es an das Share-Sheet, einen PDF-Viewer oder den System-Druckdialog. Dieser Guide zeigt den kompletten Ablauf mit code, das auf Capacitor 8 unter iOS und Android läuft.
So funktioniert Scan-to-PDF unter iOS und Android
Beide Plattformen bieten einen System-Dokumentenscanner, sodass du keine Kanten- oder Perspektivkorrektur selbst implementieren musst:
| iOS | Android | |
|---|---|---|
| Nativer Scanner | VisionKit VNDocumentCameraViewController |
Google ML Kit Document Scanner (Play-Dienste) |
| Kantenerkennung und Zuschneiden | Ja | Ja |
| Filter / Bereinigung | VisionKit integriert | base, base_with_filter, full Modi (Flecken- und Fingerentfernung in full) |
| Seitenlimit pro Sitzung | 24 | 1 bis 24, Standard 20 |
| Kamera-Berechtigungsabfrage | Ja, erforderlich NSCameraUsageDescription |
Nein, der Scanner läuft in Play-Diensten |
| Funktioniert auf Emulator / Simulator | Nein | Nein |
| Ausgabe des Plugins | JPEG-Seiten | JPEG-Seiten |
Das Plugin gibt Bilder zurück, keine PDFs. Das ist eine bewusste Trennung: Du kontrollierst die Seitengröße, Komprimierung und den Dateinamen, und dasselbe PDF code funktioniert auch für Fotos aus der Galerie oder Dateien von deinem Backend.
Plugins installieren
bun add @capgo/capacitor-document-scanner @capacitor/filesystem pdf-lib
bunx cap sync
Das Scanner-Plugin folgt den Capacitor-Major-Versionen, daher zielt Plugin v8 auf Capacitor 8 ab. pdf-lib ist eine reine JavaScript-Bibliothek ohne natives code, was bedeutet, dass sie im WebView läuft und keine Synchronisierung benötigt.
iOS-Setup
Ergänze eine Kamera-Nutzungsbeschreibung in ios/App/App/Info.plist. VisionKit öffnet die Kamera, und iOS beendet die App, wenn der Schlüssel fehlt:
<key>NSCameraUsageDescription</key>
<string>We use the camera to scan your documents.</string>
Das Swift-Package des Plugins zielt auf iOS 15 oder neuer ab, was dem Minimum von Capacitor 8 entspricht. Denke daran, dass App-Store-Uploads jetzt Xcode 26 erfordern, also baue damit, bevor du einreichst. Wenn du keinen Mac hast, Capgo Build kann das signierte iOS-Binary in der Cloud erstellen.
Android-Setup
Nichts hinzuzufügen zu AndroidManifest.xml. Das ML-Kit-Scanner-Modul ist ein Google Play services-Modul, das beim ersten Gebrauch heruntergeladen wird und die Kamera selbst übernimmt, sodass deine App keine Berechtigungen anfordert CAMERA. Das Gerät benötigt Google Play services, was die meisten Emulatoren und de-Googled-Phones ausschließt. Plane einen Fallback (einfache Kameraaufnahme) ein, falls du Märkte belieferst, in denen Google Play services fehlen.
Seiten scannen
Beginne mit einer Funktion, die den Scanner öffnet und die Seitenpfade zurückgibt, oder null wenn der Nutzer abbricht:
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 ?? [];
}
Ein paar Optionen solltest du kennen:
responseType:ImageFilePath(Standard) gibt Dateipfade zurück,Base64gibt Base64-Strings zurück. Nutze Dateipfade für mehrseitige Dokumente. Base64-Strings für 20 hochauflösende Seiten können mehrere Dutzend Megabytes erreichen, und jedes Byte geht über die Capacitor Bridge.maxNumDocuments: setze es auf1für Single-Page-Flows wie Belege oder Ausweise.reviewCapturedDocument: zeigt jede erfasste Seite vor dem Fortfahren an – ideal, wenn Nutzer wichtige Dokumente scannen.brightnessundcontrast: Nachbearbeitung der Bilder. Ein Kontrast von1.2zu1.5hilft oft bei verblassten Belegen.scannerMode(Android):Fullfügt eine ML-basierte Bereinigung hinzu,Baseist schneller und lässt das Bild unverändert.
Wo die Dateien landen, unterscheidet sich je nach Plattform. Unter Android gehen die Seiten in das Cache-Verzeichnis der App. Unter iOS schreibt das Plugin sie in das Documents-Verzeichnis der App. Keiner der beiden Orte eignet sich für die dauerhafte Ablage, deshalb erstellt der folgende Ablauf das PDF, speichert es am gewünschten Ort und löscht die Seitenbilder.
Seiten zu einer PDF zusammenfügen
pdf-lib kann JPEGs direkt einbetten, ohne sie neu zu kodieren, sodass die PDF-Größe nah an der Summe der Seitengrößen bleibt. Jede Seite wird über Capacitor.convertFileSrceingelesen, was einen nativen Pfad in eine URL umwandelt, die die WebView abrufen kann:
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();
}
Wenn du Seiten bevorzugst, die exakt dem gescannten Papier entsprechen, ersetze die A4-Logik durch pdf.addPage([image.width, image.height]) und zeichne das Bild in voller Größe. Quittungen sehen so besser aus, da sie lang und schmal sind.
Alternative: PDF nativ rendern
Wenn deine App bereits PDFs aus HTML erstellt (Rechnungen, Berichte), kannst du diesen Weg mit @capgo/capacitor-pdf-generatorwiederverwenden. Es rendert eine HTML-String mit der Print-Engine der Plattform und gibt base64 zurück:
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
}
Das ist praktisch, wenn du eine Titelseite, einen Titel oder einen Fußzeile mit Datum auf jeder Seite möchtest. Für reine Bild-zu-PDF- pdf-lib gibt dir mehr Kontrolle über die Seitengeometrie und die Bildkompression.
PDF speichern
Schreibe die Base64-Ausgabe mit @capacitor/filesystem. Wähle das Verzeichnis danach, wie lange die Datei bestehen bleiben soll:
| Verzeichnis | Android | iOS | Nutze es für |
|---|---|---|---|
Directory.Cache |
App-Cache, kann vom Betriebssystem gelöscht werden | Library/Caches, kann vom Betriebssystem gelöscht werden |
PDFs, die du sofort hochlädst oder teilst |
Directory.Data |
Private App-Dateien werden beim Deinstallieren entfernt | App hinzu Documents Ordner, in iCloud gesichert |
PDFs, die Ihre App verwaltet und später erneut anzeigt |
Directory.LibraryNoCloud |
Private App-Dateien | Library/NoCloud, nicht gesichert |
Große PDFs, die Sie erneut herunterladen können |
Unter iOS, Directory.Data und Directory.Documents zeigen auf denselben Ordner. Füge UIFileSharingEnabled und LSSupportsOpeningDocumentsInPlace zu Info.plist hinzu, wenn Nutzer diese PDFs in der Dateien-App sehen sollen.
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)),
);
}
Auf nativen Plattformen, writeFile behandelt einen String ohne encoding Option als Base64 und schreibt die Binär-Bytes. Übergib nicht Encoding.UTF8 für PDFs, sonst wird die Datei beschädigt.
Wenn der Nutzer die PDF-Datei in seinem Android-Downloadordner finden soll, verwende @capgo/capacitor-file-sharers save() Methode, die auf Android 10+ ohne Speicherberechtigung in MediaStore.Downloads schreibt. Auf iOS öffnet derselbe Aufruf das Share-Sheet, damit der Nutzer „In Dateien speichern“ auswählen kann.
Den gesamten Flow zusammenbauen
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);
}
}
Verbinde es mit einem Button und zeige einen Spinner, während pagesToPdf läuft. Auf einem Mittelklasse-Android-Gerät dauern 10 Seiten normalerweise deutlich weniger als ein paar Sekunden, da die JPEG-Daten kopiert und nicht dekodiert werden.
PDF teilen, öffnen oder drucken
Über das Share Sheet des Systems teilen
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 kopiert die Datei in den eigenen Cache-Ordner und stellt sie über den eigenen FileProvider auf Android, sodass du nichts bearbeiten musst file_paths.xml, selbst wenn die PDF-Datei in Directory.Data. Wenn du das offizielle @capacitor/share Plugin stattdessen nutzt, funktioniert dessen files Diese Option funktioniert ab Werk nur für das Cache-Verzeichnis auf Android. Andere Ordner müssen hinzugefügt werden android/app/src/main/res/xml/file_paths.xml.
In der PDF-App des Nutzers öffnen
Für eine schnelle Vorschau übergibt der Community File Opener die Datei an Quick Look auf iOS oder an den Standard-PDF-Viewer auf 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',
});
@capgo/capacitor-printer öffnet UIPrintInteractionController auf iOS und PrintManager auf Android:
import { Printer } from '@capgo/capacitor-printer';
await Printer.printPdf({
name: 'Signed contract',
path: pdfUri,
});
Der name wird zum Druckauftragsnamen und zum Standarddateinamen, wenn der Nutzer im Druckdialog „Als PDF speichern“ wählt.
PDF hochladen
Um das PDF an dein Backend zu senden, lies es als Blob ein und sende es per fetch, oder nutze einen Native Uploader für große Dateien, damit der Upload weiterläuft, wenn die App in den Hintergrund geht:
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',
});
Siehe den Capacitor-Uploader-Plugin für Progress-Events und Multipart-Uploads.
Gescannte PDFs durchsuchbar machen
PDFs mit reinen Bildern können nicht durchsucht oder kopiert werden. Wenn Nutzer ihre Scans durchsuchen möchten:
- Führe OCR für jedes Seitenbild aus, bevor du es löschst. On-Device-Optionen sind ML Kit Text Recognition unter Android und das Vision-Framework unter iOS, beide über Community-Capacitor-Plugins nutzbar, oder ein serverseitiger OCR-Dienst.
- Speichere den erkannten Text zusammen mit dem PDF-Pfad in deiner Datenbank und suche dort.
- Wenn das PDF selbst Text enthalten muss, generiere es serverseitig mit einem OCR-Tool, das eine unsichtbare Textebene schreibt, z. B. OCRmyPDF.
Für die meisten Apps reicht Schritt 2 aus und vermeidet, dass ein zweites großes Modell in die App eingebunden wird.
Fehlerbehebung
„Document scanner is not supported on Android emulators“: ist zu erwarten. Verwende ein physisches Gerät mit Google Play-Diensten.
Die App stürzt auf iOS ab, sobald der Scanner geöffnet wird: NSCameraUsageDescription is missing from Info.plist, oder der Nutzer hat den Kamerazugriff zuvor verweigert. Prüfe Einstellungen, Datenschutz, Kamera.
embedJpg wirft „SOI not found“: Die Bytes sind kein JPEG. Das passiert, wenn du eine base64-Data-URL übergeben hast, wo pdf-lib rohe Bytes erwartet, oder einen Pfad abgerufen hast, der eine HTML-Fehlerseite zurücklieferte. Logge Capacitor.convertFileSrc(path) und öffne es im Safari- oder Chrome-Inspector.
Die PDF-Datei ist riesig: Reduziere croppedImageQuality (Android) oder komprimiere Seiten neu mit @capgo/capacitor-file-compressor vor dem Einbetten. Eine Qualität von 70 bis 80 ist für Textdokumente in der Regel gut lesbar.
Seiten erscheinen rotiert: Rotiere sie in pdf-lib mit page.setRotation(degrees(90)) oder lassen Sie den Nutzer die Rotation im Scanner-Review-Screen mit reviewCapturedDocument: true.
Sharing schlägt auf Android fehl mit FileUriExposedException oder „Failed to find configured root“: Sie haben einen Pfad außerhalb der in deklarierten Ordner übergeben file_paths.xml zu @capacitor/share. Nutzen Sie das File Sharer Plugin oder kopieren Sie das PDF nach Directory.Cache zuerst.
Alte Seitenbilder füllen den Speicher auf iOS: Der Scanner schreibt JPEGs in den Documents-Ordner. Löschen Sie sie nach dem Erstellen des PDFs, wie in gezeigt scanToPdf.
Updates für den Scan-Flow bereitstellen
Der Großteil dieses Features steckt in TypeScript: PDF-Layout, Dateibenennung, Upload-Logik und UI. Änderungen an diesen Teilen brauchen kein neues Store-Release. Mit Capgo Live Updates können Sie einen Fix für das PDF-Layout oder den Upload-Endpoint am selben Tag an alle Nutzer ausrollen, während native Änderungen (Hinzufügen des Plugins, Bearbeiten von Info.plist) weiterhin über einen normalen Build laufen. Der Dokumentenscanner-Dokumentation listet alle Optionen auf, falls du den Scanner weiter anpassen musst.