Compact PDF table layout to match reference design (Task #359)

Changes to artifacts/api-server/src/lib/pdf-renderer.ts:

- Reduced cellPadX from 6 to 4 (horizontal cell padding)
- Reduced cellPadY from 3 to 2 (vertical cell padding)
- Tightened lineHeight multiplier from 1.25 to 1.2
- Removed +2 padding buffer from heightOfString measurement loop
- Removed +2 from drawWrappingLine return value
- Tightened drawMixedLine return value (1.25→1.2)
- Consolidated header spacing from two moveDown(0.2+0.4) to one moveDown(0.3)
- Tightened title Y offset multiplier from 1.25 to 1.2
- Fixed row height probe to detect script per-line (matching draw path)
  instead of per-cell, preventing measurement/draw mismatch in
  mixed Arabic/Latin content now that the +2 cushion is removed

Result: Table rows are compact with no visible gaps between them,
matching the user's reference PDF (attached_assets/rrr1_1777879598338.pdf).
Row colors, cell borders, and footer positioning are unchanged.
Both AR and EN PDFs generate successfully (200 status).
This commit is contained in:
Riyadh
2026-05-04 07:35:52 +00:00
parent 4485628038
commit 8462cfcfd1
2 changed files with 21 additions and 31 deletions
+21 -31
View File
@@ -529,7 +529,7 @@ function drawMixedLine(
});
cursorX += r.width;
}
return opts.fontSize * 1.25;
return opts.fontSize * 1.2;
}
function drawWrappingLine(
@@ -561,7 +561,7 @@ function drawWrappingLine(
lineGap: -1,
...(features ? { features } : {}),
});
return Math.max(opts.fontSize * 1.25, doc.y - before + 2);
return Math.max(opts.fontSize * 1.2, doc.y - before);
}
export async function renderSchedulePdf(input: RenderPdfInput): Promise<Buffer> {
@@ -639,9 +639,8 @@ export async function renderSchedulePdf(input: RenderPdfInput): Promise<Buffer>
baseDirection: baseDir,
mapping,
});
doc.y = headerTopY + Math.max(titleSize * 1.25, input.logo ? logoBoxSize : 0);
doc.moveDown(0.2);
doc.moveDown(0.4);
doc.y = headerTopY + Math.max(titleSize * 1.2, input.logo ? logoBoxSize : 0);
doc.moveDown(0.3);
if (input.meetings.length === 0) {
drawMixedLine(doc, input.labels.none, {
@@ -728,11 +727,9 @@ export async function renderSchedulePdf(input: RenderPdfInput): Promise<Buffer>
: [input.labels.no, input.labels.meeting, input.labels.attendees, input.labels.time];
const headerWidths = isRtl ? [...colWidths].reverse() : colWidths;
const cellPadX = 6;
// Tightened from 5→3 to keep the table compact (we aim to fit a typical
// day on one page, mirroring the user's reference layout).
const cellPadY = 3;
const lineHeight = input.font.fontSize * 1.25;
const cellPadX = 4;
const cellPadY = 2;
const lineHeight = input.font.fontSize * 1.2;
function drawHeader(): number {
const headerHeight = lineHeight + cellPadY * 2;
@@ -840,32 +837,25 @@ export async function renderSchedulePdf(input: RenderPdfInput): Promise<Buffer>
// sooner than a simple division would, causing rows to overlap.
const lines = c.text.split("\n");
let h = 0;
const arabicChars = [...c.text].filter(isArabicChar).length;
const dominantArabic = arabicChars * 2 > c.text.length;
const fk = fontKeyFor(dominantArabic, input.font.fontWeight, mapping);
const features = featuresFor(dominantArabic);
doc.font(fk).fontSize(input.font.fontSize);
const measureOpts: PDFKit.Mixins.TextOptions = {
width: widths[i] - cellPadX * 2,
align: c.align,
lineBreak: true,
// Mirror drawWrappingLine's lineGap so the probe matches the draw.
lineGap: -1,
...(features ? { features } : {}),
};
const cellW = widths[i] - cellPadX * 2;
for (const line of lines) {
if (!line) {
h += lineHeight;
continue;
}
// heightOfString returns pdfkit's exact rendered height for the
// wrapped paragraph (including its internal line gaps). Use it
// directly — rounding up to whole lineHeight units inflates rows
// by half a line and leaves visible empty space below cells.
// The +2 mirrors drawWrappingLine's per-line padding fallback so
// multi-line cells never get under-reserved.
const measured = doc.heightOfString(line, measureOpts);
h += Math.max(lineHeight, measured) + 2;
const lineArabicChars = [...line].filter(isArabicChar).length;
const lineDominantArabic = lineArabicChars * 2 > line.length;
const lineFk = fontKeyFor(lineDominantArabic, input.font.fontWeight, mapping);
const lineFeatures = featuresFor(lineDominantArabic);
doc.font(lineFk).fontSize(input.font.fontSize);
const measured = doc.heightOfString(line, {
width: cellW,
align: c.align,
lineBreak: true,
lineGap: -1,
...(lineFeatures ? { features: lineFeatures } : {}),
});
h += Math.max(lineHeight, measured);
}
const finalH = h + cellPadY * 2;
cellHeights.push(finalH);