Capacitor アプリで書類をPDFにスキャンするには、ネイティブスキャナーを次のコードで起動し @capgo/capacitor-document-scanner、返されたJPEGページを取得して、次のコードで1つのPDFに結合します pdf-lib. そこから、次の方法でPDFを書き出します: @capacitor/filesystem 共有シート、PDFビューア、またはシステムの印刷ダイアログに渡します。このガイドでは、Capacitor 8上で動作するcodeを使った一連の流れを、iOSとAndroidの両方で解説します。
iOSとAndroidでスキャン→PDFはどう動くか
両プラットフォームにはシステム標準のドキュメントスキャナーが搭載されているので、エッジ検出や台形補正を自前で実装する必要はありません:
| iOSと | Androidでは | |
|---|---|---|
| ネイティブスキャナー | VisionKit VNDocumentCameraViewController |
Google ML Kit Document Scanner (Play services) |
| エッジ検出とクロップ | はい | はい |
| フィルター / クリーンアップ | VisionKit 内蔵 | base, base_with_filter, full モード(汚れや指の除去など) full) |
| セッションごとのページ制限 | 24 | 1〜24(デフォルトは20) |
| カメラ使用の許可プロンプト | はい、必要です NSCameraUsageDescription |
いいえ、Play services で動作します |
| エミュレータ / シミュレータで動作 | いいえ | いいえ |
| プラグインの出力結果 | JPEG ページ | JPEG ページ |
このプラグインは PDF ではなく画像を返します。これは意図的な設計で、ページサイズ、圧縮率、ファイル名を自分で制御でき、ギャラリーから選択した写真やバックエンドから受信したファイルでも同じ PDF code が機能します。
プラグインをインストール
bun add @capgo/capacitor-document-scanner @capacitor/filesystem pdf-lib
bunx cap sync
スキャナープラグインは Capacitor のメジャーバージョンに追従するため、プラグイン v8 は Capacitor 8 に対応しています。 pdf-lib 純粋な JavaScript ライブラリであり、ネイティブ code を必要としないため、WebView 内で動作し、同期も不要です。
iOSの設定
にカメラ使用文字列を追加します ios/App/App/Info.plist。VisionKit がカメラを開き、このキーが欠けていると iOS はアプリを終了します:
<key>NSCameraUsageDescription</key>
<string>We use the camera to scan your documents.</string>
このプラグインの Swift パッケージは iOS 15 以降をターゲットとしており、Capacitor 8 の最低要件と一致します。App Store へのアップロードには現在 Xcode 26 が必要なので、提出前にそれを使用してビルドしてください。Mac を所有していない場合は、 Capgo のビルド がクラウドで署名済みの iOS バイナリを生成できます。
Androidの設定
に追加するものは何もありません AndroidManifest.xml。ML Kit スキャナーは Google Play サービスのモジュールであり、初回使用時にダウンロードされ、カメラを自分で処理するため、アプリは を要求しません CAMERA。デバイスには Google Play サービスが必要であり、これによりほとんどのエミュレーターと Googled されていないスマートフォンが除外されます。Play サービスがない市場にリリースする場合は、フォールバック(通常のカメラキャプチャ)を計画してください。
ページをスキャンします
スキャナーを開き、ページパスを返す関数から始めます。または null ユーザーがキャンセルした場合:
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 ?? [];
}
知っておくと便利なオプションがいくつかあります:
responseType:ImageFilePath(デフォルト) ファイルパスを返します,Base64base64文字列を返します。複数ページの文書にはファイルパスを推奨します。高解像度20ページ分のbase64文字列は数十MBに達し、そのすべてのバイトがCapacitorブリッジを通過します。maxNumDocuments: 次の値に設定します1領収書やIDカードなどの1ページフロー向けです。reviewCapturedDocument: 続行する前に撮影した各ページを表示します。重要な書類をスキャンするときに便利です。brightnessとcontrast: 画像に適用される後処理。コントラストを1.2まで1.5に設定すると、色褪せた領収書に効果的です。scannerMode(Android):FullMLベースのクリーンアップを追加します,Baseはより高速で、画像をそのまま保持します。
ファイルの保存先はプラットフォームによって異なります。Android ではページがアプリのキャッシュディレクトリに、iOS ではプラグインがアプリの Documents ディレクトリに書き込みます。どちらも長期的な保存先としては適していないため、以下のフローでは PDF を生成し、任意の場所に保存してからページ画像を削除します。
ページを 1 つの PDF に結合する
pdf-lib は JPEG を再エンコードせずに直接埋め込めるため、PDF サイズはページサイズの合計とほぼ同じに保たれます。各ページは次を介して読み取ります Capacitor.convertFileSrc。これはネイティブパスを WebView が読み込める URL に変換します:
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();
}
スキャンした紙と正確に一致するページにしたい場合は、A4 のロジックを次に置き換えて pdf.addPage([image.width, image.height]) 画像をフルサイズで描画します。レシートは細長いため、この方がきれいに見えます。
代替案:PDFをネイティブでレンダリングする
アプリですでにHTMLからPDFを生成している場合(請求書やレポートなど)は、次の方法でそのパスを再利用できます: @capgo/capacitor-pdf-generatorこれはプラットフォームのプリントエンジンでHTML文字列をレンダリングし、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
}
表紙やタイトル、各ページに日付入りのフッターを入れたい場合に便利です。純粋な画像からPDFへの変換には、 pdf-lib の方がページ構成や画像の圧縮をより細かく制御できます。
PDFを保存
base64 の出力を次で書き込みます: @capacitor/filesystemファイルの保存期間に応じてディレクトリを選択してください:
| ディレクトリ | Androidでは | iOSと | 用途: |
|---|---|---|---|
Directory.Cache |
アプリのキャッシュ(OSによってクリアされる場合があります) | Library/Caches(OSによってクリアされる場合があります) |
すぐにアップロードまたは共有するPDF |
Directory.Data |
アプリのプライベートファイル(アンインストール時に削除されます) | アプリ Documents フォルダ(iCloud にバックアップ) |
アプリが管理し、後で再表示する PDF |
Directory.LibraryNoCloud |
アプリ内の非公開ファイル | Library/NoCloud、バックアップなし |
再ダウンロード可能な大容量 PDF |
iOS では、 Directory.Data と Directory.Documents 同じフォルダを指します。追加してください UIFileSharingEnabled と LSSupportsOpeningDocumentsInPlace まで Info.plist ユーザーがFilesアプリでこれらのPDFを表示できるようにするかどうか。
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)),
);
}
ネイティブプラットフォームでは、 writeFile オプションなしの文字列を encoding base64として扱い、バイナリバイトを書き込みます。渡さないでください。 Encoding.UTF8 そうしないと、PDFの場合はファイルが破損します。
AndroidのダウンロードフォルダでPDFを見つけられるようにするには、以下を使用します: @capgo/capacitor-file-sharerの save() Android 10 以降ではストレージ権限なしで MediaStore の Downloads に書き込むメソッドです。iOS では同じ呼び出しで共有シートが開き、ユーザーは「ファイルに保存」を選べます。
フロー全体を組み立てる
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);
}
}
ボタンにつないで、実行中はスピナーを表示します pagesToPdf 。ミドルレンジの Android 端末では、JPEG データはデコードされずコピーされるだけなので、10 ページでも通常は数秒もかかりません。
PDF を共有・開く・印刷する
システムの共有シートで共有
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 ファイルを独自のキャッシュフォルダにコピーし、独自の FileProvider Android では、編集する必要はありません file_paths.xml、PDF がどこに保存されていても Directory.Data。公式の @capacitor/share プラグインを使う場合、その files オプションは Android のキャッシュディレクトリでのみデフォルトで動作します。他のフォルダは android/app/src/main/res/xml/file_paths.xml.
ユーザーの PDF アプリで開く
クイックプレビューには、コミュニティのファイルオープナーが iOS では Quick Look に、Android ではデフォルトの PDF ビューワーにファイルを渡します:
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 を開きます UIPrintInteractionController (iOS)と PrintManager Android では:
import { Printer } from '@capgo/capacitor-printer';
await Printer.printPdf({
name: 'Signed contract',
path: pdfUri,
});
この name 印刷ジョブ名となり、印刷ダイアログで「PDFとして保存」を選択した場合のデフォルトのファイル名になります。
PDFをアップロード
PDFをバックエンドに送信するには、それを Blob として読み取り、 fetchでPOSTします。または、大きなファイルにはネイティブのアップローダーを使い、アプリがバックグラウンドに移行してもアップロードが続くようにします:
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',
});
詳細については、 Capacitor uploader プラグイン 進捗イベントとマルチパートアップロード用。
スキャンしたPDFを検索可能にする
画像のみのPDFは検索もコピーもできません。ユーザーがスキャン内容を検索することを想定している場合は:
- ページ画像を削除する前に、各画像に対してOCRを実行してください。オンデバイスの選択肢としては、Androidの ML Kit Text Recognition や iOSの Vision framework(いずれもコミュニティの Capacitor プラグイン経由で利用できます)があり、ほかにサーバーサイドのOCRサービスを使う方法もあります。
- 認識したテキストをPDFのパスと一緒にデータベースに保存し、そこで検索します。
- PDF自体にテキストを含める必要がある場合は、OCRmyPDF のように不可視のテキストレイヤーを書き込むOCRツールを使って、サーバーサイドで生成してください。
ほとんどのアプリではステップ2で十分で、アプリに2つ目の大きなモデルを同梱せずに済みます。
トラブルシューティング
「Androidエミュレーターではドキュメントスキャナーはサポートされていません」: 想定内の動作です。Google Play services が利用可能な実機を使用してください。
スキャナーを開いた瞬間にiOSでアプリがクラッシュする: NSCameraUsageDescription が見つかりません Info.plist、または以前にユーザーがカメラへのアクセスを拒否しています。設定 > プライバシー > カメラを確認してください。
embedJpg “SOI not found” というエラーが発生する:バイトデータがJPEGではありません。pdf-lib が生のバイトデータを期待しているところに base64 データURLを渡したか、HTML のエラーページを返すパスを取得した場合に発生します。ログに出力し、 Capacitor.convertFileSrc(path) Safari または Chrome のインスペクターで開いてください。
PDF のサイズが巨大すぎる:解像度を下げる croppedImageQuality (Android の場合)か、ページを再圧縮する @capgo/capacitor-file-compressor (埋め込み前に)。テキスト文書なら品質 70〜80 で通常は読めます。
ページが回転して表示される:pdf-lib 内で回転させるには page.setRotation(degrees(90)) または、スキャナーのレビュー画面でユーザーに回転を修正させる reviewCapturedDocument: true.
Android では、次のエラーで共有が失敗します FileUriExposedException または “Failed to find configured root”:で宣言されたフォルダ外のパスを渡しました file_paths.xml まで @capacitor/share。file sharer プラグインを使用するか、PDF をコピーしてください Directory.Cache してください。
古いページ画像がiOSのストレージを圧迫する:スキャナーはJPEGをDocumentsディレクトリに書き出します。PDFを生成した後は、以下に示すように削除してください。 scanToPdf.
スキャンフローへのアップデート配信
この機能の大部分はTypeScriptで実装されています:PDFレイアウト、ファイル命名、アップロードロジック、UIです。これらの部分の変更には新しいストアリリースは不要です。これにより、 Capgo Live Updates PDFレイアウトやアップロードエンドポイントの修正は、ネイティブの変更(プラグインの追加、 Info.plistの編集)が通常のビルドを経る一方で、同じ日に全ユーザーへ配信できます。 ドキュメントスキャナーのドキュメント には、スキャナーをさらに調整する必要がある場合は、すべてのオプションが記載されています。