💡 ตัวอย่างการทำงานเต็มที่พร้อมใช้งานบน GitHub:
sign-pdf-in-linux-container-fonts-dotnet

วิธีเดิมทำให้เจ็บปวด

บริการนี้ทำการเซ็นใบแจ้งหนี้ มันทำงานบนแล็ปท็อปที่ติดตั้งฟอนต์สามร้อยแบบ ผ่านการตรวจสอบแล้วและถูกทำให้เป็นคอนเทนเนอร์ในวันศุกร์ ในวันจันทร์งานแรกในคลัสเตอร์จบด้วยสถานะไม่ศูนย์และแสดงข้อความ Sign document error: Font Arial was not found และมีคนใช้เวลาตลอดเช้าวิเคราะห์ stack trace ก่อนที่ใครจะคิดถามว่าภาพ mcr.microsoft.com/dotnet/runtime:8.0 มีฟอนต์อะไรบ้างจริง ๆ

คำตอบคือไม่มีเลย 0 ไฟล์ฟอนต์ เมื่อวัดจากภาพที่ตัวอย่างในบทความนี้รันอยู่

การรู้ว่ารันไทม์อื่น ๆ เปรียบเทียบอย่างไรจึงมีประโยชน์ เพราะความล้มเหลวจะแสดงต่างกันในแต่ละภาพ eclipse-temurin:17-jre มีไฟล์ DejaVu 8 ตัวและ node:18-bookworm มี 6 ตัว ทั้งสองสำหรับ AWT ทำให้ภาพ JVM และ Node เซ็นข้อความละตินได้อย่างเงียบ ๆ และล้มเหลวเฉพาะเมื่อมีสตริงญี่ปุ่นหรือจีน python:3.11-slim ไม่มีฟอนต์เลย เหมือนกับภาพ .NET runtime จึงล้มเหลวที่ลายเซ็นแรกเลย ไม่มีคอนเทนเนอร์ใดที่ให้ CJK ฟรี

การจัดหาฟอนต์ในคอนเทนเนอร์เป็นขั้นตอนที่ทำให้การเซ็นข้อความทำงานในภาพ Linux กับ GroupDocs.Signature for .NET มันสำคัญเพราะไลบรารีไม่ทำการแทนที่ฟอนต์ที่หายไป: การระบุชื่อฟอนต์ที่ไม่ได้ติดตั้งจะทำให้เกิดข้อผิดพลาดและไม่เขียนเอกสารเลย บทความนี้จะวางภาพที่ไม่มีฟอนต์ข้าง ๆ ภาพที่แก้ไขแล้ว แสดงสิ่งที่เปลี่ยนแปลง และอธิบายการแก้ไขเวลารันที่ทำให้โค้ดเดียวกันทำงานได้บนเครื่องพัฒนา

มีวิธีที่ดีกว่า

ต้องเป็นจริงสองอย่าง ภาพต้องมีฟอนต์อย่างน้อยหนึ่งตัว และโค้ดต้องหยุดสมมติว่าฟอนต์ใดฟอนต์หนึ่ง

อย่างแรกคือเลเยอร์ Dockerfile อย่างที่สองคือขั้นตอนการแก้ไข: แทนที่จะกำหนดค่า Arial อย่างตายตัว ให้ถามไลบรารีว่าครอบครัวฟอนต์ใดจากหลายตัวเลือกที่สามารถใช้ได้จริง แล้วเก็บตัวแรกที่ทำงานได้ ผลลัพธ์จะทำงานโดยไม่เปลี่ยนแปลงในคอนเทนเนอร์แบบ slim, บน Windows, และใน CI เพราะโค้ดไม่อ้างอิงสภาพแวดล้อมใด ๆ ที่ไม่ได้ตรวจสอบ

สิ่งที่ไม่ทำงานและควรบอกอย่างชัดเจนเพราะเป็นสิ่งแรกที่คนมักลองทำคือ ไม่ตั้งค่า SignatureFont หากไม่มี SignatureFont GroupDocs.Signature จะขอใช้ค่าเริ่มต้นของมันเองคือ Times New Roman ซึ่งภาพที่ไม่มีฟอนต์ก็ไม่มี Times New Roman ด้วย การเรียกก็จะล้มเหลวเช่นเดียวกัน

วิธีใหม่: สองภาพ หนึ่งความแตกต่าง

ขั้นตอนที่ 1 – ดูว่าภาพมีอะไรบ้าง

ก่อนเซ็นอะไรเลย ให้แสดงรายการไฟล์ฟอนต์ จำนวนไฟล์ทำให้ข้อยกเว้นที่คลุมเครือกลายเป็นการวินิจฉัย เพราะ 0 ฟอนต์และชื่อครอบครัวที่ผิดต้องแก้ต่างกัน:

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 เป็นฟอนต์ขั้นต่ำสำหรับ Latin, Greek และ Cyrillic fonts-liberation ให้ตัวแทนที่มีเมตริกเข้ากันได้กับ Arial, Times New Roman และ Courier New ซึ่งเป็นฟอนต์ที่เอกสารที่สร้างบน Windows มักอ้างอิง fonts-noto-cjk ครอบคลุม Chinese, Japanese และ Korean

