Saltar al contenido principal

Cómo escanear un documento a PDF en una app Capacitor

Escanea un documento a PDF en una app de Capacitor: captura las páginas con el escáner nativo, genera un único PDF y guárdalo, compártelo, ábrelo o imprímelo en iOS y Android.

Cómo escanear un documento a PDF en una app Capacitor

Para escanear un documento a PDF en una app de Capacitor, abre el escáner nativo con @capgo/capacitor-document-scanner, toma las páginas JPEG que devuelve y combínalas en un solo PDF con pdf-lib. Desde ahí, escribes el PDF con @capacitor/filesystem y lo pasas al menú de compartir, a un visor de PDF o al diálogo de impresión del sistema. Esta guía recorre todo el flujo con code que se ejecuta en Capacitor 8, tanto en iOS como en Android.

Cómo funciona el escaneo a PDF en iOS y Android

Ambas plataformas incluyen un escáner de documentos del sistema, así que no necesitas programar tú mismo la detección de bordes o la corrección de perspectiva:

iOS Android
Escáner nativo VisionKit VNDocumentCameraViewController Google ML Kit Document Scanner (Play services)
Detección de bordes y recorte Sí Sí
Filtros / limpieza VisionKit integrado base, base_with_filter, full modos (eliminación de manchas y dedos en full)
Límite de páginas por sesión 24 De 1 a 24, por defecto 20
Prompt de permiso de cámara Sí, requiere NSCameraUsageDescription No, el scanner corre en Play services
Funciona en emulador / simulador No No
Output del plugin Páginas JPEG Páginas JPEG

El plugin devuelve imágenes, no un PDF. Es una separación deliberada: tú controlas el tamaño de página, la compresión y el nombre del archivo, y el mismo PDF code funciona tanto para fotos de la galería como para archivos recibidos de tu backend.

Instala los plugins

bun add @capgo/capacitor-document-scanner @capacitor/filesystem pdf-lib
bunx cap sync

El plugin de scanner sigue Capacitor versiones mayores, por lo que la v8 del plugin apunta a Capacitor 8. pdf-lib es una librería de JavaScript puro sin code nativo, lo que significa que se ejecuta en el WebView y no requiere sincronización.

iOS setup

Añade un string de uso de cámara a ios/App/App/Info.plist. VisionKit abre la cámara, e iOS cerrará la app si falta la clave:

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

El Swift package del plugin apunta a iOS 15 o superior, lo que coincide con el mínimo de Capacitor 8. Recuerda que los uploads a la App Store ahora requieren Xcode 26, así que haz el build con él antes de enviar. Si no tienes un Mac, Build con Capgo puede generar el binario de iOS firmado en la nube.

Configuración de Android

Nada que añadir a AndroidManifest.xml. El scanner de ML Kit es un módulo de Google Play services que se descarga al primer uso y gestiona la cámara por sí mismo, por lo que tu app no solicita CAMERA. El dispositivo requiere Google Play services, lo que excluye la mayoría de los emuladores y teléfonos sin servicios de Google. Planifica un fallback (una captura de cámara normal) si lanzas en mercados donde Play services no esté disponible.

Escanear las páginas

Empieza con una función que abra el scanner y devuelva las rutas de las páginas, o null cuando el usuario cancela:

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 conocer estas opciones:

  • responseType: ImageFilePath (por defecto) devuelve rutas de archivos, Base64 devuelve strings en base64. Prefiere rutas de archivos para documentos de varias páginas. Los strings base64 de 20 páginas en alta resolución pueden alcanzar decenas de megabytes y cada byte cruza el bridge de Capacitor.
  • maxNumDocuments: cámbialo a 1 para flujos de una sola página como recibos o DNI.
  • reviewCapturedDocument: muestra cada página capturada antes de continuar, ideal para escanear documentos importantes.
  • brightness y contrast: post-procesamiento aplicado a las imágenes. Un contraste de 1.2 a 1.5 suele ayudar con recibos desgastados.
  • scannerMode (Android): Full añade limpieza basada en ML, Base es más rápido y mantiene la imagen intacta.

La ubicación de los archivos varía según la plataforma. En Android, las páginas van al directorio de caché de la app. En iOS, el plugin las escribe en el directorio Documents de la app. Ninguna ubicación es ideal para almacenamiento a largo plazo, por lo que el siguiente flujo genera el PDF, lo guarda donde prefieras y elimina las imágenes de las páginas.

Combina las páginas en un solo PDF

pdf-lib puede incrustar JPEGs directamente sin re-codificarlos, por lo que el tamaño del PDF será similar a la suma de las páginas. Lee cada página mediante Capacitor.convertFileSrc, que convierte una ruta nativa en una URL que la WebView puede cargar:

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

Si prefieres páginas que coincidan exactamente con el papel escaneado, cambia la lógica de A4 por pdf.addPage([image.width, image.height]) y dibuja la imagen a tamaño completo. Los recibos se ven mejor así por ser largos y estrechos.

Alternativa: renderizar el PDF de forma nativa

Si tu app ya genera PDFs desde HTML (facturas, reportes), puedes reutilizar ese flujo con @capgo/capacitor-pdf-generator. Renderiza un string HTML con el motor de impresión de la plataforma y devuelve un 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
}

Esto es útil si quieres una portada, un título o un footer con fecha en cada página. Para convertir imagen a PDF, pdf-lib te da más control sobre la geometría de la página y la compresión de imagen.

Guardar el PDF

Escribe el output en base64 con @capacitor/filesystem. Elige el directorio según la permanencia del archivo:

