harfix 0.1.0__tar.gz
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.
- harfix-0.1.0/.github/workflows/ci.yml +22 -0
- harfix-0.1.0/.github/workflows/publish.yml +21 -0
- harfix-0.1.0/.gitignore +10 -0
- harfix-0.1.0/CHANGELOG.md +20 -0
- harfix-0.1.0/LICENSE +202 -0
- harfix-0.1.0/NOTICE +13 -0
- harfix-0.1.0/PKG-INFO +212 -0
- harfix-0.1.0/README.md +191 -0
- harfix-0.1.0/pyproject.toml +48 -0
- harfix-0.1.0/reports/calibration.md +493 -0
- harfix-0.1.0/scripts/build_model.py +116 -0
- harfix-0.1.0/scripts/calibrate.py +534 -0
- harfix-0.1.0/scripts/fetch_shamela.py +94 -0
- harfix-0.1.0/src/harfix/__init__.py +30 -0
- harfix-0.1.0/src/harfix/cli.py +137 -0
- harfix-0.1.0/src/harfix/data/arabic_char_lm.json.gz +0 -0
- harfix-0.1.0/src/harfix/data/arabic_char_lm_unspaced.json.gz +0 -0
- harfix-0.1.0/src/harfix/data/thresholds.json +33 -0
- harfix-0.1.0/src/harfix/detect.py +365 -0
- harfix-0.1.0/src/harfix/model.py +126 -0
- harfix-0.1.0/src/harfix/normalize.py +282 -0
- harfix-0.1.0/src/harfix/pdf.py +181 -0
- harfix-0.1.0/tests/conftest.py +55 -0
- harfix-0.1.0/tests/pdf_fixture.py +103 -0
- harfix-0.1.0/tests/test_cli.py +30 -0
- harfix-0.1.0/tests/test_detect.py +148 -0
- harfix-0.1.0/tests/test_model.py +36 -0
- harfix-0.1.0/tests/test_normalize.py +42 -0
- harfix-0.1.0/tests/test_pdf.py +79 -0
- harfix-0.1.0/uv.lock +631 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --locked
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run ruff format --check .
|
|
22
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # PyPI trusted publishing; no API token stored in the repository
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v6
|
|
16
|
+
- run: uv sync --locked
|
|
17
|
+
- run: uv run pytest -q
|
|
18
|
+
- name: Check the tag matches the package version
|
|
19
|
+
run: test "${GITHUB_REF_NAME#v}" = "$(uv run python -c 'import harfix; print(harfix.__version__)')"
|
|
20
|
+
- run: uv build
|
|
21
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
harfix-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `harfix scan` audits a PDF's text layer per page and per font and routes each page to
|
|
8
|
+
`keep`, `repair`, `reextract` or `ocr`.
|
|
9
|
+
- `harfix check` and `assess_document` judge extractor output page by page (form feeds) or
|
|
10
|
+
in runs of lines, so a few bad pages are not averaged away by clean ones.
|
|
11
|
+
- Detects wrong glyph-to-letter maps, unmapped glyphs, presentation forms, mirrored and
|
|
12
|
+
mixed reading order, lam-alef transposition and lost word boundaries.
|
|
13
|
+
- Detects pre-Unicode Arabic fonts whose text layer extracts as ASCII gibberish (`AXt…`,
|
|
14
|
+
`Adv…` desktop-publishing fonts), not only those landing on Latin-1 symbols.
|
|
15
|
+
- Detects Quran glyph-ID fonts (`QCF_P…`) as `quran_glyph_codes` and never sends them to
|
|
16
|
+
`repair`, where NFKC would turn them into wrong words.
|
|
17
|
+
- Fonts too short to judge on any single page are judged on their short runs pooled across
|
|
18
|
+
the document (`"scope": "document"`).
|
|
19
|
+
- Fonts are identified by their full PDF name including the subset tag, so two subsets of
|
|
20
|
+
one typeface with different glyph maps are reported separately.
|
harfix-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
harfix-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
harfix
|
|
2
|
+
Copyright 2026 Ahmad Sebaq
|
|
3
|
+
|
|
4
|
+
This product includes a character n-gram model (src/harfix/data/arabic_char_lm.json.gz)
|
|
5
|
+
whose letter statistics were derived from the following datasets, both licensed under the
|
|
6
|
+
Apache License, Version 2.0:
|
|
7
|
+
|
|
8
|
+
- Misraj/Misraj-DocOCR, revision 7177bf70f77ce259890d6af0bef53f18faf26ec9
|
|
9
|
+
https://huggingface.co/datasets/Misraj/Misraj-DocOCR
|
|
10
|
+
- Misraj/KITAB_pdf_to_markdown_reviewed, revision 8890d721c660cd027839bc8bc48634552fe7aec6
|
|
11
|
+
https://huggingface.co/datasets/Misraj/KITAB_pdf_to_markdown_reviewed
|
|
12
|
+
|
|
13
|
+
No text from these datasets is distributed with harfix; only aggregate character counts.
|
harfix-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: harfix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Quality gate for Arabic PDF text layers: detect garbled, reversed, or unmapped Arabic before it reaches your index.
|
|
5
|
+
Project-URL: Homepage, https://github.com/asebaq/harfix
|
|
6
|
+
Project-URL: Issues, https://github.com/asebaq/harfix/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/asebaq/harfix/blob/main/CHANGELOG.md
|
|
8
|
+
Author-email: Ahmad Sebaq <ahmad.sebaq@gmail.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE
|
|
12
|
+
Keywords: arabic,data-quality,ocr,pdf,rag,text-extraction
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Natural Language :: Arabic
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: pdfminer-six>=20240706
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# harfix
|
|
23
|
+
|
|
24
|
+
**A quality gate for Arabic text extracted from PDFs.** harfix tells you, per page and per
|
|
25
|
+
font, whether the text you are about to index is trustworthy, needs a cheap repair, needs
|
|
26
|
+
re-extraction with a different tool, or must go to OCR.
|
|
27
|
+
|
|
28
|
+
Arabic PDFs fail silently. A page can render perfectly and still extract as the wrong
|
|
29
|
+
letters (`المدعى عليه` → `الددعى عليهي`), as presentation-form code points that match no
|
|
30
|
+
search query, as mirrored lines, or as pre-Unicode symbols (`Âd‡;« ≤∞`). Generic
|
|
31
|
+
"broken text" detectors check for control characters and Latin mojibake, so they pass
|
|
32
|
+
this text straight into your RAG index. harfix scores the letters themselves with a small
|
|
33
|
+
Arabic character model and reports what is wrong and what to do about it.
|
|
34
|
+
|
|
35
|
+
harfix never modifies text. It only decides.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install harfix # or: uv add harfix
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Python 3.10+. The only runtime dependency is `pdfminer.six`; the two character models
|
|
44
|
+
(~300 KiB) ship with the package.
|
|
45
|
+
|
|
46
|
+
## Use
|
|
47
|
+
|
|
48
|
+
Audit the text layer of PDFs:
|
|
49
|
+
|
|
50
|
+
```console
|
|
51
|
+
$ harfix scan --pages 8-12 --fail-on ocr mukhtasar_khuqayr.pdf
|
|
52
|
+
mukhtasar_khuqayr.pdf
|
|
53
|
+
pages: keep=1, ocr=4
|
|
54
|
+
p9 ocr legacy_encoding
|
|
55
|
+
p10 ocr legacy_encoding
|
|
56
|
+
p11 ocr legacy_encoding
|
|
57
|
+
p12 ocr legacy_encoding
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That book was typeset in pre-Unicode Arabic fonts (`AXt…`, `Adv…`), so its text layer
|
|
61
|
+
extracts as Latin gibberish. `-v` lists every font on every page; `--json` prints the full
|
|
62
|
+
report; `--fail-on repair|reextract|ocr` turns the scan into a CI or pipeline gate (exit
|
|
63
|
+
code 1).
|
|
64
|
+
|
|
65
|
+
Check text produced by any extractor (Docling, pdftotext, Marker, an OCR engine). Pages
|
|
66
|
+
separated by form feeds (pdftotext's default) are judged one by one; other text in runs of
|
|
67
|
+
whole lines holding about 500 Arabic letters, so a few bad pages are not averaged away:
|
|
68
|
+
|
|
69
|
+
```console
|
|
70
|
+
$ pdftotext -f 1 -l 12 al_mughalatat.pdf - | harfix check
|
|
71
|
+
reextract: lam_alef_transposition, presentation_forms
|
|
72
|
+
pages: keep=2, repair=3, reextract=2
|
|
73
|
+
p4 repair presentation_forms
|
|
74
|
+
p5 repair presentation_forms
|
|
75
|
+
p6 reextract presentation_forms, lam_alef_transposition
|
|
76
|
+
p9 repair presentation_forms
|
|
77
|
+
p11 reextract presentation_forms, lam_alef_transposition
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Page numbers count from the start of the input; blank pages are skipped.
|
|
81
|
+
|
|
82
|
+
From Python:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from harfix import assess_document, assess_text, scan_pdf
|
|
86
|
+
|
|
87
|
+
report = scan_pdf("book.pdf", pages=range(1, 11))
|
|
88
|
+
for page in report.pages:
|
|
89
|
+
if page.route != "keep":
|
|
90
|
+
print(page.page, page.route, page.reasons)
|
|
91
|
+
|
|
92
|
+
document = assess_document(extracted_text) # extractor output, page by page
|
|
93
|
+
document.route, [(s.page, s.assessment.route) for s in document.segments]
|
|
94
|
+
|
|
95
|
+
verdict = assess_text(snippet) # one piece of text: checks reading order too
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Routes and reasons
|
|
99
|
+
|
|
100
|
+
| Route | Meaning | Reasons |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `keep` | Index as is. | — |
|
|
103
|
+
| `repair` | Text is right but encoded or ordered badly; a deterministic fix exists. | `presentation_forms` (apply NFKC), `visual_order` (reverse each line) |
|
|
104
|
+
| `reextract` | The extractor damaged correct source text; try another extractor. | `mixed_reading_order`, `lam_alef_transposition`, `lost_word_boundaries` |
|
|
105
|
+
| `ocr` | The text layer itself is wrong; read the pixels. | `same_script_substitution`, `unmapped_glyphs`, `legacy_encoding`, `quran_glyph_codes`, `no_text_layer` |
|
|
106
|
+
|
|
107
|
+
`quran_glyph_codes`: Quran text set in the King Fahd Complex page fonts (`QCF_P…`) stores
|
|
108
|
+
one glyph ID per word under Presentation Forms-A code points. NFKC would turn those IDs
|
|
109
|
+
into wrong words, so they are never sent to `repair`; take the verse from a canonical Quran
|
|
110
|
+
text or OCR it. The rest of the same text is still judged on its own letters.
|
|
111
|
+
|
|
112
|
+
A page takes the most severe route among its font runs, so one broken heading font next to
|
|
113
|
+
a healthy body font is reported precisely instead of being averaged away. A font whose runs
|
|
114
|
+
are too short to judge on any one page (running heads, headings, verse numbers) is judged
|
|
115
|
+
on its short runs pooled across the scanned pages; such font reports carry
|
|
116
|
+
`"scope": "document"`. Fonts are identified by their PDF name including the subset tag
|
|
117
|
+
(`ABCDEE+Lotus Linotype`), because two subsets of one typeface can map glyphs differently.
|
|
118
|
+
|
|
119
|
+
`scan` (raw text layer) ignores reading order, which depends on the extractor. `check`
|
|
120
|
+
(final extractor output) also reports order and spacing faults.
|
|
121
|
+
|
|
122
|
+
## How it works
|
|
123
|
+
|
|
124
|
+
1. Text is reduced to base Arabic letters (presentation forms decomposed, diacritics,
|
|
125
|
+
tatweel and decorative letter runs removed).
|
|
126
|
+
2. Each line's reading direction is decided by comparing the model's score forwards and
|
|
127
|
+
backwards, so mirrored text is not mistaken for corruption.
|
|
128
|
+
3. A 4-gram character model with interpolated Witten-Bell smoothing scores the corrected
|
|
129
|
+
letters. Wrong glyph-to-letter maps produce improbable letter sequences (high bits per
|
|
130
|
+
character) or isolated very improbable letters (spikes).
|
|
131
|
+
4. A second model trained without spaces separates "the letters are wrong" from "the
|
|
132
|
+
extractor broke the word spacing".
|
|
133
|
+
5. Character-class checks catch private-use glyphs, raw `(cid:N)` codes, presentation
|
|
134
|
+
forms, Quran glyph-ID fonts, and pre-Unicode encodings without any model. Pre-Unicode
|
|
135
|
+
fonts land either on Latin-1 and symbol characters or on ASCII, where they read as
|
|
136
|
+
Latin "words" with almost no vowels or with digits, symbols and case flips inside
|
|
137
|
+
(`otsrqponmlkA{zvjihgf`, `h&c%G`); English text has about 40% vowels and few such words.
|
|
138
|
+
|
|
139
|
+
## Accuracy
|
|
140
|
+
|
|
141
|
+
Full numbers: [`reports/calibration.md`](reports/calibration.md). Thresholds were fitted on
|
|
142
|
+
one held-out split of 462 public Arabic pages and measured on another, then raised
|
|
143
|
+
(`DOMAIN_FLOORS`) after inspecting clean runs in Egyptian court and statute PDFs.
|
|
144
|
+
|
|
145
|
+
On the test split, windows of at least 80 Arabic letters:
|
|
146
|
+
|
|
147
|
+
| Case | Flagged correctly |
|
|
148
|
+
|---|---:|
|
|
149
|
+
| Clean text left as `keep` (PDF scan / extractor output) | 100% / 99.5% |
|
|
150
|
+
| Whole font with a wrong letter map | 100% |
|
|
151
|
+
| Private-use glyphs, raw CID codes, Latin-1 legacy encodings, presentation forms | 100% |
|
|
152
|
+
| Quran glyph-ID fonts | 100% |
|
|
153
|
+
| ASCII legacy encodings | 96% (100% at ≥500 letters) |
|
|
154
|
+
| Mirrored lines detected (`check`) | 100% |
|
|
155
|
+
| Lam-alef transposition (`check`) | 90% (97% at ≥200 letters) |
|
|
156
|
+
| Lost word boundaries (`check`) | 99.8% |
|
|
157
|
+
| Padding letters inserted (seen in Egyptian court books) | 72% (87% at ≥500 letters) |
|
|
158
|
+
| Only 6 of 36 letters swapped | 40% (52% at ≥500 letters) |
|
|
159
|
+
|
|
160
|
+
Real books: `scripts/fetch_shamela.py` samples printed-edition PDFs listed in
|
|
161
|
+
al-Maktaba al-Shamela (two per subject category, seed 0); none was used for any threshold.
|
|
162
|
+
Of 46 distinct books (first 40 pages each), 33 are image-only scans (`no_text_layer`). Of
|
|
163
|
+
the 13 with a text layer, 12 have fonts flagged on at least some pages: pre-Unicode `AXt`,
|
|
164
|
+
`Adv…` and `MCS` fonts (`legacy_encoding`), fonts with no Unicode map at all
|
|
165
|
+
(`unmapped_glyphs`), a `QCF_P206` verse font (`quran_glyph_codes`), Lotus and Uthman Taha
|
|
166
|
+
fonts whose text is fragmented with stray dammas (`same_script_substitution`), Yakout text
|
|
167
|
+
stored as presentation forms (`repair`), and OCR text layers with garbled title pages.
|
|
168
|
+
Samples of flagged fonts in 8 of those 12 books, covering every kind above, were inspected
|
|
169
|
+
by hand: all were genuinely broken, but one was misattributed (a Lotus layer with correct
|
|
170
|
+
letters and no word spaces is reported as `same_script_substitution`, see Limitations).
|
|
171
|
+
The inspection also caught stray Lotus ligature glyphs misread as a Quran font, which led
|
|
172
|
+
to the consecutive-code-point condition. This is a spot check, not a labeled evaluation.
|
|
173
|
+
|
|
174
|
+
## Limitations
|
|
175
|
+
|
|
176
|
+
- The model is trained on about 300 pages of modern standard Arabic. Classical, dialectal,
|
|
177
|
+
or heavily domain-specific text scores higher; calibrate before relying on it there.
|
|
178
|
+
- Precision is preferred over recall: partial letter swaps and light padding are missed
|
|
179
|
+
part of the time.
|
|
180
|
+
- `scan` cannot tell a text layer without word spaces from one with wrong letters: both
|
|
181
|
+
are reported as `same_script_substitution`.
|
|
182
|
+
- A pre-Unicode font is caught only once its runs hold about 15 Latin-looking words, and a
|
|
183
|
+
font mapped entirely onto digits and punctuation is not caught at all.
|
|
184
|
+
- `check` splits text without form feeds at line ends, so a single very long line is
|
|
185
|
+
judged as one piece.
|
|
186
|
+
- Scanned pages are routed to OCR but their image quality is not assessed.
|
|
187
|
+
- PDF parsing uses pdfminer.six: about 0.1–0.2 s per page on the documents above.
|
|
188
|
+
|
|
189
|
+
## Roadmap
|
|
190
|
+
|
|
191
|
+
- Text-level repairs for the `repair` route (NFKC, line reversal) with provenance.
|
|
192
|
+
- Per-font decoder for legacy and custom Arabic fonts: learn each font's glyph-to-letter
|
|
193
|
+
map once (from a typed edition of the same book, a library of known fonts, or the letter
|
|
194
|
+
model itself, with OCR only as a fallback), then decode the whole document.
|
|
195
|
+
- Docling, LangChain and LlamaIndex integrations.
|
|
196
|
+
- A public benchmark of broken Arabic PDFs with labels per failure class.
|
|
197
|
+
|
|
198
|
+
## Rebuilding the model and thresholds
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
uv sync --group model
|
|
202
|
+
uv run python scripts/build_model.py # pinned Hugging Face revisions, reproducible output
|
|
203
|
+
uv run python scripts/fetch_shamela.py # needs the Shamela desktop app's catalogue
|
|
204
|
+
uv run python scripts/calibrate.py --pages 1-40 --pdf corpus/shamela/*.pdf
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The Shamela PDFs are third-party works: keep them local, never commit or redistribute them.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
Apache-2.0. The packaged character statistics are derived from Apache-2.0 datasets by
|
|
212
|
+
Misraj; see [`NOTICE`](NOTICE).
|