Skip to content

Commit 77d3eab

Browse files
committed
fix: keep writeHTML() headers and footers on their own page
- Render each page header/footer in a single region spanning the full page width and extending below the page bottom, with the automatic page break disabled, then restore the original page regions - Fixes writeHTML() in an overridden Footer() adding an extra page on the last page, drawing the footer at the top of the next page on other pages, and leaving the graphics state unbalanced - Header/footer HTML on column pages now uses the full page width instead of the column width - Add a regression test for an HTML footer below the bottom margin on a 3-page document - Add missing CHANGELOG entries - Bump version to 7.0.15
1 parent 1102179 commit 77d3eab

4 files changed

Lines changed: 125 additions & 3 deletions

File tree

‎CHANGELOG.TXT‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
7.0.15 (2026-10-03)
2+
- Fixed headers and footers drawn with writeHTML() (or other flowing content) in an overridden Header() or Footer(): content placed below the automatic page break margin, such as a footer, was moved to another page. On the last page an extra page was added; on the other pages the footer was drawn at the top of the next page. The graphics state was also left unbalanced (poppler: "Restoring state when no valid states to pop").
3+
- Headers and footers are now always drawn on their own page, over the full page width, also on pages with columns. Content that does not fit is clipped at the page edge, as in TCPDF 6.
4+
- Fixed the automatic K_PATH_FONTS detection when `Composer\InstalledVersions` is loaded before TCPDF: the install path may contain '..' segments (e.g. 'vendor/composer/../'), which the file helper rejects. The detected font directory is now resolved with realpath().
5+
- Requires `tecnickcom/tc-lib-pdf` ^8.78.
6+
7+
*************************************************
8+
9+
7.0.13 (2026-10-03)
10+
- Fixed Cell() with text wider than the cell: the text is drawn on a single line, as in TCPDF 6, while the cell box and border keep the requested geometry. L, C and R alignment are anchored to the cell edges (left, center, right).
11+
- Fixed Cell() stretched text to start at the cell position.
12+
- Fixed the example 52 certificate path and regenerated the example certificate files.
13+
- Requires `tecnickcom/tc-lib-pdf` ^8.77.
14+
15+
*************************************************
16+
17+
7.0.12 (2026-09-26)
18+
- Updated the development tooling (mago 1.50.0). No library changes.
19+
20+
*************************************************
21+
22+
7.0.11 (2026-09-21)
23+
- Requires `tecnickcom/tc-lib-pdf` ^8.76.
24+
25+
*************************************************
26+
127
7.0.10 (2026-09-14)
228
- Fixed Output() with the 'F', 'FI' and 'FD' destinations, which failed for every call: the caller's file path was passed to `Output::savePDF()`, which takes a directory. The path is now split into the directory and the file name.
329
- The file name is sanitized by the engine, so the file written may differ from the name given ('my_report.final.pdf' is written as 'my_report_final.pdf'). Use `TCPDF::getPDFFilename()` for the effective name.

‎VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7.0.14
1+
7.0.15

‎tcpdf.php‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
//============================================================+
44
// File name : tcpdf.php
5-
// Version : 7.0.14
5+
// Version : 7.0.15
66
// Author : Nicola Asuni - Tecnick.com LTD - www.tecnick.com - info@tecnick.com
77
// License : GNU-LGPL v3 (https://www.gnu.org/copyleft/lesser.html)
88
// Copyright (C): 2002-2026 Nicola Asuni - Tecnick.com LTD
@@ -15,7 +15,7 @@
1515
* See: https://tcpdf.org
1616
* @package com.tecnick.tcpdf
1717
* @author Nicola Asuni
18-
* @version 7.0.14
18+
* @version 7.0.15
1919
*/
2020

2121
// TCPDF configuration
@@ -2071,6 +2071,7 @@ protected function decorateAllPages(): void
20712071
}
20722072

20732073
$eng->setCurrentPage((int) $pid);
2074+
$layout = $this->setDecorationPageLayout((int) $pid);
20742075
$this->lmargin = $this->orig_lmargin;
20752076
$this->rmargin = $this->orig_rmargin;
20762077
$this->setCellPadding(0);
@@ -2104,6 +2105,8 @@ protected function decorateAllPages(): void
21042105
$this->Footer();
21052106
$this->emitToPage($eng->graph->getStopTransform());
21062107
}
2108+
2109+
$this->setEnginePageLayout((int) $pid, $layout);
21072110
}
21082111

21092112
$this->posx = $saved['x'];
@@ -2121,6 +2124,69 @@ protected function decorateAllPages(): void
21212124
$this->decortotalpages = 0;
21222125
}
21232126

