Skip to content

Commit 2cb2edc

Browse files
committed
fix: Write() keeps the left cell padding and the right margin when the cursor has been moved
- Write() measures the first line offset from the unpadded flow origin, so text started after setX() is inset by the left cell padding like text started at the margin. - Require tc-lib-pdf ^8.73.5, which aligns an LTR right-aligned first line to the right margin instead of the cursor and resolves the font-relative CSS units (em, rem, ex, ch) against the real font size instead of the built-in defaults. - Add regression tests for both cursor cases, with a wordBox() helper reading exact word positions via pdftotext -bbox. - example_002: show that setX() moves where a Write() line starts without dropping the padding or moving the edge a right-aligned line is flush with. - build: install mago 1.47.6 through Composer instead of the upstream installer script, and add the tag release workflow.
1 parent 2701aa9 commit 2cb2edc

5 files changed

Lines changed: 99 additions & 4 deletions

File tree

‎composer.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"require": {
3030
"php": ">=8.2",
3131
"ext-curl": "*",
32-
"tecnickcom/tc-lib-pdf": "^8.73"
32+
"tecnickcom/tc-lib-pdf": "^8.73.5"
3333
},
3434
"require-dev": {
3535
"carthage-software/mago": "1.47.6",

‎examples/example_002.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,21 @@
8484

8585
// ---------------------------------------------------------
8686

87+
// The cursor set by setX() only says where a line may start: it carries the
88+
// left cell padding like a line starting at the margin, and it does not move
89+
// the right margin a right aligned line is flush with.
90+
$pdf->setFont('times', '', 12);
91+
$pdf->Ln(10);
92+
93+
$pdf->Write(0, 'left aligned from the left margin', '', 0, 'L', true);
94+
$pdf->setX(80);
95+
$pdf->Write(0, 'left aligned from setX(80)', '', 0, 'L', true);
96+
97+
$pdf->Write(0, 'right aligned from the left margin', '', 0, 'R', true);
98+
$pdf->setX(80);
99+
$pdf->Write(0, 'right aligned from setX(80)', '', 0, 'R', true);
100+
101+
// ---------------------------------------------------------
102+
87103
//Close and output PDF document
88104
$pdf->Output('example_002.pdf', 'I');

‎tcpdf.php‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3950,13 +3950,15 @@ public function Write(
39503950
// (and through the following columns/pages inside the engine call).
39513951
$incolumns = $this->inColumnMode();
39523952
$region = $eng->page->getRegion();
3953-
$originx = $incolumns ? $region['RX'] : $this->lmargin;
3953+
$flowx = $incolumns ? $region['RX'] : $this->lmargin;
39543954
$width = $incolumns ? $region['RW'] : $this->getPageWidth() - $this->rmargin - $this->lmargin;
39553955
// Legacy wraps the lines inside the horizontal cell padding and
39563956
// starts them at the padded edge.
3957-
$originx += $this->cellpadding['L'];
3957+
$originx = $flowx + $this->cellpadding['L'];
39583958
$width = max(0.0, $width - $this->cellpadding['L'] - $this->cellpadding['R']);
3959-
$offset = max(0.0, $this->posx - $originx);
3959+
// The cursor carries the padding too, so it is measured from the
3960+
// unpadded flow origin.
3961+
$offset = max(0.0, $this->posx - $flowx);
39603962

39613963
$styles = ['all' => ['lineWidth' => 0.0]];
39623964
if ((bool) $_fill) {

‎test/TcpdfCellTest.php‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,46 @@ public function testTextPlacesAtAbsolutePosition(): void
112112
$pdf->Text(25, 120, 'absolutely positioned');
113113
$this->assertStringContainsString('absolutely positioned', $this->extractText($pdf));
114114
}
115+
116+
public function testWriteAppliesTheLeftCellPaddingWhereverTheCursorIs(): void
117+
{
118+
$pdf = $this->newPdf();
119+
$pdf->setFont('helvetica', '', 11);
120+
$pdf->setMargins(20, 20, 20);
121+
$pdf->setCellPaddings(1, 0, 1, 0);
122+
$pdf->AddPage();
123+
124+
$pdf->Write(5, "first\n");
125+
$pdf->setX(40.0);
126+
$pdf->Write(5, "second\n");
127+
128+
$padding = $pdf->getCellPaddings()['L'];
129+
$this->assertGreaterThan(0.0, $padding);
130+
131+
// Both lines start one left padding to the right of their cursor.
132+
$topt = 72.0 / 25.4;
133+
$margin = $this->wordBox($pdf, 'first');
134+
$indented = $this->wordBox($pdf, 'second');
135+
$this->assertEqualsWithDelta((20.0 + $padding) * $topt, $margin['xmin'], 0.01);
136+
$this->assertEqualsWithDelta((40.0 + $padding) * $topt, $indented['xmin'], 0.01);
137+
}
138+
139+
public function testWriteRightAlignsToTheRightMarginWhereverTheCursorIs(): void
140+
{
141+
$pdf = $this->newPdf();
142+
$pdf->setFont('helvetica', '', 9);
143+
$pdf->setMargins(11, 11, 11);
144+
$pdf->AddPage();
145+
146+
$height = $pdf->getCellHeight($pdf->getFontSize());
147+
$pdf->Write($height, 'alpha', '', false, 'R', true);
148+
$pdf->setX(107.95);
149+
$pdf->Write($height, 'bravo', '', false, 'R', true);
150+
151+
// The cursor limits where a right aligned line may start, it does not
152+
// move the edge the line is aligned to.
153+
$plain = $this->wordBox($pdf, 'alpha');
154+
$moved = $this->wordBox($pdf, 'bravo');
155+
$this->assertEqualsWithDelta($plain['xmax'], $moved['xmax'], 0.01);
156+
}
115157
}

‎test/TcpdfTestCase.php‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,39 @@ protected function pdfInfo(TCPDF $pdf): string
6262
{
6363
return $this->popplerTool('pdfinfo', $pdf);
6464
}
65+
66+
/**
67+
* Return the bounding box in PDF points of the first word matching $word.
68+
*
69+
* @return array{xmin: float, xmax: float, ymin: float, ymax: float}
70+
*/
71+
protected function wordBox(TCPDF $pdf, string $word): array
72+
{
73+
$out = $this->popplerTool('pdftotext', $pdf, '-bbox -');
74+
$match = [];
75+
$found = preg_match(
76+
'/<word xMin="([\d.]+)" yMin="([\d.]+)" xMax="([\d.]+)" yMax="([\d.]+)">'
77+
. preg_quote($word, '/')
78+
. '<\/word>/',
79+
$out,
80+
$match,
81+
);
82+
$this->assertSame(1, $found, 'word "' . $word . '" must be present');
83+
84+
return [
85+
'xmin' => $this->toFloat($match[1] ?? ''),
86+
'ymin' => $this->toFloat($match[2] ?? ''),
87+
'xmax' => $this->toFloat($match[3] ?? ''),
88+
'ymax' => $this->toFloat($match[4] ?? ''),
89+
];
90+
}
91+
92+
/**
93+
* Parse a decimal number, defaulting to zero.
94+
*/
95+
private function toFloat(string $value): float
96+
{
97+
$number = filter_var($value, FILTER_VALIDATE_FLOAT);
98+
return $number === false ? 0.0 : $number;
99+
}
65100
}

0 commit comments

Comments
 (0)