Saltar al contenido principal

Cómo tomar y editar fotos en una app Capacitor

Toma y edita fotos en una app de Capacitor con el plugin Camera 8 API: captura, selección de galería, recorte, vista previa, compresión, EXIF y uploads.

Tomar y editar una foto con la cámara dentro de una app de Capacitor

Para tomar y editar fotos en una app de Capacitor, usa @capacitor/camera 8.1 o superior: Camera.takePhoto() abre la cámara, editable: 'in-app' añade un paso de recorte y rotación, y chooseFromGallery() selecciona fotos existentes mediante el selector del sistema. Para una pantalla de cámara personalizada con tus propios controles, usa @capgo/camera-preview. Esta guía cubre captura, edición, permisos, compresión, metadatos, guardado y subida, con code para Capacitor 8.

Elige la herramienta adecuada

¿Necesitas Plugin Por qué
Toma una foto con la UI de la cámara del sistema @capacitor/camera takePhoto Sin UI personalizada que mantener, funciona en todos lados
Permite al usuario recortar o rotar @capacitor/camera editable, editPhoto, editURIPhoto Editor integrado en la app para iOS y Android
Selecciona una o varias fotos o videos @capacitor/camera chooseFromGallery Usa Android Photo Picker y PHPicker, sin permisos amplios
Pantalla de cámara personalizada (overlays, marco de ID, guía facial) @capgo/camera-preview Vista previa nativa que controlas desde HTML y JS
Explora toda la galería en tu propia cuadrícula @capgo/capacitor-photo-library Acceso paginado a la galería con miniaturas
Comprime, redimensiona o convierte HEIC @capgo/capacitor-file-compressor Encoder nativo, elimina metadatos EXIF
Sube fotos pesadas de forma fiable @capgo/capacitor-uploader Subida nativa en segundo plano con progreso

Instala y configura el plugin Camera

bun add @capacitor/camera
bunx cap sync

Asegúrate de tener la versión 8.1 o superior. Los métodos de esta guía (takePhoto, chooseFromGallery, editPhoto, editURIPhoto) se añadieron en la 8.1, y CameraErrorCode en la 8.2. Las anteriores getPhoto y pickImages están obsoletas.

iOS

Añade estas claves a ios/App/App/Info.plist. Apple rechaza builds con strings vagos, así que explica el motivo real:

<key>NSCameraUsageDescription</key>
<string>Take a photo for your profile or attach it to a report.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose existing photos to attach.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos you take to your library.</string>

Android

Capturar y elegir de la galería no requiere permisos. El plugin inicia la actividad de la cámara del sistema y usa el Android Photo Picker en dispositivos con Android 11+ que lo soporten, recurriendo a ACTION_OPEN_DOCUMENT en otros casos. Para usar el picker backported en dispositivos antiguos con Google Play services, añade esto dentro de <application> en AndroidManifest.xml. El snippet usa el tools namespace, así que también añade xmlns:tools="http://schemas.android.com/tools" a la raíz <manifest> elemento si aún no existe (un nuevo proyecto Capacitor solo declara xmlns:android):

<service android:name="com.google.android.gms.metadata.ModuleDependencies"
    android:enabled="false"
    android:exported="false"
    tools:ignore="MissingClass">
    <intent-filter>
        <action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
    </intent-filter>
    <meta-data android:name="photopicker_activity:0:required" android:value="" />
</service>

Solo saveToGallery: true necesita permisos de almacenamiento, y solo en versiones antiguas de Android:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

Evita añadir READ_MEDIA_IMAGES a menos que el propósito principal de tu app sea una galería o un editor. La política de permisos de fotos y vídeos de Google Play exige que las apps con necesidades puntuales de fotos usen el picker; los builds que declaren el permiso sin justificación se rechazan.

Tomar una foto

import { Camera, CameraDirection, CameraErrorCode, EncodingType } from '@capacitor/camera';

export async function takePicture() {
  try {
    const photo = await Camera.takePhoto({
      quality: 85,
      targetWidth: 2048,
      targetHeight: 2048,
      correctOrientation: true,
      encodingType: EncodingType.JPEG,
      cameraDirection: CameraDirection.Rear, // iOS and web
      includeMetadata: true,
    });

    // webPath works directly in <img src>
    document.querySelector<HTMLImageElement>('#preview')!.src = photo.webPath!;
    return photo;
  } catch (e: any) {
    if (e.code === CameraErrorCode.TakePhotoCancelled) return null;
    if (e.code === CameraErrorCode.CameraPermissionDenied) {
      // show a screen that explains how to enable the camera in Settings
      return null;
    }
    throw e;
  }
}

Lo que recibes es un MediaResult:

  • uri: URI de archivo nativo de la imagen completa (no en web). Pásalo a Filesystem, a un uploader o editURIPhoto.
  • webPath: una URL que el WebView puede cargar. Úsala para previews en lugar de base64.
  • thumbnail: una miniatura en base64 en nativo, la imagen completa en web.
  • metadata: tamaño, formato, resolución, fecha de creación y EXIF cuando includeMetadata es verdadero.
  • saved: si saveToGallery se completó.

