💡 مثال کامل قابل اجرا در 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 هشت فایل DejaVu و node:18-bookworm شش فایل را برای AWT می‌آورد، به همین دلیل تصاویر JVM و Node متن لاتین را به‌راحتی امضا می‌کنند و فقط وقتی رشتهٔ ژاپنی یا چینی می‌آید، خراب می‌شوند. python:3.11-slim صفر فونت دارد، همانند تصویر زمان‌اجرایی .NET، بنابراین در اولین امضا شکست می‌خورد. هیچ‌یک به‌صورت رایگان CJK را فراهم نمی‌کند.

تأمین فونت در کانتینر گامی است که امضای متن را در یک تصویر لینوکسی با GroupDocs.Signature برای .NET کار می‌کند. این مهم است چون کتابخانهٔ مورد استفاده، خانوادهٔ گمشده را جایگزین نمی‌کند: نام‌گذاری یک فونت که نصب نشده باشد خطا می‌دهد و سندی نمی‌نویسد. این مقاله تصویر بدون فونت را در کنار تصویر اصلاح‌شده قرار می‌دهد، تغییرات را نشان می‌دهد و حل‌مسئلهٔ زمان‌اجرایی را که کد یکسان را روی ماشین توسعه‌دهنده کار می‌کند، پوشش می‌دهد.

راه بهتر وجود دارد

دو شرط باید برقرار باشد. تصویر حداقل یک فونت داشته باشد و کد باید از فرض کردن اینکه کدام فونت است، دست بکشد.

اولین شرط لایهٔ Dockerfile است. دومین شرط گام حل‌مسئله است: به‌جای کدنویسی ثابت Arial، از کتابخانه بپرسید که کدام یک از چندین خانوادهٔ نامزد را می‌تواند واقعاً استفاده کند و اولین موردی که کار می‌کند را نگه دارید. نتیجه بدون تغییر در یک کانتینر slim، روی ویندوز و در CI اجرا می‌شود، چون هرگز دربارهٔ محیطی که بررسی نکرده است ادعایی نمی‌کند.

یک نکته‌ای که کار نمی‌کند و شایستگی بیان واضح دارد چون اولین کاری است که مردم انجام می‌دهند: عدم تنظیم فونت. بدون SignatureFont، GroupDocs.Signature فونت پیش‌فرض خودش یعنی Times New Roman را می‌خواهد، که تصویر بدون فونت نیز آن را ندارد. فراخوانی به همان شکل شکست می‌خورد.

روش جدید: دو تصویر، یک تفاوت

گام ۱ – نگاه کنید تصویر چه دارد

قبل از امضای هر چیزی، فایل‌های فونت را فهرست کنید. شمارش آن‌ها یک استثنای مبهم را به تشخیص تبدیل می‌کند، چون صفر فونت و نام خانوادگی نادرست نیاز به رفع‌های متفاوتی دارند:

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 از .NET 7 به‌بعد فقط برای ویندوز است و در لینوکس خطا می‌دهد، بنابراین کد فونتی که بر پایهٔ آن ساخته شده است، به‌دلیل دلیل دیگری در کانتینر شکست می‌خورد.

گام ۲ – افزودن لایهٔ فونت

چهار بسته، یک 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 را می‌آورد، که همان‌چیزی است که اسناد ساخته‌شده در ویندوز به آن ارجاع می‌دهند. fonts-noto-cjk پوشش‌دهندهٔ چینی، ژاپنی و کره‌ای است.

گام ۳ – حل یک خانواده به‌جای نام‌گذاری یک خانواده

روش قابل حمل برای انتخاب فونت، امتحان یک امضای آزمایشی برای هر نامزد و نگه‌داشتن اولین موردی است که خطا نمی‌دهد:

foreach (string candidate in candidates)
{
    if (TryFamily(sourcePath, candidate).Ok)
    {
        return candidate;
    }
}

return null;

تشخیص بر پایهٔ نام فایل یک میان‌بر جذاب است اما اشتباه است. بستهٔ fonts-noto-cjk در دبیان NotoSansCJK-Regular.ttc را نصب می‌کند که نام خانوادگی آن Noto Sans CJK JP است. تطبیق نام فایل، فونت‌های موجود را از دست می‌دهد و خانواده‌هایی را می‌گیرد که هنگام پاس شدن به SignatureFont حل نمی‌شوند.

گام ۴ – امضای آنچه حل شد، تأیید آنچه امضا کردید

یک خانواده لاتین حل‌شده الزامی است؛ یک خانواده CJK حل‌شده اختیاری است و عدم وجود آن صرفاً یک عبور (skip) است، نه یک سقوط (crash):

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
فایل‌های فونت در تصویر ۰ DejaVu, Liberation, Noto CJK
امضای متن لاتین شکست، خروجی ۳ نوشته شد و پس از خواندن بازخوانی شد
امضای متن CJK شکست نوشته شد و بازخوانی شد
خطای ظاهر شده Font <name> was not found هیچ‌کدام
تفاوت کد هیچ‌کدام – باینری یکسان هیچ‌کدام – باینری یکسان

سطر آخر نکتهٔ اصلی است. هیچ‌یک از برنامه بین دو اجرا تغییر نکرده است. مخزن نمونه هر دو فایل را می‌فرستد تا مقایسه دو فرمان docker build به‌جای اعتماد به‌صورت صرف انجام شود. پس از آن، نسخهٔ بدون فونت را نیز در مخزن نگه دارید: این سریع‌ترین راه برای بازتولید شکست است وقتی کسی شش ماه بعد تصویر پایه را عوض می‌کند و امضاها به‌صورت ساکت متوقف می‌شوند.

چرا همهٔ فونت‌ها را نصب نکنیم؟

چون اندازهٔ تصویر یک محدودیت واقعی است و چهار بستهٔ بالا قبلاً اسکریپت‌های اکثر اسناد را پوشش می‌دهند. تنها fonts-dejavu-core برای امضای لاتین، یونانی و سیریلیک کافی است؛ Liberation زمانی مهم می‌شود که اسناد به نام‌های خانواده‌های ویندوزی ارجاع دهند؛ 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 در تصویر بماند. صفحهٔ نیازمندی‌های سیستم مکان مناسبی برای بررسی پشتیبانی پلتفرم قبل از تعهد به یک تصویر پایه است.

نتیجه‌گیری

سرویسی که به‌صورت محلی کار می‌کند و در Docker شکست می‌خورد، تقریباً همیشه به دلیل فقدان فونت‌هاست و راه‌حل آن یک لایهٔ چهار بسته‌ای به‌همراه کدی است که به‌جای فرض یک خانواده، یک خانواده را حل می‌کند. هر دو تصویر را از نمونه بسازید، آن‌ها را کنار هم اجرا کنید و خطوط [fonts] را بخوانید: تمام استدلال در همان مقایسهٔ یک‌دست قرار می‌گیرد.

منابع تکمیلی