react-reviso
v0.3.16
Published
React component for document restoration QA, review, and correction
Readme
react-reviso
An embeddable React component for reviewing and correcting OCR text on restored document images. Designed for QA workflows — users preview restored documents, validate regions, compare original vs restored, and edit corrections when needed.
Features
- Preview-First UX — default view shows restored documents for QA review, editing is entered explicitly
- Side-by-Side Comparison — original (left) vs restored (right) with independent zoom/pan
- Slider Comparison — drag slider overlay comparing original vs restored, horizontal or vertical orientation
- Comparison Sources — toggle between Overlay (original image with corrected text) and Synthetic Reconstruct (white background with text only)
- Region Validation — click checkmarks to mark regions as validated; progress bar tracks review completion
- Inline Editing — click a region to edit text, Tab/Shift+Tab to navigate between regions
- Region Management — create, resize, move, delete text regions; customise font, color, border, background, text position
- Auto Background Detection — restored preview automatically detects and fills region backgrounds from the document image
- Export — Synthetic Reconstruct, Overlay, Original, and Document JSON export types; PDF or PNG format with multi-page PNG auto-zipped; also available as a headless
exportDocument()API - Undo/Redo — Ctrl+Z / Ctrl+Shift+Z with full snapshot history
- Fit to View — reset zoom across all view modes from toolbar
- Theme Integration — inherits host app's MUI theme, accepts theme overrides
- Feature Toggles — enable/disable editing, region creation, comparison, export via props
- Keyboard Shortcuts — press
?to see all available shortcuts
Workflow
- Preview (default) — review restored documents in side-by-side or slider comparison mode
- Validate — click checkmarks on regions to confirm OCR corrections are accurate
- Edit (on demand) — enter edit mode to fix text, create/delete regions, adjust styles
- Export — download as Synthetic Reconstruct, Overlay, Original, or Document JSON in PDF or PNG format
Installation
npm install react-revisoPeer dependencies
npm install react react-dom @mui/material @mui/icons-material @emotion/react @emotion/styled framer-motionUsage
import { Reviso } from 'react-reviso';
import type { RevisoDocument } from 'react-reviso';
const document: RevisoDocument = {
id: 'doc-1',
name: 'My Document',
pages: [
{
id: 'page-1',
pageNumber: 1,
imageSrc: '/path/to/restored-image.png',
originalImageSrc: '/path/to/original-image.png',
width: 1200,
height: 1600,
regions: [
{
id: 'region-1',
x: 100,
y: 200,
width: 300,
height: 40,
text: 'Corrected text',
originalText: 'Original OCR text',
},
],
},
],
};
function App() {
return (
<Reviso
document={document}
onChange={(dirtyPages) => console.log('Dirty pages:', dirtyPages)}
onPageChange={(pageId) => console.log('Page:', pageId)}
onSelectionChange={(regionId) => console.log('Selection:', regionId)}
/>
);
}Theming
Reviso automatically inherits the MUI theme from a parent ThemeProvider. You can also pass a theme prop for component-level overrides — these are deep-merged on top of the inherited theme.
Option 1: Inherit from host app theme
Wrap your app (or a parent component) with MUI's ThemeProvider. Reviso picks up the palette, typography, and other tokens automatically.
import { ThemeProvider, createTheme } from '@mui/material/styles';
const appTheme = createTheme({
palette: {
mode: 'dark',
primary: { main: '#90caf9' },
},
});
function App() {
return (
<ThemeProvider theme={appTheme}>
<Reviso document={document} />
</ThemeProvider>
);
}Option 2: Override via theme prop
Pass MUI ThemeOptions directly to the theme prop. These overrides are deep-merged on top of whatever theme Reviso inherits from the parent.
<Reviso
document={document}
theme={{
palette: {
mode: 'dark',
primary: { main: '#ce93d8' },
background: { default: '#1a1a2e', paper: '#16213e' },
},
typography: {
fontFamily: '"Fira Code", monospace',
},
}}
/>Option 3: Combine both
Use a host theme for global styles and the theme prop for Reviso-specific tweaks.
const appTheme = createTheme({
palette: { mode: 'dark' },
typography: { fontFamily: '"Inter", sans-serif' },
});
<ThemeProvider theme={appTheme}>
<Reviso
document={document}
theme={{
palette: {
primary: { main: '#ff7043' },
},
}}
/>
</ThemeProvider>In this example, Reviso uses the host's dark mode and Inter font, but overrides the primary color to deep orange.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| document | RevisoDocument | required | The document to display and review |
| editable | boolean | true | Enable/disable editing (when false, preview and validation only) |
| showSidebar | boolean | true | Show/hide page thumbnail sidebar |
| showToolbar | boolean | true | Show/hide the inline toolbar |
| features | { comparison?, export?, regionCreation? } | all true | Feature toggles |
| defaultRegionStyles | object | — | Default styles for new regions (see below) |
| theme | ThemeOptions | — | MUI theme overrides |
| initialPageId | string | first page | Initial page to display |
| onChange | (dirtyPages: RevisoPage[]) => void | — | Fired on any change, returns only modified pages |
| onRegionChange | (event) => void | — | Granular per-region change event ({ type, pageId, regionId, region? }) |
| onPageChange | (pageId: string) => void | — | Fired on page navigation |
| onSelectionChange | (regionId: string \| null) => void | — | Fired on region select/deselect |
| onExport | (format: 'json' \| 'pdf' \| 'png', data: Blob) => void | — | Intercept export (replaces auto-download) |
defaultRegionStyles
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| fontColor | string | '#1565c0' | Text color (blue) |
| fontFamily | string | 'Inter' | Font family |
| fontWeight | 'normal' \| 'bold' | 'normal' | Font weight |
| fontStyle | 'normal' \| 'italic' | 'normal' | Font style |
| textDecoration | 'none' \| 'line-through' | 'none' | Text decoration |
| borderColor | string | '#4caf50' | Region border color (green) |
| borderVisible | boolean | true | Show/hide region border |
| backgroundColor | string | 'transparent' | Region background fill |
| textPosition | 'inside' \| 'top' \| 'bottom' | 'top' | Where text renders relative to the region box |
RevisoRegion
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| id | string | yes | Unique region identifier |
| x | number | yes | X position (pixels from left) |
| y | number | yes | Y position (pixels from top) |
| width | number | yes | Region width in pixels |
| height | number | yes | Region height in pixels |
| text | string | yes | Current text content |
| originalText | string | no | Original text (for diff tracking) |
| fontColor | string | no | Text color |
| fontFamily | string | no | Font family |
| fontWeight | 'normal' \| 'bold' | no | Font weight |
| fontStyle | 'normal' \| 'italic' | no | Font style |
| textDecoration | 'none' \| 'line-through' | no | Text decoration |
| borderColor | string | no | Border color |
| borderVisible | boolean | no | Show/hide border |
| backgroundColor | string | no | Background fill |
| textPosition | 'inside' \| 'top' \| 'bottom' \| 'left' \| 'right' | no | Text placement relative to region |
| isValidated | boolean | no | Whether the region has been reviewed and confirmed |
When does onChange fire?
onChange fires once per discrete user action — it does not fire continuously during drag operations. It returns only dirty (modified) pages, not the full document. A page is considered dirty if any region was edited, created, or deleted. Dirty flags are reset after each onChange call, so the same changes are not re-emitted.
| Action | When it fires | |--------|---------------| | Edit text | On commit (Enter, Tab, or blur — not on every keystroke) | | Move region | On mouse release (not during drag) | | Resize region | On mouse release (not during drag) | | Create region | When the new region is added | | Delete region | Immediately on delete | | Change style | Immediately on each change (bold, italic, color, text position, etc.) | | Toggle validation | Immediately when region checkmark is clicked | | Undo / Redo | Immediately on restore |
View Modes
Preview Mode (default)
The default landing view for QA review. Two comparison layouts:
- Side-by-Side — original image (left) + restored image (right) with independent zoom/pan. Validation checkmarks overlay the restored pane.
- Slider — single overlay with a draggable comparison slider. Supports horizontal (left/right) and vertical (top/bottom) orientation. No validation checkmarks in this mode.
Each layout supports two comparison sources via toolbar toggle:
- Overlay — original page image with corrected text regions rendered on top
- Synthetic Reconstruct — white background with only the corrected text at original positions
Edit Mode
Entered via the "Edit" button in the toolbar or Ctrl+E. Full editing capabilities:
- Select, edit text, create/resize/delete regions
- Undo/redo, style controls, text visibility toggle
- Return to preview via the "Preview" button or
Ctrl+E
Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| Ctrl+E | Toggle Preview / Edit mode |
| Escape | Exit edit mode (when nothing selected) |
| ← / → | Previous / Next page |
| PageUp / PageDown | Previous / Next page |
| Ctrl+↑ / Ctrl+↓ | Previous / Next document |
| Ctrl+Z | Undo |
| Ctrl+Shift+Z | Redo |
| N | Toggle create mode |
| Delete | Delete selected region |
| Tab / Shift+Tab | Next / Previous region |
| Enter | Confirm edit |
| ? | Show keyboard shortcuts help |
Export API
Export functionality is available in two forms — a ready-made dialog component and a headless function — both usable outside the Reviso editor.
Export Dialog
Use ExportDocumentDialog to show the same export dialog used inside the editor. It accepts a RevisoDocument directly — no Reviso component or stores needed.
import { useState } from 'react';
import { ExportDocumentDialog } from 'react-reviso';
import type { RevisoDocument } from 'react-reviso';
const doc: RevisoDocument = { /* ... */ };
function MyPage() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Export</button>
<ExportDocumentDialog
open={open}
onClose={() => setOpen(false)}
document={doc}
onExport={(format, blob) => {
// Optional: intercept instead of auto-downloading
console.log(format, blob);
}}
/>
</>
);
}| Prop | Type | Required | Description |
|------|------|----------|-------------|
| open | boolean | yes | Whether the dialog is visible |
| onClose | () => void | yes | Called when dialog should close |
| document | RevisoDocument \| null | yes | The document to export |
| onExport | (format: 'json' \| 'pdf' \| 'png', data: Blob) => void | no | Intercept export (replaces auto-download) |
Headless Function
Use exportDocument() for full programmatic control — useful for batch export, custom UIs, or server-side workflows.
import { exportDocument } from 'react-reviso';
import type { RevisoDocument } from 'react-reviso';
const doc: RevisoDocument = { /* ... */ };
const result = await exportDocument({
documents: [doc],
type: 'synthetic', // 'synthetic' | 'overlay' | 'original' | 'json'
format: 'pdf', // 'pdf' | 'png' (ignored for 'json')
});
// result: { blob: Blob, filename: string, mimeType: string }
// Download it
const url = URL.createObjectURL(result.blob);
const link = document.createElement('a');
link.href = url;
link.download = result.filename;
link.click();
URL.revokeObjectURL(url);Export Types
| Type | Description |
|------|-------------|
| synthetic | White background with corrected text at original positions |
| overlay | Original page image with corrected text regions overlaid |
| original | Original page image as-is |
| json | Structured document JSON with all region data |
Export Formats
| Format | Behaviour |
|--------|-----------|
| pdf | All pages combined into a single PDF |
| png | Single PNG for one page; ZIP archive for multiple pages |
PDF Font Support
PDF export uses Noto Sans font variants for multi-script text rendering. Fonts are loaded lazily — only the fonts needed for the detected scripts are loaded.
Supported scripts:
| Script | Font | Covers | |--------|------|--------| | Latin | Noto Sans | English, European languages | | CJK | Noto Sans SC | Chinese, Japanese kanji | | Tamil | Noto Sans Tamil | Tamil | | Khmer | Noto Sans Khmer | Khmer | | Thai | Noto Sans Thai | Thai |
Font resolution order:
- Local path (
/assets/by default, or custom path viasetFontBasePath) - StandardFonts fallback (Helvetica — Latin only)
Known limitations:
- Korean (Hangul) is not currently supported in PDF export due to font size constraints (~10MB). Korean text will fall back to placeholder characters.
- PDF export uses Noto Sans fonts regardless of the font selected in the editor. The editor font choice (e.g. Inter, Arial) only applies to on-screen rendering and PNG export.
- Each text region uses a single font based on its dominant script. Mixed non-Latin scripts within a single region (e.g. Tamil + Chinese in one region) are not supported — use separate regions for different scripts.
fontWeight(bold) andfontStyle(italic) are respected.fontFamilyis not used in PDF output.
Font setup (required for non-Latin PDF export):
PDF export loads font files at runtime via fetch(). These fonts are included in the npm package under dist/assets/ but must be copied to your app's public directory so they can be served over HTTP.
Step 1 — Copy font assets to your public directory:
cp -r node_modules/react-reviso/dist/assets/ public/assets/This copies the following font files:
NotoSans-Regular.ttf,NotoSans-Bold.ttf(Latin)NotoSansSC-Regular.ttf,NotoSansSC-Bold.ttf(CJK)NotoSansTamil-Regular.ttf,NotoSansTamil-Bold.ttf(Tamil)NotoSansKhmer-Regular.ttf,NotoSansKhmer-Bold.ttf(Khmer)NotoSansThai-Regular.ttf,NotoSansThai-Bold.ttf(Thai)
By default, fonts are loaded from /assets/ at runtime. If you skip this step, non-Latin text in PDF exports will fall back to Helvetica (Latin only) and other scripts will render as placeholder characters.
Step 2 (optional) — Custom font path:
If your fonts are served from a different location (e.g. a subfolder or CDN), call setFontBasePath before exporting:
import { setFontBasePath } from 'react-reviso';
// Point to your custom font directory
setFontBasePath('/assets/fonts/');
// Or use a CDN
setFontBasePath('https://cdn.example.com/fonts/');Framework-specific examples:
| Framework | Public directory | Copy command |
|-----------|-----------------|--------------|
| Vite | public/ | cp -r node_modules/react-reviso/dist/assets/ public/assets/ |
| Next.js | public/ | cp -r node_modules/react-reviso/dist/assets/ public/assets/ |
| Create React App | public/ | cp -r node_modules/react-reviso/dist/assets/ public/assets/ |
Tip: Add the copy command to a
postinstallscript in yourpackage.jsonso fonts are copied automatically afternpm install:{ "scripts": { "postinstall": "cp -r node_modules/react-reviso/dist/assets/ public/assets/" } }
Development
Prerequisites
- Node.js >= 18
- npm >= 8
Setup
git clone https://github.com/zhernrong92/reviso.git
cd reviso
npm install
npm run devThe dev server runs two demo routes:
/— Legacy standalone demo with file upload, multi-document support/reviso— Embeddable component demo with a simulated host app layout
Commands
| Command | Description |
|---------|-------------|
| npm run dev | Start Vite dev server |
| npm run build | Production build (type-check + bundle) |
| npm run build:lib | Build library for publishing |
| npm run preview | Preview production build |
| npm run type-check | TypeScript type checking |
| npm run lint | ESLint check |
Building the Library
The library build is separate from the demo app build. It produces ESM (.mjs) and CommonJS (.cjs) bundles with TypeScript declarations.
# Build the library (outputs to dist/)
npm run build:libThis generates:
dist/index.mjs— ES moduledist/index.cjs— CommonJS moduledist/index.d.ts— TypeScript declarations
Peer dependencies (React, MUI, Emotion, Framer Motion) are not bundled — consumers must install them separately.
Testing Locally
To test the library in another project before publishing:
# 1. Build and pack
npm run build:lib
npm pack
# 2. In the consumer project, install from the tgz
npm install /path/to/react-reviso-x.x.x.tgzPublishing to npm
# 1. Login
npm login
# 2. Bump version
npm version patch --no-git-tag-version
# 3. Build and publish
npm run build:lib
npm publish --access publicTech Stack
| Technology | Purpose | |------------|---------| | React 18 | UI framework | | TypeScript 5 (strict) | Type safety | | Vite 5 | Dev server + library bundler | | MUI 6 | Component library, theming | | Zustand 5 + Immer | State management | | Framer Motion | Page transitions | | react-zoom-pan-pinch | Document viewer zoom/pan | | react-compare-slider | Before/after comparison slider | | pdf-lib | PDF export generation | | fflate | ZIP compression for multi-page PNG export | | nanoid | Unique ID generation |
