💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
sign-pdf-in-linux-container-fonts-dotnet

הדרך הישנה הייתה כואבת

השירות חותם חשבוניות. הוא רץ על מחשב נייד עם שלוש מאות גופנים מותקנים, עובר ביקורת, ומקבל קונטיינר ביום שישי. ביום שני, המשימה הראשונה במאגר יוצאת עם קוד חזרה שונה מאפס עם Sign document error: Font Arial was not found, ומישהו מבלה את הבוקר בקריאת stack traces לפני שמישהו חושב לשאול אילו גופנים באמת קיימים בתמונה 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, ולכן הוא נכשל בחתימה הראשונה במקום זאת. אף אחד לא מקבל CJK בחינם באף אחת מהן.

הקצאת גופנים במכולה היא הצעד שמאפשר לחתום טקסט בתמונה לינוקס עם 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 והלאה וזורק שגיאה בלינוקס, ולכן קוד גופנים שנבנה עליו נכשל במכולה מסיבה שנייה, בלתי קשורה.

שלב 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;

זיהוי קובץ הוא קיצור דרך מפתה והוא שגוי. fonts-noto-cjk של Debian מתקין 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);

צד לצד: לפני vs. אחרי

Dockerfile.nofonts Dockerfile
קבצי גופן בתמונה 0 DejaVu, Liberation, Noto CJK
חתימת טקסט לטיני נכשלת, יציאה 3 נכתבת ומשוחזרת בקריאת‑חזרה
חתימת טקסט CJK נכשלת נכתבת ומשוחזרת
השגיאה שהוצגה Font <name> was not found אין
הבדל בקוד אין – אותו בינארי אין – אותו בינארי

השורה האחרונה היא העיקר. שום דבר באפליקציה לא השתנה בין שני ההרצות. מאגר הדוגמה כולל את שני הקבצים ולכן ההשוואה מתבצעת באמצעות שני פקודות 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]: כל הטיעון נכנס להשוואה הזאת.

משאבים נוספים