💡 مثال كامل يعمل متوفر على GitHub:
nodejs-docker-signing-with-fonts

المقدمة

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

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

لماذا يهم هذا أكثر على Node.js

الحزمة هي جسر: node-java يحمل JVM داخل العملية. لذا تحتاج صورة توقيع Node إلى JDK، وسلسلة أدوات node-gyp لبناء الجسر، وLD_LIBRARY_PATH يشير إلى libjvm.so، كل ذلك قبل أن تصبح الخطوط ذات صلة. ثم يضيف node:18-bookworm 6 ملفات خط DejaVu لـ AWT - تكفي للغات اللاتينية، ولا شيء للغات CJK.

هذا الجمع ينتج أخطاء تبدو كأخطاء تطبيق. مسار JVM مفقود، أو خط مفقود، أو عدم توافق في التحويل كلها تظهر كـ Error running instance method، لأن هذا ما تقوله node-java عن أي شيء يُرمى على جانب Java.

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

Node 18 - الجسر يُبنى ضد NAN، الذي لا يتوافق مع V8 في Node 20 أو 22 ('AccessorSignature' is not a member of 'v8'). JDK من 8 إلى 17: على JDK 25 طبقة التصوير تفشل بـ Cannot open an image. The image size can not be 0!.

التثبيت

npm install @groupdocs/groupdocs.signature

في الصورة، يحتاج هذا التثبيت إلى وجود build-essential و python3، بالإضافة إلى openjdk-17-jdk-headless ومسار التحميل:

ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ENV PATH="${JAVA_HOME}/bin:${PATH}"
# node-java dlopens libjvm.so at run time; it is not on the default loader path.
ENV LD_LIBRARY_PATH="${JAVA_HOME}/lib/server:${LD_LIBRARY_PATH}"

الطريقة 1 - ترميز اسم العائلة صراحةً

الإصدار الذي يكتبه الجميع أولًا: اختر Arial، استخدمه، وانتقل. يعمل على جهاز المطور ويفشل في أول تشغيل للحاوية، لأن صور Debian لا تثبت Arial - بل تثبت Liberation Sans، وهو متوافق من حيث المقاييس تحت اسم عائلة مختلف.

لا يوجد كود يستحق العرض هنا، وهذا هو المقصود. محتوى الطريقة بالكامل هو سلسلة حرفية تصدق في بيئة واحدة فقط.

الطريقة 2 - اكتشاف الخطوط من نظام الملفات

الإصلاح الطبيعي: مسح دلائل الخطوط، ومعرفة ما هو موجود، ثم اختيار شيء. نصفها مفيد حقًا - الجرد يخبرك ما إذا كانت الصورة تحتوي على 0 خطوط أو 6:

const roots = [
  '/usr/share/fonts',
  '/usr/local/share/fonts',
  path.join(home, '.fonts'),
  path.join(home, '.local', 'share', 'fonts'),
  '/System/Library/Fonts',
  '/Library/Fonts',
];

النصف الآخر لا يعمل. ملفات الخط نادرًا ما تحمل سلسلة العائلة التي يجب على المستدعي تمريرها: حزمة Debian fonts-noto-cjk تثبت NotoSansCJK-Regular.ttc، وعائلتها هي Noto Sans CJK JP. استخراج عائلة من اسم الملف يعطيك NotoSansCJK-Regular، وهو لا يُحل إلى شيء. اكتشاف اسم الملف يفوت الخطوط الموجودة ويُبلغ بثقة عن عائلات ستفشل.

احتفظ بالجرد كأداة تشخيص. لا تستخدمه للاختيار. العدد يجيب ما إذا كانت الصورة قد تم تهيئتها أصلاً، وهو سؤال مختلف ومفيد بنفس القدر.

الطريقة 3 - سؤال المكتبة

حاول إنشاء توقيع تجريبي لكل عائلة مرشحة واحتفظ بالأولى التي لا تُرمى استثناءً. يكلف ذلك كتابة PDF واحد لكل مرشح وهو الطريقة الوحيدة التي يكون جوابها موثوقًا، لأنها نفس الاستدعاء الذي سيُجريه التوقيع الحقيقي.

for (const candidate of candidates) {
  if (tryFamily(sourcePath, candidate) === null) {
    return candidate;
  }
}
return null;

