Skip to content

Commit d0b6b72

Browse files
hBouananeclaude
andcommitted
chore: modernise packaging, add CI and fix unconstrained rendering
- Relax dependency pins and move pandas/pyarrow/datasets behind a 'data' extra so 'import ocrsmith' stays light; add arabic-reshaper, python-bidi, typer, rich and tqdm which the v1 subsystems build on. - Configure pytest (pythonpath=src, markers, coverage) and ruff; add a CI matrix over Linux/Windows x Python 3.10/3.12. - Fix TypeError in wrap_text_by_pixels when layout.max_width/max_height are unset, and guard whitespace-only input, with regression tests. - Make HuggingFaceTextLoader accept any iterable of mappings, add iter_texts streaming, and defer the heavy 'datasets' import to call time. - Add CONTRIBUTING.md and CHANGELOG.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0c9d395 commit d0b6b72

9 files changed

Lines changed: 384 additions & 60 deletions

File tree

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ci-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
lint:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.12"
21+
cache: pip
22+
- run: pip install ruff
23+
- run: ruff check src tests
24+
- run: ruff format --check src tests
25+
26+
test:
27+
runs-on: ${{ matrix.os }}
28+
strategy:
29+
fail-fast: false
30+
matrix:
31+
os: [ubuntu-latest, windows-latest]
32+
python-version: ["3.10", "3.12"]
33+
steps:
34+
- uses: actions/checkout@v4
35+
- uses: actions/setup-python@v5
36+
with:
37+
python-version: ${{ matrix.python-version }}
38+
cache: pip
39+
- name: Install
40+
run: pip install -e ".[data,dev]"
41+
- name: Test
42+
run: pytest --cov --cov-report=term-missing

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Changelog
2+
3+
All notable changes to this project are documented here. The format follows
4+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to
5+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
9+
### Added
10+
11+
- Development tooling: ruff lint/format configuration, pytest configuration with
12+
`pythonpath = ["src"]` (so a clean clone is testable without installing), coverage
13+
settings, and a GitHub Actions CI matrix over Linux/Windows and Python 3.10/3.12.
14+
- `CONTRIBUTING.md` describing the TDD workflow, branch naming, and release process.
15+
- Regression tests covering rendering without layout constraints.
16+
17+
### Changed
18+
19+
- `pyproject.toml`: dependency pins relaxed to compatible ranges, heavy tabular and
20+
Hugging Face dependencies moved into the `data` extra, project metadata and URLs
21+
completed.
22+
- `HuggingFaceTextLoader` accepts any iterable of mappings, streams records via
23+
`iter_texts`, and defers the `datasets` import to call time.
24+
25+
### Fixed
26+
27+
- `wrap_text_by_pixels` raised `TypeError` when `layout.max_width` / `layout.max_height`
28+
were unset, which made every sample fail for configs that omit them.
29+
- Whitespace-only input no longer produces a zero-sized canvas.

