💡 ตัวอย่างทำงานเต็มที่พร้อมบน GitHub:
nodejs-docker-signing-with-fonts

บทนำ

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

มีสามวิธีในการกำหนดฟอนต์ที่ต้องส่งเข้าไป และมีเพียงวิธีเดียวที่ยังคงทำงานได้ในคอนเทนเนอร์ บทความนี้จะเปรียบเทียบวิธีเหล่านั้น แล้วอธิบายการจัดหาและพฤติกรรมการผูกที่กำหนดโค้ดรอบ ๆ พวกมัน เพราะ Node.js ผ่าน Java มีทั้งสองอย่างนี้มากกว่าทุกแพลตฟอร์มอื่นที่ไลบรารีนี้รองรับ

ทำไมเรื่องนี้ถึงสำคัญมากกว่าใน Node.js

แพคเกจนี้เป็นสะพาน: node-java โหลด JVM ภายในกระบวนการ ดังนั้นภาพเซ็นชื่อของ Node ต้องมี JDK, toolchain node-gyp เพื่อสร้างสะพาน, และ LD_LIBRARY_PATH ชี้ไปที่ libjvm.so ก่อนที่ฟอนต์จะมีความสำคัญ node:18-bookworm จะให้ไฟล์ฟอนต์ DejaVu จำนวน 6 ตัวสำหรับ AWT – เพียงพอสำหรับ Latin แต่ไม่มีฟอนต์สำหรับ CJK

การผสมผสานนี้ทำให้เกิดความล้มเหลวที่ดูเหมือนบั๊กของแอปพลิเคชัน เส้นทาง JVM ที่หายไป, ฟอนต์ที่หายไปและความไม่ตรงกันของการ marshaling ทั้งหมดปรากฏเป็น 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 ดังนั้นข้อความจริงต้องดึงจากสแตกเทรซที่ห่อหุ้ม:

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

หากไม่มีสองบรรทัดนี้, คอนเทนเนอร์ที่ไม่มีฟอนต์และคอนเทนเนอร์ที่มีเส้นทาง JVM ผิดพลาดจะให้บันทึกเดียวกัน ผมใช้เวลานานกว่าที่อยากยอมรับในการเปรียบเทียบสองคอนเทนเนอร์ที่พิมพ์ข้อผิดพลาดเดียวกันด้วยเหตุผลที่แตกต่างกันก่อนจะเพิ่ม regex นี้

ค่าใช้จ่ายของการตรวจสอบ

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

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

การเปรียบเทียบวิธีการ: ควรใช้เมื่อไหร่

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

สองจุดบกพร่องของการผูกที่ควรรู้

เมื่อฟอนต์ถูกแก้ไขแล้ว, การเรียกเซ็นเองก็มีรูปแบบเฉพาะของ Node API ของ Java รับรายการตัวเลือก, แต่ array ของ JavaScript ไม่ได้ marshaled ไปเป็น java.util.List, ทำให้การส่งหนึ่งรายการทำให้เกิด Could not find method "sign(java.lang.String, [Ljava.lang.Object;)" วิธีแก้คือเชน overload ที่รับตัวเลือกเดียวและผ่านไฟล์ชั่วคราว:

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 ไม่สามารถ round‑trip ผ่านการผูกนี้ได้: verify ยกข้อผิดพลาดสะพานทั่วไปเช่นเดียวกัน, ดังนั้นตัวอย่างจึงคืนค่า sentinel และพิมพ์ unavailable แทนที่จะทำให้ดูเหมือนลายเซ็นล้มเหลว แพคเกจ npm มีเวอร์ชัน 24.12.0, เผยแพร่ในเดือนธันวาคม 2024, และบรรจุเอนจิน 23.6.1 ขณะที่ .NET อยู่ที่ 26.6 และ Java ที่ 26.5 การเซ็นไม่ได้รับผลกระทบ; เพียงเส้นทางการตรวจสอบที่หายไป

ควรใช้การผูก Node.js ในการผลิตหรือไม่?

สำหรับการเซ็นแบบ Latin‑only, ใช่: มันเซ็นได้อย่างถูกต้องและฟอนต์ที่หายไปจะทำให้เกิดข้อผิดพลาดแทนการลดคุณภาพอย่างเงียบ ๆ, ดังนั้นโหมดความล้มเหลวจึงชัดเจน สำหรับงานที่ต้องใช้สคริปต์ผสม, พิจารณาข้อจำกัดของการอ่านกลับ, เพราะไม่มีขั้นตอนใดในกระบวนการที่สามารถยืนยันว่า glyphs ของ CJK ถูกฝังไว้หรือแสดงเป็นกล่อง ตัวตรวจสอบขนาดเล็กบน .NET หรือ Java ใน pipeline เดียวกันจะเติมช่องว่างนี้ได้

แนวปฏิบัติที่ดีที่สุดและเคล็ดลับ

  • จัดเตรียมตามลำดับ: JDK และ toolchain, เส้นทางโหลด, ฟอนต์, แล้วจึงแอป แต่ละชั้นล้มเหลวด้วยสาเหตุที่แตกต่างกันและการผสมผสานทำให้การวินิจฉัยช้า
  • แก้ไขฟอนต์ครั้งเดียวตอนเริ่มต้นและบันทึกพร้อมกับจำนวนฟอนต์
  • ค้าง Node 18 และ JDK ระหว่าง 8 ถึง 17, และถือว่าเป็นโครงสร้างพื้นฐานคงที่ ไม่ใช่การอัปเกรดประจำ
  • เก็บ Dockerfile ที่ไม่มีฟอนต์ไว้ใน repository, เพื่อให้ความล้มเหลวอยู่ห่างแค่การ build หนึ่งครั้ง

สรุป

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

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