Skip to content

Commit

Permalink
Add automatic page break capability
Browse files Browse the repository at this point in the history
resolves #34
  • Loading branch information
nilmerg committed Aug 31, 2021
1 parent 99896b9 commit a882f3d
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 13 deletions.
54 changes: 54 additions & 0 deletions library/Pdfexport/HeadlessChrome.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ class HeadlessChrome
/** @var string */
const WAIT_FOR_NETWORK = 'wait-for-network';

/** @var string Javascript Promise to wait for layout initialization */
const WAIT_FOR_LAYOUT = <<<JS
new Promise((fulfill, reject) => {
let timeoutId = setTimeout(() => reject('fail'), 10000);
if (document.documentElement.dataset.layoutReady === 'yes') {
clearTimeout(timeoutId);
fulfill(null);
return;
}
document.addEventListener('layout-ready', e => {
clearTimeout(timeoutId);
fulfill(e.detail);
}, {
once: true
});
})
JS;

/** @var string Path to the Chrome binary */
protected $binary;

Expand Down Expand Up @@ -385,13 +405,47 @@ private function printToPDF($socket, $browserId, array $parameters)
'frameId' => $targetId,
'html' => $this->document->render()
]);

// wait for page to fully load
$this->waitFor($page, 'Page.loadEventFired');
} else {
throw new LogicException('Nothing to print');
}

// Wait for network activity to finish
$this->waitFor($page, self::WAIT_FOR_NETWORK);

// Wait for layout to initialize
if (isset($this->document)) {
// Ensure layout scripts work in the same environment as the pdf printing itself
$this->communicate($page, 'Emulation.setEmulatedMedia', ['media' => 'print']);

$this->communicate($page, 'Runtime.evaluate', [
'timeout' => 1000,
'expression' => 'setTimeout(() => new Layout().apply(), 0)'
]);

$promisedResult = $this->communicate($page, 'Runtime.evaluate', [
'awaitPromise' => true,
'returnByValue' => true,
'timeout' => 1000, // Failsafe, doesn't apply to `await` it seems
'expression' => static::WAIT_FOR_LAYOUT
]);
if (isset($promisedResult['exceptionDetails'])) {
if (isset($promisedResult['exceptionDetails']['exception']['description'])) {
Logger::error(
'PDF layout failed to initialize: %s',
$promisedResult['exceptionDetails']['exception']['description']
);
} else {
Logger::warning('PDF layout failed to initialize. Pages might look skewed.');
}
}

// Reset media emulation, this may prevent the real media from coming into effect?
$this->communicate($page, 'Emulation.setEmulatedMedia', ['media' => '']);
}

// print pdf
$result = $this->communicate($page, 'Page.printToPDF', array_merge(
$parameters,
Expand Down
69 changes: 56 additions & 13 deletions library/Pdfexport/PrintableHtmlDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

namespace Icinga\Module\Pdfexport;

use Icinga\Application\Icinga;
use Icinga\Web\StyleSheet;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Html\Text;
use ipl\Html\ValidHtml;

class PrintableHtmlDocument extends BaseHtmlElement
Expand Down Expand Up @@ -105,6 +107,16 @@ class PrintableHtmlDocument extends BaseHtmlElement
*/
protected $pageRanges;

/**
* Page height in pixels minus any vertical margins, footer and header
*
* The default is roughly the amount of pixels matching the default paper height of 11 inches at scale 1.
*
* @todo Find out why subtracting the vertical margins leaves unused space behind (with a height of ~980px)
* @var int
*/
protected $pagePixelHeight = 1056;

/**
* HTML template for the print header
*
Expand Down Expand Up @@ -248,19 +260,32 @@ public function removeMargins()
*/
protected function assemble()
{
$html = Html::tag('html')
->add(Html::tag(
'style',
null,
new HtmlString(new StyleSheet())
))
->add(Html::tag(
'title',
$this->setWrapper(new HtmlElement(
'html',
null,
new HtmlElement(
'head',
null,
$this->title
));

$this->setWrapper($html);
new HtmlElement(
'title',
null,
Text::create($this->title)
),
new HtmlElement(
'style',
null,
HtmlString::create(new StyleSheet())
),
$this->createLayoutScript()
)
));

$this->getAttributes()->registerAttributeCallback('data-content-height', function () {
return $this->pagePixelHeight;
});
$this->getAttributes()->registerAttributeCallback('style', function () {
return sprintf('width: %sin;', $this->paperWidth ?: 8.5);
});
}

/**
Expand Down Expand Up @@ -332,4 +357,22 @@ public function getPrintParameters()

return $parameters;
}

/**
* Create layout javascript
*
* @return ValidHtml
*/
protected function createLayoutScript(): ValidHtml
{
$jsPath = Icinga::app()->getModuleManager()->getModule('pdfexport')->getJsDir();
$layoutJS = file_get_contents($jsPath . '/layout.js') . "\n\n\n";
$layoutJS .= file_get_contents($jsPath . '/layout-plugins/page-breaker.js') . "\n\n\n";

return new HtmlElement(
'script',
Attributes::create(['type' => 'application/javascript']),
HtmlString::create($layoutJS)
);
}
}
44 changes: 44 additions & 0 deletions public/js/layout-plugins/page-breaker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* Icinga PDF Export | (c) 2021 Icinga GmbH | GPLv2 */

"use strict";

(() => {
Layout.registerPlugin('page-breaker', () => {
let pageBreaksFor = document.querySelector('[data-pdfexport-page-breaks-at]');
if (! pageBreaksFor) {
return;
}

let pageBreaksAt = pageBreaksFor.dataset.pdfexportPageBreaksAt;
if (! pageBreaksAt) {
return;
}

let contentHeight = document.body.dataset.contentHeight;
let items = Array.from(pageBreaksFor.querySelectorAll(':scope > ' + pageBreaksAt));

let remainingHeight = contentHeight;
items.forEach((item, i) => {
let requiredHeight;
if (i < items.length - 1) {
requiredHeight = items[i + 1].getBoundingClientRect().top - item.getBoundingClientRect().top;
} else {
requiredHeight = item.parentElement.getBoundingClientRect().bottom - item.getBoundingClientRect().top;
}

if (remainingHeight < requiredHeight) {
if (!! item.previousSibling) {
item.previousSibling.style.pageBreakAfter = 'always';
item.previousSibling.classList.add('page-break-follows');
} else {
item.style.pageBreakAfter = 'always';
item.classList.add('page-break-follows');
}

remainingHeight = contentHeight;
}

remainingHeight -= requiredHeight;
});
});
})();
32 changes: 32 additions & 0 deletions public/js/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* Icinga PDF Export | (c) 2021 Icinga GmbH | GPLv2 */

"use strict";

class Layout
{
static #plugins = [];

static registerPlugin(name, plugin) {
this.#plugins.push([name, plugin]);
}

apply() {
for (let [name, plugin] of Layout.#plugins) {
try {
plugin();
} catch (error) {
console.error('Layout plugin ' + name + ' failed run: ' + error);
}
}

this.finish();
}

finish() {
document.documentElement.dataset.layoutReady = 'yes';
document.dispatchEvent(new CustomEvent('layout-ready', {
cancelable: false,
bubbles: false
}));
}
}

0 comments on commit a882f3d

Please sign in to comment.