CONTRIBUTING.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Contributing to OCRSmith
2+
3+
Thanks for helping build OCRSmith. This document describes how the project is
4+
developed so that contributions land smoothly.
5+
6+
## Development setup
7+
8+
```bash
9+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
10+
pip install -e ".[data,dev]"
11+
pytest
12+
```
13+
14+
`pytest` works from a clean clone without an install because `pythonpath = ["src"]`
15+
is set in `pyproject.toml`.
16+
17+
## Test-driven workflow
18+
19+
Every behavioural change starts with a test.
20+
21+
1. Write a failing test under `tests/` that expresses the desired behaviour.
22+
2. Run it and watch it fail for the right reason.
23+
3. Write the smallest implementation that makes it pass.
24+
4. Refactor with the suite green.
25+
26+
Rendering code is tested against *properties*, not golden images: bounding boxes
27+
must sit inside the page, text must round-trip through shaping, generated
28+
samples must carry a label. Golden-image tests are brittle across FreeType
29+
versions and are avoided.
30+
31+
## Code style
32+
33+
- `ruff check src tests` and `ruff format src tests` must pass.
34+
- Strategy classes live one-per-file under the subsystem they belong to and are
35+
re-exported from the package `__init__.py`.
36+
- Public functions carry type hints. Domain objects are frozen dataclasses.
37+
- Prefer generators over materialised lists on any path that can see a large
38+
dataset.
39+
40+
## Branch and commit conventions
41+
42+
Branches are named `<type>/<slug>`:
43+
44+
| Type | Use |
45+
| ---------- | ------------------------------------------ |
46+
| `feat/` | new capability |
47+
| `fix/` | bug fix |
48+
| `refactor/`| behaviour-preserving restructure |
49+
| `perf/` | performance work |
50+
| `docs/` | documentation only |
51+
| `chore/` | tooling, CI, dependencies |
52+
53+
Commits follow [Conventional Commits](https://www.conventionalcommits.org/):
54+
`feat(text): add bidi-aware line segmentation`.
55+
56+
## Pull requests
57+
58+
One PR per branch, one concern per PR. The description states what changed, why,
59+
and how it was verified. CI (lint + tests on Linux/Windows, Python 3.10 and 3.12)
60+
must be green before merge.
61+
62+
## Releasing
63+
64+
1. Update `CHANGELOG.md` under a new version heading.
65+
2. Bump `version` in `pyproject.toml`.
66+
3. Tag `vX.Y.Z` on `main` and publish the GitHub release.

pyproject.toml

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,109 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "ocrsmith"
77
version = "0.1.0"
8-
description = "A versatile OCR dataset generator for Arabic and Latin text, with synthetic text generation and dataset export."
8+
description = "Synthetic document and OCR dataset forge for Arabic, Darija and Latin scripts."
99
readme = "README.md"
1010
authors = [
1111
{ name = "Haitam Bouanane", email = "bouananehaitam03@gmail.com" }
1212
]
1313
license = { file = "LICENSE" }
14-
keywords = ["OCR", "synthetic data", "Arabic OCR", "dataset generator"]
15-
requires-python = ">=3.11"
14+
keywords = [
15+
"OCR",
16+
"synthetic data",
17+
"Arabic OCR",
18+
"document AI",
19+
"dataset generator",
20+
"document understanding",
21+
]
22+
classifiers = [
23+
"Development Status :: 4 - Beta",
24+
"Intended Audience :: Science/Research",
25+
"License :: OSI Approved :: MIT License",
26+
"Programming Language :: Python :: 3.10",
27+
"Programming Language :: Python :: 3.11",
28+
"Programming Language :: Python :: 3.12",
29+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
30+
"Topic :: Scientific/Engineering :: Image Processing",
31+
]
32+
requires-python = ">=3.10"
1633
dependencies = [
17-
"pydantic>=2.11.7",
18-
"PyYAML==6.0.2",
19-
"pillow==11.3.0",
20-
"numpy==2.3.1",
21-
"pandas==2.3.1",
22-
"datasets==3.6.0"
34+
"pydantic>=2.7",
35+
"PyYAML>=6.0",
36+
"pillow>=10.0",
37+
"numpy>=1.24",
38+
"arabic-reshaper>=3.0",
39+
"python-bidi>=0.4.2",
40+
"typer>=0.12",
41+
"rich>=13.0",
42+
"tqdm>=4.66",
2343
]
2444

2545
[project.optional-dependencies]
46+
# Tabular / HuggingFace text sources and dataset export targets.
47+
data = [
48+
"pandas>=2.0",
49+
"pyarrow>=14.0",
50+
"datasets>=2.19",
51+
"huggingface-hub>=0.23",
52+
]
2653
dev = [
27-
"pytest==8.4.1",
28-
"pytest-cov==6.2.1"
54+
"pytest>=8.0",
55+
"pytest-cov>=5.0",
56+
"ruff>=0.5",
57+
]
58+
all = [
59+
"ocrsmith[data,dev]",
2960
]
3061

62+
[project.urls]
63+
Homepage = "https://github.com/atlasia-ma/OCRSmith"
64+
Repository = "https://github.com/atlasia-ma/OCRSmith"
65+
Issues = "https://github.com/atlasia-ma/OCRSmith/issues"
66+
3167
[project.scripts]
3268
ocrsmith = "ocrsmith.cli:main"
3369

3470
[tool.setuptools.packages.find]
3571
where = ["src"]
72+
73+
[tool.setuptools.package-data]
74+
ocrsmith = ["config/*.yaml", "config/presets/*.yaml"]
75+
76+
[tool.pytest.ini_options]
77+
minversion = "8.0"
78+
# Makes `pytest` work straight from a clone, without an editable install.
79+
pythonpath = ["src"]
80+
testpaths = ["tests"]
81+
addopts = "-ra --strict-markers"
82+
markers = [
83+
"slow: tests that render many samples or touch the network",
84+
"network: tests that require internet access",
85+
]
86+
filterwarnings = ["ignore::DeprecationWarning"]
87+
88+
[tool.coverage.run]
89+
branch = true
90+
source = ["src/ocrsmith"]
91+
92+
[tool.coverage.report]
93+
exclude_lines = [
94+
"pragma: no cover",
95+
"raise NotImplementedError",
96+
"if TYPE_CHECKING:",
97+
]
98+
99+
[tool.ruff]
100+
line-length = 110
101+
target-version = "py310"
102+
src = ["src", "tests"]
103+
104+
[tool.ruff.lint]
105+
select = ["E", "F", "I", "UP", "B", "C4", "SIM"]
106+
ignore = [
107+
"E501", # line length is handled by the formatter
108+
"B008", # typer uses callables in argument defaults by design
109+
"SIM108", # ternaries are not always clearer
110+
]
111+
112+
[tool.ruff.lint.per-file-ignores]
113+
"tests/*" = ["B011"]

src/ocrsmith/core/text_renderers/strategies/HorizontalRenderingStrategy.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,36 @@ def wrap_text_by_pixels(paragraphs, font, max_width, max_height, spacing, line_h
1414
if not line_height:
1515
line_height = FontManager.get_text_height(font)
1616
text_height = 0
17+
# Provide safe defaults if constraints are not provided
18+
eff_max_width = max_width if isinstance(max_width, int) and max_width > 0 else 10_000
19+
eff_max_height = max_height if isinstance(max_height, int) and max_height > 0 else 10_000
1720

1821
for paragraph in paragraphs:
1922
words = paragraph.split()
2023
current_line = ''
2124
for word in words:
2225
test_line = current_line + (" " if current_line else "") + word
2326
w = FontManager.get_text_width(font, test_line)
24-
if w <= max_width:
27+
if w <= eff_max_width:
2528
current_line = test_line
2629
else:
2730
# Add current line if it fits in height
2831
if current_line: # Don't add empty lines
29-
if text_height + line_height + spacing <= max_height:
32+
if text_height + line_height + spacing <= eff_max_height:
3033
lines.append(current_line)
3134
text_height += line_height + spacing
3235
else:
3336
break # Stop if adding this line would exceed max_height
3437
current_line = word
3538

3639
# Check if single word exceeds max_width
37-
if FontManager.get_text_width(font, word) > max_width:
40+
if FontManager.get_text_width(font, word) > eff_max_width:
3841
# Handle very long words - you might want to break them or skip them
3942
current_line = word # Keep it anyway, but it will overflow
4043

4144
# Add the last line of the paragraph
4245
if current_line:
43-
if text_height + line_height <= max_height:
46+
if text_height + line_height <= eff_max_height:
4447
lines.append(current_line)
4548
text_height += line_height + spacing
4649
else:
@@ -75,6 +78,9 @@ def render_text(self, font: FreeTypeFont, text: str, spacing: int = 1,
7578

7679
line_height = FontManager.get_text_height(font)
7780
lines = wrap_text_by_pixels(paragraphs, font, max_width, max_height, spacing, line_height)
81+
# Guard against empty content (e.g., whitespace-only input)
82+
if not lines:
83+
lines = [" "]
7884

7985
line_heights = [line_height]*len(lines)
8086
text_height = sum(line_heights) + (len(lines) - 1) * spacing
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
from .HorizontalRenderingStrategy import HorizontalRenderingStrategy
2+
from .SimpleTextRenderingStrategy import SimpleTextRenderingStrategy
3+
from .VerticalRenderingStrategy import VerticalRenderingStrategy
4+
5+
__all__ = [
6+
"HorizontalRenderingStrategy",
7+
"SimpleTextRenderingStrategy",
8+
"VerticalRenderingStrategy",
9+
]

0 commit comments

Comments
 (0)