💡 مثال كامل يعمل متاح على GitHub:
python-linux-container-pdf-signing

المقدمة

يعمل البرنامج النصي محليًا. تقوم بحاويةه على python:3.11-slim، ويحدث فشل عند import groupdocs.signature. تقوم بإصلاح ذلك، ثم يحدث فشل مرة أخرى عند أول توقيع. لا يذكر أي من الخطأ ما هو المفقود فعليًا.

التوقيع داخل الحاوية باستخدام Python هو سير عمل GroupDocs.Signature يحتاج إلى طبقتين من التوفير بدلاً من واحدة: مكتبات تشغيل .NET التي بُنيت عليها الربط، والخطوط التي يجب أن يستخدمها كل توقيع نصي للعرض. يبني هذا الدليل كلا الطبقتين، ثم البرنامج النصي الذي يحدد عائلة الخط في وقت التشغيل بدلاً من ترميزها صلبًا، بحيث يعمل نفس الكود داخل الحاوية وعلى الجهاز الذي كتبت عليه.

لماذا الطبقتان مهمتان

GroupDocs.Signature للـ Python هو ربط .NET، لذا يجب أن تتوفر libicu ومكتبة متوافقة مع OpenSSL 1.1 قبل أن ينجح أي استيراد. هذه هي الطبقة الأولى، وهي موثقة جيدًا في Running in Docker.

السبب في دمج الطبقتين هو أن كلاهما يفشل في لحظات قريبة من الاستيراد ولا يذكر أي منهما سبب الخطأ. نقص libssl1.1 يعطيك خطأ تحميل حول كائن مشترك؛ نقص الخط يعطيك خطأ توقيع ملفوف في استثناء وكيل. لا شيء يقول “صورة القاعدة الأساسية صغيرة جدًا”، وهو ما يعنيه كلا الخطأين فعليًا.

الطبقة الثانية هي الخطوط، وهي التي تفاجئ الناس. يحتوي python:3.11-slim على صفر ملفات خطوط. لا يقوم GroupDocs.Signature باستبدال عائلة مفقودة – تسمية عائلة غير مثبتة يرفع استثناء، ولا يُكتب شيء – وإزالة الخط ليس حلاً بديلًا أيضًا، لأن المكتبة بعد ذلك تطلب الخط الافتراضي الخاص بها وتفشل بنفس الطريقة. على صورة بدون خطوط، يكون التوقيع النصي مستحيلًا.

المتطلبات المسبقة

Python 3.11 (العجلات أدناه لا تتجاوز CPython 3.14) و groupdocs-signature-net==26.1. Docker إذا أردت رؤية كلا الفشلين عمدًا، وهو ما يستغرق حوالي عشر دقائق.

التثبيت

pip install groupdocs-signature-net==26.1

الخطوة 1 – بناء طبقة .NET

libssl1.1 غير موجودة في bookworm، لذا تُستخرج من لقطة Debian مثبتة:

ENV SNAPSHOT_DATE=20220328T000000Z
RUN echo "deb [trusted=yes] http://snapshot.debian.org/archive/debian/${SNAPSHOT_DATE} bullseye main" \
        > /etc/apt/sources.list.d/debian-archive.list \
    && apt-get -o Acquire::Check-Valid-Until=false update \
    && apt-get install -y --no-install-recommends \
        libicu67 \
        libssl1.1 \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

نقاط رئيسية:

  • هذه الطبقة تجعل الاستيراد يعمل فقط؛ لا تتعلق بالخطوط.
  • تثبيت تاريخ اللقطة يحافظ على قابلية إعادة بناء الصورة عندما ينتقل الأرشيف.

الخطوة 2 – بناء طبقة الخطوط

أربع حزم، تُحفظ كطبقة مستقلة حتى يمكن التعليق عليها لإعادة إنتاج الفشل:

RUN apt-get update && apt-get install -y --no-install-recommends \
        fontconfig \
        fonts-dejavu-core \
        fonts-liberation \
        fonts-noto-cjk \
    && fc-cache -f \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

fontconfig هو المُحَلِّل ويعطيك fc-list. fonts-dejavu-core هو الحد الأدنى للخطوط اللاتينية واليونانية والسيريلية. fonts-liberation يغطي المستندات التي تشير إلى Arial أو Times New Roman بالاسم. fonts-noto-cjk يغطي الصينية واليابانية والكورية.

الخطوة 3 – سؤال المكتبة عن العائلة التي يمكنها استخدامها

البحث في /usr/share/fonts عن اسم ملف يبدو مكافئًا لكنه ليس كذلك: fonts-noto-cjk تثبت NotoSansCJK-Regular.ttc، واسم عائلته هو Noto Sans CJK JP. الجواب القابل للنقل هو اختبار – توقيع حقيقي في ملف مؤقت – مع تحويل الفشل إلى قيمة:

with signature.Signature(source_path) as sign:
    options = TextSignOptions()
    options.text = "probe"
    options.left = 10
    options.top = 10
    options.width = 60
    options.height = 20
    font = SignatureFont()
    font.family_name = family_name
    font.size = 10.0
    options.font = font
    sign.sign(scratch, [options])
return None

انظر عن كثب إلى font.size = 10.0. يترجم الربط الحجم إلى float في .NET ويرفض int برسالة numeric argument expected, got 'int'. لأن ذلك يحدث داخل الاختبار، كل عائلة مرشحة تفشل وتظهر النتيجة كما لو كانت صورة بلا خطوط. أضفت ثلاث حزم خطوط إلى صورة كانت تحتوي عليها جميعًا قبل أن ألاحظ الخطأ الحرفي.

الحل يكون حلقة:

