Untuk scan dokumen ke PDF di aplikasi Capacitor, buka scanner native dengan @capgo/capacitor-document-scanner, ambil halaman JPEG yang dihasilkan, lalu gabungkan menjadi satu PDF dengan pdf-lib. Dari sana, Anda menulis PDF dengan @capacitor/filesystem dan meneruskannya ke share sheet, PDF viewer, atau dialog cetak sistem. Panduan ini membahas alur lengkap dengan code yang berjalan di Capacitor 8, baik di iOS maupun Android.
Cara kerja scan-to-PDF di iOS dan Android
Kedua platform sudah menyertakan pemindai dokumen sistem, jadi Anda tidak perlu membuat edge detection atau koreksi perspektif sendiri:
| iOS | Android | |
|---|---|---|
| Scanner native | VisionKit VNDocumentCameraViewController |
Google ML Kit Document Scanner (Play services) |
| Edge detection dan crop | Ya | Ya |
| Filter / pembersihan | Bawaan VisionKit | base, base_with_filter, full mode (penghilangan noda dan sidik jari di full) |
| Batas halaman per sesi | 24 | 1 hingga 24, default 20 |
| Prompt izin kamera | Ya, diperlukan NSCameraUsageDescription |
Tidak, scanner berjalan di Play services |
| Berfungsi di emulator / simulator | Tidak | Tidak |
| Output dari plugin | Halaman JPEG | Halaman JPEG |
Plugin ini mengembalikan gambar, bukan PDF. Ini pemisahan yang disengaja: Anda mengatur ukuran halaman, kompresi, dan nama file, dan PDF code yang sama berfungsi untuk foto dari galeri maupun file dari backend Anda.
Install plugin
bun add @capgo/capacitor-document-scanner @capacitor/filesystem pdf-lib
bunx cap sync
Plugin scanner mengikuti versi mayor Capacitor, jadi plugin v8 menargetkan Capacitor 8. pdf-lib adalah library JavaScript murni tanpa code native, yang berarti ia berjalan di WebView dan tidak butuh sinkronisasi.
Setup iOS
Tambahkan string penggunaan kamera ke ios/App/App/Info.plist. VisionKit membuka kamera, dan iOS akan menghentikan aplikasi jika key tersebut tidak ada:
<key>NSCameraUsageDescription</key>
<string>We use the camera to scan your documents.</string>
Swift package plugin ini menargetkan iOS 15 atau lebih baru, sesuai dengan minimum Capacitor 8. Ingat bahwa upload ke App Store sekarang memerlukan Xcode 26, jadi lakukan build dengan versi itu sebelum submit. Jika Anda tidak punya Mac, Build Capgo dapat menghasilkan binary iOS yang sudah ditandatangani di cloud.
Setup Android
Tidak perlu menambahkan ke AndroidManifest.xml. Scanner ML Kit adalah modul Google Play services yang diunduh saat pertama kali digunakan dan menangani kamera secara mandiri, sehingga aplikasi Anda tidak meminta CAMERA. Perangkat memerlukan Google Play services, yang menyingkirkan sebagian besar emulator dan ponsel tanpa Google. Siapkan fallback (pengambilan gambar kamera biasa) jika Anda merilis ke pasar di mana Play services tidak tersedia.
Scan halaman
Mulai dengan fungsi yang membuka scanner dan mengembalikan path halaman, atau null ketika pengguna membatalkan:
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 ?? [];
}
Beberapa opsi yang perlu diketahui:
responseType:ImageFilePath(default) mengembalikan path file,Base64mengembalikan string base64. Lebih baik gunakan path file untuk dokumen multi-halaman. String base64 untuk 20 halaman resolusi tinggi bisa mencapai puluhan megabita dan setiap byte melewati bridge Capacitor.maxNumDocuments: atur menjadi1untuk alur satu halaman seperti struk atau kartu identitas.reviewCapturedDocument: menampilkan setiap halaman yang diambil sebelum melanjutkan, berguna saat pengguna memindai dokumen penting.brightnessdancontrast: pascapemrosesan yang diterapkan pada gambar. Kontras1.2ke1.5sering membantu untuk struk yang pudar.scannerMode(Android):Fullmenambahkan pembersihan berbasis ML,Baselebih cepat dan menjaga gambar tetap utuh.
Lokasi penyimpanan file berbeda di setiap platform. Di Android, halaman disimpan di direktori cache aplikasi. Di iOS, plugin menulisnya ke direktori Documents aplikasi. Kedua lokasi ini tidak cocok untuk penyimpanan jangka panjang, jadi alur di bawah ini membuat PDF, menyimpannya di lokasi yang Anda inginkan, lalu menghapus gambar halaman.
Gabungkan halaman menjadi satu PDF
pdf-lib dapat menyematkan JPEG secara langsung tanpa perlu meng-encode ulang, sehingga ukuran PDF tetap mendekati jumlah ukuran halaman. Baca setiap halaman melalui Capacitor.convertFileSrc, yang mengubah path native menjadi URL yang dapat diambil oleh WebView:
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();
}
Jika Anda lebih suka halaman yang persis seperti kertas hasil scan, ganti logika A4 dengan pdf.addPage([image.width, image.height]) dan render gambar pada ukuran penuh. Struk terlihat lebih baik dengan cara ini karena bentuknya panjang dan sempit.
Alternatif: render PDF secara native
Jika aplikasi Anda sudah membuat PDF dari HTML (tagihan, laporan), Anda dapat menggunakan jalur yang sama dengan @capgo/capacitor-pdf-generator. Ini merender string HTML dengan mesin cetak platform dan mengembalikan 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
}
Ini berguna jika Anda ingin halaman sampul, judul, atau footer dengan tanggal di setiap halaman. Untuk konversi gambar ke PDF murni, pdf-lib memberikan kontrol lebih besar atas geometri halaman dan kompresi gambar.
Simpan PDF
Tulis output base64 dengan @capacitor/filesystem. Pilih direktori berdasarkan berapa lama file harus tersimpan:
| Direktori | Android | iOS | Gunakan untuk |
|---|---|---|---|
Directory.Cache |
Cache aplikasi, bisa dihapus oleh OS | Library/Caches, bisa dihapus oleh OS |
PDF yang di-upload atau dibagikan langsung |
Directory.Data |
File aplikasi privat, dihapus saat uninstall | Aplikasi Documents folder, di-backup ke iCloud |
PDF yang dikelola dan ditampilkan kembali oleh aplikasi Anda |
Directory.LibraryNoCloud |
File aplikasi privat | Library/NoCloud, tidak di-backup |
PDF besar yang bisa diunduh lagi |
Di iOS, Directory.Data dan Directory.Documents mengarah ke folder yang sama. Tambahkan UIFileSharingEnabled dan LSSupportsOpeningDocumentsInPlace ke Info.plist jika pengguna harus melihat PDF tersebut di app Files.
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)),
);
}
Di platform native, writeFile menganggap string tanpa encoding opsi sebagai base64 dan menulis byte biner. Jangan teruskan Encoding.UTF8 untuk PDF, atau file akan rusak.
Jika pengguna harus bisa menemukan PDF di folder Downloads di Android, gunakan @capgo/capacitor-file-sharermilik save() method, yang menulis ke MediaStore Downloads tanpa izin penyimpanan apa pun di Android 10+. Di iOS, panggilan yang sama membuka share sheet sehingga pengguna dapat memilih “Save to Files”.
Gabungkan seluruh alur
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);
}
}
Hubungkan ke tombol dan tampilkan spinner selama pagesToPdf berjalan. Di ponsel Android kelas menengah, 10 halaman biasanya selesai dalam hitungan detik karena data JPEG disalin, bukan didekode.
Bagikan, buka, atau cetak PDF
Bagikan lewat share sheet sistem
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 menyalin file ke folder cache-nya sendiri dan menyajikannya melalui FileProvider di Android, jadi Anda tidak perlu mengedit file_paths.xml, bahkan jika PDF-nya berada di Directory.Data. Jika Anda menggunakan @capacitor/share plugin sebagai gantinya, opsi files hanya berfungsi langsung untuk direktori cache di Android. Folder lain harus ditambahkan ke android/app/src/main/res/xml/file_paths.xml.
Buka di aplikasi PDF pengguna
Untuk pratinjau cepat, file opener komunitas menyerahkan file ke Quick Look di iOS atau ke viewer PDF bawaan di 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',
});
Cetak
@capgo/capacitor-printer membuka UIPrintInteractionController pada iOS dan PrintManager di Android:
import { Printer } from '@capgo/capacitor-printer';
await Printer.printPdf({
name: 'Signed contract',
path: pdfUri,
});
Panduan name menjadi nama print job dan nama file default jika pengguna memilih “Save as PDF” di dialog print.
Upload PDF
Untuk mengirim PDF ke backend Anda, baca kembali sebagai Blob dan post dengan fetch, atau gunakan uploader native untuk file besar agar proses upload tetap berjalan saat aplikasi berpindah ke latar belakang:
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',
});
Lihat plugin uploader Capacitor untuk progress events dan multipart uploads.
Buat PDF hasil scan bisa dicari
PDF yang hanya berisi gambar tidak bisa dicari atau disalin. Jika pengguna mengharapkan scan mereka bisa dicari:
- Jalankan OCR pada setiap gambar halaman sebelum menghapusnya. Opsi on-device termasuk ML Kit Text Recognition di Android dan Vision framework di iOS, keduanya dapat diakses melalui plugin komunitas Capacitor, atau layanan OCR server-side.
- Simpan teks yang dikenali di database Anda di samping path PDF dan cari di sana.
- Jika PDF-nya sendiri harus berisi teks, buat di sisi server dengan alat OCR yang menulis lapisan teks tak terlihat, seperti OCRmyPDF.
Untuk sebagian besar aplikasi, langkah 2 sudah cukup dan menghindari pengiriman model besar kedua di dalam aplikasi.
Pemecahan Masalah
“Document scanner tidak didukung pada emulator Android”: seperti yang diharapkan. Gunakan perangkat fisik dengan Google Play services.
Aplikasi crash di iOS saat scanner dibuka: NSCameraUsageDescription tidak ada di Info.plist, atau pengguna menolak akses kamera sebelumnya. Cek Settings, Privacy, Camera.
embedJpg mengeluarkan error “SOI not found”: byte-nya bukan JPEG. Ini terjadi kalau Anda memberikan base64 data URL padahal pdf-lib mengharapkan raw bytes, atau mengambil path yang mengembalikan halaman error HTML. Log Capacitor.convertFileSrc(path) lalu buka di inspector Safari atau Chrome.
Ukuran PDF terlalu besar: turunkan croppedImageQuality (Android) atau kompres ulang halaman dengan @capgo/capacitor-file-compressor sebelum di-embed. Kualitas 70 sampai 80 biasanya masih terbaca untuk dokumen teks.
Halaman tampak terotasi: putar di dalam pdf-lib dengan page.setRotation(degrees(90)) atau biarkan user memperbaiki rotasi di layar review scanner dengan reviewCapturedDocument: true.
Sharing gagal di Android dengan FileUriExposedException atau “Failed to find configured root”: Anda memberikan path di luar folder yang dideklarasikan di file_paths.xml ke @capacitor/share. Gunakan plugin file sharer atau salin PDF ke Directory.Cache pertama.
Gambar halaman lama memenuhi storage di iOS: scanner menulis JPEG ke direktori Documents. Hapus file tersebut setelah membuat PDF, seperti yang ditunjukkan di scanToPdf.
Merilis update untuk alur scan
Sebagian besar fitur ini ada di TypeScript: layout PDF, penamaan file, logika upload, dan UI. Perubahan pada bagian tersebut tidak perlu rilis store baru. Dengan Live updates Capgo Anda bisa merilis perbaikan layout PDF atau endpoint upload ke semua user di hari yang sama, sementara perubahan native (menambah plugin, mengedit Info.plist) tetap melalui proses build normal. dokumentasi document scanner mencantumkan semua opsi jika Anda perlu tuning scanner lebih lanjut.