targetWidth y targetHeight deben configurarse juntos. Son la forma más barata de evitar subir fotos de 12 MP cuando solo necesitas una imagen de 2048 píxeles.

Editar la foto: recortar y rotar

Editar justo tras la captura

Configurar editable y el usuario llega a un editor antes de que se resuelva la promesa:

const photo = await Camera.takePhoto({
  quality: 85,
  editable: 'in-app', // 'in-app' | 'external' | 'no'
});

'external' pasa la imagen a otro editor instalado en Android (por ejemplo, Google Photos) y usa el editor interno como fallback. iOS no tiene edición externa y siempre usa 'in-app'.

Editar una foto existente

Para editar una foto seleccionada o una guardada previamente, pasa su URI:

const edited = await Camera.editURIPhoto({
  uri: photo.uri!,
  saveToGallery: false,
  includeMetadata: false,
});

Si solo tienes base64 (por ejemplo, de un canvas o una respuesta del servidor), usa editPhoto. El input debe ser base64 puro sin un data: prefijo:

const { outputImage } = await Camera.editPhoto({ inputImage: base64 });
img.src = `data:image/jpeg;base64,${outputImage}`;

Ninguno de estos métodos está disponible en la web. Si necesitas filtros, stickers, texto o dibujo libre, el editor integrado no será suficiente. Renderiza la imagen en un <canvas> y aplica las ediciones allí, o usa un cropper de JavaScript como Cropper.js para relaciones de aspecto fijas como avatares. Un export de canvas (canvas.toBlob(cb, 'image/jpeg', 0.85)) te devuelve un Blob que puedes subir directamente.

import { Camera, MediaTypeSelection } from '@capacitor/camera';

const { results } = await Camera.chooseFromGallery({
  mediaType: MediaTypeSelection.Photo,
  allowMultipleSelection: true,
  limit: 5,            // Android 13+ and iOS
  quality: 85,
  targetWidth: 2048,
  targetHeight: 2048,
});

const urls = results.map((r) => r.webPath);

Para una sola foto, allowMultipleSelection: false además de editable: 'in-app' gives you pick-then-crop in one call. Set mediaType: MediaTypeSelection.All para aceptar también vídeos.

En iOS, los usuarios pueden conceder acceso limitado. pickLimitedLibraryPhotos() les permite cambiar la selección y getLimitedLibraryPhotos() devuelve lo que puedes ver. Con el selector del sistema rara vez necesitas ambos, ya que los elementos seleccionados siempre son legibles.

Si tu app muestra la biblioteca del usuario en tu propia UI (una app de impresión de fotos, una app de backup), usa @capgo/capacitor-photo-library:

import { PhotoLibrary } from '@capgo/capacitor-photo-library';

const { state } = await PhotoLibrary.requestAuthorization();
if (state === 'authorized' || state === 'limited') {
  const { assets, hasMore } = await PhotoLibrary.getLibrary({
    limit: 60,
    offset: 0,
    thumbnailWidth: 256,
    thumbnailHeight: 256,
  });
  // assets[i].thumbnail?.webPath for the grid, getPhotoUrl({ id }) for full size
}

El mismo plugin pickMedia() abre el picker del sistema y copia la selección a la caché, si quieres un solo plugin para ambos flujos. Recuerda la política de Google Play mencionada arriba antes de solicitar acceso completo a la biblioteca en Android.

Crear una pantalla de cámara personalizada

La UI de la cámara del sistema no puede mostrar un marco de ID, un contorno de documento ni tu marca. @capgo/camera-preview renderiza el feed de la cámara nativa y te permite dibujar la UI en HTML:

bun add @capgo/camera-preview
bunx cap sync
import { CameraPreview } from '@capgo/camera-preview';

await CameraPreview.start({
  position: 'rear',
  toBack: true,          // preview behind the WebView
  aspectRatio: '4:3',
  storeToFile: true,     // capture() returns a file path instead of base64
  disableAudio: true,
});

await CameraPreview.setFlashMode({ flashMode: 'auto' });
await CameraPreview.setZoom({ level: 2 });

const { value: filePath, exif } = await CameraPreview.capture({ quality: 85 });

await CameraPreview.stop();

Con toBack: true, la vista previa queda detrás del WebView, así que la página y todos los elementos padre deben tener un fondo transparente mientras la cámara está activa. Un patrón común es un camera-active clase en body que establece background: transparent y oculta el resto de la app. Añade NSCameraUsageDescription en iOS, y en Android el plugin solicita el CAMERA permisos en tiempo de ejecución mediante requestPermissions(). Llama siempre a stop() cuando la pantalla se cierre para liberar la cámara.

Muestra, comprime y convierte

