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 solo PDF y luego guárdalo, compártelo, ábrelo o imprímelo en iOS y Android.

Escanear un documento en papel a PDF dentro de una app móvil de 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 a la hoja de compartir, un visor de PDF o el diálogo de impresión del sistema. Esta guía recorre el flujo completo 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 implementar la detección de bordes ni la corrección de perspectiva tú mismo:

iOS Android
Escáner nativo VisionKit VNDocumentCameraViewController Google ML Kit Document Scanner (Play services)
Detección de bordes y recorte Sí Sí
Filtros / limpieza Integrado en VisionKit base, base_with_filter, full modos (eliminación de manchas y dedos en full)
Límite de páginas por sesión 24 1 a 24, predeterminado 20
Solicitud de permiso de cámara Sí, se requiere NSCameraUsageDescription No, el escáner se ejecuta en Play services
Funciona en emulador / simulador No No
Salida 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 para fotos seleccionadas de la galería o archivos recibidos de tu backend.

Instala los plugins

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

El plugin de escáner sigue las versiones principales de Capacitor, por lo que la v8 del plugin apunta a Capacitor 8. pdf-lib es una biblioteca de JavaScript pura sin code nativo, lo que significa que se ejecuta en el WebView y no requiere sincronización.

Configurar iOS

Añade un string de uso de cámara a ios/App/App/Info.plist. VisionKit abre la cámara, e iOS cierra 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 las subidas a la App Store ahora requieren Xcode 26, así que haz el build con él antes de enviar. Si no tienes un Mac, Build de 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, así que tu app no solicita CAMERA. El dispositivo necesita Google Play services, lo que descarta la mayoría de emuladores y los teléfonos sin Google. Planifica un fallback (una captura de cámara normal) si lanzas la app en mercados donde no haya Play services.

Escanea 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 cancele:

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 ?? [];
}

Hay algunas opciones que conviene conocer:

  • responseType: ImageFilePath (por defecto) devuelve rutas de archivo, Base64 devuelve cadenas base64. Prefiere rutas de archivo para documentos de varias páginas. Las cadenas base64 de 20 páginas en alta resolución pueden alcanzar decenas de megabytes y cada byte cruza el puente Capacitor.
  • maxNumDocuments: establécelo en 1 para flujos de una sola página, como recibos o tarjetas de identidad.
  • reviewCapturedDocument: muestra cada página capturada antes de continuar, útil cuando los usuarios escanean documentos importantes.
  • brightness y contrast: posprocesamiento aplicado a las imágenes. Un contraste de 1.2 a 1.5 suele ayudar con recibos descoloridos.
  • scannerMode (Android): Full añade limpieza basada en ML, Base es más rápido y mantiene la imagen intacta.

Dónde terminan los archivos cambia 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 de las dos ubicaciones sirve para almacenar a largo plazo, así que el flujo de abajo crea el PDF, lo guarda donde quieras y elimina las imágenes de las páginas.

Combina las páginas en un solo PDF

pdf-lib puede incrustar JPEGs directamente sin recodificarlos, así que el tamaño del PDF se mantiene cerca de la suma de los tamaños de las páginas. Lee cada página mediante Capacitor.convertFileSrc, que convierte una ruta nativa en una URL que el 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, reemplaza la lógica de A4 con pdf.addPage([image.width, image.height]) y dibuja la imagen a tamaño completo. Los recibos se ven mejor así porque son largos y estrechos.

Alternativa: renderizar el PDF de forma nativa

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

Es útil cuando quieres una portada, un título o un pie de página con fecha en cada página. Para conversión pura de imagen a PDF, pdf-lib te da más control sobre la geometría de la página y la compresión de imágenes.

Guardar el PDF

Escribe la salida base64 con @capacitor/filesystem. Elige el directorio según cuánto tiempo debe durar el archivo:

Directorio Android iOS Úsalo para
Directory.Cache Caché de la app, puede ser borrada por el SO Library/Caches, puede ser borrada por el SO PDFs que subas o compartas de inmediato
Directory.Data Archivos privados de la app, eliminados al desinstalar app Documents carpeta, respaldada en iCloud PDF que tu app gestiona y vuelve a mostrar más tarde
Directory.LibraryNoCloud Archivos privados de la app Library/NoCloud, sin respaldo PDF grandes que puedes volver a descargar

En iOS, Directory.Data y Directory.Documents apuntan 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 ningún permiso de almacenamiento en Android 10+. En iOS, la misma llamada abre la hoja de compartir para que el usuario elija “Guardar en Archivos”.

Integrar 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 ejecuciones. En un teléfono Android de gama media, 10 páginas suelen tardar menos de unos segundos, ya que los datos JPEG se copian, no se decodifican.

Compartir, abrir o imprimir el PDF

Compartir con la hoja de compartir 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 caché y lo sirve a través de su propio FileProvider en Android, así que no hace falta 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 caché en Android. Las demás carpetas deben añadirse 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 envíalo 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 la plugin uploader de Capacitor para eventos de progreso y subidas multipart.

Haz que los PDF escaneados sean buscables

Los PDF solo de imagen no se pueden buscar ni copiar de ellos. Si los usuarios esperan buscar en sus escaneos:

  1. Ejecuta OCR en cada imagen de página antes de eliminarla. Las opciones on-device incluyen ML Kit Text Recognition en Android y el framework Vision en iOS, accesibles mediante plugins de la comunidad de Capacitor, o un servicio de OCR en el servidor.
  2. Almacena el texto reconocido en tu base de datos junto a la ruta del PDF y busca ahí.
  3. Si el PDF debe contener texto, genéralo en el servidor con una herramienta 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 grande en la app.

Solución de problemas

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

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

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

El PDF es enorme: más baja 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: rótalas dentro de pdf-lib con page.setRotation(degrees(90)) o deja que el usuario corrija la rotación en la pantalla de revisión del escáner con reviewCapturedDocument: true.

Compartir falla 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 file sharer o copia el PDF a Directory.Cache primero.

Old page images fill up storage on iOS: el escáner escribe JPEGs en el directorio Documents. Elimínalos después de crear el PDF, como se muestra en scanToPdf.

Publicar actualizaciones del flujo de escaneo

La mayor parte de esta función está en TypeScript: el diseño del PDF, el nombre de archivo, la lógica de upload y la UI. Los cambios en esas partes no requieren una nueva versión en la tienda. Con actualizaciones Live Update de Capgo puedes enviar un fix al layout del PDF o al endpoint de upload a 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. La documentación del escáner de documentos consulta todas las opciones si necesitas ajustar más el escáner.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de 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 flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.