メイン コンテンツにジャンプする

How to Scan a Document to PDF in a Capacitor App

CapacitorアプリでドキュメントをPDFにスキャンする方法

CapacitorアプリでドキュメントをPDFにスキャンする方法

CapacitorアプリでドキュメントをPDFにスキャンするには、ネイティブのスキャナを開きます @capgo/capacitor-document-scanner、JPEGのページを返す pdf-libを組み合わせて1つのPDFを作成します。そこからPDFを書きます @capacitor/filesystem と手元のドキュメントをシェアシート、PDFビューア、またはシステムの印刷ダイアログに渡します。このガイドでは、Capacitorアプリで実行されるcodeのフローを完全に歩きます。Capacitor 8で実行され、iOSとAndroid両方で動作します。

iOSとAndroidでPDFにスキャンする方法

両方のプラットフォームでは、システムのドキュメントスキャナーが搭載されているため、エッジ検出や視点補正を自分で構築する必要はありません:

iOS Android
ネイティブスキャナー VisionKit VNDocumentCameraViewController Google ML Kit Document Scanner (Play services)
エッジ検出とトリミング はい はい
フィルタリング/クリーンアップ VisionKitの組み込み base, base_with_filter, full モード (汚れと指の除去) full)
1 ページあたりのセッション制限 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 は、ネイティブ code を持たない純粋なJavaScriptライブラリです。つまり、WebViewで実行され、同期が必要ありません。

iOSの設定

. VisionKitがカメラを開き、iOSはキーの欠如によりアプリを終了します: ios/App/App/Info.plistVisionKitはカメラを開き、iOSはキーが欠落している場合にアプリを終了します。

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

Capacitor ビルド Capgo ビルド クラウドで署名されたiOSバイナリを生成できます。

Androidの設定

何も追加する必要はありません。 AndroidManifest.xml. ML Kit スキャナーは、Google Play サービス モジュールで、初回使用時にダウンロードされ、カメラを管理するため、アプリはカメラを要求しません。 CAMERA. デバイスには Google Play サービスが必要であり、これはほとんどのエミュレータと de-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 (デフォルト) ファイルパスを返す Base64 returns base64 strings. Prefer file paths for multi-page documents. Base64 strings for 20 high resolution pages can reach tens of megabytes and every byte crosses the Capacitor bridge.
  • maxNumDocuments: を設定します 1 base64文字列を返す
  • reviewCapturedDocumentシングルページのフロー (レシートや ID カード) の場合に使用します。
  • brightness と contrast: 画像処理が適用された。 1.2 を 1.5 よく、消し消されていたレシートの問題を解決するのに役立ちます。
  • scannerMode (Android): Full 機械学習ベースのクリーンアップを追加します。 Base 速度が速くなり、画像が変更されません。

ファイルの保存場所はプラットフォームによって異なります。Androidの場合、ページはアプリキャッシュディレクトリに保存されます。iOSの場合、プラグインはアプリのドキュメントディレクトリに保存されます。どちらの場所も長期的な保存場所ではありません。したがって、以下のフローでは、PDFを作成し、保存先を指定し、ページ画像を削除します。

ページを1つのPDFに組み合わせる

pdf-lib JPEGを直接埋め込むことができるため、PDFサイズはページサイズの合計に近づきます。各ページを読み取ります。 Capacitor.convertFileSrc,

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();
}

URLでアクセスできるように、ネイティブパスをURLに変換します。 pdf.addPage([image.width, image.height]) と、画像をフルサイズで描画します。領収書はこのように見る方が良いでしょう。なぜなら、長く細長い形だからです。

Alternative: 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 Capacitorアプリで使用する
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 ユーザーがファイルアプリで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 文字列にオプションがなければbase64と扱い、バイナリバイトを書き込む。PDFの場合は、ファイルが破損するため encoding ユーザーがAndroidのダウンロードフォルダでPDFを探すことができるようにする場合は、 Encoding.UTF8 PDFの場合、ファイルが破損する可能性があります。

context @capgo/capacitor-file-sharercontext save() context