Usa webPath (o Capacitor.convertFileSrc(path) para rutas de otros plugins) para mostrar imágenes. Evita el base64 en <img> tags para fotos de tamaño completo: un JPEG de 4 MB se convierte en un string de 5,3 MB en la memoria de JS y cruza el bridge.

Antes de subir, reduce la imagen:

import { FileCompressor } from '@capgo/capacitor-file-compressor';

const { path } = await FileCompressor.compressImage({
  path: photo.uri!,
  width: 1600,          // keeps aspect ratio
  quality: 0.75,
  mimeType: 'image/jpeg',
});

El compresor acepta entrada HEIC de iPhones, por lo que la misma llamada convierte HEIC a JPEG para backends que no pueden leer HEIC. Escribe el resultado en un directorio temporal, así que muévelo con @capacitor/filesystem si necesitas conservarlo.

Metadatos EXIF y privacidad de la ubicación

Con includeMetadata: true, metadata.exif contiene el bloque EXIF de la foto. Las fotos del carrete de la cámara pueden incluir coordenadas GPS. Dos reglas te evitan problemas:

  1. Elimina la ubicación antes de subir fotos que serán públicas (fotos de perfil, listados de mercado). @capgo/capacitor-file-compressor elimina los EXIF en todas las plataformas como parte de la compresión.
  2. Si necesitas la ubicación (informes de campo, inspecciones), léela de los EXIF antes de comprimir y envíala como un campo separado. Luego, declárala en la etiqueta de privacidad de la App Store y en el formulario de seguridad de datos de Play. Consulta nuestra guía de privacy manifest para el lado de iOS.

saveToGallery: true en takePhoto o editURIPhoto guarda la imagen en el carrete y establece saved en el resultado. Para mantener una foto privada para tu app, cópiala en el almacenamiento de la app:

import { Filesystem, Directory } from '@capacitor/filesystem';

await Filesystem.copy({
  from: photo.uri!,
  to: `photos/${Date.now()}.jpg`,
  toDirectory: Directory.Data,
});

El SO puede eliminar los archivos del directorio cache en cualquier momento, así que no guardes referencias de larga duración a ellos.

Sube la foto

Para fotos pequeñas, fetch con FormData es suficiente:

const blob = await fetch(photo.webPath!).then((r) => r.blob());
const form = new FormData();
form.append('file', blob, 'photo.jpg');
await fetch('https://api.example.com/photos', { method: 'POST', body: form });

Para archivos grandes o redes inestables, usa un uploader nativo que siga funcionando cuando la app pase a segundo plano:

import { Uploader } from '@capgo/capacitor-uploader';

const { id } = await Uploader.startUpload({
  filePath: path,
  serverUrl: 'https://api.example.com/photos',
  method: 'POST',
  mimeType: 'image/jpeg',
});

await Uploader.addListener('events', (event) => {
  console.log(event.name, event.payload);
});

Ejemplo: flujo de foto de perfil

  1. Muestra un action sheet con “Tomar foto” y “Elegir de la biblioteca”.
  2. Llama a takePhoto o chooseFromGallery con editable: 'in-app', targetWidth: 1024, targetHeight: 1024.
  3. Recorta a un cuadrado en un canvas si necesitas una relación de aspecto 1:1 garantizada.
  4. Comprime con quality: 0.8 y metadatos EXIF eliminados.
  5. Sube la imagen y muestra la URL del servidor, no el archivo local, para que el avatar persista tras reinstalar la app.

Solución de problemas

La app se cierra en iOS al abrir la cámara: falta la clave de descripción de uso en Info.plist.

La foto aparece rotada en Android: establece correctOrientation: true (el valor predeterminado) y evita leer píxeles en bruto sin aplicar la orientación EXIF.

La app se reinicia tras tomar una foto en dispositivos Android con poca memoria: Android puede cerrar tu actividad mientras la cámara está abierta. Escucha appRestoredResult de los @capacitor/app para recuperar el resultado tras el reinicio.

webPath no se carga: guardaste el webPath y reutilizado tras un reinicio. Persiste el uri o una ruta de Filesystem y conviértelo de nuevo con Capacitor.convertFileSrc.

Camera preview shows a black screen: un elemento sobre el WebView aún tiene un color de fondo, o start() se llamó antes de que se otorgara el permiso.

Play Console rechaza el build por permisos de fotos: eliminar READ_MEDIA_IMAGES y READ_MEDIA_VIDEO a menos que hayas presentado una declaración de que tu app requiere acceso amplio.

Publica fixes sin un release en la tienda

Los tamaños de imagen, niveles de compresión, ratios de recorte y endpoints de upload viven en tu code web. Con actualizaciones Live Update de Capgo puedes cambiarlos para cada usuario sin esperar la revisión, mientras que los cambios nativos como nuevos plugins requieren un build regular. Si necesitas builds de iOS sin un Mac, Build de Capgo gestiona el signing y el upload con Xcode 26, que App Store Connect exige desde abril de 2026.

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.