npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ssi-lib/document-capture

v1.0.3

Published

Captura de fotos y documentos: diálogo fullscreen, cámara o archivo, recorte al recuadro y compresión en canvas.

Readme

@ssi-lib/document-capture

Captura de cédula/ID, documentos y foto de perfil para React (ESM + CJS + tipos). Cámara o galería/explorador, recorte automático al tipo de recuadro configurado y compresión en canvas — sin OpenCV, sin detección automática, sin recortador interactivo.

El objetivo es andar en celulares de gama baja: JPEG, un solo encode, la mejor resolución que la cámara pueda dar sin frenar el arranque del preview, y foco continuo para que el documento salga nítido.

Instalación

npm i @ssi-lib/document-capture

Peer deps: react / react-dom (>=18). El CSS se inyecta automáticamente al importar el paquete — no hace falta import './styles.css'.

Los tipos de recuadro

Solo existen tres view: no hay formas genéricas tipo "cuadrado" o "sin recuadro".

| view | Qué es | Ratio (ancho:alto) | Orientación | | --- | --- | --- | --- | | 'id' (default) | Cédula / carnet / tarjeta (formato ID-1) | 1.586 : 1 | Horizontal | | 'document' | Hoja de documento (formato A4) | 1 : 1.414 | Vertical | | 'profile' | Foto de perfil (cabeza y hombros) | 3 : 4 | Vertical, ~90% del ancho |

Ese ratio se usa para dos cosas, en cámara y en archivo por igual:

  • El recuadro guía que se dibuja sobre el preview de la cámara (profile además dibuja una silueta de una sola línea — cabeza circular y hombros — para encajar el rostro).
  • El recorte automático (centrado, tipo object-fit: cover) que se aplica a la foto final — tanto la que sale de la cámara (recortada al recuadro) como la que el usuario elige de la galería/explorador (recortada al mismo ratio, centrada, sin cropper interactivo).

Se puede leer el ratio exacto en código: VIEW_ASPECT.id === 1.586, VIEW_ASPECT.document === 1 / 1.414, VIEW_ASPECT.profile === 3 / 4.

Uso básico — cédula (caso principal)

import { useState } from 'react';
import { DocumentCapture, type CaptureResult } from '@ssi-lib/document-capture';

export function CaptureCedula() {
  const [open, setOpen] = useState(false);
  const [shot, setShot] = useState<CaptureResult | null>(null);

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Capturar cédula
      </button>

      <DocumentCapture
        open={open}
        view="id"
        label="Cédula — frente"
        placeholder="Alineá la cédula dentro del recuadro"
        maxBytes={1_500_000}
        onCapture={setShot}
        onClose={() => setOpen(false)}
      />

      {shot && <img src={shot.dataUrl} alt="" />}
    </>
  );
}

view="id" es el default, así que también vale sin pasarlo:

<DocumentCapture open={open} onCapture={setShot} onClose={() => setOpen(false)} />

open controla la visibilidad. onClose se llama al cancelar (× o Escape) y justo después de un onCapture exitoso.

Ejemplo completo — cédula frente y reverso

Patrón típico de onboarding: dos capturas seguidas con la misma cámara, reutilizando el mismo componente.

import { useState } from 'react';
import { DocumentCapture, type CaptureResult } from '@ssi-lib/document-capture';

type Lado = 'frente' | 'reverso' | null;

export function CaptureCedulaCompleta() {
  const [lado, setLado] = useState<Lado>(null);
  const [frente, setFrente] = useState<CaptureResult | null>(null);
  const [reverso, setReverso] = useState<CaptureResult | null>(null);

  const listo = frente && reverso;

  return (
    <>
      {!frente && (
        <button type="button" onClick={() => setLado('frente')}>
          Capturar frente
        </button>
      )}
      {frente && !reverso && (
        <button type="button" onClick={() => setLado('reverso')}>
          Capturar reverso
        </button>
      )}
      {listo && <p>Cédula completa ✓</p>}

      <DocumentCapture
        open={lado !== null}
        view="id"
        label={lado === 'frente' ? 'Cédula — frente' : 'Cédula — reverso'}
        placeholder="Alineá la cédula dentro del recuadro"
        maxBytes={1_500_000}
        onCapture={(result) => {
          if (lado === 'frente') setFrente(result);
          else setReverso(result);
        }}
        onClose={() => setLado(null)}
      />
    </>
  );
}

