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

@traveler123/t-scanner

v0.1.0

Published

Headless document scanner for the browser: edge detection, auto corner detection, perspective warp and scan filters. Framework-agnostic core + optional React hook and UI.

Downloads

103

Readme

@traveler123/t-scanner

Сканер документов для браузера: детекция краёв, авто-поиск углов, выпрямление перспективы (гомография) и фильтры скана — всё на чистом <canvas>, без внешних зависимостей.

Библиотека разбита на три уровня, и каждый верхний — опционален:

| Слой | Импорт | Зависимости | Назначение | |------|--------|-------------|------------| | Ядро | @traveler123/t-scanner | нет | вся обработка изображения, фреймворк-агностично | | Headless-хук | @traveler123/t-scanner/react | react | состояние + действия, без разметки | | Готовый UI | @traveler123/t-scanner/react/ui | react + css | компоненты «из коробки» |

UI-часть можно полностью убрать: импортируйте только ядро или только хук и постройте собственный интерфейс. React нужен лишь для двух верхних слоёв (заявлен как опциональная peer-зависимость).

Установка

npm install @traveler123/t-scanner
# react нужен только для слоёв /react и /react/ui
npm install react react-dom

1. Только ядро (без UI и без фреймворка)

Чистые функции и классы для обработки кадра. Подходит для любого фреймворка, воркера или ванильного JS.

import {
  EdgeDetector,
  detectDocument,
  extractDocument,
  warpPerspective,
  applyFilter,
  CameraController,
} from "@traveler123/t-scanner";

// 1. Найти документ в кадре (видео/картинка/canvas) → 4 угла [0..1]
const quad = detectDocument(videoEl); // или null

// 2. Выпрямить и применить фильтр скана за один вызов
if (quad) {
  const canvas = extractDocument(videoEl, quad, { filter: "bw" });
  document.body.appendChild(canvas);
}

// Живой цикл детекции (переиспользует буферы):
const detector = new EdgeDetector();
function tick() {
  const { bin, quad, width, height } = detector.detect(videoEl);
  // ...рисуйте bin/quad как хотите
  requestAnimationFrame(tick);
}

Доступные примитивы (все экспортируются из @traveler123/t-scanner):

toGray, boxBlur, sobel, threshold, adaptiveThreshold, detectQuad, polyArea, orderCorners, getPerspectiveTransform, applyTransform, solve8, warpPerspective, bilinear, applyFilter, EdgeDetector, detectDocument, extractDocument, captureFrame, CameraController, drawQuad, drawEdgeMask, fitContain, createCanvas, mergePages, pagesToPdf, cloneCanvas, encodeCanvas, centerCropRect.

2. Headless React-хук (логика без разметки)

useDocumentScanner владеет камерой, циклом детекции и машиной состояний (capture → edit → result), но ничего не рендерит. Вы привязываете возвращённые ref-ы и вызываете действия из своей разметки.

import { useDocumentScanner } from "@traveler123/t-scanner/react";

function MyScanner() {
  const s = useDocumentScanner({ filter: "bw", autoDetect: true });

  return (
    <div>
      <div ref={s.stageRef} style={{ position: "relative", height: 480 }}>
        {s.mode === "capture" && (
          <>
            <video ref={s.videoRef} playsInline muted />
            <canvas ref={s.overlayRef} />
          </>
        )}
        {s.mode === "edit" && (
          <>
            <canvas ref={s.editCanvasRef} />
            {s.cornerHandles.map((h) => (
              <div
                key={h.index}
                style={{ position: "absolute", left: h.x, top: h.y }}
                onPointerDown={(e) => e.currentTarget.setPointerCapture(e.pointerId)}
                onPointerMove={(e) =>
                  e.currentTarget.hasPointerCapture(e.pointerId) &&
                  s.moveCorner(h.index, e.clientX, e.clientY)
                }
              />
            ))}
          </>
        )}
        {s.mode === "result" && <canvas ref={s.resultCanvasRef} />}
      </div>

      {s.mode === "capture" && <button onClick={s.capture}>Снять</button>}
      {s.mode === "edit" && <button onClick={s.scan}>Сканировать</button>}
      {s.mode === "result" && <button onClick={() => s.download()}>Скачать</button>}
    </div>
  );
}