フローを組み合わせてください

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の場合、PDFがキャッシュフォルダに存在する場合、キャッシュフォルダのパスを指定するだけで済みます file_paths.xmlキャッシュフォルダのパスを指定するだけで済みます Directory.DataPDFがキャッシュフォルダに存在する場合、キャッシュフォルダのパスを指定するだけで済みます @capacitor/share PDFがキャッシュフォルダに存在する場合、キャッシュフォルダのパスを指定するだけで済みます files PDFがキャッシュフォルダに存在する場合、キャッシュフォルダのパスを指定するだけで済みます 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 iOSとAndroidで UIPrintInteractionController iOSと PrintManager Androidで

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

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

PDFをアップロード name バックエンドにPDFを送信するには、PDFを読み戻して

とともに送信します、または大きなファイルのアップロードにnativeアップローダーを使用して、アプリがバックグラウンドに移動してもアップロードが続行されるようにします。

詳細は Blob __CAPGO_KEEP_0__ fetch__CAPGO_KEEP_1__

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',
});

See the Capacitor アップローダープラグイン 進捗イベントとマルチパートアップロードのために

スキャンしたPDFを検索可能にする

イメージのみのPDFは検索やコピーができません。ユーザーがスキャンを検索できることを期待している場合:

  1. 各ページイメージを削除する前に、機器上のオプションとしてAndroidのML Kit Text RecognitionやiOSのVision Frameworkを通じてコミュニティから利用できるCapacitor プラグインを使用して、各ページイメージにOCRを実行するか、サーバーサイドのOCRサービスを使用する。
  2. 認識されたテキストをPDFのパスとともにデータベースに保存する。
  3. PDF自体にテキストを含める必要がある場合は、サーバーサイドでOCRツールを使用して、不視覚的なテキストレイヤーを生成する。例えば、OCRmyPDFを使用する。

ほとんどのアプリでは、ステップ2が十分であり、アプリに2番目の大きなモデルを配信するのを避けることができます。

トラブルシューティング

「Android エミュレータではドキュメント スキャナーがサポートされていません」「Android エミュレータでドキュメント スキャナーがサポートされていません」

期待どおりです。Google Play サービスを搭載した物理デバイスを使用してください。: NSCameraUsageDescription は欠けている Info.plist, またはユーザーがカメラアクセスを拒否した場合に

embedJpg 設定プライバシー Capacitor.convertFileSrc(path) カメラ

“SOI not見つかりました”: lower croppedImageQuality を表示し、SafariまたはChromeのインスペクターで開きます。 @capgo/capacitor-file-compressor PDFはとても大きい

: 低く(Android) またはページを再圧縮して page.setRotation(degrees(90)) にします。品質70から80は、一般的にテキストドキュメントで読みやすいです。 reviewCapturedDocument: true.

Androidで共有が失敗する FileUriExposedException または「設定されたルートを検出できません」: パスが宣言されたフォルダの外側に渡されました。 file_paths.xml を @capacitor/share。ファイルシェアプラグインを使用するか、PDFを Directory.Cache にコピーしてください。

iOSの古いページ画像がストレージを占める: スキャナはJPEGをDocumentsディレクトリに書き込む。PDFを構築した後、削除するようにします。 scanToPdf.

を参照してください。

スキャンフローの更新 Capgo ライブアップデート __CAPGO_KEEP_0__のライブアップデートを使用すると、PDFレイアウトまたはアップロードエンドポイントの修正を、同じ日にすべてのユーザーに配信できます。ネイティブの変更 (プラグインの追加、エディット Info.plist)通常のビルドを通して行きます。 ドキュメント スキャナーのドキュメント 必要に応じてスキャナーをさらに調整するには、すべてのオプションを表示します。

Capacitor アプリ向けの即時更新

ウェブ層のバグが生じた場合、Capgo を通じて修正を配信し、 days待たずにアプリストアの承認を待つ必要がなくなる。ユーザーはバックグラウンドで更新を受け取り、ネイティブの変更は通常のレビュー経路を通じて進む。

最新のブログ

Capgo gives you the best insights you need to create a truly professional mobile app.