ocr-foundry 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
khocr_gen/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """OCR Foundry - synthetic OCR training data generator."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("ocr-foundry")
7
+ except PackageNotFoundError:
8
+ try:
9
+ __version__ = version("khocr-gen")
10
+ except PackageNotFoundError:
11
+ __version__ = "0.2.0"
@@ -0,0 +1,25 @@
1
+ """PIL compatibility shims for resampling across Pillow versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from PIL import Image
8
+
9
+
10
+ def _resolve_resample(name: str) -> Any:
11
+ resampling = getattr(Image, "Resampling", None)
12
+ if resampling is not None:
13
+ value = getattr(resampling, name, None)
14
+ if value is not None:
15
+ return value
16
+
17
+ legacy_value = getattr(Image, name, None)
18
+ if legacy_value is not None:
19
+ return legacy_value
20
+
21
+ raise AttributeError(f"PIL.Image does not define resample mode '{name}'")
22
+
23
+
24
+ RESAMPLE_BILINEAR: Any = _resolve_resample("BILINEAR")
25
+ RESAMPLE_BICUBIC: Any = _resolve_resample("BICUBIC")
@@ -0,0 +1,204 @@
1
+ """Rust acceleration backend for khocr-gen.
2
+
3
+ This module wraps the native `_rust_accel` extension (built via maturin/PyO3).
4
+ If the native extension is unavailable, Python augmentation and text splitting fallbacks are used.
5
+
6
+ Usage::
7
+
8
+ from khocr_gen._rust_accel import (
9
+ apply_sauvola, apply_blur,
10
+ split_text_spans,
11
+ estimate_bg,
12
+ HAS_RUST_ACCEL,
13
+ )
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ import numpy as np
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Callable
25
+
26
+ _LOGGER = logging.getLogger("khocr_gen._rust_accel")
27
+
28
+ # Try to load the native Rust extension
29
+
30
+ _native: Any = None
31
+
32
+ try:
33
+ import _rust_accel as _native
34
+
35
+ HAS_RUST_ACCEL = True
36
+ except ImportError:
37
+ HAS_RUST_ACCEL = False
38
+ _LOGGER.info("Rust extension not available, using pure-Python fallbacks.")
39
+
40
+
41
+ def _log_available() -> None:
42
+ if HAS_RUST_ACCEL:
43
+ try:
44
+ ver: str = getattr(_native, "__version__", "unknown")
45
+ except Exception:
46
+ ver = "unknown"
47
+ _LOGGER.info("Rust acceleration enabled (native _rust_accel v%s)", ver)
48
+ else:
49
+ _LOGGER.info("Rust acceleration NOT available.")
50
+
51
+
52
+ # Augmentation functions
53
+
54
+ # Each entry: (function_name, has_rgb_variant)
55
+ _AUG_FUNCTIONS: list[tuple[str, bool]] = [
56
+ ("apply_sauvola", False),
57
+ ("apply_geo_warp", False),
58
+ ("apply_vertical_crop", False),
59
+ ("apply_blur", False),
60
+ ("apply_salt_pepper", False),
61
+ ("apply_background_texture", False),
62
+ ("apply_jpeg_compression", False),
63
+ ("apply_rotation", True),
64
+ ("apply_lowdpi", False),
65
+ ("apply_oversample", False),
66
+ ("apply_perspective", False),
67
+ ("apply_elastic", False),
68
+ ("apply_random_crop", False),
69
+ ("apply_online_blur", False),
70
+ ("apply_online_noise", False),
71
+ ("apply_reverse", True),
72
+ ("apply_brightness_contrast", True),
73
+ ("apply_pixelation", False),
74
+ ("apply_gradient_illumination", False),
75
+ ("apply_morphological", False),
76
+ ("apply_anisotropic_dilation", False),
77
+ ("apply_hsv", True), # RGB-only in Rust
78
+ ]
79
+
80
+
81
+ def _get_rust_aug_fn(name: str) -> Callable[..., Any] | None:
82
+ """Return the native Rust augmentation function by name, or None."""
83
+ if not HAS_RUST_ACCEL:
84
+ return None
85
+ assert _native is not None
86
+ return getattr(_native, f"{name}_rust", None)
87
+
88
+
89
+ def _make_aug_fallback(name: str) -> Callable[..., Any]:
90
+ """Create a fallback that delegates to the Python augmentation module."""
91
+
92
+ def _fallback(img: np.ndarray, intensity: float, **kwargs: Any) -> np.ndarray:
93
+ from khocr_gen.augmentation import AUG_METHODS
94
+
95
+ fn = AUG_METHODS.get(name)
96
+ if fn is None:
97
+ return img
98
+ return fn(img, intensity, **kwargs)
99
+
100
+ _fallback.__name__ = name
101
+ _fallback.__qualname__ = name
102
+ return _fallback
103
+
104
+
105
+ def _export_aug_functions() -> None:
106
+ """Populate the module globals with augmentation functions.
107
+
108
+ For each augmentation method, the Rust variant is preferred when available;
109
+ otherwise the pure-Python fallback is used.
110
+
111
+ Grayscale-only methods dispatch directly.
112
+ Methods with an RGB variant (`brightness_contrast`, `hsv`) are wrapped to handle both grayscale and RGB input arrays.
113
+ """
114
+ g = globals()
115
+
116
+ for name, has_rgb in _AUG_FUNCTIONS:
117
+ rust_fn = _get_rust_aug_fn(name)
118
+ rust_rgb_fn: Callable[..., Any] | None = (
119
+ _get_rust_aug_fn(f"{name}_rgb") if has_rgb else None
120
+ )
121
+
122
+ if has_rgb and HAS_RUST_ACCEL and (rust_fn is not None or rust_rgb_fn is not None):
123
+ # Wrap to auto-detect channel count and dispatch.
124
+ # Rust RGB functions return flat (h, w*3) 2D arrays; reshape to (h, w, 3).
125
+ _gray_fn = rust_fn
126
+ _rgb_fn = rust_rgb_fn
127
+
128
+ # Capture the fallback factory by value so we can safely delete it later
129
+ _fallback_factory = _make_aug_fallback
130
+
131
+ def _make_rgb_wrapper(
132
+ gray_fn: Callable[..., Any] | None,
133
+ rgb_fn: Callable[..., Any] | None,
134
+ fallback_name: str,
135
+ fallback_factory: Callable[..., Any],
136
+ ) -> Callable[..., Any]:
137
+ def _wrapper(img: np.ndarray, intensity: float, **kwargs: Any) -> np.ndarray:
138
+ if img.ndim == 3 and img.shape[2] == 3:
139
+ if rgb_fn is not None:
140
+ h, w = img.shape[:2]
141
+ result = rgb_fn(img, intensity)
142
+ # Rust RGB fns return flat (h, w*3); reshape to (h, w, 3)
143
+ if result.ndim == 2 and result.shape[1] == w * 3:
144
+ return result.reshape(h, w, 3)
145
+ return result
146
+ if gray_fn is not None:
147
+ channels = [
148
+ gray_fn(np.ascontiguousarray(img[:, :, c]), intensity)
149
+ for c in range(3)
150
+ ]
151
+ return np.stack(channels, axis=-1)
152
+ return fallback_factory(fallback_name)(img, intensity)
153
+ if gray_fn is not None:
154
+ return gray_fn(img, intensity)
155
+ return fallback_factory(fallback_name)(img, intensity)
156
+
157
+ return _wrapper
158
+
159
+ g[name] = _make_rgb_wrapper(_gray_fn, _rgb_fn, name, _fallback_factory)
160
+ elif rust_fn is not None:
161
+ g[name] = rust_fn
162
+ else:
163
+ g[name] = _make_aug_fallback(name)
164
+
165
+
166
+ _export_aug_functions()
167
+
168
+ # Clean up the helpers so they don't pollute the module namespace.
169
+ # _make_aug_fallback and _AUG_FUNCTIONS are retained for closures referencing them.
170
+ del _export_aug_functions
171
+
172
+
173
+ # Text span splitting
174
+
175
+ if HAS_RUST_ACCEL:
176
+ assert _native is not None
177
+ split_text_spans = _native.split_text_spans # type: ignore[assignment]
178
+ else:
179
+
180
+ def split_text_spans(text: str) -> list[tuple[str, str]]: # type: ignore[no-redef]
181
+ """Split text into contiguous script spans (pure-Python fallback)."""
182
+ from khocr_gen.rendering import ImageRenderer
183
+
184
+ return ImageRenderer._split_text_into_spans(text)
185
+
186
+
187
+ # Utilities
188
+
189
+ if HAS_RUST_ACCEL:
190
+ assert _native is not None
191
+ estimate_bg = _native.estimate_bg_rust # type: ignore[assignment]
192
+ else:
193
+
194
+ def estimate_bg(img: np.ndarray) -> int: # type: ignore[no-redef]
195
+ """Estimate background colour from image border (pure-Python fallback)."""
196
+ from khocr_gen.augmentation import _estimate_bg
197
+
198
+ return int(_estimate_bg(img))
199
+
200
+
201
+ # Module-level init
202
+
203
+ _log_available()
204
+ del _log_available