Хук возвращает: mode, isReady, status, error, facingMode, quad, cornerHandles, filter, autoDetect, zoom/minZoom/maxZoom, pages; действия start, stop, capture, retake, scan, back, flipCamera, setFilter, setQuad, moveCorner, toggleAutoDetect, setZoom/zoomIn/zoomOut, addPage/removePage/clearPages; и вывод getResultCanvas, toBlob, toDataURL, download, exportPdf/downloadPdf, mergePagesCanvas. Полные типы — в DocumentScannerApi.

3. Готовый UI (опционально)

import { DocumentScanner } from "@traveler123/t-scanner/react/ui";
import "@traveler123/t-scanner/styles.css"; // стили подключаются отдельно — их тоже можно убрать

export default function App() {
  return <DocumentScanner filter="bw" onScan={(c) => console.log(c.width)} />;
}

Кастомизация UI

Всё настраивается, а любую часть можно скрыть или заменить:

<DocumentScanner
  labels={{ title: "Scan", scan: "Process →", filters: { bw: "Mono" } }}
  filters={["color", "bw"]}      // какие фильтры показывать
  showHeader={false}             // убрать шапку
  showToolbar={false}            // убрать тулбар целиком
  saveFilename="document.png"
  onSave={(api) => api.toBlob().then(upload)}   // своя логика сохранения
  renderToolbar={(api, mode) => <MyBar api={api} mode={mode} />} // свой тулбар
  className="my-scanner"
  edgeStyle={{ color: [255, 80, 80], alpha: 0.6 }}
  quadStyle={{ stroke: "#ff5050" }}
  resultBackground="#fff"
/>

Тема меняется CSS-переменными (переопределите после импорта стилей):

.ds-app {
  --ds-accent: #3b82f6;
  --ds-bg: #ffffff;
  --ds-ink: #0f172a;
}

Несколько страниц в один документ (multi-page)

Хук всегда отдаёт API страниц — собирайте несколько сканов и выгружайте в один PDF или одно «сшитое» изображение:

s.addPage();                 // зафиксировать текущий результат как страницу
s.pages;                     // ScannedPage[] — собранные страницы
s.removePage(id);
s.clearPages();
await s.downloadPdf("doc.pdf");        // все страницы → один PDF
const blob = await s.exportPdf();      // тот же PDF как Blob (для загрузки/превью)
const sheet = s.mergePagesCanvas({ direction: "vertical", gap: 16 }); // одно изображение

В готовом UI достаточно включить флаг — появится кнопка «+ Страница», а основная кнопка станет экспортом всех страниц в PDF:

<DocumentScanner multiPage saveFilename="document.pdf" />

Те же функции есть и в ядре (без React), они работают с любыми canvas:

import { pagesToPdf, mergePages } from "@traveler123/t-scanner";

const pdf = await pagesToPdf([page1Canvas, page2Canvas]); // Blob, zero-deps
const tall = mergePages([page1Canvas, page2Canvas], { gap: 12 });

pagesToPdf встраивает каждую страницу как JPEG (/DCTDecode) и сам пишет байты PDF — без сторонних зависимостей.

Цифровой зум (center crop)

Берётся не весь кадр, а его центральная часть, и растягивается на холст. Реализован чисто программно (без track.applyConstraints/ImageCapture), поэтому работает одинаково везде — desktop, iOS, Android, — предсказуемо ускоряет детекцию и убирает лишние объекты по краям. Превью увеличивается CSS-трансформом видео, а оверлей/детекция/снимок используют тот же центральный кроп, так что углы всегда совпадают с картинкой.

В готовом UI зум включён по умолчанию: пилюля − 1.0× + в режиме съёмки, плюс колесо мыши и пинч. Настройка границ — через опции хука:

<DocumentScanner minZoom={1} maxZoom={4} zoomStep={0.5} zoom={1} />
// скрыть контрол: <DocumentScanner showZoom={false} />

Через хук (headless):

s.zoom;                 // текущий зум (>= 1)
s.setZoom(2);           // с клампом в [minZoom, maxZoom]
s.zoomIn(); s.zoomOut();
s.canZoomIn; s.canZoomOut; s.minZoom; s.maxZoom;

