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 // 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, 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, 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, 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, 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, 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, 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, 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) const pdfBytes = await pdfDoc.save() fs.writeFileSync('server/templates/mitgliedschaft-fillable.pdf', pdfBytes) console.log('Wrote server/templates/mitgliedschaft-fillable.pdf') // --- 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 }) } create().catch(e => { console.error(e) process.exit(1) })