💡 דוגמה מלאה עובדת זמינה ב‑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 – מספיקים ל‑Latin, ואין שום דבר עבור 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',
];
החצי השני אינו עובד. קבצי גופן כמעט ולא נושאים את מחרוזת המשפחה שהקורא צריך להעביר: fonts-noto-cjk של Debian מתקין 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, ולכן יש לשחזר את ההודעה האמיתית מתוך ערמת ה‑stack העוטפת:
const stack = err.stack || '';
const match = stack.match(/com\.groupdocs\.signature\.exception\.[^\n]*/);
return match ? match[0].trim() : (err.message || String(err));
בלי שני השורות האלה, מכולה ללא גופנים ונתיב JVM פגום מייצרים יומנים זהים. ביליתי יותר זמן ממה שהייתי רוצה להודות בהשוואת שני מכולות שהדפיסו את אותה השגיאה מסיבות שונות לפני שהוספתי את הביטוי הרגולרי.
מה העלות של הסקר
ההתנגדות לסקר היא שהוא כותב קבצים, והוא כן עושה זאת: PDF קטן אחד לכל מועמד, שנמחק מיד. רשימת ה‑Latin בדוגמה מכילה ארבעה ערכים ורשימת ה‑CJK שמונה, ולכן אתחול קר וכתוב לכל היותר שני-עשר מסמכי עמוד אחד לתיקייה הזמנית לפני שהשירות מוכן.
זהו עלות אתחול, לא עלות לכל בקשה, והיא מוסיפה שורת יומן שמציינת את שתי המשפחות שנפתרו. בהשוואה למכולה שמתחילה בצורה נקייה ואז נכשלת במסמך הלקוח הראשון עם שגיאת גשר, שני-עשר קבצים זמניים אינם מסחר מסובך.
השוואת השיטות: מתי להשתמש בכל אחת
| שיטה | מתאים ביותר ל‑ | יתרונות מרכזיים | מגבלות |
|---|---|---|---|
| קידוד קשוח של משפחה | סביבת פיתוח מבוקרת יחידה | טריוויאלי, ללא עלות אתחול | נופל בכל תמונה שאין לה את המשפחה המדויקת |
| גילוי שם קובץ | אבחון מה מכילה תמונה | מהיר, ללא קריאות חתימה | שמות קבצים אינם שמות משפחה, ולכן בחירות נגזרות מהם נכשלות |
| סקר ספרייה | כל מכולה או ניידות | סמכותי, עובד במחשב נייד ובתמונה כאחד | כתיבת PDF אחת לכל מועמד, לכן יש לבצע את הסקר באתחול ולשמור במטמון |
שני ה quirks של הקשירה שכדאי לדעת
לאחר שהמשפחה נפתרת, קריאת החתימה עצמה מקבלת צורה ספציפית ל‑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));
}
ה‑quirk השני הוא קריאת החזרה. TextVerifyOptions אינו עובר סיבוב דרך הקשירה הזו: verify מעלה את אותה שגיאת גשר כללית, ולכן הדוגמה מחזירה ערך סנטינל ומדפיסה unavailable במקום להעמיד פנים שהחתימה נכשלה. חבילת npm היא גרסה 24.12.0, שפורסמה בדצמבר 2024, ומשלבת מנוע 23.6.1 בעוד ש‑.NET ב‑גרסה 26.6 ו‑Java ב‑גרסה 26.5. החתימה אינה מושפעת; רק נתיב האימות חסר.
האם עדיין להשתמש בקשירת Node.js בייצור?
לחתימה רק ב‑Latin, כן: היא חותמת כראוי, וגופן חסר מעלה שגיאה במקום להחליש בשקט, ולכן מצב הכשל רועש. לעבודה עם סקריפטים משולבים, שקול את חוסר קריאת האימות, מכיוון שאין שום שלב בתהליך שיכול לאשר שהגופנים של CJK מוטמעים ולא מוצגים כתיבות. מאמת קטן ב‑.NET או ב‑Java באותו צינור מכסה פער זה.
שיטות עבודה מומלצות וטיפים
- אספקה לפי סדר: JDK ושרשרת כלי, נתיב הטעינה, גופנים, ואז האפליקציה. כל שכבה נופלת בצורה שונה וערבובן מאט את האבחון.
- פותר את המשפחות פעם אחת באתחול ורושם אותן לצד ספירת הגופנים.
- קבע Node 18 ו‑JDK בין 8 ל‑17, והתייחס לשניהם כאל תשתית קבועה ולא כעדכונים שגרתיים.
- שמור את Dockerfile ללא גופנים במאגר, כך שהכשל יישאר מרחק בנייה אחד.
סיכום
שלוש דרכים לבחור גופן, אחת ששרודתה בפריסה. סרוק את הספרייה, שמור במטמון את התשובה, ותן למלאי לשמש כאבחון ולא כהחלטה. לאחר מכן עבוד עם הקשירה כפי שהיא: חתום אפשרות אחת בכל פעם, קרא את חריגת ה‑Java מתוך ערמת ה‑stack, ודווח על חוסר האימות בכנות במקום להסתיר אותו. מאגר הדוגמה בונה את שתי התמונות כך שכל טענה כאן ניתנת לבדיקה בשתי פקודות.