تخطَّ إلى المحتوى
kazma.
البحث في كاظمه والتوثيق
EN

العرض الموحّد للمستندات

دليل مرجعي المصدر لطبقة العرض الموحّدة للمستندات في كاظمه. كل صيغة إخراج تمرّ عبر DocProfile واحد، فيتشارك ملف DOCX وPDF وHTML وMarkdown وXLSX وPPTX المولَّد تصميمًا واحدًا ومعالجة صحيحة للكتابة من اليمين إلى اليسار — أيًّا كان الامتداد أو اللغة.


قبل هذه الطبقة، كان لكل صيغة مُعرِضها الخاص بسمته الخاصة، وكشفه الخاص للاتجاه، ومعالجته الخاصة لـ RTL. فنشأ صنفان من الأخطاء:

  • الانحراف — تباعدت الألوان والخطوط والتسميات بين الصيغ؛ فبدا ملف DOCX عربي وملف PDF عربي كأنهما منتجان مختلفان.
  • أخطاء RTL المتكررة — أُصلح خطأ الاتجاه/المحاذاة نفسه خمس مرات (مرة لكل صيغة) وظل يعود.

تضع الطبقة الموحّدة دلالات الاتجاه + التصميم + المحاذاة في مكان واحد (DocProfile) وتجعل كل محرّك صيغة إسقاطًا رقيقًا له. فخطأ في المحاذاة العربية يُصلح الآن في دالة واحدة، لا في خمس.

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.pyw: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.pysheet_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/المعايير.

يمرّ كل توليد وكل تحويل 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. إضافة محرّك صيغة جديد”
  1. أنشئ kazma_core/documents/engines/<format>.py بصنف يأخذ DocProfile ويطبّق render(...). اقرأ الاتجاه/السمة من الملف الشخصي (profile.rtl، وprofile.theme، وسياسة المحاذاة) — ولا تُعِد كشف الاتجاه أبدًا ولا تثبّت الألوان في الكود.
  2. اربط _generate_<format>(output, payload) في renderer_worker.py بحيث يبني ملفًا شخصيًا (عبر _build_model_and_profile لأشكال المستندات، أو DocProfile.for_content للحمولات ذات الشكل الأصلي) ويستدعي المحرّك.
  3. أضف العملية إلى سجل المُعرِضات (kazma_core/documents/renderers/__init__.py) وإلى _MIME/_EXTENSIONS. وإن احتاج إلى حزمة Python أو ملف تنفيذي للنظام، فأضفه إلى صورة العامل أيضًا (deploy/document-worker/requirements.in / deploy/document-worker/Dockerfile): ففي ملف التعريف container تكون المحرّكات الموجودة هي التي تبلّغ عنها الصورة.
  4. أضف اختبارًا إلى tests/test_unified_document_layer.py يؤكد أن المحرّك يستهلك الملف الشخصي (الاتجاه + رمز من السمة)، ووسّع scripts/verify_documents.py.

إن كانت الصيغة ذات شكل مستند (عنوان + أقسام)، فأعد استخدام ContentModel. وإن كان لها شكلها الخاص (كالأوراق/الشرائح)، فاحتفظ بتلك الحمولة لكن استهلك DocProfile — التوحيد على مستوى الملف الشخصي، لا نموذج المحتوى.

يولّد 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 إنجليزية وعربية وثنائية اللغة بهذه المحرّكات ثم تحلّلها عائدةً، فيظهر هناك أي تغيير في العرض يكسر الاستخراج.


فوق طبقة الملف الشخصي/الاتجاه الموحّدة، تشحن المحرّكات عدة قدرات للمحتوى الغني:

الميزةالموضعملاحظات
تلوين الشيفرة/الصياغةdocuments/rich_render.py، engines/html.pyتحصل كتل الشيفرة المسيّجة على مخرج ملوّن الصياغة في مساري عرض HTML وPDF كليهما.
مخططات XLSXengines/xlsx.pyBarChart / 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 بعد لمس المحرّكات.