В ядре зум — это параметр zoom у детекции/снимка (центр-кроп):

detector.detect(video, { zoom: 2 });   // углы нормализованы к кропу
const frame = captureFrame(video, 2);  // снимок только центральной части
const doc = extractDocument(video, quad, { zoom: 2, filter: "bw" });
const { sx, sy, sw, sh } = centerCropRect(video.videoWidth, video.videoHeight, 2);

Свой контроллер + готовая разметка

DocumentScannerView — чисто презентационный компонент. Можно держать хук у себя, а рендер отдать ему (или наоборот — взять хук и не брать View вовсе):

import { useDocumentScanner } from "@traveler123/t-scanner/react";
import { DocumentScannerView } from "@traveler123/t-scanner/react/ui";

function App() {
  const api = useDocumentScanner();
  // ...свои кнопки, которые дёргают api.capture() / api.scan() ...
  return <DocumentScannerView api={api} showToolbar={false} />;
}

Производительность

Живая детекция краёв идёт в main thread каждый кадр (только в режиме съёмки). Превью видео при этом всегда плавное — это нативный <video>; троттлится лишь детекция и подсветка краёв/углов. Два способа снизить нагрузку:

// Детект раз в N кадров (видео по-прежнему на полном fps):
<DocumentScanner detectEveryNFrames={2} />

// Или ограничение по времени — стабильно на любом дисплее (60/120 Гц):
<DocumentScanner detectInterval={66} />   // ~15 детекций/сек; имеет приоритет

// Меньше пикселей на детекцию (грубее углы, на качество скана не влияет):
<DocumentScanner detect={{ procWidth: 240 }} />

Те же опции есть и у хука (useDocumentScanner({ detectEveryNFrames, detectInterval, detect })). detectInterval (мс) предпочтительнее detectEveryNFrames: «каждый 2-й кадр» на 120 Гц даёт 60 детекций/с, а на 60 Гц — 30, тогда как интервал в мс держит нагрузку одинаковой. На качество финального скана ничего из этого не влияет — варп берёт полный снятый кадр.

Web Worker (полная разгрузка main thread)

Флаг worker уносит всю детекцию (downscale через OffscreenCanvas + Sobel/threshold/ поиск углов) в Web Worker. В главном потоке остаётся только захват кадра (createImageBitmap с кропом под зум, передаётся transfer-ом) и отрисовка оверлея.

<DocumentScanner worker detectInterval={66} />
const s = useDocumentScanner({ worker: true });
s.usingWorker; // true, когда детекция реально идёт в воркере

Воркер инлайнится как Blob — отдельный файл подключать не нужно, работает в любом сборщике (Vite/webpack/Rollup/esbuild) без конфига. Если окружение не поддерживает (например, Safari < 16.4, нет OffscreenCanvas в воркере) или Blob- воркер заблокирован CSP — происходит автоматический фолбэк на main-thread, usingWorker остаётся false. По умолчанию worker выключен (opt-in).

Как это работает

  1. Детекция краёв — даунскейл кадра → grayscale → box-blur → оператор Собеля → адаптивная бинаризация (mean + k·std).
  2. Поиск углов — без полного contour-tracing: из облака краевых точек берутся 4 экстремума по диагоналям x±y. Устойчиво, когда документ — доминирующий контрастный объект; дальше углы правятся вручную.
  3. Выпрямление — гомография 4→4 точки (Гаусс с выбором ведущего), обратный маппинг с билинейной интерполяцией.
  4. Фильтрыcolor (как есть), gray (по яркости), bw (адаптивный порог через интегральное изображение — чистый «скан»).

Примеры

  • examples/vanilla.html — только ядро, без React и без стилей пакета (запуск: npx serve . → открыть /examples/vanilla.html).
  • examples/react — Vite + React: готовый UI с multi-page → PDF и полностью кастомный UI на хуке (cd examples/react && npm install && npm run dev).

Требования

  • Камера доступна только по HTTPS или с localhost.
  • Нужен браузер с getUserMedia, <canvas> 2D и requestAnimationFrame.

Скрипты

npm run build      # сборка ESM + CJS + .d.ts (+ styles.css)
npm run typecheck  # проверка типов
npm test           # smoke-тесты математики ядра

Лицензия

MIT