import { PDFDocument, StandardFonts, rgb } from 'pdf-lib' import fs from 'fs' async function create() { const pdfDoc = await PDFDocument.create() const page = pdfDoc.addPage([595.28, 841.89]) // A4 const helv = await pdfDoc.embedFont(StandardFonts.Helvetica) const helvBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) const { width, height } = page.getSize() // left column moved further left to align with checkboxes const leftX = 48 const rightX = 320 // baseY is the vertical anchor for page-1 content. Increase it by ~5.2mm (≈14.739pt) const baseY = height - 160 + 14.739 // shift to apply only to labels and input fields (move those 1cm down) const labelsShift = -28.35 const gap = 36 const labelWidth = 100 const fieldXOffset = 100 // Make fields 10% narrower const fieldWidth = Math.round(180 * 0.9) // shrink most fields by another 20% on request (applied selectively later) const fieldShrinkFactor = 0.8 const fieldHeight = 14 // raise date fields by 0.4cm (≈11.34pt) when requested; add extra 1.2mm (≈3.4016pt) per user request const dateRaise = 11.34 + 3.4016 // now ≈14.7416pt // fixed checkbox positions (do not move with leftX) const cbX = 48 const cbYOffset = 5 // Header: centered club name and a full-width horizontal bar underneath (~2mm high) const headerText = 'Harheimer Tischtennis-Club 1954 e.V.' const headerSize = 20 const textWidth = helv.widthOfTextAtSize(headerText, headerSize) const headerX = (width - textWidth) / 2 const headerY = height - 72 page.drawText(headerText, { x: headerX, y: headerY, size: headerSize, font: helv }) // draw full-width bar directly under the header; 2mm ≈ 5.67 points const barHeight = 5.67 const barY = headerY - 20 page.drawRectangle({ x: 0, y: barY, width: width, height: barHeight, color: rgb(0,0,0) }) // Labels and lines // Labels left-aligned in their columns // Add form title above the fields const titleText = 'Beitrittserklärung' const titleSize = 14 const titleWidth = helvBold.widthOfTextAtSize(titleText, titleSize) // left-align title above the left labels and move up ~0.7cm (≈20pt) const titleX = leftX // move title down by 0.5cm (≈14.17pt) const titleY = baseY + 24 + 20 - 14.17 page.drawText(titleText, { x: titleX, y: titleY, size: titleSize, font: helvBold }) const subtitle = 'Hiermit beantrage ich,' const subtitleSize = 12 const subtitleX = leftX const subtitleY = titleY - 18 - 14.17 page.drawText(subtitle, { x: subtitleX, y: subtitleY, size: subtitleSize, font: helv }) // apply same vertical shift as fields so labels align const fieldsShift = -14.17 page.drawText('Name:', { x: leftX, y: baseY + fieldsShift + labelsShift, size: 12, font: helv }) page.drawText('Vorname:', { x: rightX, y: baseY + fieldsShift + labelsShift, size: 12, font: helv }) page.drawText('Straße:', { x: leftX, y: baseY - gap + fieldsShift + labelsShift, size: 12, font: helv }) page.drawText('PLZ/Wohnort:', { x: rightX, y: baseY - gap + fieldsShift + labelsShift, size: 12, font: helv }) page.drawText('Geburtsdatum:', { x: leftX, y: baseY - gap * 2 + fieldsShift + labelsShift, size: 12, font: helv }) page.drawText('Telefon(privat):', { x: rightX, y: baseY - gap * 2 + fieldsShift + labelsShift, size: 12, font: helv }) page.drawText('E-Mail:', { x: leftX, y: baseY - gap * 3 + fieldsShift + labelsShift, size: 12, font: helv }) page.drawText('Telefon(Mobil):', { x: rightX, y: baseY - gap * 3 + fieldsShift + labelsShift, size: 12, font: helv }) // Create form fields const form = pdfDoc.getForm() // Place fields on the same baseline as their labels // We need to move only the input fields on page 1 up by 0.6cm (≈17.01pt) without moving labels. const labelToFieldYDelta = 2 // small vertical offset so field baseline matches label visually const lift = 0 // original lift value // previously raised inputs by 17.01pt (0.6cm); move them down by 5.67pt (0.2cm) const inputFieldRaise = 11.34 // net upward offset now ~11.34pt form.createTextField('nachname').addToPage(page, { x: leftX + fieldXOffset, y: baseY - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) form.createTextField('vorname').addToPage(page, { x: rightX + fieldXOffset, y: baseY - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) form.createTextField('strasse').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) form.createTextField('plz_ort').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) form.createTextField('geburtsdatum').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap * 2 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) form.createTextField('telefon').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap * 2 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) form.createTextField('email').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap * 3 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) form.createTextField('telefon_mobil').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap * 3 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv }) // read membership amounts from config (fall back to defaults) let erw = 120, jug = 72, passv = 30 try { const cfg = JSON.parse(fs.readFileSync('server/data/config.json', 'utf8')) const ms = cfg.mitgliedschaft || [] erw = (ms.find(m => /Erwachsene/i.test(m.typ)) || ms.find(m => m.typ === 'Erwachsene') || {}).preis || erw jug = (ms.find(m => /Jugend|Kinder/i.test(m.typ)) || ms.find(m => m.typ === 'Kinder/Jugend') || {}).preis || jug passv = (ms.find(m => /Passiv/i.test(m.typ)) || {}).preis || passv } catch (e) { console.error('Could not read config for membership amounts', e) } const paraLines = [ 'Den derzeitigen jährlichen Mitgliedsbeitrag in Höhe von', `€ ${erw},-- (Erwachsene)`, `€ ${jug},-- (Jugendliche bis zum vollendeten 18. Lebensjahr)`, `€ ${passv},-- (passive Mitglieder)`, 'bitte ich per Lastschrift jährlich von meinem Konto einzuziehen.', 'Hierzu erteile ich beigefügtes SEPA-Lastschriftmandat.', 'Mir ist bekannt, dass die Mitgliedschaft im Harheimer Tischtennis-Club erst nach Bestätigung durch den', 'Vorstand Wirksamkeit erlangt. Die Beitragspflicht beginnt mit dem darauf folgenden Monat.', 'Ich erkenne die Vereinssatzung (erhältlich beim Vorstand bzw. auf der Vereinshomepage) an.' ] // compute intermediate line position and move it down by additional 0.3cm ≈ 8.5pt const intermediateY = baseY - gap * 3 + fieldsShift - 6 - 14.17 - 8.5 - 8.5 + labelsShift page.drawText('meine Aufnahme in den Harheimer Tischtennis-Club 1954 e.V. als', { x: leftX, y: intermediateY, size: 12, font: helv }) // membership checkboxes // Keep checkbox labels and boxes at fixed left positions, but move them down by 0.8cm ≈ 22.68pt const cbLift = -22.68 page.drawText('aktives Mitglied', { x: cbX + 16, y: baseY - gap * 5 + cbLift + labelsShift, size: 12, font: helv }) page.drawText('passives Mitglied', { x: cbX + 16, y: baseY - gap * 6 + cbLift + labelsShift, size: 12, font: helv }) form.createCheckBox('mitglied_aktiv').addToPage(page, { x: cbX, y: baseY - gap * 5 - cbYOffset + cbLift + labelsShift, width: 12, height: 12 }) form.createCheckBox('mitglied_passiv').addToPage(page, { x: cbX, y: baseY - gap * 6 - cbYOffset + cbLift + labelsShift, width: 12, height: 12 }) // place the paragraph below the checkboxes with extra spacing before and after the cost lines (~0.3cm) try { // increase gap around cost lines for better readability (~0.6cm) const beforeCostGap = 17 const afterCostGap = 17 const paraStartY = baseY - gap * 6 + cbLift - 28 - beforeCostGap const lineHeight = 14 // insert explicit gaps before and after the cost lines (lines 1-3) let y = paraStartY // first line page.drawText(paraLines[0], { x: leftX, y: y, size: 11, font: helv }) y -= lineHeight // gap before costs y -= beforeCostGap // cost lines (1..3) for (let k = 1; k <= 3; k++) { page.drawText(paraLines[k], { x: leftX + 6, y: y, size: 11, font: helv }) y -= lineHeight } // gap after costs y -= afterCostGap // remaining lines for (let i = 4; i < paraLines.length; i++) { page.drawText(paraLines[i], { x: leftX, y: y, size: 11, font: helv }) y -= lineHeight } // shift following content down by afterCostGap if needed (none after currently) // now add signature/city line 2 cm below the current text const twoCm = 56.7 const signY = y - twoCm const sigText = 'Frankfurt/Main-Harheim, den' const sigSize = 12 page.drawText(sigText + ' ', { x: leftX, y: signY, size: sigSize, font: helv }) const sigStartX = leftX + helv.widthOfTextAtSize(sigText + ' ', sigSize) const sigWidth = 220 // draw thin line for signature page.drawRectangle({ x: sigStartX, y: signY - 2, width: sigWidth, height: 1, color: rgb(0,0,0) }) // draw 'Datum' centered under the signature line const datum = 'Datum' const datumSize = 11 const datumX = sigStartX + sigWidth / 2 - helv.widthOfTextAtSize(datum, datumSize) / 2 page.drawText(datum, { x: datumX, y: signY - 18, size: datumSize, font: helv }) // create a date form field on page 1 centered under the date line const dateFieldWidth = 120 const dateFieldX = sigStartX + sigWidth / 2 - dateFieldWidth / 2 // position date field so its bottom edge is exactly on the signature line const signatureLineY = signY - 2 // the 1pt-high rectangle was drawn at signY-2 // raise date fields by 0.4cm (≈11.34pt) upward relative to the line const dateRaise = 11.34 // For page 1 we need to move only the date input up by 5.2mm (≈14.739pt) const signDatumExtraRaise = 14.739 form.createTextField('sign_datum').addToPage(page, { x: dateFieldX, y: signatureLineY - fieldHeight + signDatumExtraRaise, width: dateFieldWidth, height: fieldHeight, font: helv }) // second signature line 3cm below first const threeCm = 85.04 const secondY = signY - threeCm const line2Width = 300 page.drawRectangle({ x: leftX, y: secondY - 2, width: line2Width, height: 1, color: rgb(0,0,0) }) const label2 = 'Unterschrift (bei Jugendlichen gesetzlicher Vertreter)' page.drawText(label2, { x: leftX, y: secondY - 18, size: 11, font: helv }) } catch (e) { // ignore } // footer: right-aligned 'Seite 1 von 3' 2cm from bottom const footerY = 56.7 const footerText = 'Seite 1 von 3' const footerSize = 10 const footerWidth = helv.widthOfTextAtSize(footerText, footerSize) page.drawText(footerText, { x: width - footerWidth - leftX, y: footerY, size: footerSize, font: helv }) // --- Add a second page with the same header and horizontal bar --- const page2 = pdfDoc.addPage([595.28, 841.89]) const { width: width2, height: height2 } = page2.getSize() const textWidth2 = helv.widthOfTextAtSize(headerText, headerSize) const headerX2 = (width2 - textWidth2) / 2 const headerY2 = height2 - 72 page2.drawText(headerText, { x: headerX2, y: headerY2, size: headerSize, font: helv }) const barY2 = headerY2 - 20 page2.drawRectangle({ x: 0, y: barY2, width: width2, height: barHeight, color: rgb(0,0,0) }) // --- Page 2: SEPA-Lastschriftmandat text and fields --- // move SEPA section slightly up so title sits closer to the header bar const sepaYStart = headerY2 - 54 const sepaLeft = leftX // increase line gap for SEPA section (~22pt) const lineGap = 22 let sy = sepaYStart const small = 11 page2.drawText('Erteilung eines SEPA-Lastschriftmandates', { x: sepaLeft, y: sy, size: 12, font: helvBold }) sy -= lineGap // draw these two lines as a tight block (no extra gap) page2.drawText('Harheimer Tischtennis-Club 1954 e.V.', { x: sepaLeft, y: sy, size: small, font: helv }) sy -= 12 page2.drawText('Unsere Gläubiger-Identifikationsnummer: DE46ZZZ00000745362', { x: sepaLeft, y: sy, size: small, font: helv }) // add 0.7cm vertical gap (≈19.8pt) before the authorization paragraph sy -= 19.8 // draw the authorization text as wrapped lines within page margins const wrapMaxWidth = width2 - sepaLeft - 48 function drawWrapped(text, x, y, size, font) { const words = text.split(' ') let line = '' let curY = y for (const w of words) { const test = line ? line + ' ' + w : w const testWidth = helv.widthOfTextAtSize(test, size) if (testWidth > wrapMaxWidth) { page2.drawText(line, { x, y: curY, size, font }) line = w curY -= size + 4 } else { line = test } } if (line) { page2.drawText(line, { x, y: curY, size, font }) curY -= size + 4 } return curY } sy = drawWrapped('Hiermit ermächtige ich den Harheimer Tischtennis-Club 1954 e.V. die jährlichen Mitgliedsbeiträge von meinem untenstehenden Konto per Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die vom Harheimer Tischtennis-Club 1954 e.V. auf mein Konto gezogenen Lastschriften einzulösen.', sepaLeft, sy, small, helv) sy -= lineGap * 0.2 // draw mandate reference and validity with a small gap page2.drawText('Mandatsreferenz: HTC0000 _ _ _', { x: sepaLeft, y: sy, size: small, font: helv }) // add 0.7cm space after Mandatsreferenz as requested sy -= 19.8 page2.drawText('Dieses Mandat gilt für die zugrundeliegende Beitrittserklärung ab sofort.', { x: sepaLeft, y: sy, size: small, font: helv }) sy -= lineGap * 0.6 // Draw labeled lines and create text fields for mandate details const fieldHeight2 = 14 const fieldWidth2 = 380 const labelOffset = 0 // place SEPA inputs 1cm left (≈28.35pt) from previous and slightly up; adjust to align with labels const inputX = sepaLeft + 220 - 28.35 // reduce vertical raise so inputs sit on same baseline as labels (previously too high) const inputYAdjust = 6 + 5.67 // Move SEPA inputs (except date) up by additional 2mm (≈5.67pt) // adjust page2DownShift so inputs move up by 0.75mm (net) // 0.75mm = 0.075cm -> ~2.12625 pt const page2DownShift = 0.075 * 28.35 // 0.075 cm -> ~2.12625 pt // apply labelsShift only to the SEPA form labels/inputs so paragraphs remain unaffected let syFields = sy + labelsShift page2.drawText('Mitglied (Vorname und Name)', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) form.createTextField('sepa_mitglied').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust - page2DownShift, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) syFields -= lineGap * 1.1 page2.drawText('Kontoinhaber (Vorname und Name):', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) form.createTextField('sepa_kontoinhaber').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust - page2DownShift, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) syFields -= lineGap * 1.1 page2.drawText('Straße und Hausnummer:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) form.createTextField('sepa_strasse').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust - page2DownShift, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) syFields -= lineGap * 1.1 page2.drawText('PLZ und Ort:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) form.createTextField('sepa_plz_ort').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust - page2DownShift, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) syFields -= lineGap * 1.1 page2.drawText('Kreditinstitut:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) form.createTextField('sepa_bank').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust - page2DownShift, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) syFields -= lineGap * 1.1 page2.drawText('IBAN:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) form.createTextField('sepa_iban').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust - page2DownShift, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv }) syFields -= lineGap * 1.1 page2.drawText('BIC:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv }) // BIC remains full width as requested form.createTextField('sepa_bic').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust - page2DownShift, width: 220, height: fieldHeight2, font: helv }) syFields -= lineGap * 1.1 // add signature and date lines 2cm below last field const twoCm = 56.7 const signY2 = syFields - twoCm // date text and line const sigDateText = 'Frankfurt/Main-Harheim, den' page2.drawText(sigDateText + ' ', { x: sepaLeft, y: signY2, size: 12, font: helv }) const sigDateStartX = sepaLeft + helv.widthOfTextAtSize(sigDateText + ' ', 12) const sigDateWidth = 160 page2.drawRectangle({ x: sigDateStartX, y: signY2 - 2, width: sigDateWidth, height: 1, color: rgb(0,0,0) }) page2.drawText('Datum', { x: sigDateStartX + sigDateWidth / 2 - helv.widthOfTextAtSize('Datum', 11) / 2, y: signY2 - 18, size: 11, font: helv }) // create a date form field on page 2 centered under the date line const sepaDateFieldWidth = 120 const sepaDateFieldX = sigDateStartX + sigDateWidth / 2 - sepaDateFieldWidth / 2 // position sepa date field so its bottom edge is on the signature line // raise SEPA date field by the same amount so its top/bottom alignment matches requested position form.createTextField('sepa_datum').addToPage(page2, { x: sepaDateFieldX, y: signY2 - 2 - fieldHeight2 + dateRaise, width: sepaDateFieldWidth, height: fieldHeight2, font: helv }) // footer on page 2: right-aligned 'Seite 2 von 3' 2cm from bottom const footerText2 = 'Seite 2 von 3' const footerSize2 = 10 const footerWidth2 = helv.widthOfTextAtSize(footerText2, footerSize2) page2.drawText(footerText2, { x: width2 - footerWidth2 - leftX, y: 56.7, size: footerSize2, font: helv }) // signature line // move signature label/line 2cm (≈56.7pt) further down const signLineY = signY2 - 36 - 56.7 const signLineWidth = 300 page2.drawRectangle({ x: sepaLeft, y: signLineY - 2, width: signLineWidth, height: 1, color: rgb(0,0,0) }) page2.drawText('Unterschrift des Kontoinhabers', { x: sepaLeft, y: signLineY - 18, size: 11, font: helv }) // no form field for signature on page 2 (physical signature) page2.drawText('Unterschrift des Kontoinhabers', { x: sepaLeft, y: signLineY - 18, size: 11, font: helv }) // no form field for signature on page 2 (physical signature) // --- Add a third page with same header/bar/footer and title 'Einwilligungserklärung' --- const page3 = pdfDoc.addPage([595.28, 841.89]) const { width: width3, height: height3 } = page3.getSize() const headerX3 = (width3 - textWidth) / 2 const headerY3 = height3 - 72 page3.drawText(headerText, { x: headerX3, y: headerY3, size: headerSize, font: helv }) const barY3 = headerY3 - 20 page3.drawRectangle({ x: 0, y: barY3, width: width3, height: barHeight, color: rgb(0,0,0) }) // title for page 3 const page3Title = 'Einwilligungserklärung' const page3TitleSize = 14 const page3TitleX = leftX const page3TitleY = headerY3 - 48 page3.drawText(page3Title, { x: page3TitleX, y: page3TitleY, size: page3TitleSize, font: helvBold }) // footer on page 3 const footerText3 = 'Seite 3 von 3' const footerSize3 = 10 const footerWidth3 = helv.widthOfTextAtSize(footerText3, footerSize3) page3.drawText(footerText3, { x: width3 - footerWidth3 - leftX, y: 56.7, size: footerSize3, font: helv }) // --- consent paragraphs on page 3 (wrapped) --- const consentText = `für die Veröffentlichung von Mitgliederdaten im Internet. Der Vereinsvorstand weist hiermit darauf hin, dass ausreichende technische Maßnahmen zur Gewährleistung des Datenschutzes getroffen wurden. Dennoch kann bei einer Veröffentlichung von personenbezogenen Mitgliederdaten im Internet ein umfassender Datenschutz nicht garantiert werden. Daher nimmt das Vereinsmitglied die Risiken für eine eventuelle Persönlichkeitsrechtsverletzung zur Kenntnis und ist sich bewusst, dass: • die personenbezogenen Daten auch in Staaten abrufbar sind, die keine der Bundesrepublik Deutschland vergleichbaren Datenschutzbestimmungen kennen, • die Vertraulichkeit, die Integrität (Unverletzlichkeit), die Authentizität (Echtheit) und die Verfügbarkeit der personenbezogenen Daten nicht garantiert ist. Das Vereinsmitglied trifft die Entscheidung zur Veröffentlichung seiner Daten im Internet freiwillig und kann seine Einwilligung gegenüber dem Vereinsvorstand jederzeit widerrufen.` // helper for wrapped text on page3 const wrapMaxWidth3 = width3 - leftX - 48 function drawWrappedOnPage3(text, x, y, size, font) { // handle bullet lines: split by newline to preserve existing bullet markers const lines = text.split(/\r?\n/) let curY = y const indent = 14 // indent for text following bullet for (const rawLine of lines) { const line = rawLine.trim() if (!line) { curY -= size + 4; continue } if (line.startsWith('•')) { // draw bullet circle and then wrap remainder with indentation const after = line.replace(/^•\s?/, '') // bullet position const bx = x + 4 // place bullet aligned with the first text line (slightly above center) const by = curY - size * 0.25 page3.drawCircle({ x: bx, y: by, size: 1.8, color: rgb(0,0,0) }) // wrap the 'after' text with indent const words = after.replace(/\s+/g, ' ').trim().split(' ') let curLine = '' for (const w of words) { const test = curLine ? curLine + ' ' + w : w const testWidth = helv.widthOfTextAtSize(test, size) if (testWidth > wrapMaxWidth3 - indent) { page3.drawText(curLine, { x: x + indent, y: curY, size, font }) curLine = w curY -= size + 4 } else { curLine = test } } if (curLine) { page3.drawText(curLine, { x: x + indent, y: curY, size, font }) curY -= size + 4 } } else { // regular paragraph line: wrap normally const words = line.replace(/\s+/g, ' ').trim().split(' ') let curLine = '' for (const w of words) { const test = curLine ? curLine + ' ' + w : w const testWidth = helv.widthOfTextAtSize(test, size) if (testWidth > wrapMaxWidth3) { page3.drawText(curLine, { x, y: curY, size, font }) curLine = w curY -= size + 4 } else { curLine = test } } if (curLine) { page3.drawText(curLine, { x, y: curY, size, font }) curY -= size + 4 } } } return curY } // split into paragraphs and draw with 0.5cm (~14.17pt) paragraph spacing const paras = consentText.split('\n\n') let py = page3TitleY - 28 const paraSpacing = 14.17 const paraSize = 11 for (const p of paras) { py = drawWrappedOnPage3(p.trim(), leftX, py, paraSize, helv) py -= paraSpacing } // Additional confirmation block (formatted) with 0.4cm spacing const blockSpacing = 11.34 // 0.4 cm const boldSize = 12 const normalSize = 11 // ensure a bit of space before the block py -= blockSpacing py = drawWrappedOnPage3('Erklärung:', leftX, py, boldSize, helvBold) py -= blockSpacing py = drawWrappedOnPage3('Ich bestätige das Vorstehende zur Kenntnis genommen zu haben und willige ein, dass der', leftX, py, normalSize, helv) py -= blockSpacing py = drawWrappedOnPage3('Harheimer Tischtennis-Club 1954 e.V.', leftX, py, boldSize, helvBold) py -= blockSpacing py = drawWrappedOnPage3('folgende allgemeine Daten zu meiner Person:', leftX, py, normalSize, helv) py -= blockSpacing // Rebuild a simple, single-column, non-overlapping form area on page 3 const formLeft = leftX const formFieldW = 360 const formFieldH = 14 const formGap = 22 // start a bit lower to separate from the block text py -= 28 // Row layout const colGap = 220 // Shifts (in points): 1 cm = ~28.35 pt const leftColShift = 0.3 * 28.35 // 0.3 cm -> ~8.505 pt const vornameLabelShift = 1.0 * 28.35 // 1.0 cm -> ~28.35 pt const vornameFieldShift = 1.3 * 28.35 // 1.3 cm -> ~36.855 pt const leftColX = formLeft + leftColShift const rightColX = formLeft + colGap // Row 1: Name (left) and Vorname (right) // Keep the left label at the original formLeft, move only the input field slightly right by 0.1cm const leftInputShift = 0.35 * 28.35 // 0.35 cm -> ~9.9225 pt // extra small horizontal nudge for all non-date inputs: 1mm const extraHShift = 0.1 * 28.35 // 0.1 cm (1 mm) -> ~2.835 pt // vertical shift for all input fields (including date): 0.6 cm up const verticalFieldShift = 0.6 * 28.35 // 0.6 cm -> ~17.01 pt // downward nudge for non-date inputs requested now: 0.2 cm -> ~5.67 pt const nonDateDownShift = 0.2 * 28.35 // 0.2 cm -> ~5.67 pt page3.drawText('Name:', { x: formLeft, y: py, size: normalSize, font: helv }) form.createTextField('page3_name').addToPage(page3, { x: formLeft + 70 + leftInputShift + extraHShift, y: py - formFieldH + verticalFieldShift - nonDateDownShift, width: 160, height: formFieldH, font: helv }) // Vorname label and field moved further right as requested page3.drawText('Vorname:', { x: rightColX + vornameLabelShift, y: py, size: normalSize, font: helv }) form.createTextField('page3_vorname').addToPage(page3, { x: rightColX + 70 + vornameFieldShift + extraHShift, y: py - formFieldH + verticalFieldShift - nonDateDownShift, width: 160, height: formFieldH, font: helv }) py -= formGap // Anschrift (full width) page3.drawText('Anschrift:', { x: formLeft, y: py, size: normalSize, font: helv }) form.createTextField('page3_anschrift').addToPage(page3, { x: formLeft + 70 + leftInputShift + extraHShift, y: py - formFieldH + verticalFieldShift - nonDateDownShift, width: formFieldW + 40, height: formFieldH, font: helv }) py -= formGap // Row 3: Telefonnummer (left) and E-Mail (right) // Keep left label at formLeft, shift only the input field by leftInputShift page3.drawText('Telefonnummer:', { x: formLeft, y: py, size: normalSize, font: helv }) form.createTextField('page3_telefon').addToPage(page3, { x: formLeft + 70 + leftInputShift + extraHShift, y: py - formFieldH + verticalFieldShift - nonDateDownShift, width: 160, height: formFieldH, font: helv }) page3.drawText('E-Mail-Adresse:', { x: rightColX + vornameLabelShift, y: py, size: normalSize, font: helv }) form.createTextField('page3_email').addToPage(page3, { x: rightColX + 70 + vornameFieldShift + extraHShift, y: py - formFieldH + verticalFieldShift - nonDateDownShift, width: 180, height: formFieldH, font: helv }) py -= formGap // remove fax field/label per request (space preserved) py -= formGap py -= formGap // Date and signature line const dateFieldW = 120 page3.drawText('Frankfurt/Main-Harheim, den', { x: formLeft, y: py, size: normalSize, font: helv }) const dateX = formLeft + helv.widthOfTextAtSize('Frankfurt/Main-Harheim, den ', normalSize) // date field also moves up by verticalFieldShift (but not horizontally shifted by extraHShift) // now move the date and signature line 0.2cm down as requested const dateFieldY = py - formFieldH + verticalFieldShift - nonDateDownShift form.createTextField('page3_datum').addToPage(page3, { x: dateX, y: dateFieldY, width: dateFieldW, height: formFieldH, font: helv }) // signature line starts directly under the (moved) date field page3.drawRectangle({ x: dateX, y: dateFieldY - 6, width: 300, height: 1, color: rgb(0,0,0) }) // label under signature line const sigLabel = 'Datum, Unterschrift (bei Jugendlichen gesetzlicher Vertreter)' const sigLabelSize = 10 page3.drawText(sigLabel, { x: dateX, y: dateFieldY - 18, size: sigLabelSize, font: helv }) // Ensure appearance streams are generated for all form fields using the embedded font try { form.updateFieldAppearances(helv) } catch (e) { console.warn('Warning: updateFieldAppearances failed while generating template:', e) } const pdfBytes = await pdfDoc.save() fs.writeFileSync('server/templates/mitgliedschaft-fillable.pdf', pdfBytes) console.log('Wrote server/templates/mitgliedschaft-fillable.pdf') } create().catch(e => { console.error(e) process.exit(1) })