diff --git a/lib/documents.js b/lib/documents.js index 3ab2dd3..17dea0b 100644 --- a/lib/documents.js +++ b/lib/documents.js @@ -568,7 +568,7 @@ const RC = { }; // Section heading: bold uppercase tracked label with a thin hairline beneath. -function cvHeading(doc, S, dry, cur, title) { +function cvHeading(doc, S, dry, cur, title, ruleRight) { const pt = 11; cur.y += 3 * S; doc.setFont('Lato', 'bold'); @@ -580,7 +580,7 @@ function cvHeading(doc, S, dry, cur, title) { doc.setCharSpace(0); doc.setDrawColor(RC.hair[0], RC.hair[1], RC.hair[2]); doc.setLineWidth(0.3 * S); - doc.line(RV.mx, cur.y + pt * S * PT * 1.0, RV_R, cur.y + pt * S * PT * 1.0); + doc.line(RV.mx, cur.y + pt * S * PT * 1.0, ruleRight || RV_R, cur.y + pt * S * PT * 1.0); } cur.y += pt * S * PT + 3.4 * S; } @@ -679,7 +679,8 @@ function composeCvHeader(doc, S, dry, cur, header, titel, foto) { } catch (e) { photoOk = false; } } - const leftW = photoOk ? (RV_W - PHOTO_W * S - 6 * S) : RV_W * 0.62; + const gapPhoto = 6 * S; + const leftW = photoOk ? (RV_W - photoW - gapPhoto) : RV_W * 0.62; write(doc, S, dry, cur, header.name, { pt: 22, style: 'bold', color: RC.ink, x: RV.mx, width: leftW, factor: 1.05 }); if (titel) write(doc, S, dry, cur, titel, { pt: 12, color: RC.sub, x: RV.mx, width: leftW, factor: 1.15 }); if (header.geburtsdatum) write(doc, S, dry, cur, `Geburtsdatum: ${header.geburtsdatum}`, { pt: 9, color: RC.sub, x: RV.mx, width: leftW, factor: 1.3 }); @@ -689,31 +690,73 @@ function composeCvHeader(doc, S, dry, cur, header, titel, foto) { } const leftBottom = cur.y; + let photoBottom = leftBottom, photoLeft = RV_R; if (photoOk) { const px = RV_R - photoW; const py = top; + photoLeft = px; + photoBottom = py + photoH; if (!dry) { doc.addImage(foto.dataUrl, foto.format || 'PNG', px, py, photoW, photoH); doc.setDrawColor(RC.hair[0], RC.hair[1], RC.hair[2]); doc.setLineWidth(0.3 * S); doc.rect(px, py, photoW, photoH); } - cur.y = Math.max(leftBottom, py + photoH) + 3.2 * S; + // Tuck the first section up right under the contact block; the photo's + // lower flank is filled by the Profil (handled in cvProfil), not by a gap. + cur.y = leftBottom + 1.5 * S; } else { const rc = { y: top }; contactLines.forEach((line) => write(doc, S, dry, rc, line, { pt: 9, color: RC.sub, x: RV.mx, width: RV_W, align: 'right', right: RV_R, factor: 1.34 })); cur.y = Math.max(leftBottom, rc.y) + 3.2 * S; + cur.y += 2.5 * S; } - // No separator rule above the first section (Profil) — cleaner modern head. - cur.y += 2.5 * S; + return { photoOk, photoBottom, photoLeft, gapPhoto }; +} + +// Profil section that flows around an optional photo: the heading and first +// lines tuck into the empty space beside the photo's lower flank (with the +// hairline stopping short of the photo), then the text spills to full width +// once it clears the photo's bottom edge. +function cvProfil(doc, S, dry, cur, text, geo) { + const beside = geo.photoOk && (cur.y + 3 * S) < geo.photoBottom; + const ruleRight = beside ? (geo.photoLeft - geo.gapPhoto) : RV_R; + cvHeading(doc, S, dry, cur, 'Profil', ruleRight); + + const pt = 10, factor = 1.45; + const lineH = pt * S * PT * factor; + const narrowW = geo.photoOk ? (geo.photoLeft - geo.gapPhoto - RV.mx) : RV_W; + doc.setFont('Lato', 'normal'); + doc.setFontSize(pt * S); + + const drawLine = (ln) => { + if (!dry) { doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]); doc.text(ln, RV.mx, cur.y + pt * S * PT * 0.76); } + cur.y += lineH; + }; + + let remaining = String(text); + if (geo.photoOk && cur.y < geo.photoBottom) { + const nBeside = Math.floor((geo.photoBottom - cur.y) / lineH); + if (nBeside > 0) { + const narrow = doc.splitTextToSize(remaining, narrowW); + const take = Math.min(nBeside, narrow.length); + for (let i = 0; i < take; i++) drawLine(narrow[i]); + remaining = narrow.slice(take).join(' '); + } + } + if (remaining) doc.splitTextToSize(remaining, RV_W).forEach(drawLine); + + // Ensure the next section starts below the photo. + cur.y = Math.max(cur.y, geo.photoBottom); } function composeCV(doc, S, dry, cur, { cv, header, titel, foto }) { - composeCvHeader(doc, S, dry, cur, header, titel, foto); + const geo = composeCvHeader(doc, S, dry, cur, header, titel, foto); if (cv.profil) { - cvHeading(doc, S, dry, cur, 'Profil'); - write(doc, S, dry, cur, cv.profil, { pt: 10, color: RC.ink, x: RV.mx, width: RV_W, factor: 1.45 }); + cvProfil(doc, S, dry, cur, cv.profil, geo); + } else { + cur.y = Math.max(cur.y, geo.photoBottom); } if (cv.berufserfahrung.length) { cvHeading(doc, S, dry, cur, 'Berufserfahrung');