([\s\S]*?)<\/tr>/g;
+ let tr: RegExpExecArray | null;
+ while ((tr = trRe.exec(html))) {
+ const cells: Cell[] = [];
+ const tdRe = /]*)>([\s\S]*?)<\/td>/g;
+ let td: RegExpExecArray | null;
+ while ((td = tdRe.exec(tr[1]!))) {
+ const attrs = td[1] ?? '';
+ cells.push({
+ text: td[2] ?? '',
+ colspan: Number(/colspan="(\d+)"/.exec(attrs)?.[1] ?? 1),
+ rowspan: Number(/rowspan="(\d+)"/.exec(attrs)?.[1] ?? 1),
+ });
+ }
+ rows.push(cells);
+ }
+ return rows;
+}
+
+function TableView({ html }: { html: string }) {
+ const rows = parseTable(html);
+ if (rows.length === 0) {
+ return {html};
+ }
+ // Fixed-width cells inside a horizontal scroll — wide tables scroll instead of
+ // squishing every column into the screen width.
+ return (
+
+
+ {rows.map((cells, r) => (
+
+ {cells.map((c, i) => (
+
+ {c.text}
+
+ ))}
+
+ ))}
+
+
+ );
+}
+
+function DocumentContent() {
+ const [backendIdx, setBackendIdx] = useState(0);
+ const [layoutOn, setLayoutOn] = useState(true);
+ const [supportingOn, setSupportingOn] = useState(true);
+ const [orientation, setOrientation] = useState(true);
+ // Off by default: dewarp (UVDoc) corrects photographed, physically-warped pages;
+ // on a flat screenshot it has nothing to fix and visibly distorts clean text.
+ const [dewarp, setDewarp] = useState(false);
+ const [imageUri, setImageUri] = useState(null);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [blocks, setBlocks] = useState([]);
+ // The frame the result boxes are relative to (orientation/dewarp may move it
+ // away from the original), so the overlay lines up.
+ const [processed, setProcessed] = useState(null);
+ const [wallMs, setWallMs] = useState(null);
+ const [error, setError] = useState(null);
+
+ const backend = AVAILABLE[backendIdx]!;
+
+ const skiaImage = useImage(imageUri, (err) => setError(err.message || String(err)));
+
+ // Hosted configs — `useDocumentOcr` downloads + caches each enabled model.
+ // orientation/dewarp are NOT baked here: they're passed per-run to
+ // `runDocumentOcr` below, so toggling them takes effect without a reload.
+ const config = {
+ ocr: models.ocr.PADDLE.PPOCRV6_SMALL[backend.key],
+ ...(layoutOn ? { layout: models.layoutDetection.PP_DOCLAYOUT[backend.key] } : {}),
+ ...(supportingOn ? { documentModels: models.documentModels.PP_HELPERS[backend.key] } : {}),
+ };
+
+ const { isReady, downloadProgress, error: loadError, runDocumentOcr } = useDocumentOcr(config);
+
+ const handlePick = async (useCamera: boolean) => {
+ setError(null);
+ try {
+ const uri = await getImage(useCamera);
+ if (uri) {
+ setImageUri(uri);
+ setBlocks([]);
+ setProcessed(null);
+ setWallMs(null);
+ }
+ } catch (e: any) {
+ setError(e.message || String(e));
+ }
+ };
+
+ const run = async () => {
+ if (!skiaImage || !runDocumentOcr) return;
+ setIsProcessing(true);
+ setError(null);
+ try {
+ const pixels = skiaImage.readPixels();
+ if (!(pixels instanceof Uint8Array)) throw new Error('Expected Uint8Array from readPixels');
+ const start = Date.now();
+ const out = await runDocumentOcr(
+ {
+ data: pixels,
+ width: skiaImage.width(),
+ height: skiaImage.height(),
+ format: 'rgba' as const,
+ layout: 'hwc' as const,
+ },
+ { orientation, dewarp }
+ );
+ setWallMs(Date.now() - start);
+ setBlocks(out.blocks as DocBlock[]);
+ // Show the frame the boxes are relative to (orientation/dewarp may have
+ // rotated/warped it), so the overlaid boxes align.
+ const frame = out.image;
+ const skData = Skia.Data.fromBytes(frame.data);
+ const frameImage = Skia.Image.MakeImage(
+ {
+ width: frame.width,
+ height: frame.height,
+ colorType: ColorType.RGBA_8888,
+ alphaType: AlphaType.Unpremul,
+ },
+ skData,
+ frame.width * 4
+ );
+ setProcessed(frameImage);
+ } catch (e: any) {
+ setError(e.message || String(e));
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const activeError = loadError ? String(loadError) : error;
+ const boxes = blocks.map((b) => [
+ { x: b.bbox.xmin, y: b.bbox.ymin },
+ { x: b.bbox.xmax, y: b.bbox.ymin },
+ { x: b.bbox.xmax, y: b.bbox.ymax },
+ { x: b.bbox.xmin, y: b.bbox.ymax },
+ ]);
+
+ return (
+
+
+ Full document pipeline: layout → OCR grouped into reading-ordered blocks, with orientation,
+ table-structure recognition and (optional) dewarp. PaddleOCR is always on; dewarp is off by
+ default — it only helps photographed, warped pages. Orientation/dewarp are per-run, so
+ toggling them takes effect on the next run without reloading the models.
+
+
+ {
+ setBackendIdx(v);
+ setBlocks([]);
+ setProcessed(null);
+ setWallMs(null);
+ }}
+ />
+
+
+
+
+
+
+
+
+ handlePick(false)}
+ />
+
+
+
+
+
+
+
+ {wallMs !== null && (
+
+ Performance
+
+
+
+ {wallMs}
+ ms
+
+ Wall time
+
+
+ {blocks.length}
+ Blocks
+
+
+
+ )}
+
+ {blocks.length > 0 && (
+
+ Blocks ({blocks.length})
+ {blocks.map((b, i) => (
+
+
+ {b.regionType}
+ {b.isTable ? ' · table' : ''}
+
+ {b.isTable && b.tableHtml ? (
+
+ ) : (
+ {b.text}
+ )}
+
+ ))}
+
+ )}
+
+ );
+}
+
+function Toggle({
+ label,
+ value,
+ onChange,
+ hint,
+}: {
+ label: string;
+ value: boolean;
+ onChange: (v: boolean) => void;
+ hint?: string;
+}) {
+ return (
+
+
+ {label}
+ {hint ? {hint} : null}
+
+
+
+ );
+}
+
+export default function DocumentScreen() {
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ toggleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ width: '100%',
+ marginBottom: 8,
+ },
+ toggleText: { flex: 1, marginRight: 12 },
+ toggleLabel: { fontSize: 15, fontWeight: '600', color: ColorPalette.strongPrimary },
+ toggleHint: { fontSize: 12, color: '#868e96', marginTop: 2 },
+ statsCard: {
+ width: '100%',
+ backgroundColor: '#fff',
+ borderRadius: 12,
+ padding: 16,
+ marginBottom: 16,
+ borderWidth: 1,
+ borderColor: '#e9ecef',
+ },
+ statsTitle: {
+ fontSize: 12,
+ fontWeight: '700',
+ letterSpacing: 1,
+ color: '#868e96',
+ textTransform: 'uppercase',
+ marginBottom: 12,
+ },
+ statTiles: { flexDirection: 'row', gap: 12 },
+ tile: {
+ flex: 1,
+ backgroundColor: '#f2f4ff',
+ borderRadius: 10,
+ paddingVertical: 12,
+ paddingHorizontal: 14,
+ },
+ tileValue: {
+ fontSize: 24,
+ fontWeight: '800',
+ color: '#001A72',
+ fontVariant: ['tabular-nums'],
+ },
+ tileUnit: { fontSize: 14, fontWeight: '600', color: '#6b73a3' },
+ tileLabel: { fontSize: 11, color: '#868e96', marginTop: 4 },
+ results: {
+ width: '100%',
+ backgroundColor: '#fff',
+ borderRadius: 12,
+ padding: 16,
+ borderWidth: 1,
+ borderColor: '#e9ecef',
+ marginTop: 12,
+ },
+ resultsTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: ColorPalette.strongPrimary,
+ marginBottom: 12,
+ },
+ block: { paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f1f3f5' },
+ regionType: {
+ fontSize: 12,
+ fontWeight: '700',
+ color: '#2b8a3e',
+ textTransform: 'uppercase',
+ marginBottom: 4,
+ },
+ blockText: { fontSize: 14, color: '#333' },
+ table: { borderWidth: 1, borderColor: '#ced4da', borderRadius: 4, overflow: 'hidden' },
+ tr: { flexDirection: 'row' },
+ td: {
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: '#ced4da',
+ paddingHorizontal: 6,
+ paddingVertical: 4,
+ minWidth: 24,
+ },
+ tdText: { fontSize: 13, color: '#333' },
+});
diff --git a/apps/computer-vision/app/index.tsx b/apps/computer-vision/app/index.tsx
index cc44604e67..291450a2a8 100644
--- a/apps/computer-vision/app/index.tsx
+++ b/apps/computer-vision/app/index.tsx
@@ -32,6 +32,12 @@ export default function Home() {
router.navigate('keypoint/')}>
Keypoint Detection
+ router.navigate('ocr/')}>
+ OCR
+
+ router.navigate('document/')}>
+ Document Pipeline
+
router.navigate('inspect/')}>
Model Inspector
diff --git a/apps/computer-vision/app/ocr/index.tsx b/apps/computer-vision/app/ocr/index.tsx
new file mode 100644
index 0000000000..1248ccfbca
--- /dev/null
+++ b/apps/computer-vision/app/ocr/index.tsx
@@ -0,0 +1,300 @@
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, ScrollView, Platform, Switch } from 'react-native';
+import { commonStyles, ColorPalette } from '../../theme';
+import { useImage } from '@shopify/react-native-skia';
+import { useOcr, models, type OcrDetection } from 'react-native-executorch';
+import ScreenWrapper from '../../components/ScreenWrapper';
+import { getImage } from '../../utils';
+import { ModelPicker, type ModelOption } from '../../components/ModelPicker';
+import { ImageViewport } from '../../components/ImageViewport';
+import { ModelStatus } from '../../components/ModelStatus';
+import { Button } from '../../components/Button';
+
+const PREVIEW_HEIGHT = 280;
+
+// Hosted PTEs — downloaded + cached on-device from Hugging Face by `useOcr`.
+// Backends per platform: XNNPACK runs everywhere, Vulkan on Android, CoreML on iOS.
+const ALL_MODELS = [
+ {
+ label: 'PaddleOCR (XNNPACK)',
+ config: models.ocr.PADDLE.PPOCRV6_SMALL.XNNPACK,
+ platforms: ['ios', 'android'],
+ },
+ {
+ label: 'PaddleOCR (Vulkan)',
+ config: models.ocr.PADDLE.PPOCRV6_SMALL.VULKAN,
+ platforms: ['android'],
+ },
+ {
+ label: 'PaddleOCR (CoreML)',
+ config: models.ocr.PADDLE.PPOCRV6_SMALL.COREML,
+ platforms: ['ios'],
+ },
+ {
+ label: 'EasyOCR English (XNNPACK)',
+ config: models.ocr.EASYOCR.ENGLISH.XNNPACK,
+ platforms: ['ios', 'android'],
+ },
+ {
+ label: 'EasyOCR English (Vulkan)',
+ config: models.ocr.EASYOCR.ENGLISH.VULKAN,
+ platforms: ['android'],
+ },
+ {
+ label: 'EasyOCR English (CoreML)',
+ config: models.ocr.EASYOCR.ENGLISH.COREML,
+ platforms: ['ios'],
+ },
+];
+
+const OCR_MODELS = ALL_MODELS.filter((m) => m.platforms.includes(Platform.OS));
+
+const MODEL_OPTIONS: ModelOption[] = OCR_MODELS.map((m, i) => ({ label: m.label, value: i }));
+
+function OCRContent() {
+ const [selectedIdx, setSelectedIdx] = useState(0);
+ const [vertical, setVertical] = useState(false);
+ const [imageUri, setImageUri] = useState(null);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [results, setResults] = useState([]);
+ const [wallMs, setWallMs] = useState(null);
+ const [error, setError] = useState(null);
+
+ const selected = OCR_MODELS[selectedIdx]!;
+
+ const skiaImage = useImage(imageUri, (err) => setError(err.message || String(err)));
+
+ // `useOcr` downloads + caches the hosted PTE from its Hugging Face URL.
+ const { isReady, downloadProgress, error: loadError, runOcr } = useOcr(selected.config);
+
+ const handlePickImage = async (useCamera: boolean) => {
+ setError(null);
+ try {
+ const uri = await getImage(useCamera);
+ if (uri) {
+ setImageUri(uri);
+ setResults([]);
+ setWallMs(null);
+ }
+ } catch (e: any) {
+ setError(e.message || String(e));
+ }
+ };
+
+ const runRecognition = async () => {
+ if (!skiaImage || !runOcr) return;
+ setIsProcessing(true);
+ setError(null);
+ try {
+ const pixels = skiaImage.readPixels();
+ if (!(pixels instanceof Uint8Array)) {
+ throw new Error('Expected Uint8Array from readPixels');
+ }
+ const buffer = {
+ data: pixels,
+ width: skiaImage.width(),
+ height: skiaImage.height(),
+ format: 'rgba' as const,
+ layout: 'hwc' as const,
+ };
+ const start = Date.now();
+ // `vertical` is a per-run option now — toggling it needs no model reload.
+ const output = await runOcr(buffer, { vertical });
+ setWallMs(Date.now() - start);
+ setResults(output.detections);
+ } catch (e: any) {
+ setError(e.message || String(e));
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const activeError = loadError ? String(loadError) : error;
+
+ return (
+
+
+ Upload or capture an image to detect and recognize text on-device.
+
+
+ {
+ setSelectedIdx(idx);
+ setResults([]);
+ setWallMs(null);
+ setError(null);
+ }}
+ />
+
+
+
+ Vertical text
+
+ Read upright stacked columns (character-under-character)
+
+
+
+
+
+
+
+ r.quad)}
+ onPressPlaceholder={() => handlePickImage(false)}
+ />
+
+
+ handlePickImage(false)} variant="secondary" />
+ handlePickImage(true)} variant="secondary" />
+
+
+
+
+
+
+ {wallMs !== null && (
+
+ Performance
+
+
+
+ {wallMs}
+ ms
+
+ Wall time
+
+
+ {results.length}
+ Regions read
+
+
+
+ )}
+
+ {results.length > 0 && (
+
+ Detected text ({results.length})
+ {results.map((res, idx) => (
+
+
+ {res.text}
+
+
+ {Math.round(res.confidence * 100)}%
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default function OCRScreen() {
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ toggleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ width: '100%',
+ marginBottom: 12,
+ },
+ toggleText: { flex: 1, marginRight: 12 },
+ toggleLabel: { fontSize: 15, fontWeight: '600', color: ColorPalette.strongPrimary },
+ toggleHint: { fontSize: 12, color: '#868e96', marginTop: 2 },
+ statsCard: {
+ width: '100%',
+ backgroundColor: '#fff',
+ borderRadius: 12,
+ padding: 16,
+ marginBottom: 16,
+ borderWidth: 1,
+ borderColor: '#e9ecef',
+ },
+ statsTitle: {
+ fontSize: 12,
+ fontWeight: '700',
+ letterSpacing: 1,
+ color: '#868e96',
+ textTransform: 'uppercase',
+ marginBottom: 12,
+ },
+ statTiles: {
+ flexDirection: 'row',
+ gap: 12,
+ marginBottom: 12,
+ },
+ tile: {
+ flex: 1,
+ backgroundColor: '#f2f4ff',
+ borderRadius: 10,
+ paddingVertical: 12,
+ paddingHorizontal: 14,
+ },
+ tileValue: {
+ fontSize: 24,
+ fontWeight: '800',
+ color: '#001A72',
+ fontVariant: ['tabular-nums'],
+ },
+ tileUnit: { fontSize: 14, fontWeight: '600', color: '#6b73a3' },
+ tileLabel: { fontSize: 11, color: '#868e96', marginTop: 4 },
+ resultMeta: { flexDirection: 'row', alignItems: 'center' },
+ resultsContainer: {
+ width: '100%',
+ backgroundColor: '#fff',
+ borderRadius: 12,
+ padding: 16,
+ borderWidth: 1,
+ borderColor: '#e9ecef',
+ },
+ resultsTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: ColorPalette.strongPrimary,
+ marginBottom: 12,
+ },
+ resultRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingVertical: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: '#f1f3f5',
+ },
+ resultLabel: {
+ fontSize: 14,
+ color: '#333',
+ flex: 1,
+ marginRight: 8,
+ },
+ resultConfidence: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#2b8a3e',
+ },
+});
diff --git a/apps/computer-vision/components/ImageViewport.tsx b/apps/computer-vision/components/ImageViewport.tsx
index 593133eaf6..cd258f1528 100644
--- a/apps/computer-vision/components/ImageViewport.tsx
+++ b/apps/computer-vision/components/ImageViewport.tsx
@@ -1,9 +1,11 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import {
Canvas,
Image as SkImage,
BlendColor,
+ Path,
+ Skia,
type SkImage as SkiaImageType,
} from '@shopify/react-native-skia';
@@ -12,6 +14,11 @@ import { theme } from '../theme';
const VIEW_WIDTH = Dimensions.get('window').width - 32;
const VIEW_HEIGHT = Math.round((VIEW_WIDTH * 16) / 9);
+/** A 2D point in the displayed image's pixel coordinates. */
+type Point = { readonly x: number; readonly y: number };
+/** A polygon (e.g. an OCR quad) in the displayed image's pixel coordinates. */
+type Polygon = readonly Point[];
+
export interface ImageViewportProps {
skiaImage: SkiaImageType | null;
overlayImage?: SkiaImageType | null;
@@ -20,6 +27,10 @@ export interface ImageViewportProps {
placeholderText?: string;
overlayOpacity?: number;
children?: React.ReactNode;
+ /** Height of the preview box in px. Defaults to a 16:9 box. */
+ height?: number;
+ /** Polygons (in the displayed image's px) to stroke over the image, e.g. OCR quads. */
+ boxes?: readonly Polygon[];
}
export function ImageViewport({
@@ -30,17 +41,47 @@ export function ImageViewport({
placeholderText = 'Tap to select an image from gallery',
overlayOpacity = 0.8,
children,
+ height,
+ boxes,
}: ImageViewportProps) {
+ const viewHeight = height ?? VIEW_HEIGHT;
+
+ // Map image-pixel polygons into canvas space using the same contain-fit
+ // transform Skia uses to draw the image, then build one stroked path.
+ const boxesPath = useMemo(() => {
+ if (!skiaImage || !boxes?.length) return null;
+ const ow = skiaImage.width();
+ const oh = skiaImage.height();
+ if (ow === 0 || oh === 0) return null;
+ const scale = Math.min(VIEW_WIDTH / ow, viewHeight / oh);
+ const dx = (VIEW_WIDTH - ow * scale) / 2;
+ const dy = (viewHeight - oh * scale) / 2;
+
+ const path = Skia.Path.Make();
+ for (const poly of boxes) {
+ if (poly.length < 2) continue;
+ path.moveTo(dx + poly[0]!.x * scale, dy + poly[0]!.y * scale);
+ for (let i = 1; i < poly.length; i++) {
+ path.lineTo(dx + poly[i]!.x * scale, dy + poly[i]!.y * scale);
+ }
+ path.close();
+ }
+ return path;
+ }, [skiaImage, boxes, viewHeight]);
+
if (!skiaImage) {
return (
-
+
{placeholderText}
);
}
return (
-
+
))}
+ {boxesPath && }
{children}
@@ -85,7 +127,6 @@ export function ImageViewport({
const styles = StyleSheet.create({
placeholder: {
width: '100%',
- height: VIEW_HEIGHT,
borderWidth: 2,
borderColor: theme.colors.border,
borderStyle: 'dashed',
diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp
index ba50d9d12b..88d2d418f1 100644
--- a/packages/react-native-executorch/cpp/core/model.cpp
+++ b/packages/react-native-executorch/cpp/core/model.cpp
@@ -433,6 +433,39 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) {
return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "execute"), 3, fnBody);
}
+ if (nameStr == "unloadMethod") {
+ auto self = shared_from_this();
+ auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value {
+ if (count != 1) {
+ throw jsi::JSError(rt, "unloadMethod: Usage: unloadMethod(methodName)");
+ }
+
+ if (!args[0].isString()) {
+ throw jsi::JSError(rt, "unloadMethod: Expected arg0 to be a string");
+ }
+
+ std::unique_lock lock(self->mutex_, std::try_to_lock);
+ if (!lock.owns_lock()) {
+ throw jsi::JSError(rt, "unloadMethod: Model is currently in use");
+ }
+
+ if (!self->etModule_) {
+ throw jsi::JSError(rt, "unloadMethod: Model has been disposed");
+ }
+
+ // Free a single previously-executed method's planned-memory activation
+ // arena (and, on graph-compiling backends like CoreML, its compiled
+ // graph). The method reloads on next execute. Returns
+ // whether a loaded method was actually freed (false = not loaded, a
+ // harmless no-op). Bounds memory when many distinct bucketed methods
+ // accumulate over a session.
+ auto methodName = args[0].asString(rt).utf8(rt);
+ bool unloaded = self->etModule_->unload_method(methodName);
+ return jsi::Value(unloaded);
+ };
+ return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "unloadMethod"), 1, fnBody);
+ }
+
if (nameStr == "dispose") {
auto self = shared_from_this();
auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value {
@@ -462,6 +495,7 @@ std::vector ModelHostObject::getPropertyNames(jsi::Ru
properties.push_back(jsi::PropNameID::forAscii(rt, "getMethodNames"));
properties.push_back(jsi::PropNameID::forAscii(rt, "getMethodMeta"));
properties.push_back(jsi::PropNameID::forAscii(rt, "execute"));
+ properties.push_back(jsi::PropNameID::forAscii(rt, "unloadMethod"));
properties.push_back(jsi::PropNameID::forAscii(rt, "dispose"));
return properties;
}
diff --git a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp
index 501b9245c1..59315641a2 100644
--- a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp
+++ b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp
@@ -65,6 +65,7 @@ std::array decodeToXyxy(
case BoxFormat::CXCYWH:
return {a - c / 2.0f, b - d / 2.0f, a + c / 2.0f, b + d / 2.0f};
}
+ throw std::invalid_argument("decodeToXyxy: unhandled box format");
}
} // namespace
@@ -165,21 +166,29 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) {
std::vector> groups;
std::vector suppressed(candidates.size(), false);
+ // Decode every candidate to xyxy once, not per pair in the O(n²) loop.
+ std::vector> decoded(candidates.size());
+ for (size_t k = 0; k < candidates.size(); ++k) {
+ const std::int32_t idx = candidates[k].first;
+ decoded[k] = decodeToXyxy(
+ boxesPtr[idx * 4 + 0],
+ boxesPtr[idx * 4 + 1],
+ boxesPtr[idx * 4 + 2],
+ boxesPtr[idx * 4 + 3],
+ boxFormat);
+ }
+ const auto boxArea = [](const std::array &box) {
+ return (box[2] - box[0]) * (box[3] - box[1]);
+ };
+
for (size_t i = 0; i < candidates.size(); ++i) {
if (suppressed[i]) {
continue;
}
- std::int32_t idxI = candidates[i].first;
-
- auto [xminA, yminA, xmaxA, ymaxA] = decodeToXyxy(
- boxesPtr[idxI * 4 + 0],
- boxesPtr[idxI * 4 + 1],
- boxesPtr[idxI * 4 + 2],
- boxesPtr[idxI * 4 + 3],
- boxFormat);
-
- const float areaA = (xmaxA - xminA) * (ymaxA - yminA);
+ const std::int32_t idxI = candidates[i].first;
+ const auto &[aXmin, aYmin, aXmax, aYmax] = decoded[i];
+ const float areaA = boxArea(decoded[i]);
std::vector overlapping = {idxI};
@@ -188,21 +197,14 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) {
continue;
}
- std::int32_t idxJ = candidates[j].first;
-
- auto [xminB, yminB, xmaxB, ymaxB] = decodeToXyxy(
- boxesPtr[idxJ * 4 + 0],
- boxesPtr[idxJ * 4 + 1],
- boxesPtr[idxJ * 4 + 2],
- boxesPtr[idxJ * 4 + 3],
- boxFormat);
-
- const float areaB = (xmaxB - xminB) * (ymaxB - yminB);
+ const std::int32_t idxJ = candidates[j].first;
+ const auto &[bXmin, bYmin, bXmax, bYmax] = decoded[j];
+ const float areaB = boxArea(decoded[j]);
- const float interYMin = std::max(yminA, yminB);
- const float interXMin = std::max(xminA, xminB);
- const float interYMax = std::min(ymaxA, ymaxB);
- const float interXMax = std::min(xmaxA, xmaxB);
+ const float interYMin = std::max(aYmin, bYmin);
+ const float interXMin = std::max(aXmin, bXmin);
+ const float interYMax = std::min(aYmax, bYmax);
+ const float interXMax = std::min(aXmax, bXmax);
const float interH = std::max(0.0f, interYMax - interYMin);
const float interW = std::max(0.0f, interXMax - interXMin);
@@ -242,6 +244,7 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) {
return resultGroups;
}
}
+ throw jsi::JSError(rt, "nms: unhandled nmsType");
};
module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody));
diff --git a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp
index 13bf66cd81..69b08e0154 100644
--- a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp
+++ b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp
@@ -1,6 +1,7 @@
#include "image_ops.h"
#include
+#include
#include
#include
#include
@@ -714,4 +715,351 @@ void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) {
};
module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody));
}
+
+void install_rotate(jsi::Runtime &rt, jsi::Object &module) {
+ const auto *name = "rotate";
+ auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value {
+ if (count != 3) {
+ throw jsi::JSError(rt, "Usage: rotate(src, dst, degCW)");
+ }
+
+ if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) {
+ throw jsi::JSError(rt, "rotate: src must be a Tensor");
+ }
+
+ if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) {
+ throw jsi::JSError(rt, "rotate: dst must be a Tensor");
+ }
+
+ if (!args[2].isNumber()) {
+ throw jsi::JSError(rt, "rotate: degCW must be a number (90, 180, or 270)");
+ }
+
+ auto src = args[0].asObject(rt).getHostObject(rt);
+ auto dst = args[1].asObject(rt).getHostObject(rt);
+
+ if (src.get() == dst.get()) {
+ throw jsi::JSError(rt, "rotate: In-place operations (src == dst) are not supported.");
+ }
+
+ const auto degCW = static_cast(args[2].asNumber());
+ int rotateCode = 0;
+ if (degCW == 90) {
+ rotateCode = ::cv::ROTATE_90_CLOCKWISE;
+ } else if (degCW == 180) {
+ rotateCode = ::cv::ROTATE_180;
+ } else if (degCW == 270) {
+ rotateCode = ::cv::ROTATE_90_COUNTERCLOCKWISE;
+ } else {
+ throw jsi::JSError(rt, "rotate: degCW must be 90, 180, or 270");
+ }
+
+ if (src->shape_.size() != 3 || dst->shape_.size() != 3) {
+ throw jsi::JSError(rt, "rotate: src and dst must be [H, W, C]");
+ }
+
+ if (src->dtype_ != dst->dtype_) {
+ throw jsi::JSError(rt, "rotate: src and dst must have the same dtype");
+ }
+
+ if (src->shape_[2] != dst->shape_[2]) {
+ throw jsi::JSError(rt, "rotate: src and dst must have the same number of channels");
+ }
+
+ const int32_t srcH = src->shape_[0];
+ const int32_t srcW = src->shape_[1];
+ const int32_t channels = src->shape_[2];
+ // 90/270 transpose the axes; 180 preserves them. dst must be pre-sized to match,
+ // else cv::rotate reallocates off the tensor buffer and the result is lost.
+ const bool swap = degCW != 180;
+ const int32_t expH = swap ? srcW : srcH;
+ const int32_t expW = swap ? srcH : srcW;
+ if (dst->shape_[0] != expH || dst->shape_[1] != expW) {
+ throw jsi::JSError(rt, "rotate: dst must be sized [" + std::to_string(expH) + ", " +
+ std::to_string(expW) + ", C] for a " + std::to_string(degCW) +
+ " degree rotation");
+ }
+
+ std::shared_lock srcLock(src->mutex_, std::try_to_lock);
+ if (!srcLock.owns_lock()) {
+ throw jsi::JSError(rt, "rotate: src tensor is currently in use");
+ }
+
+ std::unique_lock dstLock(dst->mutex_, std::try_to_lock);
+ if (!dstLock.owns_lock()) {
+ throw jsi::JSError(rt, "rotate: dst tensor is currently in use");
+ }
+
+ if (!src->data_ || !dst->data_) {
+ throw jsi::JSError(rt, "rotate: a tensor has been disposed");
+ }
+
+ int cvType{};
+ try {
+ cvType = CV_MAKETYPE(dtypeToCvDepth(src->dtype_), channels);
+ } catch (const std::invalid_argument &e) {
+ throw jsi::JSError(rt, "rotate: " + std::string(e.what()));
+ }
+
+ const ::cv::Mat srcMat(srcH, srcW, cvType, src->data_.get());
+ ::cv::Mat dstMat(expH, expW, cvType, dst->data_.get());
+ ::cv::rotate(srcMat, dstMat, rotateCode);
+
+ return jsi::Value(rt, args[1]);
+ };
+
+ module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody));
+}
+
+// ------------------------------- warpByGrid --------------------------------
+// Warp `src` through a backward sampling field (torch grid_sample step of a
+// geometric dewarp) into `dst` via cv::remap. grid is [..,2,gH,gW], normalized
+// to [-1,1] with align_corners=true (channel 0 = x, 1 = y).
+void install_warpByGrid(jsi::Runtime &rt, jsi::Object &module) {
+ const auto *name = "warpByGrid";
+ auto fnBody = [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value {
+ if (count != 3) {
+ throw jsi::JSError(rt, "Usage: warpByGrid(src, grid, dst)");
+ }
+ if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt) ||
+ !args[1].isObject() || !args[1].asObject(rt).isHostObject(rt) ||
+ !args[2].isObject() || !args[2].asObject(rt).isHostObject(rt)) {
+ throw jsi::JSError(rt, "warpByGrid: src, grid, and dst must be Tensors");
+ }
+
+ auto src = args[0].asObject(rt).getHostObject(rt);
+ auto grid = args[1].asObject(rt).getHostObject(rt);
+ auto dst = args[2].asObject(rt).getHostObject(rt);
+
+ if (src.get() == dst.get()) {
+ throw jsi::JSError(rt, "warpByGrid: In-place operations (src == dst) are not supported.");
+ }
+ if (src->dtype_ != rnexecutorch::core::types::DType::uint8 ||
+ dst->dtype_ != rnexecutorch::core::types::DType::uint8) {
+ throw jsi::JSError(rt, "warpByGrid: src and dst must be uint8");
+ }
+ if (grid->dtype_ != rnexecutorch::core::types::DType::float32) {
+ throw jsi::JSError(rt, "warpByGrid: grid must be float32");
+ }
+ if (src->shape_.size() != 3 || dst->shape_.size() != 3) {
+ throw jsi::JSError(rt, "warpByGrid: src and dst must be [H, W, C]");
+ }
+ if (src->shape_ != dst->shape_) {
+ throw jsi::JSError(rt, "warpByGrid: src and dst must have the same shape");
+ }
+ // grid is the torch grid_sample field [..,2,gH,gW], channel 0 = x, 1 = y,
+ // normalized to [-1,1] with align_corners=true.
+ const auto &gs = grid->shape_;
+ if (gs.size() < 3 || gs[gs.size() - 3] != 2) {
+ throw jsi::JSError(rt, "warpByGrid: grid must be [..,2,gH,gW]");
+ }
+
+ std::shared_lock srcLock(src->mutex_, std::try_to_lock);
+ std::shared_lock gridLock(grid->mutex_, std::try_to_lock);
+ std::unique_lock dstLock(dst->mutex_, std::try_to_lock);
+ if (!srcLock.owns_lock() || !gridLock.owns_lock() || !dstLock.owns_lock()) {
+ throw jsi::JSError(rt, "warpByGrid: a tensor is currently in use");
+ }
+ if (!src->data_ || !grid->data_ || !dst->data_) {
+ throw jsi::JSError(rt, "warpByGrid: a tensor has been disposed");
+ }
+
+ const int32_t h = src->shape_[0];
+ const int32_t w = src->shape_[1];
+ const int32_t channels = src->shape_[2];
+ const int32_t gridH = gs[gs.size() - 2];
+ const int32_t gridW = gs[gs.size() - 1];
+ const int32_t plane = gridH * gridW;
+ const auto *g = reinterpret_cast(grid->data_.get());
+
+ // Bilinearly sample channel `c` of the low-res grid at fractional (gx, gy).
+ auto sampleGrid = [&](int32_t c, float gx, float gy) -> float {
+ const int32_t x0 = std::clamp(static_cast(std::floor(gx)), 0, gridW - 1);
+ const int32_t y0 = std::clamp(static_cast(std::floor(gy)), 0, gridH - 1);
+ const int32_t x1 = std::min(x0 + 1, gridW - 1);
+ const int32_t y1 = std::min(y0 + 1, gridH - 1);
+ const float dx = gx - static_cast(x0);
+ const float dy = gy - static_cast(y0);
+ const int32_t base = c * plane;
+ const float top = g[base + y0 * gridW + x0] +
+ (g[base + y0 * gridW + x1] - g[base + y0 * gridW + x0]) * dx;
+ const float bot = g[base + y1 * gridW + x0] +
+ (g[base + y1 * gridW + x1] - g[base + y1 * gridW + x0]) * dx;
+ return top + (bot - top) * dy;
+ };
+
+ ::cv::Mat mapX(h, w, CV_32F);
+ ::cv::Mat mapY(h, w, CV_32F);
+ for (int32_t oy = 0; oy < h; ++oy) {
+ const float gy = h > 1 ? (static_cast(oy) / static_cast(h - 1)) *
+ static_cast(gridH - 1)
+ : 0.0f;
+ auto *rowX = mapX.ptr(oy);
+ auto *rowY = mapY.ptr(oy);
+ for (int32_t ox = 0; ox < w; ++ox) {
+ const float gx = w > 1 ? (static_cast(ox) / static_cast(w - 1)) *
+ static_cast(gridW - 1)
+ : 0.0f;
+ const float nx = sampleGrid(0, gx, gy); // [-1,1]
+ const float ny = sampleGrid(1, gx, gy);
+ rowX[ox] = ((nx + 1.0f) / 2.0f) * static_cast(w - 1);
+ rowY[ox] = ((ny + 1.0f) / 2.0f) * static_cast(h - 1);
+ }
+ }
+
+ const int cvType = CV_MAKETYPE(CV_8U, channels);
+ ::cv::Mat srcMat(h, w, cvType, src->data_.get());
+ ::cv::Mat dstMat(h, w, cvType, dst->data_.get());
+ try {
+ ::cv::remap(srcMat, dstMat, mapX, mapY, ::cv::INTER_LINEAR, ::cv::BORDER_REPLICATE);
+ } catch (const std::exception &e) {
+ throw jsi::JSError(rt, std::string("warpByGrid: OpenCV error: ") + e.what());
+ }
+ return jsi::Value(rt, args[2]);
+ };
+ module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody));
+}
+
+// ------------------------------- warpQuad ----------------------------------
+// Perspective-crop an oriented quad of `src` into the `dst` canvas (crop +
+// resize-to-height + pad/align). Used by the OCR recognizer to normalize a
+// detected text box into the fixed recognizer canvas.
+void install_warpQuad(jsi::Runtime &rt, jsi::Object &module) {
+ const auto *name = "warpQuad";
+ auto fnBody = [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
+ size_t count) -> jsi::Value {
+ if (count != 4) {
+ throw jsi::JSError(rt, "Usage: warpQuad(src, dst, quad, options)");
+ }
+ if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) {
+ throw jsi::JSError(rt, "warpQuad: src must be a Tensor");
+ }
+ if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) {
+ throw jsi::JSError(rt, "warpQuad: dst must be a Tensor");
+ }
+ if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) {
+ throw jsi::JSError(rt, "warpQuad: quad must be an array of 8 numbers");
+ }
+ if (!args[3].isObject()) {
+ throw jsi::JSError(rt, "warpQuad: options must be an object");
+ }
+ auto src = args[0].asObject(rt).getHostObject(rt);
+ auto dst = args[1].asObject(rt).getHostObject(rt);
+ if (src.get() == dst.get()) {
+ throw jsi::JSError(rt, "warpQuad: In-place operations (src == dst) are not supported.");
+ }
+ auto quadArr = args[2].asObject(rt).asArray(rt);
+ auto opts = args[3].asObject(rt);
+
+ if (quadArr.length(rt) != 8) {
+ throw jsi::JSError(rt, "warpQuad: quad must have exactly 8 numbers (4 points)");
+ }
+ if (src->shape_.size() != 3 || dst->shape_.size() != 3) {
+ throw jsi::JSError(rt, "warpQuad: src and dst must be [H,W,C]");
+ }
+ if (src->dtype_ != rnexecutorch::core::types::DType::uint8 ||
+ dst->dtype_ != rnexecutorch::core::types::DType::uint8) {
+ throw jsi::JSError(rt, "warpQuad: src and dst must be uint8");
+ }
+ if (src->shape_[2] != dst->shape_[2]) {
+ throw jsi::JSError(rt, "warpQuad: src and dst must have the same channel count");
+ }
+
+ const int32_t channels = src->shape_[2];
+ const int32_t recH = dst->shape_[0];
+ const int32_t bucketW = dst->shape_[1];
+
+ if (!opts.hasProperty(rt, "contentWidth") ||
+ !opts.getProperty(rt, "contentWidth").isNumber()) {
+ throw jsi::JSError(rt, "warpQuad: options.contentWidth is required");
+ }
+ const int32_t contentWidth =
+ std::clamp(static_cast(opts.getProperty(rt, "contentWidth").asNumber()), 1,
+ bucketW);
+ const std::string padMode = opts.getProperty(rt, "padMode").asString(rt).utf8(rt);
+ const double padValue = opts.getProperty(rt, "padValue").asNumber();
+ const std::string align = opts.getProperty(rt, "align").asString(rt).utf8(rt);
+ // offsetX >= 0 places content at that x (overriding align); clear=false skips
+ // wiping dst first, so successive warps compose into one canvas (glyph strips).
+ const auto offsetXOpt = static_cast(opts.getProperty(rt, "offsetX").asNumber());
+ const bool clear = opts.getProperty(rt, "clear").asBool();
+
+ std::array<::cv::Point2f, 4> quad;
+ for (std::size_t i = 0; i < 8; ++i) {
+ if (!quadArr.getValueAtIndex(rt, i).isNumber()) {
+ throw jsi::JSError(rt, "warpQuad: quad must contain only numbers");
+ }
+ }
+ for (std::size_t i = 0; i < 4; ++i) {
+ quad[i] = {static_cast(quadArr.getValueAtIndex(rt, i * 2).asNumber()),
+ static_cast(quadArr.getValueAtIndex(rt, i * 2 + 1).asNumber())};
+ }
+
+ std::shared_lock srcLock(src->mutex_, std::try_to_lock);
+ if (!srcLock.owns_lock()) {
+ throw jsi::JSError(rt, "warpQuad: src tensor is currently in use");
+ }
+ std::unique_lock dstLock(dst->mutex_, std::try_to_lock);
+ if (!dstLock.owns_lock()) {
+ throw jsi::JSError(rt, "warpQuad: dst tensor is currently in use");
+ }
+ if (!src->data_ || !dst->data_) {
+ throw jsi::JSError(rt, "warpQuad: a tensor has been disposed");
+ }
+
+ const int cvType = CV_MAKETYPE(CV_8U, channels);
+ ::cv::Mat srcMat(src->shape_[0], src->shape_[1], cvType, src->data_.get());
+ ::cv::Mat dstMat(recH, bucketW, cvType, dst->data_.get());
+
+ try {
+ const std::array<::cv::Point2f, 4> dstPts = {
+ ::cv::Point2f{0.0f, 0.0f},
+ {static_cast(contentWidth), 0.0f},
+ {static_cast(contentWidth), static_cast(recH)},
+ {0.0f, static_cast(recH)}};
+ const std::array<::cv::Point2f, 4> srcPts = {quad[0], quad[1], quad[2], quad[3]};
+ ::cv::Mat m = ::cv::getPerspectiveTransform(srcPts.data(), dstPts.data());
+ ::cv::Mat content;
+ ::cv::warpPerspective(srcMat, content, m, ::cv::Size(contentWidth, recH),
+ ::cv::INTER_CUBIC, ::cv::BORDER_REPLICATE);
+
+ ::cv::Scalar padColor;
+ if (padMode == "cornerMean") {
+ const int patch = std::max(1, std::min(recH, contentWidth) / 30);
+ ::cv::Scalar acc(0, 0, 0, 0);
+ const std::array<::cv::Rect, 4> rects = {
+ ::cv::Rect(0, 0, patch, patch),
+ ::cv::Rect(contentWidth - patch, 0, patch, patch),
+ ::cv::Rect(0, recH - patch, patch, patch),
+ ::cv::Rect(contentWidth - patch, recH - patch, patch, patch)};
+ for (const auto &r : rects) {
+ acc += ::cv::mean(content(r));
+ }
+ padColor = acc / 4.0;
+ } else {
+ padColor = ::cv::Scalar::all(padValue);
+ }
+
+ if (clear) {
+ dstMat.setTo(padColor);
+ }
+ int32_t offsetX = offsetXOpt;
+ if (offsetX < 0) {
+ offsetX = (align == "center") ? (bucketW - contentWidth) / 2 : 0;
+ }
+ if (offsetX < bucketW) {
+ const int32_t copyW = std::min(contentWidth, bucketW - offsetX);
+ content(::cv::Rect(0, 0, copyW, recH))
+ .copyTo(dstMat(::cv::Rect(offsetX, 0, copyW, recH)));
+ }
+ } catch (const std::exception &e) {
+ throw jsi::JSError(rt, std::string("warpQuad: OpenCV error: ") + e.what());
+ }
+ return jsi::Value(rt, args[1]);
+ };
+ module.setProperty(rt, name,
+ jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name),
+ 4, fnBody));
+}
+
} // namespace rnexecutorch::extensions::cv::image_ops
diff --git a/packages/react-native-executorch/cpp/extensions/cv/image_ops.h b/packages/react-native-executorch/cpp/extensions/cv/image_ops.h
index 893c0957e5..6a4235f3bc 100644
--- a/packages/react-native-executorch/cpp/extensions/cv/image_ops.h
+++ b/packages/react-native-executorch/cpp/extensions/cv/image_ops.h
@@ -9,4 +9,7 @@ void install_toChannelsFirst(facebook::jsi::Runtime &rt, facebook::jsi::Object &
void install_toChannelsLast(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
void install_normalize(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
void install_applyColormap(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
+void install_rotate(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
+void install_warpByGrid(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
+void install_warpQuad(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
} // namespace rnexecutorch::extensions::cv::image_ops
diff --git a/packages/react-native-executorch/cpp/extensions/cv/install.cpp b/packages/react-native-executorch/cpp/extensions/cv/install.cpp
index 0540f45c9b..ba32a96041 100644
--- a/packages/react-native-executorch/cpp/extensions/cv/install.cpp
+++ b/packages/react-native-executorch/cpp/extensions/cv/install.cpp
@@ -1,6 +1,7 @@
#include "install.h"
#include "box_ops.h"
#include "image_ops.h"
+#include "ocr_ops.h"
namespace rnexecutorch::extensions::cv {
namespace jsi = facebook::jsi;
@@ -14,10 +15,17 @@ void install(facebook::jsi::Runtime &rt, facebook::jsi::Object &module) {
image_ops::install_toChannelsLast(rt, cvModule);
image_ops::install_normalize(rt, cvModule);
image_ops::install_applyColormap(rt, cvModule);
+ image_ops::install_rotate(rt, cvModule);
+ image_ops::install_warpByGrid(rt, cvModule);
+ image_ops::install_warpQuad(rt, cvModule);
box_ops::install_nms(rt, cvModule);
box_ops::install_restrictToBox(rt, cvModule);
+ ocr_ops::install_extractCraftTextBoxes(rt, cvModule);
+ ocr_ops::install_extractDbnetTextBoxes(rt, cvModule);
+ ocr_ops::install_ctcGreedyDecode(rt, cvModule);
+
module.setProperty(rt, "cv", cvModule);
}
} // namespace rnexecutorch::extensions::cv
diff --git a/packages/react-native-executorch/cpp/extensions/cv/ocr_ops.cpp b/packages/react-native-executorch/cpp/extensions/cv/ocr_ops.cpp
new file mode 100644
index 0000000000..1868ca0365
--- /dev/null
+++ b/packages/react-native-executorch/cpp/extensions/cv/ocr_ops.cpp
@@ -0,0 +1,623 @@
+#include "ocr_ops.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "core/dtype.h"
+#include "core/tensor.h"
+
+namespace rnexecutorch::extensions::cv::ocr_ops {
+namespace jsi = facebook::jsi;
+using TensorHostObject = rnexecutorch::core::tensor::TensorHostObject;
+
+namespace {
+// ----------------------------- geometry types ------------------------------
+struct Box {
+ float x0{}, y0{}, x1{}, y1{}; // axis-aligned (p1=min, p2=max)
+ float angle = 0.0f;
+ [[nodiscard]] float width() const { return x1 - x0; }
+ [[nodiscard]] float height() const { return y1 - y0; }
+};
+
+struct Quad {
+ std::array<::cv::Point2f, 4> pts;
+ float score = 1.0f;
+};
+
+float dist(const ::cv::Point2f &a, const ::cv::Point2f &b) {
+ return std::hypot(b.x - a.x, b.y - a.y);
+}
+::cv::Point2f center(const Box &b) {
+ return {(b.x0 + b.x1) * 0.5f, (b.y0 + b.y1) * 0.5f};
+}
+float minSide(const Box &b) { return std::min(b.width(), b.height()); }
+float maxSide(const Box &b) { return std::max(b.width(), b.height()); }
+bool isClose(float a, float b, float eps = 1e-3f) { return std::fabs(a - b) < eps; }
+
+std::array<::cv::Point2f, 4> corners(const Box &b) {
+ return {::cv::Point2f{b.x0, b.y0}, {b.x1, b.y0}, {b.x1, b.y1}, {b.x0, b.y1}};
+}
+
+::cv::Point2f rotateAround(const ::cv::Point2f &p, const ::cv::Point2f &ctr, float rad) {
+ const float tx = p.x - ctr.x;
+ const float ty = p.y - ctr.y;
+ return {tx * std::cos(rad) - ty * std::sin(rad) + ctr.x,
+ tx * std::sin(rad) + ty * std::cos(rad) + ctr.y};
+}
+
+// ------------------------------ CRAFT branch -------------------------------
+void dilateComponent(::cv::Mat &segMap, const ::cv::Mat &stats, int32_t i, int32_t area,
+ int32_t imgW, int32_t imgH) {
+ const int32_t x = stats.at(i, ::cv::CC_STAT_LEFT);
+ const int32_t y = stats.at(i, ::cv::CC_STAT_TOP);
+ const int32_t w = stats.at(i, ::cv::CC_STAT_WIDTH);
+ const int32_t h = stats.at(i, ::cv::CC_STAT_HEIGHT);
+ const auto dilationRadius =
+ static_cast(std::sqrt(static_cast(area) / std::max(w, h)) * 2);
+ const int32_t sx = std::max(x - dilationRadius, 0);
+ const int32_t ex = std::min(x + w + dilationRadius, imgW);
+ const int32_t sy = std::max(y - dilationRadius, 0);
+ const int32_t ey = std::min(y + h + dilationRadius, imgH);
+ const int32_t kSize = 1 + dilationRadius;
+ ::cv::Mat kernel = ::cv::getStructuringElement(::cv::MORPH_RECT, ::cv::Size(kSize, kSize));
+ ::cv::Mat roi = segMap(::cv::Rect(sx, sy, ex - sx, ey - sy));
+ ::cv::dilate(roi, roi, kernel, ::cv::Point(-1, -1), 1);
+}
+
+std::optional boxFromComponent(const ::cv::Mat &textMap, const ::cv::Mat &labels,
+ const ::cv::Mat &stats, int32_t i, int32_t imgW, int32_t imgH,
+ float lowTextThreshold) {
+ const int32_t area = stats.at(i, ::cv::CC_STAT_AREA);
+ if (area < 10) {
+ return std::nullopt;
+ }
+ ::cv::Mat mask = (labels == i);
+ double maxVal = 0.0;
+ ::cv::minMaxLoc(textMap, nullptr, &maxVal, nullptr, nullptr, mask);
+ if (maxVal < static_cast(lowTextThreshold)) {
+ return std::nullopt;
+ }
+ ::cv::Mat segMap = ::cv::Mat::zeros(textMap.size(), CV_8U);
+ segMap.setTo(255, mask);
+ dilateComponent(segMap, stats, i, area, imgW, imgH);
+
+ std::vector> contours;
+ ::cv::findContours(segMap, contours, ::cv::RETR_EXTERNAL, ::cv::CHAIN_APPROX_SIMPLE);
+ if (contours.empty()) {
+ return std::nullopt;
+ }
+ ::cv::RotatedRect rr = ::cv::minAreaRect(contours[0]);
+ std::array<::cv::Point2f, 4> v;
+ rr.points(v.data());
+ Box box;
+ box.x0 = std::min({v[0].x, v[1].x, v[2].x, v[3].x});
+ box.y0 = std::min({v[0].y, v[1].y, v[2].y, v[3].y});
+ box.x1 = std::max({v[0].x, v[1].x, v[2].x, v[3].x});
+ box.y1 = std::max({v[0].y, v[1].y, v[2].y, v[3].y});
+ box.angle = rr.angle;
+ return box;
+}
+
+// CRAFT text+affinity maps -> component boxes, in two modes:
+// - line grouping (charLevel=false): affinity is ADDED to the text map so
+// adjacent glyphs link into one region; boxes keep their rotated-rect angle.
+// - char level (charLevel=true): affinity is SUBTRACTED to BREAK those links,
+// and the mask is eroded/dilated to clean up, yielding one upright box per
+// glyph (used by the per-column pass that reads stacked text glyph by glyph;
+// mirrors the old VerticalDetector's single-character path).
+// Everything after the combine step (binarize -> connected components -> one box
+// per component) is shared. charLevel boxes are forced upright (angle 0).
+std::vector componentBoxes(::cv::Mat &textMap, ::cv::Mat &affinityMap, float textThreshold,
+ float linkThreshold, float lowTextThreshold, bool charLevel) {
+ const int32_t imgH = textMap.rows;
+ const int32_t imgW = textMap.cols;
+ ::cv::Mat textScore;
+ ::cv::Mat affinityScore;
+ ::cv::threshold(textMap, textScore, static_cast(textThreshold), 1.0, ::cv::THRESH_BINARY);
+ ::cv::threshold(affinityMap, affinityScore, static_cast(linkThreshold), 1.0,
+ ::cv::THRESH_BINARY);
+
+ ::cv::Mat comb;
+ if (charLevel) {
+ comb = textScore - affinityScore; // subtract to separate adjacent glyphs
+ ::cv::threshold(comb, comb, 0.0, 1.0, ::cv::THRESH_TOZERO);
+ ::cv::threshold(comb, comb, 1.0, 1.0, ::cv::THRESH_TRUNC);
+ ::cv::Mat kernel = ::cv::getStructuringElement(::cv::MORPH_RECT, ::cv::Size(3, 3));
+ ::cv::erode(comb, comb, kernel, ::cv::Point(-1, -1), 1);
+ ::cv::dilate(comb, comb, kernel, ::cv::Point(-1, -1), 4);
+ } else {
+ comb = textScore + affinityScore; // add to link adjacent glyphs into lines
+ ::cv::threshold(comb, comb, 0.0, 1.0, ::cv::THRESH_BINARY);
+ }
+
+ ::cv::Mat binary;
+ comb.convertTo(binary, CV_8UC1);
+ ::cv::Mat labels;
+ ::cv::Mat stats;
+ ::cv::Mat centroids;
+ const int32_t nLabels = ::cv::connectedComponentsWithStats(binary, labels, stats, centroids, 4);
+
+ std::vector boxes;
+ boxes.reserve(static_cast(nLabels));
+ for (int32_t i = 1; i < nLabels; ++i) {
+ auto box = boxFromComponent(textMap, labels, stats, i, imgW, imgH, lowTextThreshold);
+ if (box) {
+ if (charLevel) {
+ box->angle = 0.0f; // glyphs are read upright, never rotated
+ }
+ boxes.push_back(*box);
+ }
+ }
+ return boxes;
+}
+
+// fit a line to the two shortest sides' midpoints; returns slope, intercept, vertical?
+std::tuple fitLineToShortestSides(const Box &b, float verticalThreshold) {
+ const auto pts = corners(b);
+ std::array, 4> sides;
+ std::array<::cv::Point2f, 4> mids;
+ for (int i = 0; i < 4; ++i) {
+ const auto &p1 = pts[static_cast(i)];
+ const auto &p2 = pts[static_cast((i + 1) % 4)];
+ sides[static_cast(i)] = {dist(p1, p2), i};
+ mids[static_cast(i)] = {(p1.x + p2.x) * 0.5f, (p1.y + p2.y) * 0.5f};
+ }
+ std::ranges::sort(sides);
+ ::cv::Point2f m1 = mids[static_cast(sides[0].second)];
+ ::cv::Point2f m2 = mids[static_cast(sides[1].second)];
+ const bool isVertical = std::fabs(m2.x - m1.x) < verticalThreshold;
+ std::vector<::cv::Point2f> fitPts = {m1, m2};
+ if (isVertical) {
+ for (auto &p : fitPts) {
+ std::swap(p.x, p.y);
+ }
+ }
+ ::cv::Vec4f line;
+ ::cv::fitLine(fitPts, line, ::cv::DIST_L2, 0, 0.01, 0.01);
+ const float m = line[1] / line[0];
+ const float c = line[3] - m * line[2];
+ return {m, c, isVertical};
+}
+
+Box rotateBox(const Box &b, float angleDeg) {
+ const ::cv::Point2f ctr = center(b);
+ const float rad = angleDeg * std::numbers::pi_v / 180.0f;
+ float minX = std::numeric_limits::max();
+ float minY = std::numeric_limits::max();
+ float maxX = std::numeric_limits::lowest();
+ float maxY = std::numeric_limits::lowest();
+ for (const auto &p : corners(b)) {
+ const ::cv::Point2f r = rotateAround(p, ctr, rad);
+ minX = std::min(minX, r.x);
+ minY = std::min(minY, r.y);
+ maxX = std::max(maxX, r.x);
+ maxY = std::max(maxY, r.y);
+ }
+ return {.x0 = minX, .y0 = minY, .x1 = maxX, .y1 = maxY, .angle = b.angle};
+}
+
+float minDistanceBetween(const Box &a, const Box &b) {
+ float md = std::numeric_limits::max();
+ for (const auto &c1 : corners(a)) {
+ for (const auto &c2 : corners(b)) {
+ md = std::min(md, dist(c1, c2));
+ }
+ }
+ return md;
+}
+
+std::optional>
+findClosestBox(const std::vector &boxes, const std::unordered_set &ignored,
+ const Box ¤t, bool isVertical, float m, float c, float centerThreshold) {
+ float smallest = std::numeric_limits::max();
+ std::ptrdiff_t idx = -1;
+ float boxHeight = 0.0f;
+ const ::cv::Point2f cc = center(current);
+ for (std::size_t i = 0; i < boxes.size(); ++i) {
+ if (ignored.contains(i)) {
+ continue;
+ }
+ const ::cv::Point2f pc = center(boxes[i]);
+ const float d = dist(cc, pc);
+ if (d >= smallest) {
+ continue;
+ }
+ const float h = minSide(boxes[i]);
+ const float lineDistance =
+ isVertical ? std::fabs(pc.x - (m * pc.y + c)) : std::fabs(pc.y - (m * pc.x + c));
+ if (lineDistance < h * centerThreshold) {
+ idx = static_cast(i);
+ smallest = d;
+ boxHeight = h;
+ }
+ }
+ if (idx == -1) {
+ return std::nullopt;
+ }
+ return std::make_pair(static_cast(idx), boxHeight);
+}
+
+Box mergeBoxes(const Box &a, const Box &b) {
+ return {.x0 = std::min(a.x0, b.x0), .y0 = std::min(a.y0, b.y0), .x1 = std::max(a.x1, b.x1), .y1 = std::max(a.y1, b.y1), .angle = a.angle};
+}
+
+// CRAFT box grouping -> reading-ordered text lines.
+std::vector groupTextBoxes(std::vector boxes, float centerThreshold,
+ float distanceThreshold, float heightThreshold,
+ float minSideThreshold, float maxSideThreshold,
+ float verticalThreshold) {
+ std::ranges::sort(boxes,
+ [](const Box &a, const Box &b) { return maxSide(a) > maxSide(b); });
+
+ std::vector merged;
+ std::unordered_set ignored;
+ while (!boxes.empty()) {
+ Box current = boxes.front();
+ const float normalizedAngle = (current.angle > 45.0f) ? current.angle - 90.0f : current.angle;
+ boxes.erase(boxes.begin());
+ ignored.clear();
+ float lineAngle = 0.0f;
+
+ while (true) {
+ auto [slope, intercept, isVertical] = fitLineToShortestSides(current, verticalThreshold);
+ lineAngle = isVertical ? -90.0f : std::atan(slope) * 180.0f / std::numbers::pi_v;
+ auto closest =
+ findClosestBox(boxes, ignored, current, isVertical, slope, intercept, centerThreshold);
+ if (!closest) {
+ break;
+ }
+ const auto [candIdx, candHeight] = *closest;
+ Box candidate = boxes[candIdx];
+ if ((isClose(candidate.angle, 90.0f) && !isVertical) ||
+ (isClose(candidate.angle, 0.0f) && isVertical)) {
+ candidate = rotateBox(candidate, normalizedAngle);
+ }
+ const float md = minDistanceBetween(candidate, current);
+ const float mergedHeight = minSide(current);
+ if (md < distanceThreshold * candHeight &&
+ std::fabs(mergedHeight - candHeight) < candHeight * heightThreshold) {
+ current = mergeBoxes(current, candidate);
+ boxes.erase(boxes.begin() + static_cast(candIdx));
+ ignored.clear();
+ } else {
+ ignored.insert(candIdx);
+ }
+ }
+ current.angle = lineAngle;
+ merged.push_back(current);
+ }
+
+ // Remove small boxes. Output order is unspecified — the TypeScript pipeline
+ // derives reading order geometrically for every result set.
+ std::vector filtered;
+ for (const auto &b : merged) {
+ if (minSide(b) > minSideThreshold && maxSide(b) > maxSideThreshold) {
+ filtered.push_back(b);
+ }
+ }
+ return filtered;
+}
+
+// CRAFT half-res heatmap (text+affinity interleaved) -> oriented quads in
+// detector-input pixels; restoreRatio scales the half-res boxes back up. With
+// charLevel the boxes are individual upright glyphs (no grouping); otherwise
+// they are grouped reading-ordered lines. `data` points at heatW*heatH*2 floats.
+std::vector extractCraft(float *data, int32_t heatW, int32_t heatH, float textThreshold,
+ float linkThreshold, float lowTextThreshold, float restoreRatio,
+ bool charLevel) {
+ // Deinterleave the [text, affinity] channels of the half-res heatmap.
+ ::cv::Mat interleaved(heatH, heatW, CV_32FC2, data);
+ std::array<::cv::Mat, 2> channels;
+ ::cv::split(interleaved, channels);
+ std::vector boxes = componentBoxes(channels[0], channels[1], textThreshold, linkThreshold,
+ lowTextThreshold, charLevel);
+ for (auto &b : boxes) {
+ b.x0 *= restoreRatio;
+ b.y0 *= restoreRatio;
+ b.x1 *= restoreRatio;
+ b.y1 *= restoreRatio;
+ }
+ if (!charLevel) {
+ // Grouping constants in detector-input space. Lines are merged without a width
+ // cap; the recognizer reads each line whole (snapping to its widest bucket).
+ boxes = groupTextBoxes(boxes, /*center*/ 0.5f, /*distance*/ 2.0f, /*height*/ 2.0f,
+ /*minSide*/ 15.0f, /*maxSide*/ 30.0f,
+ /*verticalThreshold*/ 20.0f);
+ }
+
+ std::vector quads;
+ quads.reserve(boxes.size());
+ for (const auto &b : boxes) {
+ Quad q;
+ q.score = 1.0f;
+ // De-skew near-horizontal lines by rotating the AABB corners about the
+ // center. A near-vertical line (angle ~ -90, from a tall/stacked region)
+ // is NOT flipped flat — that would lay an upright column on its side and
+ // misplace the box; keep it as an upright tall AABB so the column reader
+ // can take it.
+ const ::cv::Point2f ctr = center(b);
+ const float rad =
+ (std::fabs(b.angle) > 45.0f) ? 0.0f : b.angle * std::numbers::pi_v / 180.0f;
+ const auto cs = corners(b);
+ for (std::size_t i = 0; i < 4; ++i) {
+ q.pts[i] = rotateAround(cs[i], ctr, rad);
+ }
+ quads.push_back(q);
+ }
+ return quads;
+}
+
+// ------------------------------ DBNet branch -------------------------------
+// DBNet prob map [H,W] -> oriented quads. The map must be post-sigmoid
+// probabilities — any activation is baked into the model's export.
+std::vector extractDbnet(const ::cv::Mat &prob, float binThreshold, float boxThreshold,
+ float unclipRatio, int32_t minBoxSide, int32_t maxCandidates) {
+ const int32_t w = prob.cols;
+ const int32_t h = prob.rows;
+
+ ::cv::Mat bitmap;
+ ::cv::threshold(prob, bitmap, static_cast(binThreshold), 255, ::cv::THRESH_BINARY);
+ bitmap.convertTo(bitmap, CV_8UC1);
+
+ std::vector> contours;
+ ::cv::findContours(bitmap, contours, ::cv::RETR_LIST, ::cv::CHAIN_APPROX_SIMPLE);
+
+ std::vector quads;
+ const int32_t maxN = static_cast(
+ std::min(contours.size(), static_cast(maxCandidates)));
+ for (int32_t i = 0; i < maxN; ++i) {
+ const auto &contour = contours[static_cast(i)];
+ if (contour.size() < 4) {
+ continue;
+ }
+ ::cv::RotatedRect rr = ::cv::minAreaRect(contour);
+ if (std::min(rr.size.width, rr.size.height) < static_cast(minBoxSide)) {
+ continue;
+ }
+ ::cv::Mat mask = ::cv::Mat::zeros(prob.size(), CV_8UC1);
+ ::cv::drawContours(mask, contours, i, ::cv::Scalar(255), ::cv::FILLED);
+ const float score = static_cast(::cv::mean(prob, mask)[0]);
+ if (score < boxThreshold) {
+ continue;
+ }
+ const double area = static_cast(rr.size.width) * static_cast(rr.size.height);
+ const double perim =
+ 2.0 * (static_cast(rr.size.width) + static_cast(rr.size.height));
+ const double distance = perim > 0.0 ? area * static_cast(unclipRatio) / perim : 0.0;
+ const auto grow = static_cast(2.0 * distance);
+ ::cv::RotatedRect expanded(rr.center,
+ ::cv::Size2f(rr.size.width + grow, rr.size.height + grow),
+ rr.angle);
+ if (std::min(expanded.size.width, expanded.size.height) <
+ static_cast(minBoxSide + 2)) {
+ continue;
+ }
+ std::array<::cv::Point2f, 4> c;
+ expanded.points(c.data());
+ Quad q;
+ q.score = score;
+ auto minX = static_cast(w);
+ auto minY = static_cast(h);
+ float maxX = 0;
+ float maxY = 0;
+ for (int32_t k = 0; k < 4; ++k) {
+ const float px = std::clamp(c[static_cast(k)].x, 0.0f, static_cast(w));
+ const float py = std::clamp(c[static_cast(k)].y, 0.0f, static_cast(h));
+ q.pts[static_cast(k)] = {px, py};
+ minX = std::min(minX, px);
+ minY = std::min(minY, py);
+ maxX = std::max(maxX, px);
+ maxY = std::max(maxY, py);
+ }
+ if (maxX - minX < 1.0f || maxY - minY < 1.0f) {
+ continue;
+ }
+ quads.push_back(q);
+ }
+ // Output order is unspecified — the TypeScript pipeline derives reading
+ // order geometrically for every result set.
+ return quads;
+}
+
+// Flatten quads to a JS double array, 9 per box (x0,y0..x3,y3,score).
+jsi::Array quadsToArray(jsi::Runtime &rt, const std::vector &quads) {
+ jsi::Array out(rt, quads.size() * 9);
+ size_t idx = 0;
+ for (const auto &q : quads) {
+ for (std::size_t k = 0; k < 4; ++k) {
+ out.setValueAtIndex(rt, idx++, jsi::Value(static_cast(q.pts[k].x)));
+ out.setValueAtIndex(rt, idx++, jsi::Value(static_cast(q.pts[k].y)));
+ }
+ out.setValueAtIndex(rt, idx++, jsi::Value(static_cast(q.score)));
+ }
+ return out;
+}
+
+} // namespace
+
+void install_extractCraftTextBoxes(jsi::Runtime &rt, jsi::Object &module) {
+ const auto *name = "extractCraftTextBoxes";
+ auto fnBody = [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
+ size_t count) -> jsi::Value {
+ if (count != 2) {
+ throw jsi::JSError(rt, "Usage: extractCraftTextBoxes(src, options)");
+ }
+ if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) {
+ throw jsi::JSError(rt, "extractCraftTextBoxes: src must be a Tensor");
+ }
+ if (!args[1].isObject()) {
+ throw jsi::JSError(rt, "extractCraftTextBoxes: options must be an object");
+ }
+ auto src = args[0].asObject(rt).getHostObject(rt);
+ auto opts = args[1].asObject(rt);
+ if (src->dtype_ != rnexecutorch::core::types::DType::float32) {
+ throw jsi::JSError(rt, "extractCraftTextBoxes: src must be a float32 Tensor");
+ }
+
+ std::shared_lock srcLock(src->mutex_, std::try_to_lock);
+ if (!srcLock.owns_lock()) {
+ throw jsi::JSError(rt, "extractCraftTextBoxes: src tensor is currently in use");
+ }
+ if (!src->data_) {
+ throw jsi::JSError(rt, "extractCraftTextBoxes: src tensor has been disposed");
+ }
+ auto *dataPtr = reinterpret_cast(src->data_.get());
+
+ // src is [1,Hd,Wd,2] or [Hd,Wd,2] interleaved (text, affinity), half-res.
+ const auto &s = src->shape_;
+ if (s.size() < 3 || s.back() != 2) {
+ throw jsi::JSError(rt, "extractCraftTextBoxes: src must be [..,Hd,Wd,2]");
+ }
+ const int32_t heatW = s[s.size() - 2];
+ const int32_t heatH = s[s.size() - 3];
+ const double targetH = opts.getProperty(rt, "targetHeight").asNumber();
+ const float restoreRatio = static_cast(targetH) / static_cast(heatH);
+ // Required option — default values live in the TypeScript wrapper layer.
+ const bool charLevel = opts.getProperty(rt, "charLevel").asBool();
+
+ std::vector quads;
+ try {
+ quads = extractCraft(
+ dataPtr, heatW, heatH,
+ static_cast(opts.getProperty(rt, "textThreshold").asNumber()),
+ static_cast(opts.getProperty(rt, "linkThreshold").asNumber()),
+ static_cast(opts.getProperty(rt, "lowTextThreshold").asNumber()),
+ restoreRatio, charLevel);
+ } catch (const std::exception &e) {
+ throw jsi::JSError(rt, std::string("extractCraftTextBoxes: OpenCV error: ") + e.what());
+ }
+ return quadsToArray(rt, quads);
+ };
+ module.setProperty(rt, name,
+ jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name),
+ 2, fnBody));
+}
+
+void install_extractDbnetTextBoxes(jsi::Runtime &rt, jsi::Object &module) {
+ const auto *name = "extractDbnetTextBoxes";
+ auto fnBody = [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
+ size_t count) -> jsi::Value {
+ if (count != 2) {
+ throw jsi::JSError(rt, "Usage: extractDbnetTextBoxes(src, options)");
+ }
+ if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) {
+ throw jsi::JSError(rt, "extractDbnetTextBoxes: src must be a Tensor");
+ }
+ if (!args[1].isObject()) {
+ throw jsi::JSError(rt, "extractDbnetTextBoxes: options must be an object");
+ }
+ auto src = args[0].asObject(rt).getHostObject(rt);
+ auto opts = args[1].asObject(rt);
+ if (src->dtype_ != rnexecutorch::core::types::DType::float32) {
+ throw jsi::JSError(rt, "extractDbnetTextBoxes: src must be a float32 Tensor");
+ }
+
+ std::shared_lock srcLock(src->mutex_, std::try_to_lock);
+ if (!srcLock.owns_lock()) {
+ throw jsi::JSError(rt, "extractDbnetTextBoxes: src tensor is currently in use");
+ }
+ if (!src->data_) {
+ throw jsi::JSError(rt, "extractDbnetTextBoxes: src tensor has been disposed");
+ }
+ auto *dataPtr = reinterpret_cast(src->data_.get());
+
+ // src is [1,1,H,W] or [H,W] probability map (full-res).
+ const auto &s = src->shape_;
+ if (s.size() < 2) {
+ throw jsi::JSError(rt, "extractDbnetTextBoxes: src must be [..,H,W]");
+ }
+ const int32_t w = s[s.size() - 1];
+ const int32_t h = s[s.size() - 2];
+
+ std::vector quads;
+ try {
+ ::cv::Mat prob(h, w, CV_32F, dataPtr);
+ quads = extractDbnet(
+ prob, static_cast(opts.getProperty(rt, "binThreshold").asNumber()),
+ static_cast(opts.getProperty(rt, "boxThreshold").asNumber()),
+ static_cast(opts.getProperty(rt, "unclipRatio").asNumber()),
+ static_cast(opts.getProperty(rt, "minBoxSide").asNumber()),
+ static_cast(opts.getProperty(rt, "maxCandidates").asNumber()));
+ } catch (const std::exception &e) {
+ throw jsi::JSError(rt, std::string("extractDbnetTextBoxes: OpenCV error: ") + e.what());
+ }
+ return quadsToArray(rt, quads);
+ };
+ module.setProperty(rt, name,
+ jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name),
+ 2, fnBody));
+}
+
+// --------------------------- ctcGreedyDecode -------------------------------
+// Per-timestep argmax + max value over [..,T,V] logits. `values` are the raw
+// max activations; if a caller needs probabilities it softmaxes the tensor (via
+// the math.softmax op) before decoding — this op takes no options.
+void install_ctcGreedyDecode(jsi::Runtime &rt, jsi::Object &module) {
+ const auto *name = "ctcGreedyDecode";
+ auto fnBody = [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args,
+ size_t count) -> jsi::Value {
+ if (count != 1) {
+ throw jsi::JSError(rt, "Usage: ctcGreedyDecode(src)");
+ }
+ if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) {
+ throw jsi::JSError(rt, "ctcGreedyDecode: src must be a Tensor");
+ }
+ auto src = args[0].asObject(rt).getHostObject(rt);
+
+ std::shared_lock srcLock(src->mutex_, std::try_to_lock);
+ if (!srcLock.owns_lock()) {
+ throw jsi::JSError(rt, "ctcGreedyDecode: src tensor is currently in use");
+ }
+ if (!src->data_) {
+ throw jsi::JSError(rt, "ctcGreedyDecode: src tensor has been disposed");
+ }
+
+ if (src->dtype_ != rnexecutorch::core::types::DType::float32) {
+ throw jsi::JSError(rt, "ctcGreedyDecode: src must be a float32 Tensor");
+ }
+ const auto &s = src->shape_;
+ if (s.size() < 2) {
+ throw jsi::JSError(rt, "ctcGreedyDecode: src must be at least 2-D [..,T,V]");
+ }
+ const int32_t vocab = s.back();
+ if (vocab < 1) {
+ throw jsi::JSError(rt, "ctcGreedyDecode: vocab dimension must be >= 1");
+ }
+ if (src->numel_ % static_cast(vocab) != 0) {
+ throw jsi::JSError(rt, "ctcGreedyDecode: numel must be a multiple of the vocab dim");
+ }
+ const int32_t timesteps = static_cast(src->numel_) / vocab;
+ const auto *data = reinterpret_cast(src->data_.get());
+
+ jsi::Array out(rt, static_cast(timesteps) * 2);
+ size_t oi = 0;
+ for (int32_t t = 0; t < timesteps; ++t) {
+ const float *row = data + static_cast(t) * static_cast(vocab);
+ const float *maxIt = std::max_element(row, row + vocab);
+ const auto maxIdx = static_cast(maxIt - row);
+ out.setValueAtIndex(rt, oi++, jsi::Value(static_cast(maxIdx)));
+ out.setValueAtIndex(rt, oi++, jsi::Value(static_cast(*maxIt)));
+ }
+ return out;
+ };
+ module.setProperty(rt, name,
+ jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name),
+ 1, fnBody));
+}
+
+} // namespace rnexecutorch::extensions::cv::ocr_ops
diff --git a/packages/react-native-executorch/cpp/extensions/cv/ocr_ops.h b/packages/react-native-executorch/cpp/extensions/cv/ocr_ops.h
new file mode 100644
index 0000000000..1e47193bb0
--- /dev/null
+++ b/packages/react-native-executorch/cpp/extensions/cv/ocr_ops.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include
+
+namespace rnexecutorch::extensions::cv::ocr_ops {
+void install_extractCraftTextBoxes(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
+void install_extractDbnetTextBoxes(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
+void install_ctcGreedyDecode(facebook::jsi::Runtime &rt, facebook::jsi::Object &module);
+} // namespace rnexecutorch::extensions::cv::ocr_ops
diff --git a/packages/react-native-executorch/cpp/extensions/cv/utils.h b/packages/react-native-executorch/cpp/extensions/cv/utils.h
index a29961d0b6..bb6a42ecac 100644
--- a/packages/react-native-executorch/cpp/extensions/cv/utils.h
+++ b/packages/react-native-executorch/cpp/extensions/cv/utils.h
@@ -1,8 +1,10 @@
#pragma once
#include "core/dtype.h"
+#include
#include
#include
+#include
namespace rnexecutorch::extensions::cv {
diff --git a/packages/react-native-executorch/src/constants.ts b/packages/react-native-executorch/src/constants.ts
index 7eafc57abf..eda8ac9769 100644
--- a/packages/react-native-executorch/src/constants.ts
+++ b/packages/react-native-executorch/src/constants.ts
@@ -1301,3 +1301,100 @@ export type BlazeFaceLandmark = (typeof BLAZEFACE_LANDMARKS)[number];
* @category Types
*/
export type CocoLandmark = (typeof COCO_LANDMARKS)[number];
+export { alphabets, symbols, PPOCR_SYMBOLS } from './extensions/cv/tasks/ocr/charsets';
+export type { OcrLanguage } from './extensions/cv/tasks/ocr/charsets';
+
+/**
+ * PP-DocLayoutV3 region classes, in model output order (index = class id).
+ * @category Constants
+ */
+export const DOC_LAYOUT_LABELS = [
+ 'abstract',
+ 'algorithm',
+ 'aside_text',
+ 'chart',
+ 'content',
+ 'formula',
+ 'doc_title',
+ 'figure_title',
+ 'footer',
+ 'footer',
+ 'footnote',
+ 'formula_number',
+ 'header',
+ 'header',
+ 'image',
+ 'formula',
+ 'number',
+ 'paragraph_title',
+ 'reference',
+ 'reference_content',
+ 'seal',
+ 'table',
+ 'text',
+ 'text',
+ 'vision_footnote',
+] as const;
+
+/**
+ * Type representing a valid PP-DocLayout region class string.
+ * @category Types
+ */
+export type DocLayoutLabel = (typeof DOC_LAYOUT_LABELS)[number];
+
+/**
+ * SLANet_plus table-structure token vocabulary (50 tokens; index = token id).
+ * @category Constants
+ */
+export const SLANET_STRUCTURE_VOCAB = [
+ 'sos',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ' ',
+ '',
+ ' | ',
+ ' colspan="2"',
+ ' colspan="3"',
+ ' colspan="4"',
+ ' colspan="5"',
+ ' colspan="6"',
+ ' colspan="7"',
+ ' colspan="8"',
+ ' colspan="9"',
+ ' colspan="10"',
+ ' colspan="11"',
+ ' colspan="12"',
+ ' colspan="13"',
+ ' colspan="14"',
+ ' colspan="15"',
+ ' colspan="16"',
+ ' colspan="17"',
+ ' colspan="18"',
+ ' colspan="19"',
+ ' colspan="20"',
+ ' rowspan="2"',
+ ' rowspan="3"',
+ ' rowspan="4"',
+ ' rowspan="5"',
+ ' rowspan="6"',
+ ' rowspan="7"',
+ ' rowspan="8"',
+ ' rowspan="9"',
+ ' rowspan="10"',
+ ' rowspan="11"',
+ ' rowspan="12"',
+ ' rowspan="13"',
+ ' rowspan="14"',
+ ' rowspan="15"',
+ ' rowspan="16"',
+ ' rowspan="17"',
+ ' rowspan="18"',
+ ' rowspan="19"',
+ ' rowspan="20"',
+ ' | ',
+ 'eos',
+] as const;
diff --git a/packages/react-native-executorch/src/core/model.ts b/packages/react-native-executorch/src/core/model.ts
index e539afecdd..3b5b133476 100644
--- a/packages/react-native-executorch/src/core/model.ts
+++ b/packages/react-native-executorch/src/core/model.ts
@@ -119,6 +119,16 @@ export interface Model {
*/
execute(methodName: string, inputs: ModelInput[], outputTensors: Tensor[]): ModelOutput[];
+ /**
+ * Unloads a single previously-executed method, freeing its memory-planned
+ * activation arena (and, on graph-compiling backends like CoreML, its
+ * compiled graph). The method reloads on its next `execute`.
+ * @param methodName The exported method to unload.
+ * @returns `true` if a loaded method was freed, `false` if it was not loaded
+ * (a harmless no-op).
+ */
+ unloadMethod(methodName: string): boolean;
+
/**
* Releases the native ExecuTorch model and frees all associated resources.
*
diff --git a/packages/react-native-executorch/src/extensions/cv/ops/boxes.ts b/packages/react-native-executorch/src/extensions/cv/ops/boxes.ts
index 8360472980..0ce289be00 100644
--- a/packages/react-native-executorch/src/extensions/cv/ops/boxes.ts
+++ b/packages/react-native-executorch/src/extensions/cv/ops/boxes.ts
@@ -73,23 +73,8 @@ export function scaleBox(
}
): BoundingBox {
'worklet';
- const { from, to, resizeMode } = opts;
-
- let scaleX: number;
- let scaleY: number;
- switch (resizeMode) {
- case 'letterbox': {
- const scale = Math.min(from.width / to.width, from.height / to.height);
- scaleX = scale;
- scaleY = scale;
- break;
- }
- case 'stretch':
- scaleX = from.width / to.width;
- scaleY = from.height / to.height;
- break;
- }
-
+ // Both resize maps are affine, so a span scales exactly as the difference of
+ // two mapped points.
switch (box.format) {
case 'xyxy': {
const pMin = scalePoint({ x: box.xmin, y: box.ymin }, opts);
@@ -104,22 +89,24 @@ export function scaleBox(
}
case 'xywh': {
const pMin = scalePoint({ x: box.xmin, y: box.ymin }, opts);
+ const pFar = scalePoint({ x: box.xmin + box.w, y: box.ymin + box.h }, opts);
return {
format: 'xywh',
xmin: pMin.x,
ymin: pMin.y,
- w: box.w / scaleX,
- h: box.h / scaleY,
+ w: pFar.x - pMin.x,
+ h: pFar.y - pMin.y,
} as BoundingBox;
}
case 'cxcywh': {
const pCenter = scalePoint({ x: box.cx, y: box.cy }, opts);
+ const pFar = scalePoint({ x: box.cx + box.w, y: box.cy + box.h }, opts);
return {
format: 'cxcywh',
cx: pCenter.x,
cy: pCenter.y,
- w: box.w / scaleX,
- h: box.h / scaleY,
+ w: pFar.x - pCenter.x,
+ h: pFar.y - pCenter.y,
} as BoundingBox;
}
}
diff --git a/packages/react-native-executorch/src/extensions/cv/ops/image.ts b/packages/react-native-executorch/src/extensions/cv/ops/image.ts
index 1d98b73c41..1a6cd1ce7f 100644
--- a/packages/react-native-executorch/src/extensions/cv/ops/image.ts
+++ b/packages/react-native-executorch/src/extensions/cv/ops/image.ts
@@ -209,3 +209,75 @@ export function applyColormap(
'worklet';
return rnexecutorchJsi.cv.applyColormap(src, dst, colormap);
}
+
+/**
+ * Rotates `src` clockwise by `degCW` degrees (90, 180, or 270) into the
+ * pre-allocated `dst`. A 90/270 rotation swaps width and height, so `dst` must be
+ * sized with `src`'s height and width transposed.
+ * @category Typescript API
+ * @param src The source image tensor (HWC).
+ * @param dst The destination tensor, pre-sized for the rotation.
+ * @param degCW The clockwise rotation in degrees: 90, 180, or 270.
+ * @returns The destination tensor `dst`.
+ */
+export function rotate(src: Tensor, dst: Tensor, degCW: number): Tensor {
+ 'worklet';
+ return rnexecutorchJsi.cv.rotate(src, dst, degCW);
+}
+
+/**
+ * Options for {@link warpQuad}. `contentWidth` is the warped content's width (px)
+ * in the canvas; `align` (`'left'`/`'center'`, default `'left'`) with `padMode`
+ * (`'constant'`/`'cornerMean'`, default `'constant'`) and `padValue` (default `0`)
+ * place and fill it. `offsetX` (default `-1` = use `align`) pins the content at an
+ * exact x, and `clear` (default `true`) wipes the canvas first — pass an explicit
+ * `offsetX` with `clear: false` to compose successive warps side-by-side into one
+ * canvas (e.g. a glyph strip).
+ * @category Types
+ */
+export type WarpQuadOptions = {
+ readonly contentWidth: number;
+ readonly align?: 'left' | 'center';
+ readonly padMode?: 'constant' | 'cornerMean';
+ readonly padValue?: number;
+ readonly offsetX?: number;
+ readonly clear?: boolean;
+};
+
+/**
+ * Perspective-crops an oriented quad region of `src` into the pre-allocated canvas
+ * `dst`, folding crop + resize-to-height + pad into one native pass.
+ * @category Typescript API
+ * @param src The source image tensor in HWC uint8 layout, shape `[H, W, C]`.
+ * @param dst The pre-allocated destination canvas in HWC uint8 layout.
+ * @param quad Eight numbers `[x0,y0,..,x3,y3]` (TL,TR,BR,BL) in `src` pixels.
+ * @param opts Content width, alignment, and padding configuration.
+ * @returns The destination tensor `dst`.
+ */
+export function warpQuad(src: Tensor, dst: Tensor, quad: number[], opts: WarpQuadOptions): Tensor {
+ 'worklet';
+ return rnexecutorchJsi.cv.warpQuad(src, dst, quad, {
+ contentWidth: opts.contentWidth,
+ align: opts.align ?? 'left',
+ padMode: opts.padMode ?? 'constant',
+ padValue: opts.padValue ?? 0,
+ offsetX: opts.offsetX ?? -1,
+ clear: opts.clear ?? true,
+ });
+}
+
+/**
+ * Warps `src` through a backward sampling field (a `torch.grid_sample`-style remap
+ * — the grid gives, per output pixel, where to read from in `src`) into the
+ * pre-allocated `dst`, natively via `cv::remap`.
+ * @category Typescript API
+ * @param src The source image tensor in HWC uint8 layout, shape `[H, W, C]`.
+ * @param grid The sampling field tensor (float32), shape `[..,2,gH,gW]`, channel
+ * 0 = x and 1 = y, normalized to `[-1, 1]` with `align_corners=true`.
+ * @param dst The pre-allocated destination tensor, same shape/dtype as `src`.
+ * @returns The destination tensor `dst`.
+ */
+export function warpByGrid(src: Tensor, grid: Tensor, dst: Tensor): Tensor {
+ 'worklet';
+ return rnexecutorchJsi.cv.warpByGrid(src, grid, dst);
+}
diff --git a/packages/react-native-executorch/src/extensions/cv/ops/index.ts b/packages/react-native-executorch/src/extensions/cv/ops/index.ts
index 84a274101d..4128d25527 100644
--- a/packages/react-native-executorch/src/extensions/cv/ops/index.ts
+++ b/packages/react-native-executorch/src/extensions/cv/ops/index.ts
@@ -1,3 +1,4 @@
export * as image from './image';
export * as boxes from './boxes';
export * as points from './points';
+export * as quad from './quad';
diff --git a/packages/react-native-executorch/src/extensions/cv/ops/points.ts b/packages/react-native-executorch/src/extensions/cv/ops/points.ts
index 4464061e18..fa803dd4a2 100644
--- a/packages/react-native-executorch/src/extensions/cv/ops/points.ts
+++ b/packages/react-native-executorch/src/extensions/cv/ops/points.ts
@@ -10,8 +10,9 @@ export type Point = {
};
/**
- * Helper function to scale a 2D point based on resize mode and resolution
- * changes.
+ * Maps a `from`-space coordinate (e.g. model input pixels) back into `to`-space
+ * (e.g. original image pixels), inverting the aspect-preserving letterbox or the
+ * per-axis stretch the source was produced with.
* @category Utils
* @param point The original coordinate point to scale.
* @param opts Options detailing the scaling factors and resize mode.
@@ -30,18 +31,15 @@ export function scalePoint(
}
): Point {
'worklet';
- const { from, to, resizeMode } = opts;
- switch (resizeMode) {
+ const { from, to } = opts;
+ switch (opts.resizeMode) {
case 'letterbox': {
const scale = Math.min(from.width / to.width, from.height / to.height);
- const offsetX = (from.width - to.width * scale) / 2.0;
- const offsetY = (from.height - to.height * scale) / 2.0;
+ const offsetX = (from.width - to.width * scale) / 2;
+ const offsetY = (from.height - to.height * scale) / 2;
return { x: (point.x - offsetX) / scale, y: (point.y - offsetY) / scale };
}
- case 'stretch': {
- const scaleX = from.width / to.width;
- const scaleY = from.height / to.height;
- return { x: point.x / scaleX, y: point.y / scaleY };
- }
+ case 'stretch':
+ return { x: (point.x * to.width) / from.width, y: (point.y * to.height) / from.height };
}
}
diff --git a/packages/react-native-executorch/src/extensions/cv/ops/quad.ts b/packages/react-native-executorch/src/extensions/cv/ops/quad.ts
new file mode 100644
index 0000000000..ad2071d051
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/ops/quad.ts
@@ -0,0 +1,245 @@
+import { scalePoint, type Point } from './points';
+import type { BoundingBox, BoxFormat } from './boxes';
+
+/**
+ * An oriented quadrilateral in pixel space: `points` are the four corners ordered
+ * top-left, top-right, bottom-right, bottom-left, and `score` is the region
+ * confidence in `[0, 1]`. Orientation lives in the corners themselves.
+ * @category Types
+ */
+export type Quad = {
+ readonly points: readonly Point[];
+ readonly score: number;
+};
+
+const distance = (a: Point, b: Point): number => {
+ 'worklet';
+ return Math.hypot(b.x - a.x, b.y - a.y);
+};
+
+const interpolatePoint = (a: Point, b: Point, t: number): Point => {
+ 'worklet';
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
+};
+
+/**
+ * Computes the axis-aligned bounding box enclosing a set of points, in the
+ * requested box format. Returns a zero box for empty input.
+ * @category Typescript API
+ * @typeParam F Bounding box coordinate format.
+ * @param points The points to enclose.
+ * @param format The coordinate format of the returned box.
+ * @returns The enclosing {@link BoundingBox} in `format`.
+ */
+export function boundsOfPoints(
+ points: readonly Point[],
+ format: F
+): BoundingBox {
+ 'worklet';
+ let xmin = Infinity;
+ let ymin = Infinity;
+ let xmax = -Infinity;
+ let ymax = -Infinity;
+ for (const p of points) {
+ if (p.x < xmin) xmin = p.x;
+ if (p.y < ymin) ymin = p.y;
+ if (p.x > xmax) xmax = p.x;
+ if (p.y > ymax) ymax = p.y;
+ }
+ if (points.length === 0) {
+ xmin = ymin = xmax = ymax = 0;
+ }
+ switch (format) {
+ case 'xyxy':
+ return { format: 'xyxy', xmin, ymin, xmax, ymax } as BoundingBox;
+ case 'xywh':
+ return { format: 'xywh', xmin, ymin, w: xmax - xmin, h: ymax - ymin } as BoundingBox;
+ case 'cxcywh':
+ return {
+ format: 'cxcywh',
+ cx: (xmin + xmax) / 2,
+ cy: (ymin + ymax) / 2,
+ w: xmax - xmin,
+ h: ymax - ymin,
+ } as BoundingBox;
+ }
+}
+
+/**
+ * Orders four corner points as top-left, top-right, bottom-right, bottom-left
+ * using their coordinate-sum and coordinate-difference extremes. Inputs that do
+ * not have exactly four points are returned unchanged.
+ * @category Typescript API
+ * @param points The four unordered corners.
+ * @returns The corners ordered TL, TR, BR, BL.
+ */
+export function orderQuad(points: readonly Point[]): Point[] {
+ 'worklet';
+ if (points.length !== 4) {
+ return [...points];
+ }
+ let topLeft = 0;
+ let topRight = 0;
+ let bottomRight = 0;
+ let bottomLeft = 0;
+ let minSum = points[0]!.x + points[0]!.y;
+ let maxSum = minSum;
+ let minDiff = points[0]!.y - points[0]!.x;
+ let maxDiff = minDiff;
+ for (let i = 1; i < 4; i++) {
+ const sum = points[i]!.x + points[i]!.y;
+ const diff = points[i]!.y - points[i]!.x;
+ if (sum < minSum) {
+ minSum = sum;
+ topLeft = i;
+ }
+ if (sum > maxSum) {
+ maxSum = sum;
+ bottomRight = i;
+ }
+ if (diff < minDiff) {
+ minDiff = diff;
+ topRight = i;
+ }
+ if (diff > maxDiff) {
+ maxDiff = diff;
+ bottomLeft = i;
+ }
+ }
+ return [points[topLeft]!, points[topRight]!, points[bottomRight]!, points[bottomLeft]!];
+}
+
+/**
+ * Computes the width and height (in pixels) of an ordered TL,TR,BR,BL quad, taking
+ * the longer of each pair of opposite sides.
+ * @category Typescript API
+ * @param ordered The quad corners ordered TL, TR, BR, BL.
+ * @returns The quad's width and height in pixels.
+ */
+export function quadSize(ordered: readonly Point[]): { width: number; height: number } {
+ 'worklet';
+ const [tl, tr, br, bl] = ordered as [Point, Point, Point, Point];
+ const width = Math.max(distance(tl, tr), distance(bl, br));
+ const height = Math.max(distance(tl, bl), distance(tr, br));
+ return { width, height };
+}
+
+/**
+ * Maps a quad expressed in a resized (letterboxed) frame back to the original
+ * image frame, clamping the result to the image bounds.
+ * @category Typescript API
+ * @param quad The quad in the resized frame.
+ * @param fromWidth The width of the resized frame the quad is expressed in.
+ * @param fromHeight The height of the resized frame the quad is expressed in.
+ * @param toWidth The original image width.
+ * @param toHeight The original image height.
+ * @returns The four corners in original image pixels.
+ */
+export function mapQuadToImage(
+ quad: Quad,
+ fromWidth: number,
+ fromHeight: number,
+ toWidth: number,
+ toHeight: number
+): Point[] {
+ 'worklet';
+ return quad.points.map((p) => {
+ const m = scalePoint(p, {
+ from: { width: fromWidth, height: fromHeight },
+ to: { width: toWidth, height: toHeight },
+ resizeMode: 'letterbox',
+ });
+ return { x: Math.max(0, Math.min(m.x, toWidth)), y: Math.max(0, Math.min(m.y, toHeight)) };
+ });
+}
+
+/**
+ * Splits an ordered TL,TR,BR,BL quad into `parts` equal vertical bands (each an
+ * ordered quad), top to bottom. `parts <= 1` returns the quad unchanged.
+ * @category Typescript API
+ * @param ordered The quad corners ordered TL, TR, BR, BL.
+ * @param parts The number of equal vertical bands to split into.
+ * @returns The bands as ordered TL,TR,BR,BL quads, top to bottom.
+ */
+export function splitTallQuad(ordered: readonly Point[], parts: number): Point[][] {
+ 'worklet';
+ if (parts <= 1) {
+ return [ordered as Point[]];
+ }
+ const [tl, tr, br, bl] = ordered as [Point, Point, Point, Point];
+ const out: Point[][] = [];
+ for (let i = 0; i < parts; i++) {
+ const t0 = i / parts;
+ const t1 = (i + 1) / parts;
+ out.push([
+ interpolatePoint(tl, bl, t0),
+ interpolatePoint(tr, br, t0),
+ interpolatePoint(tr, br, t1),
+ interpolatePoint(tl, bl, t1),
+ ]);
+ }
+ return out;
+}
+
+/**
+ * Computes the axis-aligned bounding quad (ordered TL,TR,BR,BL) enclosing a set of
+ * quads. Returns a zero quad for empty input.
+ * @category Typescript API
+ * @param quads The quads to enclose.
+ * @returns The four enclosing corners, ordered TL, TR, BR, BL.
+ */
+export function boundingQuadOf(quads: readonly (readonly Point[])[]): Point[] {
+ 'worklet';
+ const all: Point[] = [];
+ for (const q of quads) {
+ for (const p of q) {
+ all.push(p);
+ }
+ }
+ const { xmin, ymin, xmax, ymax } = boundsOfPoints(all, 'xyxy');
+ return [
+ { x: xmin, y: ymin },
+ { x: xmax, y: ymin },
+ { x: xmax, y: ymax },
+ { x: xmin, y: ymax },
+ ];
+}
+
+/**
+ * Flattens an ordered TL,TR,BR,BL quad into the 8-number `[x0,y0,..,x3,y3]` array.
+ * @category Typescript API
+ * @param corners The four quad corners (TL, TR, BR, BL).
+ * @returns The eight coordinates `[x0,y0,x1,y1,x2,y2,x3,y3]`.
+ */
+export function flattenQuad(corners: readonly Point[]): number[] {
+ 'worklet';
+ // prettier-ignore
+ return [
+ corners[0]!.x, corners[0]!.y, corners[1]!.x, corners[1]!.y,
+ corners[2]!.x, corners[2]!.y, corners[3]!.x, corners[3]!.y,
+ ];
+}
+
+/**
+ * Builds oriented quads from a detector's flat output array — 9 numbers per box:
+ * `x0,y0,..,x3,y3,score`.
+ * @category Typescript API
+ * @param flat The flat number array from a native detector decode.
+ * @returns The parsed quads.
+ */
+export function quadsFromFlat(flat: number[]): Quad[] {
+ 'worklet';
+ const quads: Quad[] = [];
+ for (let i = 0; i < flat.length; i += 9) {
+ quads.push({
+ points: [
+ { x: flat[i]!, y: flat[i + 1]! },
+ { x: flat[i + 2]!, y: flat[i + 3]! },
+ { x: flat[i + 4]!, y: flat[i + 5]! },
+ { x: flat[i + 6]!, y: flat[i + 7]! },
+ ],
+ score: flat[i + 8]!,
+ });
+ }
+ return quads;
+}
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/ocr/charsets.ts b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/charsets.ts
new file mode 100644
index 0000000000..f8a151299e
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/charsets.ts
@@ -0,0 +1,161 @@
+/* eslint-disable @cspell/spellchecker */
+/* eslint-disable camelcase */
+export const alphabets = {
+ cyrillic:
+ '0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ €₽ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюяЂђЃѓЄєІіЇїЈјЉљЊњЋћЌќЎўЏџҐґҒғҚқҮүҲҳҶҷӀӏӢӣӨөӮӯ',
+ english:
+ '0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ €ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
+ latin:
+ ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ªÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿĀāĂ㥹ĆćČčĎďĐđĒēĖėĘęĚěĞğĨĩĪīĮįİıĶķĹĺĻļĽľŁłŃńŅņŇňŒœŔŕŘřŚśŞşŠšŤťŨũŪūŮůŲųŸŹźŻżŽžƏƠơƯưȘșȚțə̇ḌḍḶḷṀṁṂṃṄṅṆṇṬṭẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹ€',
+ japanese:
+ ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~‥…※♪、。々〈〉《》「」『』【】〔〕あぃいうぇえおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろわをんァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロワヱヲンヴヶ・ー一丁七万丈三上下不与丑世丘丙丞両並中串丸丹主乃久之乗乙九也乱乳乾亀了予争事二互五井亘亜亡交亥亨享京亭亮人仁今介仏仕他付仙代令以仮仰仲件任企伊伍伎伏休会伝伯伴伶伸似伽位低住佐佑体何余作佳使例侍侑供依価便係俊俗保信俣修俵倉個倍倒倖候借値倫倭倶偉偏健側偵偽傍傑備債傷働像僧儀優允元兄充先光克免兎児党入全八公六共兵其具典兼内円再写冠冨冬冴冶冷凍凡処凪出刀刃分切刈刑列初判別利制刷券刺刻則削前剛剣剤剥副割創劇力功加助労効勅勇勉動勘務勝勢勤勧勲包化北匠匡区医十千升午半卒卓協南単博占卯印危即卵卸厚原厨厳去参又叉及友双反収取受叡口古句只叫召可台史右叶号司各合吉同名后吏吐向君吟否吸吹吾呂呉告呑周味呼命和咲哀品哉員哲唄唐唯唱商問啓善喜喬喰営嗣嘉噌器四回因団囲図固国國園土圧在圭地坂均坊坐坪垂型垢垣埋城埜域執基埼堀堂堅堤堰報場堺塔塗塚塩塵境墓増墨墳壁壇壊士壬壮声壱売壷変夏夕外多夜夢大天太夫央失夷奇奈奉奏契奥奨女奴好如妃妙妹妻姉始姓委姥姫姿威娘婆婚婦嫌嬉子孔字存孝孟季孤学孫宅宇守安完宏宗官宙定宜宝実客宣室宥宮宰害家容宿寂寄寅密富寒寛寝察寧審寸寺対寿封専射将尊尋導小少尚尭就尺尻尼尽尾尿局居屈屋展属層屯山岐岡岩岬岱岳岸峠峡峨峯峰島峻崇崎崩嵐嵩嵯嶋嶺巌川州巡巣工左巧巨差己巳巴巻巾市布帆希帝師席帯帰帳常帽幅幌幕幡幣干平年幸幹幽幾庁広庄床序底店府度座庫庭庵康庸廃廉廣延建廻弁式弐弓引弘弟弥弦弱張強弾当形彦彩彪彫彬彰影役彼往征径待律後徒従得御復微徳徹心必忌忍志応忠快念怒怜思急性恋恐恒恩恭息恵悌悟悠患悦悪悲情惇惑惟惣想意愚愛感慈態慎慣慧慶憂憲憶懐懸戎成我戒戦戯戸戻房所扇手才打払扶承技投抗折抜抱押担拓拝拡拳拾持指挙振捕捨捷掃排掘掛採探接推掻提揖揚換揮援揺損摂摘摩摺撃撫播撮操擦擬支改攻放政故敏救敗教敢散敦敬数整敵敷文斉斎斐斑斗料斜斤断斯新方於施旅旋族旗日旦旧旨早旬旭旺昂昆昇昌明易星映春昭是昼時晃晋晩普景晴晶智暁暖暗暢暦暮暴曇曙曜曲曳更書曹曽曾替最月有朋服朔朗望朝期木未末本札朱朴杉李杏材村杖杜束条来杭東杵松板析枕林枚果枝枯架柄柊柏柑染柔柚柱柳柴査柿栃栄栖栗校株核根格桂桃案桐桑桜桝桧桶梁梅梓梢梨梯械梶棄棒棚棟森椋植椎検椿楊楓楠楢業楯極楼楽榊榎榛構槌様槙槻樋標模権横樫樹樺樽橋橘機檀櫛欠次欣欧欲欽歌歓止正此武歩歯歳歴死殊残殖段殺殻殿毅母毎毒比毛氏民気水氷永汀汁求汐汗汚江池汰汲決沈沓沖沙沢河油治沼泉泊法波泣泥注泰洋洗洞津洪洲活派流浄浅浜浦浩浪浮浴海消涌涙液涼淀淑淡深淳淵混添清済渉渋渓渕渚減渡渥温測港湊湖湧湯湾湿満源準溜溝溶滅滋滑滝漁漆漏演漢漬潔潜潟潤潮潰澄澤激濃濱瀧瀬灘火灯灰災炉炎炭点為烈烏無焦然焼煙照煮熊熟熱燃燈燕燦燭爆爪父爽爾片版牛牟牧物特犬犯状狂狐狗狩独狭狼猛猪猫献猿獄獅獣玄率玉王玖玲珍珠現球理琉琢琳琴瑚瑛瑞瑠瑳璃環瓜瓦瓶甘甚生産用甫田由甲申男町画界畑畔留畜畝畠略番異畳疾病症痛療発登白百的皆皇皮皿盆益盗盛盟監盤目盲直相省眉看県眞真眠眼着督睦瞬瞳矢知矩短石砂研砲破硫硬碑碧碩確磁磐磨磯礁示礼社祇祈祉祐祖祝神祢祥票祭禁禄禅禎福禰秀私秋科秘秦秩称移稀程税稔稗稚種稲穂積穏穴究空突窓窪立竜章童竪端競竹笑笛笠符第笹筆等筋筑筒答策箇箕算管箱箸節範築篠篤篭簡簾籍米粉粒粕粗粟粥精糖糞糠糸系紀約紅紋納純紗紘紙級素紡索紫細紳紹紺終組経結絡絢給統絵絶絹継続綜維綱網綾綿緋総緑緒線締編緩練縁縄縦縫縮績繁織繰罪置羅羊美群義羽翁習翔翠翼耀老考者耐耕耳耶聖聞聡聴職肇肉肌肝股肥肩育肺背胞胡胤胸能脂脇脈脚脱脳腐腕腫腰腸腹膜膳臣臥臨自臭至致臼興舌舎舘舛舜舞舟航般船艦良色艶芋芙芝芥芦花芳芸芹芽苅苑苔苗若苦苫英茂茄茅茉茜茨茶草荒荘荷荻莉菅菊菌菓菖菜華菱萌萩萱落葉葛葦葵蒔蒲蒸蒼蓋蓑蓬蓮蔦蔭蔵蕗薄薩薫薬薮藁藍藤藻蘇蘭虎虚虫虹虻蚊蛇蛍蛭蜂蜜蝦蝶融螺蟹蟻血衆行術街衛衝衡衣表袋袖被裁裂装裏裕補裟裸製複西要覆覇見規視覚覧親観角解触言計訓託記訪設許訳訴診証評詞詠試詩詰話誉誌認誓誘語誠誤説読課調談請諏論諭諸謙講謝謹識警議譲護讃谷豆豊豚象豪貝貞負財貢貧貨販貫責貯貴買貸費賀賃資賞賢質赤赦走起超越足跡路跳踏身車軌軍軒軟転軸軽載輔輝輪輸辛辞辰農辺辻込迎近返迦迫述迷追退送逃逆透途通速造逢連週進逸遅遊運過道達違遠遣遥適選遺遼避邑那邦邪郁郎郡部郭郵郷都配酒酔酢酸醍醐采釈里重野量金釘釜針釣鈴鉄鉛鉢鉱鉾銀銃銅銘銭鋭鋼錦録鍋鍛鍬鍵鎌鎖鎮鏡鐘鑑長門閉開閑間関閣闇闘阪防阿陀附降限院陣除陰陳陵陶陸険陽隅隆隈隊階随隔際障隠隣隼雀雁雄雅集雑雛離難雨雪雲零雷電震霊霜霞霧露青靖静非面革鞍鞠韓音響頂頃項順須預頓領頭頼題額顔顕願類風飛食飯飲飼飽飾餅養館首香馨馬駄駅駆駐駒駿騎験骨高髪鬼魁魂魅魔魚鮎鮫鮮鯉鯨鳥鳩鳳鳴鴨鴻鵜鶏鶴鷲鷹鷺鹿麓麗麦麻麿黄黒黙鼓鼠鼻齢龍*',
+ zh_sim:
+ ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~。〈〉《》「」『』一丁七万丈三上下不与丐丑专且丕世丘丙业丛东丝丞丢两严丧个丫中丰串临丸丹为主丽举乃久么义之乌乍乎乏乐乒乓乔乖乘乙乜九乞也习乡书乩买乱乳乾了予争事二亍于亏云互亓五井亘亚些亟亡亢交亥亦产亨亩享京亭亮亲亳亵人亿什仁仂仃仄仅仆仇仉今介仍从仑仓仔仕他仗付仙仞仟仡代令以仨仪仫们仰仲仳仵件价任份仿企伉伊伍伎伏伐休众优伙会伛伞伟传伢伤伦伧伪伫伯估伴伶伸伺似伽佃但位低住佐佑体何佗佘余佚佛作佝佞佟你佣佤佥佩佬佯佰佳佴佶佻佼佾使侃侄侈侉例侍侏侑侔侗供依侠侣侥侦侧侨侩侪侬侮侯侵便促俄俅俊俎俏俐俑俗俘俚俜保俞俟信俣俦俨俩俪俭修俯俱俳俸俺俾倌倍倏倒倔倘候倚倜借倡倥倦倨倩倪倬倭债值倾偃假偈偌偎偏偕做停健偬偶偷偻偾偿傀傅傈傍傣傥傧储傩催傲傻像僖僚僦僧僬僭僮僳僵僻儆儇儋儒儡儿兀允元兄充兆先光克免兑兔兕兖党兜兢入全八公六兮兰共关兴兵其具典兹养兼兽冀冁内冈冉册再冒冕冗写军农冠冢冤冥冬冯冰冱冲决况冶冷冻冼冽净凄准凇凉凋凌减凑凛凝几凡凤凫凭凯凰凳凶凸凹出击凼函凿刀刁刃分切刈刊刍刎刑划刖列刘则刚创初删判刨利别刭刮到刳制刷券刹刺刻刽刿剀剁剂剃削剌前剐剑剔剖剜剞剡剥剧剩剪副割剽剿劁劂劈劐劓力劝办功加务劢劣动助努劫劬劭励劲劳劾势勃勇勉勋勐勒勖勘募勤勰勺勾勿匀包匆匈匍匏匐匕化北匙匝匠匡匣匦匪匮匹区医匾匿十千卅升午卉半华协卑卒卓单卖南博卜卞卟占卡卢卣卤卦卧卫卮卯印危即却卵卷卸卺卿厂厄厅历厉压厌厍厕厘厚厝原厢厣厥厦厨厩厮去县叁参又叉及友双反发叔取受变叙叛叟叠口古句另叨叩只叫召叭叮可台叱史右叵叶号司叹叻叼叽吁吃各吆合吉吊同名后吏吐向吒吓吕吗君吝吞吟吠吡吣否吧吨吩含听吭吮启吱吲吴吵吸吹吻吼吾呀呃呆呈告呋呐呓呔呕呖呗员呙呛呜呢呤呦周呱呲味呵呶呷呸呻呼命咀咂咄咆咋和咎咏咐咒咔咕咖咙咚咛咝咣咤咦咧咨咩咪咫咬咭咯咱咳咴咸咻咽咿哀品哂哄哆哇哈哉哌响哎哏哐哑哓哔哕哗哙哚哝哞哟哥哦哧哨哩哪哭哮哲哳哺哼哽哿唁唆唇唉唏唐唑唔唛唠唢唣唤唧唪唬售唯唱唳唷唼唾唿啁啃啄商啉啊啐啕啖啜啡啤啥啦啧啪啬啭啮啵啶啷啸啻啼啾喀喁喂喃善喇喈喉喊喋喏喑喔喘喙喜喝喟喧喱喳喵喷喹喻喽喾嗄嗅嗉嗌嗍嗑嗒嗓嗔嗖嗜嗝嗟嗡嗣嗤嗥嗦嗨嗪嗫嗬嗯嗲嗳嗵嗷嗽嗾嘀嘁嘈嘉嘌嘎嘏嘘嘛嘞嘟嘣嘤嘧嘬嘭嘱嘲嘴嘶嘹嘻嘿噌噍噎噔噗噙噜噢噤器噩噪噫噬噱噶噻噼嚅嚆嚎嚏嚓嚣嚯嚷嚼囊囔囚四囝回囟因囡团囤囫园困囱围囵囹固国图囿圃圄圆圈圉圊圜土圣在圩圪圬圭圮圯地圳圹场圻圾址坂均坊坌坍坎坏坐坑块坚坛坜坝坞坟坠坡坤坦坨坩坪坫坭坯坳坷坻坼垂垃垄垅垆型垌垒垓垛垠垡垢垣垤垦垧垩垫垭垮垲垸埂埃埋城埏埒埔埕埘埙埚埝域埠埤埭埯埴埸培基埽堂堆堇堋堍堑堕堙堞堠堡堤堪堰堵塄塌塍塑塔塘塞塥填塬塾墀墁境墅墉墒墓墙增墟墨墩墼壁壅壑壕壤士壬壮声壳壶壹处备复夏夔夕外夙多夜够夤夥大天太夫夭央夯失头夷夸夹夺夼奁奂奄奇奈奉奋奎奏契奔奕奖套奘奚奠奢奥女奴奶奸她好妁如妃妄妆妇妈妊妍妒妓妖妗妙妞妣妤妥妨妩妪妫妮妯妲妹妻妾姆姊始姐姑姒姓委姗姘姚姜姝姣姥姨姬姹姻姿威娃娄娅娆娇娈娉娌娑娓娘娜娟娠娣娥娩娱娲娴娶娼婀婆婉婊婕婚婢婧婪婴婵婶婷婺婿媒媚媛媪媲媳媵媸媾嫁嫂嫉嫌嫒嫔嫖嫘嫜嫠嫡嫣嫦嫩嫫嫱嬉嬖嬗嬴嬷孀子孑孓孔孕字存孙孚孛孜孝孟孢季孤孥学孩孪孬孰孱孳孵孺孽宁它宄宅宇守安宋完宏宓宕宗官宙定宛宜宝实宠审客宣室宥宦宪宫宰害宴宵家宸容宽宾宿寂寄寅密寇富寐寒寓寝寞察寡寤寥寨寮寰寸对寺寻导寿封射将尉尊小少尔尕尖尘尚尜尝尤尥尧尬就尴尸尹尺尻尼尽尾尿局屁层居屈屉届屋屎屏屐屑展屙属屠屡屣履屦屯山屹屺屿岁岂岈岌岍岐岑岔岖岗岘岙岚岛岢岣岩岫岬岭岱岳岵岷岸岿峁峄峋峒峙峡峤峥峦峨峪峭峰峻崂崃崆崇崎崔崖崛崞崤崦崧崩崭崮崴崽嵇嵊嵋嵌嵘嵛嵝嵩嵫嵬嵯嵴嶂嶙嶝嶷巅巍川州巡巢工左巧巨巩巫差巯己已巳巴巷巽巾币市布帅帆师希帏帐帑帔帕帖帘帙帚帛帜帝带帧席帮帱帷常帻帼帽幂幄幅幌幔幕幛幞幡幢干平年并幸幺幻幼幽广庀庄庆庇床庋序庐庑库应底庖店庙庚府庞废庠庥度座庭庳庵庶康庸庹庾廉廊廒廓廖廛廨廪延廷建廿开弁异弃弄弈弊弋式弑弓引弗弘弛弟张弥弦弧弩弭弯弱弹强弼彀归当录彖彗彘彝形彤彦彩彪彬彭彰影彳彷役彻彼往征徂径待徇很徉徊律後徐徒徕得徘徙徜御徨循徭微徵德徼徽心必忆忉忌忍忏忐忑忒忖志忘忙忝忠忡忤忧忪快忭忮忱念忸忻忽忾忿怀态怂怃怄怅怆怊怍怎怏怒怔怕怖怙怛怜思怠怡急怦性怨怩怪怫怯怵总怼怿恁恂恃恋恍恐恒恕恙恚恝恢恣恤恧恨恩恪恫恬恭息恰恳恶恸恹恺恻恼恽恿悃悄悉悌悍悒悔悖悚悛悝悟悠患悦您悫悬悭悯悱悲悴悸悻悼情惆惊惋惑惕惘惚惜惝惟惠惦惧惨惩惫惬惭惮惯惰想惴惶惹惺愀愁愆愈愉愍愎意愕愚感愠愣愤愦愧愫愿慈慊慌慎慑慕慝慢慧慨慰慵慷憋憎憔憝憧憨憩憬憷憾懂懈懊懋懑懒懦懵懿戆戈戊戋戌戍戎戏成我戒戕或戗战戚戛戟戡戢戥截戬戮戳戴户戽戾房所扁扃扇扈扉手扌才扎扑扒打扔托扛扣扦执扩扪扫扬扭扮扯扰扳扶批扼找承技抄抉把抑抒抓投抖抗折抚抛抟抠抡抢护报抨披抬抱抵抹抻押抽抿拂拄担拆拇拈拉拊拌拍拎拐拒拓拔拖拗拘拙拚招拜拟拢拣拥拦拧拨择括拭拮拯拱拳拴拶拷拼拽拾拿持挂指挈按挎挑挖挚挛挝挞挟挠挡挢挣挤挥挨挪挫振挲挹挺挽捂捃捅捆捉捋捌捍捎捏捐捕捞损捡换捣捧捩捭据捱捶捷捺捻掀掂掇授掉掊掌掎掏掐排掖掘掠探掣接控推掩措掬掭掮掰掳掴掷掸掺掼掾揄揆揉揍揎描提插揖揞揠握揣揩揪揭援揶揸揽揿搀搁搂搅搋搌搏搐搓搔搛搜搞搠搡搦搪搬搭搴携搽摁摄摅摆摇摈摊摒摔摘摞摧摩摭摸摹摺撂撄撅撇撑撒撕撖撙撞撤撩撬播撮撰撵撷撸撺撼擀擂擅操擎擐擒擘擞擢擤擦攀攉攒攘攥攫攮支收攸改攻放政故效敉敌敏救敕敖教敛敝敞敢散敦敫敬数敲整敷文斋斌斐斑斓斗料斛斜斟斡斤斥斧斩斫断斯新方於施旁旃旄旅旆旋旌旎族旒旖旗无既日旦旧旨早旬旭旮旯旰旱时旷旺昀昂昃昆昊昌明昏易昔昕昙昝星映春昧昨昭是昱昴昵昶昼显晁晃晋晌晏晒晓晔晕晖晗晚晟晡晤晦晨普景晰晴晶晷智晾暂暄暇暌暑暖暗暝暧暨暮暴暹暾曙曛曜曝曦曩曰曲曳更曷曹曼曾替最月有朊朋服朐朔朕朗望朝期朦木未末本札术朱朴朵机朽杀杂权杆杈杉杌李杏材村杓杖杜杞束杠条来杨杪杭杯杰杲杳杵杷杼松板极构枇枉枋析枕林枘枚果枝枞枢枣枥枧枨枪枫枭枯枰枳枵架枷枸柁柃柄柏某柑柒染柔柘柙柚柜柝柞柠柢查柩柬柯柰柱柳柴柽柿栀栅标栈栉栊栋栌栎栏树栓栖栗栝校栩株栲栳样核根格栽栾桀桁桂桃桄桅框案桉桊桌桎桐桑桓桔桕桠桡桢档桤桥桦桧桨桩桫桴桶桷梁梃梅梆梏梓梗梢梦梧梨梭梯械梳梵检棂棉棋棍棒棕棘棚棠棣森棰棱棵棹棺棼椁椅椋植椎椐椒椟椠椤椭椰椴椹椽椿楂楔楗楚楝楞楠楣楦楫楮楷楸楹楼榀概榄榆榇榈榉榍榔榕榛榜榧榨榫榭榱榴榷榻槁槊槌槎槐槔槛槟槠槭槲槽槿樊樗樘樟模樨横樯樱樵樽樾橄橇橐橘橙橛橡橥橱橹橼檀檄檎檐檑檗檠檩檫檬欠次欢欣欤欧欲欷欺款歃歆歇歉歌歙止正此步武歧歪歹死歼殁殂殃殄殆殇殉殊残殍殒殓殖殚殛殡殪殳殴段殷殿毁毂毅毋母每毒毓比毕毖毗毙毛毡毪毫毯毳毵毹毽氅氆氇氍氏氐民氓气氕氖氘氙氚氛氟氡氢氤氦氧氨氩氪氮氯氰氲水永氽汀汁求汆汇汉汊汐汔汕汗汛汜汝汞江池污汤汨汩汪汰汲汴汶汹汽汾沁沂沃沅沆沈沉沌沏沐沓沔沙沛沟没沣沤沥沦沧沩沪沫沭沮沱河沸油治沼沽沾沿泄泅泉泊泌泐泓泔法泖泗泛泞泠泡波泣泥注泪泫泮泯泰泱泳泵泷泸泺泻泼泽泾洁洄洇洋洌洎洒洗洙洚洛洞津洧洪洫洮洱洲洳洵洹活洼洽派流浃浅浆浇浈浊测浍济浏浑浒浓浔浙浚浜浞浠浣浦浩浪浮浯浴海浸浼涂涅消涉涌涎涑涓涔涕涛涝涞涟涠涡涣涤润涧涨涩涪涫涮涯液涵涸涿淀淄淅淆淇淋淌淑淖淘淙淝淞淠淡淤淦淫淬淮深淳混淹添淼清渊渌渍渎渐渑渔渖渗渚渝渠渡渣渤渥温渫渭港渲渴游渺湃湄湍湎湔湖湘湛湟湫湮湾湿溃溅溆溉溏源溘溜溟溢溥溧溪溯溱溲溴溶溷溺溻溽滁滂滇滋滏滑滓滔滕滗滚滞滟滠满滢滤滥滦滨滩滴滹漂漆漉漏漓演漕漠漤漩漪漫漭漯漱漳漶漾潆潇潋潍潘潜潞潢潦潭潮潲潴潸潺潼澄澈澉澌澍澎澜澡澧澳澶澹激濂濉濑濒濞濠濡濮濯瀑瀚瀛瀣瀵瀹灌灏灞火灭灯灰灵灶灸灼灾灿炀炅炉炊炎炒炔炕炖炙炜炝炫炬炭炮炯炱炳炷炸点炻炼炽烀烁烂烃烈烊烘烙烛烟烤烦烧烨烩烫烬热烯烷烹烽焉焊焐焓焕焖焘焙焚焦焯焰焱然煅煊煌煎煜煞煤煦照煨煮煲煳煸煺煽熄熊熏熔熘熙熟熠熨熬熵熹燃燎燔燕燠燥燧燮燹爆爝爨爪爬爰爱爵父爷爸爹爻爽爿片版牌牍牒牖牙牛牝牟牡牢牦牧物牮牯牲牵特牺牾犀犁犄犊犋犍犏犒犟犬犯犰犴状犷犸犹狁狂狃狄狈狍狎狐狒狗狙狞狠狡狨狩独狭狮狯狰狱狲狳狴狷狸狺狻狼猁猃猊猎猕猖猗猛猜猝猞猡猢猥猩猪猫猬献猱猴猷猸猹猾猿獍獐獒獗獠獬獭獯獾玄率玉王玎玑玖玛玢玩玫玮环现玲玳玷玺玻珀珂珈珉珊珍珏珐珑珙珞珠珥珧珩班珲球琅理琉琏琐琚琛琢琥琦琨琪琬琮琰琳琴琵琶琼瑁瑕瑗瑙瑚瑛瑜瑞瑟瑭瑰瑶瑾璀璁璃璇璋璎璐璜璞璧璨璩瓒瓜瓞瓠瓢瓣瓤瓦瓮瓯瓴瓶瓷瓿甄甏甑甓甘甙甚甜生甥用甩甫甬甭田由甲申电男甸町画甾畀畅畈畋界畎畏畔留畚畛畜略畦番畲畴畸畹畿疃疆疋疏疑疔疖疗疙疚疝疟疠疡疣疤疥疫疬疮疯疰疱疲疳疴疵疸疹疼疽疾痂痃痄病症痈痉痊痍痒痔痕痘痛痞痢痣痤痦痧痨痪痫痰痱痴痹痼痿瘀瘁瘃瘅瘊瘌瘐瘗瘘瘙瘛瘟瘠瘢瘤瘥瘦瘩瘪瘫瘭瘰瘳瘴瘵瘸瘼瘾瘿癀癃癌癍癔癖癜癞癣癫癯癸登白百皂的皆皇皈皋皎皑皓皖皙皤皮皱皲皴皿盂盅盆盈益盍盎盏盐监盒盔盖盗盘盛盟盥目盯盱盲直相盹盼盾省眄眇眈眉看眍眙眚真眠眢眦眨眩眭眯眵眶眷眸眺眼着睁睃睇睐睑睚睛睡睢督睥睦睨睫睬睹睽睾睿瞀瞄瞅瞌瞍瞎瞑瞒瞟瞠瞢瞥瞧瞩瞪瞬瞰瞳瞵瞻瞽瞿矍矗矛矜矢矣知矧矩矫矬短矮石矶矸矽矾矿砀码砂砉砌砍砑砒研砖砗砘砚砜砝砟砣砥砧砭砰破砷砸砹砺砻砼砾础硅硇硌硎硐硒硕硖硗硝硪硫硬硭确硷硼碇碉碌碍碎碑碓碗碘碚碛碜碟碡碣碥碧碰碱碲碳碴碹碾磁磅磉磊磋磐磔磕磙磨磬磲磴磷磺礁礅礓礞礤礴示礻礼社祀祁祆祈祉祓祖祗祚祛祜祝神祟祠祢祥祧票祭祯祷祸祺禀禁禄禅禊福禚禧禳禹禺离禽禾秀私秃秆秉秋种科秒秕秘租秣秤秦秧秩秫秭积称秸移秽稀稂稆程稍税稔稗稚稞稠稣稳稷稻稼稽稿穆穑穗穰穴究穷穸穹空穿窀突窃窄窈窍窑窒窕窖窗窘窜窝窟窠窥窦窨窬窭窳窿立竖站竞竟章竣童竦竭端竹竺竽竿笃笄笆笈笊笋笏笑笔笕笙笛笞笠笤笥符笨笪笫第笮笱笳笸笺笼笾筅筇等筋筌筏筐筑筒答策筘筚筛筝筠筢筮筱筲筵筷筹筻签简箅箍箐箔箕算箜管箢箦箧箨箩箪箫箬箭箱箴箸篁篆篇篌篑篓篙篚篝篡篥篦篪篮篱篷篼篾簇簋簌簏簖簟簦簧簪簸簿籀籁籍米籴类籼籽粉粑粒粕粗粘粜粝粞粟粤粥粪粮粱粲粳粹粼粽精糁糅糇糈糊糌糍糕糖糗糙糜糟糠糨糯系紊素索紧紫累絮絷綦綮縻繁繇纂纛纠纡红纣纤纥约级纨纩纪纫纬纭纯纰纱纲纳纵纶纷纸纹纺纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绗绘给绚绛络绝绞统绠绡绢绣绥绦继绨绩绪绫续绮绯绰绲绳维绵绶绷绸绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缌缎缏缑缒缓缔缕编缗缘缙缚缛缜缝缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵缶缸缺罂罄罅罐网罔罕罗罘罚罟罡罢罨罩罪置罱署罴罹罾羁羊羌美羔羚羝羞羟羡群羧羯羰羲羸羹羼羽羿翁翅翊翌翎翔翕翘翟翠翡翥翦翩翮翰翱翳翻翼耀老考耄者耆耋而耍耐耒耔耕耖耗耘耙耜耠耢耥耦耧耨耩耪耱耳耵耶耷耸耻耽耿聂聃聆聊聋职聍聒联聘聚聩聪聱聿肃肄肆肇肉肋肌肓肖肘肚肛肝肟肠股肢肤肥肩肪肫肭肮肯肱育肴肷肺肼肽肾肿胀胁胂胃胄胆背胍胎胖胗胙胚胛胜胝胞胡胤胥胧胨胩胪胫胬胭胯胰胱胲胳胴胶胸胺胼能脂脆脉脊脍脎脏脐脑脒脓脔脖脘脚脞脬脯脱脲脶脸脾腆腈腊腋腌腐腑腓腔腕腙腚腠腥腧腩腭腮腰腱腴腹腺腻腼腽腾腿膀膂膈膊膏膑膘膛膜膝膦膨膪膳膺膻臀臁臂臃臆臊臌臣臧自臬臭至致臻臼臾舀舁舂舄舅舆舌舍舐舒舔舛舜舞舟舡舢舣舨航舫般舰舱舳舴舵舶舷舸船舻舾艄艇艋艘艚艟艨艮良艰色艳艴艺艽艾艿节芄芈芊芋芍芎芏芑芒芗芘芙芜芝芟芡芥芦芨芩芪芫芬芭芮芯芰花芳芴芷芸芹芽芾苁苄苇苈苊苋苌苍苎苏苑苒苓苔苕苗苘苛苜苞苟苠苡苣苤若苦苫苯英苴苷苹苻茁茂范茄茅茆茈茉茌茎茏茑茔茕茗茚茛茜茧茨茫茬茭茯茱茳茴茵茶茸茹茼荀荃荆荇草荏荐荑荒荔荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭药荷荸荻荼荽莅莆莉莎莒莓莘莛莜莞莠莨莩莪莫莰莱莲莳莴莶获莸莹莺莼莽菀菁菅菇菊菌菏菔菖菘菜菝菟菠菡菥菩菪菰菱菲菹菽萁萃萄萋萌萍萎萏萑萘萜萝萤营萦萧萨萱萸萼落葆葑著葚葛葜葡董葩葫葬葭葱葳葵葶葸葺蒂蒇蒈蒉蒋蒌蒎蒗蒙蒜蒡蒯蒲蒴蒸蒹蒺蒽蒿蓁蓄蓉蓊蓍蓐蓑蓓蓖蓝蓟蓠蓣蓥蓦蓬蓰蓼蓿蔌蔑蔓蔗蔚蔟蔡蔫蔬蔷蔸蔹蔺蔻蔼蔽蕃蕈蕉蕊蕖蕙蕞蕤蕨蕲蕴蕹蕺蕻蕾薄薅薇薏薛薜薤薨薪薮薯薰薷薹藁藉藏藐藓藕藜藤藩藻藿蘅蘑蘖蘧蘩蘸蘼虎虏虐虑虔虚虞虢虫虬虮虱虹虺虻虼虽虾虿蚀蚁蚂蚊蚋蚌蚍蚓蚕蚜蚝蚣蚤蚧蚨蚩蚬蚯蚰蚱蚴蚶蚺蛀蛄蛆蛇蛉蛊蛋蛎蛏蛐蛑蛔蛘蛙蛛蛞蛟蛤蛩蛭蛮蛰蛱蛲蛳蛴蛸蛹蛾蜀蜂蜃蜇蜈蜉蜊蜍蜒蜓蜕蜗蜘蜚蜜蜞蜡蜢蜣蜥蜩蜮蜱蜴蜷蜻蜾蜿蝇蝈蝉蝌蝎蝓蝗蝙蝠蝣蝤蝥蝮蝰蝴蝶蝻蝼蝽蝾螂螃螅螈螋融螗螟螨螫螬螭螯螳螵螺螽蟀蟆蟊蟋蟑蟒蟛蟠蟥蟪蟮蟹蟾蠃蠊蠓蠕蠖蠡蠢蠲蠹蠼血衄衅行衍衔街衙衡衢衣补表衩衫衬衮衰衲衷衽衾衿袁袂袄袅袈袋袍袒袖袜袢袤被袭袱袼裁裂装裆裉裎裒裔裕裘裙裟裢裣裤裥裨裰裱裳裴裸裹裼裾褂褊褐褒褓褙褚褛褡褥褪褫褰褴褶襁襄襞襟襦襻西要覃覆见观规觅视觇览觉觊觋觌觎觏觐觑角觖觚觜觞解觥触觫觯觳言訇訾詈詹誉誊誓謇警譬计订讣认讥讦讧讨让讪讫训议讯记讲讳讴讵讶讷许讹论讼讽设访诀证诂诃评诅识诈诉诊诋诌词诎诏译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诫诬语诮误诰诱诲诳说诵请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谙谚谛谜谝谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豁豆豇豉豌豕豚象豢豪豫豳豸豹豺貂貅貉貊貌貔貘贝贞负贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赓赔赖赘赙赚赛赜赝赞赠赡赢赣赤赦赧赫赭走赳赴赵赶起趁趄超越趋趑趔趟趣趱足趴趵趸趺趼趾趿跃跄跆跋跌跎跏跑跖跗跚跛距跞跟跣跤跨跪跬路跳践跷跸跹跺跻跽踅踉踊踌踏踔踝踞踟踢踣踩踪踬踮踯踱踵踹踺踽蹀蹁蹂蹄蹇蹈蹉蹊蹋蹑蹒蹙蹦蹩蹬蹭蹯蹰蹲蹴蹶蹼蹿躁躅躇躏躐躔躜躞身躬躯躲躺车轧轨轩轫转轭轮软轰轱轲轳轴轵轶轷轸轺轻轼载轾轿辁辂较辄辅辆辇辈辉辊辋辍辎辏辐辑输辔辕辖辗辘辙辚辛辜辞辟辣辨辩辫辰辱边辽达迁迂迄迅过迈迎运近迓返迕还这进远违连迟迢迤迥迦迨迩迪迫迭迮述迷迸迹追退送适逃逄逅逆选逊逋逍透逐逑递途逖逗通逛逝逞速造逡逢逦逭逮逯逵逶逸逻逼逾遁遂遄遇遍遏遐遑遒道遗遘遛遢遣遥遨遭遮遴遵遽避邀邂邃邈邋邑邓邕邗邙邛邝邡邢那邦邪邬邮邯邰邱邳邴邵邶邸邹邺邻邾郁郄郅郇郊郎郏郐郑郓郗郛郜郝郡郢郦郧部郫郭郯郴郸都郾鄂鄄鄙鄞鄢鄣鄯鄱鄹酃酆酉酊酋酌配酎酏酐酒酗酚酝酞酡酢酣酤酥酩酪酬酮酯酰酱酲酴酵酶酷酸酹酽酾酿醅醇醉醋醌醍醐醑醒醚醛醢醪醭醮醯醴醵醺采釉释里重野量金釜鉴銎銮鋈錾鍪鎏鏊鏖鐾鑫钆钇针钉钊钋钌钍钎钏钐钒钓钔钕钗钙钚钛钜钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钷钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铌铍铎铐铑铒铕铗铘铙铛铜铝铞铟铠铡铢铣铤铥铧铨铩铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锎锏锐锑锒锓锔锕锖锗锘错锚锛锝锞锟锡锢锣锤锥锦锨锩锪锫锬锭键锯锰锱锲锴锵锶锷锸锹锺锻锾锿镀镁镂镄镅镆镇镉镊镌镍镎镏镐镑镒镓镔镖镗镘镛镜镝镞镡镢镣镤镥镦镧镨镩镪镫镬镭镯镰镱镲镳镶长门闩闪闫闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾阀阁阂阃阄阅阆阈阉阊阋阌阍阎阏阐阑阒阔阕阖阗阙阚阜队阡阢阪阮阱防阳阴阵阶阻阼阽阿陀陂附际陆陇陈陉陋陌降限陔陕陛陟陡院除陧陨险陪陬陲陴陵陶陷隅隆隈隋隍随隐隔隗隘隙障隧隰隳隶隼隽难雀雁雄雅集雇雉雌雍雎雏雒雕雠雨雩雪雯雳零雷雹雾需霁霄霆震霈霉霍霎霏霓霖霜霞霪霭霰露霸霹霾青靓靖静靛非靠靡面靥革靳靴靶靼鞅鞋鞍鞑鞒鞘鞠鞣鞫鞭鞯鞲鞴韦韧韩韪韫韬韭音韵韶页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颌颍颏颐频颓颔颖颗题颚颛颜额颞颟颠颡颢颤颥颦颧风飑飒飓飕飘飙飚飞食飧飨餍餐餮饔饕饥饧饨饩饪饫饬饭饮饯饰饱饲饴饵饶饷饺饼饽饿馀馁馄馅馆馇馈馊馋馍馏馐馑馒馓馔馕首馗馘香馥馨马驭驮驯驰驱驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骄骅骆骇骈骊骋验骏骐骑骒骓骖骗骘骚骛骜骝骞骟骠骡骢骣骤骥骧骨骰骶骷骸骺骼髀髁髂髅髋髌髑髓高髡髦髫髭髯髹髻鬃鬈鬏鬓鬟鬣鬯鬲鬻鬼魁魂魃魄魅魇魈魉魍魏魑魔鱼鱿鲁鲂鲅鲆鲇鲈鲋鲍鲎鲐鲑鲔鲚鲛鲜鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲫鲭鲮鲰鲱鲲鲳鲴鲵鲷鲸鲺鲻鲼鲽鳃鳄鳅鳆鳇鳊鳌鳍鳎鳏鳐鳓鳔鳕鳖鳗鳘鳙鳜鳝鳞鳟鳢鸟鸠鸡鸢鸣鸥鸦鸨鸩鸪鸫鸬鸭鸯鸱鸲鸳鸵鸶鸷鸸鸹鸺鸽鸾鸿鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹎鹏鹑鹕鹗鹘鹚鹛鹜鹞鹣鹤鹦鹧鹨鹩鹪鹫鹬鹭鹰鹱鹳鹿麂麇麈麋麒麓麝麟麦麸麻麽麾黄黉黍黎黏黑黔默黛黜黝黟黠黢黥黧黩黯黹黻黼黾鼋鼍鼎鼐鼓鼗鼙鼠鼢鼬鼯鼷鼹鼻鼾齐齑齿龀龃龄龅龆龇龈龉龊龋龌龙龚龛龟龠',
+ korean:
+ ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~가각간갇갈감갑값강갖같갚갛개객걀거걱건걷걸검겁것겉게겨격겪견결겹경곁계고곡곤곧골곰곱곳공과관광괜괴굉교구국군굳굴굵굶굽궁권귀규균그극근글긁금급긋긍기긴길김깅깊까깎깐깔깜깝깥깨꺼꺾껍껏껑께껴꼬꼭꼴꼼꼽꽂꽃꽉꽤꾸꿀꿈뀌끄끈끊끌끓끔끗끝끼낌나낙낚난날낡남납낫낭낮낯낱낳내냄냉냐냥너넉널넓넘넣네넥넷녀녁년념녕노녹논놀놈농높놓놔뇌뇨누눈눕뉘뉴늄느늑는늘늙능늦늬니닐님다닥닦단닫달닭닮담답닷당닿대댁댐더덕던덜덤덥덧덩덮데델도독돈돌돕동돼되된두둑둘둠둡둥뒤뒷드득든듣들듬듭듯등디딩딪따딱딴딸땀땅때땜떠떡떤떨떻떼또똑뚜뚫뚱뛰뜨뜩뜯뜰뜻띄라락란람랍랑랗래랜램랫략량러럭런럴럼럽럿렁렇레렉렌려력련렬렵령례로록론롬롭롯료루룩룹룻뤄류륙률륭르른름릇릎리릭린림립릿마막만많말맑맘맙맛망맞맡맣매맥맨맵맺머먹먼멀멈멋멍멎메멘멩며면멸명몇모목몰몸몹못몽묘무묵묶문묻물뭄뭇뭐뭣므미민믿밀밉밌및밑바박밖반받발밝밟밤밥방밭배백뱀뱃뱉버번벌범법벗베벤벼벽변별볍병볕보복볶본볼봄봇봉뵈뵙부북분불붉붐붓붕붙뷰브블비빌빗빚빛빠빨빵빼뺨뻐뻔뻗뼈뽑뿌뿐쁘쁨사삭산살삶삼상새색샌생서석섞선설섬섭섯성세센셈셋션소속손솔솜솟송솥쇄쇠쇼수숙순술숨숫숲쉬쉽슈스슨슬슴습슷승시식신싣실싫심십싱싶싸싹쌀쌍쌓써썩썰썹쎄쏘쏟쑤쓰쓸씀씌씨씩씬씹씻아악안앉않알앓암압앗앙앞애액야약얇양얗얘어억언얹얻얼엄업없엇엉엌엎에엔엘여역연열엷염엽엿영옆예옛오옥온올옮옳옷와완왕왜왠외왼요욕용우욱운울움웃웅워원월웨웬위윗유육율으윽은을음응의이익인일읽잃임입잇있잊잎자작잔잖잘잠잡장잦재쟁저적전절젊점접젓정젖제젠젯져조족존졸좀좁종좋좌죄주죽준줄줌줍중쥐즈즉즌즐즘증지직진질짐집짓징짙짚짜짝짧째쨌쩌쩍쩐쪽쫓쭈쭉찌찍찢차착찬찮찰참창찾채책챔챙처척천철첫청체쳐초촉촌총촬최추축춘출춤춥춧충취츠측츰층치칙친칠침칭카칸칼캐캠커컨컬컴컵컷켓켜코콜콤콩쾌쿠퀴크큰클큼키킬타탁탄탈탑탓탕태택탤터턱털텅테텍텔템토톤톱통퇴투툼퉁튀튜트특튼튿틀틈티틱팀팅파팎판팔패팩팬퍼퍽페펴편펼평폐포폭표푸푹풀품풍퓨프플픔피픽필핏핑하학한할함합항해핵핸햄햇행향허헌험헤헬혀현혈협형혜호혹혼홀홍화확환활황회획횟효후훈훌훔훨휘휴흉흐흑흔흘흙흡흥흩희흰히힘',
+ telugu:
+ '0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZఁంఃఅఆఇఈఉఊఋఌఎఏఐఒఓఔకఖగఘఙచఛజఝఞటఠడఢణతథదధనపఫబభమయరఱలళవశషసహాిీుూృౄెేైొోౌ్ౠౡౢౣ',
+ kannada:
+ '0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZಂಃಅಆಇಈಉಊಋಎಏಐಒಓಔಕಖಗಘಙಚಛಜಝಞಟಠಡಢಣತಥದಧನಪಫಬಭಮಯರಲಳವಶಷಸಹಾಿೀುೂೃೆೇೈೊೋೌ್೦೧೨೩೪೫೬೭೮೯',
+};
+
+/**
+ * Mapping of language codes to their corresponding symbol sets.
+ */
+export const symbols = {
+ // Abaza
+ abq: alphabets.cyrillic,
+ // Adyghe
+ ady: alphabets.cyrillic,
+ // Africans
+ af: alphabets.latin,
+ // Avar
+ ava: alphabets.cyrillic,
+ // Azerbaijani
+ az: alphabets.latin,
+ // Belarusian
+ be: alphabets.cyrillic,
+ // Bulgarian
+ bg: alphabets.cyrillic,
+ // Bosnian
+ bs: alphabets.latin,
+ // Simplified Chinese
+ chSim: alphabets.zh_sim,
+ // Chechen
+ che: alphabets.cyrillic,
+ // Czech
+ cs: alphabets.latin,
+ // Welsh
+ cy: alphabets.latin,
+ // Danish
+ da: alphabets.latin,
+ // Dargwa
+ dar: alphabets.cyrillic,
+ // German
+ de: alphabets.latin,
+ // English
+ en: alphabets.english,
+ // Spanish
+ es: alphabets.latin,
+ // Estonian
+ et: alphabets.latin,
+ // French
+ fr: alphabets.latin,
+ // Irish
+ ga: alphabets.latin,
+ // Croatian
+ hr: alphabets.latin,
+ // Hungarian
+ hu: alphabets.latin,
+ // Indonesian
+ id: alphabets.latin,
+ // Ingush
+ inh: alphabets.cyrillic,
+ // Icelandic
+ ic: alphabets.latin,
+ // Italian
+ it: alphabets.latin,
+ // Japanese
+ ja: alphabets.japanese,
+ // Karbadian
+ kbd: alphabets.cyrillic,
+ // Kannada
+ kn: alphabets.kannada,
+ // Korean
+ ko: alphabets.korean,
+ // Kurdish
+ ku: alphabets.latin,
+ // Latin
+ la: alphabets.latin,
+ // Lak
+ lbe: alphabets.cyrillic,
+ // Lezghian
+ lez: alphabets.cyrillic,
+ // Lithuanian
+ lt: alphabets.latin,
+ // Latvian
+ lv: alphabets.latin,
+ // Maori
+ mi: alphabets.latin,
+ // Mongolian
+ mn: alphabets.cyrillic,
+ // Malay
+ ms: alphabets.latin,
+ // Maltese
+ mt: alphabets.latin,
+ // Dutch
+ nl: alphabets.latin,
+ // Norwegian
+ no: alphabets.latin,
+ // Occitan
+ oc: alphabets.latin,
+ // Pali
+ pi: alphabets.latin,
+ // Polish
+ pl: alphabets.latin,
+ // Portuguese
+ pt: alphabets.latin,
+ // Romanian
+ ro: alphabets.latin,
+ // Russian
+ ru: alphabets.cyrillic,
+ // Serbian (cyrillic)
+ rsCyrillic: alphabets.cyrillic,
+ // Serbian (latin)
+ rsLatin: alphabets.latin,
+ // Slovak
+ sk: alphabets.latin,
+ // Slovenian
+ sl: alphabets.latin,
+ // Albanian
+ sq: alphabets.latin,
+ // Swedish
+ sv: alphabets.latin,
+ // Swahili
+ sw: alphabets.latin,
+ // Tabassaran
+ tab: alphabets.cyrillic,
+ // Telugu
+ te: alphabets.telugu,
+ // Tajik
+ tjk: alphabets.cyrillic,
+ // Tagalog
+ tl: alphabets.latin,
+ // Turkish
+ tr: alphabets.latin,
+ // Ukrainian
+ uk: alphabets.cyrillic,
+ // Uzbek
+ uz: alphabets.latin,
+ // Vietnamese
+ vi: alphabets.latin,
+};
+
+/**
+ * Supported OCR language codes (EasyOCR alphabets).
+ * @category Types
+ */
+export type OcrLanguage = keyof typeof symbols;
+
+export const PPOCR_SYMBOLS =
+ '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψωϐϑϒϕϖϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϴϵẠồ–—―‖‘’“”†‡•‥…‰′″※⁎⁰⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₒ₠₡₢₣₤₥₦₧₨₩₪₫€₭₮₯₰₱₲₳₴₵₶₷₸₹₺₻₼₽₾₿℃℉ℎℏℑ℘ℜ™℧Åℵℶℷℸℹ⅀ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼↀↁↂ←↑→↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥↦↧↨↩↪↫↬↭↮↯↰↱↲↳↴↵↶↷↸↹↺↻↼↽↾↿⇀⇁⇂⇃⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇚⇛⇜⇝⇞⇟⇠⇡⇢⇣⇤⇥⇦⇧⇨⇩⇪⇫⇬⇭⇮⇯⇰⇱⇲⇳⇴⇵⇶⇷⇸⇹⇺⇻⇼⇽⇾⇿∀∁∂∃∄∅∆∇∈∉∋∏∑−∓∕∖∙√∛∜∝∞∟∠∡∢∥∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿≀≁≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩≪≫≬≭≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊌⊍⊎⊏⊐⊑⊒⊓⊔⊕⊖⊗⊘⊙⊚⊛⊜⊝⊞⊟⊠⊡⊢⊣⊤⊥⊦⊧⊨⊩⊪⊫⊬⊭⊮⊯⊰⊱⊲⊳⊴⊵⊶⊷⊸⊹⊺⊻⊼⊽⊾⊿⋅⌀⌃⌘⌚⌛⌤⌥⌦⌧⌨〉⌫⌬⌭⌮⌯⍵⍺⎆⎇⎈⎉⎊⎋⎌⎍⎎⎏⎐⎑⎒⎓⎔⎕⎖⎗⎘⎙⎚⎛⎜⎝⎞⎟⎠⎡⎢⎣⎤⎥⎦⎧⎨⎩⎪⎫⎬⎭⎮⎯⎰⎱⎲⎳⎴⎵⎶⎷⎸⎹⎺⎻⎼⎽⎾⎿⏀⏁⏂⏃⏄⏅⏆⏇⏈⏉⏊⏋⏌⏍⏎⏏⏐⏑⏒⏓⏔⏕⏖⏗⏘⏙⏚⏛⏜⏝⏞⏟⏠⏡⏢⏣⏤⏥⏦⏧⏨⏩⏪⏫⏬⏭⏮⏯⏰⏱⏲⏳⏴⏵⏶⏷⏸⏹⏺⏻⏼⏽⏾⏿①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳Ⓜ━╆╳▁█■□▪▫▬▲△▶▷▼▽◀◆◇◉◊○◎●◐◥◻◼◽◾☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♈♉♊♋♌♍♎♏♐♑♒♓♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚇⚈⚉⚊⚋⚌⚍⚎⚏⚐⚑⚒⚓⚔⚕⚖⚗⚘⚙⚚⚛⚜⚝⚞⚟⚠⚡⚢⚣⚤⚥⚦⚧⚨⚩⚪⚫⚬⚭⚮⚯⚰⚱⚲⚳⚴⚵⚶⚷⚸⚹⚺⚻⚼⚽⚾⚿⛀⛁⛂⛃⛄⛅⛆⛇⛈⛉⛊⛋⛌⛍⛎⛏⛐⛑⛒⛓⛔⛕⛖⛗⛘⛙⛚⛛⛜⛝⛞⛠⛡⛢⛣⛤⛥⛦⛧⛨⛩⛪⛫⛬⛭⛮⛯⛲⛳⛵⛶⛺⛽⛾⛿✂✅✆✇✈✉✊✋✌✍✎✏✐✑✒✓✔✕✖✘✙✚✛✜✝✞✟✠✡✢✣✤✥✦✧✨✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀❁❂❃❄❅❆❇❈❉❊❋❌❍❎❏❐❑❒❓❔❕❖❗❘❙❚❛❜❝❞❡❢❣❤❥❦❧❨❩❪❫❬❭❮❯❰❱❲❳❴❵❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓➔➕➖➗➘➙➚➛➜➝➞➟➠➡➢➣➤➥➦➧➨➩➪➫➬➭➮➯➰➱➲➳➴➵➶➷➸➹➺➻➼➽➾➿⟶⟹⤴⤵⬅⬆⬇⬛⬜⭐⭕ 、。々〆〇〈〉《》「」『』【】〒〔〕〖〗〜〰〵〽ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゑをんゕゖゝゞァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヲンヴヵヶヷヸヹヺ・ーヽヾヿ㈱㊗㊙㐂㐱㑇㑊㑳㑺㒺㕁㕑㕥㕮㘎㘭㙍㙘㙟㙦㚷㛃㛚㛹㝛㝠㝡㟁㟃㠇㠓㠣㠯㠶㡌㢘㤘㤙㥄㥫㥮㧐㧑㧟㧱㨗㨪㩗㩦㩳㪚㪟㫰㬉㬊㬎㬚㬪㭎㭕㮣㮾㰀㳄㳇㳒㳘㳚㴔㴪㴱㵐㶲㸃㸆㸌㺄㻬㼝㽏㽞㿠䁖䂮䃅䃎䃮䅟䈰䊀䌹䎃䎖䏝䏡䏲䐃䓖䓛䓣䓨䓫䓬䖝䗖䗛䗪䗬䗴䘏䘑䘚䜣䝉䝔䝙䠀䠶䢺䢼䣘䥽䦃䮄䰟䰾䲁䲟䲠䲢䴉䴓䴔䴕䴖䴗䴘䴙䶊䶮一丁七丄万丈三上下丌不与丏丐丑专且丕世丘丙业丛东丝丞丟両丢两严並丧个丫中丮丰丱串临丸丹为主丼丽举丿乂乃久么义之乌乍乎乏乐乒乓乔乖乗乘乙乜九乞也习乡书乩乭买乱乳乸乹乾亀亂了予争亊事二亍于亏云互亓五井亘亙亚些亜亞亟亡亢交亥亦产亨亩享京亭亮亯亰亱亲亳亵亶亷亸亹人亾亿什仁仂仃仄仅仆仇仉今介仍从仏仑仓仔仕他仗付仙仛仜仝仞仟仡代令以仨仩仪仫们仮仰仱仲仳仵件价仺任仼份仿伀企伃伈伉伊伋伍伎伏伐休伔伕众优伙会伛伝伞伟传伢伣伤伥伦伧伪伫伬伭伯估伱伲伴伶伷伸伺伻似伽伾佀佁佃但佇佈佉佋佌位低住佐佑佒体佔何佖佗佘余佚佛作佝佞佟你佢佣佤佥佧佩佪佬佯佰佳佴併佶佷佸佹佺佻佼佽佾使侀侁侂侃侄侅來侇侈侉侊例侍侏侐侑侔侖侗侘侚供侜依侞侠価侣侥侦侧侨侩侪侬侮侯侲侳侴侵侶侷侹侻便俁係促俄俅俇俉俊俋俌俍俎俏俐俑俓俔俗俘俙俚俛俜保俞俟俠信俣俤俦俨俩俪俫俬俭修俯俱俳俴俵俶俷俸俺俽俾倀倃倅倆倇倈倉個倌倍倏倐們倒倓倔倕倖倗倘候倚倛倜倞借倠倡倢倣値倥倦倧倨倩倪倫倬倭倮倰倳倴倶倷倸倹债倻值倾偁偃偅假偈偉偊偋偌偍偎偏偐偓偕偘做偛停偝偞偟偠偡偢健偨偩偪偫偬偭偯偰偲側偵偶偷偸偺偻偽偾偿傀傂傃傅傈傉傋傌傍傎傑傒傔傕傖傘備傚傛傜傝傞傢傣傥傧储傩催傭傮傯傰傱傲傳傴債傷傺傻傽傾傿僁僂僄僅僆僇僈僉僊僋働僎像僑僓僔僕僖僗僙僚僛僜僝僞僡僤僥僦僧僨僩僪僬僭僮僰僱僳僴僵僶僸價僻僽僾僿儀儁儂儃億儆儇儈儉儋儌儐儑儒儓儔儕儗儘儚儛儜儞償儠儡儢儤儥儦優儫儰儱儲儳儴儵儷儸儹儺儻儼儽儿兀允元兄充兆兇先光克兌免兎児兑兒兔兕兖兗兙党兛兜兝兞兠兡兢兣入內全兩兪八公六兮兰共关兴兵其具典兹养兼兽兿冀冁冂冄内円冇冈冉冊册再冏冐冑冒冓冔冕冗冘写冚军农冞冠冢冣冤冥冧冨冪冬冮冯冰冱冲决冴况冶冷冺冻冼冽净凃凄凅准凇凈凉凊凋凌凍减凑凔凖凘凛凜凝凞几凡凢凤処凪凫凬凭凯凰凱凳凵凶凸凹出击凼函凿刀刁刂刃刄分切刈刉刊刌刍刎刑划刓刖列刘则刚创刜初刞删判別刦刧刨利刪别刬刭刮到刱刲刳刵制刷券刹刺刻刼刽刿剀剁剂剃剄剅剆則剉削剋剌前剎剏剐剑剒剔剕剖剗剚剛剜剝剞剟剡剣剤剥剧剩剪剫剬剭剮副剰割剳剴創剷剸剹剺剻剼剽剿劀劁劂劃劄劇劈劉劊劋劌劍劐劑劓劔劖劗劘劙力劝办功加务劢劣劦动助努劫劬劭励劲劳労劵効劼劾势勁勃勅勇勉勋勌勍勐勑勒勔動勖勗勘務勚勛勝勞募勢勣勤勦勧勩勰勱勲勳勴勵勷勸勺勻勾勿匀匁匂匃匄包匆匈匊匋匍匏匐匕化北匙匜匝匟匠匡匢匣匦匪匭匮匯匰匱匲匴匷匸匹区医匼匽匾匿區十千卅升午卉半卋卌卍华协卐卑卒卓協单卖南単博卜卞卟占卡卢卣卤卦卧卫卬卮卯印危卲即却卵卷卸卹卺卻卼卽卿厀厂厄厅历厉压厌厍厎厒厓厔厕厖厗厘厙厚厜厝厞原厠厢厣厤厥厦厨厩厬厭厮厰厲厳厴厹去厾县叁参參叄叆叇又叉及友双反収发叔叕取受变叙叚叛叟叠叡叢口古句另叨叩只叫召叭叮可台叱史右叵叶号司叹叻叼叽吁吃各吆合吉吊吋同名后吏吐向吒吓吔吕吖吗吚君吝吞吟吠吡吣吥否吧吨吩吪含听吭吮启吰吱吲吳吴吵吶吷吸吹吻吼吽吾呀呂呃呆呇呈呉告呋呌呎呐呑呒呓呔呕呖呗员呙呛呜呢呣呤呥呦周呪呫呬呰呱呲味呴呵呶呷呸呺呻呼命呾呿咀咁咂咄咅咆咇咈咉咊咋和咍咎咏咐咑咒咔咕咖咗咘咙咚咛咝咟咠咡咢咣咤咥咦咧咨咩咪咫咬咭咮咯咱咲咳咴咶咷咸咺咻咼咽咾咿哀品哂哃哄哅哆哇哈哉哌响哎哏哐哑哒哓哔哕哖哗哙哚哝哞哟員哢哤哥哦哧哨哩哪哫哭哮哱哲哳哶哺哼哽哿唁唃唄唅唆唇唈唉唌唏唐唑唒唔唕唖唗唘唚唛唝唠唢唣唤唦唧唪唫唬唭售唯唰唱唲唳唴唵唶唷唸唻唼唾唿啁啃啄啅商啉啊啍啎問啐啑啒啓啕啖啗啚啜啞啟啡啢啣啤啥啦啧啪啫啬啭啮啯啰啱啲啴啵啶啷啸啻啼啽啾喀喁喂喃善喆喇喈喉喊喋喌喍喏喑喒喓喔喘喙喚喜喝喟喢喣喤喥喦喧喨喩喪喫喬喭單喰喱喲喳喵営喷喹喻喼喽喾喿嗀嗁嗃嗄嗅嗆嗇嗈嗉嗊嗋嗌嗍嗎嗏嗐嗑嗒嗓嗔嗕嗖嗚嗛嗜嗝嗞嗟嗡嗢嗣嗤嗥嗦嗧嗨嗩嗪嗫嗬嗮嗯嗲嗳嗵嗶嗷嗹嗺嗼嗽嗾嗿嘀嘁嘂嘄嘅嘆嘈嘉嘌嘍嘎嘏嘐嘑嘒嘓嘔嘕嘖嘗嘘嘚嘛嘜嘝嘞嘟嘠嘡嘢嘣嘤嘥嘧嘩嘬嘭嘮嘯嘰嘱嘲嘳嘴嘵嘶嘷嘸嘹嘻嘽嘾嘿噀噁噂噅噆噇噈噉噊噌噍噎噏噓噔噗噘噙噚噛噜噝噞噠噢噣噤噥噦器噩噪噫噬噭噮噯噰噱噲噳噴噶噸噹噺噻噼嚀嚃嚄嚅嚆嚇嚈嚌嚍嚎嚏嚐嚒嚓嚕嚗嚘嚙嚚嚜嚞嚟嚢嚣嚤嚥嚦嚧嚨嚩嚪嚫嚬嚭嚮嚯嚲嚳嚴嚵嚶嚷嚼嚽嚾嚿囀囁囂囃囅囆囈囉囊囋囌囍囑囒囓囔囗囙囚四囝回囟因囡团団囤囧囪囫园囮囯困囱囲図围囵囶囷囹固国图囿圀圁圂圃圄圅圆圇圈圉圊國圌圍圏圐園圓圖圗團圙圜圞土圠圢圣圧在圩圪圫圬圭圮圯地圲圳圹场圻圾址坁坂坅均坉坊坋坌坍坎坏坐坑坒坖块坚坛坜坝坞坟坠坡坣坤坥坦坨坩坪坫坬坭坮坯坰坱坲坳坴坵坶坷坻坼坽坿垀垂垃垄垆垈型垌垍垎垏垒垓垔垕垗垘垙垚垛垜垝垞垟垠垡垢垣垤垦垧垩垫垭垮垯垰垱垲垴垵垸垺垻垼垽垾垿埂埃埅埆埇埈埋埌城埏埒埔埕埗埘埙埚埜埝域埠埡埢埤埧埨埪埫埭埮埯埳埴埵埶執埸培基埻埼埽堀堂堃堅堆堇堈堉堊堋堌堍堎堐堑堔堕堖堘堙堝堞堠堡堣堤堥堦堧堨堩堪堮堯堰報堲場堵堷堺堼堽堾堿塀塁塄塅塆塈塉塊塋塌塍塏塑塒塓塔塕塗塘塙塚塜塝塞塟塡塢塤塥塨塩填塬塭塯塰塱塲塴塵塸塹塺塼塽塾塿墀墁境墅墆墈墉墊墋墎墐墒墓墕墖増墘墙墚墜墝增墟墠墡墣墦墨墩墪墫墬墮墯墱墳墶墺墻墼墽墾壁壄壅壆壇壈壊壋壌壎壐壑壓壔壕壖壘壙壚壛壝壞壟壠壢壤壩壪士壬壮壯声壱売壳壴壶壷壸壹壺壻壼壽壾壿夀夃处夆备変夊夋夌复夎夏夐夒夔夕外夗夘夙多夜够夠夢夤夥大天太夫夬夭央夯失头夷夸夹夺夼夾奀奁奂奄奅奇奈奉奊奋奎奏奐契奓奔奕奖套奘奚奠奡奢奣奥奧奨奩奪奫奬奭奮奰奲女奴奶奸她奼好妀妁如妃妄妆妇妈妊妍妎妏妐妑妒妓妖妗妘妙妝妞妠妢妣妤妥妦妧妨妩妪妫妬妭妮妯妲妳妵妷妸妹妺妻妾姀姁姃姆姈姉姊始姌姍姎姏姐姑姒姓委姗姘姙姚姜姝姞姡姣姤姥姦姨姪姫姬姮姱姵姶姸姹姺姻姼姽姿娀威娃娄娅娆娇娈娉娋娌娍娎娑娓娖娘娙娛娜娟娠娣娥娩娫娭娯娱娲娳娴娵娶娷娸娼娽娿婀婁婆婉婊婌婍婐婑婓婕婗婘婚婞婟婠婢婣婤婥婦婧婪婫婬婭婯婳婴婵婶婷婸婺婻婼婿媂媃媄媆媊媐媒媓媔媕媖媗媚媛媜媞媟媢媥媦媧媪媬媭媮媯媰媱媲媳媵媸媺媻媼媽媾媿嫁嫂嫄嫇嫈嫉嫋嫌嫐嫒嫔嫕嫖嫗嫘嫚嫛嫜嫠嫡嫢嫣嫥嫦嫩嫪嫫嫭嫮嫰嫱嫲嫳嫴嫵嫶嫷嫺嫻嫽嫿嬀嬃嬅嬈嬉嬋嬌嬎嬐嬓嬖嬗嬙嬛嬝嬡嬢嬤嬥嬨嬪嬬嬭嬰嬲嬴嬷嬸嬽嬾嬿孀孁孃孅孆孈孋孌子孑孓孔孕孖字存孙孚孛孜孝孟孢季孤孥学孩孪孫孬孮孰孱孲孳孵孷學孺孻孼孽孿宁宂它宄宅宇守安宋完宍宎宏宓宕宗官宙定宛宜宝实実宠审客宣室宥宦宧宨宪宫宬宮宰害宴宵家宸容宽宾宿寀寁寂寃寄寅密寇寈寊寋富寍寐寑寒寓寔寕寖寗寘寙寛寜寝寞察寠寡寢寤寥實寧寨審寪寫寬寮寯寰寱寳寵寶寸对寺寻导対寿封専尃射尅将將專尉尊尋尌對導小尐少尒尓尔尕尖尗尘尙尚尜尝尟尢尤尥尧尨尪尬尭尰就尳尴尷尸尹尺尻尼尽尾尿局屁层屃屄居屆屇屈屉届屋屌屍屎屏屐屑屓屔展屖屘屙屚屜屝属屠屡屢屣層履屦屧屨屩屬屭屮屯山屴屹屺屻屼屾屿岀岁岂岅岈岉岊岋岌岍岏岐岑岒岔岖岗岘岙岚岛岜岝岞岟岠岡岢岣岤岧岨岩岪岫岬岭岮岯岰岱岳岵岷岸岻岽岿峁峂峃峄峅峇峉峊峋峌峍峐峒峗峘峙峚峛峝峞峠峡峣峤峥峦峧峨峩峪峬峭峮峯峰峱峴島峷峻峼峽峿崀崁崂崃崄崆崇崈崋崌崍崎崏崐崑崒崔崖崗崘崙崚崛崝崞崟崠崡崢崣崤崥崦崧崨崩崬崭崮崰崱崳崴崵崶崷崺崽崾崿嵁嵂嵃嵅嵇嵊嵋嵌嵎嵐嵑嵒嵕嵖嵗嵘嵙嵚嵛嵜嵝嵞嵣嵥嵨嵩嵫嵬嵮嵯嵰嵱嵲嵴嵷嵺嵻嵽嵾嵿嶀嶁嶂嶃嶄嶅嶆嶇嶈嶉嶊嶋嶌嶍嶒嶓嶔嶕嶗嶙嶚嶜嶝嶞嶟嶠嶡嶢嶦嶧嶨嶩嶪嶬嶭嶮嶰嶱嶲嶴嶵嶷嶸嶺嶼嶽巀巂巃巄巅巆巇巉巋巌巍巎巏巑巒巔巕巖巗巘巛川州巟巡巢巣工左巧巨巩巫差巯巰己已巳巴巵巶巷巹巻巽巾巿帀币市布帅帆师帊希帏帐帑帔帕帖帗帘帙帚帛帜帝帟帠帡帢帣帤帥带帧帨帩師帬席帮帯帰帱帳帴帶帷常帻帼帽帾幀幂幃幄幅幇幌幎幏幑幓幔幕幖幗幘幙幚幛幝幞幟幠幡幢幣幦幧幨幩幪幫幬幭幰干平年幵并幷幸幹幺幻幼幽幾广庀庁庂広庄庆庇庈庉床庋庌序庐庑库应底庖店庙庚庛府庞废庠庢庣庤庥度座庨庪庫庬庭庮庰庱庲庳庴庵庶康庸庹庻庼庽庾廁廂廃廄廅廆廇廈廉廊廋廌廍廎廐廑廒廓廔廕廖廗廘廙廚廛廜廝廞廟廠廡廢廣廥廦廧廨廩廪廬廮廯廰廱廳廵延廷廸廹建廻廼廾廿开弁异弃弄弅弆弇弈弉弊弋弌弍式弐弑弒弓弔引弖弗弘弚弛弝弟张弢弣弤弥弦弧弨弩弭弮弯弱弳張弶強弸弹强弼弾彀彃彄彅彆彈彉彊彋彌彎彏彐归当彔录彖彗彘彙彜彝彞彟彠彡形彣彤彥彦彧彩彪彫彬彭彯彰影彳彴彷彸役彻彼彾彿往征徂徃径待徇很徉徊律後徐徑徒従徕得徘徙徛徜從徠御徥徦徧徨復循徫徬徭微徯徲徳徴徵德徸徹徼徽徾徿忀忁心必忆忉忌忍忏忐忑忒忔忕忖志忘忙応忝忞忠忡忣忤忥忧忨忪快忬忭忮忯忱忳念忷忸忺忻忼忽忾忿怀态怂怃怄怅怆怊怋怍怎怏怐怒怓怔怕怖怗怙怚怛怜思怞怠怡怢急怦性怨怩怪怫怬怭怮怯怱怲怳怴怵怷怹总怼怿恀恁恂恃恅恆恇恉恊恋恌恍恐恒恓恔恕恙恚恛恝恞恟恠恡恢恣恤恥恧恨恩恪恫恬恭息恰恲恳恵恶恸恹恺恻恼恽恿悀悁悃悄悅悆悇悈悉悊悌悍悐悒悔悕悖悗悚悛悝悞悟悠悢患悤悦悧您悩悪悫悬悭悯悰悱悲悳悴悵悶悷悸悹悺悻悼悽悾惄情惆惇惈惉惊惋惌惎惏惑惓惔惕惘惙惚惛惜惝惟惠惡惢惣惤惥惦惧惨惩惪惫惬惭惮惯惰惱惲想惴惵惶惷惸惹惺惻惼惾愀愁愃愅愆愈愉愊愍愎意愐愒愓愔愕愖愚愛愜感愠愣愤愦愧愨愫愬愭愮愯愲愴愶愷愼愽愾愿慁慂慄慅慆慇慈慉慊態慌慍慎慑慒慓慔慕慘慚慛慜慝慞慟慢慣慤慥慧慨慪慫慬慭慮慰慱慲慳慴慵慶慷慹慺慼慽慾憀憂憃憇憉憊憋憍憎憐憑憒憓憔憕憖憙憚憛憝憟憡憢憤憧憨憩憪憫憬憭憮憯憰憱憲憴憵憶憷憸憺憻憼憾憿懁懂懃懅懆懇懈應懊懋懌懍懐懑懒懔懘懞懟懠懣懤懥懦懧懨懪懫懭懮懰懱懲懵懶懷懸懺懻懼懽懾懿戀戁戃戄戆戇戈戉戊戋戌戍戎戏成我戒戔戕或戗战戙戚戛戞戟戠戡戢戣戤戥戦戧戩截戬戭戮戯戰戱戲戳戴戶户戸戹戺戻戼戽戾房所扁扂扃扅扆扇扈扉扊手才扎扐扑扒打扔払托扙扛扜扞扡扢扣扤扥扦执扩扪扫扬扭扮扯扰扱扳扶批扺扻扼扽找承技抃抄抆抇抉把抌抎抏抑抒抓抔投抖抗折抚抛抜抝択抟抠抡抢护报抦抨抪披抬抭抮抱抴抵抶抹抻押抽抾抿拂拃拄担拆拇拈拉拊拋拌拍拎拏拐拑拒拓拔拕拖拗拘拙拚招拜拝拟拠拡拢拣拤拥拦拧拨择拫括拭拮拯拱拲拳拴拵拶拷拸拹拺拼拽拾拿挀持挂挃指挈按挋挌挍挎挏挐挑挓挖挙挚挛挝挞挟挠挡挣挤挥挦挨挩挪挫挬振挱挲挴挵挶挹挺挻挼挽挾挿捁捂捃捄捅捆捉捊捋捌捍捎捏捐捒捔捕捖捗捘捜捞损捡换捣捥捦捧捨捩捫捭据捯捱捲捵捶捷捺捻捼捽掀掂掃掄掇授掉掊掌掎掏掐排掔掖掗掘掙掛掜掝掞掟掠採探掣掤接控推掩措掫掬掭掮掯掰掱掲掳掴掷掸掺掻掼掽掾揀揃揄揅揆揉揊揍揎描提揑插揔揕揖揘揚換揜揝揟揠握揣揤揥揨揩揪揫揭揮揯揱揲揳援揵揶揷揸揹揺揽揿搀搁搂搅搆搉搊搋搌損搏搐搒搓搔搕搖搗搘搚搛搜搞搟搠搡搢搣搤搥搦搧搨搪搫搬搭搯搰搳搴搵搶搷搹携搽搾摀摁摂摃摄摅摆摇摈摊摋摍摎摏摐摑摒摓摔摘摛摜摝摞摟摠摡摦摧摩摭摮摯摰摲摳摴摵摶摸摹摺摻摽摿撂撃撄撅撇撈撊撋撌撏撐撑撒撓撕撖撗撙撚撜撝撞撟撠撢撣撤撥撦撩撫撬播撮撰撲撳撵撷撸撹撺撻撼撽撾撿擀擁擂擄擅擇擉擊擋操擎擏擐擒擓擔擕擖擗擘據擞擠擡擢擣擤擥擦擧擨擩擫擬擭擯擰擱擲擴擷擸擺擻擼擽擾擿攀攄攆攇攉攌攍攏攐攒攓攔攕攖攗攘攙攛攜攝攞攠攡攢攣攤攥攦攩攪攫攬攭攮支攲攳攴收攷攸改攻攽放政敁敂故敆效敉敊敌敍敎敏救敓敔敕敖敗敘教敛敜敝敞敟敢散敤敦敧敩敪敫敬敭敯数敲敳整敵敶敷數敹敺敻敾敿斀斁斂斃斄文斉斋斌斎斐斑斒斓斔斕斖斗料斛斜斝斞斟斠斡斤斥斧斨斩斪斫斬断斮斯新斲斵斶斷方斻於施斿旁旂旃旄旅旆旉旋旌旍旎族旐旒旓旖旗旙旚旛旝旞旟无旡既日旦旧旨早旬旭旮旯旰旱旲旳旴旵时旷旸旺旻旼旽旾旿昀昂昃昄昆昇昈昉昊昌昍明昏昐昒易昔昕昙昚昛昜昝昞星映昡昢昣昤春昦昧昨昪昫昬昭是昰昱昲昳昴昵昶昺昼昽显晁時晃晅晇晈晉晊晋晌晏晐晒晓晔晕晖晗晙晚晛晜晝晞晟晡晢晤晥晦晧晨晩晪晫晬晭普景晰晱晳晴晶晷晸智晻晼晾暀暁暂暃暄暅暆暇暈暉暊暋暌暍暎暏暐暑暕暖暗暘暝暟暠暡暢暦暧暨暩暫暮暰暱暲暴暵暶暸暹暻暾暿曀曄曅曆曇曈曉曊曋曌曏曒曔曖曙曚曛曜曝曠曡曣曦曧曨曩曬曭曮曰曲曳更曶曷書曹曺曼曽曾替最朁會朄朅月有朊朋服朏朐朒朓朔朕朖朗朘望朝朞期朡朢朣朦朧木未末本札朮术朱朳朴朵朶朸朹机朻朼朽朾朿杀杁杂权杄杅杆杇杈杉杋杌李杏材村杓杕杖杗杙杜杝杞束杠条杢杣杤来杧杨杩杪杬杭杮杯杰東杲杳杴杵杶杷杻杼松板极构枅枆枇枉枋枌枍枎枏析枒枓枕枖林枘枚枛果枝枞枠枡枢枣枥枧枨枪枫枭枯枰枱枲枳枴枵架枷枸枹枺枻枼柀柁柂柃柄柅柈柉柊柍柎柏某柑柒染柔柖柘柙柚柜柝柞柟柠柢柣柤查柧柩柫柬柮柯柰柱柲柳柴柵柶柷柸柹柺査柽柾柿栀栁栂栃栄栅标栈栉栊栋栌栎栏栐树栒栓栔栖栗栘栚栜栝栞栟校栢栨栩株栫栭栯栰栱栲栳栴栵样核根栻格栽栾桀桁桂桃桄桅框案桉桊桋桌桎桏桐桑桒桓桔桕桖桙桜桝桟桠桡桢档桤桥桦桧桨桩桫桭桮桯桱桲桴桵桶桷桹桺桼桾桿梀梁梂梃梅梆梇梉梌梏梐梒梓梔梗梘梛梜條梟梠梡梢梣梦梧梨梩梫梬梭梮梯械梱梲梳梴梵梶梼梽梾梿检棁棂棃棄棆棉棊棋棌棍棐棒棓棕棖棗棘棚棛棜棟棠棡棣棤棧棨棩棪棫棬森棯棰棱棲棳棵棶棷棸棹棺棻棼棽椀椁椄椅椆椇椈椉椋椌植椎椏椐椑椒椓椔椗椙検椟椠椤椥椪椭椰椲椳椴椵椶椸椹椽椿楀楂楄楅楈楉楊楎楒楓楔楕楖楗楘楙楚楛楝楞楟楠楡楢楣楤楥楦楧楨楩楪楫楬業楮楯楰楱楳極楶楷楸楹楺楼楽楿概榃榄榅榆榇榈榉榊榍榎榑榔榕榖榗榘榙榛榜榞榠榢榣榤榦榧榨榩榪榫榬榭榮榯榰榱榲榴榶榷榹榻榼榽榾榿槀槁槃槄槇槉槊構槌槍槎槐槑槓槔槕槗様槙槚槛槜槟槠槢槤槥槧槨槩槬槭槮槰槱槲槳槷槸槺槻槼槽槾槿樀樁樂樅樆樊樋樑樓樔樕樗樘標樛樝樞樟樠模樣樧樨権横樫樯樱樲樴樵樸樹樺樻樽樾樿橀橁橄橇橈橉橋橐橑橒橓橘橙橚橛橜橝橞機橡橢橤橥橦橧橨橪橫橭橱橶橹橼橾橿檀檁檃檄檇檉檊檍檎檐檑檓檔檕檖檗檛檜檝檞檟檠檡檢檣檥檦檨檩檫檬檮檯檳檵檸檹檻檽櫂櫃櫅櫆櫈櫋櫌櫐櫑櫓櫙櫚櫛櫜櫝櫞櫟櫠櫡櫥櫧櫨櫪櫫櫬櫰櫱櫳櫸櫹櫺櫻櫼櫾櫿欂欃欄欅欈欉權欋欏欐欑欒欖欘欞欠次欢欣欤欥欧欨欬欭欯欱欲欳欴欵欶欷欸欹欺欻欽款欿歂歃歅歆歇歈歉歊歋歌歍歎歐歓歔歕歙歛歜歟歠歡止正此步武歧歩歪歭歯歲歳歴歶歷歸歹死歼歾歿殀殁殂殃殄殆殇殈殉殊残殍殏殑殒殓殔殖殗殘殙殚殛殞殟殠殡殢殣殤殥殦殧殪殫殭殮殯殰殲殳殴段殶殷殺殻殼殽殿毀毁毂毃毄毅毆毇毈毉毊毋毌母毎每毐毒毓比毕毖毗毘毙毚毛毞毡毣毤毦毨毪毫毬毯毰毲毳毴毵毸毹毻毼毽毾毿氀氁氂氄氅氆氇氈氊氌氍氏氐民氓气氕氖気氘氙氚氛氟氠氡氢氣氤氦氧氨氩氪氫氬氮氯氰氲氳水氶氷永氹氻氽氾氿汀汁求汃汆汇汈汉汊汋汍汎汏汐汒汔汕汗汙汚汛汜汝汞江池污汤汦汧汨汩汪汫汭汯汰汱汲汳汴汶汸汹決汽汾沁沂沃沄沅沆沇沈沉沋沌沍沏沐沒沓沔沕沖沘沙沚沛沜沟没沢沣沤沥沦沧沨沩沪沫沬沭沮沯沱河沴沶沷沸油沺治沼沽沾沿況泂泃泄泅泆泇泉泊泌泐泒泓泔法泖泗泙泚泛泜泝泞泠泡波泣泥泧注泩泪泫泬泭泮泯泰泱泲泳泵泷泸泺泻泼泽泾洀洁洄洇洈洊洋洌洍洎洑洒洓洗洘洙洚洛洞洟洢洣洤津洧洨洩洪洫洭洮洱洲洳洴洵洶洸洹洺活洼洽派洿流浃浄浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕浘浙浚浛浜浞浟浠浡浢浣浤浥浦浧浩浪浬浭浮浯浰浲浴浵浶海浸浹浺浼浿涂涄涅涆涇消涉涊涌涍涎涐涑涒涓涔涕涖涗涘涙涛涝涞涟涠涡涢涣涤润涧涨涩涪涫涬涮涯液涳涴涵涷涸涼涽涾涿淀淄淅淆淇淈淊淋淌淍淎淏淑淒淓淔淖淘淙淚淛淜淝淞淟淠淡淢淤淥淦淨淩淪淫淬淭淮淯淰深淳淴淵淶混淸淹淺添淼淽渀渃清渇済渉渊渋渌渍渎渐渑渓渔渕渗渙渚減渜渝渟渠渡渢渣渤渥渦渨温渫測渭渮港渰渱渲渳渴游渹渺渻渼渽渾渿湀湁湃湄湅湆湇湉湊湋湍湎湑湓湔湖湘湛湜湝湞湟湠湡湢湣湥湧湨湩湫湮湯湱湲湳湴湻湼湾湿満溁溃溅溆溇溈溉溋溍溎溏源溓溔準溘溙溚溛溜溝溞溟溠溢溥溦溧溪溫溮溯溰溱溲溳溴溵溶溷溹溺溻溼溽溾滀滁滂滃滄滅滆滇滈滉滋滌滍滎滏滑滒滓滔滕滖滗滘滙滚滛滜滝滞滟滠满滢滤滥滦滧滨滩滪滫滬滭滮滯滱滲滴滵滷滸滹滻滽滾滿漁漂漃漅漆漇漈漉漊漋漎漏漑漒漓演漕漖漘漙漚漠漢漣漤漥漦漧漩漪漫漬漭漮漯漰漱漲漳漴漵漶漷漸漹漻漼漾漿潀潁潃潄潅潆潇潋潍潎潏潐潑潒潔潕潖潗潘潙潚潛潜潝潞潟潠潢潤潦潩潬潭潮潯潰潲潳潴潵潶潷潸潺潼潽潾潿澀澁澂澄澅澆澇澈澉澋澌澍澎澐澒澓澔澖澗澛澜澞澠澡澣澤澥澦澧澨澩澪澫澭澮澯澱澳澴澶澹澻澼澽激濁濂濃濄濆濇濈濉濊濋濌濎濑濒濔濕濘濙濛濜濞濟濠濡濣濤濦濧濩濫濬濭濮濯濰濱濲濴濶濺濻濼濾濿瀀瀁瀄瀅瀆瀇瀉瀋瀌瀍瀎瀏瀑瀔瀕瀖瀘瀙瀚瀛瀝瀞瀟瀠瀡瀢瀣瀦瀧瀨瀩瀫瀬瀯瀰瀱瀲瀴瀵瀶瀷瀸瀹瀺瀼瀾瀿灂灃灄灈灉灊灋灌灏灑灕灖灗灘灛灝灞灟灡灣灤灧灨灩灪火灭灯灰灴灵灶灸灼災灾灿炀炁炅炆炉炊炌炎炒炔炕炖炘炙炜炝炟炣炤炫炬炭炮炯炰炱炲炳炷炸点為炻炼炽炾烀烁烂烃烈烉烊烋烏烑烒烓烔烖烘烙烛烜烝烟烠烤烦烧烨烩烫烬热烯烰烱烴烶烷烹烺烻烽焃焄焆焉焊焌焍焐焓焔焕焖焗焘焙焚焜焞焠無焣焦焮焯焰焱焴然焻焼焿煁煃煅煆煇煉煊煋煌煎煐煑煒煓煔煕煖煗煙煚煜煝煞煟煠煢煣煤煥煦照煨煩煬煮煲煳煴煵煶煸煺煽熀熂熄熅熇熈熉熊熏熐熒熔熖熗熘熙熚熛熜熟熠熤熥熨熬熯熰熱熲熳熵熸熹熺熻熼熾熿燀燁燂燃燄燅燈燉燊燋燎燏燐燒燔燕燖燗燘燙燚燜燝營燠燡燥燦燧燫燬燭燮燴燹燻燼燾燿爀爁爂爆爇爊爌爍爐爓爔爕爚爛爝爞爟爢爣爨爪爬爭爯爰爱爲爵父爷爸爹爺爻爼爽爾爿牀牁牂牄牆片版牉牋牌牍牎牏牒牓牕牖牘牙牚牛牝牟牠牡牢牣牤牥牦牧物牬牮牯牰牲牴牵牷牸特牺牻牼牽牾牿犀犁犂犄犅犆犇犈犉犊犋犌犍犎犏犐犑犒犓犕犖犗犘犚犛犝犞犟犠犢犣犤犥犦犧犨犩犪犬犮犯犰犴犵状犷犸犹犽犿狀狁狂狃狄狆狈狉狊狋狌狍狎狐狑狒狓狔狖狗狘狙狚狛狜狝狞狟狠狡狣狤狥狦狨狩狪狫独狭狮狯狰狱狲狳狴狶狷狸狹狺狻狼狽狾狿猀猁猂猃猄猇猈猊猋猎猏猑猒猓猕猖猗猘猙猛猜猝猞猟猡猢猣猥猦猧猨猩猪猫猬猭献猯猰猱猲猳猴猵猶猷猹猺猻猼猾猿獀獁獂獃獄獅獇獊獋獌獍獎獏獐獑獒獗獘獛獝獞獟獠獡獢獣獥獦獧獨獩獪獫獬獭獮獯獰獲獳獴獵獶獷獸獺獻獼獽獾獿玀玁玂玃玄玅玆率玈玉玊王玎玏玑玒玓玔玕玖玗玘玙玚玛玞玟玠玡玢玤玥玦玧玨玩玫玭玮环现玱玲玳玶玷玹玺玻玼玾玿珀珂珅珇珈珉珊珋珌珍珎珏珐珑珒珓珕珖珙珛珝珞珠珡珢珣珤珥珦珧珩珪珫班珮珰珲珵珶珷珸珹珺珽現琀琁球琄琅理琇琈琉琊琍琎琏琐琔琖琚琛琟琡琢琤琥琦琨琪琫琬琭琮琯琰琱琲琳琴琵琶琹琺琼琿瑀瑁瑂瑃瑄瑅瑆瑇瑈瑊瑋瑑瑒瑓瑔瑕瑖瑗瑙瑚瑛瑜瑝瑞瑟瑠瑢瑣瑤瑥瑧瑨瑩瑪瑬瑭瑯瑰瑱瑲瑳瑴瑵瑶瑷瑺瑽瑾瑿璀璁璃璄璅璆璇璈璉璊璋璌璎璐璒璕璗璘璙璚璜璞璟璠璡璢璣璥璦璧璨璩璪璫璬璮環璱璲璵璸璹璺璽璿瓀瓁瓅瓈瓊瓋瓌瓏瓑瓒瓔瓖瓘瓚瓛瓜瓝瓞瓟瓠瓡瓢瓣瓤瓥瓦瓨瓩瓬瓮瓯瓴瓵瓶瓷瓻瓽瓾瓿甀甂甃甄甈甋甌甍甏甐甑甒甓甔甕甖甗甘甙甚甜甝甞生甡產産甥甦用甩甪甫甬甭甮甯田由甲申电男甸甹町画甽甾甿畀畅畇畈畊畋界畎畏畐畑畔畖留畚畛畜畝畟畠畢畣畤略畦畧番畫畬畯異畱畲畳畴畵當畷畸畹畺畽畿疀疁疃疆疇疊疋疌疍疎疏疐疑疒疔疕疖疗疘疙疚疝疟疠疡疢疣疤疥疧疫疬疭疮疯疰疱疲疳疴疵疸疹疻疼疽疾疿痀痁痂痃痄病症痈痉痊痋痌痍痎痏痐痑痒痓痔痕痗痘痙痚痛痝痞痟痠痡痢痣痤痦痧痨痩痪痫痯痰痱痲痳痴痷痸痹痺痻痼痾痿瘀瘁瘃瘅瘆瘈瘉瘊瘋瘌瘍瘏瘐瘑瘓瘕瘖瘗瘘瘙瘛瘜瘝瘞瘟瘠瘡瘢瘣瘤瘥瘦瘧瘩瘪瘫瘭瘮瘯瘰瘱瘲瘳瘴瘵瘸瘺瘻瘼瘽瘾瘿癀療癃癄癅癆癇癈癉癌癍癐癒癓癔癖癗癘癙癜癞癟癠癡癢癣癤癥癧癩癪癫癬癭癮癯癰癱癲癵癸癹発登發白百癿皁皂的皆皇皈皊皋皎皏皐皑皓皕皖皙皚皛皜皝皞皤皦皫皭皮皯皰皱皲皴皵皷皸皺皻皽皾皿盂盃盄盅盆盇盈盉益盋盌盍盎盏盐监盒盓盔盖盗盘盛盜盝盞盟盡監盤盥盦盧盨盩盪盫盬盭目盯盱盲盳直盷相盹盺盻盼盾眀省眃眄眅眇眈眉眊看県眍眐眑眒眓眕眙眚眛眜眝眞真眠眢眣眥眦眨眩眬眭眯眱眲眳眴眵眶眷眸眹眺眼眽眾着睁睃睄睅睆睇睊睋睌睍睎睏睐睑睒睔睕睖睚睛睜睞睟睠睡睢督睥睦睧睨睩睪睫睬睭睮睯睹睺睼睽睾睿瞀瞁瞂瞄瞅瞇瞉瞋瞌瞍瞎瞏瞑瞒瞓瞖瞘瞚瞛瞜瞝瞞瞟瞠瞡瞢瞥瞧瞩瞪瞫瞬瞭瞰瞲瞳瞴瞵瞶瞷瞻瞼瞽瞿矁矂矇矉矊矌矍矎矏矐矓矔矕矗矘矙矚矛矜矞矠矢矣知矧矩矫矬短矮矯矰矱矲石矴矶矸矹矺矻矼矽矾矿砀码砂砄砅砆砉砋砌砍砎砏砐砑砒研砕砖砗砘砚砜砝砟砠砢砣砥砦砧砨砩砫砬砭砮砯砰砲砳破砵砷砸砹砺砻砼砾础硁硂硃硅硇硈硉硊硌硍硎硏硐硒硓硔硕硖硗硙硚硜硝硠硤硥硨硩硪硫硬硭确硯硰硱硼硾硿碁碃碄碅碆碇碈碉碌碍碎碏碑碓碔碕碗碘碚碛碜碞碟碡碣碥碧碨碩碪碫碭碰碱碲碳碴碶碸碹確碻碼碽碾磁磃磅磈磉磊磋磌磍磎磏磐磑磔磕磘磙磚磛磜磝磞磟磡磣磥磧磨磩磪磬磯磱磲磳磴磵磷磹磺磻磼磽磾磿礁礂礄礅礉礌礎礐礑礒礓礔礙礛礜礝礞礠礣礥礦礧礨礪礫礬礭礮礱礴礵示礼礽社礿祀祁祂祃祅祆祇祈祉祊祋祌祎祏祐祑祒祓祔祕祖祗祙祚祛祜祝神祟祠祡祢祣祤祥祧票祩祪祫祭祯祲祳祴祷祸祹祺祼祾祿禀禁禂禃禄禅禇禊禋禍禎福禑禒禓禔禕禖禗禘禚禛禜禟禠禡禤禦禧禨禩禪禫禬禭禮禰禱禳禴禵禷禸禹禺离禼禽禾禿秀私秃秅秆秈秉秊秋种秎秏科秒秔秕秖秘租秠秣秤秦秧秩秪秫秬秭秮积称秳秶秸秺移秽秾稀稂稃稅稆稈稉稊程稌稍税稑稒稔稗稘稙稚稜稞稟稠稣稫稬稭種稯稰稱稲稳稷稹稺稻稼稽稾稿穀穂穄穅穆穇穈穉穊穋穌積穎穏穑穖穗穙穛穜穟穠穡穢穣穤穧穨穩穫穬穭穮穰穱穴穵究穷穸穹空穽穾穿窀突窃窄窅窆窈窉窊窋窌窍窎窏窐窑窒窓窔窕窖窗窘窙窜窝窞窟窠窢窣窥窦窨窩窪窫窬窭窮窯窰窱窲窳窴窵窶窸窺窻窾窿竀竁竃竄竅竇竈竊立竑竖竘站竚竜竝竞竟章竢竣童竦竩竪竫竭端竴競竷竹竺竻竽竿笃笄笅笆笈笊笋笏笐笑笓笔笕笘笙笛笝笞笠笢笤笥符笨笩笪笫第笭笮笯笰笱笲笳笴笵笸笹笺笻笼笾筀筄筅筆筇筈等筊筋筌筍筏筐筑筒答策筘筚筛筜筝筞筠筡筢筤筥筦筧筩筬筭筮筯筰筱筲筳筴筵筶筷筹筻筼签简箄箅箆箇箈箊箋箌箍箎箏箐箑箒箓箔箕箖算箘箙箛箜箝箠管箢箦箧箨箩箪箫箬箭箯箱箴箷箸箹箾節篁範篆篇築篊篋篌篎篑篓篔篕篙篚篛篝篞篠篡篣篤篥篦篧篨篩篪篭篮篯篱篲篳篴篷篸篹篻篼篽篾篿簀簁簂簃簅簇簉簋簌簍簎簏簑簒簕簖簗簙簜簝簞簟簠簡簢簣簥簦簧簨簩簪簫簬簭簮簰簳簷簸簹簺簻簼簽簾簿籀籁籃籈籉籊籌籍籐籔籗籙籛籜籝籟籠籣籤籥籦籧籩籪籬籮籯籲米籴籸籹籺类籼籽籾粂粃粄粇粉粊粋粍粑粒粔粕粗粘粛粜粝粞粟粢粤粥粦粧粩粪粮粱粲粳粵粹粺粻粼粽精粿糀糁糅糇糈糉糊糌糍糎糒糔糕糖糗糙糜糝糞糟糠糢糧糨糪糬糯糰糱糲糴糵糶糷糸糹糺系糾紀紂紃約紅紆紇紈紉紊紋納紎紐紑紒紓純紕紖紗紘紙級紛紜紝紞紟素紡索紥紧紩紫紬紮累細紱紲紳紵紶紸紹紺紼紽紾紿絀絁終絃組絅絆絇経絎絏結絓絕絖絘絛絜絝絞絡絢絣給絧絨絪絫絭絮絯絰統絲絳絵絶絷絹絺絻絼絽絿綀綁綃綄綅綆綈綉綌綍綎綏綑經綔綖継続綜綝綞綟綠綢綣綦綧綪綫綬維綮綯綰綱網綳綴綵綷綸綹綺綻綼綽綾綿緀緁緂緃緄緅緆緇緈緊緋緌緎総緐緑緒緔緖緗緘緙線緛緜緝緞締緡緣緤緥緦緧編緩緪緬緯緰緱緲緳練緵緶緷緹緺緻縁縂縃縄縅縈縉縊縋縌縎縏縐縑縒縓縕縗縚縛縜縝縞縟縠縡縢縣縤縦縧縩縪縫縭縮縯縰縱縲縳縴縵縶縷縸縹縻縼總績縿繀繁繂繃繄繅繆繇繈繉繊繋繍繐繑繒織繕繖繘繙繚繜繝繞繟繠繡繢繣繦繧繩繪繫繭繮繯繰繲繳繴繵繶繷繸繹繺繻繼繽繾纀纁纂纆纇纈纊纋續纍纏纐纑纒纓纔纕纖纗纘纚纛纜纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缌缎缐缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵缶缸缹缺缽缾缿罂罃罄罅罇罈罉罊罋罌罍罎罏罐网罔罕罗罘罚罛罜罝罞罟罠罡罢罣罥罦罧罨罩罪罫罬罭置罰罱署罳罴罵罶罷罹罺罻罼罽罾罿羁羂羃羅羆羇羈羉羊羋羌羍美羑羒羓羔羕羖羗羙羚羛羜羝羞羟羠羡羢羣群羥羦羧羨義羬羭羯羰羱羲羳羴羵羶羷羸羹羺羼羽羾羿翀翁翂翃翄翅翇翈翊翋翌翍翎翏翐翑習翔翕翘翙翚翛翜翟翠翡翢翣翥翦翨翩翪翫翬翭翮翯翰翱翲翳翴翷翹翺翻翼翽翾翿耀老考耄者耆耇耋而耍耎耏耐耑耒耔耕耖耗耘耙耜耞耟耠耡耢耤耥耦耧耨耩耪耬耮耰耱耳耴耵耶耷耸耹耻耽耾耿聂聃聆聇聈聊聋职聍聏聑聒联聖聘聚聝聞聟聡聨聩聪聯聰聱聲聳聴聵聶職聹聽聾聿肂肃肄肅肆肇肉肋肌肎肏肐肒肓肕肖肘肙肚肛肜肝肟肠股肢肣肤肥肧肩肪肫肭肮肯肱育肴肵肷肸肹肺肼肽肾肿胀胁胂胃胄胅胆胇胈胉胊背胍胎胏胐胑胔胕胖胗胘胙胚胛胜胝胞胠胡胣胤胥胧胨胩胪胫胬胭胯胰胱胲胳胴胵胶胷胸胹胺胻胼能胾脀脁脂脃脅脆脇脈脉脊脍脎脏脐脑脒脓脔脕脖脗脘脙脚脛脞脟脡脣脤脥脧脩脫脬脭脯脰脱脲脳脶脷脸脹脽脾脿腃腄腆腇腈腊腋腌腍腎腏腐腑腒腓腔腕腖腘腙腚腛腜腞腠腢腤腥腦腧腨腩腫腭腮腯腰腱腲腳腴腶腷腸腹腺腻腼腽腾腿膀膂膃膆膇膈膉膊膋膌膍膏膑膕膘膙膚膛膜膝膞膟膠膢膣膥膦膨膩膫膬膮膰膱膲膳膴膵膷膹膺膻膽膾膿臀臂臃臄臅臆臇臈臉臊臋臌臍臏臐臑臒臓臕臗臘臙臚臛臜臝臞臟臠臡臢臣臥臦臧臨臩自臬臭臮臯臲至致臷臸臺臻臼臾臿舀舁舂舄舅舆與興舉舊舋舌舍舎舐舑舒舔舕舖舗舘舛舜舝舞舟舠舡舢舣舥舨舩航舫般舭舯舰舱舲舳舴舵舶舷舸船舺舻舼舽舾艀艂艄艅艇艉艋艎艏艐艑艒艓艔艕艘艙艚艛艟艤艦艨艫艮良艰艱色艳艴艵艶艷艸艹艺艼艽艾艿芀节芃芄芅芈芊芋芍芎芏芐芑芒芓芔芗芘芙芚芛芜芝芞芟芠芡芣芤芥芦芧芨芩芪芫芬芭芮芯芰花芲芳芴芵芷芸芹芺芻芼芽芾芿苀苁苄苅苇苈苉苊苋苌苍苎苏苑苒苓苔苕苖苗苘苙苛苜苝苞苟苠苡苣苤若苦苧苨苪苫苬苯英苲苳苴苵苶苷苹苺苻苼苾茀茁茂范茄茅茆茇茈茉茋茌茍茎茏茑茓茔茕茖茗茘茙茚茛茜茝茠茢茤茥茦茧茨茩茪茫茬茭茮茯茱茲茳茴茵茶茷茸茹茺茻茼茽茿荀荁荂荃荄荅荆荇草荊荍荎荏荐荑荒荓荔荖荘荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药荳荴荵荷荸荺荻荼荽荾莁莃莅莆莉莊莋莌莍莎莏莐莒莓莔莕莖莘莙莛莜莝莞莠莢莣莤莥莧莨莩莪莫莬莰莱莲莳莴莵莶获莸莹莺莼莽莿菀菁菂菅菆菇菈菉菊菋菌菍菎菏菑菓菔菕菖菘菙菜菝菞菟菠菡菣菤菥菧菨菩菪菫華菰菱菲菳菴菵菶菸菹菺菼菽菾菿萁萃萄萆萇萉萊萋萌萍萎萏萐萑萒萘萚萜萝萠萡萣萤营萦萧萨萩萬萭萯萰萱萲萳萴萵萷萸萹萺萼落萿葀葂葃葅葆葉葊葋葌葍葎葑葒葖著葙葚葛葜葝葞葟葡董葤葥葦葧葩葫葬葭葯葰葱葳葴葵葶葷葸葹葺葽蒂蒄蒆蒇蒈蒉蒋蒌蒍蒎蒏蒐蒓蒔蒗蒘蒙蒚蒛蒜蒝蒞蒟蒠蒡蒢蒤蒧蒨蒩蒪蒫蒬蒭蒮蒯蒱蒲蒴蒶蒸蒹蒺蒻蒼蒽蒾蒿蓀蓁蓂蓄蓆蓇蓉蓊蓋蓌蓍蓎蓏蓐蓑蓓蓖蓘蓚蓝蓟蓠蓢蓣蓥蓦蓧蓨蓩蓪蓫蓬蓭蓮蓯蓰蓱蓲蓳蓴蓶蓷蓹蓺蓻蓼蓽蓾蓿蔀蔂蔃蔄蔆蔇蔈蔉蔊蔌蔍蔎蔏蔑蔒蔓蔔蔕蔖蔗蔘蔚蔜蔝蔞蔟蔠蔡蔣蔤蔥蔦蔧蔨蔩蔪蔫蔬蔭蔮蔰蔱蔴蔵蔷蔸蔹蔺蔻蔼蔽蔾蔿蕀蕁蕃蕄蕅蕆蕇蕈蕉蕊蕋蕍蕎蕑蕒蕓蕕蕖蕗蕘蕙蕚蕛蕝蕞蕟蕠蕡蕢蕣蕤蕦蕧蕨蕩蕪蕫蕭蕮蕰蕱蕲蕴蕵蕷蕸蕹蕺蕻蕼蕾蕿薀薁薂薃薄薅薆薇薈薉薊薋薌薍薎薏薐薑薔薕薖薗薘薙薚薛薜薝薞薟薠薡薢薣薤薦薧薨薩薪薫薬薭薮薯薰薱薲薳薴薵薶薷薸薹薺薽薾薿藀藁藂藃藄藅藆藇藈藉藋藍藎藏藐藑藒藓藔藕藗藘藙藚藜藝藟藠藡藣藤藥藦藨藩藪藫藬藭藰藱藲藴藶藷藸藹藺藻藼藾藿蘀蘁蘂蘄蘅蘆蘇蘉蘊蘋蘌蘐蘑蘓蘖蘗蘘蘙蘚蘛蘜蘞蘟蘠蘡蘢蘣蘤蘥蘦蘧蘩蘪蘬蘭蘮蘱蘳蘴蘵蘶蘸蘹蘺蘻蘼蘾蘿虀虃虆虇虈虉虋虌虎虏虐虑虒虓虔處虖虙虚虛虜虞號虡虢虣虤虧虨虩虪虫虬虭虮虯虰虱虴虵虷虸虹虺虻虼虽虾虿蚀蚁蚂蚄蚅蚆蚇蚊蚋蚌蚍蚑蚓蚕蚖蚗蚘蚙蚚蚜蚝蚞蚡蚢蚣蚤蚥蚧蚨蚩蚪蚬蚯蚰蚱蚲蚳蚴蚵蚶蚷蚸蚹蚺蚻蚼蚿蛀蛁蛂蛃蛄蛅蛆蛇蛈蛉蛊蛋蛌蛍蛎蛏蛐蛑蛓蛔蛕蛖蛗蛘蛙蛚蛛蛜蛝蛞蛟蛢蛣蛤蛦蛩蛪蛫蛬蛭蛮蛯蛰蛱蛲蛳蛴蛵蛶蛷蛸蛹蛺蛻蛾蜀蜁蜂蜃蜄蜆蜇蜈蜉蜊蜋蜌蜍蜎蜐蜑蜒蜓蜕蜖蜗蜘蜙蜚蜛蜜蜞蜠蜡蜢蜣蜤蜥蜦蜧蜨蜩蜪蜬蜭蜮蜯蜰蜱蜲蜳蜴蜵蜷蜸蜺蜻蜼蜾蜿蝀蝁蝂蝃蝆蝇蝈蝉蝋蝌蝍蝎蝐蝑蝒蝓蝔蝕蝖蝗蝘蝙蝚蝛蝜蝝蝞蝟蝠蝡蝣蝤蝥蝦蝨蝪蝫蝬蝭蝮蝯蝰蝱蝲蝳蝴蝶蝷蝸蝹蝺蝻蝼蝽蝾蝿螁螂螃螄螅螇螈螋融螎螏螐螑螒螓螔螖螗螘螚螛螜螝螞螟螠螡螢螣螤螥螨螪螫螬螭螮螯螰螱螳螴螵螶螷螸螹螺螻螼螽螾螿蟀蟁蟂蟃蟄蟅蟆蟇蟈蟉蟊蟋蟌蟎蟏蟑蟒蟓蟔蟗蟘蟙蟛蟜蟞蟟蟠蟡蟢蟣蟤蟥蟦蟧蟨蟪蟫蟬蟭蟮蟯蟲蟳蟴蟶蟷蟹蟺蟻蟼蟾蟿蠀蠁蠂蠃蠄蠅蠆蠈蠉蠊蠋蠌蠍蠏蠐蠑蠓蠔蠕蠖蠗蠘蠙蠛蠜蠝蠟蠠蠡蠢蠣蠤蠥蠦蠨蠩蠪蠫蠬蠭蠮蠯蠰蠱蠲蠳蠵蠶蠷蠸蠹蠻蠼蠽蠾蠿血衁衂衃衄衅衆衇衈衊衋行衍衎衒術衔衕衖街衙衚衛衜衝衞衠衡衢衣补衧表衩衪衫衬衭衮衯衰衱衲衵衶衷衹衺衽衾衿袀袁袂袃袄袅袆袈袉袋袌袍袑袒袓袕袖袗袘袙袚袛袜袞袟袠袡袢袤袧袨袪被袬袭袯袰袱袲袴袵袶袷袸袹袺袼袽袾袿裀裁裂裃装裆裈裉裊裋裍裎裏裒裔裕裖裗裘裙裚裛補裝裞裟裠裡裢裣裤裥裧裨裬裮裯裰裱裲裳裴裵裶裷裸裹裺裻裼製裾裿褀褁褂褄褅褆複褊褋褌褍褎褐褑褒褓褔褕褖褗褘褙褚褛褞褟褡褢褥褦褧褩褪褫褭褮褯褰褱褲褳褴褵褶褷褸褻褼褽褾襁襂襃襄襆襇襋襌襏襐襒襓襕襖襗襘襚襛襜襝襞襟襠襡襢襣襤襦襩襪襫襬襭襮襯襲襳襴襶襷襹襺襻襾西要覂覃覅覆覇覈見覌覎規覓覕視覗覘覚覛覜覝覡覢覣覤覦覧覩親覬覭覮覯覲観覶覷覺覽覿觀见观觃规觅视觇览觉觊觋觌觎觏觐觑角觓觔觕觖觙觚觜觝觞觟觠觡觢解觤觥触觩觫觬觭觯觰觱觲觳觴觶觷觸觺觻觼觾觿言訁訂訃訄訇計訊訌討訏訐訑訒訓訔訕訖託記訚訛訝訞訟訢訣訥訧訩訪訬設訰許訳訴訶訹診註証訾訿詀詁詄詅詆詈詍詎詐詒詔評詖詗詘詙詛詝詞詟詠詡詢詣詥試詧詩詪詫詬詭詮詰話該詳詵詶詷詹詻詼詿誂誃誄誅誆誇誉誊誋誌認誑誒誓誕誖誘誙誚語誠誡誣誤誥誦誨說誫説読誰課誴誶誸誹誻誼誾調諀諂諄諅諆談諈諉請諌諍諏諐諑諒諓諔諕論諗諚諛諜諝諞諟諠諡諢諤諦諧諪諫諭諮諰諱諲諳諴諵諶諷諸諺諼諾謀謁謂謄謅謇謈謊謋謌謍謎謏謐謑謒謔謕謖謗謘謙謚講謜謝謞謠謡謢謣謤謥謦謧謨謩謪謫謬謯謰謱謳謵謷謹謼謾譀譁譅譆譇譈證譊譋譌譎譏譐譑譓譔譕譖識譙譚譛譜譝譞譟譠譣譥警譨譪譫譬譭譯議譲譳譴護譸譹譺譻譽譾譿讀讂讃讄讆變讋讌讎讐讒讓讔讕讖讘讙讚讜讞讟计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诫诬语诮误诰诱诲诳说诵请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷谹谻谼谽谾谿豁豃豅豆豇豈豉豊豋豌豍豎豏豐豔豕豗豚豜豝豟象豢豤豥豦豨豪豫豬豭豮豯豰豱豲豳豵豶豷豸豹豺豻豽豿貀貁貂貄貅貆貉貊貌貍貏貐貑貒貓貔貕貗貘貙貛貜貝貞負財貢貣貤貧貨販貪貫責貭貮貯貰貲貳貴貶買貸貺費貼貽貾貿賀賁賂賃賄賅資賈賉賊賌賎賑賒賓賔賕賙賚賛賜賞賟賠賡賢賣賤賥賦賧賨質賫賬賭賮賴賵賸賹賺賻購賽賾贄贅贆贇贈贊贋贌贍贏贐贓贔贕贖贗贙贛贜贝贞负贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞赟赠赡赢赣赤赦赧赨赩赪赫赬赭赮走赳赴赵赶起赸趀趁趂趄超越趋趌趍趎趏趐趑趓趔趕趖趙趜趟趠趡趣趥趧趨趪趫趬趭趮趯趱趲足趴趵趷趸趹趺趼趾趿跂跃跄跅跆跇跈跋跌跍跎跏跐跑跓跕跖跗跘跙跚跛跜距跞跟跠跡跢跣跤跥跦跧跨跩跪跫跬跮路跰跱跲跳跴践跶跷跸跹跺跻跼跽跾跿踂踃踅踆踇踉踊踌踍踎踏踐踑踒踓踔踕踖踘踙踛踝踞踟踠踡踢踣踤踥踦踧踩踪踫踬踮踯踰踱踳踴踵踶踸踹踺踼踽踾蹀蹁蹂蹄蹅蹇蹈蹉蹊蹋蹌蹍蹎蹏蹐蹑蹒蹓蹔蹕蹖蹗蹙蹚蹛蹜蹝蹞蹟蹠蹡蹢蹣蹤蹥蹦蹧蹩蹪蹬蹭蹯蹰蹲蹳蹴蹵蹶蹸蹺蹻蹼蹽蹾蹿躁躂躄躅躆躇躈躉躊躋躍躏躐躑躒躓躔躕躖躗躙躚躜躝躞躟躠躡躣躤躥躦躨躩躪身躬躭躯躰躲躳躶躺躽軀車軋軌軍軎軏軑軒軓軔軘軛軝軞軟転軤軥軦軧軨軫軬軮軯軱軲軵軶軷軸軹軺軻軼軽軾軿輀輁輂較輄輅輆輇輈載輊輋輌輐輑輒輓輔輕輖輗輘輚輛輜輝輞輟輠輣輤輥輦輩輪輬輭輮輯輲輳輴輵輶輷輸輹輻輾輿轀轂轃轄轅轆轇轈轉轍轎轏轐轑轒轓轔轕轖轗轘轙轚轛轝轞轟轠轡轢轤车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辛辜辞辟辠辢辣辥辦辧辨辩辫辭辮辯辰辱農辴辵边辺辻込辽达辿迁迂迄迅迆过迈迉迋迍迎运近迒迓返迕迖还这进远违连迟迡迢迣迤迥迦迨迩迪迫迭迮述迳迴迵迷迸迹迺迻追迾迿退送适逃逄逅逆逈选逊逋逌逍逎透逐逑递逓途逕逖逗這通逛逜逝逞速造逡逢連逤逦逨逩逭逮逯週進逴逵逶逸逹逺逻逼逾逿遁遂遄遅遆遇遊運遍過遏遐遑遒道達違遗遘遙遛遜遝遞遠遡遢遣遥遨適遭遮遯遰遲遳遴遵遶遷選遹遺遻遼遽遾避邀邁邂邃還邅邆邇邈邉邊邋邍邏邐邑邓邔邕邗邘邙邛邝邞邟邠邡邢那邥邦邧邨邪邬邮邯邰邱邲邳邴邵邶邸邹邺邻邽邾邿郁郃郄郅郇郈郊郋郎郏郐郑郓郔郕郖郗郘郙郚郛郜郝郞郟郠郡郢郣郤郥郦郧部郪郫郭郯郰郱郲郳郴郵郷郸郹郺郻郼都郾郿鄀鄁鄂鄃鄄鄅鄆鄇鄈鄉鄋鄌鄍鄎鄏鄐鄑鄒鄔鄖鄗鄘鄙鄚鄛鄜鄝鄞鄟鄠鄡鄢鄣鄤鄦鄧鄨鄩鄪鄫鄬鄭鄮鄯鄰鄱鄲鄳鄴鄵鄶鄸鄹鄺鄻鄾酀酁酂酃酄酅酆酇酈酉酊酋酌配酎酏酐酒酓酔酖酗酘酚酝酞酟酡酢酣酤酥酦酧酨酩酪酬酮酯酰酱酲酳酴酵酶酷酸酹酺酼酽酾酿醁醂醃醅醆醇醉醊醋醌醍醐醑醒醓醕醖醗醙醚醛醜醞醟醢醣醤醥醧醨醪醫醬醭醮醯醰醱醲醳醴醵醷醸醹醺醻醼醽醾醿釀釁釂釃釅釆采釈釉释釋里重野量釐金釓釔釕釗釘釙釚釜針釢釣釤釦釧釩釪釬釭釱釳釴釵釷釸釹釺釽釿鈀鈁鈃鈄鈆鈇鈈鈉鈊鈌鈍鈎鈏鈐鈑鈒鈔鈕鈖鈚鈞鈢鈣鈥鈦鈧鈫鈮鈰鈳鈴鈶鈷鈸鈹鈺鈽鈾鈿鉀鉄鉅鉆鉇鉈鉉鉊鉋鉌鉍鉏鉑鉒鉓鉕鉗鉚鉛鉞鉟鉠鉢鉤鉥鉦鉧鉬鉭鉮鉯鉱鉲鉴鉶鉷鉸鉹鉺鉻鉼鉾鉿銀銂銃銅銈銋銍銎銑銓銔銕銖銗銘銚銛銜銠銣銤銥銦銨銩銪銫銬銭銮銱銲銳銶銷銹銻銼銾鋀鋁鋂鋃鋄鋅鋆鋇鋈鋊鋋鋌鋏鋐鋒鋕鋗鋘鋙鋝鋟鋡鋤鋥鋦鋨鋩鋪鋭鋮鋯鋰鋱鋲鋳鋶鋸鋹鋺鋼錀錁錄錆錈錍錎錏錐錒錕錘錙錚錛錞錟錠錡錢錣錦錧錨錫錬錭錮錯録錳錵錶錸錻錼錾鍀鍁鍆鍇鍈鍉鍊鍋鍌鍍鍏鍐鍑鍔鍖鍘鍚鍛鍜鍝鍟鍠鍤鍥鍩鍪鍬鍭鍮鍰鍱鍳鍵鍶鍷鍺鍼鍾鎂鎃鎅鎉鎊鎌鎎鎏鎒鎓鎔鎖鎗鎘鎚鎛鎝鎞鎡鎢鎣鎦鎧鎩鎪鎬鎭鎮鎯鎰鎳鎵鎷鎸鎹鎻鏂鏃鏇鏈鏊鏌鏍鏏鏐鏑鏓鏖鏗鏘鏙鏚鏜鏝鏞鏟鏡鏢鏤鏦鏨鏬鏰鏳鏴鏵鏷鏹鏺鏻鏽鏾鐀鐃鐇鐈鐋鐍鐎鐏鐐鐓鐔鐕鐖鐘鐙鐚鐝鐠鐡鐤鐦鐧鐨鐫鐬鐭鐮鐯鐲鐳鐵鐶鐸鐺鐻鐽鐾鐿鑀鑁鑂鑄鑅鑊鑋鑌鑐鑑鑒鑔鑕鑚鑛鑞鑠鑢鑣鑤鑨鑪鑫鑭鑮鑯鑰鑱鑲鑴鑵鑷鑹鑼鑽鑾鑿钀钁钂钃钆钇针钉钊钋钌钍钎钏钐钒钓钔钕钖钗钘钙钚钛钜钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钷钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铌铍铎铏铐铑铒铕铖铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铧铨铩铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗锘错锚锛锜锝锞锟锡锢锣锤锥锦锧锨锩锪锫锬锭键锯锰锱锲锳锴锵锶锷锸锹锺锻锼锽锾锿镀镁镂镃镄镅镆镇镈镉镊镋镌镍镎镏镐镑镒镓镔镕镖镗镘镚镛镜镝镞镠镡镢镣镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镵镶長镺镻镼镽长門閂閃閆閇閈閉開閌閍閎閏閑閒間閔閘閜閞閟閡関閣閤閥閦閧閨閩閫閬閭閰閱閲閵閶閷閹閺閻閼閽閾閿闀闃闅闆闇闈闉闊闋闌闍闐闑闒闓闔闕闖闘闚闛關闞闟闠闡闢闤闥门闩闪闫闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阔阕阖阗阘阙阚阜阝阞队阠阡阢阤阨阪阬阭阮阯阰阱防阳阴阵阶阸阹阺阻阼阽阿陀陁陂附际陆陇陈陉陊陋陌降陎陏限陑陓陔陕陗陘陛陜陝陞陟陡院陣除陥陧陨险陪陫陬陭陰陲陳陴陵陶陷陸険陻陼陽陾隃隄隅隆隇隈隉隊隋隍階随隐隑隒隓隔隕隗隘隙際障隞隠隣隤隧隨隩險隮隰隱隲隳隴隶隷隸隹隺隻隼隽难隿雀雁雂雄雅集雇雈雉雊雋雌雍雎雏雑雒雓雔雕雖雗雘雙雚雛雜雝雞雟雠離難雨雩雪雫雯雰雱雲雳零雷雹雺電雽雾雿需霁霂霄霅霆震霈霉霊霋霍霎霏霐霑霒霓霖霙霜霞霠霢霣霤霧霨霩霪霫霬霭霮霰露霵霶霸霹霽霾霿靁靂靃靄靆靈靉靑青靓靖静靚靛靜非靠靡面靥靦靨革靪靫靬靭靮靰靱靲靳靴靶靷靸靺靻靼靽靾靿鞀鞁鞂鞃鞄鞅鞆鞈鞊鞋鞌鞍鞎鞏鞑鞒鞔鞗鞘鞙鞚鞜鞞鞠鞡鞣鞤鞥鞦鞧鞨鞪鞫鞬鞭鞮鞯鞲鞳鞴鞵鞶鞷鞹鞻鞽鞾鞿韁韂韃韄韅韆韇韉韋韌韍韎韏韐韑韓韔韕韗韙韜韝韞韟韠韡韣韥韦韧韨韩韪韫韬韭韮韰韱音韵韶韹韺韻韽韾響頁頂頃頄項順頇須頊頌頍頎頏預頑頒頓頔頖頗領頚頜頝頞頠頡頤頦頨頩頫頬頭頯頰頲頴頵頷頸頹頻頼頽顁顄顅顆顇顈顉顊顋題額顎顏顐顑顒顓顔顕顗願顙顛顜顝類顟顠顢顣顤顥顦顧顩顪顫顯顰顱顲顳顴页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颓颔颖颗题颙颚颛颜额颞颟颠颡颢颤颥颦颧風颬颭颮颯颱颲颳颶颸颺颻颼颽颾颿飁飂飄飆飈飉飋飌风飏飐飑飒飓飔飕飗飘飙飛飜飞食飡飢飣飥飧飨飩飪飫飬飭飮飯飱飲飴飶飺飼飽飾飿餀餂餃餄餅餈餉養餌餍餎餏餐餑餒餓餔餕餖餗餘餚餛餜餝餞餟餠餡餤餥餧館餪餫餬餭餮餯餰餱餲餳餵餷餹餺餻餼餽餾餿饁饂饃饅饇饈饉饊饋饌饍饎饐饑饒饔饕饗饘饙饛饜饞饟饡饢饥饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饿馁馃馄馅馆馇馈馉馊馋馌馍馏馐馑馒馓馔馕首馗馘香馛馝馞馣馥馦馧馨馬馭馮馯馰馱馲馳馴馵馹馺馻馼馽駁駂駃駄駅駆駈駉駍駎駏駐駑駒駓駔駕駖駘駙駛駜駝駟駡駢駣駤駥駩駪駬駭駮駰駱駴駶駷駸駹駺駻駼駽駾駿騁騂騃騄騅騇騉騊騋騌騍騎騏騑騒験騕騖騙騚騛騜騝騞騠騢騣騤騥騧騨騩騪騫騬騭騮騰騱騲騴騵騶騷騸騹騺騽騾驀驁驂驃驄驅驆驈驉驊驌驍驎驏驒驓驔驕驖驗驚驛驞驟驠驢驤驥驦驨驩驪驫马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑骒骓骕骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骦骧骨骫骭骯骰骱骴骶骷骸骹骺骼骽骾骿髀髁髂髃髄髅髆髇髈髊髋髌髍髎髏髐髑髒髓體髕髖高髙髟髡髢髣髥髦髧髪髫髬髭髮髯髲髳髴髶髷髹髺髻髼髽髾鬀鬁鬃鬄鬅鬆鬈鬉鬊鬋鬌鬍鬎鬏鬐鬒鬓鬕鬖鬗鬘鬙鬚鬞鬟鬠鬢鬣鬤鬥鬧鬨鬩鬪鬫鬬鬮鬯鬱鬲鬳鬵鬶鬷鬹鬺鬻鬼鬾鬿魁魂魃魄魅魆魇魈魉魊魋魌魍魎魏魑魔魕魖魘魚魛魞魟魠魡魣魦魧魨魩魮魯魱魴魵魶魷魻魼魾鮀鮁鮂鮃鮄鮅鮆鮇鮊鮋鮍鮎鮐鮑鮒鮓鮗鮚鮛鮜鮝鮞鮟鮠鮡鮣鮤鮥鮦鮨鮪鮫鮭鮮鮰鮵鮸鮹鮻鮿鯀鯁鯃鯆鯇鯈鯉鯊鯏鯒鯓鯔鯕鯖鯗鯙鯛鯝鯞鯠鯡鯢鯤鯦鯧鯨鯪鯫鯬鯭鯮鯰鯱鯶鯷鯸鯻鯽鯿鰂鰃鰅鰆鰈鰉鰋鰌鰍鰏鰐鰒鰓鰕鰗鰛鰜鰝鰟鰣鰤鰥鰧鰨鰩鰫鰬鰭鰮鰯鰰鰱鰲鰳鰴鰶鰷鰹鰺鰻鰼鰾鰿鱀鱁鱂鱄鱅鱆鱇鱈鱉鱊鱋鱍鱐鱒鱓鱔鱕鱖鱗鱘鱚鱝鱞鱟鱠鱢鱣鱥鱦鱧鱨鱬鱭鱮鱰鱲鱳鱴鱵鱷鱸鱹鱺鱻鱼鱽鱾鱿鲀鲁鲂鲃鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳇鳈鳉鳊鳌鳍鳎鳏鳐鳑鳒鳓鳔鳕鳖鳗鳘鳙鳚鳛鳜鳝鳞鳟鳠鳡鳢鳣鳤鳥鳦鳧鳩鳪鳬鳭鳯鳰鳱鳲鳳鳴鳵鳶鳷鳹鳺鳻鳼鳽鳾鳿鴀鴃鴅鴆鴇鴈鴉鴎鴐鴒鴓鴔鴕鴗鴘鴛鴝鴞鴟鴠鴡鴢鴣鴥鴦鴨鴩鴫鴮鴯鴰鴱鴳鴴鴶鴷鴸鴻鴽鴾鴿鵀鵁鵂鵃鵄鵅鵊鵋鵌鵎鵏鵐鵑鵒鵓鵔鵖鵗鵙鵛鵜鵝鵞鵟鵠鵡鵧鵩鵪鵫鵬鵮鵯鵰鵱鵲鵳鵴鵵鵷鵸鵹鵺鵻鵼鵽鵾鶀鶁鶂鶄鶅鶆鶇鶈鶉鶊鶋鶌鶏鶐鶒鶓鶔鶖鶗鶘鶙鶚鶛鶜鶝鶞鶟鶠鶡鶢鶣鶤鶥鶨鶩鶪鶬鶭鶯鶱鶲鶴鶵鶶鶷鶹鶺鶻鶼鶾鶿鷀鷁鷂鷃鷄鷅鷇鷈鷉鷊鷋鷎鷏鷐鷑鷒鷓鷕鷖鷗鷘鷙鷚鷛鷜鷞鷟鷡鷢鷣鷤鷥鷦鷩鷫鷮鷯鷰鷲鷳鷵鷶鷷鷸鷹鷺鷻鷽鷾鷿鸀鸁鸂鸃鸄鸅鸆鸇鸉鸊鸋鸌鸍鸏鸐鸑鸒鸓鸔鸕鸗鸘鸙鸚鸛鸜鸝鸞鸟鸠鸡鸢鸣鸤鸥鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹔鹕鹖鹗鹘鹙鹚鹛鹜鹝鹞鹟鹠鹡鹢鹣鹤鹦鹧鹨鹩鹪鹫鹬鹭鹮鹯鹰鹱鹲鹳鹴鹵鹸鹹鹺鹻鹼鹽鹾鹿麀麁麂麃麅麆麇麈麉麊麋麌麎麐麑麒麓麔麖麗麙麚麛麜麝麞麟麠麡麤麥麦麧麩麪麮麯麰麴麵麶麷麸麹麺麻麼麽麾麿黀黁黂黃黄黇黈黉黌黍黎黏黐黑黒黓黔黕黖默黙黚黛黜黝點黟黠黡黢黤黥黧黨黩黪黫黭黮黯黰黲黳黴黵黶黷黹黺黻黼黽黾黿鼀鼁鼂鼃鼆鼇鼈鼉鼊鼋鼍鼎鼏鼐鼒鼓鼕鼖鼗鼘鼙鼛鼜鼠鼢鼣鼤鼥鼨鼩鼪鼫鼬鼭鼮鼯鼰鼱鼲鼳鼴鼵鼶鼷鼸鼹鼻鼽鼾齀齁齂齃齇齉齊齋齌齍齎齏齐齑齒齔齕齖齗齘齙齛齜齝齞齟齠齡齢齣齤齥齦齧齩齪齫齬齮齯齰齱齲齴齵齶齷齸齹齺齻齾齿龀龁龂龃龄龅龆龇龈龉龊龋龌龍龎龐龑龒龔龕龗龙龚龛龜龝龟龠龢龤鿍鿎鿏郎凉︽︾﹏﹥﹪!#%&()+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz|~・ヲァィゥェォャュョッアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚¥𠅤𠙶𠳐𠴰𡎚𡐓𡑍𡚁𣇉𣗋𣲗𣲘𣸣𤋮𤧛𤩽𤫉𤺥𥔲𥕢𥖨𥻗𦈡𦍑𦒍𦙶𦝼𦭜𦰡𧿹𨐈𨙸𨚕𨟠𨭉𨱇𨱏𨱑𨱔𨺙𩓐𩽾𩾃𩾌𪟝𪣻𪤗𪨰𪨶𪩘𪾢𫄧𫄨𫄷𫄸𫇭𫌀𫍣𫍯𫍲𫍽𫐄𫐐𫐓𫑡𫓧𫓯𫓶𫓹𫔍𫔎𫔶𫖮𫖯𫖳𫗧𫗴𫘜𫘝𫘦𫘧𫘨𫘪𫘬𫚕𫚖𫚭𫛭𫞩𫟅𫟦𫟹𫟼𫠆𫠊𫠜𫢸𫫇𫭟𫭢𫭼𫮃𫰛𫵷𫶇𫷷𫸩𬀩𬀪𬂩𬃊𬇕𬇙𬇹𬉼𬊈𬊤𬌗𬍛𬍡𬍤𬒈𬒔𬒗𬕂𬘓𬘘𬘡𬘩𬘫𬘬𬘭𬘯𬙂𬙊𬙋𬜬𬜯𬞟𬟁𬟽𬣙𬣞𬣡𬣳𬤇𬤊𬤝𬨂𬨎𬩽𬪩𬬩𬬭𬬮𬬱𬬸𬬹𬬻𬬿𬭁𬭊𬭎𬭚𬭛𬭤𬭩𬭬𬭯𬭳𬭶𬭸𬭼𬮱𬮿𬯀𬯎𬱖𬱟𬳵𬳶𬳽𬳿𬴂𬴃𬴊𬶋𬶍𬶏𬶐𬶟𬶠𬶨𬶭𬶮𬷕𬸘𬸚𬸣𬸦𬸪𬹼𬺈𬺓🀄🃏🅰🅱🅾🅿🆎🆑🆒🆓🆔🆕🆖🆗🆘🆙🆚🇨🇩🇪🇫🇬🇮🇯🇰🇷🇺🈁🌀🌁🌂🌃🌄🌅🌆🌇🌈🌉🌊🌋🌌🌍🌎🌏🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜🌝🌞🌟🌠🌰🌱🌲🌳🌴🌵🌷🌸🌹🌺🌻🌼🌽🌾🌿🍀🍁🍂🍃🍄🍅🍆🍇🍈🍉🍊🍋🍌🍍🍎🍏🍐🍑🍒🍓🍔🍕🍖🍗🍘🍙🍚🍛🍜🍝🍞🍟🍠🍡🍢🍣🍤🍥🍦🍧🍨🍩🍪🍫🍬🍭🍮🍯🍰🍱🍲🍳🍴🍵🍶🍷🍸🍹🍺🍻🍼🎀🎁🎂🎃🎄🎅🎆🎇🎈🎉🎊🎋🎌🎍🎎🎏🎐🎑🎒🎓🎠🎡🎢🎣🎤🎥🎦🎧🎨🎩🎪🎫🎬🎭🎮🎯🎰🎱🎲🎳🎴🎵🎶🎷🎸🎹🎺🎻🎼🎽🎾🎿🏀🏁🏂🏃🏄🏆🏇🏈🏉🏊🏠🏡🏢🏣🏤🏥🏦🏧🏨🏩🏪🏫🏬🏭🏮🏯🏰🐀🐁🐂🐃🐄🐅🐆🐇🐈🐉🐊🐋🐌🐍🐎🐏🐐🐑🐒🐓🐔🐕🐖🐗🐘🐙🐚🐛🐜🐝🐞🐟🐠🐡🐢🐣🐤🐥🐦🐧🐨🐩🐪🐫🐬🐭🐮🐯🐰🐱🐲🐳🐴🐵🐶🐷🐸🐹🐺🐻🐼🐽🐾👀👂👃👄👅👆👇👈👉👊👋👌👍👎👏👐👑👒👓👔👕👖👗👘👙👚👛👜👝👞👟👠👡👢👣👤👥👦👧👨👩👪👫👬👭👮👯👰👱👲👳👴👵👶👷👸👹👺👻👼👽👾👿💀💁💂💃💄💅💆💇💈💉💊💋💌💍💎💏💐💑💒💓💔💕💖💗💘💙💚💛💜💝💞💟💠💡💢💣💤💥💦💧💨💩💪💫💬💭💮💯💰💲💳💴💵💶💷💸💹💺💻💼💽💾💿📀📁📂📃📄📅📆📇📈📉📊📋📌📍📎📏📐📑📒📓📔📕📖📗📘📙📚📛📜📝📞📟📠📡📢📣📤📥📦📧📨📩📪📫📬📭📮📯📰📱📲📳📴📵📶📷📹📺📻📼🔀🔁🔂🔃🔄🔅🔆🔇🔉🔊🔋🔌🔍🔎🔏🔐🔑🔒🔓🔔🔕🔖🔗🔘🔙🔚🔛🔜🔝🔞🔟🔠🔡🔢🔣🔤🔥🔦🔧🔨🔩🔪🔫🔬🔭🔮🔯🔰🔱🔲🔳🔴🔵🔶🔷🔸🔹🔺🔻🔼🔽🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛🕜🕝🕞🕟🕠🕡🕢🕣🕤🕥🕦🕧🗻🗼🗽🗾🗿😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟😠😡😢😣😤😥😦😧😨😩😪😫😬😭😮😯😰😱😲😳😴😵😶😷😸😹😺😻😼😽😾😿🙀🙅🙆🙇🙈🙉🙊🙋🙌🙍🙎🙏🚀🚁🚂🚃🚄🚅🚆🚇🚈🚉🚊🚌🚍🚎🚏🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚝🚞🚟🚠🚡🚢🚣🚤🚥🚦🚧🚨🚩🚪🚫🚬🚭🚮🚯🚰🚱🚲🚳🚴🚵🚶🚷🚸🚹🚺🚻🚼🚽🚾🚿🛀🛁🛂🛃🛄🛅 ';
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/ocr/detectors.ts b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/detectors.ts
new file mode 100644
index 0000000000..c4e02dbf8c
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/detectors.ts
@@ -0,0 +1,145 @@
+// Built-in detector box-extraction strategies for the OCR pipeline. Each is a
+// TextBoxExtractor for one detector architecture: it calls that architecture's
+// native decoder and reshapes the flat output into quads. The pipeline stays
+// model-agnostic — it just invokes OcrOptions.extractBoxes; the presets in
+// models.ts wire the zero-config built-ins, a model with non-standard
+// thresholds uses the make* factories, and a new architecture plugs in by
+// supplying its own conforming function.
+
+import { rnexecutorchJsi } from '../../../../native/bridge';
+import type { Tensor } from '../../../../core/tensor';
+import { quadsFromFlat, type Quad } from '../../ops/quad';
+
+/**
+ * A detector's box-extraction strategy: turns a model's raw `detect_` output
+ * tensors into oriented {@link Quad}s in detector-input pixel space. A model plugs a
+ * new detector into the OCR pipeline by supplying a function of this type (the
+ * built-ins below, or its own). MUST be a worklet.
+ * @category Types
+ * @param outputs The model's `detect_` output tensors, in order
+ * @param side The snapped square detector side `S` (input is `S × S` letterboxed).
+ * @param charLevel Emit one box per glyph instead of grouped lines; strategies
+ * without a char-level mode ignore it.
+ * @returns Oriented quads (TL, TR, BR, BL) in detector-input pixel space.
+ */
+export type TextBoxExtractor = (
+ outputs: readonly Tensor[],
+ side: number,
+ charLevel: boolean
+) => Quad[];
+
+/**
+ * Threshold overrides for {@link makeCraftExtractBoxes}. Omitted fields keep the
+ * CRAFT defaults: `textHeatmapThreshold` 0.4, `linkHeatmapThreshold` 0.4,
+ * `minBoxPeakScore` 0.7.
+ * @category Types
+ */
+export type CraftExtractorOptions = {
+ readonly textHeatmapThreshold?: number;
+ readonly linkHeatmapThreshold?: number;
+ readonly minBoxPeakScore?: number;
+};
+
+/**
+ * Threshold overrides for {@link makeDbnetExtractBoxes}. Omitted fields keep the
+ * DBNet defaults: `binarizationThreshold` 0.3, `minBoxScore` 0.6, `unclipRatio`
+ * 1.5, `minBoxSidePx` 3, `maxContourCandidates` 1000.
+ * @category Types
+ */
+export type DbnetExtractorOptions = {
+ readonly binarizationThreshold?: number;
+ readonly minBoxScore?: number;
+ readonly unclipRatio?: number;
+ readonly minBoxSidePx?: number;
+ readonly maxContourCandidates?: number;
+};
+
+// CRAFT region+affinity heatmap thresholds — stable across models, the defaults.
+const CRAFT_TEXT_THRESHOLD = 0.4;
+const CRAFT_LINK_THRESHOLD = 0.4;
+const CRAFT_LOW_TEXT_THRESHOLD = 0.7;
+
+// DBNet probability-map thresholds — stable across models, the defaults.
+const DBNET_BIN_THRESHOLD = 0.3;
+const DBNET_BOX_THRESHOLD = 0.6;
+const DBNET_UNCLIP_RATIO = 1.5;
+const DBNET_MIN_BOX_SIDE = 3;
+const DBNET_MAX_CANDIDATES = 1000;
+
+/**
+ * Builds a CRAFT {@link TextBoxExtractor}. Groups the
+ * half-resolution region+affinity heatmap (`outputs[0]` is the `[1,Hd,Wd,2]`
+ * heatmap) into oriented text-line quads, or per-glyph boxes when `charLevel`.
+ * For the standard thresholds use the ready-made {@link craftExtractBoxes}.
+ * @category Typescript API
+ * @param overrides Threshold overrides; omitted fields keep the CRAFT defaults.
+ * @returns A {@link TextBoxExtractor} to assign to `OcrOptions.extractBoxes`.
+ */
+export function makeCraftExtractBoxes(overrides?: CraftExtractorOptions): TextBoxExtractor {
+ const textThreshold = overrides?.textHeatmapThreshold ?? CRAFT_TEXT_THRESHOLD;
+ const linkThreshold = overrides?.linkHeatmapThreshold ?? CRAFT_LINK_THRESHOLD;
+ const lowTextThreshold = overrides?.minBoxPeakScore ?? CRAFT_LOW_TEXT_THRESHOLD;
+ return (outputs, side, charLevel) => {
+ 'worklet';
+ // The half-resolution heatmap requires an even detector side; the pipeline is
+ // architecture-agnostic and can't check this, so the strategy does.
+ if (side % 2 !== 0) {
+ throw new Error(
+ 'OCR: every CRAFT detect bucket side must be even (half-resolution heatmap).'
+ );
+ }
+ const flat = rnexecutorchJsi.cv.extractCraftTextBoxes(outputs[0]!, {
+ textThreshold,
+ linkThreshold,
+ lowTextThreshold,
+ targetHeight: side,
+ charLevel,
+ }) as number[];
+ return quadsFromFlat(flat);
+ };
+}
+
+/**
+ * Builds a DBNet {@link TextBoxExtractor}. Thresholds and
+ * unclips the probability map (`outputs[0]` is the `[1,1,H,W]` post-sigmoid prob
+ * map) into oriented text quads. It decodes at full resolution with no char-level
+ * mode, so the extractor uses neither `side` nor `charLevel`. For the standard
+ * thresholds use the ready-made {@link dbnetExtractBoxes}.
+ * @category Typescript API
+ * @param overrides Threshold overrides; omitted fields keep the DBNet defaults.
+ * @returns A {@link TextBoxExtractor} to assign to `OcrOptions.extractBoxes`.
+ */
+export function makeDbnetExtractBoxes(overrides?: DbnetExtractorOptions): TextBoxExtractor {
+ const binThreshold = overrides?.binarizationThreshold ?? DBNET_BIN_THRESHOLD;
+ const boxThreshold = overrides?.minBoxScore ?? DBNET_BOX_THRESHOLD;
+ const unclipRatio = overrides?.unclipRatio ?? DBNET_UNCLIP_RATIO;
+ const minBoxSide = overrides?.minBoxSidePx ?? DBNET_MIN_BOX_SIDE;
+ const maxCandidates = overrides?.maxContourCandidates ?? DBNET_MAX_CANDIDATES;
+ return (outputs) => {
+ 'worklet';
+ const flat = rnexecutorchJsi.cv.extractDbnetTextBoxes(outputs[0]!, {
+ binThreshold,
+ boxThreshold,
+ unclipRatio,
+ minBoxSide,
+ maxCandidates,
+ }) as number[];
+ return quadsFromFlat(flat);
+ };
+}
+
+/**
+ * Built-in CRAFT {@link TextBoxExtractor} with the standard thresholds. Assign to
+ * `OcrOptions.extractBoxes` for a CRAFT-family model (the detector side must be
+ * even), or build a custom-threshold variant with {@link makeCraftExtractBoxes}.
+ * @category Typescript API
+ */
+export const craftExtractBoxes: TextBoxExtractor = makeCraftExtractBoxes();
+
+/**
+ * Built-in DBNet {@link TextBoxExtractor} with the standard thresholds. Assign to
+ * `OcrOptions.extractBoxes` for a DBNet-family model, or build a
+ * custom-threshold variant with {@link makeDbnetExtractBoxes}.
+ * @category Typescript API
+ */
+export const dbnetExtractBoxes: TextBoxExtractor = makeDbnetExtractBoxes();
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/ocr/documentModels.ts b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/documentModels.ts
new file mode 100644
index 0000000000..186f17239e
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/documentModels.ts
@@ -0,0 +1,452 @@
+import type { WorkletRuntime } from 'react-native-worklets';
+
+import { tensor, type Tensor } from '../../../../core/tensor';
+import { loadModel } from '../../../../core/model';
+import { validateModelSchema, SymbolicTensor } from '../../../../core/modelSchema';
+import { wrapAsync } from '../../../../core/runtime';
+
+import type { ImageBuffer, ImageFormat } from '../../image';
+import { IMAGENET_NORM } from '../../../../constants';
+import { FORMAT_CHANNELS } from '../../ops/image';
+import { warpByGrid } from '../../ops/image';
+import type { BoundingBox } from '../../ops/boxes';
+import { boundsOfPoints } from '../../ops/quad';
+import { createImagePreprocessor } from '../preprocessing';
+import type { OcrDetection } from './ocr';
+
+/**
+ * A detected page orientation: the clockwise rotation (rotate by its negation to
+ * correct) and the classifier confidence in `[0, 1]`.
+ * @category Types
+ */
+export type Orientation = {
+ readonly rotationCW: 0 | 90 | 180 | 270;
+ readonly confidence: number;
+};
+
+/**
+ * A recognized table structure: the `/` HTML skeleton (empty cells) and
+ * the raw structure token ids it was built from (start/end tokens stripped).
+ * @category Types
+ */
+export type TableStructure = {
+ readonly html: string;
+ readonly tokens: number[];
+};
+
+/**
+ * Configuration for the fused document models — page orientation, geometric
+ * dewarp, and table-structure recognition, all exposed by one model file.
+ * `structureVocab` maps a table token id (array index) to its HTML fragment;
+ * `eosTokenId` ends table decoding and `maxSteps` caps it.
+ * @category Types
+ */
+export type DocumentModelsConfig = {
+ readonly modelPath: string;
+ readonly structureVocab: readonly string[];
+ readonly eosTokenId: number;
+ readonly maxSteps: number;
+};
+
+// A dewarp grid estimated on a page without clear boundaries (e.g. text floating
+// on white) can map most of the output off the source, collapsing the page to
+// near-blank and OCR to zero detections. The dewarp guard compares content before
+// and after: if the warp keeps less than this fraction of the source's activity,
+// it is declined and the original page is kept.
+const DEWARP_MIN_ACTIVITY_RATIO = 0.5;
+const DEWARP_ACTIVITY_STRIDE = 31;
+
+// The variance of one channel, sampled every DEWARP_ACTIVITY_STRIDE pixels — a
+// cheap, polarity-independent proxy for how much content (ink/edges) an image
+// carries; a blank page is ~0. Used by the dewarp degeneracy guard.
+function dewarpActivity(data: Uint8Array, channels: number): number {
+ 'worklet';
+ let n = 0;
+ let sum = 0;
+ let sumSq = 0;
+ const step = channels * DEWARP_ACTIVITY_STRIDE;
+ for (let i = 0; i < data.length; i += step) {
+ const v = data[i]!;
+ sum += v;
+ sumSq += v * v;
+ n++;
+ }
+ if (n === 0) {
+ return 0;
+ }
+ const mean = sum / n;
+ return sumSq / n - mean * mean;
+}
+
+// Index of the maximum value in `arr[offset, offset+len)` (single pass, no allocation).
+function argmaxRange(arr: ArrayLike, offset: number, len: number): number {
+ 'worklet';
+ let index = 0;
+ let best = arr[offset]!;
+ for (let i = 1; i < len; i++) {
+ const value = arr[offset + i]!;
+ if (value > best) {
+ best = value;
+ index = i;
+ }
+ }
+ return index;
+}
+
+// Assembles the table-structure content tokens into an HTML skeleton, dropping the
+// reserved start/end range.
+function tokensToHtml(
+ tokens: number[],
+ structureVocab: readonly string[],
+ eosTokenId: number
+): string {
+ 'worklet';
+ let html = '';
+ for (const t of tokens) {
+ if (t > 0 && t < eosTokenId && t < structureVocab.length) {
+ html += structureVocab[t]!;
+ }
+ }
+ return html;
+}
+
+/**
+ * Creates the document-models runner: page orientation, geometric dewarp (applied
+ * via the predicted sampling grid), and table-structure recognition (autoregressive
+ * decode). One model file, loaded once. Internal to the document pipeline.
+ * @category Typescript API
+ * @param config Model path, table-structure vocabulary, and decode limits.
+ * @param runtime Optional worklet runtime thread.
+ * @returns A promise resolving to the three capabilities plus disposal controls.
+ */
+export async function createDocumentModels(
+ config: DocumentModelsConfig,
+ runtime?: WorkletRuntime
+): Promise<{
+ dispose: () => void;
+ detectOrientation: (page: Tensor, format: ImageFormat) => Promise;
+ detectOrientationWorklet: (page: Tensor, format: ImageFormat) => Orientation;
+ dewarp: (page: Tensor, format: ImageFormat) => Promise;
+ dewarpWorklet: (page: Tensor, format: ImageFormat) => Tensor;
+ recognizeTable: (input: ImageBuffer) => Promise;
+ recognizeTableWorklet: (input: ImageBuffer) => TableStructure;
+}> {
+ const { modelPath, structureVocab, eosTokenId, maxSteps } = config;
+ const model = await wrapAsync(loadModel, runtime)(modelPath);
+
+ // Everything built is pushed into `created` as it is created — one by one, so
+ // a mid-sequence failure can't strand its predecessors — and the catch
+ // disposes it all: a bad config must not leak native memory (mirrors createOcr).
+ const created: { dispose: () => void }[] = [];
+ try {
+ // orientation: image -> class logits
+ const oriMeta = validateModelSchema(
+ model,
+ 'orientation',
+ [SymbolicTensor('float32', [1, 3, 'H', 'W'])],
+ [SymbolicTensor('float32', [1, 'K'])]
+ );
+ // dewarp: image -> sampling grid
+ const dewMeta = validateModelSchema(
+ model,
+ 'dewarp',
+ [SymbolicTensor('float32', [1, 3, 'H', 'W'])],
+ [SymbolicTensor('float32', [1, 2, 'gH', 'gW'])]
+ );
+ // table: image encoder + autoregressive decode step
+ const encMeta = validateModelSchema(
+ model,
+ 'table_encode',
+ [SymbolicTensor('float32', [1, 3, 'H', 'W'])],
+ [SymbolicTensor('float32', [1, 'C', 'F'])]
+ );
+ const decMeta = validateModelSchema(
+ model,
+ 'table_decode_step',
+ [
+ SymbolicTensor('float32', [1, 'C', 'F']),
+ SymbolicTensor('float32', [1, 'H']),
+ SymbolicTensor('float32', [1, 'V']),
+ ],
+ [SymbolicTensor('float32', [1, 'V']), SymbolicTensor('float32', [1, 'H'])]
+ );
+
+ const oriShape = oriMeta.inputTensorMeta[0]!.shape;
+ const oriOutLen = oriMeta.outputTensorMeta[0]!.shape[1]!;
+ const dewShape = dewMeta.inputTensorMeta[0]!.shape;
+ const gridShape = dewMeta.outputTensorMeta[0]!.shape; // [1,2,gH,gW]
+ const tabShape = encMeta.inputTensorMeta[0]!.shape;
+ const featShape = encMeta.outputTensorMeta[0]!.shape;
+ const hidShape = decMeta.outputTensorMeta[1]!.shape;
+ const probShape = decMeta.outputTensorMeta[0]!.shape;
+ const hidLen = hidShape[1]!;
+ const vocabLen = probShape[1]!;
+
+ if (vocabLen !== structureVocab.length) {
+ throw new Error(
+ `DocumentModels: structure vocab length (${structureVocab.length}) must match the model's token dim (${vocabLen}).`
+ );
+ }
+
+ const orientationPreprocessor = createImagePreprocessor(
+ {
+ resizeMode: 'stretch',
+ interpolation: 'linear',
+ alpha: IMAGENET_NORM.alpha,
+ beta: IMAGENET_NORM.beta,
+ },
+ oriShape
+ );
+ created.push(orientationPreprocessor);
+ const dewarpPreprocessor = createImagePreprocessor(
+ { resizeMode: 'stretch', interpolation: 'linear', alpha: 1 / 255, beta: 0 },
+ dewShape
+ );
+ created.push(dewarpPreprocessor);
+ const tablePreprocessor = createImagePreprocessor(
+ {
+ resizeMode: 'stretch',
+ interpolation: 'linear',
+ alpha: IMAGENET_NORM.alpha,
+ beta: IMAGENET_NORM.beta,
+ },
+ tabShape
+ );
+ created.push(tablePreprocessor);
+
+ const tOri = tensor('float32', oriMeta.outputTensorMeta[0]!.shape);
+ created.push(tOri);
+ const tGrid = tensor('float32', gridShape);
+ created.push(tGrid);
+ const tFeatures = tensor('float32', featShape);
+ created.push(tFeatures);
+ const tHidden = tensor('float32', hidShape);
+ created.push(tHidden);
+ const tOnehot = tensor('float32', probShape);
+ created.push(tOnehot);
+ const tProbs = tensor('float32', probShape);
+ created.push(tProbs);
+ const tNewHidden = tensor('float32', hidShape);
+ created.push(tNewHidden);
+
+ const oriBuf = new Float32Array(oriOutLen);
+ const zeroHidden = new Float32Array(hidLen);
+ const zeroVocab = new Float32Array(vocabLen);
+ const onehotBuf = new Float32Array(vocabLen);
+ const probsBuf = new Float32Array(vocabLen);
+
+ const dispose = () => {
+ created.forEach((c) => c.dispose());
+ model.dispose();
+ };
+
+ const detectOrientationWorklet = (page: Tensor, format: ImageFormat): Orientation => {
+ 'worklet';
+ const tInput = orientationPreprocessor.processTensor(page, format);
+ model.execute('orientation', [tInput], [tOri]);
+ tOri.getData(oriBuf);
+ const cls = argmaxRange(oriBuf, 0, oriOutLen);
+ const best = oriBuf[cls]!;
+ let sumExp = 0;
+ for (let i = 0; i < oriOutLen; i++) {
+ sumExp += Math.exp(oriBuf[i]! - best);
+ }
+ const rotationCW = ((cls % 4) * 90) as 0 | 90 | 180 | 270;
+ const confidence = 1 / sumExp;
+ return { rotationCW, confidence };
+ };
+
+ // Dewarps the full-res page tensor in place: estimate the sampling field, apply
+ // it natively (cv::remap). Returns the dewarped tensor, or the input `page`
+ // unchanged when the warp is declined (caller owns whichever is returned).
+ const dewarpWorklet = (page: Tensor, format: ImageFormat): Tensor => {
+ 'worklet';
+ const tInput = dewarpPreprocessor.processTensor(page, format);
+ model.execute('dewarp', [tInput], [tGrid]);
+ const h = page.shape[0]!;
+ const w = page.shape[1]!;
+ const ch = FORMAT_CHANNELS[format];
+ const tDst = tensor('uint8', [h, w, ch]);
+ try {
+ warpByGrid(page, tGrid, tDst);
+ const out = new Uint8Array(w * h * ch);
+ const src = new Uint8Array(w * h * ch);
+ tDst.getData(out);
+ page.getData(src);
+ // Degenerate-warp guard: a grid lacking page boundaries can push content
+ // off-canvas, leaving a near-blank page. If the dewarp collapsed the image's
+ // activity, decline it and keep the original (better an un-dewarped read than
+ // zero detections).
+ if (dewarpActivity(out, ch) < DEWARP_MIN_ACTIVITY_RATIO * dewarpActivity(src, ch)) {
+ tDst.dispose();
+ return page;
+ }
+ return tDst;
+ } catch (e) {
+ // On failure the caller can't see tDst to free it (success path returns it),
+ // so release it here before propagating.
+ tDst.dispose();
+ throw e;
+ }
+ };
+
+ const recognizeTableWorklet = (input: ImageBuffer): TableStructure => {
+ 'worklet';
+ const tInput = tablePreprocessor.process(input);
+ model.execute('table_encode', [tInput], [tFeatures]);
+ tHidden.setData(zeroHidden);
+ tOnehot.setData(zeroVocab);
+ const tokens: number[] = [];
+ for (let step = 0; step < maxSteps; step++) {
+ model.execute('table_decode_step', [tFeatures, tHidden, tOnehot], [tProbs, tNewHidden]);
+ tProbs.getData(probsBuf);
+ const tok = argmaxRange(probsBuf, 0, vocabLen);
+ tokens.push(tok);
+ if (tok === eosTokenId) {
+ break;
+ }
+ tNewHidden.copyTo(tHidden);
+ onehotBuf.fill(0);
+ onehotBuf[tok] = 1;
+ tOnehot.setData(onehotBuf);
+ }
+ return { html: tokensToHtml(tokens, structureVocab, eosTokenId), tokens };
+ };
+
+ return {
+ dispose,
+ detectOrientation: wrapAsync(detectOrientationWorklet, runtime),
+ detectOrientationWorklet,
+ dewarp: wrapAsync(dewarpWorklet, runtime),
+ dewarpWorklet,
+ recognizeTable: wrapAsync(recognizeTableWorklet, runtime),
+ recognizeTableWorklet,
+ };
+ } catch (e) {
+ created.forEach((c) => c.dispose());
+ model.dispose();
+ throw e;
+ }
+}
+
+/**
+ * Crops an axis-aligned region out of an image as a plain pixel slice (same format
+ * and layout). Used to feed a layout region to another model.
+ * @category Typescript API
+ * @param input The source image.
+ * @param bbox The crop region, in `xyxy` pixels.
+ * @returns The cropped image.
+ */
+export function cropImageBuffer(input: ImageBuffer, bbox: BoundingBox<'xyxy'>): ImageBuffer {
+ 'worklet';
+ const { data, width, height, format } = input;
+ const channels = FORMAT_CHANNELS[format];
+ const x0 = Math.max(0, Math.min(Math.round(bbox.xmin), width));
+ const y0 = Math.max(0, Math.min(Math.round(bbox.ymin), height));
+ const x1 = Math.max(0, Math.min(Math.round(bbox.xmax), width));
+ const y1 = Math.max(0, Math.min(Math.round(bbox.ymax), height));
+ const cropWidth = Math.max(1, x1 - x0);
+ const cropHeight = Math.max(1, y1 - y0);
+ const out = new Uint8Array(cropWidth * cropHeight * channels);
+ for (let y = 0; y < cropHeight; y++) {
+ const rowStart = ((y0 + y) * width + x0) * channels;
+ out.set(data.subarray(rowStart, rowStart + cropWidth * channels), y * cropWidth * channels);
+ }
+ return { data: out, width: cropWidth, height: cropHeight, format, layout: input.layout };
+}
+
+// 1-D clustering of cell-center coordinates into `k` table rows (or columns).
+// The sorted values are split at their k-1 widest gaps — each resulting run of
+// values is one row/column, represented by its mean coordinate. Splitting at the
+// widest gaps (instead of at fixed intervals) matches how table cells actually
+// distribute: values within a row are tightly packed while rows are separated by
+// clear gaps, so uneven row heights / column widths still cluster correctly.
+// Fewer than `k` values means every value is its own cluster.
+function clusterCentersByGaps(values: readonly number[], k: number): number[] {
+ 'worklet';
+ const sorted = [...values].sort((a, b) => a - b);
+ if (sorted.length <= k) {
+ return sorted;
+ }
+ // Rank the interior gaps (gap i sits between sorted[i-1] and sorted[i]) and take
+ // the k-1 widest as cut points, restored to ascending order.
+ const gaps = sorted.slice(1).map((value, i) => ({ at: i + 1, size: value - sorted[i]! }));
+ gaps.sort((a, b) => b.size - a.size);
+ const cuts = gaps
+ .slice(0, k - 1)
+ .map((gap) => gap.at)
+ .sort((a, b) => a - b);
+ // Average each [prev, cut) span into its center.
+ const centers: number[] = [];
+ let prev = 0;
+ for (const cut of [...cuts, sorted.length]) {
+ const group = sorted.slice(prev, cut);
+ centers.push(group.reduce((sum, value) => sum + value, 0) / group.length);
+ prev = cut;
+ }
+ return centers;
+}
+
+/**
+ * Fills a table-structure HTML skeleton with a region's OCR lines. The grid size
+ * comes from the skeleton (row count, and the widest row's cell count); each
+ * line's box center is assigned to its nearest row and column cluster, so shared
+ * column centers keep columns aligned. Falls back to a document-order fill when the
+ * skeleton has no grid.
+ *
+ * Alignment is geometric only — dense rows can misplace a value, since the
+ * skeleton carries no per-cell coordinates.
+ * @category Typescript API
+ * @param html The structure HTML skeleton (empty cells).
+ * @param lines The region's OCR lines, with page-space quads.
+ * @returns A `` with each cell filled by its nearest-assigned text.
+ */
+export function fillTableCells(html: string, lines: readonly OcrDetection[]): string {
+ 'worklet';
+ const rowCount = (html.match(//g) ?? []).length;
+ let colCount = 0;
+ const rowRegex = / ([\s\S]*?)<\/tr>/g;
+ let row: RegExpExecArray | null;
+ while ((row = rowRegex.exec(html)) !== null) {
+ colCount = Math.max(colCount, (row[1]!.match(/| ]*)><\/td>/g, (_match, attrs) => {
+ const text = i < lines.length ? lines[i]!.text : '';
+ i++;
+ return ` | ${text} | `;
+ });
+ }
+
+ const centersX: number[] = [];
+ const centersY: number[] = [];
+ for (const line of lines) {
+ const box = boundsOfPoints(line.quad, 'xyxy');
+ centersX.push((box.xmin + box.xmax) / 2);
+ centersY.push((box.ymin + box.ymax) / 2);
+ }
+ const rowCenters = clusterCentersByGaps(centersY, rowCount);
+ const colCenters = clusterCentersByGaps(centersX, colCount);
+ const grid: string[][] = Array.from({ length: rowCenters.length }, () =>
+ new Array(colCenters.length).fill('')
+ );
+ // Assign each line to the row/column whose cluster center is nearest.
+ for (let i = 0; i < lines.length; i++) {
+ const r = rowCenters.reduce(
+ (best, center, j) =>
+ Math.abs(centersY[i]! - center) < Math.abs(centersY[i]! - rowCenters[best]!) ? j : best,
+ 0
+ );
+ const c = colCenters.reduce(
+ (best, center, j) =>
+ Math.abs(centersX[i]! - center) < Math.abs(centersX[i]! - colCenters[best]!) ? j : best,
+ 0
+ );
+ grid[r]![c] = `${grid[r]![c]!} ${lines[i]!.text}`.trim();
+ }
+ return `${grid
+ .map((cells) => `${cells.map((text) => `| ${text} | `).join('')} `)
+ .join('')} `;
+}
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/ocr/documentOcr.ts b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/documentOcr.ts
new file mode 100644
index 0000000000..6736432b48
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/documentOcr.ts
@@ -0,0 +1,286 @@
+import type { WorkletRuntime } from 'react-native-worklets';
+
+import { tensor } from '../../../../core/tensor';
+import { wrapAsync } from '../../../../core/runtime';
+import type { ImageBuffer } from '../../image';
+import type { Point } from '../../ops/points';
+import type { BoundingBox } from '../../ops/boxes';
+import { boundsOfPoints } from '../../ops/quad';
+import { rotate, FORMAT_CHANNELS } from '../../ops/image';
+import { createOcr, type OcrModel, type OcrDetection } from './ocr';
+import {
+ createObjectDetector,
+ type ObjectDetectorModel,
+ type ObjectDetection,
+} from '../objectDetection';
+import {
+ createDocumentModels,
+ cropImageBuffer,
+ fillTableCells,
+ type DocumentModelsConfig,
+} from './documentModels';
+import { orderByReadingOrder } from './ocrUtils';
+
+/**
+ * One assembled document block: a layout region (or an ungrouped catch-all) with
+ * its OCR lines grouped and concatenated, in reading order.
+ * @category Types
+ */
+export type DocumentBlock = {
+ /** Region class from layout (e.g. `'text'`, `'table'`), or `'ungrouped'`. */
+ readonly regionType: L | 'ungrouped';
+ /** Block box in processing-frame pixels. */
+ readonly bbox: BoundingBox<'xyxy'>;
+ /** Layout confidence for the region (1 for `'ungrouped'`). */
+ readonly score: number;
+ /** The block's text, lines joined top-to-bottom by newlines. */
+ readonly text: string;
+ /** The OCR lines inside this block, top-to-bottom. */
+ readonly lines: readonly OcrDetection[];
+ /** Whether this block is a table region. */
+ readonly isTable: boolean;
+ /** For table blocks: the recognized HTML structure with OCR text filled in. */
+ readonly tableHtml?: string;
+};
+
+/**
+ * The result of a document OCR run.
+ * @category Types
+ */
+export type DocumentResult = {
+ readonly blocks: DocumentBlock[];
+ readonly regions: ObjectDetection<'xyxy', L>[];
+ readonly detections: OcrDetection[];
+ /**
+ * The frame all `bbox`/`quad` coordinates are relative to. Equals the input
+ * image unless orientation correction or dewarp was applied, in which case it
+ * is the corrected image — overlay boxes on THIS, not the original input.
+ */
+ readonly image: ImageBuffer;
+};
+
+/**
+ * Configuration for the document OCR orchestrator. Provides an OCR model, an
+ * optional layout model (regions/blocks), and optional document models
+ * (orientation/dewarp pre-processing + table-structure recognition). The
+ * `orientation`/`dewarp` flags are *defaults* for the per-run options of the
+ * same name — supply them here to bias every run, or leave them off and pass
+ * them to `runDocumentOcr` per call (the document models are loaded either way).
+ * @category Types
+ */
+export type DocumentOcrModel = {
+ readonly ocr: OcrModel;
+ readonly layout?: ObjectDetectorModel<'xyxy', L>;
+ readonly documentModels?: DocumentModelsConfig;
+ /** Default for the per-run `orientation` option (needs `documentModels`). */
+ readonly orientation?: boolean;
+ /** Default for the per-run `dewarp` option (needs `documentModels`). */
+ readonly dewarp?: boolean;
+ /**
+ * Minimum orientation-classifier confidence (softmax of the argmax class) to act
+ * on a non-zero rotation — below it the page is treated as already upright, so
+ * out-of-distribution inputs (photos/non-documents) don't spuriously flip. Genuine
+ * documents score >0.95; defaults to 0.85.
+ */
+ readonly orientationMinConfidence?: number;
+};
+
+/**
+ * Per-run document options (passed to `runDocumentOcr`, not baked into the
+ * model — toggling them needs no reload). Each pre-processing pass still
+ * requires the document models to have been loaded (`config.documentModels`).
+ * @category Types
+ */
+export type RunDocumentOcrOptions = {
+ /**
+ * Detect + correct page orientation before OCR. No-op without loaded document
+ * models. Defaults to the model's `config.orientation`.
+ */
+ readonly orientation?: boolean;
+ /**
+ * Geometrically dewarp the page before OCR. No-op without loaded document
+ * models. Defaults to the model's `config.dewarp`.
+ */
+ readonly dewarp?: boolean;
+};
+
+// Layout classes that carry no text — skip OCR on them.
+const VISUAL_LABELS = ['image', 'chart', 'seal'];
+
+function makeBlock(
+ regionType: L | 'ungrouped',
+ bbox: BoundingBox<'xyxy'>,
+ score: number,
+ lines: OcrDetection[],
+ isTable: boolean
+): DocumentBlock {
+ 'worklet';
+ const sorted = orderByReadingOrder(lines);
+ return {
+ regionType,
+ bbox,
+ score,
+ isTable,
+ lines: sorted,
+ text: sorted.map((l) => l.text).join('\n'),
+ };
+}
+
+/**
+ * Creates the document OCR orchestrator. Pipeline: correct orientation → dewarp
+ * (document models) → layout → per-region OCR (each text region is cropped and OCR'd on
+ * its own, upscaled into the detector — far better recall on dense pages than one
+ * whole-page pass; lines are offset back to page coords) → tables recognize their
+ * structure and fill cells with that region's OCR. Visual regions are skipped.
+ * Without layout it OCRs the whole page into one block. Layout/document models optional.
+ * @category Typescript API
+ * @param config OCR model + optional layout + optional document models + flags.
+ * @param runtime Optional worklet runtime thread.
+ * @returns A promise resolving to run + disposal controls.
+ */
+export async function createDocumentOcr(
+ config: DocumentOcrModel,
+ runtime?: WorkletRuntime
+): Promise<{
+ dispose: () => void;
+ runDocumentOcr: (
+ input: ImageBuffer,
+ options?: RunDocumentOcrOptions
+ ) => Promise>;
+ runDocumentOcrWorklet: (input: ImageBuffer, options?: RunDocumentOcrOptions) => DocumentResult;
+}> {
+ const ocr = await createOcr(config.ocr, runtime);
+ let layout: Awaited>> | null = null;
+ let documentModels: Awaited> | null = null;
+ try {
+ layout = config.layout ? await createObjectDetector<'xyxy', L>(config.layout, runtime) : null;
+ documentModels = config.documentModels
+ ? await createDocumentModels(config.documentModels, runtime)
+ : null;
+ } catch (e) {
+ // A later model failing to build must not leak the ones already built.
+ layout?.dispose();
+ ocr.dispose();
+ throw e;
+ }
+ const defaultOrientation = !!config.orientation;
+ const defaultDewarp = !!config.dewarp;
+ const minConfidence = config.orientationMinConfidence ?? 0.85;
+
+ const dispose = () => {
+ ocr.dispose();
+ layout?.dispose();
+ documentModels?.dispose();
+ };
+
+ const runDocumentOcrWorklet = (
+ input: ImageBuffer,
+ options?: RunDocumentOcrOptions
+ ): DocumentResult => {
+ 'worklet';
+ const useOrientation = !!documentModels && (options?.orientation ?? defaultOrientation);
+ const useDewarp = !!documentModels && (options?.dewarp ?? defaultDewarp);
+ let img = input;
+ if ((useOrientation || useDewarp) && documentModels) {
+ const ch = FORMAT_CHANNELS[input.format];
+ let page = tensor('uint8', [input.height, input.width, ch]);
+ page.setData(input.data);
+ let pw = input.width;
+ let ph = input.height;
+ try {
+ if (useOrientation) {
+ const orientation = documentModels.detectOrientationWorklet(page, input.format);
+ const deg = ((360 - orientation.rotationCW) % 360) as 0 | 90 | 180 | 270;
+ if (deg !== 0 && orientation.confidence >= minConfidence) {
+ const swap = deg === 90 || deg === 270;
+ const rotated = tensor('uint8', [swap ? pw : ph, swap ? ph : pw, ch]);
+ try {
+ rotate(page, rotated, deg);
+ } catch (e) {
+ rotated.dispose(); // rotate threw before we adopted `rotated` as `page`
+ throw e;
+ }
+ page.dispose();
+ page = rotated;
+ if (swap) {
+ [pw, ph] = [ph, pw];
+ }
+ }
+ }
+ if (useDewarp) {
+ // dewarp returns the input tensor unchanged when it declines the warp.
+ const dewarped = documentModels.dewarpWorklet(page, input.format);
+ if (dewarped !== page) {
+ page.dispose();
+ page = dewarped;
+ }
+ }
+ const out = new Uint8Array(pw * ph * ch);
+ page.getData(out);
+ img = { data: out, width: pw, height: ph, format: input.format, layout: input.layout };
+ } finally {
+ page.dispose();
+ }
+ }
+
+ try {
+ if (!layout) {
+ const detections = ocr.runOcrWorklet(img, { release: false }).detections;
+ const blocks = detections.length
+ ? [
+ makeBlock(
+ 'ungrouped',
+ boundsOfPoints(
+ detections.flatMap((d) => d.quad as Point[]),
+ 'xyxy'
+ ),
+ 1,
+ detections,
+ false
+ ),
+ ]
+ : [];
+ return { blocks, regions: [], detections, image: img };
+ }
+
+ const regions = layout.detectObjectsWorklet(img);
+ const blocks: DocumentBlock[] = [];
+ const detections: OcrDetection[] = [];
+ for (const region of regions) {
+ if (VISUAL_LABELS.includes(String(region.label))) {
+ continue;
+ }
+ const { xmin, ymin } = region.box;
+ const crop = cropImageBuffer(img, region.box);
+ const lines = ocr.runOcrWorklet(crop, { release: false }).detections.map((d) => ({
+ ...d,
+ quad: d.quad.map((p) => ({ x: p.x + xmin, y: p.y + ymin })),
+ }));
+ if (lines.length === 0 && region.label !== 'table') {
+ continue;
+ }
+ detections.push(...lines);
+ let block = makeBlock(
+ region.label,
+ region.box,
+ region.confidence,
+ lines,
+ region.label === 'table'
+ );
+ if (region.label === 'table' && documentModels) {
+ const structure = documentModels.recognizeTableWorklet(crop);
+ block = { ...block, tableHtml: fillTableCells(structure.html, block.lines) };
+ }
+ blocks.push(block);
+ }
+ blocks.sort((a, b) => a.bbox.ymin - b.bbox.ymin || a.bbox.xmin - b.bbox.xmin);
+ return { blocks, regions, detections, image: img };
+ } finally {
+ // Per-region runs pass release: false; the bucket arenas are freed once per page.
+ ocr.releaseMethodsWorklet();
+ }
+ };
+
+ const runDocumentOcr = wrapAsync(runDocumentOcrWorklet, runtime);
+ return { runDocumentOcr, runDocumentOcrWorklet, dispose };
+}
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/ocr/ocr.ts b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/ocr.ts
new file mode 100644
index 0000000000..d1263873b1
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/ocr.ts
@@ -0,0 +1,405 @@
+import type { WorkletRuntime } from 'react-native-worklets';
+
+import { tensor, type Tensor } from '../../../../core/tensor';
+import { loadModel } from '../../../../core/model';
+import { wrapAsync } from '../../../../core/runtime';
+
+import type { ImageBuffer } from '../../image';
+import type { Point } from '../../ops/points';
+import { FORMAT_CHANNELS, FORMAT_CONVERSION, cvtColor } from '../../ops/image';
+import { orderQuad, quadSize, boundingQuadOf } from '../../ops/quad';
+import type { TextBoxExtractor } from './detectors';
+import { orderByReadingOrder, groupVerticalColumns, type Buckets } from './ocrUtils';
+import {
+ detectQuads,
+ recognizeQuad,
+ recognizeGlyphStrip,
+ readStackedColumn,
+ resolveDetectorContract,
+ resolveRecognizerContract,
+ type DetSet,
+ type RecSet,
+ type DetectContext,
+ type RecContext,
+ type VerticalContext,
+} from './pipeline';
+
+export type { Buckets } from './ocrUtils';
+export type { Quad } from '../../ops/quad';
+export type { TextBoxExtractor } from './detectors';
+
+/**
+ * Configuration for the OCR pipeline: a model declares its input-size buckets, its
+ * charset, and its detector box-extraction strategy. The pipeline is
+ * architecture-agnostic — it validates the detect/recognize contract at load and
+ * takes everything model-specific here. The built-in {@link craftExtractBoxes} /
+ * {@link dbnetExtractBoxes} cover EasyOCR / PaddleOCR; other models supply their own
+ * {@link TextBoxExtractor} and override the recognizer normalization/padding/decode.
+ * @category Types
+ */
+export type OcrOptions = {
+ /**
+ * The model's static input-size buckets. The pipeline snaps each image to the
+ * closest `detect`/`recognize` bucket and calls the matching per-size method
+ * (`detect_` / `recognize_`). See {@link Buckets}.
+ */
+ readonly buckets: Buckets;
+ /**
+ * Recognizer charset (a string = one codepoint per index; an array is taken
+ * verbatim, for multi-codepoint entries like ligatures).
+ */
+ readonly charset: string | readonly string[];
+ /**
+ * Detector box-extraction strategy: maps the raw `detect_` outputs to oriented
+ * quads. Use the built-in {@link craftExtractBoxes} / {@link dbnetExtractBoxes}, or
+ * supply your own {@link TextBoxExtractor} to plug in a new detector.
+ */
+ readonly extractBoxes: TextBoxExtractor;
+ /** Drop detections scoring below this. Defaults to 0. */
+ readonly dropScore?: number;
+ /**
+ * Recognizer input normalization, applied after the warp as `x·alpha + beta`
+ * (scalar, or per-RGB-channel `[r,g,b]`). Defaults to `(x/255 − 0.5)/0.5` →
+ * `[−1,1]` (`alpha = 1/127.5`, `beta = −1`). Override for a recognizer trained
+ * with different normalization (e.g. ImageNet). The detector input norm is
+ * fixed by contract: RGB ÷ 255, with mean/std baked into the PTE.
+ */
+ readonly recognizerNorm?: {
+ readonly alpha: number | readonly number[];
+ readonly beta: number | readonly number[];
+ };
+ /** Recognizer canvas padding fill value. Defaults to 128 (neutral gray). */
+ readonly recognizerPadValue?: number;
+ /**
+ * Custom recognizer decode, replacing the built-in greedy CTC. Receives the raw
+ * `recognize_` output tensor (shape `[1, T, V]`) and the charset, and returns the
+ * recognized text plus a confidence in `[0,1]`. Use for non-CTC heads (attention/AR
+ * decoders) or custom scoring. MUST be a worklet.
+ */
+ readonly decode?: (
+ logits: Tensor,
+ charset: readonly string[]
+ ) => { readonly text: string; readonly confidence: number };
+};
+
+/**
+ * Per-run OCR options (passed to `runOcr`, not baked into the model — toggling
+ * them needs no reload).
+ * @category Types
+ */
+export type RunOcrOptions = {
+ /**
+ * Add handling for upright stacked columns (e.g. vertical signage, shipping-
+ * container codes — letters stacked top-to-bottom) on top of the normal
+ * horizontal read. X-aligned stacked glyph boxes are joined into one column
+ * word; a single tall box is cropped and its glyphs re-detected. Horizontal
+ * lines still read normally, so this only ADDS capability (at extra compute).
+ */
+ readonly vertical?: boolean;
+ /** Height/width ratio above which a box is treated as a stacked column. Default 1.5. */
+ readonly tallCropRatio?: number;
+ /** Max stacked-column re-detection passes per page (each is detector-scale). Default 8. */
+ readonly maxRedetections?: number;
+ /**
+ * Free the model's bucket-method activation arenas (`detect_`/`recognize_`)
+ * after this run, so memory doesn't accumulate as image/box sizes vary across
+ * runs (worse on CoreML, which compiles a graph per method). Default `true`.
+ * The document orchestrator passes `false` for its per-region OCR calls and
+ * frees once per page via `releaseMethodsWorklet` instead, so it keeps the run's
+ * working set cached while still bounding memory.
+ */
+ readonly release?: boolean;
+};
+
+/**
+ * Model configuration required to instantiate an OCR task runner: one fused PTE
+ * exposing the per-size `detect_` / `recognize_` methods, plus its options.
+ * @category Types
+ */
+export type OcrModel = {
+ readonly modelPath: string;
+ readonly ocrOpts: OcrOptions;
+};
+
+/**
+ * A single recognized text region.
+ * @category Types
+ */
+export type OcrDetection = {
+ readonly text: string;
+ readonly confidence: number;
+ /**
+ * The oriented quad (TL,TR,BR,BL) in original image pixels. Derive the
+ * axis-aligned bounds with `boundsOfPoints(quad, 'xyxy')` from `cv.ops.quad` if needed.
+ */
+ readonly quad: readonly Point[];
+};
+
+/**
+ * The result of one OCR run: the recognized text regions.
+ * @category Types
+ */
+export type OcrResult = {
+ readonly detections: OcrDetection[];
+};
+
+const RECOGNIZER_ALPHA = 1 / 127.5; // (x/255 - 0.5)/0.5 -> [-1, 1]
+const RECOGNIZER_BETA = -1;
+const RECOGNIZER_PAD_VALUE = 128; // neutral gray
+const TALL_CROP_RATIO = 1.5;
+const MAX_VERTICAL_REDETECTIONS = 8;
+// Vertical reads are lower-confidence and opt-in, so they skip the drop-score gate.
+const VERTICAL_DROP_SCORE = 0;
+
+function pushDetection(
+ out: OcrDetection[],
+ threshold: number,
+ text: string,
+ conf: number,
+ quad: readonly Point[]
+): void {
+ 'worklet';
+ if (text.length > 0 && conf >= threshold) {
+ out.push({ text, confidence: conf, quad });
+ }
+}
+
+/**
+ * Creates a unified OCR runner for two-stage detect → recognize models
+ * (EasyOCR / PaddleOCR). It loads one fused PTE, validates every
+ * `detect_` / `recognize_` bucket method, pre-allocates static scratch
+ * tensors sized from the model's compiled shapes, and returns recognition +
+ * disposal controls.
+ * @category Typescript API
+ * @param config The model path and OCR options ({@link OcrModel}).
+ * @param runtime Optional worklet runtime thread on which to run the pipeline.
+ * @returns A promise resolving to an object with recognition and disposal
+ * controls.
+ */
+export async function createOcr(
+ config: OcrModel,
+ runtime?: WorkletRuntime
+): Promise<{
+ dispose: () => void;
+ runOcr: (input: ImageBuffer, options?: RunOcrOptions) => Promise;
+ runOcrWorklet: (input: ImageBuffer, options?: RunOcrOptions) => OcrResult;
+ /** Free all bucket-method arenas without disposing the model (see `RunOcrOptions.release`). */
+ releaseMethodsWorklet: () => void;
+}> {
+ const { modelPath, ocrOpts } = config;
+ const model = await wrapAsync(loadModel, runtime)(modelPath);
+
+ const dropScore = ocrOpts.dropScore ?? 0;
+ const recNormAlpha = ocrOpts.recognizerNorm?.alpha ?? RECOGNIZER_ALPHA;
+ const recNormBeta = ocrOpts.recognizerNorm?.beta ?? RECOGNIZER_BETA;
+ const recPadValue = ocrOpts.recognizerPadValue ?? RECOGNIZER_PAD_VALUE;
+ const recDecode = ocrOpts.decode;
+
+ const detBuckets = ocrOpts.buckets.detect;
+ const recBuckets = ocrOpts.buckets.recognize;
+ // Validation + scratch allocation can throw; each tensor is pushed into
+ // `allocated` the moment it exists (one call per statement) so the catch can
+ // dispose every native allocation — a bad config must not leak.
+ const allocated: Tensor[] = [];
+ const recSets: RecSet[] = [];
+ let recC = 3;
+ let recH = 0;
+ let charset: string[] = [];
+ let recSetByWidth: ReadonlyMap = new Map();
+ const detSets: DetSet[] = [];
+ let detSetByS: ReadonlyMap = new Map();
+ try {
+ if (detBuckets.length === 0 || recBuckets.length === 0) {
+ throw new Error(
+ 'OCR: buckets.detect and buckets.recognize must each list at least one size.'
+ );
+ }
+ const detContract = resolveDetectorContract(model, detBuckets);
+ const rec = resolveRecognizerContract(model, recBuckets);
+ recC = rec.recC;
+ recH = rec.recH;
+ for (const bucket of rec.buckets) {
+ const tCanvas = tensor('uint8', [rec.recH, bucket.width, rec.recC]);
+ allocated.push(tCanvas);
+ const tCF = tensor('uint8', [rec.recC, rec.recH, bucket.width]);
+ allocated.push(tCF);
+ const tNorm = tensor('float32', [rec.recC, rec.recH, bucket.width]);
+ allocated.push(tNorm);
+ const tInput = tensor('float32', bucket.inShape);
+ allocated.push(tInput);
+ const tLogits = tensor('float32', bucket.outShape);
+ allocated.push(tLogits);
+ recSets.push({ width: bucket.width, tCanvas, tCF, tNorm, tInput, tLogits });
+ }
+ recSetByWidth = new Map(recSets.map((recSet) => [recSet.width, recSet]));
+
+ if (recC !== 3) {
+ throw new Error(`OCR: recognizer must take RGB (3 channels), but the model expects ${recC}.`);
+ }
+ // CTC lookup: index 0 is the blank (ctcCollapse never decodes it), then the
+ // model's characters — a string splits into codepoints, an array is taken
+ // verbatim (preserving multi-codepoint entries like ligatures).
+ charset = [
+ '[blank]',
+ ...(typeof ocrOpts.charset === 'string' ? Array.from(ocrOpts.charset) : ocrOpts.charset),
+ ];
+ if (charset.length !== rec.vocabSize) {
+ throw new Error(
+ `OCR: charset size (${charset.length}, incl. blank) must match recognizer output vocab (${rec.vocabSize}).`
+ );
+ }
+ for (const { s, outputs } of detContract) {
+ const tColor = tensor('uint8', [s, s, 3]);
+ allocated.push(tColor);
+ const tCF = tensor('uint8', [3, s, s]);
+ allocated.push(tCF);
+ const tNorm = tensor('float32', [3, s, s]);
+ allocated.push(tNorm);
+ const tInput = tensor('float32', [1, 3, s, s]);
+ allocated.push(tInput);
+ const tOutputs: Tensor[] = [];
+ for (const spec of outputs) {
+ const tOut = tensor(spec.dtype, spec.shape);
+ allocated.push(tOut);
+ tOutputs.push(tOut);
+ }
+ detSets.push({ s, tColor, tCF, tNorm, tInput, tOutputs });
+ }
+ detSetByS = new Map(detSets.map((detSet) => [detSet.s, detSet]));
+ } catch (e) {
+ for (const t of allocated) {
+ t.dispose();
+ }
+ model.dispose();
+ throw e;
+ }
+
+ const dispose = () => {
+ allocated.forEach((t) => t.dispose());
+ model.dispose();
+ };
+
+ // Frees each bucket method's activation arena without disposing the model; a
+ // freed method transparently reloads on its next execute. Must precede
+ // runOcrWorklet: the worklet plugin resolves referenced worklets by source order.
+ const releaseMethodsWorklet = () => {
+ 'worklet';
+ for (const s of detBuckets) {
+ model.unloadMethod(`detect_${s}`);
+ }
+ for (const w of recBuckets) {
+ model.unloadMethod(`recognize_${w}`);
+ }
+ };
+
+ const runOcrWorklet = (input: ImageBuffer, options?: RunOcrOptions): OcrResult => {
+ 'worklet';
+ const vertical = options?.vertical ?? false;
+ const tallCropRatio = options?.tallCropRatio ?? TALL_CROP_RATIO;
+ const maxRedetections = options?.maxRedetections ?? MAX_VERTICAL_REDETECTIONS;
+ const release = options?.release ?? true;
+ const { data, width, height, format } = input;
+ const numChannels = FORMAT_CHANNELS[format];
+ const rgbCode = FORMAT_CONVERSION[format].rgb;
+
+ const detCtx: DetectContext = {
+ model,
+ detBuckets,
+ numChannels,
+ toRgbCode: rgbCode,
+ extractBoxes: ocrOpts.extractBoxes,
+ detSets: detSetByS,
+ };
+
+ const tInputRaw = tensor('uint8', [height, width, numChannels]);
+ let tRecImage: Tensor | null = null;
+ try {
+ tInputRaw.setData(data);
+
+ const quads = detectQuads(detCtx, tInputRaw, width, height);
+ if (quads.length === 0) {
+ return { detections: [] };
+ }
+
+ let recSrc = tInputRaw;
+ if (rgbCode !== null) {
+ tRecImage = tensor('uint8', [height, width, recC]);
+ recSrc = cvtColor(tInputRaw, tRecImage, rgbCode);
+ }
+ const recCtx: RecContext = {
+ model,
+ recSetByWidth,
+ recBuckets,
+ recH,
+ charset,
+ normAlpha: recNormAlpha,
+ normBeta: recNormBeta,
+ padValue: recPadValue,
+ decode: recDecode,
+ };
+ const verticalCtx: VerticalContext = {
+ detCtx,
+ rawPage: tInputRaw,
+ recC,
+ tallCropRatio,
+ redetectBudget: { remaining: maxRedetections },
+ };
+
+ const detections: OcrDetection[] = [];
+
+ const ordered: Point[][] = [];
+ for (const quad of quads) {
+ const orderedQuad = orderQuad(quad);
+ const size = quadSize(orderedQuad);
+ if (size.width >= 3 && size.height >= 3) {
+ ordered.push(orderedQuad);
+ }
+ }
+
+ if (!vertical) {
+ for (const orderedQuad of ordered) {
+ const { text, conf } = recognizeQuad(recCtx, recSrc, orderedQuad);
+ pushDetection(detections, dropScore, text, conf, orderedQuad);
+ }
+ return { detections: orderByReadingOrder(detections) };
+ }
+
+ const { columns, singles } = groupVerticalColumns(ordered);
+ for (const col of columns) {
+ const strip = recognizeGlyphStrip(recCtx, recSrc, col);
+ if (strip) {
+ pushDetection(
+ detections,
+ VERTICAL_DROP_SCORE,
+ strip.text,
+ strip.conf,
+ boundingQuadOf(col)
+ );
+ }
+ }
+ for (const orderedQuad of singles) {
+ const size = quadSize(orderedQuad);
+ if (size.height >= size.width * verticalCtx.tallCropRatio) {
+ const stacked = readStackedColumn(recCtx, verticalCtx, orderedQuad, size);
+ if (stacked) {
+ pushDetection(detections, VERTICAL_DROP_SCORE, stacked.text, stacked.conf, orderedQuad);
+ continue;
+ }
+ }
+ const { text, conf } = recognizeQuad(recCtx, recSrc, orderedQuad);
+ pushDetection(detections, dropScore, text, conf, orderedQuad);
+ }
+ return { detections: orderByReadingOrder(detections) };
+ } finally {
+ tInputRaw.dispose();
+ tRecImage?.dispose();
+ if (release) {
+ releaseMethodsWorklet();
+ }
+ }
+ };
+
+ const runOcr = wrapAsync(runOcrWorklet, runtime);
+
+ return { runOcr, runOcrWorklet, dispose, releaseMethodsWorklet };
+}
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/ocr/ocrUtils.ts b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/ocrUtils.ts
new file mode 100644
index 0000000000..32c18131ad
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/ocrUtils.ts
@@ -0,0 +1,284 @@
+import type { Point } from '../../ops/points';
+import { boundsOfPoints } from '../../ops/quad';
+
+/**
+ * The static input-size buckets a bucketed OCR model exposes. Each model ships
+ * per-size detect (`detect_`, square `S×S` input) and recognize
+ * (`recognize_`, fixed height, width `W`) methods; the pipeline snaps each
+ * input to the nearest bucket and calls the matching method. Both lists must be
+ * ascending.
+ * @category Types
+ */
+export type Buckets = {
+ readonly detect: readonly number[];
+ readonly recognize: readonly number[];
+};
+
+// A size within this fraction above the next-lower bucket snaps DOWN to it rather
+// than up, so a marginal overflow (e.g. 641 px against a 640 bucket) doesn't jump
+// to the next, much larger and slower, bucket. Kept small: a larger downscale
+// loses detail the model was trained to see.
+const BUCKET_SNAP_TOLERANCE = 0.1;
+
+/**
+ * Selects the smallest bucket that fits `size`, but snaps down to the next-lower
+ * bucket when `size` exceeds it by no more than the snap tolerance; oversized
+ * inputs clamp to the largest bucket. Detector callers pass the image's longest
+ * side (selecting `detect_`); recognizer callers pass the desired crop
+ * content width (selecting `recognize_`).
+ * @category Typescript API
+ * @param size The size to fit, in pixels.
+ * @param buckets The model's ascending bucket sizes.
+ * @returns The selected bucket size.
+ */
+export function snapBucket(size: number, buckets: readonly number[]): number {
+ 'worklet';
+ for (let i = 0; i < buckets.length; i++) {
+ if (buckets[i]! >= size) {
+ const lower = i > 0 ? buckets[i - 1]! : 0;
+ return lower > 0 && size <= lower * (1 + BUCKET_SNAP_TOLERANCE) ? lower : buckets[i]!;
+ }
+ }
+ return buckets[buckets.length - 1]!;
+}
+
+/**
+ * Computes the content width (px) of a recognizer crop: the region resized to the
+ * recognizer height keeping its aspect ratio, clamped to the bucket width.
+ * @category Typescript API
+ * @param regionWidth The region's natural width.
+ * @param regionHeight The region's natural height.
+ * @param recognizerHeight The recognizer input height.
+ * @param bucketWidth The recognizer input (canvas) width.
+ * @returns The clamped content width in pixels.
+ */
+export function contentWidthFor(
+ regionWidth: number,
+ regionHeight: number,
+ recognizerHeight: number,
+ bucketWidth: number
+): number {
+ 'worklet';
+ const width = Math.round((recognizerHeight * regionWidth) / Math.max(1, regionHeight));
+ return Math.max(1, Math.min(width, bucketWidth));
+}
+
+// A gutter must be at least this fraction of the content width to split columns;
+// two boxes share a line when their vertical extents overlap by at least this
+// fraction of the shorter box's height.
+const COLUMN_GAP_FRACTION = 0.06;
+const LINE_OVERLAP_FRACTION = 0.3;
+
+/**
+ * Reorders items carrying a `quad` into human reading order: multi-column inputs
+ * read column-by-column, single-column inputs line-by-line, and boxes within a
+ * line left-to-right. Detectors emit boxes in an arbitrary order, so detections
+ * and assembled blocks are ordered through this.
+ * @category Typescript API
+ * @param items The items to reorder, each carrying a `quad`.
+ * @returns The items in reading order.
+ */
+export function orderByReadingOrder(items: T[]): T[] {
+ 'worklet';
+ const count = items.length;
+ if (count <= 1) {
+ return items;
+ }
+
+ // 1. Axis-aligned bounds per quad, plus the content x-range: a column gutter
+ // must be at least COLUMN_GAP_FRACTION of that range to count.
+ const boxes = items.map((it) => boundsOfPoints(it.quad, 'xyxy'));
+ let minX = Infinity;
+ let maxX = -Infinity;
+ for (const box of boxes) {
+ if (box.xmin < minX) minX = box.xmin;
+ if (box.xmax > maxX) maxX = box.xmax;
+ }
+ const minGap = COLUMN_GAP_FRACTION * Math.max(1, maxX - minX);
+
+ // 2. Find column gutters with an x-coverage sweep over the box edges: while
+ // inside any box the coverage counter is > 0; a zero-coverage span wider
+ // than minGap is a gutter, and its midpoint becomes a column cut.
+ const edges: { x: number; delta: number }[] = [];
+ for (const box of boxes) {
+ edges.push({ x: box.xmin, delta: 1 });
+ edges.push({ x: box.xmax, delta: -1 });
+ }
+ // At equal x, open (+1) before close (-1) so touching boxes don't open a gutter.
+ edges.sort((a, b) => a.x - b.x || b.delta - a.delta);
+ const cuts: number[] = [];
+ let coverage = 0;
+ let gutterStart = 0;
+ for (const edge of edges) {
+ const before = coverage;
+ coverage += edge.delta;
+ if (before > 0 && coverage === 0) {
+ gutterStart = edge.x;
+ } else if (before === 0 && coverage > 0 && edge.x - gutterStart >= minGap) {
+ cuts.push((gutterStart + edge.x) / 2);
+ }
+ }
+
+ // 3. Assign each box to a column: count how many (ascending) cuts its
+ // center-x lies to the right of.
+ const columns: number[][] = Array.from({ length: cuts.length + 1 }, () => []);
+ for (let i = 0; i < count; i++) {
+ const centerX = (boxes[i]!.xmin + boxes[i]!.xmax) / 2;
+ let column = 0;
+ for (const cut of cuts) {
+ if (centerX > cut) column++;
+ }
+ columns[column]!.push(i);
+ }
+
+ // 4. Within each (left-to-right) column: group boxes into lines by vertical
+ // overlap (≥ LINE_OVERLAP_FRACTION of the shorter height joins a line),
+ // order lines top-to-bottom and boxes within a line left-to-right.
+ const order: number[] = [];
+ for (const column of columns) {
+ column.sort((a, b) => boxes[a]!.ymin - boxes[b]!.ymin);
+ const lines: { items: number[]; ymin: number; ymax: number }[] = [];
+ for (const i of column) {
+ const box = boxes[i]!;
+ let placed = false;
+ for (const line of lines) {
+ const overlap = Math.min(line.ymax, box.ymax) - Math.max(line.ymin, box.ymin);
+ const minHeight = Math.min(line.ymax - line.ymin, box.ymax - box.ymin);
+ if (overlap >= LINE_OVERLAP_FRACTION * Math.max(1, minHeight)) {
+ line.items.push(i);
+ line.ymin = Math.min(line.ymin, box.ymin);
+ line.ymax = Math.max(line.ymax, box.ymax);
+ placed = true;
+ break;
+ }
+ }
+ if (!placed) {
+ lines.push({ items: [i], ymin: box.ymin, ymax: box.ymax });
+ }
+ }
+ lines.sort((a, b) => a.ymin - b.ymin);
+ for (const line of lines) {
+ line.items.sort(
+ (a, b) => boxes[a]!.xmin + boxes[a]!.xmax - (boxes[b]!.xmin + boxes[b]!.xmax)
+ );
+ order.push(...line.items);
+ }
+ }
+ return order.map((i) => items[i]!);
+}
+
+// A box wider than this multiple of its height is a horizontal line, never a
+// stacked-column glyph. A box joins a column when its x-span overlaps the
+// column's by COLUMN_X_OVERLAP of the narrower width and the y-gap is within
+// COLUMN_Y_GAP of its height.
+const COLUMN_GLYPH_ASPECT = 1.6;
+const COLUMN_X_OVERLAP = 0.25;
+const COLUMN_Y_GAP = 2.5;
+
+/**
+ * Clusters glyph-like, x-aligned, vertically-stacked boxes into columns; wide
+ * lines and isolated boxes are returned as `singles` to read normally. This lets
+ * a vertical-text pass add column reading without disturbing horizontal reads.
+ * @category Typescript API
+ * @param quads The detected text quads (ordered TL,TR,BR,BL).
+ * @returns The detected `columns` (each a top-to-bottom list of quads) and the
+ * leftover `singles` (horizontal lines / isolated boxes).
+ */
+export function groupVerticalColumns(quads: readonly (readonly Point[])[]): {
+ columns: Point[][][];
+ singles: Point[][];
+} {
+ 'worklet';
+ type Candidate = {
+ quad: Point[];
+ xmin: number;
+ xmax: number;
+ ymin: number;
+ ymax: number;
+ width: number;
+ height: number;
+ };
+ const candidates: Candidate[] = [];
+ const singles: Point[][] = [];
+ for (const q of quads) {
+ const { xmin, ymin, xmax, ymax } = boundsOfPoints(q, 'xyxy');
+ const width = xmax - xmin;
+ const height = ymax - ymin;
+ if (width > height * COLUMN_GLYPH_ASPECT) {
+ singles.push(q as Point[]);
+ } else {
+ candidates.push({ quad: q as Point[], xmin, xmax, ymin, ymax, width, height });
+ }
+ }
+ // Grow each column top-to-bottom from its current bottom box, checking alignment
+ // against the column's accumulated x-range so a narrow glyph between wider ones
+ // doesn't break the run.
+ candidates.sort((a, b) => a.ymin - b.ymin);
+ type Column = { boxes: Candidate[]; xmin: number; xmax: number; bottom: number };
+ const columns: Column[] = [];
+ for (const box of candidates) {
+ let placed = false;
+ for (const column of columns) {
+ const overlap = Math.min(box.xmax, column.xmax) - Math.max(box.xmin, column.xmin);
+ const aligned = overlap > COLUMN_X_OVERLAP * Math.min(box.width, column.xmax - column.xmin);
+ const gap = box.ymin - column.bottom;
+ if (aligned && gap < COLUMN_Y_GAP * box.height && gap > -0.5 * box.height) {
+ column.boxes.push(box);
+ column.xmin = Math.min(column.xmin, box.xmin);
+ column.xmax = Math.max(column.xmax, box.xmax);
+ column.bottom = box.ymax;
+ placed = true;
+ break;
+ }
+ }
+ if (!placed) {
+ columns.push({ boxes: [box], xmin: box.xmin, xmax: box.xmax, bottom: box.ymax });
+ }
+ }
+ const grouped: Point[][][] = [];
+ for (const column of columns) {
+ if (column.boxes.length >= 2) {
+ grouped.push(column.boxes.map((b) => b.quad));
+ } else {
+ singles.push(column.boxes[0]!.quad);
+ }
+ }
+ return { columns: grouped, singles };
+}
+
+/**
+ * Collapses a greedy-CTC argmax path into recognized text and a confidence score:
+ * reserved low indices (the CTC blank, `< numSpecials`) and consecutive repeats
+ * are dropped from the text, and the confidence is the mean value over the
+ * non-reserved timesteps.
+ * @category Typescript API
+ * @param indices The per-timestep argmax indices.
+ * @param values The per-timestep max values (probabilities).
+ * @param charset The charset lookup, reserved tokens at the front.
+ * @param numSpecials Number of reserved low indices (default 1 = CTC blank).
+ * @returns The decoded `text` and its `confidence` in `[0, 1]`.
+ */
+export function ctcCollapse(
+ indices: number[],
+ values: number[],
+ charset: readonly string[],
+ numSpecials = 1
+): { text: string; confidence: number } {
+ 'worklet';
+ let text = '';
+ let last = -1;
+ let sum = 0;
+ let count = 0;
+ for (let i = 0; i < indices.length; i++) {
+ const idx = indices[i]!;
+ if (idx >= numSpecials) {
+ sum += values[i]!;
+ count++;
+ if (idx !== last && idx < charset.length) {
+ text += charset[idx]!;
+ }
+ }
+ last = idx;
+ }
+ return { text, confidence: count === 0 ? 0 : sum / count };
+}
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/ocr/pipeline.ts b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/pipeline.ts
new file mode 100644
index 0000000000..5397b41dd0
--- /dev/null
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/ocr/pipeline.ts
@@ -0,0 +1,347 @@
+// OCR pipeline engine: the per-page / per-box worklet functions and the
+// construction-time contract resolvers behind `createOcr` (in ocr.ts). Internal
+// to the OCR task — not re-exported from the package index.
+//
+// Worklet source-order: the worklet plugin captures a referenced worklet by its
+// position in this file, so a worklet must be defined before any worklet that
+// calls it (recognizeCanvas -> recognizeQuad/recognizeGlyphStrip;
+// recognizeGlyphStrip -> readStackedColumn). The construction-time resolvers run
+// on the JS thread and have no such constraint.
+
+import { rnexecutorchJsi } from '../../../../native/bridge';
+import { tensor, type Tensor, type DType } from '../../../../core/tensor';
+import { validateModelSchema, SymbolicTensor } from '../../../../core/modelSchema';
+import type { Model } from '../../../../core/model';
+
+import type { Point } from '../../ops/points';
+import {
+ resize,
+ cvtColor,
+ toChannelsFirst,
+ normalize,
+ warpQuad,
+ type ColorConversionCode,
+} from '../../ops/image';
+import { mapQuadToImage, orderQuad, quadSize, flattenQuad, splitTallQuad } from '../../ops/quad';
+import type { TextBoxExtractor } from './detectors';
+import { contentWidthFor, ctcCollapse, snapBucket } from './ocrUtils';
+
+// The detector consumes raw RGB scaled to [0,1]; its mean/std normalization is
+// baked into the model, so the client only divides by 255.
+const DETECTOR_ALPHA = 1 / 255;
+const DETECTOR_BETA = 0;
+
+// Per-timestep argmax + max value over recognizer logits `[..,T,V]`, computed
+// natively on the tensor buffer (avoids copying the whole tensor into JS). The
+// native op returns a flat [idx, value, ...] array, reshaped here.
+function ctcGreedyDecode(src: Tensor): { indices: number[]; values: number[] } {
+ 'worklet';
+ const flat = rnexecutorchJsi.cv.ctcGreedyDecode(src) as number[];
+ const indices: number[] = [];
+ const values: number[] = [];
+ for (let i = 0; i < flat.length; i += 2) {
+ indices.push(flat[i]!);
+ values.push(flat[i + 1]!);
+ }
+ return { indices, values };
+}
+
+/**
+ * Per-detect-bucket scratch tensors, allocated once and reused across runs:
+ * `tColor [s,s,3]` → `tCF [3,s,s]` → `tNorm [3,s,s]` → `tInput [1,3,s,s]`.
+ */
+export type DetSet = {
+ readonly s: number;
+ readonly tColor: Tensor;
+ readonly tCF: Tensor;
+ readonly tNorm: Tensor;
+ readonly tInput: Tensor;
+ readonly tOutputs: readonly Tensor[];
+};
+
+/** The detector state, bundled so it can run on the full page or on a box crop. */
+export type DetectContext = {
+ readonly model: Model;
+ readonly detBuckets: readonly number[];
+ readonly numChannels: number;
+ readonly toRgbCode: ColorConversionCode | null;
+ readonly extractBoxes: TextBoxExtractor;
+ readonly detSets: ReadonlyMap;
+};
+
+/** Per-recognize-width scratch tensors, allocated once and reused across runs. */
+export type RecSet = {
+ readonly width: number;
+ readonly tCanvas: Tensor;
+ readonly tCF: Tensor;
+ readonly tNorm: Tensor;
+ readonly tInput: Tensor;
+ readonly tLogits: Tensor;
+};
+
+/** The recognizer state; the source image is passed per call. */
+export type RecContext = {
+ readonly model: Model;
+ readonly recSetByWidth: ReadonlyMap;
+ readonly recBuckets: readonly number[];
+ readonly recH: number;
+ readonly charset: string[];
+ readonly normAlpha: number | readonly number[];
+ readonly normBeta: number | readonly number[];
+ readonly padValue: number;
+ /** Optional custom decode; falls back to greedy CTC when absent. */
+ readonly decode?: (
+ logits: Tensor,
+ charset: readonly string[]
+ ) => { readonly text: string; readonly confidence: number };
+};
+
+/** Extra state the vertical-text path needs: the detector and the source page. */
+export type VerticalContext = {
+ readonly detCtx: DetectContext;
+ readonly rawPage: Tensor;
+ readonly recC: number;
+ /** Height/width ratio above which a box is treated as a stacked column. */
+ readonly tallCropRatio: number;
+ /** Per-page budget for the (expensive) stacked-column re-detection pass. */
+ readonly redetectBudget: { remaining: number };
+};
+
+// Detects text boxes in `src` and returns their quads in `src` pixel space:
+// letterbox into the snapped square bucket, run the detector, and hand the raw
+// outputs to the model's extractor. `charLevel` requests per-glyph boxes for the
+// stacked-text pass.
+export function detectQuads(
+ ctx: DetectContext,
+ src: Tensor,
+ width: number,
+ height: number,
+ charLevel = false
+): Point[][] {
+ 'worklet';
+ const detS = snapBucket(Math.max(width, height), ctx.detBuckets);
+ const detSet = ctx.detSets.get(detS)!;
+ // Only the source resize depends on the run's channel count; the rest is cached.
+ const tDetResize = tensor('uint8', [detS, detS, ctx.numChannels]);
+ try {
+ src
+ .through(resize, tDetResize, { mode: 'letterbox', interpolation: 'area', padValue: 0 })
+ .throughIf(ctx.toRgbCode !== null, cvtColor, detSet.tColor, ctx.toRgbCode!)
+ .through(toChannelsFirst, detSet.tCF)
+ .through(normalize, detSet.tNorm, { alpha: DETECTOR_ALPHA, beta: DETECTOR_BETA })
+ .copyTo(detSet.tInput);
+
+ ctx.model.execute(`detect_${detS}`, [detSet.tInput], [...detSet.tOutputs]);
+ const quads = ctx.extractBoxes(detSet.tOutputs, detS, charLevel);
+ return quads.map((q) => mapQuadToImage(q, detS, detS, width, height));
+ } finally {
+ tDetResize.dispose();
+ }
+}
+
+// Normalizes the already-warped recognizer canvas, runs the recognizer, and
+// decodes the logits — a custom decode if the model provides one, else greedy CTC
+// (the recognizer emits probabilities). Callers prepare `tCanvas` via `warpQuad`.
+function recognizeCanvas(
+ recCtx: RecContext,
+ recSet: RecSet,
+ bucketW: number
+): { text: string; conf: number } {
+ 'worklet';
+ recSet.tCanvas
+ .through(toChannelsFirst, recSet.tCF)
+ .through(normalize, recSet.tNorm, { alpha: recCtx.normAlpha, beta: recCtx.normBeta })
+ .copyTo(recSet.tInput);
+ recCtx.model.execute(`recognize_${bucketW}`, [recSet.tInput], [recSet.tLogits]);
+ if (recCtx.decode) {
+ const r = recCtx.decode(recSet.tLogits, recCtx.charset);
+ return { text: r.text, conf: r.confidence };
+ }
+ const { indices, values } = ctcGreedyDecode(recSet.tLogits);
+ const { text, confidence } = ctcCollapse(indices, values, recCtx.charset);
+ return { text, conf: confidence };
+}
+
+// Recognizes one ordered (TL,TR,BR,BL) quad from `src`: snap its content width to
+// a recognizer bucket, warp it into the canvas, then recognize.
+export function recognizeQuad(
+ ctx: RecContext,
+ src: Tensor,
+ corners: readonly Point[]
+): { text: string; conf: number } {
+ 'worklet';
+ const size = quadSize(corners);
+ const maxRec = ctx.recBuckets[ctx.recBuckets.length - 1]!;
+ const desiredW = contentWidthFor(size.width, size.height, ctx.recH, maxRec);
+ const bucketW = snapBucket(desiredW, ctx.recBuckets);
+ const recSet = ctx.recSetByWidth.get(bucketW)!;
+ warpQuad(src, recSet.tCanvas, flattenQuad(corners), {
+ contentWidth: Math.min(desiredW, bucketW),
+ align: 'left',
+ padMode: 'constant',
+ padValue: ctx.padValue,
+ });
+ return recognizeCanvas(ctx, recSet, bucketW);
+}
+
+// Recognizes a sequence of glyph quads as a single line: each glyph is warped
+// upright to the recognizer height and placed side by side in one canvas (the
+// native warp composes directly, so there is no JS pixel assembly), then read in
+// one pass. A glyph box much taller than wide is first split into ~square
+// single-letter cells. Returns null when no usable text was produced.
+export function recognizeGlyphStrip(
+ recCtx: RecContext,
+ src: Tensor,
+ glyphs: readonly (readonly Point[])[]
+): { text: string; conf: number } | null {
+ 'worklet';
+ const recH = recCtx.recH;
+ const maxRec = recCtx.recBuckets[recCtx.recBuckets.length - 1]!;
+ // Split any tall multi-letter box into single-letter cells and size each cell's
+ // warped width (aspect preserved) to lay out the strip.
+ const cells: { quad: readonly Point[]; width: number }[] = [];
+ let totalW = 0;
+ for (const glyph of glyphs) {
+ const glyphSize = quadSize(glyph);
+ if (glyphSize.width < 1 || glyphSize.height < 1) {
+ continue;
+ }
+ const parts = Math.max(1, Math.round(glyphSize.height / Math.max(1, glyphSize.width)));
+ for (const cell of splitTallQuad(glyph, parts)) {
+ const cellSize = quadSize(cell);
+ if (cellSize.width < 1 || cellSize.height < 1) {
+ continue;
+ }
+ const width = contentWidthFor(cellSize.width, cellSize.height, recH, maxRec);
+ cells.push({ quad: cell, width });
+ totalW += width;
+ }
+ }
+ if (cells.length === 0) {
+ return null;
+ }
+ const bucketW = snapBucket(totalW, recCtx.recBuckets);
+ const recSet = recCtx.recSetByWidth.get(bucketW)!;
+ // Warp each cell into the canvas at its x-offset; the first warp clears + pads
+ // the whole canvas, the rest compose in with `clear: false`.
+ let xOff = 0;
+ for (let i = 0; i < cells.length; i++) {
+ if (xOff >= bucketW) {
+ break;
+ }
+ warpQuad(src, recSet.tCanvas, flattenQuad(cells[i]!.quad), {
+ contentWidth: cells[i]!.width,
+ offsetX: xOff,
+ clear: i === 0,
+ padMode: 'constant',
+ padValue: recCtx.padValue,
+ });
+ xOff += cells[i]!.width;
+ }
+ const { text, conf } = recognizeCanvas(recCtx, recSet, bucketW);
+ return text.length > 0 ? { text, conf } : null;
+}
+
+// Reads one tall box that the detector merged from several vertically-stacked
+// glyphs: crops it upright, re-detects the individual glyphs (char-level pass),
+// and recognizes them top-to-bottom via `recognizeGlyphStrip`. Returns null — the
+// caller then reads the box horizontally — when the box is too small, the per-page
+// re-detect budget is spent, or no glyphs are found.
+export function readStackedColumn(
+ recCtx: RecContext,
+ verticalCtx: VerticalContext,
+ ordered: readonly Point[],
+ size: { width: number; height: number }
+): { text: string; conf: number } | null {
+ 'worklet';
+ const boxW = Math.round(size.width);
+ const boxH = Math.round(size.height);
+ if (boxW < 3 || boxH < 3 || verticalCtx.redetectBudget.remaining <= 0) {
+ return null;
+ }
+ verticalCtx.redetectBudget.remaining--;
+ const tBoxRaw = tensor('uint8', [boxH, boxW, verticalCtx.detCtx.numChannels]);
+ // RGB conversion target — allocated lazily, only when the crop isn't RGB.
+ let tRecBox: Tensor | null = null;
+ try {
+ warpQuad(verticalCtx.rawPage, tBoxRaw, flattenQuad(ordered), {
+ contentWidth: boxW,
+ align: 'left',
+ padMode: 'constant',
+ padValue: 0,
+ });
+ const charQuads = detectQuads(verticalCtx.detCtx, tBoxRaw, boxW, boxH, /* charLevel */ true);
+ if (charQuads.length === 0) {
+ return null;
+ }
+ let boxSrc = tBoxRaw;
+ if (verticalCtx.detCtx.toRgbCode !== null) {
+ tRecBox = tensor('uint8', [boxH, boxW, verticalCtx.recC]);
+ boxSrc = cvtColor(tBoxRaw, tRecBox, verticalCtx.detCtx.toRgbCode);
+ }
+ // Read the stack top-to-bottom by each glyph's upper edge.
+ const glyphs = charQuads.map((q) => orderQuad(q)).sort((a, b) => a[0]!.y - b[0]!.y);
+ return recognizeGlyphStrip(recCtx, boxSrc, glyphs);
+ } finally {
+ tBoxRaw.dispose();
+ tRecBox?.dispose();
+ }
+}
+
+// Validates each `detect_` method against the shared RGB `[1, 3, S, S]` input
+// contract and reads its (model-defined) output tensor specs from the model
+// metadata. Returns specs only — the task factory allocates and owns the tensors.
+// Runs at construction; throws on a contract mismatch.
+export function resolveDetectorContract(
+ model: Model,
+ detBuckets: readonly number[]
+): { s: number; outputs: { dtype: DType; shape: number[] }[] }[] {
+ return detBuckets.map((s) => {
+ const method = `detect_${s}`;
+ // Match the declared output count with wildcard specs so validateModelSchema
+ // enforces the RGB input contract without constraining the model's outputs.
+ const outCount = model.getMethodMeta(method).outputTensorMeta.length;
+ const meta = validateModelSchema(
+ model,
+ method,
+ [SymbolicTensor('float32', [1, 3, s, s])],
+ Array.from({ length: outCount }, () => SymbolicTensor())
+ );
+ return { s, outputs: meta.outputTensorMeta.map((m) => ({ dtype: m.dtype, shape: m.shape })) };
+ });
+}
+
+// Validates each `recognize_` method and reads the recognizer contract: the
+// channel/height/vocab dimensions (constant across widths) plus each bucket's
+// input/output shapes. Returns specs only — the task factory allocates the
+// tensors. Runs at construction; throws on a contract mismatch.
+export function resolveRecognizerContract(
+ model: Model,
+ recBuckets: readonly number[]
+): {
+ recC: number;
+ recH: number;
+ vocabSize: number;
+ buckets: { width: number; inShape: number[]; outShape: number[] }[];
+} {
+ let recC = 0;
+ let recH = 0;
+ let vocabSize = 0;
+ const buckets = recBuckets.map((w, i) => {
+ const m = validateModelSchema(
+ model,
+ `recognize_${w}`,
+ [SymbolicTensor('float32', [1, 'C', 'H', 'W'])],
+ [SymbolicTensor('float32', [1, 'T', 'V'])]
+ );
+ const inShape = m.inputTensorMeta[0]!.shape;
+ const outShape = m.outputTensorMeta[0]!.shape;
+ if (i === 0) {
+ recC = inShape[1]!;
+ recH = inShape[2]!;
+ vocabSize = outShape[2]!;
+ }
+ return { width: w, inShape, outShape };
+ });
+ return { recC, recH, vocabSize, buckets };
+}
diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/preprocessing.ts b/packages/react-native-executorch/src/extensions/cv/tasks/preprocessing.ts
index 3569210481..202fe71999 100644
--- a/packages/react-native-executorch/src/extensions/cv/tasks/preprocessing.ts
+++ b/packages/react-native-executorch/src/extensions/cv/tasks/preprocessing.ts
@@ -1,7 +1,7 @@
import { tensor, type Tensor } from '../../../core/tensor';
import { matchShape } from '../../../core/modelSchema';
-import type { ImageBuffer } from '../image';
+import type { ImageBuffer, ImageFormat } from '../image';
import {
type ResizeMode,
type InterpolationMethod,
@@ -56,6 +56,16 @@ export function createImagePreprocessor(
* data.
*/
process: (input: ImageBuffer) => Tensor;
+ /**
+ * Like {@link process}, but reads from a full-res image tensor (`[H, W, C]`,
+ * uint8) already on-device instead of an `ImageBuffer`, avoiding the raw-data
+ * copy. `format` supplies the source channel count and color conversion. The
+ * returned tensor is preprocessor-managed (do not dispose).
+ * @param src The full-res source image tensor in HWC layout.
+ * @param format The pixel format of `src` (for channels + color conversion).
+ * @returns A reference to the managed output tensor.
+ */
+ processTensor: (src: Tensor, format: ImageFormat) => Tensor;
/**
* Releases all allocated native resources.
*/
@@ -114,5 +124,27 @@ export function createImagePreprocessor(
return tOutput;
};
- return { process, dispose };
+ const processTensor = (src: Tensor, format: ImageFormat): Tensor => {
+ 'worklet';
+ const numChannels = FORMAT_CHANNELS[format];
+ const colorCode = FORMAT_CONVERSION[format].rgb;
+ const tResize = tensor('uint8', [targetH, targetW, numChannels]);
+ try {
+ src
+ .through(resize, tResize, {
+ mode: resizeMode,
+ interpolation: interpolation,
+ padValue: padValue,
+ })
+ .throughIf(colorCode !== null, cvtColor, tColor, colorCode!)
+ .through(toChannelsFirst, tChanFirst)
+ .through(normalize, tNorm, { alpha, beta })
+ .copyTo(tOutput);
+ } finally {
+ tResize.dispose();
+ }
+ return tOutput;
+ };
+
+ return { process, processTensor, dispose };
}
diff --git a/packages/react-native-executorch/src/hooks/useDocumentOcr.ts b/packages/react-native-executorch/src/hooks/useDocumentOcr.ts
new file mode 100644
index 0000000000..810e2ff4cd
--- /dev/null
+++ b/packages/react-native-executorch/src/hooks/useDocumentOcr.ts
@@ -0,0 +1,64 @@
+import { useModel } from './useModel';
+import { useResourceDownload } from './useResourceDownload';
+import { createDocumentOcr, type DocumentOcrModel } from '../extensions/cv/tasks/ocr/documentOcr';
+
+// Swap a model spec's hosted `modelPath` for its downloaded local path. Returns
+// undefined when the spec is absent (an optional model) or its path hasn't
+// finished downloading yet.
+const localize = (
+ spec: M | undefined,
+ localPath: string | undefined
+): M | undefined => (spec && localPath ? { ...spec, modelPath: localPath } : undefined);
+
+/**
+ * React hook for the document OCR pipeline: OCR + optional layout detection +
+ * optional document models (orientation/dewarp/table), assembled into reading-ordered
+ * blocks. Downloads/compiles all enabled models, tracks progress and errors, and
+ * cleans up native memory on unmount or config change.
+ * @category Hooks
+ * @typeParam L The type representing the layout region class labels.
+ * @param config OCR model + optional layout/document models + flags.
+ * @param options Hook options.
+ * @param options.preventLoad If true, prevents downloading and compiling the models.
+ * @returns Loading state, error, download progress, and the document run functions.
+ */
+export function useDocumentOcr(
+ config: DocumentOcrModel,
+ options?: { preventLoad?: boolean }
+) {
+ const ocrDl = useResourceDownload(config.ocr.modelPath, options?.preventLoad);
+ const layoutDl = useResourceDownload(config.layout?.modelPath, options?.preventLoad);
+ const documentModelsDl = useResourceDownload(
+ config.documentModels?.modelPath,
+ options?.preventLoad
+ );
+
+ const ocr = localize(config.ocr, ocrDl.localPath);
+ const layout = localize(config.layout, layoutDl.localPath);
+ const documentModels = localize(config.documentModels, documentModelsDl.localPath);
+ const ready =
+ !!ocr && (!config.layout || !!layout) && (!config.documentModels || !!documentModels);
+ const localConfig: DocumentOcrModel | null = ready
+ ? { ...config, ocr: ocr!, layout, documentModels }
+ : null;
+
+ const { model, error } = useModel(createDocumentOcr, localConfig, [
+ ocrDl.localPath,
+ layoutDl.localPath,
+ documentModelsDl.localPath,
+ ]);
+
+ const downloads = [
+ ocrDl,
+ ...(config.layout ? [layoutDl] : []),
+ ...(config.documentModels ? [documentModelsDl] : []),
+ ];
+
+ return {
+ isReady: !!model,
+ error: downloads.map((d) => d.downloadError).find(Boolean) || error,
+ downloadProgress: Math.min(...downloads.map((d) => d.downloadProgress)),
+ runDocumentOcr: model?.runDocumentOcr,
+ runDocumentOcrWorklet: model?.runDocumentOcrWorklet,
+ };
+}
diff --git a/packages/react-native-executorch/src/hooks/useOcr.ts b/packages/react-native-executorch/src/hooks/useOcr.ts
new file mode 100644
index 0000000000..d234502211
--- /dev/null
+++ b/packages/react-native-executorch/src/hooks/useOcr.ts
@@ -0,0 +1,39 @@
+import { createOcr, type OcrModel } from '../extensions/cv/tasks/ocr/ocr';
+import { useResourceDownload } from './useResourceDownload';
+import { useModel } from './useModel';
+
+/**
+ * React hook for running the unified OCR pipeline (EasyOCR / PaddleOCR).
+ *
+ * Downloads the fused PTE, instantiates the OCR task runner, and manages its
+ * lifetime. Heavy work runs on a worklet thread; the returned `runOcr` resolves
+ * with the recognized text regions.
+ * @category Hooks
+ * @param config OCR model configuration (one fused PTE + flat options). Use a
+ * preset from `models.ocr.*`.
+ * @param options Optional flags. `preventLoad` defers downloading/compiling the
+ * model until set to `false`.
+ * @returns Readiness flags, download progress, and the `runOcr` /
+ * `runOcrWorklet` runners.
+ */
+export function useOcr(config: OcrModel, options?: { preventLoad?: boolean }) {
+ const { localPath, downloadProgress, downloadError } = useResourceDownload(
+ config.modelPath,
+ options?.preventLoad
+ );
+
+ const { model, error } = useModel(
+ createOcr,
+ localPath ? { ...config, modelPath: localPath } : null,
+ [localPath]
+ );
+
+ return {
+ isReady: !!model,
+ error: downloadError || error,
+ downloadProgress,
+ localPath,
+ runOcr: model?.runOcr,
+ runOcrWorklet: model?.runOcrWorklet,
+ };
+}
diff --git a/packages/react-native-executorch/src/index.ts b/packages/react-native-executorch/src/index.ts
index 00557be78b..23f4e471c3 100644
--- a/packages/react-native-executorch/src/index.ts
+++ b/packages/react-native-executorch/src/index.ts
@@ -6,6 +6,8 @@ export * from './hooks/useInstanceSegmenter';
export * from './hooks/useKeypointDetector';
export * from './hooks/useObjectDetector';
export * from './hooks/useTokenizer';
+export * from './hooks/useOcr';
+export * from './hooks/useDocumentOcr';
export * from './hooks/useResourceDownload';
export * from './hooks/useModel';
@@ -21,6 +23,10 @@ export * from './extensions/cv/tasks/instanceSegmentation';
export * from './extensions/cv/tasks/keypointDetection';
export * from './extensions/cv/tasks/objectDetection';
export * from './extensions/nlp/tasks/tokenization';
+export * from './extensions/cv/tasks/ocr/ocr';
+export * from './extensions/cv/tasks/ocr/detectors';
+export type { DocumentModelsConfig } from './extensions/cv/tasks/ocr/documentModels';
+export * from './extensions/cv/tasks/ocr/documentOcr';
// Core primitives — for library builders and power users
export { tensor } from './core/tensor';
diff --git a/packages/react-native-executorch/src/models.ts b/packages/react-native-executorch/src/models.ts
index 3397288e23..567a667e13 100644
--- a/packages/react-native-executorch/src/models.ts
+++ b/packages/react-native-executorch/src/models.ts
@@ -4,6 +4,9 @@ import type { StyleTransferModel } from './extensions/cv/tasks/styleTransfer';
import type { SemanticSegmentationModel } from './extensions/cv/tasks/semanticSegmentation';
import type { KeypointDetectorModel } from './extensions/cv/tasks/keypointDetection';
import type { InstanceSegmenterModel } from './extensions/cv/tasks/instanceSegmentation';
+import type { OcrModel, OcrOptions } from './extensions/cv/tasks/ocr/ocr';
+import { craftExtractBoxes, dbnetExtractBoxes } from './extensions/cv/tasks/ocr/detectors';
+import type { DocumentModelsConfig } from './extensions/cv/tasks/ocr/documentModels';
import {
IMAGENET_NORM,
IMAGENET1K_LABELS,
@@ -12,12 +15,17 @@ import {
COCO_CLASSES_YOLO,
BLAZEFACE_LANDMARKS,
COCO_LANDMARKS,
+ DOC_LAYOUT_LABELS,
+ SLANET_STRUCTURE_VOCAB,
+ alphabets,
+ PPOCR_SYMBOLS,
type ImageNet1KLabel,
type PascalVocLabel,
type CocoClass,
type CocoClassYolo,
type BlazeFaceLandmark,
type CocoLandmark,
+ type DocLayoutLabel,
} from './constants';
const BASE_URL = 'https://huggingface.co/software-mansion/react-native-executorch';
@@ -533,6 +541,196 @@ const YOLO26_XLARGE_SEG_640_XNNPACK_FP32: InstanceSegmenterModel<'xyxy', CocoCla
// =============================================================================
const ALL_MINILM_L6_V2_TOKENIZER = `${BASE_URL}-all-MiniLM-L6-v2/${VERSION_TAG}/tokenizer.json`;
+// =============================================================================
+// OCR
+// =============================================================================
+const EASYOCR_OPTS: OcrOptions = {
+ extractBoxes: craftExtractBoxes,
+ charset: alphabets.english,
+ buckets: { detect: [800, 1280], recognize: [64, 128, 256, 512] },
+};
+
+const PADDLE_PPOCRV6_OPTS: OcrOptions = {
+ extractBoxes: dbnetExtractBoxes,
+ dropScore: 0.5,
+ charset: PPOCR_SYMBOLS,
+ buckets: { detect: [640, 960, 1280], recognize: [160, 320, 480, 640, 1280] },
+};
+const OCR_REVISION = 'resolve/main';
+
+// English
+const EASYOCR_ENGLISH_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/english/EasyOCR_english_xnnpack.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.english },
+};
+const EASYOCR_ENGLISH_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/english/EasyOCR_english_coreml.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.english },
+};
+const EASYOCR_ENGLISH_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/english/EasyOCR_english_vulkan.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.english },
+};
+
+// Cyrillic
+const EASYOCR_CYRILLIC_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/cyrillic/EasyOCR_cyrillic_xnnpack.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.cyrillic },
+};
+const EASYOCR_CYRILLIC_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/cyrillic/EasyOCR_cyrillic_coreml.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.cyrillic },
+};
+const EASYOCR_CYRILLIC_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/cyrillic/EasyOCR_cyrillic_vulkan.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.cyrillic },
+};
+
+// Latin
+const EASYOCR_LATIN_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/latin/EasyOCR_latin_xnnpack.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.latin },
+};
+const EASYOCR_LATIN_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/latin/EasyOCR_latin_coreml.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.latin },
+};
+const EASYOCR_LATIN_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/latin/EasyOCR_latin_vulkan.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.latin },
+};
+
+// Japanese
+const EASYOCR_JAPANESE_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/japanese/EasyOCR_japanese_xnnpack.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.japanese },
+};
+const EASYOCR_JAPANESE_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/japanese/EasyOCR_japanese_coreml.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.japanese },
+};
+const EASYOCR_JAPANESE_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/japanese/EasyOCR_japanese_vulkan.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.japanese },
+};
+
+// Simplified Chinese
+const EASYOCR_ZH_SIM_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/zh_sim/EasyOCR_zh_sim_xnnpack.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.zh_sim },
+};
+const EASYOCR_ZH_SIM_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/zh_sim/EasyOCR_zh_sim_coreml.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.zh_sim },
+};
+const EASYOCR_ZH_SIM_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/zh_sim/EasyOCR_zh_sim_vulkan.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.zh_sim },
+};
+
+// Korean
+const EASYOCR_KOREAN_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/korean/EasyOCR_korean_xnnpack.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.korean },
+};
+const EASYOCR_KOREAN_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/korean/EasyOCR_korean_coreml.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.korean },
+};
+const EASYOCR_KOREAN_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/korean/EasyOCR_korean_vulkan.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.korean },
+};
+
+// Telugu
+const EASYOCR_TELUGU_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/telugu/EasyOCR_telugu_xnnpack.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.telugu },
+};
+const EASYOCR_TELUGU_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/telugu/EasyOCR_telugu_coreml.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.telugu },
+};
+const EASYOCR_TELUGU_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/telugu/EasyOCR_telugu_vulkan.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.telugu },
+};
+
+// Kannada
+const EASYOCR_KANNADA_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/kannada/EasyOCR_kannada_xnnpack.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.kannada },
+};
+const EASYOCR_KANNADA_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/kannada/EasyOCR_kannada_coreml.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.kannada },
+};
+const EASYOCR_KANNADA_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-easy-ocr/${OCR_REVISION}/kannada/EasyOCR_kannada_vulkan.pte`,
+ ocrOpts: { ...EASYOCR_OPTS, charset: alphabets.kannada },
+};
+
+const PADDLE_PPOCRV6_XNNPACK: OcrModel = {
+ modelPath: `${BASE_URL}-pp-ocrv6/${OCR_REVISION}/PP-OCRv6_xnnpack.pte`,
+ ocrOpts: PADDLE_PPOCRV6_OPTS,
+};
+const PADDLE_PPOCRV6_COREML: OcrModel = {
+ modelPath: `${BASE_URL}-pp-ocrv6/${OCR_REVISION}/PP-OCRv6_coreml.pte`,
+ ocrOpts: PADDLE_PPOCRV6_OPTS,
+};
+const PADDLE_PPOCRV6_VULKAN: OcrModel = {
+ modelPath: `${BASE_URL}-pp-ocrv6/${OCR_REVISION}/PP-OCRv6_vulkan.pte`,
+ ocrOpts: PADDLE_PPOCRV6_OPTS,
+};
+
+// =============================================================================
+// Document layout — PP-DocLayoutV3
+// =============================================================================
+const PP_DOCLAYOUT_OPTS = {
+ labels: DOC_LAYOUT_LABELS,
+ boxFormat: 'xyxy' as const,
+ resizeMode: 'stretch' as const,
+ interpolation: 'linear' as const,
+ alpha: 1 / 255.0,
+ beta: 0.0,
+ defaultConfidenceThreshold: 0.3,
+ defaultIouThreshold: 1.0,
+};
+const PP_DOCLAYOUT_XNNPACK: ObjectDetectorModel<'xyxy', DocLayoutLabel> = {
+ modelPath: `${BASE_URL}-pp-doclayout-v3/${OCR_REVISION}/PP-DocLayoutV3_xnnpack.pte`,
+ opts: PP_DOCLAYOUT_OPTS,
+};
+const PP_DOCLAYOUT_COREML: ObjectDetectorModel<'xyxy', DocLayoutLabel> = {
+ modelPath: `${BASE_URL}-pp-doclayout-v3/${OCR_REVISION}/PP-DocLayoutV3_coreml.pte`,
+ opts: PP_DOCLAYOUT_OPTS,
+};
+const PP_DOCLAYOUT_VULKAN: ObjectDetectorModel<'xyxy', DocLayoutLabel> = {
+ modelPath: `${BASE_URL}-pp-doclayout-v3/${OCR_REVISION}/PP-DocLayoutV3_vulkan.pte`,
+ opts: PP_DOCLAYOUT_OPTS,
+};
+
+// =============================================================================
+// Document helper models - PaddleHelpers (orientation / dewarp / table structure)
+// =============================================================================
+const PP_HELPERS_XNNPACK: DocumentModelsConfig = {
+ modelPath: `${BASE_URL}-paddle-helpers/${OCR_REVISION}/PaddleHelpers_xnnpack.pte`,
+ structureVocab: SLANET_STRUCTURE_VOCAB,
+ eosTokenId: 49,
+ maxSteps: 501,
+};
+const PP_HELPERS_COREML: DocumentModelsConfig = {
+ modelPath: `${BASE_URL}-paddle-helpers/${OCR_REVISION}/PaddleHelpers_coreml.pte`,
+ structureVocab: SLANET_STRUCTURE_VOCAB,
+ eosTokenId: 49,
+ maxSteps: 501,
+};
+const PP_HELPERS_VULKAN: DocumentModelsConfig = {
+ modelPath: `${BASE_URL}-paddle-helpers/${OCR_REVISION}/PaddleHelpers_vulkan.pte`,
+ structureVocab: SLANET_STRUCTURE_VOCAB,
+ eosTokenId: 49,
+ maxSteps: 501,
+};
+
/**
* Registry of pre-configured ExecuTorch models.
*
@@ -737,4 +935,69 @@ export const models = {
tokenizer: {
ALL_MINILM_L6_V2: ALL_MINILM_L6_V2_TOKENIZER,
},
+ ocr: {
+ EASYOCR: {
+ ENGLISH: {
+ XNNPACK: EASYOCR_ENGLISH_XNNPACK,
+ COREML: EASYOCR_ENGLISH_COREML,
+ VULKAN: EASYOCR_ENGLISH_VULKAN,
+ },
+ CYRILLIC: {
+ XNNPACK: EASYOCR_CYRILLIC_XNNPACK,
+ COREML: EASYOCR_CYRILLIC_COREML,
+ VULKAN: EASYOCR_CYRILLIC_VULKAN,
+ },
+ LATIN: {
+ XNNPACK: EASYOCR_LATIN_XNNPACK,
+ COREML: EASYOCR_LATIN_COREML,
+ VULKAN: EASYOCR_LATIN_VULKAN,
+ },
+ JAPANESE: {
+ XNNPACK: EASYOCR_JAPANESE_XNNPACK,
+ COREML: EASYOCR_JAPANESE_COREML,
+ VULKAN: EASYOCR_JAPANESE_VULKAN,
+ },
+ ZH_SIM: {
+ XNNPACK: EASYOCR_ZH_SIM_XNNPACK,
+ COREML: EASYOCR_ZH_SIM_COREML,
+ VULKAN: EASYOCR_ZH_SIM_VULKAN,
+ },
+ KOREAN: {
+ XNNPACK: EASYOCR_KOREAN_XNNPACK,
+ COREML: EASYOCR_KOREAN_COREML,
+ VULKAN: EASYOCR_KOREAN_VULKAN,
+ },
+ TELUGU: {
+ XNNPACK: EASYOCR_TELUGU_XNNPACK,
+ COREML: EASYOCR_TELUGU_COREML,
+ VULKAN: EASYOCR_TELUGU_VULKAN,
+ },
+ KANNADA: {
+ XNNPACK: EASYOCR_KANNADA_XNNPACK,
+ COREML: EASYOCR_KANNADA_COREML,
+ VULKAN: EASYOCR_KANNADA_VULKAN,
+ },
+ },
+ PADDLE: {
+ PPOCRV6_SMALL: {
+ XNNPACK: PADDLE_PPOCRV6_XNNPACK,
+ VULKAN: PADDLE_PPOCRV6_VULKAN,
+ COREML: PADDLE_PPOCRV6_COREML,
+ },
+ },
+ },
+ layoutDetection: {
+ PP_DOCLAYOUT: {
+ XNNPACK: PP_DOCLAYOUT_XNNPACK,
+ VULKAN: PP_DOCLAYOUT_VULKAN,
+ COREML: PP_DOCLAYOUT_COREML,
+ },
+ },
+ documentModels: {
+ PP_HELPERS: {
+ XNNPACK: PP_HELPERS_XNNPACK,
+ VULKAN: PP_HELPERS_VULKAN,
+ COREML: PP_HELPERS_COREML,
+ },
+ },
};
| |