Per scansionare un documento in PDF in un'app Capacitor, apri lo scanner nativo con @capgo/capacitor-document-scanner, prendi le pagine JPEG che restituisce e combinali in un unico PDF con pdf-lib. Da lì scrivi il PDF con @capacitor/filesystem e passalo alla share sheet, a un visualizzatore PDF o alla finestra di stampa di sistema. Questa guida illustra il flusso completo con code che gira su Capacitor 8, sia su iOS che su Android.
Come funziona la scansione in PDF su iOS e Android
Entrambe le piattaforme includono uno scanner di documenti di sistema, quindi non devi implementare tu stesso il rilevamento dei bordi o la correzione prospettica:
| iOS | Android | |
|---|---|---|
| Scanner nativo | VisionKit VNDocumentCameraViewController |
Google ML Kit Document Scanner (Play services) |
| Rilevamento dei bordi e ritaglio | Sì | Sì |
| Filtri / pulizia | VisionKit integrato | base, base_with_filter, full modalità (rimozione macchie e dita in full) |
| Limite pagine per sessione | 24 | Da 1 a 24, default 20 |
| Prompt permesso fotocamera | Sì, richiede NSCameraUsageDescription |
No, lo scanner gira in Play services |
| Funziona su emulatore / simulatore | No | No |
| Output del plugin | Pagine JPEG | Pagine JPEG |
Il plugin restituisce immagini, non un PDF. È una scelta deliberata: tu controlli le dimensioni della pagina, la compressione e il nome del file, e lo stesso PDF code funziona per le foto selezionate dalla galleria o per i file ricevuti dal tuo backend.
Installa i plugin
bun add @capgo/capacitor-document-scanner @capacitor/filesystem pdf-lib
bunx cap sync
Il plugin scanner segue le versioni principali di Capacitor, quindi la v8 del plugin è destinata a Capacitor 8. pdf-lib è una libreria JavaScript pura senza code nativo, il che significa che gira nel WebView e non richiede sincronizzazione.
Setup iOS
Aggiungi una stringa di utilizzo della fotocamera a ios/App/App/Info.plist. VisionKit apre la fotocamera e iOS termina l'app se la chiave è assente:
<key>NSCameraUsageDescription</key>
<string>We use the camera to scan your documents.</string>
Il pacchetto Swift del plugin è destinato a iOS 15 o versioni successive, in linea con il minimo di Capacitor 8. Tieni presente che i caricamenti su App Store richiedono ora Xcode 26, quindi compila con esso prima di inviare. Se non possiedi un Mac, Build Capgo può generare il binario iOS firmato nel cloud.
Configurazione Android
Niente da aggiungere a AndroidManifest.xml. Lo scanner ML Kit è un modulo di Google Play services che viene scaricato al primo utilizzo e gestisce la fotocamera da solo, quindi la tua app non richiede CAMERA. Il dispositivo necessita di Google Play services, il che esclude la maggior parte degli emulatori e dei telefoni senza Google. Prevedi un fallback (una semplice acquisizione fotocamera) se pubblichi in mercati dove Play services non è presente.
Scansiona le pagine
Inizia con una funzione che apre lo scanner e restituisce i percorsi delle pagine, oppure null quando l'utente annulla:
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 ?? [];
}
Vale la pena conoscere alcune opzioni:
responseType:ImageFilePath(default) restituisce i percorsi dei file,Base64restituisce stringhe base64. Per documenti multipagina, preferisci i percorsi dei file. Le stringhe base64 per 20 pagine ad alta risoluzione possono raggiungere decine di megabyte e ogni byte attraversa il bridge Capacitor.maxNumDocuments: impostalo su1per flussi a pagina singola come ricevute o carte d'identità.reviewCapturedDocument: mostra ogni pagina catturata prima di procedere, utile quando gli utenti scansionano documenti importanti.brightnessecontrast: post-processing applicato alle immagini. Un contrasto di1.2a1.5spesso aiuta con gli scontrini sbiaditi.scannerMode(Android):Fullaggiunge una pulizia basata su ML,Baseè più veloce e mantiene l'immagine intatta.
La destinazione dei file varia per piattaforma. Su Android, le pagine vanno nella directory cache dell'app. Su iOS, il plugin le scrive nella directory Documents dell'app. Nessuna delle due è adatta per l'archiviazione a lungo termine, quindi il flusso seguente crea il PDF, lo salva dove desideri e cancella le immagini delle pagine.
Unisci le pagine in un unico PDF
pdf-lib può incorporare i JPEG direttamente senza ricodificarli, così la dimensione del PDF rimane vicina alla somma delle dimensioni delle pagine. Leggi ogni pagina tramite Capacitor.convertFileSrc, che trasforma un percorso nativo in un URL che la WebView può recuperare:
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();
}
Se preferisci pagine che corrispondano esattamente al documento scansionato, sostituisci la logica A4 con pdf.addPage([image.width, image.height]) e disegna l'immagine a dimensione intera. I scontrini risultano migliori in questo modo, perché sono lunghi e stretti.
Alternativa: renderizza il PDF nativamente
Se la tua app crea già PDF da HTML (fatture, report), puoi riutilizzare quel percorso con @capgo/capacitor-pdf-generator. Renderizza una stringa HTML con il motore di stampa della piattaforma e restituisce 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
}
Comodo quando vuoi una copertina, un titolo o un piè di pagina con la data su ogni pagina. Per la semplice conversione da immagine a PDF, pdf-lib ti dà più controllo sulla geometria della pagina e sulla compressione delle immagini.
Salva il PDF
Scrivi l'output base64 con @capacitor/filesystem. Scegli la directory in base a quanto tempo il file deve restare:
| Directory | Android | iOS | Usalo per |
|---|---|---|---|
Directory.Cache |
Cache dell'app, cancellabile dal sistema operativo | Library/Caches, cancellabile dal sistema operativo |
PDF da caricare o condividere subito |
Directory.Data |
File privati dell'app, rimossi alla disinstallazione | App Documents cartella, sincronizzata con iCloud |
PDF gestiti dall'app e visualizzati in seguito |
Directory.LibraryNoCloud |
File privati dell'app | Library/NoCloud, non sincronizzati |
PDF grandi che puoi scaricare di nuovo |
Su iOS, Directory.Data e Directory.Documents puntano alla stessa cartella. Aggiungi UIFileSharingEnabled e LSSupportsOpeningDocumentsInPlace a Info.plist se gli utenti devono vedere quei PDF nell'app File.
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)),
);
}
Sulle piattaforme native, writeFile tratta una stringa senza encoding opzione come base64 e scrive i byte binari. Non passare Encoding.UTF8 per i PDF, altrimenti il file sarà corrotto.
Se l'utente deve trovare il PDF nella cartella Download su Android, usa @capgo/capacitor-file-sharer, il cui save() metodo, che scrive in MediaStore Downloads senza alcun permesso di archiviazione su Android 10+. Su iOS, la stessa chiamata apre lo share sheet così l'utente può scegliere “Salva su File”.
Metti insieme l'intero flusso
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);
}
}
Collegalo a un pulsante e mostra uno spinner mentre pagesToPdf su un telefono Android di fascia media, 10 pagine richiedono in genere ben meno di qualche secondo, dato che i dati JPEG vengono copiati, non decodificati.
Condividi, apri o stampa il PDF
Condividi tramite lo share sheet di sistema
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 copia il file nella propria cartella cache e lo fornisce tramite il proprio FileProvider su Android, quindi non serve modificare file_paths.xml, anche se il PDF si trova in Directory.Data. Se invece utilizzi il @capacitor/share plugin ufficiale, la sua files opzione funziona out of the box solo per la cache directory su Android. Le altre cartelle vanno aggiunte a android/app/src/main/res/xml/file_paths.xml.
Apri con l'app PDF dell'utente
Per un'anteprima rapida, il file opener della community passa il file a Quick Look su iOS o al visualizzatore PDF predefinito su 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',
});
Stampa
@capgo/capacitor-printer apre UIPrintInteractionController su iOS e PrintManager su Android:
import { Printer } from '@capgo/capacitor-printer';
await Printer.printPdf({
name: 'Signed contract',
path: pdfUri,
});
Il name diventa il nome del lavoro di stampa e il nome file predefinito se l'utente seleziona "Salva come PDF" nella finestra di stampa.
Upload del PDF
Per inviare il PDF al tuo backend, leggilo come Blob e invialo tramite fetch, oppure usa un uploader nativo per file grandi, così l'upload continua anche quando l'app passa in background:
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',
});
Guarda il plugin uploader Capacitor per gli eventi di avanzamento e gli upload multipart.
Rendi ricercabili i PDF scansionati
I PDF solo immagine non sono ricercabili né copiabili. Se gli utenti si aspettano di poter cercare nei propri scan:
- Esegui l'OCR su ogni immagine di pagina prima di eliminarla. Le opzioni on-device includono ML Kit Text Recognition su Android e il framework Vision su iOS, entrambi accessibili tramite plugin Capacitor della community, oppure un servizio OCR lato server.
- Salva il testo riconosciuto nel database accanto al percorso del PDF e cerca lì.
- Se il PDF deve contenere testo, generalo lato server con uno strumento OCR che scrive un layer di testo invisibile, come OCRmyPDF.
For most apps, step 2 is enough and avoids shipping a second large model in the app.
Risoluzione problemi
“Document scanner non è supportato sugli emulatori Android”: previsto. Usa un dispositivo fisico con Google Play services.
L'app si blocca su iOS nel momento in cui si apre lo scanner: NSCameraUsageDescription is missing from Info.plist, oppure l'utente ha negato l'accesso alla fotocamera in precedenza. Controlla Impostazioni, Privacy, Fotocamera.
embedJpg lancia “SOI not found”: i byte non sono un JPEG. Succede se hai passato una data URL base64 dove pdf-lib si aspettava byte grezzi, o se hai recuperato un percorso che ha restituito una pagina di errore HTML. Logga Capacitor.convertFileSrc(path) e aprilo nell'ispettore di Safari o Chrome.
Il PDF è enorme: inferiore croppedImageQuality (Android) o ricomprimi le pagine con @capgo/capacitor-file-compressor prima dell'incorporamento. Una qualità tra 70 e 80 è di solito leggibile per documenti di testo.
Le pagine appaiono ruotate: ruota dentro pdf-lib con page.setRotation(degrees(90)) o lascia che l'utente corregga la rotazione nella schermata di revisione dello scanner con reviewCapturedDocument: true.
La condivisione fallisce su Android con FileUriExposedException o “Failed to find configured root”: hai passato un percorso esterno alle cartelle dichiarate in file_paths.xml a @capacitor/share. Usa il plugin file sharer o copia il PDF in Directory.Cache prima.
Le immagini delle pagine vecchie occupano spazio su iOS: lo scanner scrive JPEG nella directory Documents. Eliminali dopo aver creato il PDF, come mostrato in scanToPdf.
Rilasciamo aggiornamenti al flusso di scansione
La maggior parte di questa funzionalità è in TypeScript: layout PDF, naming file, logica di upload e UI. Le modifiche a queste parti non richiedono un nuovo rilascio sullo store. Con Live updates di Capgo puoi pubblicare una correzione al layout PDF o all'endpoint di upload a tutti gli utenti lo stesso giorno, mentre le modifiche native (aggiunta del plugin, modifica di Info.plist) passano ancora attraverso una build normale. La documentazione dello scanner di documenti elenca tutte le opzioni se devi regolare ulteriormente lo scanner.