Metadata-Version: 2.4
Name: arafix
Version: 0.8.0
Summary: Recover broken Arabic text from PDFs — diagnose first, then graded repair (bidi, presentation forms, layout)
Project-URL: Homepage, https://github.com/bio-colab/arafix
Project-URL: Documentation, https://github.com/bio-colab/arafix#readme
Project-URL: Issues, https://github.com/bio-colab/arafix/issues
Project-URL: Changelog, https://github.com/bio-colab/arafix/blob/main/CHANGELOG.md
Project-URL: Release notes, https://github.com/bio-colab/arafix/releases
Author: bio-colab
License-Expression: MIT
License-File: LICENSE
Keywords: arabic,bidi,lam-alef,markitdown,nlp,ocr-alternative,pdf,presentation-forms,rtl,text-extraction,unicode
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: Arabic
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Linguistic
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: all
Requires-Dist: fonttools>=4.40; extra == 'all'
Requires-Dist: pymupdf>=1.23; extra == 'all'
Provides-Extra: cmap
Requires-Dist: fonttools>=4.40; extra == 'cmap'
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: fonttools>=4.40; extra == 'dev'
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pymupdf>=1.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Provides-Extra: markitdown
Requires-Dist: markitdown>=0.1.0a1; extra == 'markitdown'
Requires-Dist: pymupdf>=1.23; extra == 'markitdown'
Provides-Extra: pdf
Requires-Dist: pymupdf>=1.23; extra == 'pdf'
Description-Content-Type: text/markdown