Ejemplo — documento (hoja A4)

<DocumentCapture
  open={open}
  view="document"
  label="Comprobante de domicilio"
  placeholder="Que se vea la hoja completa"
  maxEdge={1800}
  onCapture={setShot}
  onClose={() => setOpen(false)}
/>

Ejemplo — cámara frontal

<DocumentCapture
  open={open}
  facing="user"
  view="profile"
  onCapture={setShot}
  onClose={() => setOpen(false)}
/>

facing es solo el arranque. Dentro del diálogo el usuario puede invertir la cámara con ↻ aunque el prop diga otra cosa. La frontal se espeja solo en el preview; la foto sale sin espejar. Trasera, normal.

Ejemplo — foto de perfil (view="profile")

Retrato vertical 3:4. En cámara el recuadro usa ~90% del ancho; si no entra en alto, se achica y mantiene el ratio. Lo que se recorta es el recuadro con las marcas de esquina, no el contorno de la silueta.

La silueta es una guía visual, solo en source="camera":

  • Una sola línea abierta: empieza abajo a la izquierda, sube el hombro, rodea la cabeza en círculo y termina abajo a la derecha. Sin cruces ni cierre en el pecho.
  • Cabeza circular; hombros al ancho del recuadro; torso más corto en vertical; cuello un poco más ancho, no más alto. La figura queda un poco más abajo del borde superior del recuadro.
  • Trazo blanco al 50% de opacidad, stroke-width 0.1, difuminado.
  • El interior es un hueco (se ve la cámara). El resto del recuadro va velado, con un desvanecido suave entre el hueco y el velo — no un corte seco.
  • En source="file" no hay silueta: el recorte es al ratio 3:4, centrado.

Sin copy default: label / placeholder / html los pone el cliente si los necesita. Cámara trasera por default.

import { useState } from 'react';
import { DocumentCapture, type CaptureResult } from '@ssi-lib/document-capture';

export function CapturePerfil() {
  const [open, setOpen] = useState(false);
  const [shot, setShot] = useState<CaptureResult | null>(null);

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Foto de perfil
      </button>

      <DocumentCapture
        open={open}
        view="profile"
        onCapture={setShot}
        onClose={() => setOpen(false)}
      />

      {shot && <img src={shot.dataUrl} alt="" />}
    </>
  );
}

Frontal de entrada: facing="user" (el preview se espeja; la foto no).

Ejemplo — galería/explorador (source="file")

source="file" no tiene diálogo propio: apenas open pasa a true se abre directamente el selector nativo del sistema (galería en mobile, explorador en desktop) — no hay recuadro ni pantalla de por medio que tocar primero. Al elegir una imagen, se recorta automáticamente al ratio de view (centrado) antes de comprimir.

import { useState } from 'react';
import { DocumentCapture, type CaptureResult } from '@ssi-lib/document-capture';

export function ElegirDeGaleria() {
  const [open, setOpen] = useState(false);
  const [shot, setShot] = useState<CaptureResult | null>(null);

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Elegir de galería
      </button>

      <DocumentCapture
        open={open}
        source="file"
        view="id"
        accept="image/*"
        maxBytes={1_500_000}
        onCapture={setShot}
        onClose={() => setOpen(false)}
      />
    </>
  );
}

label/placeholder/html no aplican a source="file" (no hay dónde mostrarlos: no hay diálogo). Si la lectura del archivo falla (por ejemplo un PDF que supera maxBytes), se muestra un toast fijo abajo de la pantalla con el error — eso es lo único que renderiza este modo aparte del <input> oculto.

Si el usuario cancela el selector sin elegir nada, onClose se llama automáticamente en navegadores que soportan el evento cancel del input (Chrome/Firefox recientes). En navegadores sin ese evento (Safari viejo), open se mantiene en true hasta que el consumidor lo cierre por su cuenta.

Ejemplo — cámara o galería con el mismo estado

Patrón común: un botón "Tomar foto" y otro "Elegir de galería", ambos alimentando el mismo flujo.

