Capacitor 앱에서 문서를 PDF로 스캔하려면 네이티브 스캐너를 @capgo/capacitor-document-scanner로 열고, 반환된 JPEG 페이지를 받아 하나의 PDF로 합치려면 pdf-lib을 사용하세요. 그다음 PDF를 작성하려면 @capacitor/filesystem 을 사용해 공유 시트, PDF 뷰어 또는 시스템 인쇄 대화상자로 넘기면 됩니다. 이 가이드는 iOS와 Android 모두에서 Capacitor 8에서 동작하는 code을 사용한 전체 플로우를 단계별로 안내합니다.
iOS 및 Android의 스캔→PDF 변환 방식
두 플랫폼 모두 시스템 문서 스캐너를 제공하므로, 엣지 감지나 원근 보정을 직접 구현할 필요가 없습니다:
| iOS | Android | |
|---|---|---|
| 네이티브 스캐너 | VisionKit VNDocumentCameraViewController |
Google ML Kit 문서 스캐너 (Play services) |
| 엣지 감지 및 크롭 | 지원 | 지원 |
| 필터 / 정리 | VisionKit 내장 | base, base_with_filter, full 모드 (얼룩 및 손가락 제거) full) |
| 세션당 페이지 수 제한 | 24 | 1~24, 기본값 20 |
| 카메라 권한 프롬프트 | 예, 필요합니다 NSCameraUsageDescription |
아니요, 스캐너는 Play 서비스에서 실행됩니다 |
| 에뮬레이터/시뮬레이터에서 작동 | 아니요 | 아니요 |
| 플러그인 출력 | 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 services 모듈이고 카메라를 직접 처리하므로, 앱에서 요청하지 않습니다 CAMERA. 기기에는 Google Play services가 필요하므로 대부분의 에뮬레이터와 de-Googled 폰에서는 사용할 수 없습니다. Play services가 없는 시장에 출시한다면 일반 카메라 캡처 같은 폴백을 준비하세요.
페이지 스캔하기
스캐너를 열고 페이지 경로를 반환하는 함수부터 시작하거나, 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 문자열은 수십 메가바이트에 달할 수 있고, 모든 바이트가 Capacitor 브리지를 거칩니다.maxNumDocuments: 다음으로 설정하세요:1영수증이나 신분증 같은 단일 페이지 플로우에 적합합니다.reviewCapturedDocument: 각 캡처된 페이지를 계속하기 전에 표시하며, 사용자가 중요한 서류를 스캔할 때 유용합니다.brightness및contrast: 이미지에 적용되는 후처리입니다. 대비가1.2대상1.5흐려진 영수증을 처리하는 데 도움이 됩니다.scannerMode(Android):FullML 기반 정리를 추가합니다.Base더 빠르고 이미지를 그대로 유지합니다.
파일이 저장되는 위치는 플랫폼마다 다릅니다. Android에서는 페이지가 앱 캐시 디렉토리에, iOS에서는 플러그인이 앱의 Documents 디렉토리에 기록합니다. 두 위치 모두 장기 저장에는 적합하지 않으므로, 아래 흐름은 PDF를 생성한 후 원하는 위치에 저장하고 페이지 이미지를 삭제합니다.
페이지를 하나의 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)),
);
}
Native 플랫폼에서는, writeFile 옵션이 없는 문자열을 encoding base64로 처리하고 바이너리 바이트를 기록합니다. PDF의 경우 전달하지 마세요 Encoding.UTF8 그렇지 않으면 파일이 손상됩니다.
Android 사용자가 Downloads 폴더에서 PDF를 찾을 수 있게 하려면, 사용하세요 @capgo/capacitor-file-sharer의 save() Android 10+에서 저장 권한 없이 MediaStore Downloads에 기록하는 메서드입니다. iOS에서는 동일한 호출이 공유 시트를 열어 사용자가 'Files에 저장'을 선택할 수 있습니다.
전체 플로우를 하나로 묶기
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 기기에서 10페이지는 JPEG 데이터가 복제되므로 몇 초 이내로 처리됩니다.
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 안드로이드에서는 수정할 필요가 없으므로 file_paths.xml, PDF가 어디에 있든 Directory.Data. 공식 @capacitor/share 플러그인을 대신 사용하면, 해당 files 옵션은 안드로이드에서 캐시 디렉터리에서만 바로 작동합니다. 다른 폴더는 다음에 추가해야 합니다. android/app/src/main/res/xml/file_paths.xml.
사용자의 PDF 앱으로 열기
빠른 미리보기를 위해, community file opener는 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 그리고 그것으로 POST 하거나 fetch또는 대용량 파일의 경우 앱이 백그라운드로 전환되어도 업로드가 계속되도록 native uploader를 사용하세요:
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 업로더 플러그인 진행 이벤트와 멀티파트 업로드용.
스캔한 PDF를 검색 가능하게 만들기
이미지만 있는 PDF는 검색도, 텍스트 복사도 할 수 없습니다. 사용자가 스캔본을 검색하길 기대한다면:
- 각 페이지 이미지를 삭제하기 전에 OCR을 실행하세요. 온디바이스 옵션으로는 Android의 ML Kit Text Recognition과 iOS의 Vision 프레임워크가 있고, 둘 다 커뮤니티 Capacitor 플러그인으로 쓸 수 있습니다. 아니면 서버 측 OCR 서비스를 사용해도 됩니다.
- 인식된 텍스트를 PDF 경로와 함께 데이터베이스에 저장하고, 거기서 검색하세요.
- PDF 자체에 텍스트가 포함되어야 한다면, OCRmyPDF와 같은 도구를 사용해 서버 측에서 보이지 않는 텍스트 레이어를 생성하세요.
대부분의 앱에서는 2단계만으로도 충분하며, 앱에 두 번째 대형 모델을 포함할 필요가 없습니다.
문제 해결
“Android 에뮬레이터에서는 문서 스캐너가 지원되지 않습니다”: 예상되는 현상입니다. Google Play 서비스를 지원하는 실제 기기를 사용하세요.
스캐너가 열리는 즉시 iOS에서 앱이 크래시됩니다: NSCameraUsageDescription 에서 누락되었습니다 Info.plist, 또는 사용자가 이전에 카메라 접근을 거부한 경우입니다. 설정 > 개인정보 보호 > 카메라를 확인하세요.
embedJpg “SOI not found” 오류가 발생합니다: 바이트가 JPEG가 아닙니다. pdf-lib가 raw bytes를 기대하는 자리에 base64 data 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. 파일 공유 플러그인을 사용하거나 PDF를 복사하세요 Directory.Cache 먼저.
iOS에서 오래된 페이지 이미지가 저장 공간을 차지합니다: 스캐너가 JPEG 파일을 Documents 디렉터리에 저장합니다. PDF 생성 후 삭제해야 하며, 방법은 다음과 같습니다. scanToPdf.
스캔 플로우 업데이트 배포
이 기능의 대부분은 TypeScript로 구현됩니다: PDF 레이아웃, 파일명, 업로드 로직, UI. 이러한 부분을 변경해도 새 스토어 릴리스는 필요하지 않습니다. Capgo Live Update 사용하기 을(를) 사용하면 PDF 레이아웃이나 업로드 엔드포인트 수정 사항을 같은 날 모든 사용자에게 배포할 수 있으며, 네이티브 변경 사항(플러그인 추가, Info.plist편집)은 여전히 일반 빌드를 거칩니다. 문서 스캐너 문서 에서 스캐너를 더 세밀하게 조정하려면 모든 옵션을 확인할 수 있습니다.