Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/PdfExportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,10 @@ protected function renderDocument(): Writer
: new ExportException('forbidden');
}

$writer->startDocument($this->collector->getTitle());
// initial context for placeholder replacements, before any pages are loaded
// Set the initial context (first page) before the document is started, so the headers and
// footers defined in startDocument() resolve their placeholders against the first page.
$template->setContext($this->collector, $pages[0], null);
$writer->startDocument($this->collector->getTitle());
$writer->cover();

if ($this->config->hasToC()) {
Expand Down
118 changes: 78 additions & 40 deletions src/Writer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ class Writer
/** @var string Store HTML when debugging */
protected string $debugHTML = '';

/** @var false Is this the first page being written? */
protected bool $isFirstPage = true;

/**
* @param DokuMpdf $mpdf
* @param Template $template
Expand Down Expand Up @@ -71,6 +68,8 @@ public function startDocument(string $title): void
$styles .= '@page portrait-page { size:portrait }';
$styles .= 'div.dw2pdf-portrait { page:portrait-page }';
$styles .= $this->styles->getCSS();
$styles .= $this->defineHeaderFooters();

$this->write($styles, HTMLParserMode::HEADER_CSS);

//start body html
Expand All @@ -97,8 +96,10 @@ public function pageBreak(): void
*/
public function wikiPage(string $html): void
{
$this->conditionalPageBreak();
// Redefine the odd/even headers/footers for this page *before* the break: mPDF captures a
// page's header and footer when the page begins.
$this->applyHeaderFooters();
$this->conditionalPageBreak();
$this->write($html, HTMLParserMode::HTML_BODY, false, false);

// add citation box if any
Expand Down Expand Up @@ -270,8 +271,8 @@ public function cover(): void
$html = $this->template->getHTML('cover');
if (!$html) return;

$this->conditionalPageBreak();
$this->applyHeaderFooters();
$this->conditionalPageBreak();
$this->write($html, HTMLParserMode::HTML_BODY, false, false);

$this->breakAfterMe();
Expand Down Expand Up @@ -308,51 +309,88 @@ public function endDocument(): void
}

/**
* Set new headers and footers
* Define the named headers/footers and return the CSS that binds them to the pages
*
* mPDF selects a page's header and footer through @page CSS rules that reference named blocks.
* The "first" block is bound to "@page :first" so mPDF places it on the first physical page
* only. The odd/even blocks are bound to "@page" and apply to every following page.
*
* The blocks defined here resolve their placeholders against the first page (the current
* context when startDocument() runs). The first-page block keeps that content, while the
* odd/even blocks are redefined per wiki page by applyHeaderFooters().
*
* Templates that do not exist are skipped, so their pages simply carry no header/footer.
*
* @return string The @page CSS binding the defined blocks
* @throws MpdfException
*/
protected function defineHeaderFooters(): string
{
$firstRules = '';
$pageRules = '';
foreach (['header', 'footer'] as $section) {
foreach (['first', 'odd', 'even'] as $order) {
if (!$this->defineNamedBlock($section, $order)) continue;

if ($order === 'first') {
$firstRules .= $section . ': html_' . $section . '_first;';
} else {
$pageRules .= $order . '-' . $section . '-name: html_' . $section . '_' . $order . ';';
}
}
}

$css = '';
if ($firstRules !== '') $css .= '@page :first { ' . $firstRules . ' }';
if ($pageRules !== '') $css .= '@page { ' . $pageRules . ' }';
return $css;
}

/**
* Redefine the odd/even headers and footers for the wiki page about to be written
*
* This will call the appropriate mpdf methods to set headers and footers. It should be called
* before each wiki page is added to the PDF.
* Must run *before* the page break that starts the page, because mPDF captures a page's header
* and footer when the page begins. The blocks are re-read from the template on every call so
* per-page placeholders (@ID@, @PAGEURL@, @QRCODE@, ...) resolve against the current page.
*
* On first call on this instance it will set the headers/footers for the first page, afterwards
* it will use the standard headers/footers.
* The first-page block is left untouched: "@page :first" consumes it on the first physical page
* only, so it never needs a per-page value.
*
* We always set even and odd headers/footers, though they may be identical.
* @return void
* @throws MpdfException
*/
protected function applyHeaderFooters(): void
{
if ($this->isFirstPage) {
$header = $this->template->getHTML('header', 'first');
$footer = $this->template->getHTML('footer', 'first');

if ($header) {
$this->mpdf->SetHTMLHeader($header, 'O');
$this->mpdf->SetHTMLHeader($header, 'E');
}
if ($footer) {
$this->mpdf->SetHTMLFooter($footer, 'O');
$this->mpdf->SetHTMLFooter($footer, 'E');
foreach (['header', 'footer'] as $section) {
foreach (['odd', 'even'] as $order) {
$this->defineNamedBlock($section, $order);
}
$this->isFirstPage = false;
} else {
$headerOdd = $this->template->getHTML('header', 'odd');
$headerEven = $this->template->getHTML('header', 'even');
$footerOdd = $this->template->getHTML('footer', 'odd');
$footerEven = $this->template->getHTML('footer', 'even');
}
}

if ($headerOdd) {
$this->mpdf->SetHTMLHeader($headerOdd, 'O');
}
if ($headerEven) {
$this->mpdf->SetHTMLHeader($headerEven, 'E');
}
if ($footerOdd) {
$this->mpdf->SetHTMLFooter($footerOdd, 'O');
}
if ($footerEven) {
$this->mpdf->SetHTMLFooter($footerEven, 'E');
}
/**
* Define a single named header or footer block from its template
*
* The block is named "<section>_<order>" (e.g. "header_odd") and can be referenced in CSS as
* "html_<section>_<order>". Placeholders are resolved against the template's current context.
*
* @param string $section Either 'header' or 'footer'
* @param string $order The variant to load: 'first', 'odd' or 'even'
* @return bool True if the template existed and the block was defined, false otherwise
* @throws MpdfException
*/
protected function defineNamedBlock(string $section, string $order): bool
{
$html = $this->template->getHTML($section, $order);
if ($html === '') return false;

$name = $section . '_' . $order;
if ($section === 'header') {
$this->mpdf->DefHTMLHeaderByName($name, $html);
} else {
$this->mpdf->DefHTMLFooterByName($name, $html);
}
return true;
}

/**
Expand Down