import { useState } from 'react';
import { DocumentCapture, type CaptureResult } from '@ssi-lib/document-capture';

export function CapturaOGaleria() {
  const [open, setOpen] = useState(false);
  const [source, setSource] = useState<'camera' | 'file'>('camera');
  const [shot, setShot] = useState<CaptureResult | null>(null);

  return (
    <>
      <button
        type="button"
        onClick={() => {
          setSource('camera');
          setOpen(true);
        }}
      >
        Tomar foto
      </button>
      <button
        type="button"
        onClick={() => {
          setSource('file');
          setOpen(true);
        }}
      >
        Elegir de galería
      </button>

      <DocumentCapture
        open={open}
        source={source}
        view="id"
        label="Cédula — frente"
        placeholder="Alineá la cédula dentro del recuadro"
        maxBytes={1_500_000}
        onCapture={setShot}
        onClose={() => setOpen(false)}
      />
    </>
  );
}

Ejemplo — sin recortar (crop={false})

Entrega la imagen completa (cámara: el frame entero, sin recortar al recuadro que queda solo de guía visual; archivo: la imagen tal cual, sin recorte de aspect ratio).

<DocumentCapture
  open={open}
  view="document"
  crop={false}
  onCapture={setShot}
  onClose={() => setOpen(false)}
/>

Ejemplo — tope de tamaño y formato

<DocumentCapture
  open={open}
  view="id"
  mimeType="image/jpeg"
  quality={0.75}
  maxEdge={1280}
  maxBytes={800_000}
  onCapture={setShot}
  onClose={() => setOpen(false)}
/>

maxBytes primero baja la calidad JPEG (hasta 0.5) y, si no alcanza, baja la resolución en pasos del 82% hasta 6 intentos.

Ejemplo — copy propio con HTML

<DocumentCapture
  open={open}
  view="id"
  html="<strong>Cédula</strong><br/><span style='opacity:.8'>Que no tape las esquinas</span>"
  onCapture={setShot}
  onClose={() => setOpen(false)}
/>

html reemplaza label/placeholder (innerHTML, sin sanitizar — solo contenido de confianza propio). Solo aplica a source="camera".

Ejemplo — aceptar también PDF en source="file"

<DocumentCapture
  open={open}
  source="file"
  view="document"
  accept="image/*,.pdf,application/pdf"
  maxBytes={2_000_000}
  onCapture={setShot}
  onClose={() => setOpen(false)}
/>

Los PDF (o cualquier tipo que no empiece con image/) no se recortan ni redimensionan: se devuelven como data URL tal cual, y solo se valida maxBytes (si lo supera, error).

Ejemplo — helper sin diálogo (processImageFile)

Para integrar el mismo pipeline de recorte + resize + compresión en tu propio input de archivo, sin usar el componente:

import { processImageFile, VIEW_ASPECT } from '@ssi-lib/document-capture';

async function onChange(file: File) {
  const result = await processImageFile(file, {
    aspectRatio: VIEW_ASPECT.id, // 1.586:1; también .document o .profile. Omitilo para no recortar
    maxEdge: 1600,
    maxBytes: 1_500_000,
  });
  console.log(result.dataUrl, result.width, result.height, result.bytes);
}

Props de DocumentCapture

| Prop | Tipo | Default | Requerido | Descripción | | --- | --- | --- | --- | --- | | open | boolean | — | sí | Abre/cierra. En source="camera" monta un diálogo fullscreen; en source="file" dispara el picker nativo del sistema. | | source | 'camera' \| 'file' | 'camera' | no | Cámara con diálogo propio, o selector de archivo/galería sin diálogo. | | facing | 'environment' \| 'user' | 'environment' | no | Trasera o frontal. Solo source="camera"; se puede invertir dentro del diálogo. | | view | 'id' \| 'document' \| 'profile' | 'id' | no | Recuadro: cédula (horizontal, 1.586:1), hoja (vertical, 1:1.414) o perfil (retrato 3:4, silueta de cabeza y hombros en cámara). Define el recuadro guía y el ratio de recorte. | | crop | boolean | true | no | Recorta al ratio de view. En cámara, al recuadro; en archivo, centrado. Si es false, entrega la imagen completa. | | maxEdge | number | 1600 | no | Lado mayor en px después del encode. | | maxBytes | number | — | no | Tope del data URL. Baja calidad JPEG y después resolución. | | mimeType | 'image/jpeg' \| 'image/png' | 'image/jpeg' | no | Formato de salida de imágenes. | | quality | number | 0.82 | no | Calidad JPEG 0–1. | | accept | string | 'image/*' | no | accept del input file. Ej. 'image/*,.pdf'. Solo source="file". | | onCapture | (result: CaptureResult) => void | — | sí | Entrega la captura. | | onClose | () => void | — | no | Cancelar y también tras una captura exitosa. En source="file", también al cancelar el picker nativo (si el navegador soporta el evento cancel). | | label | string | — | no | Título sobre el recuadro. Solo source="camera". Sin copy default. | | placeholder | string | — | no | Texto de ayuda sobre el recuadro. Solo source="camera". Sin copy default. | | html | string | — | no | Reemplaza label/placeholder (innerHTML, sin sanitizar). Solo source="camera". |

