Repository: digitalservicebund/a2j-rechtsantragstelle
Pull request #2030: bug(PKH): allow PDF generation with special characters 
Author: login: ingriddorio
Created: 2025-04-30T12:40:09Z | Merged: 2025-04-30T15:26:51Z

This PR fixes the PDF generation with special characters:
- Register fontkit with PDFDocument in the printNameInSignatureFormField to allow pdf to be generated with special characters like "ń"


Diff:
diff --git a/app/domains/prozesskostenhilfe/services/pdf/__test__/printNameInSignatureFormField.test.ts b/app/domains/prozesskostenhilfe/services/pdf/__test__/printNameInSignatureFormField.test.ts
index 015f61edf5..a5e7de874b 100644
--- a/app/domains/prozesskostenhilfe/services/pdf/__test__/printNameInSignatureFormField.test.ts
+++ b/app/domains/prozesskostenhilfe/services/pdf/__test__/printNameInSignatureFormField.test.ts
@@ -1,7 +1,11 @@
+import fs from "fs";
+import fontkit from "@pdf-lib/fontkit";
 import type { PDFDocument } from "pdf-lib";
 import type { ProzesskostenhilfeFormularContext } from "~/domains/prozesskostenhilfe/formular/context";
 import { printNameInSignatureFormField } from "../printNameInSignatureFormField";
 