ขั้นตอนที่ 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 – เซ็นสิ่งที่แก้ไขแล้ว ตรวจสอบสิ่งที่คุณเซ็น

ต้องมีครอบครัว Latin ที่แก้ไขแล้ว; ครอบครัว 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
ลายเซ็นข้อความ Latin ล้มเหลว, exit 3 เขียนสำเร็จและอ่านกลับได้
ลายเซ็นข้อความ CJK ล้มเหลว เขียนสำเร็จและอ่านกลับได้
ข้อความข้อผิดพลาดที่แสดง Font <name> was not found ไม่มี
ความแตกต่างของโค้ด ไม่มี – ไบนารีเดียวกัน ไม่มี – ไบนารีเดียวกัน

แถวสุดท้ายคือประเด็นสำคัญ ไม่มีอะไรในแอปพลิเคชันเปลี่ยนแปลงระหว่างสองการรัน ตัวอย่างรีโพสิตอรีส่งมาพร้อมไฟล์ทั้งสองจึงต้องใช้สองคำสั่ง docker build เพื่อเปรียบเทียบ ไม่ใช่แค่เชื่อถือไว้ เก็บเวอร์ชันที่ไม่มีฟอนต์ไว้ในรีโพสิตอรีต่อไปด้วย เพราะมันเป็นวิธีที่เร็วที่สุดในการทำให้เกิดความล้มเหลวเมื่อใครสักคนเปลี่ยนฐานภาพหกเดือนต่อจากนี้และลายเซ็นหยุดปรากฏโดยเงียบ ๆ

ทำไมไม่ติดตั้งฟอนต์ทุกตัว?

เพราะขนาดภาพเป็นข้อจำกัดจริง ๆ และสี่แพ็กเกจข้างต้นครอบคลุมสคริปต์ที่เอกสารส่วนใหญ่ใช้ fonts-dejavu-core เพียงอย่างเดียวก็พอสำหรับการเซ็น Latin, Greek และ Cyrillic; Liberation มีความสำคัญเมื่อเอกสารอ้างอิงครอบครัวฟอนต์ของ Windows ด้วยชื่อ; Noto CJK เป็นแพ็กเกจที่ใหญ่จริง ๆ และจ่ายค่าใช้จ่ายเองเฉพาะเมื่อคุณเซ็นข้อความเอเชียตะวันออก ติดตั้งเฉพาะฟอนต์ที่เอกสารของคุณต้องการ แล้วตรวจสอบด้วยการอ่านกลับ

ตัวอย่างจากโลกจริง: Worker เซ็นแบบแบตช์

Worker คิวเซ็น PDF หลายพันไฟล์ต่อคืน ด้วยการแก้ไขที่เริ่มต้น มันจะบันทึกบรรทัดเดียวที่ระบุครอบครัวฟอนต์ที่ใช้ และหากไม่มีอะไรแก้ได้ก็ออกจากการทำงานก่อนจะจับคิวแทนที่จะล้มเหลวต่อข้อความ การตรวจสอบตอนเริ่มต้นนี้ทำให้ปัญหาฟอนต์เปลี่ยนจากกระแสของงานล้มเหลวเป็นคอนเทนเนอร์ที่ไม่เริ่มทำงานด้วยเหตุผลบรรทัดเดียว

ค่าใช้จ่ายของการสำรวจเล็กพอที่จะละเลยที่การเริ่มต้นและใหญ่เกินกว่าจะทำซ้ำต่อเอกสารแต่ละไฟล์ การสำรวจแต่ละครั้งคือการเซ็นจริงลงไฟล์ชั่วคราว ดังนั้นรายการ Latin มีค่าใช้จ่ายสูงสุดสี่ครั้งและรายการ CJK สูงสุดแปดครั้ง ทั้งหมดต่อ PDF หนึ่งหน้า ทำการแก้ไขครั้งเดียว แคชชื่อครอบครัวสองตัว แล้วเส้นทางต่อเอกสารก็เหมือนเดิม: สร้าง options, เรียก 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") ให้เปิดใช้งาน globalization ไว้และให้ ICU อยู่ในภาพ หน้า system requirements คือที่ตรวจสอบการสนับสนุนแพลตฟอร์มก่อนเลือกฐานภาพ

สรุป

บริการเซ็นที่ทำงานในเครื่องท้องถิ่นแต่ล้มเหลวใน Docker ส่วนใหญ่มักขาดฟอนต์ และวิธีแก้คือเพิ่มเลเยอร์สี่แพ็กเกจพร้อมโค้ดที่แก้ไขครอบครัวแทนการสมมติว่ามีฟอนต์หนึ่งตัว สร้างภาพทั้งสองจากตัวอย่าง รันข้าง ๆ กัน แล้วดูบรรทัด [fonts] ทั้งหมดสรุปได้ในเปรียบเทียบเดียวนี้

แหล่งข้อมูลเพิ่มเติม