Resultado (CaptureResult)

interface CaptureResult {
  dataUrl: string;
  base64: string;
  mimeType: string;
  width: number;   // 0 si no es imagen (p. ej. PDF)
  height: number;
  bytes: number;
  source: 'camera' | 'file';
  name?: string;    // nombre original si vino de archivo
}

Cámara: arranque rápido + mejor resolución + foco continuo

getUserMedia se intenta en este orden, todo con ideal (nunca lanza OverconstrainedError por sí solo):

  1. facingMode pedido, 1280×720, con focusMode/exposureMode/whiteBalanceMode en 'continuous'.
  2. facingMode pedido, sin resolución fija (mismos hints de foco).
  3. Cualquier cámara (video: true).

Se pide 720p primero a propósito: cambiar el sensor a un modo de mayor resolución es notablemente más lento en hardware de gama baja, y eso es demora real antes de ver el primer frame. Una vez que el video ya está mostrando algo, se pide 1080p en segundo plano sobre el mismo track (applyConstraints — sin permiso nuevo, sin reabrir el stream); si el dispositivo no puede, se queda con lo que ya tenía andando.

focusMode: 'continuous' es lo que le pide al driver que reenfoque solo — sin este hint, varios drivers de Android no reajustan el foco a la distancia corta típica de una cédula/documento sostenido cerca de la cámara.

Hace falta HTTPS o localhost. Al cerrar, desmontar o cambiar de cámara se cortan los tracks. La vista frontal se espeja solo en preview (transform); la foto sale sin espejar.

Archivo/galería: por qué no hay diálogo

Elegir un archivo nunca necesitó un recuadro propio ni un paso extra: source="file" dispara el <input type="file"> nativo apenas se abre, así el usuario ve directamente el selector del sistema (galería, Fotos, explorador de archivos) sin una pantalla negra intermedia que tocar primero. El recorte al tipo de documento configurado (view) pasa después, solo, sobre lo que el usuario eligió — no requiere que el usuario ajuste nada a mano.

CSS

Clases con prefijo ssi-doc- (solo relevantes en source="camera", salvo .ssi-doc-file y .ssi-doc-file-error):

| Clase | Elemento | | --- | --- | | .ssi-doc-dialog | Contenedor fullscreen de la cámara | | .ssi-doc-dialog.is-id / .is-document / .is-profile | Recuadro según view | | .ssi-doc-silhouette | Guía de perfil: línea abierta (cabeza circular + hombros) y hueco velado. Solo view="profile" | | .ssi-doc-dialog.is-user | Preview espejado (cámara frontal) | | .ssi-doc-video | <video> | | .ssi-doc-frame | Recuadro guía/recorte | | .ssi-doc-shutter / .ssi-doc-flip / .ssi-doc-close | Controles | | .ssi-doc-copy / .ssi-doc-label / .ssi-doc-hint | Título y ayuda | | .ssi-doc-error | Error dentro del diálogo de cámara | | .ssi-doc-file | <input type="file">, oculto, usado en source="file" | | .ssi-doc-file-error | Toast fijo con el error de lectura en source="file" (no hay diálogo donde mostrarlo) |

Desarrollo

npm install
npm run build
npm run typecheck

Preview local: library-hub/ui-preview-library → ruta /document-capture.