Directorio Android iOS Úsalo para
Directory.Cache Caché de la app, el SO puede borrarla Library/Caches, el SO puede borrarla PDFs que subes o compartes al instante
Directory.Data Archivos privados, se borran al desinstalar App Documents carpeta, con copia en iCloud PDFs que la app gestiona y muestra luego
Directory.LibraryNoCloud Archivos privados de la app Library/NoCloud, sin copia de seguridad PDFs pesados que puedes descargar de nuevo

En iOS, Directory.Data y Directory.Documents apunta a la misma carpeta. Añade UIFileSharingEnabled y LSSupportsOpeningDocumentsInPlace a Info.plist si los usuarios deben ver esos PDFs en la app Archivos.

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

En plataformas nativas, writeFile trata un string sin una encoding opción como base64 y escribe los bytes binarios. No pases Encoding.UTF8 para PDFs, o el archivo se corromperá.

Si el usuario debe poder encontrar el PDF en su carpeta de Descargas en Android, usa @capgo/capacitor-file-sharerde save() método, que escribe en MediaStore Downloads sin necesidad de permisos de almacenamiento en Android 10+. En iOS, la misma llamada abre el share sheet para que el usuario elija “Guardar en Archivos”.

Integra todo el flujo

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

Conéctalo a un botón y muestra un spinner mientras pagesToPdf se ejecuta. En un Android de gama media, 10 páginas suelen tardar mucho menos de unos pocos segundos, ya que los datos JPEG se copian, no se decodifican.

Comparte, abre o imprime el PDF

Comparte con el share sheet del 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 el archivo en su propia carpeta de cache y lo sirve a través de su propio FileProvider en Android, así que no necesitas editar file_paths.xml, incluso si el PDF está en Directory.Data. Si usas el oficial @capacitor/share plugin oficial en su lugar, su files opción funciona directamente solo para el directorio de cache en Android. Debes añadir otras carpetas a android/app/src/main/res/xml/file_paths.xml.

Abrir en la app de PDF del usuario

Para una vista previa rápida, el file opener de la comunidad pasa el archivo a Quick Look en iOS o al visor de PDF predeterminado en 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',
});

Imprimir

@capgo/capacitor-printer abre UIPrintInteractionController en iOS y PrintManager en Android:

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

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

El name se convierte en el nombre del trabajo de impresión y en el nombre de archivo por defecto si el usuario elige “Guardar como PDF” en el diálogo de impresión.

Subir el PDF

Para enviar el PDF a tu backend, léelo como un Blob y haz un post con fetch, o usa un uploader nativo para archivos grandes para que la subida continúe cuando la app pase a segundo plano:

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

Consulta el plugin uploader de Capacitor para eventos de progreso y subidas multipart.

Haz que los PDFs escaneados sean buscables

Image-only PDFs cannot be searched or copied from. If users expect to search their scans:

  1. Ejecuta OCR en cada imagen de página antes de borrarla. Las opciones on-device incluyen ML Kit Text Recognition en Android y el framework Vision en iOS, ambos accesibles mediante plugins de la comunidad de Capacitor; también puedes usar un servicio de OCR en el servidor.
  2. Guarda el texto reconocido en tu base de datos junto a la ruta del PDF y busca allí.
  3. Si el PDF en sí debe contener texto, genéralo en el servidor con una herramienta de OCR que escriba una capa de texto invisible, como OCRmyPDF.

Para la mayoría de las apps, el paso 2 es suficiente y evita incluir un segundo modelo pesado en la app.

Solución de problemas

“El escáner de documentos no es compatible con los emuladores de Android”: es lo esperado. Usa un dispositivo físico con Google Play services.

La app se cierra en iOS en cuanto se abre el escáner: NSCameraUsageDescription is missing from Info.plist, o el usuario denegó el acceso a la cámara. Revisa Ajustes > Privacidad > Cámara.

embedJpg lanza “SOI not found”: los bytes no son un JPEG. Esto pasa si pasaste un data URL en base64 donde pdf-lib esperaba bytes puros, o si obtuviste una ruta que devolvió una página de error HTML. Loguea Capacitor.convertFileSrc(path) y ábrelo en el inspector de Safari o Chrome.

El PDF es enorme: reduce croppedImageQuality (Android) o vuelve a comprimir las páginas con @capgo/capacitor-file-compressor antes de incrustarlas. Una calidad de 70 a 80 suele ser legible para documentos de texto.

Las páginas aparecen rotadas: rota dentro de pdf-lib con page.setRotation(degrees(90)) o permite que el usuario corrija la rotación en la pantalla de revisión con reviewCapturedDocument: true.

El error al compartir en Android con FileUriExposedException o “Failed to find configured root”: pasaste una ruta fuera de las carpetas declaradas en file_paths.xml a @capacitor/share. Usa el plugin de file sharer o copia el PDF a Directory.Cache primero.

Old page images fill up storage on iOS: el scanner escribe JPEGs en el directorio Documents. Elimínalos tras generar el PDF, como en scanToPdf.

Lanzando actualizaciones al flujo de escaneo

La mayor parte de esta funcionalidad está en TypeScript: el layout del PDF, el nombrado de archivos, la lógica de upload y la UI. Los cambios en estas partes no requieren un nuevo release en la tienda. Con Live Updates de Capgo puedes publicar un fix del layout del PDF o del endpoint de upload para todos los usuarios el mismo día, mientras que los cambios nativos (añadir el plugin, editar Info.plist) siguen pasando por un build normal. El documentación del scanner de documentos revisa todas las opciones si necesitas ajustar más el scanner.

Live Updates para apps de Capacitor

Si hay un bug en la capa web, publica el fix con Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el proceso de revisión normal.

Lo último de nuestro blog

Capgo te da los mejores insights para crear una app móvil realmente profesional.