Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fill pdf field or move to attachment - function #1327

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 40 additions & 65 deletions app/domains/prozesskostenhilfe/services/pdf/A_person.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import type { ProzesskostenhilfeFormularContext } from "~/domains/prozesskostenhilfe/formular";
import { maritalDescriptionMapping } from "~/domains/shared/pdf/maritalDescriptionMapping";
import {
type AttachmentEntries,
SEE_IN_ATTACHMENT_DESCRIPTION,
} from "~/services/pdf/attachment";
import { type AttachmentEntries } from "~/services/pdf/attachment";
import { fillPdfFieldOrMoveToAttachment } from "~/services/pdf/fillPdfFieldOrMoveToAttachment";
import type { PkhPdfFillFunction } from ".";

export const GESETZLICHERVERTRETER_FIELD_MAX_CHARS = 80;
export const NAME_VORNAME_FIELD_MAX_CHARS = 35;
export const ANSCHRIFT_FIELD_MAX_CHARS = 50;
export const FAMILIENSTAND_FIELD_MAX_CHARS = 10;
export const BERUF_FIELD_MAX_CHARS = 25;

export const concatenateGesetzlicherVertreterString = ({
gesetzlicheVertretungDaten,
}: ProzesskostenhilfeFormularContext): string => {
Expand Down Expand Up @@ -44,67 +36,50 @@ export const fillPerson: PkhPdfFillFunction = ({ userData, pdfValues }) => {
const gesetzlicherVertreterString =
concatenateGesetzlicherVertreterString(userData);

if (nameVornameString.length > NAME_VORNAME_FIELD_MAX_CHARS) {
attachment.push({
title: "Name, Vorname, ggf. Geburtsname",
text: nameVornameString,
});
pdfValues.nameVornameggfGeburtsname.value = SEE_IN_ATTACHMENT_DESCRIPTION;
} else {
pdfValues.nameVornameggfGeburtsname.value = nameVornameString;
}

if (userData.beruf && userData.beruf.length > BERUF_FIELD_MAX_CHARS) {
attachment.push({
title: "Beruf, Erwerbstätigkeit",
text: userData.beruf,
});
pdfValues.berufErwerbstaetigkeit.value = SEE_IN_ATTACHMENT_DESCRIPTION;
} else {
pdfValues.berufErwerbstaetigkeit.value = userData.beruf;
}
fillPdfFieldOrMoveToAttachment({
pdfFieldName: "nameVornameggfGeburtsname",
pdfFieldValue: nameVornameString,
attachmentTitle: "Name, Vorname, ggf. Geburtsname",
pdfValues,
attachment,
});

fillPdfFieldOrMoveToAttachment({
pdfFieldName: "berufErwerbstaetigkeit",
pdfFieldValue: userData?.beruf,
attachmentTitle: "Beruf, Erwerbstätigkeit",
pdfValues,
attachment,
});

pdfValues.geburtsdatum.value = userData?.geburtsdatum;

const maritalDescription =
maritalDescriptionMapping[userData.partnerschaft ?? ""];
if (maritalDescription.length > FAMILIENSTAND_FIELD_MAX_CHARS) {
attachment.push({
title: "Familienstand",
text: maritalDescription,
});
pdfValues.text3.value = "s.A.";
} else {
pdfValues.text3.value = maritalDescription;
}

if (anschriftString.length > ANSCHRIFT_FIELD_MAX_CHARS) {
attachment.push({
title: "Anschrift (Straße, Hausnummer, Postleitzahl Wohnort)",
text: anschriftString,
});
pdfValues.anschriftStrasseHausnummerPostleitzahlWohnort.value =
SEE_IN_ATTACHMENT_DESCRIPTION;
} else {
pdfValues.anschriftStrasseHausnummerPostleitzahlWohnort.value =
anschriftString;
}
fillPdfFieldOrMoveToAttachment({
pdfFieldName: "text3",
pdfFieldValue: maritalDescriptionMapping[userData.partnerschaft ?? ""],
attachmentTitle: "Familienstand",
pdfValues,
attachment,
});

fillPdfFieldOrMoveToAttachment({
pdfFieldName: "anschriftStrasseHausnummerPostleitzahlWohnort",
pdfFieldValue: anschriftString,
attachmentTitle: "Anschrift (Straße, Hausnummer, Postleitzahl Wohnort)",
pdfValues,
attachment,
});

pdfValues.text2.value = userData?.telefonnummer;

if (
gesetzlicherVertreterString.length > GESETZLICHERVERTRETER_FIELD_MAX_CHARS
) {
attachment.push({
title: "Gesetzlicher Vertreter",
text: gesetzlicherVertreterString,
});
pdfValues.sofernvorhandenGesetzlicherVertreterNameVornameAnschriftTelefon.value =
SEE_IN_ATTACHMENT_DESCRIPTION;
} else {
pdfValues.sofernvorhandenGesetzlicherVertreterNameVornameAnschriftTelefon.value =
gesetzlicherVertreterString;
}
fillPdfFieldOrMoveToAttachment({
pdfFieldName:
"sofernvorhandenGesetzlicherVertreterNameVornameAnschriftTelefon",
pdfFieldValue: gesetzlicherVertreterString,
attachmentTitle: "Gesetzlicher Vertreter",
pdfValues,
attachment,
});

if (attachment.length > 0) {
attachment.unshift({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,10 @@ import type { ProzesskostenhilfeFormularContext } from "~/domains/prozesskostenh
import { maritalDescriptionMapping } from "~/domains/shared/pdf/maritalDescriptionMapping";
import { SEE_IN_ATTACHMENT_DESCRIPTION } from "~/services/pdf/attachment";
import {
ANSCHRIFT_FIELD_MAX_CHARS,
BERUF_FIELD_MAX_CHARS,
concatenateAnschriftString,
concatenateGesetzlicherVertreterString,
concatenateNameVornameString,
fillPerson,
GESETZLICHERVERTRETER_FIELD_MAX_CHARS,
NAME_VORNAME_FIELD_MAX_CHARS,
} from "../A_person";

let pdfParams: ProzesskostenhilfePDF;
Expand Down Expand Up @@ -58,7 +54,9 @@ describe("A_person", () => {
const userDataWithLongString = {
...userData,
nachname: "a".repeat(
NAME_VORNAME_FIELD_MAX_CHARS - userData.nachname!.length + 2, // one more char than NAME_VORNAME_FIELD_MAX_CHARS
(pdfParams.nameVornameggfGeburtsname.maxCharacters ?? 0) -
userData.nachname!.length +
2, // one more char than maxCharacters
),
} as ProzesskostenhilfeFormularContext;

Expand Down Expand Up @@ -91,7 +89,7 @@ describe("A_person", () => {
const userDataWithLongString = {
...userData,
beruf: "a".repeat(
BERUF_FIELD_MAX_CHARS + 1, // one more char than BERUF_FIELD_MAX_CHARS
(pdfParams.berufErwerbstaetigkeit.maxCharacters ?? 0) + 1, // one more char than maxCharacters
),
} as ProzesskostenhilfeFormularContext;

Expand Down Expand Up @@ -160,7 +158,10 @@ describe("A_person", () => {
const userDataWithLongString = {
...userData,
ort: "a".repeat(
ANSCHRIFT_FIELD_MAX_CHARS - userData.ort!.length + 2, // one more char than ANSCHRIFT_FIELD_MAX_CHARS
(pdfParams.anschriftStrasseHausnummerPostleitzahlWohnort
.maxCharacters ?? 0) -
userData.ort!.length +
2, // one more char than maxCharacters
),
} as ProzesskostenhilfeFormularContext;
const anschriftStringLong = concatenateAnschriftString(
Expand Down Expand Up @@ -213,9 +214,11 @@ describe("A_person", () => {
gesetzlicheVertretungDaten: {
...userData.gesetzlicheVertretungDaten,
nachname: "a".repeat(
GESETZLICHERVERTRETER_FIELD_MAX_CHARS -
(pdfParams
.sofernvorhandenGesetzlicherVertreterNameVornameAnschriftTelefon
.maxCharacters ?? 0) -
gesetzlicherVertreterString.length +
2, // one more char than GESETZLICHERVERTRETER_FIELD_MAX_CHARS
2, // one more char than maxCharacters
),
},
} as ProzesskostenhilfeFormularContext;
Expand Down
1 change: 1 addition & 0 deletions app/services/pdf/attachment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export type AttachmentEntries = {
}[];

export const SEE_IN_ATTACHMENT_DESCRIPTION = "Siehe Anhang";
export const SEE_IN_ATTACHMENT_DESCRIPTION_SHORT = "s.A.";
2 changes: 2 additions & 0 deletions app/services/pdf/fileTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export interface BooleanField {
export interface StringField {
name: string;
value?: string;
maxCharacters: number;
maxLineBreaks: number;
}

type JsonField = BooleanField | StringField;
Expand Down
45 changes: 45 additions & 0 deletions app/services/pdf/fillPdfFieldOrMoveToAttachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
SEE_IN_ATTACHMENT_DESCRIPTION,
SEE_IN_ATTACHMENT_DESCRIPTION_SHORT,
type AttachmentEntries,
} from "./attachment";
import type { BooleanField, StringField } from "./fileTypes";

const MIN_CHARACTERS_FOR_LONG_ATTACHMENT_DESCRIPTION = 10;

export function fillPdfFieldOrMoveToAttachment<
T extends Record<string, StringField | BooleanField>,
>({
pdfFieldName,
pdfFieldValue,
attachmentTitle,
pdfValues,
attachment,
}: {
pdfFieldName: keyof T;
pdfFieldValue: string | undefined;
attachmentTitle: string;
pdfValues: T;
attachment: AttachmentEntries;
}): { pdfValues: T; attachment: AttachmentEntries } {
if (!pdfFieldValue) {
return { pdfValues, attachment };
}

const pdfField = pdfValues[pdfFieldName];
const { maxCharacters } =
"maxCharacters" in pdfField ? pdfField : { maxCharacters: 0 };

if (pdfFieldValue.length > maxCharacters) {
pdfValues[pdfFieldName].value =
maxCharacters > MIN_CHARACTERS_FOR_LONG_ATTACHMENT_DESCRIPTION
? SEE_IN_ATTACHMENT_DESCRIPTION
: SEE_IN_ATTACHMENT_DESCRIPTION_SHORT;
attachment.push({ title: attachmentTitle, text: pdfFieldValue });

return { pdfValues, attachment };
}

pdfValues[pdfFieldName].value = pdfFieldValue;
return { pdfValues, attachment };
}
64 changes: 62 additions & 2 deletions app/services/pdf/pdf.generator.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,78 @@
import fs from "node:fs";
import path from "node:path";
import { PDFDocument, PDFCheckBox, PDFTextField, type PDFField } from "pdf-lib";
import type { PDFField } from "pdf-lib";
import {
PDFCheckBox,
PDFDocument,
PDFTextField,
PDFName,
PDFArray,
PDFString,
} from "pdf-lib";
import { pdfs } from "~/services/pdf/pdfs";
import { uppercaseFirstLetter } from "~/util/strings";
import { normalizePropertyName } from "./normalizePropertyName";

const dataDirectory = "data/pdf/";
// For most fonts, the average character width is around 0.5 to 0.6 times the font size.
const CHARACTER_WIDTH_TO_HEIGHT_RATIO = 0.5;
const LINE_HEIGHT_TO_FONT_SIZE_RATIO = 1.2;
const FONT_SIZE_DEFAULT = 12;

function isCheckBoxOrTextField(field: PDFField) {
return field instanceof PDFCheckBox || field instanceof PDFTextField;
}

function fontSizeFromTextField(field: PDFField) {
// DA holds "default appearance settings" in the format like "/Helv 12 Tf 0 g"
// "FontName, fontSize, Operator Texffont, color grescale, set grayscale"
const daEntry = field.acroField.dict.lookup(PDFName.of("DA"));
if (daEntry instanceof PDFString) {
return Number(daEntry.asString().split(" ").at(1));
}
}

function rectangleFromTextField(field: PDFField) {
// Rect holds the rectangle coordinates of the form field
const rect = field.acroField.dict.lookup(PDFName.of("Rect"));

if (rect instanceof PDFArray) return rect.asRectangle();
}

function calculateMaxCharactersAndLinebreaksInRectangle(field: PDFTextField) {
const width = rectangleFromTextField(field)?.width;
const height = rectangleFromTextField(field)?.height;

const textFieldFontSize = fontSizeFromTextField(field);
// font size 0 means auto scale and is replaced with default font size
const fontSize =
!textFieldFontSize || textFieldFontSize === 0
? FONT_SIZE_DEFAULT
: textFieldFontSize;
const characterWidth = fontSize * CHARACTER_WIDTH_TO_HEIGHT_RATIO;

return {
maxCharacters: width ? Math.floor(width / characterWidth) : undefined,
maxLineBreaks: height
? Math.floor(height / (fontSize * LINE_HEIGHT_TO_FONT_SIZE_RATIO))
: undefined,
};
}

function pdfFieldToEntry(field: PDFField) {
return [normalizePropertyName(field.getName()), { name: field.getName() }];
if (field instanceof PDFTextField) {
const { maxCharacters, maxLineBreaks } =
calculateMaxCharactersAndLinebreaksInRectangle(field);
return [
normalizePropertyName(field.getName()),
{
name: field.getName(),
...{ maxCharacters },
...{ maxLineBreaks },
},
];
} else
return [normalizePropertyName(field.getName()), { name: field.getName() }];
}

function pdfFieldToType(field: PDFField) {
Expand Down
Loading