💡 مثال كامل يعمل متاح على GitHub:
sign-pdf-in-linux-container-fonts-dotnet
الطريقة القديمة كانت مؤلمة
الخدمة توقع الفواتير. تعمل على لابتوب يحتوي على ثلاثمائة خط مثبت، تجتاز المراجعة، وتُحزم في حاوية يوم الجمعة. في يوم الاثنين، تنتهي أول مهمة في العنقود بخطأ غير صفري مع Sign document error: Font Arial was not found، ويقضي أحدهم الصباح في قراءة تتبع الأخطاء قبل أن يفكر أحد في سؤال ما الخطوط التي يحتويها صورة mcr.microsoft.com/dotnet/runtime:8.0 فعليًا.
الإجابة هي لا شيء. لا ملفات خطوط، حسب القياس على الصورة التي يعمل فيها عينة هذا المقال.
من المفيد معرفة كيف تقارن بيئات التشغيل الأخرى، لأن الفشل يختلف في كل منها. eclipse-temurin:17-jre يحتوي على 8 ملفات DejaVu و node:18-bookworm يحتوي على 6، كلاهما لـ AWT، وهذا هو السبب في أن صور JVM و Node توقع النص اللاتيني بهدوء ولا تتعطل إلا عندما يصل نص ياباني أو صيني. python:3.11-slim لا يحتوي على أي خطوط، مثل صورة .NET runtime، لذا يفشل عند التوقيع الأول. لا أحد يحصل على CJK مجانًا في أي منها.
توفير الخطوط في الحاوية هو الخطوة التي تجعل توقيع النص يعمل في صورة Linux مع GroupDocs.Signature for .NET. هذا مهم لأن المكتبة لا تستبدل عائلة الخط المفقودة: تسمية خط غير مثبت يثير خطأ ولا يكتب أي مستند. يضع هذا المقال الصورة بدون خطوط بجانب الصورة المعدلة، يوضح ما تغير، ويغطي حل وقت التشغيل الذي يبقي نفس الكود يعمل على جهاز المطور.
هناك طريقة أفضل
هناك شرطان يجب أن يتحققا. تحتاج الصورة إلى خط واحد على الأقل، ويجب على الكود التوقف عن افتراض أي خط هو.
الأول هو طبقة في Dockerfile. الثاني هو خطوة حل: بدلاً من كتابة Arial صراحةً، اسأل المكتبة أي من العائلات المرشحة يمكنها فعليًا الاستخدام، واحتفظ بالأول الذي يعمل. النتيجة تعمل دون تغيير في حاوية خفيفة، على Windows، وفي CI، لأنها لا تفترض أي شيء عن البيئة التي لم تتحقق منها.
شيء واحد لا يعمل، ومن الجدير ذكره بوضوح لأنه أول ما يجربه الناس: ترك الخط غير محدد. بدون SignatureFont، يطلب GroupDocs.Signature الخط الافتراضي الخاص به، Times New Roman، والذي تفتقر إليه الصورة بدون خطوط أيضًا. الفعل يفشل بنفس الطريقة.
الطريقة الجديدة: صورتان، فرق واحد
الخطوة 1 - انظر ما تحتويه الصورة
قبل توقيع أي شيء، قم بإدراج ملفات الخطوط. العدد يحول الاستثناء الغامض إلى تشخيص، لأن عدم وجود خطوط واسم عائلة خاطئ يحتاجان إلى إصلاحات مختلفة:
string[] roots =
{
"/usr/share/fonts",
"/usr/local/share/fonts",
Path.Combine(home, ".fonts"),
Path.Combine(home, ".local/share/fonts"),
Environment.GetFolderPath(Environment.SpecialFolder.Fonts),
"/System/Library/Fonts",
"/Library/Fonts",
};
لاحظ ما هو غائب: System.Drawing. System.Drawing.Common متاح فقط على Windows بدءًا من .NET 7 فصاعدًا ويثير استثناءً على Linux، لذا فشل كود الخط المبني عليه في الحاوية لسبب ثانٍ غير مرتبط.
الخطوة 2 - إضافة طبقة الخطوط
أربعة حزم، أمر RUN واحد، وتختفي المشكلة:
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 و Courier New، وهي ما تشير إليها المستندات التي تم إنشاؤها على Windows فعليًا. fonts-noto-cjk يغطي الصينية واليابانية والكورية.
الخطوة 3 - حل عائلة بدلاً من تسميتها
الطريقة القابلة للنقل لاختيار خط هي محاولة توقيع تجريبي لكل مرشح والاحتفاظ بالأول الذي لا يثير استثناءً:
foreach (string candidate in candidates)
{
if (TryFamily(sourcePath, candidate).Ok)
{
return candidate;
}
}
return null;
اكتشاف اسم الملف هو الاختصار المغري لكنه خاطئ. حزمة Debian fonts-noto-cjk تثبت NotoSansCJK-Regular.ttc، الذي اسم عائلته هو Noto Sans CJK JP. مطابقة اسم الملف تتخطى الخطوط الموجودة وتدعي عائلات لن تُحل عندما تُمرّر إلى SignatureFont.
الخطوة 4 - توقع ما تم حله، وتحقق مما توقعته
عائلة لاتينية محلولة مطلوبة؛ عائلة CJK محلولة اختيارية وغيابها يعني تخطيًا، لا تعطلًا:
var options = new List<SignOptions>
{
BuildTextOptions(LatinText, latinFamily, top: 50),
};
if (cjkFamily is not null)
{
options.Add(BuildTextOptions(CjkText, cjkFamily, top: 120));
}
SignResult result = signature.Sign(outputPath, options);
ثم اقرأ الملف مرة أخرى، لأن CJK بدون خط CJK قد يُظهر مربعات فارغة دون إثارة أي خطأ على الإطلاق:
var options = new TextSearchOptions { AllPages = true };
List<TextSignature> found = signature.Search<TextSignature>(options);
جنبًا إلى جنب: قبل مقابل بعد
Dockerfile.nofonts |
Dockerfile |
|
|---|---|---|
| ملفات الخطوط في الصورة | 0 | DejaVu, Liberation, Noto CJK |
| توقيع النص اللاتيني | يفشل، خروج 3 | مكتوب وتم استرجاعه عند القراءة مرة أخرى |
| توقيع النص CJK | يفشل | مكتوب وتم استرجاعه |
| الخطأ الظاهر | Font <name> was not found |
none |
| اختلاف الكود | none - same binary | none - same binary |
الصف الأخير هو النقطة. لم يتغير شيء في التطبيق بين التشغيلين. يُرسل مستودع العينة كلا الملفين بحيث يتطلب المقارنة أمرين docker build بدلاً من الاعتماد على الثقة. احتفظ بالنسخة بدون خطوط في المستودع أيضًا: فهي أسرع طريقة لإعادة إنتاج الفشل عندما يغيّر أحدهم صور الأساس بعد ستة أشهر ويتوقف ظهور التوقيعات بهدوء.
لماذا لا نثبت كل الخطوط؟
لأن حجم الصورة هو قيد حقيقي، والحزم الأربعة المذكورة أعلاه تغطي بالفعل النصوص التي تستخدمها معظم المستندات. fonts-dejavu-core وحده يكفي لتوقيع النصوص اللاتينية واليونانية والسيريلية؛ Liberation مهم عندما تشير المستندات إلى عائلات Windows بالاسم؛ Noto CJK هو الأكبر فعليًا ولا يدفع إلا إذا كنت توقع نصًا شرق آسيوي. قم بتثبيت ما تحتاجه مستنداتك، ثم تحقق من ذلك بالقراءة مرة أخرى.
مثال واقعي: عامل التوقيع الدفعي
عامل قائمة الانتظار يوقع بضع آلاف ملفات PDF كل ليلة. مع حل العائلة عند بدء التشغيل، يسجل سطرًا واحدًا يذكر العائلات التي سيستخدمها، وإذا لم يتم حل أي منها يخرج قبل لمس القائمة بدلاً من الفشل لكل رسالة. هذا الفحص عند بدء التشغيل هو ما يحول مشكلة الخط من تدفق وظائف فاشلة إلى حاوية ترفض البدء بسبب سبب سطر واحد.
تكلفة الفحص صغيرة بما يكفي لتجاهلها عند بدء التشغيل وكبيرة جدًا لتكرارها لكل مستند. كل فحص هو توقيع حقيقي يُكتب إلى ملف مؤقت، لذا قائمة اللاتينية تكلف حتى أربعة منها وقائمة CJK حتى ثمانية، جميعها على ملف PDF من صفحة واحدة. حل مرة واحدة، خزن اسم العائلتين في الذاكرة، والمسار لكل مستند هو بالضبط ما كان عليه من قبل: بناء الخيارات، استدعاء Sign، قراءة عدد النتائج.
أضعت بعد ظهرًا على النسخة التي كانت تخمن. قامت بمسح دليل الخطوط، وجدت NotoSansCJK-Regular.ttc، وأبلغت أن CJK متاح، ثم فشلت في كل اسم عائلة استخرجته من اسم الملف هذا. الفحص بتوقيع حقيقي كان أبسط وصحيحًا.
ما الذي يسبب مشاكل أخرى في الحاوية؟
واحدة أخرى، ولا علاقة لها بالخطوط: InvariantGlobalization=true. هي نصيحة قياسية لتقليص ICU من صورة .NET، ومع GroupDocs.Signature تجعل أول استدعاء new Signature(...) يرمي CultureNotFoundException: ... en-US is an invalid culture identifier، لأن SignatureSettings يبني CultureInfo("en-US"). حافظ على تمكين العولمة ودع ICU يبقى في الصورة. صفحة system requirements هي المكان للتحقق من دعم المنصة قبل الالتزام بصورة أساسية.
الخلاصة
خدمة التوقيع التي تعمل محليًا وتفشل في Docker تكون في الغالب تفتقر إلى الخطوط، والحل هو طبقة من أربع حزم بالإضافة إلى كود يحل عائلة بدلاً من افتراض واحدة. ابنِ الصورتين من العينة، شغّلهما جنبًا إلى جنب، واقرأ أسطر [fonts]: الحجة بأكملها تتناسب مع تلك المقارنة الواحدة.