# arafix

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)
![Status: Alpha](https://img.shields.io/badge/status-alpha-orange)
![Typing](https://img.shields.io/badge/typing-py.typed-blue)

**Recover broken Arabic text from PDFs** — diagnose first, then apply a graded repair ladder. Not a single hammer, and not “just run OCR.”

| | |
|---|---|
| **Core** | Zero dependencies (stdlib only) for text stages 0–2 |
| **PDF** | `pip install "arafix[pdf]"` — geometric extract + Arabic repair |
| **Layout** | Multi-column RTL, headers/footers, simple tables (`layout=auto`) |
| **Status** | **Alpha 0.8** — production-curious, still evolving |

### Install

```bash
pip install arafix              # text repair only
pip install "arafix[pdf]"       # recommended — PDF extract
pip install "arafix[all]"       # + fonttools (CMap / stage 3)
```

### 30-second start

```python
from arafix import repair_text, extract_pdf

# Presentation-form garbage → readable Arabic
print(repair_text("\ufee3\ufeae\ufea3\ufe92\ufe8e").text)  # مرحبا

# Native (not scanned) Arabic PDF
doc = extract_pdf("thesis.pdf")
print(doc.text)
print(doc.confidence, doc.pages[0].n_columns)
```

```bash
arafix diagnose thesis.pdf -v
arafix extract  thesis.pdf -o out.txt
arafix extract  paper.pdf --layout full -v --tables
```

### What it fixes (and what it doesn’t)

| Symptom | Cause | Stage |
|---|---|---|
| Reversed letter order | Visual storage order | 2 |
| Isolated Arabic glyphs (`ﻣﺮﺣﺒﺎ`) | Presentation forms | 1 |
| `Ø§Ù„…` mojibake | UTF-8 read as Latin-1 | 0 |
| `المجالت` / `االنترنيت` | Lam-alef ligature broken before reorder | 1a→2→1b |
| `()مقدمة` | Engine bidi vs neutrals | geometric read |
| Two columns mixed | Line-joined gutters | layout (0.8) |
| Empty / PUA soup | Broken ToUnicode / scan | 3 / 4 (OCR not shipped) |

**Philosophy:** never invent characters; never “fix just in case”; every decision carries evidence and confidence.

Further reading: [INTEGRATING.md](INTEGRATING.md) · [DEPLOY.md](DEPLOY.md) · [CHANGELOG.md](CHANGELOG.md) · [RELEASING.md](RELEASING.md)

> **Name note:** other GitHub projects may also expose an `arafix` import. See the Arabic naming section and [RELEASING.md](RELEASING.md). Publish promptly after configuring the PyPI publisher — a pending publisher does **not** reserve the name.

---

<div dir="rtl">

# arafix — التوثيق العربي

**استرجاع النص العربي من ملفات PDF المعطوبة.**
سلّمٌ من خمس درجات، لا مطرقةٌ واحدة.

> ⚠️ **تنبيه تسمية — والقرار عاجل.** الاسم `arafix` **مأخوذ فعلياً** على GitHub بأربعة
> مستودعات، وأحدها ([AraFix-V3.0](https://github.com/Basma2423/AraFix-V3.0))
> يوفّر حزمة بايثون عليا اسمها `arafix` بالحرف. تثبيتهما معاً يكسر
> أحدهما — ومن يستورد `arafix` لا يدري أيّهما جاءه. الاسم على PyPI شاغرٌ
> بعدُ — لكنّ **الناشر المعلَّق على PyPI لا يحجز الاسم**، وإعادة التسمية
> قبل النشر أسلم منها بعده بمراتب. انظر [التسمية والجيران](#التسمية-والجيران)
> و[RELEASING.md](RELEASING.md).

---

## المسألة

عندك ملف PDF عربي أصليّ — لا صورة ممسوحة، بل نصٌّ حقيقيّ مُصدَّر من Word.
تفتحه فتقرؤه بلا عناء. تستخرجه ببايثون فيخرج:

| ما ترى | العلّة | الدرجة العلاجية |
|---|---|---|
| `ا ب ح ر م` | ترتيب بصريّ مخزَّن معكوساً | ٢ |
| `م ر ح ب ا` متفرقة | أشكال رسومية مطبوخة (U+FB50–FEFF) | ١ |
| `Ø§Ù„Ù…ØªÙˆØ³Ø·` | موجيبيك — **علّة أنبوبك لا علّة الملف** | ٠ |
| `المجالت` بدل `المجلات` | رباط «ﻻ» فُكّ قبل إصلاح الاتجاه | ١أ+٢+١ب |
| `()مقدمة` بدل `(مقدمة)` | بِدي المحرّك يبعثر المحايدات | القراءة الهندسية |
| `نشُرت` بدل `نُشرت` | العَكس على المحارف لا على العناقيد | ٢ |
| `` أو `?????` | خريطة ToUnicode تالفة | ٣ |
| لا شيء | لا طبقة نصية (ممسوح ضوئياً) | ٤ |

**العلل خمس، والعلاجات خمسة، ولكلٍّ دواؤه.** أكثر ما يُتداول من حلول
يخلط بينها، فيطبّق دواء الثانية على الرابعة، ثم يستنتج أن «العربية
مستحيلة في PDF».

### تصحيحان لخرافتين شائعتين

> ❌ «الـ OCR هو الحل الأسرع والأدق للعربية.»

خطأ. OCR العربي **آخر الدواء لا أوّله**: أبطأ بمراتب، ويخطئ في الهمزة
والتشكيل والأرقام، ويهدم بنية الجداول. لا تنزل إليه إلا حين تنعدم طبقة
النص أصلاً (الدرجة ٤). ما دون ذلك يُحلّ بسطرٍ إلى عشرين.

> ❌ «الرباط ﻻ حرفان مثل ﬁ في اللاتينية.»

خطأ، والفرق ليس تفصيلاً. رباط لام-ألف في العربية **إلزاميّ** لا اختياريّ:
لا يوجد خطٌّ يرسم لاماً ثم ألفاً منفصلتين. فهو في ملف الـ PDF **جليفٌ
واحد**. ومن فكّه إلى حرفين ثم عكس السطر، عكس الحرفين معه فصارت «لا» ←
«ال». وهذا مصدر أشهر عطبٍ في استخراج العربية:

```
الانترنيت → االنترنيت     المجلات → المجالت
الأطاريح  → األطاريح      الإجراء → اإلجراء
```

> ❌ «المشكلة في الحروف؛ فإن خرجت العربية سليمةً فقد نجوت.»

خطأ، وهو أخبث ما في الباب. **الحروف أمتنُ ما في السطر، والترقيم أهشُّه.**
المحايدات (`( ) [ ] . ! ,`) لا اتجاه لها في يونيكود، فيتنازعها ما حولها،
فتُخرج المحرّكاتُ عربيةً سليمةً وترقيماً مبعثراً:

```
(مقدمة الدراسة)   →  ()مقدمة الدراسة
الفقرة [أ-ج] هنا  →  ج[ هنا-الفقرة ]أ
```

ولاحظ أن `؟` و`؛` تنجوان دائماً حيث تعطب `!` و`.` — لأنهما **عربيّتان**
(صنف `AL` قويّ الاتجاه) لا محايدتين. من لا يقرأ العربية لا يرى العطب أصلاً.

> ❌ «`Ø§Ù„Ù…` يعني أن الـ CMap تالف.»

خطأ. هذا موجيبيك: بايتات UTF-8 فُكّت بـ Latin-1. الملف سليم، والعطب في
**كودك أنت**. علاجه `.encode('latin-1').decode('utf-8')`. أما الـ CMap
التالف فعلامته رموز PUA (`U+E000–F8FF`) أو خانات فارغة، وعلاجه شيء آخر
تماماً (الدرجة ٣).

---

## التثبيت

</div>

```bash
# النواة (الدرجات ٠–٢): بلا أيّ تبعيّة — بايثون قياسيّ خالص
pip install arafix

# مع دعم PDF (مستحسن)
pip install "arafix[pdf]"

# كل شيء بما فيه الدرجة ٣
pip install "arafix[all]"

# جسر MarkItDown (إضافة PDF عربية + post-process)
pip install "arafix[markitdown]"

# من المصدر
git clone https://github.com/bio-colab/arafix
cd arafix && pip install -e ".[dev]" && pytest
```

<div dir="rtl">

الـ sdist يحمل الاختبارات والأمثلة عمداً: مَن حمّل المصدر يجب أن يستطيع
تشغيل `pytest` عليه فيتحقق بنفسه، لا أن يصدّق شهادتنا. وللنشر انظر
[RELEASING.md](RELEASING.md).

</div>

<div dir="rtl">

قرارٌ مقصود: **النواة بلا تبعيّات**. الدرجات ٠–٢ تعمل في أيّ بيئة —
Colab مقيّد، خادم بلا إنترنت، Lambda. التبعيّات كلها اختيارية.

---

## جرّبها الآن (٣٠ ثانية، بلا ملفٍّ منك)

</div>

```bash
# ١) ولّد ملفاً معطوباً عمداً — يحاكي مُصدِّراً رديئاً حقيقياً
python examples/make_broken_pdf.py broken.pdf

# ٢) شخّص. لاحظ: هذا الأمر لا يكتب شيئاً، يريك فقط
arafix diagnose broken.pdf -v

# ٣) عالِج
arafix extract broken.pdf
```

<div dir="rtl">

المخرَج قبل وبعد:

</div>

```
─ ما تراه أدوات بايثون:
ﺩﺭﺍﺳﺔ ﻤﻘﺎﺭﻧﺔ ﻔﻲ ﺎﻟﺴﻴﺎﺳﺔ ﺎﻟﻌﺎﻣﺔ
 ﻔﻲ ﻤﺠﻠﺔ ﻤﺤﻜﻤﺔ2024 ﻧﹹﺸﺮﺕ ﻬﺬﻩ ﺎﻟﺪﺭﺍﺳﺔ ﻌﺎﻡ

─ بعد arafix:
دراسة مقارنة في السياسة العامة
 في مجلة محكمة2024 نُشرت هذه الدراسة عام
```

<div dir="rtl">

---

## الاستعمال

### نصّاً

</div>

```python
from arafix import repair_text

r = repair_text(broken_text)

r.text                          # النص بعد العلاج
r.diagnosis.summary()           # 'presentation_forms، visual_order'
r.confidence                    # 0.94
[s.value for s in r.stages_applied]   # ['hygiene', 'diagnose', 'normalize', 'reorder']
r.notes                         # لماذا فُعل كل شيء
```

<div dir="rtl">

### كتلًا / جدولاً (كل خلية مستقلة)

</div>

```python
from arafix import repair_blocks, fix_table, TextBlock

fix_table([["خلية معطوبة", "سليمة"], ["…", "…"]])

out = repair_blocks([
    TextBlock(cell, id=f"r{i}c{j}", role="cell")
    for i, row in enumerate(grid)
    for j, cell in enumerate(row)
])
out.by_id()["r0c1"].text
```

<div dir="rtl">

### بعد MarkItDown أو أيّ مستخرج

انظر [INTEGRATING.md](INTEGRATING.md).

</div>

```python
from arafix import fix_markitdown, fix_any
from markitdown import MarkItDown  # اختياري

fixed = fix_markitdown(MarkItDown().convert("thesis.pdf"))
# أو: fix_any(open("paste.txt", encoding="utf-8").read())
```

<div dir="rtl">

### ملفاً

</div>

```python
from arafix import extract_pdf

doc = extract_pdf("thesis.pdf")
print(doc.text)
print(doc.confidence)           # أدنى ثقة عبر الصفحات

for page in doc.pages:          # كل صفحة تُشخَّص وحدها — عمداً
    if page.repair.confidence < 0.7:
        print(page.page_number, page.repair.diagnosis.summary())
```

<div dir="rtl">

### التشخيص وحده (بلا علاج)

</div>

```python
from arafix import diagnose

d = diagnose(text)
d.defects                                    # [PRESENTATION_FORMS, VISUAL_ORDER]
d.confidence_in(Defect.PRESENTATION_FORMS)   # 1.0   ← شاهدٌ قاطع
d.confidence_in(Defect.VISUAL_ORDER)         # 0.93  ← شاهدٌ ظنّيّ
d.confidence                                 # 0.93  ← أضعف حلقة
for e in d.evidence:
    print(e)     # final_only_letters=+0.940 :: ة/ى في أول 47 كلمة مقابل آخر 3
```

<div dir="rtl">

**ولاحظ أن الثقة مفصولة لكل علّة.** الرقم الواحد يُخفي أن بعض شواهدنا
قاطعة وبعضها ظنّيّ، فيظلم الأولى ويجمّل الثانية:

| نوع الشاهد | الثقة | لماذا |
|---|---|---|
| قاطع (نطاقٌ أو اختبارٌ جبريّ) | **١٫٠ دائماً** | فحصُ نطاقٍ على ٥ محارف قاطعٌ كفحصه على ٥٠٠٠. حجمُ العيّنة لا دخل له. |
| ظنّيّ (الاتجاه) | بدرجة شاهده | |
| «سليم» — شهادةُ نفي | ٠٫٣ ← ٠٫٩ بحجم العيّنة | وحدَها يحكمها الحجم، إذ هي استدلالٌ بغياب الدليل. **وسقفُها ٠٫٩ عمداً: غيابُ العلّة ليس برهانَ سلامة.** |

**لاحظ الشواهد.** الفرق بين أداةٍ تقول «النص معكوس» وأداةٍ تقول «معكوس
لأن ٩٤٪ من التاءات المربوطة وقعت أوّل الكلمة» هو الفرق بين أداةٍ تُصدَّق
وأداةٍ تُستعمل على عمى.

### الضبط

</div>

```python
from arafix import PipelineConfig, NormalizeConfig, repair_text

cfg = PipelineConfig(
    normalize=NormalizeConfig(
        strip_diacritics=False,   # افتراضيّ: الاسترجاع لا التعديل
        unify_alef=True,          # للبحث والفهرسة فقط
    ),
    thresholds={"visual_order": 0.45},   # عتبة أشدّ
    force_reorder=False,
)
r = repair_text(text, cfg)
```

<div dir="rtl">

### ترقيع نصٍّ أعطبته أداةٌ أخرى

إن كان نصّك مُستخرَجاً بأداةٍ غير هذه وفيه `المجالت` و`االنترنيت`:

</div>

```python
from arafix import repair_lam_alef_transposition

r = repair_lam_alef_transposition("االنترنيت والمجالت")
r.text              # 'الانترنيت والمجالت'  ← القاطع أُصلح
r.fixed_decisive    # 1
r.suspects_left     # 1
r.suspect_words     # ['المجالت'] ← مُبلَّغٌ عنه، غير مُخمَّن

# ومع معجم، يُحسم المُبهَم أيضاً:
repair_lam_alef_transposition("المجالت", lexicon=my_words).text   # 'المجلات'
```

<div dir="rtl">

يمرّ هذا تلقائياً داخل `repair_text` / `extract_pdf`؛ ومرِّر
`PipelineConfig(lexicon=...)` لتزويده بالمعجم.

---

## المعمار

</div>

```
النص الخام  ◄── extractors/: القراءة الهندسية من تيار الرسم لا من بِدي المحرّك
    │
    ├─ ٠ diagnose.py   ◄── لا تعالج قبل أن تعرف. لا يكتب شيئاً.
    │      ├── detect_mojibake            اختبار جبريّ قاطع
    │      ├── detect_presentation_forms  عدّ نطاقيّ
    │      ├── detect_pua                 عدّ نطاقيّ
    │      └── detect_visual_order        ٣ شواهد لغوية، تصويت مرجَّح
    │
    ├─ ١أ normalize.py ◄── المفردات وحدها. ما يغيّر العنقود يُؤجَّل.
    ├─ ٢ order.py      ◄── بصريّ ← منطقيّ، بحماية الأرقام واللاتينية
    ├─ ١ب normalize.py ◄── الرباطات والتشكيل الفاصل — بعد استقرار الترتيب
    ├─ ⚕ lamalef.py    ◄── ترقيع عطبٍ وَرِثناه من أداةٍ أخرى
    ├─ ٣ cmap.py       ◄── إعادة بناء الخريطة من الخط المضمَّن
    └─ ٤ OCR           ◄── آخر الدواء (لم يُنفَّذ بعد)
```

<div dir="rtl">

### القرارات المعمارية، ولماذا اتُّخذت

**١. كل جدول يونيكود مُولَّد، لا مكتوب بيد.**
٧٣١ مدخلةً مشتقّةً من `unicodedata` — لا خطأ مطبعياً ممكناً، وتتحدّث مع
نسخة يونيكود تلقائياً. الاستثناءات وحدها يدوية، وكلٌّ منها مُبرَّرٌ في
تعليقٍ إلى جانبه.

**٢. تطبيعٌ مُوجَّه لا `NFKC`.**
`NFKC` يحلّ المشكلة ويحلّ معها عشرين لم تطلبها: يقلب `R²` إلى `R2`،
و`ﬁle` إلى `file`، و`①` إلى `1`. في بحثٍ أكاديميّ فيه رموز رياضية، هذا
تخريبٌ صامت. فنحن نطبّع نطاق الأشكال العربية وحده.

**٣. لا مرحلة تُرجع نصاً عارياً.**
كلٌّ تُرجع كائناً يحمل النص ومعه سببَ ما فعلت ودرجةَ ثقتها. القرار
للمستعمل، والمكتبة قابلة للتدقيق.

**٤. لا درجةَ تُطبَّق بلا شاهد.**
المكتبة **لا تعالج «احتياطاً»**. عكسُ نصٍّ سليمٍ تخريبٌ بأيدينا. وأهمّ
اختبارٍ في الحزمة اسمه `test_does_not_touch_healthy_text`.

**٥. التطبيع مُشطَّرٌ حول الاتجاه: ١أ ← ٢ ← ١ب.**
هذا القرار كُتب أوّلاً «التطبيع قبل الاتجاه»، وكان نصفَ صواب. فالتطبيع
الكامل قبل الاتجاه يفكّ «ﻻ» إلى حرفين، ثم يعكسهما العكسُ إلى «ال». فصار:
تُطبَّع المفردات (فتنكشف التاء المربوطة لكاشف الاتجاه)، **ويبقى الرباط
ذرّةً**، ثم يُعكس، ثم يُفكّ الرباط. الدرجة ١ تفتح عين الدرجة ٢ **ولا
تسلّمها سكيناً**.

**٥ب. معيارُ التأجيل تغيُّرُ بنية العنقود، لا طولُ التفكيك.**
يُؤجَّل صنفان: الرباطات (محرفٌ يصير محرفين)، وأشكال التشكيل الفاصلة
`U+FE70–FE7F` (محرفٌ فئتُه `Lo` يصير علامةً لاصقة `Mn`). كلاهما يغيّر
وحدةَ العكس، فتطبيعُهما المبكر يهدم ما بعده. ومعيارُ الطول وحده يُعمي عن
الثاني: تفكيك `U+FE79` هو [كشيدة + ضمّة]، ونحن نطرح الكشيدة فيعود الطول
واحداً فيبدو بريئاً. فالفئةُ تفضح ما لا يفضحه الطول.

**٥ج. الكاشف يقرأ طبقتين، لأن التطبيع يفقأ عيناً وهو يفتح أخرى.**
التاء المربوطة لا تُرى إلا بعد التطبيع، وصيغُ الوصل لا تُرى إلا قبله.
فيأخذ `detect_visual_order` النصّ المطبَّع **ومعه** الأصل الرسوميّ
(`shaped_source`)، فيشهد كلٌّ من طبقته.

**٥د. هويّة الوصل برهانٌ لا أمارة.**
في العربية: `joins_forward(a) == joins_backward(b)` لكل حرفين متجاورين —
لا تتخلّف أبداً في نصٍّ منطقيّ. فخرقُها **مرّةً واحدة** يُثبت الانعكاس.
شاهدٌ لا تماثليّ: يدحض ولا يُزكّي. (كان الفحص أوّلاً على طرفَي الكلمة
وحدهما، فأفلتت منه كلماتٌ كـ«الإجراء» طرفاها منفصلان.)

**٦. `text[::-1]` خطأ، لا اختصار — لثلاثة أسباب لا سبب.**
(أ) الأرقام واللاتينية LTR في الحالين، فالعكس يفسد `2024` فتصير `4202`
و`GDP_2024` تصير `2024_GDP`. (ب) الأقواس **مِرآتية**: جليفُ أقصى اليسار
في سطرٍ عربيّ هو `(` وإن كان المحرف المنطقيّ هناك `)`، فبلا مرآةٍ تخرج
`)مقدمة(`. (ج) **وحدةُ العكس العنقودُ لا المحرف**: التشكيل عرضُه صفر
ويشترك في موضع حرفه، فعكسُ المحارف يُلصقه بالجار (`أولاً` ← `أوًلا`).

**٧. القراءة من تيار الرسم، لا من بِدي المحرّك.**
قياسٌ لا رأي: على ١٢ سطراً فيها ترقيم، أخفق مسار `get_text()` في ٩
وأخفق المسار الهندسيّ في صفر. ونقرأ بـ `get_texttrace` لا `rawdict`،
وهذا **شرط**: `rawdict` يعيد ترتيب محارفه ببِدي MuPDF قبل تسليمها، فيهدم
الربط الذي جئنا نستشهد به.

**٨. ولكلٍّ من ربط العنقود وترتيبه شاهدٌ مختلف — والخلطُ بينهما فخّ.**
*الربط* (أيّ علامةٍ لأيّ حرف؟) **من التيار**: الهندسة تكذب هنا، إذ
العلامة عرضُها صفر فتُرسَم عند القلم بعد أن تجاوز حرفَها، فـ `x` عندها
يساوي `x` للحرف **التالي**. *الترتيب* (أيّ عنقودٍ قبل أيّ؟) **من
الهندسة**: التيار قد يكون بصرياً، و`x` وحده يقول أين وقع كلُّ شيء.

**٩. لا نكتب قارئ PDF.**
كتابته عملُ سنين، وموجودٌ منه ما يكفي. كل محرّك يُغلَّف خلف `Extractor`
واحد، فتبديله سطرٌ وإضافةُ جديدٍ ملفٌّ واحد.

**١٠. `cid1234` لا يُفكّ.**
رقمٌ داخليّ للخط بلا دلالة. من يفكّه يخترع من عنده — وهذا خطٌّ أحمر:
المكتبة تعجز صراحةً ولا تخترع أبداً.

---

## التوسيع

### محرّك استخراج جديد

</div>

```python
from arafix.extractors import Extractor, RawPage, register

@register
class PdfMinerExtractor(Extractor):
    name = "pdfminer"

    @classmethod
    def available(cls) -> bool:
        try:
            import pdfminer; return True
        except ImportError:
            return False

    def pages(self, path):
        from pdfminer.high_level import extract_pages
        ...
        yield RawPage(number=i, text=text)

    def font_bytes(self, path):
        return {}
```

<div dir="rtl">

سطرٌ واحد (`@register`)، ولا يُمسّ شيءٌ آخر في المكتبة. ثم:
`extract_pdf(path, PipelineConfig(extractor="pdfminer"))`.

### كاشف علّة جديد

١. أضف عضواً إلى `Defect` في `types.py`.
٢. اكتب دالةً في `diagnose.py` تُرجع `(score, Evidence)`.
٣. نادها داخل `diagnose()` مع عتبةٍ في `DEFAULT_THRESHOLDS`.
٤. اكتب اختباراً يوثّق **القرار** لا السطر.

### شاهد اتجاه جديد

أضف دالة `_signal_*` تُرجع `(score, detail)` في `[-1, 1]`، وسجّل وزنها في
`_ORDER_WEIGHTS`. التصويت يُعاد تطبيعه على الشواهد الحاضرة وحدها، فغياب
شاهدٍ لا يُميّع النتيجة.

---

## خارطة الطريق

- [x] الدرجة ٠ — التشخيص بشواهد
- [x] الدرجة ١ — التطبيع المُوجَّه
- [x] الدرجة ٢ — الاتجاه بحماية LTR
- [x] الدرجة ٣ — الخريطة من `cmap` وأسماء الجليفات
- [x] الرباطات — تشطير التطبيع حول الاتجاه + ترقيع رجعيّ (0.2.0)
- [x] المحايدات — قراءة هندسية، مرآة الأقواس، عكسٌ عنقوديّ (0.3.0)
- [ ] معجم عربيّ مدمج يحسم مواضع «ال» الوسطية بلا تدخّل
- [ ] الدرجة ٣+ — مطابقة الشكل (perceptual hash للجليف)
- [ ] الدرجة ٤ — غلاف OCR
- [ ] استخراج بنيويّ (جداول، حواشٍ، أعمدة)
- [ ] محرّكات: pdfminer، pypdf، pdftotext
- [x] القياس — CER/WER ومقارنةُ المسارات على ملفك (0.4.0)
- [x] نظافة الاستخراج — NBSP / soft-hyphen (0.7.0)
- [x] `repair_blocks` / جداول — إصلاحٌ مستقلّ لكل خلية (0.7.0)
- [x] معجم الوثيقة الداخليّ يحسم «المجالت» عبر الصفحات (0.7.0)
- [x] جسر MarkItDown — plugin + `fix_markitdown` (0.7.0)
- [x] أعمدة RTL + ترويسة/تذييل + جداول بنيوية (0.8.0)
- [ ] حزمة ملفات مرجعية (corpus) عامّة لقياس الانحدار
- [ ] حسمُ «المجالت» بنموذج n-gram على مستوى المحرف (اقتباساً من CAMeL)
- [ ] إخراج PDF قابل للبحث بالنصّ المصحَّح

**الدرجة ٣+ فكرتها:** ارسم كل جليفٍ من الخط المضمَّن، وقارنه بصرياً
بمرجعٍ لكل حرفٍ عربيّ. هذا OCR على مستوى **الجليف** لا الصفحة: مساحة
البحث ٣٦ حرفاً × ٤ أشكال، لا لغةٌ كاملة. أدقّ بمراتب وأسرع.

---

## الاختبارات

</div>

```bash
pytest                      # ~١٨٠ اختباراً (+ hygiene/blocks/plugin)
pytest --doctest-modules src/arafix
ruff check src tests
```

<div dir="rtl">

كل اختبارٍ يوثّق **قراراً** لا سطر كود. فإن كسرته يوماً، عرفت من اسمه
أيّ قرارٍ كسرت ولماذا اتُّخذ أوّلاً.

---

## حدودٌ مُعلَنة

بصراحةٍ تسبق الاستعمال:

- **الدرجة ٤ (OCR) غير منفَّذة، ولا حزمةَ `arafix[ocr]`.** المكتبة تكشف
  الحاجة وتقولها ولا تدّعي. وكان هناك `extra` باسم `ocr` يجرّ `pytesseract`
  بلا كودٍ يستعمله — وعدٌ بلا سند، فحُذف. لا نبيع تبعيّةً مقابل نيّة.
- **الدرجة ٣ تعجز عن الخطوط CID** ذات الأسماء العديمة الدلالة. تُصرّح
  بالعجز (تغطية منخفضة) ولا تخترع.
- **كاشف الاتجاه احتماليّ** لا حتميّ — ولذلك يُرجع درجةً وشواهد لا حكماً.
- **الجداول والأعمدة** خارج النطاق حالياً؛ الاستخراج خطّيّ.
- **ترقيع لام-ألف الموروث ناقصٌ بطبعه.** يُصلح القاطع (ألفان متجاورتان:
  `االنترنيت`) يقيناً، ويُبلّغ عن المُبهَم (`المجالت`) ولا يخمّنه — إذ لا
  قاعدةَ إملائية تميّزه عن «أفعالهم». مرِّر `lexicon=` ليُحسم. **والوقاية
  وحدها تامّة**: ما تعالجه المكتبة من أوّله يخرج سليماً بلا معجم.
- **الأعمدة والجداول** مدعومة منذ 0.8.0 عبر `layout="auto"|"columns"|"full"`
  (ميزاب أفقي + RTL). الكشف إحصائيّ: صفحات بثلاثة أعمدة متداخلة أو
  جداول بلا فجوات واضحة قد تحتاج ضبط `LayoutConfig`.
- **لم تُختبر إلا على ملفاتٍ مولَّدة**، وكلُّ رقمٍ في هذا الملف مقيسٌ
  عليها. وهو نصفُ حجّة. `arafix eval --compare` موجودٌ لتُتمّها أنت على
  ملفاتك. وإن كسرَتها ملفاتُك، افتح issue بها — هذا أنفع إسهامٍ ممكن:
  عطبُ الرباط الذي أصلحته 0.2.0، وأعطابُ الترقيم الثلاثة في 0.3.0،
  كلُّها وصلت من مستعملٍ فعل ذلك بالضبط.

---

## المساهمة

الأنفع بالترتيب: (١) ملفات تكسرها، (٢) محرّكات جديدة، (٣) شواهد اتجاه
أقوى، (٤) الدرجة ٣+.

## الترخيص

MIT — انظر [LICENSE](LICENSE).

</div>
