💡 Ví dụ hoạt động đầy đủ có sẵn trên GitHub:
nodejs-docker-signing-with-fonts

Giới thiệu

Font resolution là phần của việc ký container quyết định dịch vụ Node của bạn tạo ra tài liệu hay ngoại lệ. GroupDocs.Signature không thay thế một họ phông chữ bị thiếu: nếu bạn chỉ định một họ mà image không có, lời gọi sẽ gây lỗi, không ghi gì. Xóa phông chữ cũng không phải là giải pháp tạm thời, vì thư viện sau đó sẽ yêu cầu mặc định của nó và cũng sẽ thất bại theo cùng cách.

Có ba cách để quyết định họ phông chữ nào sẽ truyền, và chỉ một trong số chúng tồn tại trong container. Bài viết này so sánh chúng, sau đó đề cập đến việc cung cấp và hành vi binding định hình mã xung quanh chúng, vì Node.js thông qua Java có nhiều hơn cả hai so với bất kỳ nền tảng nào khác mà thư viện này hỗ trợ.

Tại sao điều này quan trọng hơn trên Node.js

Gói này là một cầu nối: node-java tải một JVM trong tiến trình. Vì vậy một image ký Node cần một JDK, công cụ node-gyp để xây dựng cầu nối, và LD_LIBRARY_PATH trỏ tới libjvm.so, tất cả trước khi phông chữ trở nên liên quan. node:18-bookworm sau đó cung cấp 6 tệp phông chữ DejaVu cho AWT - đủ cho Latin, không có gì cho CJK.

Sự kết hợp này tạo ra các lỗi trông giống như lỗi ứng dụng. Một đường dẫn JVM thiếu, một phông chữ thiếu và một sự không khớp trong marshalling đều xuất hiện dưới dạng Error running instance method, vì đó là những gì node-java báo cáo cho bất kỳ ngoại lệ nào được ném từ phía Java.

Yêu cầu trước

Node 18 - cầu nối được xây dựng dựa trên NAN, không biên dịch được với V8 trong Node 20 hoặc 22 ('AccessorSignature' is not a member of 'v8'). JDK 8 đến 17: trên JDK 25 lớp hình ảnh thất bại với Cannot open an image. The image size can not be 0!.

Cài đặt

npm install @groupdocs/groupdocs.signature

Trong image, việc cài đặt này cần có build-essential và python3, cộng thêm openjdk-17-jdk-headless và đường dẫn loader:

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}"

Phương pháp 1 - Ghi cứng tên họ phông chữ

Phiên bản mà mọi người viết đầu tiên: chọn Arial, đóng gói, tiếp tục. Nó hoạt động trên máy của nhà phát triển nhưng thất bại ở lần chạy container đầu tiên, vì các image Debian không cài đặt Arial - chúng cài đặt Liberation Sans, vốn tương thích về metric nhưng có tên họ khác.

Không có mã nào đáng để hiển thị ở đây, và đó là mục đích. Toàn bộ nội dung của phương pháp chỉ là một chuỗi ký tự literal, đúng trong một môi trường.

Phương pháp 2 - Phát hiện phông chữ từ hệ thống tệp

Giải pháp tự nhiên: quét các thư mục phông chữ, xem có gì, chọn một cái. Một nửa của nó thực sự hữu ích - danh sách kiểm kê cho bạn biết image có 0 phông chữ hay 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',
];

Nửa còn lại không hoạt động. Các tệp phông chữ hiếm khi chứa chuỗi họ mà người gọi phải truyền: fonts-noto-cjk của Debian cài đặt NotoSansCJK-Regular.ttc, có họ là Noto Sans CJK JP. Lấy họ từ tên tệp này sẽ cho bạn NotoSansCJK-Regular, mà không khớp được gì. Phát hiện dựa trên tên tệp vừa bỏ lỡ các phông chữ có sẵn vừa báo cáo một cách chắc chắn các họ sẽ thất bại.

Giữ danh sách kiểm kê như một công cụ chẩn đoán. Không dùng nó để lựa chọn. Số lượng trả lời liệu image đã được cung cấp hay chưa, đây là một câu hỏi khác nhưng cũng hữu ích.

Phương pháp 3 - Hỏi thư viện

Cố gắng tạo một chữ ký tạm thời cho mỗi họ phông chữ ứng viên và giữ lại cái đầu tiên không ném lỗi. Nó tốn một lần ghi PDF cho mỗi ứng viên và là phương pháp duy nhất có câu trả lời có thẩm quyền, vì nó là cùng một lời gọi mà chữ ký thực tế sẽ thực hiện.

for (const candidate of candidates) {
  if (tryFamily(sourcePath, candidate) === null) {
    return candidate;
  }
}
return null;

Trên Node, việc kiểm tra cần một đoạn bổ sung. node-java gộp mọi ngoại lệ Java thành Error running instance method, vì vậy thông điệp thực tế phải được khôi phục từ stack trace được bao bọc:

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