+vi.mock("fs");
+vi.mock("@pdf-lib/fontkit");
 vi.mock("pdf-lib", async () => {
   const original = await vi.importActual("pdf-lib");
   return {
@@ -16,22 +20,22 @@ const mockPdfDoc = {
   getPage: vi.fn().mockReturnValue({
     drawText: vi.fn(),
   }),
+  registerFontkit: vi.fn(),
+  embedFont: vi.fn(),
 } as unknown as PDFDocument;
 
-const userData: ProzesskostenhilfeFormularContext = {
-  vorname: "Rosa",
-  nachname: "Ritter",
-  versandArt: "digital",
-};
-
 describe("printNameInSignatureFormField", () => {
   afterEach(() => {
     vi.clearAllMocks();
   });
 
-  it('should call drawText with correct arguments when versandArt is "digital"', () => {
-    printNameInSignatureFormField(mockPdfDoc, userData);
-
+  it('should call drawText with correct arguments when versandArt is "digital"', async () => {
+    const userData: ProzesskostenhilfeFormularContext = {
+      vorname: "Rosa",
+      nachname: "Ritter",
+      versandArt: "digital",
+    };
+    await printNameInSignatureFormField(mockPdfDoc, userData);
     expect(mockPdfDoc.getPage(3).drawText).toHaveBeenCalledWith("Rosa Ritter", {
       x: 200,
       y: 75,
@@ -39,14 +43,43 @@ describe("printNameInSignatureFormField", () => {
     });
   });
 
-  it('should not call drawText when versandArt is "analog"', () => {
+  it('should not call drawText when versandArt is "analog"', async () => {
     const userDataAnalog: ProzesskostenhilfeFormularContext = {
-      ...userData,
+      vorname: "Rosa",
+      nachname: "Ritter",
       versandArt: "analog",
     };
+    await printNameInSignatureFormField(mockPdfDoc, userDataAnalog);
+    expect(mockPdfDoc.getPage(3).drawText).not.toHaveBeenCalled();
+  });
+
+  it("should handle correctly special characters", async () => {
+    const userData: ProzesskostenhilfeFormularContext = {
+      vorname: "Włodzimierz",
+      nachname: "Ćwikłań",
+      versandArt: "digital",
+    };
+    vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from("mock font data"));
 
-    printNameInSignatureFormField(mockPdfDoc, userDataAnalog);
+    await printNameInSignatureFormField(mockPdfDoc, userData);
 
-    expect(mockPdfDoc.getPage(3).drawText).not.toHaveBeenCalled();
+    expect(mockPdfDoc.registerFontkit).toHaveBeenCalledWith(fontkit);
+
+    expect(fs.readFileSync).toHaveBeenCalledWith(
+      "public/fonts/BundesSansWeb-Regular.woff",
+    );
+    expect(mockPdfDoc.embedFont).toHaveBeenCalledWith(
+      Buffer.from("mock font data"),
+      { subset: true },
+    );
+
+    expect(mockPdfDoc.getPage(3).drawText).toHaveBeenCalledWith(
+      "Włodzimierz Ćwikłań",
+      {
+        x: 200,
+        y: 75,
+        size: 10,
+      },
+    );
   });
 });
diff --git a/app/domains/prozesskostenhilfe/services/pdf/index.ts b/app/domains/prozesskostenhilfe/services/pdf/index.ts
index 6fa3357eab..965d6f6d2d 100644
--- a/app/domains/prozesskostenhilfe/services/pdf/index.ts
+++ b/app/domains/prozesskostenhilfe/services/pdf/index.ts
@@ -99,7 +99,7 @@ export async function prozesskostenhilfePdfFromUserdata(
     xPositionsDruckvermerk: 9,
   });
 
-  printNameInSignatureFormField(filledPdfFormDocument, userData);
+  await printNameInSignatureFormField(filledPdfFormDocument, userData);
 
   const filledPdfFormDocumentWithMetadata = addMetadataToPdf(
     filledPdfFormDocument,
diff --git a/app/domains/prozesskostenhilfe/services/pdf/printNameInSignatureFormField.ts b/app/domains/prozesskostenhilfe/services/pdf/printNameInSignatureFormField.ts
index baea1c86e5..0da9b87883 100644
--- a/app/domains/prozesskostenhilfe/services/pdf/printNameInSignatureFormField.ts
+++ b/app/domains/prozesskostenhilfe/services/pdf/printNameInSignatureFormField.ts
@@ -1,15 +1,26 @@
+import fs from "fs";
+import fontkit from "@pdf-lib/fontkit";
 import type { PDFDocument } from "pdf-lib";
 import type { ProzesskostenhilfeFormularContext } from "../../formular/context";
 
-export function printNameInSignatureFormField(
+export async function printNameInSignatureFormField(
   pdfDoc: PDFDocument,
   userData: ProzesskostenhilfeFormularContext,
 ) {
   if (userData.versandArt === "digital") {
+    // registering fontkit with PDFDocument to encode and embed Unicode characters like "ń", "ś", "ł"
+    // without fontkit, pdf-lib will use simple encoding (WinAnsi) and will throw "WinAnsi cannot encode
+    // "ń" when it does not know how to encode
+    pdfDoc.registerFontkit(fontkit);
+    const fontBytes = fs.readFileSync(
+      "public/fonts/BundesSansWeb-Regular.woff",
+    );
+    const font = await pdfDoc.embedFont(fontBytes, { subset: true });
     pdfDoc.getPage(3).drawText(`${userData.vorname} ${userData.nachname}`, {
       x: 200,
       y: 75,
       size: 10,
+      font: font,
     });
   }
 }


Reviews:
user: login: judithmh
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T12:58:34Z
user: login: Spencer6497
body: Should we also do the same for [BerH](https://github.com/digitalservicebund/a2j-rechtsantragstelle/blob/9d7afaa9b6ef24c5a52003122abf506829dfa0d5/app/domains/beratungshilfe/services/pdf/pdfForm/header.ts#L27)?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:05:29Z
user: login: aaschlote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:09:37Z
user: login: ingriddorio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:46:09Z
user: login: aaschlote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:52:31Z

Review comments:
diff_hunk: @@ -1,15 +1,26 @@
+import fs from "fs";
+import fontkit from "@pdf-lib/fontkit";
 import type { PDFDocument } from "pdf-lib";
 import type { ProzesskostenhilfeFormularContext } from "../../formular/context";
 
-export function printNameInSignatureFormField(
+export async function printNameInSignatureFormField(
   pdfDoc: PDFDocument,
   userData: ProzesskostenhilfeFormularContext,
 ) {
   if (userData.versandArt === "digital") {
+    // registering fontkit with PDFDocument to encode and embed Unicode characters like "ń", "ś", "ł"
+    // without fontkit, pdf-lib will use simple encoding (WinAnsi) and will throw "WinAnsi cannot encode
+    // "ń" when it does not know how to encode
+    pdfDoc.registerFontkit(fontkit);
path: app/domains/prozesskostenhilfe/services/pdf/printNameInSignatureFormField.ts
user: login: aaschlote
body: in the function `fillPdf` is already doing it, should you call the method `embedFont` there as well?
created_at: 2025-04-30T13:09:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,15 +1,26 @@
+import fs from "fs";
+import fontkit from "@pdf-lib/fontkit";
 import type { PDFDocument } from "pdf-lib";
 import type { ProzesskostenhilfeFormularContext } from "../../formular/context";
 
-export function printNameInSignatureFormField(
+export async function printNameInSignatureFormField(
   pdfDoc: PDFDocument,
   userData: ProzesskostenhilfeFormularContext,
 ) {
   if (userData.versandArt === "digital") {
+    // registering fontkit with PDFDocument to encode and embed Unicode characters like "ń", "ś", "ł"
+    // without fontkit, pdf-lib will use simple encoding (WinAnsi) and will throw "WinAnsi cannot encode
+    // "ń" when it does not know how to encode
+    pdfDoc.registerFontkit(fontkit);
path: app/domains/prozesskostenhilfe/services/pdf/printNameInSignatureFormField.ts
user: login: ingriddorio
body: I guess it id already being done here:

` const customPdfFormFont = await pdfDoc.embedFont(bundesSansCondensed, {
    features: { liga: false },
  });`
  
  Is that what you mean?
created_at: 2025-04-30T13:46:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,15 +1,26 @@
+import fs from "fs";
+import fontkit from "@pdf-lib/fontkit";
 import type { PDFDocument } from "pdf-lib";
 import type { ProzesskostenhilfeFormularContext } from "../../formular/context";
 
-export function printNameInSignatureFormField(
+export async function printNameInSignatureFormField(
   pdfDoc: PDFDocument,
   userData: ProzesskostenhilfeFormularContext,
 ) {
   if (userData.versandArt === "digital") {
+    // registering fontkit with PDFDocument to encode and embed Unicode characters like "ń", "ś", "ł"
+    // without fontkit, pdf-lib will use simple encoding (WinAnsi) and will throw "WinAnsi cannot encode
+    // "ń" when it does not know how to encode
+    pdfDoc.registerFontkit(fontkit);
path: app/domains/prozesskostenhilfe/services/pdf/printNameInSignatureFormField.ts
user: login: aaschlote
body: Yeah, but can you do there as well? There is embed the `BundesSansCond-DTP-Regular.otf`, maybe it's missing to add the `public/fonts/BundesSansWeb-Regular.woff`.

If you do there, it's a global change, so it won't need to apply for the other parts of the code
created_at: 2025-04-30T13:52:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-30T12:44:00Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=2030) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=2030&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=2030&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=2030&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=2030&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=2030&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=2030)


author_association: NONE
user: login: ingriddorio
created_at: 2025-04-30T13:59:09Z
body: > Should we also do the same for [BerH](https://github.com/digitalservicebund/a2j-rechtsantragstelle/blob/9d7afaa9b6ef24c5a52003122abf506829dfa0d5/app/domains/beratungshilfe/services/pdf/pdfForm/header.ts#L27)?

@Spencer6497  in BerH is being done [here](https://github.com/digitalservicebund/a2j-rechtsantragstelle/blob/9ab5f8308862a2997f2fcc65a8e6528bcea83c27/app/services/pdf/fillPdf.server.ts#L56)

The difference between both implementations are:

PHK uses `printNameInSignatureFormField` function to sign the PDF with name + nachname

BerH uses the fillFunction and the signing happens in the [fillFooter ](https://github.com/digitalservicebund/a2j-rechtsantragstelle/blob/9ab5f8308862a2997f2fcc65a8e6528bcea83c27/app/domains/beratungshilfe/services/pdf/pdfForm/footer.ts#L24)
author_association: CONTRIBUTOR