How to get page numbers in pdf generated by html with wkhtmltopdf - javascript

Here is our answer:
We are currently using wkhtmltopdf to generate a PDF from a given html template.
Some background information:
We are using Sulu CMF to build our back end, which is based on Symfony2.
The KnpSnappy Bundle is used as a Symfony wrapper for wkhtmltopdf.
How we generate PDFs:
As many PDFs share the same header and footer we have created a BasePDFBundle which offers a PDFManager to build the PDF on the fly by a given TWIG template. Per default a generic header and footer (usually with the customer's name and logo) is included.
The footer Problem / Page numbers in the footer (or header):
It is very useful to add page numbers to a PDFs eg. for orders, however most of our content is added dynamically (eg a product list). As the styling of the PDF can change and the content itself is dynamically added there had to be a quick and easy way to add the current and the total page to the generated PDF. Here is what we did:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<base href="{{ app.request.schemeAndHttpHost }}" />
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link rel="stylesheet" href="{{ asset('bundles/pdfbase/css/pdfstyles.css') }}"/>
</head>
<body class="footer">
<div class="footer-container">
<div class="footer-widget">
<b>FooBar Company Name</b>
</div>
<div class="text-align-right">
<span class="page"></span>/<span class="topage"></span>
</div>
</div>
<script type="text/javascript">
(function() {
// get all url parameters and transform it to a list
// pdf generator adds a param for current and the total page sum
// add the value to the html elements
var urlParams = document.location.search.substring(1).split('&');
var urlParamsList = {};
var pagingElements = ['topage', 'page'];
for (var i in urlParams) {
var param = urlParams[i].split('=', 2);
urlParamsList[param[0]] = unescape(param[1]);
}
for (var i in pagingElements) {
var elem = document.getElementsByClassName(pagingElements[i]);
for (var j = 0; j < elem.length; ++j) {
elem[j].textContent = urlParamsList[pagingElements[i]];
}
}
})();
</script>
</body>
Yes the variable names of page and topage could be better, however they are the same as the KnpSnappy wrapper uses when merging twig templates to the final PDF template. This is the easiest way to get the current and total page number because you can let the wrapper do all the calculations.
In the end you simply have to replace the text of html tags and thats it!
Differences between your local machine and server:
As wkhtmltopdf opens a virtual browser to "render" the twig templates this could lead to errors in your pdf generation on your server. We found out it is not a good idea to use event tags like <body onload="doSomething()"> you rather should trigger your javascript code like we did it in the example above.

If you are using KnpSnappy as wrapper of wkhtmltopdf then you can setup various options for your pdf.
see "Footers And Headers:" section in wkhtmltopdf documentation, here http://wkhtmltopdf.org/usage/wkhtmltopdf.txt
[page] Replaced by the number of the pages currently being printed
[topage] Replaced by the number of the last page to be printed
Following is sample of Symfony2 controller, check footer-html option in $pdfOptions array, where I have used both placeholder to print page number in footer of each page of pdf file.
<?php
namespace Rm\PdfBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
/**
* Pdf controller
*/
class PdfController extends Controller
{
/**
* Serve pdf
*
* #Route("/article/{slug}", name="rm_pdf_view")
* #Method({"GET"})
*/
public function viewAction(Request $request, $slug)
{
// build html
$html = $this->renderView('RmPdfBundle:Pdf:view.html.twig', array(
'data' => $yourDataToTemplate,
));
// prepare pdf options
$pdfOptions = array(
'footer-html' => '<p>Page : [page] of [pageTo]</p>',
'footer-font-size' => '10',
'page-size' => 'A4',
'orientation' => 'Portrait',
'margin-top' => 10,
'margin-bottom' => 20,
'margin-left' => 15,
'margin-right' => 15,
);
// file name of pdf
$pdfFileName = "nameOfYourPdfFile.pdf";
// server pdf file to user using knp_snappy.pdf service
return new Response(
$this->get('knp_snappy.pdf')->getOutputFromHtml($html, $pdfOptions),
200,
array(
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="'.$pdfFileName.'"',
)
);
}
}

Related

Attach pdf to input in ReactJs

I am making a react application where I am in the need to generate a pdf from specific div.
Current scenario:
-> Made an app with a printable div and upon click button, the pdf is generated and downloaded.
Code:
<div id="printable-div">
<h1>Generate PDF</h1>
<p>Create a screenshot from this div, and make it as a PDF file.</p>
<p style={{ color: "red" }}>
*Then do not download instead attach to contact us form as attachment.
</p>
</div>
<button id="print" onClick={printPDF}>
Contact Us
</button>
printPDF Function:
const printPDF = () => {
setIsShowContact(true);
const domElement = document.getElementById("printable-div");
html2canvas(domElement).then((canvas) => {
const doc = new jsPdf();
doc.save(`attachment.pdf`);
});
};
On click of the Contact Us button, two actions happen.
-> Pdf file is downloaded.
-> A form with inputs Name, Email and Attachment will be shown.
Working Codesandbox:
Requirement:
Here the requirement is onclick of the Contact Us button, the pdf should be generated but not downloadable instead the generate pdf needs to be attached to the input type="file" in the contact form.
We need to send information of the data user have right now in the specific div id="printable-div" to backend api as pdf attachment on click of the contact button.
In real application, this is like an enquiry of a product, where user
selects a product with some config's and finally that product info
will be shown to user based on the config they choosen. Then the user will
click Contact Us button, so the printable-div will have that product
information which user searched, so we need to capture it as pdf and
attach to input and send to backend on form submission.
Kindly help me with the inputs on achieving this scenario of making the generated pdf to attach as attachment to the input field.
The problem
1. You need to convert the PDF correctly, although the intention is to attach the PDF in the input field, you are downloading a blank PDF.
The first step would be to download the PDF correctly, referring to the DIV element whose id is printable-div, and after that, instead of downloading, attach it to the input field.
The invalid code is here:
const printPDF = () => {
setIsShowContact(true);
const domElement = document.getElementById("printable-div");
html2canvas(domElement).then((canvas) => {
const doc = new jsPdf(); <<- YOU`RE CREATING AN EMPTY PDF AND
doc.save('attachment.pdf'); <<- DOWNLOADING THIS EMPTY PDF
});
};
The solution is very simple, just use the argument
canvas passed to the callback function instead of generating a new PDF
2. You need to append the .files property and not add
instead the generate pdf needs to be attached to the input type="file" in the contact form.
It is impossible to add new items to the .files of input[type="file"] field that belongs to the FileList class, on the other hand, it is possible to change it, that is, remove the old FileList and attach a new one with the necessary file(s).
Which in this example would just be a single file.
The solution
1. You need to convert the canvas that was passed as a callback from the html2canvas function to a file.
You can do this in the following way:
const canvas2file = (canvas, fileName = 't.jpg') =>
new Promise(resolve => {
canvas.toBlob((blob) => {
resolve(new File([blob], fileName, { type: "image/jpeg" }))
}, 'image/jpeg');
})
2. You need to use this function in the promise that is expected by the html2canvas function, that is:
html2canvas(domElement)
.then(canvas2file)
3. You will need to get a reference (or document.querySelector / document.getElementXXX) to the input field whose type is file, and also a state variable for the file itself that was converted previously (by the canvas2file function), that is:
function App() {
...
const fileRef = useRef(); //Reference to input[type="file"]
const [file, setFile] = useState(); //State variable that contains the File
...
}
4. Modify the printPDF function to save the File to the state variable
const printPDF = () => {
setIsShowContact(true);
const domElement = document.getElementById("printable-div");
html2canvas(domElement)
.then(canvas2file)
.then(setFile);
};
5. Use the useEffect hook to detect the change of the File in the state variable, that is, every time the user clicks on "Contact Us", a new File will be generated through the canvas2file function, and this file will be stored in the file state variable.
After detecting this change, we remove the .files (of type FileList) from the input[type="file"] and we will re-attach a new FileList to the input, example:
useEffect(() => {
if(!fileRef.current) return;
let list = new DataTransfer();
list.items.add(file);
fileRef.current.files = list.files;
console.log(fileRef.current)
}, [file])
The code
const { useEffect, useRef, useState } = React;
const canvas2file = (canvas, fileName = 't.jpg') =>
new Promise(resolve => {
canvas.toBlob((blob) => {
resolve(new File([blob], fileName, { type: "image/jpeg" }))
}, 'image/jpeg');
})
function App() {
const [isShowContact, setIsShowContact] = useState(false);
const fileRef = useRef();
const [file, setFile] = useState();
useEffect(() => {
if(!fileRef.current) return;
let list = new DataTransfer();
list.items.add(file);
fileRef.current.files = list.files;
console.log(fileRef.current)
}, [file])
const printPDF = () => {
setIsShowContact(true);
const domElement = document.getElementById("printable-div");
html2canvas(domElement)
.then(canvas2file)
.then(setFile);
};
return (
<div className="App">
<div id="printable-div">
<h1>Generate PDF</h1>
<p>Create a screenshot from this div, and make it as a PDF file.</p>
<p style={{ color: "red" }}>
*Then do not download instead attach to contact us form as attachment.
</p>
</div>
<br />
<button id="print" onClick={printPDF}>
Contact Us
</button>
<br />
<br />
<br />
{isShowContact && (
<form>
<div id="contact">
<div className="block">
<label>Name:</label>
<input type="text" defaultValue="John Doe" />
</div>
<br />
<div className="block">
<label>Email:</label>
<input type="email" defaultValue="xyz#example.com" />
</div>
<br />
<div className="block">
<label>Table pdf as attachment:</label>
<input ref={fileRef} type="file" />
</div>
</div>
</form>
)}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
label {
display: inline-block;
width: 75x;
text-align: right;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<script src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Try this
Due to Stackoverflow Sandbox policies, the code above will probably not run, for this reason I am hosting the working code on Codesandbox.
Demo URL: https://codesandbox.io/s/html2canvas-jspdf-forked-5ennvt?file=/src/index.js
You cannot set the value of a file input unfortunately, even if you give it a "file" yourself. You will have to intercept the form submission and construct your own request while adding the attachment. FormData API is useful for that.
Here's the link to the updated sandbox: https://codesandbox.io/s/html2canvas-jspdf-forked-eun59q?file=/src/index.js
And here's the used jsPDF function docs: https://artskydj.github.io/jsPDF/docs/jsPDF.html#output
From what I understand, you're essentially trying to take a screenshot of an area and send that in the form data.
You can use a library called use-react-screenshot. It's really easy to implement and you'll need to set the reference area using the createRef hook from react.
Once you've got the screenshot, you can silently upload that somewhere to a database, alongside that form data, so that you can reference the screenshot when looking at the form data.
With the current method, I don't think there is a way to set the file input programmatically.
I hope this helps.

Why did fetch() API failed to retrieve custom HIBP JSON data?

I am trying to use the Have I Been Pwned? API to retrieve a list of breaches for a given email account.
I retrieve this list using the fetch() API. In the browser it looks like there is a connection to the HIBP website but the expected breaches are not visible.
I think this is a JSON problem because the API returns results without a root tree (?) (e.g. [breaches:{"Name"... - only the {"Name"}), so I think I'm making a mistake at the iteration step in the JS file. Also, I'm not calling the 'retrieve' function in the HTML file correctly because the browser throws an error: 'Uncaught ReferenceError: retrieve is not defined', but this is a side-issue (fetch('https://haveibeenpwned.com/api/v2/breachedaccount/test#example.com') doesn't work either).
This is my first week working with JS, fetch(), and JSON, so I consulted a couple of sources before asking this question (but I still can't figure it out, after a couple of days):
How to Use the JavaScript Fetch API to Get Data
fetch API
API methods for HaveIBeenPwnd.com (unofficial)
Where is the actual problem?
The index.html file:
<!DOCTYPE html>
<html lang=en>
<head>
<title>test</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
</head>
<body id="top">
<header id="header">
<div class="content">
<h1 style="text-align: center">Put an email in this box</h1>
<input type="email" id="InputBox" value="" autocapitalize="off" spellcheck="false" />
<button type="submit" id="PwnedButton" onclick="retrieve">pwned?</button>
<ul id="results"></ul>
</div>
</header>
<script src="test.js"></script>
</body>
</html>
The test.js file (I know that JS is an interpreted language - so empty characters affect execution speed - but I made it more readable for this example):
function createNode(element) {
return document.createElement(element); // Create the type of element you pass in the parameters
}
function append(parent, el) {
return parent.appendChild(el); // Append the second parameter(element) to the first one
}
const account = document.getElementById('InputBox');
const PwnedButton = document.getElementById('PwnedButton');
const results = document.getElementById('results');
fetch('https://haveibeenpwned.com/api/v2/breachedaccount/' + account)
.then((resp) => resp.json()) // Transform the data into json
.then(function(retrieve) {
let breaches = retrieve.Name; // Get the results
return breaches.map(function(check) { // Map through the results and for each one run the code below
let span = createNode('span'); // Create the element we need (breach title)
span.innerHTML = `${breaches}`;
append(results, span);
})
})
.catch(function(error) {
console.log(JSON.stringify(error));
});
let breaches = retrieve.Name;
retrieve is not an object with a Name property.
It is an array containing multiple objects, each of which has a Name property.
You have to loop over it.
e.g.
retrieve.forEach( item => {
let breaches = retrieve.Name;
console.log(breaches);
});
breaches.map
… and the Name is a string, so you can't map it. You can only map an array (like the one you have in retrieve).
I have created working version of what are you possible going to implement, taking Name field from result. https://jsfiddle.net/vhnzm1fu/1/ Please notice:
return retrieve.forEach(function(check) {
let span = createNode('span');
span.innerHTML = `${check.Name}<br/>`;
append(results, span);
})

How to load external html template?

Given HTML code such :
<!-- 2. Anchor -->
<div id="anchor">This div is the <b>#anchor</b>.</div>
<!-- 3. Template -->
<script id="tpl" type="text/template">
{{#people}}
<div><img src="{{photo}}"><b>{{family}} {{name}}</b> — {{title}}, {{place}} : {{introduction}}.</div>
{{/people}}
</script>
Given JS/Handlebars such as :
<!--4. Handlebars.js slingshot -->
//4a.function creation
var slingshot = function (url, tplId, anchor) {
$.getJSON(url, function(data) {
var template = $(tplId).html();
var stone = Handlebars.compile(template)(data);
$(anchor).append(stone);
});
}
slingshot('data.json', '#tpl', '#anchor'); // since url = 'data.json' , we can use both notations.
How to externalize the my 3. Template (#tpl) into a proper .txt text file (or other extension) ? How to load it back ? so I may use the same template into various .html webpages.
Full code : http://bl.ocks.org/hugolpz/8075193 / http://bl.ocks.org/hugolpz/raw/8075193/
Put the following template content into a file named test.handlebars
{{#people}}
<div><img src="{{photo}}">
<b>
{{family}} {{name}}
</b> — {{title}},
{{place}} : {{introduction}}.
</div>
{{/people}}
Write a function which will use the template as below
function getTemplate(name) {
if (Handlebars.templates === undefined || Handlebars.templates[name] === undefined) {
$.ajax({
url : name + ".handlebars",
success : function(data) {
if (Handlebars.templates === undefined) {
Handlebars.templates = {};
}
Handlebars.templates[name] = Handlebars.compile(data);
},
async : false
});
}
return Handlebars.templates[name];
}
In the main program you can write the below statement to insert the template contents into div with id="anchor", as shown below
var Template = getTemplate("test")
this.$("#anchor).append(Template(data));
where data is the contents of a json file or some db query output which will give you the values meant for the following attributes in json format
people, twitter, name, family, photo, title, place, introduction
I'm assuming you have already compiled your template. So you can use the technique I have described in Bootstrapping Multiple Instances of an HandlebarsJS Template Into a Page.
Hook and libs
Place this in your index.html:
<div class="hook" data-json="data/whatever.json"></div>
and the JavaScript libs
<!-- Helper to inject data-set in templates instance -->
<script src="scripts/template-loader.js"></script>
<!-- Get the (compiled) template -->
<script src="scripts/myTemplate.hbs.js"></script>
template-loader.js helper
$(function(){
'use strict';
var compiledTemplate = myApp.Templates['app/templates/myTemplate.hbs'];
$('.hook').each(function(i, h){ # h = current hook
var url = $(h).data('json'); # data-set's url
$.getJSON(url).then(function (json) { # fetch data-set
var tpl = compiledTemplate( json ); # inject data into template
$(h).html(tpl); # inflate template in page
});
});
});
Please read the complete article for further details.

Generate javascript file on the fly in asp.net mvc

Friends,
I am trying to use DyGraph in my application. Please look at the code below -
<head>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
<title>crosshairs</title>
<script type="text/javascript" src="dygraph-combined.js"></script>
<script type="text/javascript" src="data.js"></script>
</head>
The code uses data.js file containing function to get some static data.
I want data.js to be generated using a controller method so that it will generate data using database.
Can anybody help me out to resolve this issue.
Thanks for sharing your valuable time.
You could define a controller action:
public ActionResult Data()
{
// Obviously this will be dynamically generated
var data = "alert('Hello World');";
return JavaScript(data);
}
and then:
<script type="text/javascript" src="<%= Url.Action("Data", "SomeController") %>"></script>
If you have some complex script that you don't want to generate in the controller you could follow the standard MVC pattern by defining a view model:
public class MyViewModel
{
... put required properties
}
a controller action which would populate this view model and pass it to the view:
public ActionResult Data()
{
MyViewModel model = ...
Response.ContentType = "application/javascript";
return PartialView(model);
}
and finally a view which in this case will be the javascript representation of the view model (~/Views/SomeController/Data.ascx):
<%# Control
Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<MyViewModel>" %>
alert(<%= new JavaScriptSerializer().Serialize(Model.Name) %>);
Full Disclosure
This answer is copy/pasted from another question:
Dynamically generated Javascript, CSS in ASP.NET MVC
This answer is similar to other answers here.
This answer uses cshtml pages rather than ascx controls.
This answer offers a View-Only solution rather than a Controller-Only solution.
I don't think my answer is 'better' but I think it might be easier for some.
Dynamic CSS in a CSHTML File
I use CSS comments /* */ to comment out a new <style> tag and then I return; before the closing style tag:
/*<style type="text/css">/* */
CSS GOES HERE
#{return;}</style>
Dynamic JS in a CSHTML File
I use JavaScript comments // to comment out a new <script> tag and then I return; before the closing script tag:
//<script type="text/javascript">
JAVASCRIPT GOES HERE
#{return;}</script>
MyDynamicCss.cshtml
#{
var fieldList = new List<string>();
fieldList.Add("field1");
fieldList.Add("field2");
}
/*<style type="text/css">/* */
#foreach (var field in fieldList) {<text>
input[name="#field"]
, select[name="#field"]
{
background-color: #bbb;
color: #6f6f6f;
}
</text>}
#{return;}</style>
MyDynamicJavsScript.cshtml
#{
var fieldList = new List<string>();
fieldList.Add("field1");
fieldList.Add("field2");
fieldArray = string.Join(",", fieldList);
}
//<script type="text/javascript">
$(document).ready(function () {
var fieldList = "#Html.Raw(fieldArray)";
var fieldArray = fieldList.split(',');
var arrayLength = fieldArray.length;
var selector = '';
for (var i = 0; i < arrayLength; i++) {
var field = fieldArray[i];
selector += (selector == '' ? '' : ',')
+ 'input[name="' + field + '"]'
+ ',select[name="' + field + '"]';
}
$(selector).attr('disabled', 'disabled');
$(selector).addClass('disabled');
});
#{return;}</script>
No Controller Required (using Views/Shared)
I put both of my dynamic scripts into Views/Shared/ and I can easily embed them into any existing page (or in _Layout.cshtml) using the following code:
<style type="text/css">#Html.Partial("MyDynamicCss")</style>
<script type="text/javascript">#Html.Partial("MyDynamicJavaScript")</script>
Using a Controller (optional)
If you prefer you may create a controller e.g.
<link rel="stylesheet" type="text/css" href="#Url.Action("MyDynamicCss", "MyDynamicCode")">
<script type="text/javascript" src="#Url.Action("MyDynamicJavaScript", "MyDynamicCode")"></script>
Here's what the controller might look like
MyDynamicCodeController.cs (optional)
[HttpGet]
public ActionResult MyDynamicCss()
{
Response.ContentType = "text/css";
return View();
}
[HttpGet]
public ActionResult MyDynamicJavaScript()
{
Response.ContentType = "application/javascript";
return View();
}
Notes
The controller version is not tested. I just typed that off the top of my head.
After re-reading my answer, it occurs to me it might be just as easy to comment out the closing tags rather than use the cshtml #{return;}, but I haven't tried it. I imagine it's a matter of preference.
Concerning my entire answer, if you find any syntax errors or improvements please let me know.

How to do page numbering in header/footer htmls with wkhtmltopdf?

I'm developing an electronic invoicing system, and one of our features is generating PDFs of the invoices, and mailing them. We have multiple templates for invoices, and will create more later, so we decided to use HTML templates, generate HTML document, and then convert it to PDF. But we're facing a problem with wkhtmltopdf, that as far as I know (I've been Googleing for days to find the solution) we cannot simply both use HTML as header/footer, and show page numbers in them.
In a bug report (or such) ( http://code.google.com/p/wkhtmltopdf/issues/detail?id=140 ) I read that with JavaScript it is achievable this combo. But no other information on how to do it can be found on this page, or elsewhere.
It is, of course not so important to force using JavaScript, if with wkhtmltopdf some CSS magic could work, it would be just as awesome, as any other hackish solutions.
Thanks!
Actually it's much simpler than with the code snippet. You can add the following argument on the command line: --footer-center [page]/[topage].
Like richard mentioned, further variables are in the Footers and Headers section of the documentation.
Among a few other parameters, the page number and total page number are passed to the footer HTML as query params, as outlined in the official docs:
... the [page number] arguments are sent to the header/footer html documents in GET fashion.
Source: http://wkhtmltopdf.org/usage/wkhtmltopdf.txt
So the solution is to retrieve these parameters using a bit of JS and rendering them into the HTML template. Here is a complete working example of a footer HTML:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script>
function substitutePdfVariables() {
function getParameterByName(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
function substitute(name) {
var value = getParameterByName(name);
var elements = document.getElementsByClassName(name);
for (var i = 0; elements && i < elements.length; i++) {
elements[i].textContent = value;
}
}
['frompage', 'topage', 'page', 'webpage', 'section', 'subsection', 'subsubsection']
.forEach(function(param) {
substitute(param);
});
}
</script>
</head>
<body onload="substitutePdfVariables()">
<p>Page <span class="page"></span> of <span class="topage"></span></p>
</body>
</html>
substitutePdfVariables() is called in body onload. We then get each supported variable from the query string and replace the content in all elements with a matching class name.
To show the page number and total pages you can use this javascript snippet in your footer or header code:
var pdfInfo = {};
var x = document.location.search.substring(1).split('&');
for (var i in x) { var z = x[i].split('=',2); pdfInfo[z[0]] = unescape(z[1]); }
function getPdfInfo() {
var page = pdfInfo.page || 1;
var pageCount = pdfInfo.topage || 1;
document.getElementById('pdfkit_page_current').textContent = page;
document.getElementById('pdfkit_page_count').textContent = pageCount;
}
And call getPdfInfo with page onload
Of course pdfkit_page_current and pdfkit_page_count will be the two elements that show the numbers.
Snippet taken from here
From the wkhtmltopdf documentation (http://madalgo.au.dk/~jakobt/wkhtmltoxdoc/wkhtmltopdf-0.9.9-doc.html) under the heading "Footers and Headers" there is a code snippet to achieve page numbering:
<html><head><script>
function subst() {
var vars={};
var x=document.location.search.substring(1).split('&');
for(var i in x) {var z=x[i].split('=',2);vars[z[0]] = unescape(z[1]);}
var x=['frompage','topage','page','webpage','section','subsection','subsubsection'];
for(var i in x) {
var y = document.getElementsByClassName(x[i]);
for(var j=0; j<y.length; ++j) y[j].textContent = vars[x[i]];
}
}
</script></head><body style="border:0; margin: 0;" onload="subst()">
<table style="border-bottom: 1px solid black; width: 100%">
<tr>
<td class="section"></td>
<td style="text-align:right">
Page <span class="page"></span> of <span class="topage"></span>
</td>
</tr>
</table>
</body></html>
There are also more available variables which can be substituted other than page numbers for use in Headers/Footers.
Safe approach, even if you are using XHTML (for example, with thymeleaf). The only difference with other's solution is the use of // tags.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<script>
/*<![CDATA[*/
function subst() {
var vars = {};
var query_strings_from_url = document.location.search.substring(1).split('&');
for (var query_string in query_strings_from_url) {
if (query_strings_from_url.hasOwnProperty(query_string)) {
var temp_var = query_strings_from_url[query_string].split('=', 2);
vars[temp_var[0]] = decodeURI(temp_var[1]);
}
}
var css_selector_classes = ['page', 'topage'];
for (var css_class in css_selector_classes) {
if (css_selector_classes.hasOwnProperty(css_class)) {
var element = document.getElementsByClassName(css_selector_classes[css_class]);
for (var j = 0; j < element.length; ++j) {
element[j].textContent = vars[css_selector_classes[css_class]];
}
}
}
}
/*]]>*/
</script>
</head>
<body onload="subst()">
<div class="page-counter">Page <span class="page"></span> of <span class="topage"></span></div>
</body>
Last note: if using thymeleaf, replace <script> with <script th:inline="javascript">.
My example shows how to hide some text on a particular page, for this case it shows the text from page 2 onwards
<span id='pageNumber'>{#pageNum}</span>
<span id='pageNumber2' style="float:right; font-size: 10pt; font-family: 'Myriad ProM', MyriadPro;"><strong>${siniestro.numeroReclamo}</strong></span>
<script>
var elem = document.getElementById('pageNumber');
document.getElementById("pageNumber").style.display = "none";
if (parseInt(elem.innerHTML) <= 1) {
elem.style.display = 'none';
document.getElementById("pageNumber2").style.display = "none";
}
</script>
Right From the wkhtmltopdf Docs
Updated for 0.12.6.
Footers And Headers:
Headers and footers can be added to the
document by the --header-* and --footer* arguments respectively. In
header and footer text string supplied to e.g. --header-left, the
following variables will be substituted.
[page] Replaced by the number of the pages currently being printed
[frompage] Replaced by the number of the first page to be printed
[topage] Replaced by the number of the last page to be printed
[webpage] Replaced by the URL of the page being printed
[section] Replaced by the name of the current section
[subsection] Replaced by the name of the current subsection
[date] Replaced by the current date in system local format
[isodate] Replaced by the current date in ISO 8601 extended format
[time] Replaced by the current time in system local format
[title] Replaced by the title of the of the current page object
[doctitle] Replaced by the title of the output document
[sitepage] Replaced by the number of the page in the current site being converted
[sitepages] Replaced by the number of pages in the current site being converted
As an example specifying --header-right "Page [page] of [topage]", will result in the text "Page x of y" where x is the
number of the current page and y is the number of the last page, to
appear in the upper left corner in the document.
Headers and footers can also be supplied with HTML documents. As an
example one could specify --header-html header.html, and use the
following content in header.html:
<!DOCTYPE html>
<html>
<head><script>
function subst() {
var vars = {};
var query_strings_from_url = document.location.search.substring(1).split('&');
for (var query_string in query_strings_from_url) {
if (query_strings_from_url.hasOwnProperty(query_string)) {
var temp_var = query_strings_from_url[query_string].split('=', 2);
vars[temp_var[0]] = decodeURI(temp_var[1]);
}
}
var css_selector_classes = ['page', 'frompage', 'topage', 'webpage', 'section', 'subsection', 'date', 'isodate', 'time', 'title', 'doctitle', 'sitepage', 'sitepages'];
for (var css_class in css_selector_classes) {
if (css_selector_classes.hasOwnProperty(css_class)) {
var element = document.getElementsByClassName(css_selector_classes[css_class]);
for (var j = 0; j < element.length; ++j) {
element[j].textContent = vars[css_selector_classes[css_class]];
}
}
}
}
</script></head>
<body style="border:0; margin: 0;" onload="subst()">
<table style="border-bottom: 1px solid black; width: 100%">
<tr>
<td class="section"></td>
<td style="text-align:right">
Page <span class="page"></span> of <span class="topage"></span>
</td>
</tr>
</table>
</body>
</html>
ProTip
If you are not using certain information like the webpage, section, subsection, subsubsection, then you should remove them. We are generating fairly large PDFs and were running into a segmentation fault at ~1,000 pages.
After a thorough investigation, it came down to removing those unused variables. No we can generate 7,000+ page PDFs without seeing the Segmentation Fault.
I have not understood the command line en finally I find the solution to put this information directly in the controller without any JS en command line.
In my controller when I call the format.pdf I just put the line footer:
format.pdf do
render :pdf => "show",
page_size: 'A4',
layouts: "pdf.html",
encoding: "UTF-8",
footer: {
right: "[page]/[topage]",
center: "Qmaker",
},
margin: { top:15,
bottom: 15,
left: 10,
right: 10}
end
The way it SHOULD be done (that is, if wkhtmltopdf supported it) would be using proper CSS Paged Media: http://www.w3.org/TR/css3-gcpm/
I'm looking into what it will take now.

Categories

Resources