على Node يحتاج الفحص قطعة إضافية. node-java يدمج كل استثناء Java في Error running instance method، لذا يجب استخراج الرسالة الحقيقية من تتبع المكدس المغلف:

const stack = err.stack || '';
const match = stack.match(/com\.groupdocs\.signature\.exception\.[^\n]*/);
return match ? match[0].trim() : (err.message || String(err));

بدون هذين السطرين، حاوية بدون خطوط وحاوية بمسار JVM مكسور ينتجان سجلات متطابقة. قضيت وقتًا أطول مما أريد الاعتراف به في مقارنة حاويتين طبعتا نفس الخطأ لأسباب مختلفة تمامًا قبل إضافة التعبير النمطي.

ما يكلفه الفحص

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

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

مقارنة الطرق: متى تستخدم كل واحدة

الطريقة الأنسب لـ المزايا الرئيسية القيود
Hard-coded family بيئة واحدة مُتحكم فيها بسيط، لا تكلفة بدء تشغيل يتعطل في أي صورة لا تحتوي على تلك العائلة بالضبط
Filename detection تشخيص ما تحتويه الصورة سريع، لا استدعاءات توقيع أسماء الملفات ليست أسماء عائلات، لذا الاختيارات المستخلصة منها تفشل
Library probing أي حاوية أو تطبيق قابل للنقل موثوق، يعمل على اللاب توب والصورة على حد سواء كتابة PDF واحد لكل مرشح، لذا يُفضَّل الحل عند بدء التشغيل وتخزين النتيجة مؤقتًا

الخصيستان في الربط التي تستحق المعرفة

بمجرد حل العائلة، يصبح استدعاء التوقيع نفسه له شكل خاص بـ Node. API Java يأخذ قائمة من الخيارات، لكن مصفوفة JavaScript لا تُحوَّل إلى java.util.List، لذا تمرير واحدة ينتج Could not find method "sign(java.lang.String, [Ljava.lang.Object;)". الحل هو ربط التحميل الأحادي للخيارات والمرور عبر ملف مؤقت:

new signatureLib.Signature(sourcePath)
  .sign(firstOutput, buildTextOptions(LATIN_TEXT, latinFamily, 50));

if (stageTwo) {
  new signatureLib.Signature(firstOutput)
    .sign(outputPath, buildTextOptions(CJK_TEXT, cjkFamily, 120));
}

الخصيصة الثانية هي القراءة العكسية. TextVerifyOptions لا تُعيد المسار عبر هذا الربط: verify يرفع نفس خطأ الجسر العام، لذا تُعيد العينة قيمة sentinel وتطبع unavailable بدلاً من التظاهر بأن التوقيع فشل. حزمة npm إصدارتها 24.12.0، نُشرت في ديسمبر 2024، وتضم محرك 23.6.1 بينما .NET في الإصدار 26.6 وJava في 26.5. التوقيع غير متأثر؛ فقط مسار التحقق مفقود.

هل يجب أن أستمر في استخدام ربط Node.js في الإنتاج؟

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

أفضل الممارسات والنصائح

  • قم بالتهيئة بالترتيب: JDK وسلسلة الأدوات، مسار التحميل، الخطوط، ثم التطبيق. كل طبقة تفشل بطريقة مختلفة وخلطها يجعل التشخيص بطيئًا.
  • حل العائلات مرة واحدة عند بدء التشغيل وسجّلها بجانب عدد الخطوط.
  • ثبّت Node 18 وJDK بين 8 و17، واعتبرهما بنية ثابتة بدلاً من ترقيات روتينية.
  • احتفظ بملف Dockerfile الخالي من الخطوط في المستودع، حتى يبقى الفشل على بعد بناء واحد.

الخلاصة

ثلاث طرق لاختيار خط، واحدة فقط تنجو من النشر. افحص المكتبة، خزن النتيجة، ودع الجرد يكون أداة تشخيص وليس قرارًا. ثم اعمل مع الربط كما هو: وقع خيارًا واحدًا في كل مرة، استخرج استثناء Java من تتبع المكدس، وبلغ عن عدم وجود التحقق بصدق بدلاً من إخفائه. مستودع العينة يبني الصورتين بحيث يمكن التحقق من كل ادعاء هنا بأمرين.

موارد إضافية