for candidate in candidates:
    if try_family(source_path, candidate) is None:
        return candidate
return None

الخطوة 4 – توقيع ما تم حله، والتحقق مما تم توقيعه

العائلة اللاتينية مطلوبة، والعائلة CJK اختيارية:

with signature.Signature(source_path) as sign:
    options = [build_text_options(LATIN_TEXT, latin_family, 50)]
    if cjk_family:
        options.append(build_text_options(CJK_TEXT, cjk_family, 120))
    result = sign.sign(output_path, options)
    return len(result.succeeded)

ثم التحقق، لأن CJK التي تُعرض كمربعات فارغة لا ترفع أي استثناء:

options = TextVerifyOptions()
options.text = expected_text
options.match_type = gsd.TextMatchType.CONTAINS
options.all_pages = True
result = sign.verify(options)

CONTAINS مقصود: في وضع التقييم تضيف المكتبة نصًا تجريبيًا إلى الصفحة، ومطابقة دقيقة ستُبلغ عن مستند صالح كفاشل.

ماذا عن الوثائق التي تقول إن Python يدعم Linux بشكل محدود؟

صفحة Running in Docker تسرد حزم Python الجاهزة لـ Linux وتستثني Signature. على groupdocs-signature-net==26.1 نجح هذا المثال في التوقيع والتحقق داخل python:3.11-slim، بما في ذلك CJK، مع تثبيت كلا الطبقتين. اعتبر القائمة قديمة وليس عائقًا، وتأكد من نسختك الخاصة قبل الالتزام بنشرها.

تطبيقات واقعية

خدمة الفوترة التي تُلصق خط موافقة على ملفات PDF المُولدة تحتاج بالضبط إلى ذلك: طبقة .NET، خط لاتيني واحد، وفحص حل عند بدء التشغيل. الفحص هو ما يحول نشرًا سيئًا إلى حاوية ترفض البدء، بدلاً من طابور فواتير يفشل صامتًا واحدةً تلو الأخرى. بوابة مستندات تقبل أسماء العملاء بأي كتابة تحتاج حزمة CJK أيضًا، بالإضافة إلى خطوة التحقق، لأن هذا هو الشيء الوحيد الذي يمنع ظهور مربع فارغ بدلاً من الاسم الموقّع.

أين يجب وضع فحص الحل

ضعه في أي مكان يُنفّذ مرة واحدة لكل عملية: استدعاء على مستوى الوحدة، معالج FastAPI في lifespan, AppConfig.ready في Django، أو أول أسطر في دالة العامل الرئيسية. ينتج عن الفحص قيمتان، العائلة اللاتينية والعائلة CJK، وكلاهما يُسجَّل في سجل بدء التشغيل بجوار عدد الخطوط.

هذا الموضع لا يوفر وقت الاختبار فقط؛ بل ينقل الفشل من معالجة الطلب—حيث يصبح مشكلة عميل واحد وتتبع الأخطاء لا يقرأه أحد—إلى بدء التشغيل، حيث يصبح فشل النشر واضحًا ومُراقبًا. حاوية تنتهي بـ “no usable font family, install fonts-dejavu-core” لا تحتاج إلى أي تصحيح.

استكشاف المشكلات الشائعة

import groupdocs.signature يفشل
الطبقة .NET مفقودة أو لم يكن مستودع اللقطة متاحًا أثناء البناء. هذه هي الطبقة الأولى، ولا علاقة لها بالخطوط. افحص سجل البناء لخطوة apt قبل لمس أي كود توقيع، لأن فشل جلب اللقطة لا يمنع الصورة من البناء.

كل خط مرشح يفشل، لكن fc-list يظهر خطوطًا
تحقق من أن font.size ليس عددًا صحيحًا قبل إضافة حزم أخرى.

التوقيع موجود لكن نص CJK يظهر كمربعات
fonts-noto-cjk مفقودة. تم كتابة التوقيع بعائلة لا تحتوي على رموز لتلك النقاط، وهذا هو السبب في وجود خطوة التحقق: فهي تفشل في هذه الحالة بالذات، حيث أظهر التوقيع نجاحًا.

ما الذي تطبعه الصورتان فعليًا

شغّل الصورتين واقرأ الأربع أسطر الأولى. الصورة بدون خطوط تُظهر font files on disk: 0، وخطّي الحل كـ (none)، وخطأ الخط المفقود المتعمد، ثم تخرج برمز 3 مع طباعة الإصلاح الأدنى. الصورة المزوَّدة تُظهر عدد خطوط غير صفر، DejaVu Sans لللاتينية وNoto Sans CJK JP للـ CJK، توقيعين مطبقين، وتحقق من النصين.

هذا الزوج من المخرجات هو الأثر الذي يستحق الاحتفاظ به. الصقه في ملاحظات النشر، وسيكون لدى الشخص التالي الذي يغيّر صورة القاعدة مرجع لما تبدو عليه الحاوية السليمة، دون الحاجة لفهم fontconfig على الإطلاق.

الخلاصة

طبقتان واختبار واحد. ثبّت تبعيات .NET، وثبّت على الأقل fontconfig وDejaVu، وحلّ العائلة بالسؤال بدلاً من الافتراض، وتحقق من النتيجة قبل اعتبار المهمة مكتملة. لا شيء من ذلك يتطلب الكثير من الكود، وكل ذلك من الأشياء التي تبدو بديهية بعد الفهم وتختفي في تتبع الأخطاء. المستودع النموذجي يضم كلا ملفي Dockerfile، لذا الفرق بين صورة تعمل وصورة معطوبة هو بناء واحد فقط.

موارد إضافية