Không có hai dòng này, một container không có phông chữ và một đường dẫn JVM bị hỏng sẽ tạo ra các log giống hệt nhau. Tôi đã dành thời gian hơn tôi muốn thừa nhận để so sánh hai container in ra cùng một lỗi vì các lý do hoàn toàn khác nhau trước khi thêm regex.

Chi phí của việc kiểm tra

Phản đối việc kiểm tra là nó ghi file, và thực sự như vậy: một PDF nhỏ cho mỗi ứng viên, bị xóa ngay lập tức. Danh sách Latin trong mẫu có bốn mục và danh sách CJK có tám, vì vậy khi khởi động lạnh sẽ ghi tối đa mười hai tài liệu một trang vào thư mục tạm trước khi dịch vụ sẵn sàng.

Đó là chi phí khởi động, không phải chi phí mỗi yêu cầu, và nó cung cấp một dòng log ghi tên cả hai họ đã được giải quyết. So với một container khởi động sạch sẽ rồi thất bại ở tài liệu khách hàng đầu tiên với lỗi cầu nối, mười hai file tạm không phải là một giao dịch khó khăn.

So sánh các phương pháp: Khi nào nên dùng mỗi phương pháp

Phương pháp Tốt nhất cho Ưu điểm chính Hạn chế
Ghi cứng họ một môi trường kiểm soát duy nhất đơn giản, không tốn chi phí khởi động gây lỗi trên bất kỳ image nào thiếu chính xác họ đó
Phát hiện dựa trên tên tệp chẩn đoán những gì một image chứa nhanh, không có lời gọi ký tên tệp không phải là tên họ, vì vậy các lựa chọn dựa trên chúng sẽ thất bại
Kiểm tra thư viện bất kỳ thứ gì được container hoá hoặc di động có thẩm quyền, hoạt động trên laptop và image giống nhau một lần ghi PDF cho mỗi ứng viên, vì vậy giải quyết khi khởi động và lưu cache

Hai điểm kỳ quặc của binding cần biết

Khi một họ đã được giải quyết, lời gọi ký tự bản thân có dạng đặc thù của Node. API Java nhận một danh sách các tùy chọn, nhưng một mảng JavaScript không được marshal thành java.util.List, vì vậy việc truyền một mảng gây ra Could not find method "sign(java.lang.String, [Ljava.lang.Object;)". Giải pháp là xâu chuỗi overload một tùy chọn duy nhất và qua một file tạm:

new signatureLib.Signature(sourcePath)
  .sign(firstOutput, buildTextOptions(LATIN_TEXT, latinFamily, 50));

if (stageTwo) {
  new signatureLib.Signature(firstOutput)
    .sign(outputPath, buildTextOptions(CJK_TEXT, cjkFamily, 120));
}

Điểm kỳ quặc thứ hai là việc đọc lại. TextVerifyOptions không thực hiện vòng tròn qua binding này: verify gây ra cùng lỗi cầu nối chung, vì vậy mẫu trả về một sentinel và in unavailable thay vì giả vờ chữ ký thất bại. Gói npm có phiên bản 24.12.0, phát hành vào tháng 12 năm 2024, và bao gồm engine 23.6.1 trong khi .NET ở 26.6 và Java ở 26.5. Việc ký không bị ảnh hưởng; chỉ đường kiểm chứng bị thiếu.

Tôi có nên vẫn sử dụng binding Node.js trong môi trường production không?

Đối với ký chỉ Latin, có: nó ký đúng, và một phông chữ thiếu sẽ gây lỗi thay vì giảm chất lượng âm thầm, vì vậy chế độ lỗi sẽ rõ ràng. Đối với công việc hỗn hợp script, cân nhắc việc thiếu khả năng đọc lại, vì không có gì trong quy trình có thể xác nhận các glyph CJK đã được nhúng thay vì hiển thị dưới dạng hộp. Một công cụ kiểm chứng nhỏ trên .NET hoặc Java trong cùng pipeline sẽ lấp đầy khoảng trống đó.

Thực hành tốt nhất và Mẹo

  • Cung cấp theo thứ tự: JDK và toolchain, đường dẫn loader, phông chữ, rồi đến ứng dụng. Mỗi lớp thất bại khác nhau và việc trộn lẫn chúng làm chẩn đoán chậm.
  • Giải quyết các họ một lần khi khởi động và ghi log chúng bên cạnh số lượng phông chữ.
  • Gắn Node 18 và một JDK từ 8 đến 17, và coi cả hai là hạ tầng cố định thay vì nâng cấp thường xuyên.
  • Giữ Dockerfile không có phông chữ trong repository, để lỗi luôn cách một lần build.

Kết luận

Ba cách để chọn phông chữ, một cách tồn tại sau khi triển khai. Kiểm tra thư viện, lưu cache câu trả lời, và để danh sách kiểm kê làm công cụ chẩn đoán thay vì quyết định. Sau đó làm việc với binding như hiện tại: ký một tùy chọn mỗi lần, đọc ngoại lệ Java từ stack trace, và báo cáo việc thiếu kiểm chứng một cách trung thực thay vì giấu đi. Repository mẫu xây dựng cả hai image nên mọi khẳng định ở đây có thể được kiểm tra bằng hai lệnh.

Tài nguyên bổ sung