العرض الموحّد للمستندات
دليل مرجعي المصدر لطبقة العرض الموحّدة للمستندات في كاظمه. كل صيغة إخراج تمرّ عبر
DocProfileواحد، فيتشارك ملف DOCX وPDF وHTML وMarkdown وXLSX وPPTX المولَّد تصميمًا واحدًا ومعالجة صحيحة للكتابة من اليمين إلى اليسار — أيًّا كان الامتداد أو اللغة.
1. لماذا طبقة موحّدة
Section titled “1. لماذا طبقة موحّدة”قبل هذه الطبقة، كان لكل صيغة مُعرِضها الخاص بسمته الخاصة، وكشفه الخاص للاتجاه، ومعالجته الخاصة لـ RTL. فنشأ صنفان من الأخطاء:
- الانحراف — تباعدت الألوان والخطوط والتسميات بين الصيغ؛ فبدا ملف DOCX عربي وملف PDF عربي كأنهما منتجان مختلفان.
- أخطاء RTL المتكررة — أُصلح خطأ الاتجاه/المحاذاة نفسه خمس مرات (مرة لكل صيغة) وظل يعود.
تضع الطبقة الموحّدة دلالات الاتجاه + التصميم + المحاذاة في مكان واحد
(DocProfile) وتجعل كل محرّك صيغة إسقاطًا رقيقًا له. فخطأ في المحاذاة
العربية يُصلح الآن في دالة واحدة، لا في خمس.
2. الطبقات الثلاث
Section titled “2. الطبقات الثلاث”payload ──► _build_model_and_profile ──► (ContentModel, DocProfile) ──► Engine ──► fileالطبقة 1 — DocProfile (kazma_core/documents/profile.py)
Section titled “الطبقة 1 — DocProfile (kazma_core/documents/profile.py)”المرجع الموثوق الوحيد لـ اتجاه المستند + تصميمه + إطاره.
direction(ltr/rtl) — الاتجاه الأساسي للمستند (الهوامش، والقسم، والإطار). ترتيب الحسم: يفوزrtlالصريح، ثمlang(رمز للعربية أو الفارسية أو الأردية أو العبرية… يثبّت RTL؛ وأي رمز آخر يثبّت LTR)، ثم المحتوى: يقرّرarabic.direction_ofبحسب صنف bidi في يونيكود (R/AL)، إذ يُفحص أول حرف قوي مقابل حصة RTL من النص — ولا يقرّر أبدًا بتعبير نمطي لكتل نقاط الترميز. وتعيش كل سياسة اتجاه النص فيkazma_core/documents/arabic.py.shape_arabic— يحوي المحتوى أي حرف يُكتب من اليمين إلى اليسار، فيجب أن يشكّله محرّك مرئي. وهو سؤال مختلف عنdirection: المستند الإنجليزي في معظمه الذي يقتبس العربية هو LTR ويحتاج إلى تشكيل.block_direction(text)— اتجاه كتلة واحدة. لا يلزم أن يكون المستند بلغة واحدة؛ فكل محرّك يحسم اتجاه كل كتلة على حدة، والكتلة التي لا حرف قويًّا فيها (خط فاصل، أو رقم مجرد، أو تاريخ) ترث اتجاه المستند.theme— الألوان والخطوط والأحجام وحجم الصفحة (A4)، مشتركة منstyle_theme.THEME. ألوان العلامة (الأزرق الملكي#3b82f6، والحبر الكحلي#16223a). تستخدم اللاتينية والعربية كلتاهما IBM Plex Sans Arabic (font_latin/font_arabic)؛ والمتن 11pt في الاتجاهين (theme_cs_sizeبنسبة 1:1 افتراضيًا)، وتباعد الأسطر 1.65 في الاتجاهين، وهوامش 1.8 سم في DOCX، و56 pt (نحو 2 سم) في مسار PDF عبر reportlab. ويُشحن IBM Plex Sans Arabic وأميري فيkazma_core/documents/assets/fonts/(ويتجاوزKAZMA_DOCUMENT_FONT_DIRذلك)؛ وأميري احتياطي نسخي حين يغيب Plex. ولا يثبّت PPTX/XLSX خط Calibri في الكود.chrome— تسميات مُوطَّنة (المحتويات/Contents، ونص العلامة) عبرlocalized_chrome.- سياسة المحاذاة — القطعة الحاسمة. تربط
docx_jc(intent)/pdf_align(intent)/html_text_align(intent)النيّة (start/justify/end) بالقيمة الأصلية لكل محرّك. انقلابw:jcفي اتجاه Word يعيش هنا، مرة واحدة.
قاعدة الاتجاه (لا تُعِد اشتقاقها): تحت
w:bidi، يربط Word w:jc="right"بـالنهاية المنطقية = اليسار الفعلي. ولتثبيت العربية عند حافة بداية القراءة (اليمين الفعلي)، استخدمw:jc="start"(أو"left") — ولا تستخدم"right"أبدًا. تعيدdocx_jc("start")في الملف الشخصي"start"؛ ولا يختار أي موضع استدعاءRIGHT/LEFTخامًا بعد الآن.
الطبقة 2 — ContentModel (kazma_core/documents/content_model.py)
Section titled “الطبقة 2 — ContentModel (kazma_core/documents/content_model.py)”كتل مستقلة عن الصيغة: Title، وHeading، وBody (Markdown)، وTable، وTOC،
وCitation، إضافة إلى الترويسة/التذييل/البيانات الوصفية. كل مسار إدخال ينتج هذه
الشجرة؛ وكل محرّك ذي شكل مستند يستهلكها. (تحتفظ XLSX/PPTX بحمولات
أوراقها/شرائحها — انظر أدناه — لكنها تستهلك DocProfile أيضًا.)
الطبقة 3 — المحرّكات (kazma_core/documents/engines/)
Section titled “الطبقة 3 — المحرّكات (kazma_core/documents/engines/)”محرّك لكل صيغة. يطبّق كلٌّ منها render(...) ويملك آليات صيغته الخاصة،
قارئًا الاتجاه/السمة/المحاذاة من الملف الشخصي:
| المحرّك | آلية RTL |
|---|---|
docx.py | w:bidi لكل فقرة من profile.block_direction (تُختم الفقرة LTR بـ w:bidi val="0") + w:jc من profile.docx_jc؛ ومتغير النص المركّب w:szCs، وتوأم w:bCs/w:iCs لكل w:b/w:i (التوأم هو الذي يرسم العريض والمائل العربيين)؛ وw:rtl على المقاطع العربية فقط؛ وفي كتلة من اليمين إلى اليسار، يُعلَّم كل مقطع من اليسار إلى اليمين يحوي حرفًا لاتينيًا بعلامة LRM على جانبيه (ومقطع الأرقام وحده يحتفظ بالعُرف العربي)؛ وعلامات الاتجاه تُرسم بخط فيه رسمها؛ وw:bidiVisual على الجداول التي لغتها هي من اليمين إلى اليسار؛ وأشرطة العناوين على جانب قراءة العنوان نفسه؛ وكل مستوى تعداد نقطي ”•” بخط الدار؛ وw:keepNext على العناوين؛ وw:outlineLvl لحقل جدول المحتويات. |
pdf.py | أي مستند يحتاج إلى تشكيل (profile.shape_arabic) يُوجَّه DOCX←LibreOffice←PDF؛ وكل ما عداه، والمسار الاحتياطي حين يغيب LibreOffice أو يفشل، هو reportlab مع محاذاة لكل كتلة وأعمدة جداول RTL معكوسة. |
html.py | سمة dir + CSS unicode-bidi؛ وdir على كل جدول من profile.table_direction؛ وtext-align-last: start، لتُنهي كل كتلة فقراتها على جانبها هي؛ وغلاف dir على الكتل التي يختلف اتجاهها عن اتجاه المستند؛ وعوازل <bdi dir="ltr"> للاتينية المضمّنة؛ والخط المثبَّت مضمَّنًا في صادرات العربية (documents.render.embed_html_fonts). |
xlsx.py | sheet_view.rightToLeft + readingOrder للنص المركّب. |
pptx.py | <a:pPr rtl="1"> و algn="r" (لا يكفي rtl وحده للمحاذاة يمينًا؛ وسمة DrawingML هي algn لا al). |
3. صحة الاتجاه — التفاصيل التي كلّف تعلّمها كثيرًا
Section titled “3. صحة الاتجاه — التفاصيل التي كلّف تعلّمها كثيرًا”هذه هي الحقائق غير البديهية التي يرمّزها كل محرّك مرة واحدة:
- DOCX — يجب أن يضبط الخط العريض/المائل/الحجم للعربية متغيرات النص المركّب
(
w:bCs/w:iCs/w:szCs)؛ ويضيف المحرّك التوأم إلى كل علامة عريض ومائل في نهاية العرض، فيكون النصف العربي من عنوان ثنائي اللغة والمقدّمات العريضة في قائمة عربية عريضةً. فـrun.bold/run.font.sizeفي python-docx لا يكتبان إلاw:b/w:szاللاتينيين، فترتد العربية بصمت إلى الحجم الافتراضي مع عريض مزيّف — عَرَض «الحروف الغريبة» في شريط العنوان. يكتبهاDocxEngine._mark_run. لا تنسخw:szإلىw:szCs: كان خط عربي أقدم (Sakkal Majalla) يُقرأ أصغر من Calibri عند الحجم الاسمي نفسه، وكثيرًا ما تخلو مقاطع المتن منw:szخاص بها (ترثه من Normal). ويوازي IBM Plex Sans Arabic الحجم البصري اللاتيني (body_size_arيساويbody_size، أي 11pt). تبقى اللاتينية عندbody_size؛ وتستخدم العربيةtheme_cs_size()←body_size_arعلىw:szCs(النمط + كل مقطع RTL). ارفعbody_size_arلتختار تكبيرًا للنص المركّب. - PDF (العربية) — reportlab محرّك مرئي LTR لا يستطيع تشكيل العربية المختلطة
باللاتينية وMarkdown المضمّن تشكيلًا صحيحًا (تتكدس الرموز، وتنقسم اللاتينية بين
الأسطر). يوجّه
PdfEngineكل مستند يحوي كتابة من اليمين إلى اليسار (profile.shape_arabic، لا المستندات التي تغلب عليها RTL وحدها) عبر محرّك DOCX (اتجاه صحيح) ثم LibreOffice بلا واجهة ← PDF، في استدعاء محدود يقتل شجرة العمليات عند انتهاء المهلة. وكل تحويل ينسخ أولًا الخطوط المرفقة إلى ملف LibreOffice الشخصي (fonts.give_soffice_the_bundled_fonts): فـ LibreOffice لا يستخدم إلا خطوط النظام وخطوط ملفه الشخصي، وكان المضيف أو صورة العامل الخاليان من IBM Plex يعرضان DejaVu Sans وNoto Serif. ودون LibreOffice، أو حين يفشل، يُستخدم مسار reportlab مع تحذير؛ وهو يشكّل كل فقرة مرة واحدة (arabic.shape_spans)، ويرفض العرض (arabic_font_unavailable) بدل إخراج PDF عربي فارغ حين لا يغطي أي خط أشكال العرض العربية. وتبقى المستندات الخالية من كتابة RTL على reportlab مباشرة. - ملف DOCX يحمل خطوطه — ملف Word يسمّي خطوطه ولا يحملها، لذا يضمّن كل
ملف DOCX يكتبه المحرّك خط IBM Plex Sans Arabic المرفق، العادي والعريض، كما
يضمّن Word الخطوط (أجزاء خطوط مموّهة و
w:embedTrueTypeFonts،engines/docx_fonts.py). ودونه كان الجهاز الذي لا يملك IBM Plex يعرض الملف بخط آخر. ويضيف نحو 210 كيلوبايت للملف بعد الضغط. - لكل كتلة، لا لكل مستند — يُبقي المستند ثنائي اللغة كتل كل لغة محاذاةً
إلى بداية قراءتها هي في كل محرّك:
w:bidiلكل فقرة (DOCX)، وأنماط لكل كتلة (PDF)، وغلافdir(HTML). - الجداول تُقرأ بلغتها هي، أيًّا كانت لغة المستند
(
profile.table_direction): فالجدول الإنجليزي في مستند عربي يُقرأ من اليسار إلى اليمين، والجدول العربي في مستند إنجليزي من اليمين إلى اليسار. والآلية هيw:bidiVisual(DOCX)، وdirعلى الجدول (HTML)، وأعمدة معكوسة (PDF عبر reportlab). والخلية التي لا حرف فيها (نطاق مثل “29 – 32”) تأخذ اتجاه جدولها، فتُقرأ كما كُتبت. ويفحصtests/test_document_table_direction.pyذلك عبر LibreOffice حقيقي. - مقياس خطوط واحد — يحدّد
theme_cs_size()حجم النص المركّب في DOCX (w:szCs) وHTML ومحرّك PDF على السواء، فيتوزّع المستند نفسه على الصفحات بالطريقة نفسها في كلٍّ منها. ويقيسtests/test_document_layout.pyذلك على ملفات PDF معروضة. - PPTX — يحتاج RTL الفقرة إلى
rtl="1"و محاذاةalgnصريحة؛ وrtlوحده يترك النص محاذيًا لليسار (يرث من التخطيط). - HTML — الاتجاه أصلي (
dir+ خوارزمية المتصفح)؛ و<bdi>الصريح ليس إلا لمسة إضافية لعناوين URL/المعايير.
4. خط المعالجة
Section titled “4. خط المعالجة”يمرّ كل توليد وكل تحويل Markdown←X عبر مترجم واحد:
renderer_worker._build_model_and_profile(payload) ← (ContentModel, DocProfile)
← المحرّك المختار. تصل طبقة المهارات وطبقة الخدمة ونقطة النهاية HTTP /generate
كلها إلى المحرّكات عبر هذا الباب الوحيد، فيسري تغيير التصميم على كل مخرج معًا.
مصدر Markdown يحتفظ بشكله: يبقى فاصل السطر الصلب (مسافتان في آخر السطر أو
شرطة مائلة عكسية) فاصلَ سطر في الفقرات والاقتباسات؛ ومستوى عنوان القسم هو عدد
علامات # فيه نسبةً إلى أقل العناوين عمقًا تحت العنوان الرئيسي؛ والاقتباس قائم
غير مائل، بحبر خافت، مع شريط على جانب قراءته هو، ويبقى في صفحة واحدة مع السطر
الذي يقدّمه؛ ويُرسم --- خطًّا فاصلًا؛ ويُحذف العنوان الذي يكرّر العنوان
الرئيسي، ومنه العنوان ثنائي اللغة الذي يأتي نصفاه بالترتيب الآخر
(tests/test_document_archive_layout.py،
tests/test_document_bilingual_title_and_arabic_bold.py).
التحويلات الأمينة تجاوزات مقصودة (لا «توحّدها»):
تحويل LibreOffice من Office إلى PDF، وحجب mutation_worker، يحفظان تنسيق المصدر
عمدًا.
تحويل HTML←PDF هو الاستثناء: يخرج بأسلوب الدار
(renderer_worker._house_style_html). يدخل محتوى الملف (العناوين، والفقرات،
والقوائم، والجداول، وعلامات dir) في غلاف HtmlEngine، مع خط العلامة التجارية
مضمَّنًا، وأشرطة العناوين، ونمط الجداول؛ وتُسقط خطوطه وألوانه وسمات العرض فيه
وCSS الخاص به، وتُحفظ فواصل الصفحات التي طلبها. وتحمل الصفحة المطبوعة إطار محرّك
DOCX: العلامة التجارية المختصرة في الأعلى، والعلامة التجارية مع رقم الصفحة في
الأسفل، على جانب بداية القراءة (HtmlEngine.page_chrome_css). والفقرات المكتوبة
بنودَ قائمة (”▪ …”) تصبح قائمة، ورموز التعداد والتأشير التي يفتقدها الخط تُرسم
• و✓، ويُعلَّم المقطع اللاتيني داخل نص من اليمين إلى اليسار، فلا يُرسم “50 °C”
على هيئة “C° 50”. وأداة المسار convert_document(keep_source_style=true) تحفظ
تصميم الملف نفسه؛ أما document_convert في المنصة، ومسار الويب، و/docs convert
فلا خيار كهذا فيها (tests/test_document_html_house_style.py).
أين يجري. يجري العرض في عامل العرض المعزول (renderer_worker.py) — عملية
فرعية على المضيف في ملف التعريف process، وحاوية جديدة من صورة العامل في ملف
التعريف container — ولا يجري أبدًا في عملية كاظمه. ولا يجلب العرض شيئًا
أبدًا: لا يُقرأ إلا عناوين data: URI في المصدر والأصول المنسوخة إلى المهمة
(بعد التحقق منها مقابل مجاميعها الاختبارية). ويُرفض مصدر HTML أو Markdown يسمّي
عنوان URL خارجيًا أو أي ملف محلي آخر (external_resource_denied)؛ وتُستبعد كتلة
الصورة التي تسمّي أصلًا لا تحمله المهمة. ويجتاز كل ملف PDF ينتجه العامل سياسة
المحتوى النشط في PDF (pdf_policy.py) قبل أن يغادر العامل.
5. إضافة محرّك صيغة جديد
Section titled “5. إضافة محرّك صيغة جديد”- أنشئ
kazma_core/documents/engines/<format>.pyبصنف يأخذDocProfileويطبّقrender(...). اقرأ الاتجاه/السمة من الملف الشخصي (profile.rtl، وprofile.theme، وسياسة المحاذاة) — ولا تُعِد كشف الاتجاه أبدًا ولا تثبّت الألوان في الكود. - اربط
_generate_<format>(output, payload)فيrenderer_worker.pyبحيث يبني ملفًا شخصيًا (عبر_build_model_and_profileلأشكال المستندات، أوDocProfile.for_contentللحمولات ذات الشكل الأصلي) ويستدعي المحرّك. - أضف العملية إلى سجل المُعرِضات
(
kazma_core/documents/renderers/__init__.py) وإلى_MIME/_EXTENSIONS. وإن احتاج إلى حزمة Python أو ملف تنفيذي للنظام، فأضفه إلى صورة العامل أيضًا (deploy/document-worker/requirements.in/deploy/document-worker/Dockerfile): ففي ملف التعريفcontainerتكون المحرّكات الموجودة هي التي تبلّغ عنها الصورة. - أضف اختبارًا إلى
tests/test_unified_document_layer.pyيؤكد أن المحرّك يستهلك الملف الشخصي (الاتجاه + رمز من السمة)، ووسّعscripts/verify_documents.py.
إن كانت الصيغة ذات شكل مستند (عنوان + أقسام)، فأعد استخدام ContentModel. وإن
كان لها شكلها الخاص (كالأوراق/الشرائح)، فاحتفظ بتلك الحمولة لكن استهلك
DocProfile — التوحيد على مستوى الملف الشخصي، لا نموذج المحتوى.
6. التحقق
Section titled “6. التحقق”يولّد scripts/verify_documents.py عينات عربية + إنجليزية لكل الصيغ الست،
ويفحص كلًّا منها بحثًا عن علامات السمة + الاتجاه المتوقعة، ومنها فحص بكسلات
لملف PDF معروض (العربية على اليمين، والإنجليزية على اليسار) لا تستطيعه
تأكيدات رايات XML. شغّله قبل أي تغيير في العرض:
python scripts/verify_documents.pyتثبّت اختبارات الانحدار (tests/test_unified_document_layer.py،
tests/test_docx_rtl_visual.py) ربط الاتجاه jc=start، ومتغيرات النص المركّب
szCs/bCs، وalgn في PPTX، ورموز السمة عبر الصيغ.
ويقيس tests/test_document_layout.py ملفات PDF المعروضة بـ PyMuPDF: مقياس خطوط
النص المركّب، وترتيب أعمدة جداول RTL، والاتجاه والمحاذاة لكل كتلة، وبقاء
العناوين مع نصّها، وبقاء الأسطر المشكَّلة داخل عمودها. وتعرض
tests/test_document_table_direction.py (كل جدول بلغته هو)،
وtests/test_document_house_bullets.py (التعداد النقطي بخط الدار)،
وtests/test_document_html_house_style.py (HTML←PDF بأسلوب الدار) عبر
LibreOffice حقيقي. ويُتخطّى tests/test_docx_rtl_visual.py دون LibreOffice أو
PyMuPDF. وتعرض مجموعة
اختبار الدقة (kazma_core/documents/fidelity.py) أيضًا ملفات DOCX وHTML وPDF
إنجليزية وعربية وثنائية اللغة بهذه المحرّكات ثم تحلّلها عائدةً، فيظهر هناك أي
تغيير في العرض يكسر الاستخراج.
7. ميزات المحتوى الغني
Section titled “7. ميزات المحتوى الغني”فوق طبقة الملف الشخصي/الاتجاه الموحّدة، تشحن المحرّكات عدة قدرات للمحتوى الغني:
| الميزة | الموضع | ملاحظات |
|---|---|---|
| تلوين الشيفرة/الصياغة | documents/rich_render.py، engines/html.py | تحصل كتل الشيفرة المسيّجة على مخرج ملوّن الصياغة في مساري عرض HTML وPDF كليهما. |
| مخططات XLSX | engines/xlsx.py | BarChart / LineChart (+ أنواع أخرى) مع دعم تعدد السلاسل وعناوين المحاور عبر add_chart. |
| تضمين الصور | engines/docx.py، mutation_worker.py | يمكن تضمين الأصول المعتمدة ضمن النص في عروض DOCX (add_picture) وHTML وPDF (بأسلوب InlineImage). وتُحل مراجع صور Markdown إلى أصول معتمدة. |
| جدول محتويات PDF حقيقي | محرّك PDF | جدول محتويات بنيوي يُولَّد من مخطط العناوين (لا مجرد حقل مخطط). |
| جدول محتويات HTML قابل للنقر | محرّك HTML | جدول محتويات HTML مرتبط بروابط — تقفز مدخلاته إلى أقسامها. |
الأصول المعتمدة فقط. لا يُحلّ مرجع الصورة إلا إلى أصل اعتمده المستدعي لهذه المهمة، منسوخ إلى المهمة مع مجموعه الاختباري (ويرفض العامل أصلًا تغيّر)؛ ويُقرأ عنوان
data:URI في المصدر بياناتٍ مضمَّنة. ولا يُجلب شيء من عنوان URL أو من مسار محلي آخر: يُرفض مصدر HTML أو Markdown يسمّي أحدهما (external_resource_denied)، وتُتخطّى كتلة الصورة التي غاب أصلها، ولا تُضمَّن أبدًا. وهذا يبقي مسار التوليد/التحويل متسقًا مع نموذج أمان ذكاء المستندات (انظر أمان المستندات).
ويمتد التحقق إلى هذه الميزات — أعد تشغيل scripts/verify_documents.py
بعد لمس المحرّكات.

