Skip to content

Instantly share code, notes, and snippets.

@lesleyandreza
Last active October 10, 2022 20:40
Show Gist options
  • Save lesleyandreza/7699603b20f2459d1fcf3b9480fb22e1 to your computer and use it in GitHub Desktop.
Save lesleyandreza/7699603b20f2459d1fcf3b9480fb22e1 to your computer and use it in GitHub Desktop.
Generate PDF with Electron
const styleWorkAround = `style="
font-size: 14px;
margin: 0 1cm;
zoom: 0.74;
font-family: arial;
width: 100%;
"`
const customHeadersFooters = [
{
id: 0,
label: 'No custom',
},
{
id: 1,
label: 'Template 1',
header: `
<div>
[Header] Lorem ipsum dolor sit amet consectetur
</div>`,
footer: `[Footer] Simple footer`,
},
{
id: 2,
label: 'Template 2 (with workaround)',
header: `
<div ${styleWorkAround}>
[Header] Lorem ipsum dolor sit amet consectetur
</div>
`,
footer: `
<div ${styleWorkAround}>
[Footer] Lorem ipsum dolor sit amet consectetur
</div>`,
},
{
id: 3,
label: 'Template 3 (with variables)',
header: `
<div ${styleWorkAround}>
<span class=date></span>
-
<span class=title></span>
</div>
`,
footer: `
<div ${styleWorkAround}>
Page <span class=pageNumber></span> of <span class=totalPages></span>
-
<span class=url></span>
</div>
`,
},
]
const $customHeaderFooter = document.querySelector('[name="customHeaderFooter"]')
const customOptions = customHeadersFooters.map((item) => {
return `<option value="${item.id}"> ${item.label} </option>`
}).join('')
$customHeaderFooter.insertAdjacentHTML('afterbegin', customOptions)
function getCustomHeaderFooter() {
const customHeaderFooter = customHeadersFooters.find(e => e.id === +$customHeaderFooter.value)
return {
header: customHeaderFooter.header,
footer: customHeaderFooter.footer,
}
}
module.exports = { customHeadersFooters, getCustomHeaderFooter }
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="./styles.css" rel="stylesheet">
<title>Generate PDF</title>
</head>
<body>
<div class="wrapper">
<aside class="hide-in-print sidebar">
<form method="dialog" class="form-layout">
<label>
<span>
Layout:
</span>
<select name="landscape" data-type="boolean">
<option value="false"> Portrait </option>
<option value="true"> Landscape </option>
</select>
</label>
<label>
<span>
Paper size:
</span>
<select name="pageSize">
<option value="A0"> A0 </option>
<option value="A1"> A1 </option>
<option value="A2"> A2 </option>
<option value="A3"> A3 </option>
<option value="A4" selected> A4 </option>
<option value="A5"> A5 </option>
<option value="A6"> A6 </option>
<option value="Legal"> Legal </option>
<option value="Letter"> Letter </option>
<option value="Tabloid"> Tabloid </option>
<option value="Ledger"> Ledger </option>
</select>
</label>
<label>
<span>Scale:</span>
<input name="scale" value="1" type="number" min="0" max="2" step="0.25" />
</label>
<div class="separator">
<span> Options </span>
</div>
<label>
<span>
Header and footer:
</span>
<input name="displayHeaderFooter" type="checkbox" checked />
</label>
<label>
<span>
Background graphics:
</span>
<input name="printBackground" type="checkbox" />
</label>
<div class="separator">
<span> Margins </span>
</div>
<label>
<span>Margin type:</span>
<select name="margins.marginType">
<option value="default"> Default </option>
<option value="none"> None </option>
<option value="printableArea"> Printable area </option>
<option value="custom" selected> Custom </option>
</select>
</label>
<label>
<span>Top:</span>
<input name="margins.top" value="0.4" type="number" step="0.01" />
</label>
<label>
<span>Bottom:</span>
<input name="margins.bottom" value="0.4" type="number" step="0.01" />
</label>
<label>
<span>Left:</span>
<input name="margins.left" value="0.4" type="number" step="0.01" />
</label>
<label>
<span>Right:</span>
<input name="margins.right" value="0.4" type="number" step="0.01" />
</label>
<div class="separator">
<span>
Custom header and footer
</span>
</div>
<label>
<select name="customHeaderFooter" style="width: 100%;"></select>
<button data-bind="show-content-header-and-footer" class="show-content-header-and-footer" title="Show content HTML of header and footer">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 512.011 512.011"
style="enable-background:new 0 0 512.011 512.011;" xml:space="preserve">
<g>
<g>
<g>
<path d="M505.755,240.92l-89.088-89.088c-88.576-88.597-232.747-88.597-321.323,0L6.256,240.92
c-8.341,8.341-8.341,21.824,0,30.165l89.088,89.088c44.288,44.288,102.464,66.453,160.661,66.453s116.373-22.165,160.661-66.453
l89.088-89.088C514.096,262.744,514.096,249.261,505.755,240.92z M256.005,362.669c-58.816,0-106.667-47.851-106.667-106.667
s47.851-106.667,106.667-106.667s106.667,47.851,106.667,106.667S314.821,362.669,256.005,362.669z" />
<path
d="M256.005,192.003c-35.285,0-64,28.715-64,64s28.715,64,64,64s64-28.715,64-64S291.291,192.003,256.005,192.003z" />
</g>
</g>
</g>
</svg>
</button>
</label>
<button type="submit" class="btn">
Generate PDF
</button>
</form>
</aside>
<section class="printable">
<h1 style="background-color: #222; color: #CCC">Hello World!</h1>
<p>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Reiciendis deserunt voluptas saepe inventore
autem
facilis quam quia rerum ipsum illo. Quasi, hic. Praesentium voluptatum, accusamus esse illum quaerat
quos
quis, corrupti consequuntur explicabo ad nemo a dolorem quidem magnam non sapiente ipsam odit illo
exercitationem, facilis sunt! Iste, obcaecati debitis?
</p>
</section>
<dialog class="hide-in-print pdf-preview">
<div class="center-inline-elements">
<button data-bind="btn-close-preview" class="btn">
Close Preview
</button>
</div>
<iframe></iframe>
</dialog>
<dialog data-bind="dialog-content-header-and-footer" class="hide-in-print">
<details open>
<summary>Header</summary>
<pre data-bind="custom-header"></pre>
</details>
<details open>
<summary>Footer</summary>
<pre data-bind="custom-footer"></pre>
</details>
</dialog>
<div class="backdrop"></div>
</div>
<script src="./renderer.js"></script>
</body>
</html>
// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
let mainWindow
function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1300,
height: 700,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: path.join(__dirname, 'preload.js')
}
})
// and load the index.html of the app.
mainWindow.loadFile('index.html')
// Open the DevTools.
mainWindow.webContents.openDevTools()
}
ipcMain.on('generate-pdf', async(event, options) => {
console.log(options);
const buffer = await mainWindow.webContents.printToPDF(options)
const base64PDF = Buffer.from(buffer).toString('base64')
mainWindow.webContents.send('base64-pdf', base64PDF)
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
{
"name": "cut-alarm-jump-1cbvj",
"productName": "cut-alarm-jump-1cbvj",
"description": "My Electron application description",
"keywords": [],
"main": "./main.js",
"version": "1.0.0",
"author": "lesleyandreza",
"scripts": {
"start": "electron ."
},
"dependencies": {},
"devDependencies": {
"electron": "21.1.0"
}
}
/**
* The preload script runs before. It has access to web APIs
* as well as Electron's renderer process modules and some
* polyfilled Node.js functions.
*
* https://www.electronjs.org/docs/latest/tutorial/sandbox
*/
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})
const { ipcRenderer } = require('electron')
const { customHeadersFooters, getCustomHeaderFooter } = require('./customHeaderFooter')
const { serializeForm, base64ToUrl, pdfPreview } = require('./util')
const $form = document.querySelector('form')
const $dialogShowContentHeaderAndFooter = document.querySelector('[data-bind="dialog-content-header-and-footer"]')
const $backropDialogShowContentHeaderAndFooter = document.querySelector('[data-bind="dialog-content-header-and-footer"]+.backdrop')
const $btnShowContentHeaderAndFooter = document.querySelector('[data-bind="show-content-header-and-footer"]')
const $btnClosePreview = document.querySelector('[data-bind="btn-close-preview"]')
ipcRenderer.on('base64-pdf', (event, base64) => {
const $iframe = document.querySelector('iframe')
$iframe.src = base64ToUrl(base64)
})
$form.onsubmit = (event) => {
const formData = serializeForm(event.target)
ipcRenderer.send('generate-pdf', formData)
console.log(formData)
pdfPreview.show()
}
$btnShowContentHeaderAndFooter.onclick = (event) => {
event.preventDefault()
const customHeaderFooter = getCustomHeaderFooter();
const $customHeader = document.querySelector('[data-bind="custom-header"]')
const $customFooter = document.querySelector('[data-bind="custom-footer"]')
$customHeader.innerText = customHeaderFooter.header?.trim() || 'Not defined'
$customFooter.innerText = customHeaderFooter.footer?.trim() || 'Not defined'
$dialogShowContentHeaderAndFooter.show()
}
$backropDialogShowContentHeaderAndFooter.onclick = () => {
$dialogShowContentHeaderAndFooter.close()
}
$btnClosePreview.onclick = () => {
pdfPreview.hide()
}
document.onkeyup = (event) => {
if (event.key.toLowerCase() === 'escape') {
document.querySelector('dialog[open]')?.close?.()
}
}
* {
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: sans-serif;
accent-color: hsl(221deg 100% 61%);
outline-color: hsl(221deg 100% 61%);
}
body {
background-color: #CBCBCB;
}
button {
font-family: sans-serif;
}
.wrapper {
display: flex;
}
.sidebar {
z-index: 1;
min-width: 290px;
max-width: 290px;
height: 100vh;
padding: 1rem;
box-shadow: 3px 0px 4px -2px rgb(0 0 0 / 40%);
user-select: none;
position: sticky;
top: 0;
overflow: auto;
background-color: #FFF;
;
}
.printable {
max-width: 120ch;
z-index: 0;
background-color: #CBCBCB;
}
section {
min-height: 100vh;
width: 100vw;
padding: 1rem;
}
h1 {
margin-bottom: 1.5rem;
}
p+p {
margin-top: 1rem;
}
iframe {
display: none;
will-change: transform, opacity;
}
iframe {
display: block;
height: calc(100% - 3rem);
width: 100%;
border: 0;
border-radius: 4px;
box-shadow: 0.7px 1px 2px 1px #000;
}
input,
select,
textarea {
background: #f1f3f4;
border: none;
border-radius: 5px;
padding: 4px 8px;
width: 118px;
}
.pdf-preview {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
padding: 1rem;
padding-top: 1rem;
background-color: #191c1c;
display: none;
will-change: opacity;
z-index: 2;
}
.pdf-preview iframe {
margin-top: 1rem;
border-radius: 10px;
opacity: 0;
}
.pdf-preview.show {
display: block;
}
.pdf-preview.show iframe {
animation: fadeIn .3s, slideUp .3s;
animation-fill-mode: forwards;
animation-delay: 0.5s;
}
.form-layout {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.separator {
padding-top: 0.5rem;
}
.separator span {
font-size: 0.8rem;
font-weight: 900;
color: #777;
}
label {
display: flex;
gap: 0.5rem;
align-items: center;
}
label span {
width: 50%;
text-align: right;
font-size: 0.8rem;
color: #444;
}
.btn {
height: 2rem;
padding: 0 1rem;
background: hsl(221deg 100% 61%);
border-radius: 5px;
border: none;
color: #FFF;
font-size: 0.9rem;
outline-offset: 2px;
}
.btn[type="submit"] {
margin-top: 1rem;
}
.btn:hover,
.btn:focus-visible {
background: hsl(221deg 100% 66%);
}
.btn:active {
background: hsl(221deg 100% 51%);
transform: translateY(1px);
}
[type="checkbox"] {
height: 1.1rem;
width: 1.1rem;
}
.center-inline-elements {
text-align: center;
}
.show-content-header-and-footer {
width: 1.8rem;
height: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
padding: 2px;
border: none;
background: transparent;
opacity: 0.8;
}
.show-content-header-and-footer:hover,
.show-content-header-and-footer:focus-visible {
opacity: 0.6;
}
.show-content-header-and-footer:active {
opacity: 1;
transform: translateY(1px);
}
[data-bind="dialog-content-header-and-footer"] {
z-index: 10;
height: 50vh;
width: 50vw;
position: fixed;
margin-left: 25vw;
margin-top: 25vh;
padding: 1rem;
overflow: auto;
}
[data-bind="dialog-content-header-and-footer"][open]+.backdrop {
display: block;
}
.backdrop {
display: none;
content: '';
background: rgba(0, 0, 0, .3);
z-index: 9;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
pre {
background: #E5E5E5;
border: 1px solid #DDD;
padding: 0.5rem;
border-radius: 5px;
margin-bottom: 1rem;
color: #444;
user-select: all;
overflow-x: auto;
}
@media print {
.hide-in-print {
display: none !important;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes slideUp {
0% {
transform: translateY(2rem);
}
100% {
transform: translateY(0);
}
}
const { customHeadersFooters } = require("./customHeaderFooter")
function base64ToUrl(base64 = '', typeOfBase64 = 'application/pdf') {
const binary = atob(base64.replace(/\s/g, ''))
const len = binary.length
const buffer = new ArrayBuffer(len)
const view = new Uint8Array(buffer)
for (let i = 0; i < len; i++) {
view[i] = binary.charCodeAt(i)
}
const blob = new Blob([view], { type: typeOfBase64 })
return URL.createObjectURL(blob)
}
function serializeForm($formElement) {
return [...$formElement.elements].reduce((prev, curr) => {
const inputName = curr.name
let inputValue = null
if (curr.type === 'checkbox') {
inputValue = curr.checked
} else if (curr.type === 'number') {
inputValue = curr.valueAsNumber
} else if (curr.dataset.type === 'boolean') {
inputValue = curr.value.toLowerCase() === 'true'
} else {
inputValue = curr.value
}
if (inputName === 'customHeaderFooter') {
const customHeaderFooter = customHeadersFooters.find(e => e.id === +curr.value)
if (!customHeaderFooter.header && !customHeaderFooter.footer) {
return prev
}
return {
...prev,
headerTemplate: customHeaderFooter.header,
footerTemplate: customHeaderFooter.footer,
}
}
if (inputName.startsWith('margins')) {
let marginInputs = null
const [base, property] = inputName.split('.')
const value = curr.type === 'number' ? curr.valueAsNumber : curr.value
marginInputs = {
...(marginInputs || {}),
[property]: value
}
return {
...prev,
...{
margins: {
...(prev.margins || {}),
...marginInputs
}
}
}
}
if (inputName === '') return prev
return { ...prev, ...{ [inputName]: inputValue } }
}, {})
}
const pdfPreview = {
$pdfPreview: document.querySelector('.pdf-preview'),
show() {
this.$pdfPreview.classList.add('show')
document.body.style.overflow = 'hidden'
},
hide() {
this.$pdfPreview.classList.remove('show')
document.body.style.overflow = 'unset'
},
}
module.exports = { base64ToUrl, serializeForm, pdfPreview }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment