Skip to content

Commit e130808

Browse files
committed
Release v0.3.2 - Table Header Positioning Bug Fix
### Bug Fixes - Fixed critical bug where table headers were rendering below data rows instead of above - Root cause: Incorrect handling of PDF coordinate system (Y-axis grows upward from bottom) - Comprehensive coordinate system overhaul in Table.ts affecting 8 critical locations ### Changes - Changed rect(x, y, width, height) to rect(x, y, width, -height) for downward drawing (3 locations) - Inverted calculateTextY() logic to properly position text in downward-drawing cells - Fixed header border Y position from y + height to y - height - Fixed row border Y position from y + height to y - height - Corrected vertical border drawing direction - Rewrote outer borders logic to properly handle top/bottom/left/right borders ### Testing - Added comprehensive table test suite with 39 new unit tests - Total test count increased from 229 to 268 tests - All tests passing (268/268) - Coverage includes: basic rendering, column handling, styling, alignment, borders, page breaks, edge cases - Browser compatibility verified with test-tables-browser.html
1 parent 7c252bc commit e130808

5 files changed

Lines changed: 777 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.2] - 2025-02-14
9+
10+
### 🐛 Bug Fixes
11+
12+
#### Table Header Positioning (Critical Bug Fix)
13+
- **Fixed table headers rendering below data rows instead of above**
14+
- Root cause: Incorrect handling of PDF coordinate system where Y-axis grows upward from bottom
15+
- Solution: Comprehensive coordinate system overhaul in Table.ts affecting 8 critical locations:
16+
- Changed `rect(x, y, width, height)` to `rect(x, y, width, -height)` for downward drawing (3 locations)
17+
- Inverted `calculateTextY()` logic to properly position text in downward-drawing cells
18+
- Fixed header border Y position from `y + height` to `y - height`
19+
- Fixed row border Y position from `y + height` to `y - height`
20+
- Corrected vertical border drawing direction
21+
- Rewrote outer borders logic to properly handle top/bottom/left/right borders
22+
23+
### ✅ Testing
24+
- Added comprehensive table test suite with 39 new unit tests
25+
- Total test count increased from 229 to 268 tests
26+
- All tests passing (268/268)
27+
- Coverage includes: basic rendering, column handling, styling, alignment, borders, page breaks, edge cases
28+
829
## [0.3.1] - 2025-02-02
930

1031
### 🎉 New Text Layout Features

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)
1010
[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D14-green.svg)](https://nodejs.org/)
1111
[![Browser](https://img.shields.io/badge/Browser-Compatible-success.svg)](BROWSER.md)
12-
[![Tests](https://img.shields.io/badge/tests-180%20passing-brightgreen.svg)](https://github.com/pdfstudio-dev/pdfstudio)
12+
[![Tests](https://img.shields.io/badge/tests-268%20passing-brightgreen.svg)](https://github.com/pdfstudio-dev/pdfstudio)
1313

1414
[Features](#-features)[Quick Start](#-quick-start)[Browser Support](#-browser-support)[Examples](#-examples)[API](#-api-reference)[Documentation](#-documentation)
1515

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pdfstudio/pdfstudio",
3-
"version": "0.3.1",
3+
"version": "0.3.2",
44
"description": "Professional PDF generation library for Node.js and Browsers. Features native charts, advanced vector graphics, QR codes, interactive forms, and complete TypeScript support.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/tables/Table.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ export class Table {
8484
const headerHeight = this.options.headerStyle?.height || this.options.rowHeight!
8585
if (columns.length > 0) {
8686
this.drawHeader(columns, columnWidths, startX, currentY, headerHeight)
87-
currentY += headerHeight
87+
currentY -= headerHeight
8888
}
8989

9090
// Draw rows
9191
rows.forEach((row, rowIndex) => {
9292
const rowHeight = this.options.rowHeight!
9393

9494
// Check if we need a page break
95-
if (autoPageBreak && currentY + rowHeight > pageHeight - bottomMargin) {
95+
if (autoPageBreak && currentY - rowHeight < bottomMargin) {
9696
// Add a new page
9797
this.writer.addPage()
9898

@@ -103,12 +103,12 @@ export class Table {
103103
// Repeat header if enabled
104104
if (repeatHeader && columns.length > 0) {
105105
this.drawHeader(columns, columnWidths, startX, currentY, headerHeight)
106-
currentY += headerHeight
106+
currentY -= headerHeight
107107
}
108108
}
109109

110110
this.drawRow(row, columns, columnWidths, startX, currentY, rowHeight, rowIndex)
111-
currentY += rowHeight
111+
currentY -= rowHeight
112112
})
113113

114114
// Draw outer borders if needed (only if not using auto page breaks)
@@ -220,7 +220,8 @@ export class Table {
220220
const [r, g, b] = parseColor(headerStyle.backgroundColor)
221221
this.writer.setFillColor(r, g, b)
222222
const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0)
223-
this.writer.rect(x, y, totalWidth, height)
223+
// Use negative height to draw downward from y position
224+
this.writer.rect(x, y, totalWidth, -height)
224225
this.writer.fill()
225226
}
226227

@@ -251,9 +252,9 @@ export class Table {
251252
currentX += cellWidth
252253
})
253254

254-
// Draw header separator
255+
// Draw header separator (at bottom of header)
255256
if (this.shouldDrawBorder('header')) {
256-
this.drawHorizontalBorder(x, y + height, columnWidths.reduce((s, w) => s + w, 0))
257+
this.drawHorizontalBorder(x, y - height, columnWidths.reduce((s, w) => s + w, 0))
257258
}
258259
}
259260

@@ -274,7 +275,8 @@ export class Table {
274275
const [r, g, b] = parseColor(this.options.alternateRowColor)
275276
this.writer.setFillColor(r, g, b)
276277
const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0)
277-
this.writer.rect(x, y, totalWidth, height)
278+
// Use negative height to draw downward from y position
279+
this.writer.rect(x, y, totalWidth, -height)
278280
this.writer.fill()
279281
}
280282

@@ -288,7 +290,8 @@ export class Table {
288290
if (normalizedCell.backgroundColor) {
289291
const [r, g, b] = parseColor(normalizedCell.backgroundColor)
290292
this.writer.setFillColor(r, g, b)
291-
this.writer.rect(currentX, y, cellWidth, height)
293+
// Use negative height to draw downward from y position
294+
this.writer.rect(currentX, y, cellWidth, -height)
292295
this.writer.fill()
293296
}
294297

@@ -312,9 +315,9 @@ export class Table {
312315
currentX += cellWidth
313316
})
314317

315-
// Draw horizontal border after row (if not last)
318+
// Draw horizontal border after row (at bottom of row)
316319
if (this.shouldDrawBorder('horizontal')) {
317-
this.drawHorizontalBorder(x, y + height, columnWidths.reduce((s, w) => s + w, 0))
320+
this.drawHorizontalBorder(x, y - height, columnWidths.reduce((s, w) => s + w, 0))
318321
}
319322
}
320323

@@ -372,7 +375,7 @@ export class Table {
372375

373376
/**
374377
* Calculate text Y position based on vertical alignment
375-
* Note: In PDF coordinates, Y grows upward from bottom.
378+
* Note: cellY is the TOP of the cell, and cells draw downward with negative height.
376379
* Text is drawn at the baseline (roughly 30% of fontSize above the bottom of the character)
377380
*/
378381
private calculateTextY(
@@ -387,16 +390,16 @@ export class Table {
387390

388391
switch (valign) {
389392
case 'top':
390-
// Top of cell, accounting for padding and descenders
391-
return cellY + cellHeight - padding - baselineOffset
393+
// Top of cell (cellY is top), move down by padding and font height, up by baseline
394+
return cellY - padding - fontSize + baselineOffset
392395
case 'middle':
393396
// Center of cell, adjusted for baseline
394-
return cellY + cellHeight / 2 + baselineOffset
397+
return cellY - cellHeight / 2 + baselineOffset
395398
case 'bottom':
396399
// Bottom of cell, accounting for padding and baseline
397-
return cellY + padding + fontSize - baselineOffset
400+
return cellY - cellHeight + padding + baselineOffset
398401
default:
399-
return cellY + cellHeight / 2 + baselineOffset
402+
return cellY - cellHeight / 2 + baselineOffset
400403
}
401404
}
402405

@@ -477,56 +480,58 @@ export class Table {
477480
this.writer.setStrokeColor(r, g, b)
478481
this.writer.setLineWidth(style.width!)
479482

483+
// Draw from top (y) to bottom (y - height)
480484
this.writer.moveTo(x, y)
481-
this.writer.lineTo(x, y + height)
485+
this.writer.lineTo(x, y - height)
482486
this.writer.stroke()
483487
}
484488

485489
/**
486490
* Draw outer borders
491+
* Note: y is the TOP of the table, height extends downward
487492
*/
488493
private drawOuterBorders(x: number, y: number, width: number, height: number): void {
489-
// Top
494+
// Top border (at y)
490495
if (this.shouldDrawBorder('top')) {
491496
const style = this.getBorderStyle('top')
492497
const [r, g, b] = parseColor(style.color!)
493498
this.writer.setStrokeColor(r, g, b)
494499
this.writer.setLineWidth(style.width!)
495-
this.writer.moveTo(x, y + height)
496-
this.writer.lineTo(x + width, y + height)
500+
this.writer.moveTo(x, y)
501+
this.writer.lineTo(x + width, y)
497502
this.writer.stroke()
498503
}
499504

500-
// Bottom
505+
// Bottom border (at y - height)
501506
if (this.shouldDrawBorder('bottom')) {
502507
const style = this.getBorderStyle('bottom')
503508
const [r, g, b] = parseColor(style.color!)
504509
this.writer.setStrokeColor(r, g, b)
505510
this.writer.setLineWidth(style.width!)
506-
this.writer.moveTo(x, y)
507-
this.writer.lineTo(x + width, y)
511+
this.writer.moveTo(x, y - height)
512+
this.writer.lineTo(x + width, y - height)
508513
this.writer.stroke()
509514
}
510515

511-
// Left
516+
// Left border (from y to y - height)
512517
if (this.shouldDrawBorder('left')) {
513518
const style = this.getBorderStyle('left')
514519
const [r, g, b] = parseColor(style.color!)
515520
this.writer.setStrokeColor(r, g, b)
516521
this.writer.setLineWidth(style.width!)
517522
this.writer.moveTo(x, y)
518-
this.writer.lineTo(x, y + height)
523+
this.writer.lineTo(x, y - height)
519524
this.writer.stroke()
520525
}
521526

522-
// Right
527+
// Right border (from y to y - height)
523528
if (this.shouldDrawBorder('right')) {
524529
const style = this.getBorderStyle('right')
525530
const [r, g, b] = parseColor(style.color!)
526531
this.writer.setStrokeColor(r, g, b)
527532
this.writer.setLineWidth(style.width!)
528533
this.writer.moveTo(x + width, y)
529-
this.writer.lineTo(x + width, y + height)
534+
this.writer.lineTo(x + width, y - height)
530535
this.writer.stroke()
531536
}
532537
}

0 commit comments

Comments
 (0)