2127+
/**
2128+
* Give a page a single region that starts at the page origin and ends far
2129+
* below the page bottom, with the automatic page break disabled, so the
2130+
* header and footer content (e.g. writeHTML()) never flows to another
2131+
* column or page.
2132+
*
2133+
* @return array{region: array<int, array{RB: float, RH: float, RL: float, RR: float, RT: float, RW: float, RX: float, RY: float, x: float, y: float}>, columns: int, currentRegion: int, autobreak: bool} Original page layout.
2134+
*/
2135+
protected function setDecorationPageLayout(int $pid): array
2136+
{
2137+
$page = $this->engine()->page->getPage($pid);
2138+
$original = [
2139+
'region' => $page['region'],
2140+
'columns' => $page['columns'],
2141+
'currentRegion' => $page['currentRegion'],
2142+
'autobreak' => $page['autobreak'],
2143+
];
2144+
2145+
$width = $page['width'];
2146+
$height = $page['height'] * 1000;
2147+
$this->setEnginePageLayout($pid, [
2148+
'region' => [[
2149+
'RB' => $page['height'] - $height,
2150+
'RH' => $height,
2151+
'RL' => $width,
2152+
'RR' => 0.0,
2153+
'RT' => $height,
2154+
'RW' => $width,
2155+
'RX' => 0.0,
2156+
'RY' => 0.0,
2157+
'x' => 0.0,
2158+
'y' => 0.0,
2159+
]],
2160+
'columns' => 1,
2161+
'currentRegion' => 0,
2162+
'autobreak' => false,
2163+
]);
2164+
2165+
return $original;
2166+
}
2167+
2168+
/**
2169+
* Write the region and page break settings of an engine page.
2170+
*
2171+
* The engine exposes no mutator for the region list of an existing page:
2172+
* the data is written directly into the page store.
2173+
*
2174+
* @param array{region: array<int, array{RB: float, RH: float, RL: float, RR: float, RT: float, RW: float, RX: float, RY: float, x: float, y: float}>, columns: int, currentRegion: int, autobreak: bool} $layout
2175+
*/
2176+
protected function setEnginePageLayout(int $pid, array $layout): void
2177+
{
2178+
$eng = $this->engine();
2179+
$prop = new \ReflectionProperty(\Com\Tecnick\Pdf\Page\Settings::class, 'page');
2180+
$pages = $prop->getValue($eng->page);
2181+
if (!is_array($pages) || !isset($pages[$pid]) || !is_array($pages[$pid])) {
2182+
return;
2183+
}
2184+
2185+
/** @var array<int, array<string, mixed>> $pages */
2186+
$pages[$pid] = array_merge($pages[$pid], $layout);
2187+
$prop->setValue($eng->page, $pages);
2188+
}
2189+
21242190
// ===================================================================
21252191
// Document settings.
21262192
// ===================================================================

‎test/TcpdfHeaderFooterTest.php‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,34 @@ public function Footer()
9090
$this->assertStringContainsString('CUSTOM-HEADER-MARK', $text);
9191
$this->assertStringContainsString('CUSTOM-FOOTER-1', $text);
9292
}
93+
94+
public function testHtmlFooterBelowBottomMarginStaysOnItsPage(): void
95+
{
96+
$pdf = new class('P', 'mm', 'A4', true, 'UTF-8', false) extends TCPDF {
97+
public function Footer()
98+
{
99+
$this->writeHTML('HTMLFOOTER' . $this->PageNo() . ' <em>test</em>');
100+
}
101+
};
102+
$pdf->setPrintHeader(false);
103+
$pdf->setMargins(15, 28, 15);
104+
$pdf->setFooterMargin(10);
105+
$pdf->setAutoPageBreak(true, 25);
106+
$pdf->setFont('helvetica', '', 10);
107+
for ($page = 1; $page <= 3; $page++) {
108+
$pdf->AddPage();
109+
$pdf->writeHTML('<p>BODY' . $page . '</p>');
110+
}
111+
112+
$this->assertMatchesRegularExpression('/^Pages:\s+3$/m', $this->pdfInfo($pdf));
113+
for ($page = 1; $page <= 3; $page++) {
114+
$text = $this->popplerTool('pdftotext', $pdf, '-f ' . $page . ' -l ' . $page . ' -');
115+
$this->assertStringContainsString('BODY' . $page, $text);
116+
$this->assertStringContainsString('HTMLFOOTER' . $page, $text);
117+
}
118+
119+
// A4 height (841.89 pt) minus the 10 mm footer margin (28.35 pt).
120+
$box = $this->wordBox($pdf, 'HTMLFOOTER1');
121+
$this->assertEqualsWithDelta(813.54, $box['ymin'], 3.0);
122+
}
93123
}

0 commit comments

Comments
 (0)