Attach pdf to input in ReactJs - javascript

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.

Related

Asp.net Project - Javascript button is clickable but not carrying out the function

I've been trying to integrate an api into a project that I have been working on with some friends but I'm having difficulty with getting the "ok" button to actually execute the function. It's supposed to allow you to upload a photo, click ok, and then it returns data about the plant. The "choose files button works, but the ok button doesn't.
Since the API sample was already created I tested it in a separate solution and was able to get it to work which leads me to believe that I've made a mistake in the code somewhere else or maybe there's something blocking the program from talking to API's web address. But for some reason it doesn't work within the project that I'm trying to integrate it into. (ASP.NET razor page).
I've also tried making a new button and moving the javascript tag into the header and other spots but that didn't work either, and I've run out of ideas to try. I have omitted the api key itself below for the sake of privacy. I'd really appreciate any help on the subject!
#{
ViewData["Title"] = "Identify a Plant";
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form>
<input type="file" multiple />
<!--<button type="button">OK</button> -->
<button type="button">OK</button>
</form>
<script type="text/javascript">
document.querySelector('button').onclick = function sendIdentification() {
const files = [...document.querySelector('input[type=file]').files];
const promises = files.map((file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const res = event.target.result;
console.log(res);
resolve(res);
}
reader.readAsDataURL(file)
})
})
Promise.all(promises).then((base64files) => {
console.log(base64files)
const data = {
api_key: "Die8ewFGvpw5JrRTuOEjgGR10uL--",
images: base64files,
modifiers: ["crops_fast", "similar_images"],
plant_language: "en",
plant_details: ["common_names",
"url",
"name_authority",
"wiki_description",
"taxonomy",
"synonyms"]
};
fetch('https://api.plant.id/v2/identify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
});
})
};
</script>
</body>
</html>
I think you are better off to give the button a "ID" and you don't.
and I never really did like this idea of "selecting" some button and then hoping we can attached a click event to that button. So, I always preferred that you have a button. You place a button. You specify a click event for that button. You have to really struggle to figure out which button that selector going to pick up - which one does it like?
And then when you click on that button, the code or function runs. It just a lot easier to follow.
So, your button thus is this:
<form id="form1" runat="server">
<div>
<br />
<input id="myfiles" type="file" multiple="multiple" />
<!--<button type="button">OK</button> -->
<button id="MyButton" type="button" onclick="sendIdentification()" >OK</button>
</div>
</form>
<script type="text/javascript">
function sendIdentification() {
alert('start');
const files = [...document.querySelector('input[type=file]').files];
etc.
The problem is that selector for the click event is subject to the order of the controls and things on the page - might not be picked up correct.
So, just drop in a button. State what that button supposed to do on some click event, and this should help here.
With the querySelector method you add the onClick event on a first button within the document. Since the _Layout.cshtml is rendered first, my first assumption is that you have a button in that view? What about giving an id to the button and adding the onClick event like this:
document.getElementById("myButton").onclick = function sendIdentification() {
//the code
};

To do list app delete function js

I am trying to make an extremely basic to do list. I have researched and looked at many examples to no avail. All I want to do is have the ability to click an item that has been added to my list and have it deleted. I am not sure how to access the value of what Is entered in my items, or how to manipulate those into a function.
function todoList() {
let item = document.getElementById('todoInput').value //pulling value from input box
let text = document.createTextNode(item) //turning input text into node
let newItem = document.createElement('li') //creates a list
newItem.appendChild(text) //appends task entered from input
document.getElementById('todoList').appendChild(newItem) //appends the entered task to the list
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<title>To do list</title>
</head>
<body>
<h1>To Do List</h1>
<form id="todoForm">
<input type="text" id="todoInput">
<button type="button" onclick="todoList()">Add Item</button>
</form>
<ul id="todoList"></ul>
<script src="app.js"></script>
</body>
</html>
Here is a likely course of actions. There are many ways you can do it, and here is one that is functional.
I have broken it down for you. I also renamed your add function to be a little more clear what it does:
<!DOCTYPE html>
<html>
<head>
<!-- <link rel="stylesheet" type="text/css" href="style.css"> -->
<title>To do list</title>
<!-- Put this in your style.css -->
<style>
.item {
color: blue;
}
</style>
</head>
<body>
<h1>To Do List</h1>
<form id="todoForm">
<input type="text" id="todoInput">
<button type="button" onclick="addItem()">Add Item</button>
</form>
<ul id="todoList"></ul>
<!-- <script src="app.js"></script> -->
</body>
</html>
<script>
function addItem(){
//get current number of todo Items (for creating the ID)
const currentNumberOfItems = document.querySelectorAll('.item').length
console.log(currentNumberOfItems)
console.log('Research:', document.querySelectorAll('.item'))
const item = document.getElementById('todoInput').value //pulling value from input box
const text = document.createTextNode(item) //turning input text into node
const newItem = document.createElement('li') //creates a list
newItem.id = currentNumberOfItems //give the new <li> an auto-incrementing id property
newItem.classList.add('item') //add the item class so we can search for it by class
//we didn't end up searching by class, but you can find every <li> on the page
//using console.log(document.querySelectorAll('.item'))
newItem.appendChild(text) //appends task entered from input
document.getElementById('todoList').appendChild(newItem) //appends the entered task to the list
const btn = document.createElement('button') // Create a <button> element
const t = document.createTextNode('Delete') // Create a text node
btn.appendChild(t) // Append the text to <button>
newItem.appendChild(btn) // Append <button> into the new <li>
//we are going to create an event listener on the button
//this takes 2 parameters
//first = event type (on click)
//second = callback function to run when the event is detected
btn.addEventListener('click', function(event) {
console.log(event.target.parentNode) //we want the element in which the button exists
console.log('Research:', event) //look at all the stuff in here!
deleteItem(event.target.parentNode) //run our delete function
})
}
//now that we have an event listener and know the parent
//we just have to find a way to delete the parent
//we can call the input anything, and it will be a DOM element (event.target.parentNode)
function deleteItem(parent) {
console.log(parent.id) //lets check the parent's id
//we can use element.removeChild() to ditch the todo item
//first, we have to get the <ul> somehow
const todoList = document.getElementById('todoList') //let's get the <ul> by ID
todoList.removeChild(parent) //cya later "parent that the button was inside of"
}
</script>
I tried to make this a snippet, but it seems the code editor crashes when you delete, so I will leave it like this.
Bonus
You will see I used const instead of let, because it does not allow re-assignment, which tells JavaScript and other coders that you do not plan to change that variable once it is set.
You can sample that by putting this in your JS file:
app.js
'use strict'
const test = 'cool'
test = 'not cool'
console.log(test)
Notice the behaviour now with let (swap the code for this):
'use strict'
let test = 'cool'
test = 'not cool'
console.log(test)
This some basics with "immutability" that you should research a bit when you want to do some reading. It means you dont have to worry quite as much with strange bugs when you accidently mutate some variable. const will get mad if you try.
More advanced, you can still re-assign properties on objects when using const:
const object = {
name: 'Bob Alice'
}
object.name = 'Not Bob Anymore'
When you use let, it tells yourself and other coders that you expect the value of the variable will likely change somewhere nearby in the code.
I recommend you try this out and if you ever encounter any issues, just Google it and you will quickly discover. Don't worry, nothing will blow up on you if you always use const "unless you cant". Issues will only occur in highly advanced code, with const vs. let vs. var.

Load, manipulate and save text file with javascript?

I've got a text file and want to do some find and replace operations to it inside the browser. Unfortunately my coding experience is just really elementary and complete tutorials about building web apps are far too much input at the moment.
Basically I want to upload the file into the browser, then let javascript do the find-and-replace-thing and finally want to download the changed file again.
I've already read about the HTML5 File API and was actually able to load the text file into the browser. But that is where I'm getting lost. In order to split problems up into smaller ones I thought a good next step would be to download the uploaded file again and finally learn how to put the find-and-replace action in between. But I really don't know how to go further and would appreciate any help.
Thanks so far. Benny
document.getElementById('input-file')
.addEventListener('change', getFile)
function getFile(event) {
const input = event.target
if ('files' in input && input.files.length > 0) {
placeFileContent(
document.getElementById('content-target'),
input.files[0])
}
}
function placeFileContent(target, file) {
readFileContent(file).then(content => {
target.value = content
}).catch(error => console.log(error))
}
function readFileContent(file) {
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.onload = event => resolve(event.target.result)
reader.onerror = error => reject(error)
reader.readAsText(file, "windows-1252")
})
}
<html>
<head>
<meta content="text/html; charset=ANSI" http-equiv="content-type">
<title>Text file manipulator</title>
</head>
<body>
<h1>Text file manipulator</h1>
<p>
<input type="file" id="input-file">
</p>
<p>
<textarea id="content-target" style="width:440px;height:400px;"></textarea>
</p>
</body>
</html>
screenshot of text file uploader
You can add a button and call a function in your JavaScript. Something like
<button onclick="downloadText()">Download</button>
Being the function
function downloadText(){
var content = document.getElementById('content-target').value;
var dl = document.createElement('a');
dl.setAttribute('href', 'data:text/csv;charset=utf-8,' +
encodeURIComponent(content));
dl.setAttribute('download', 'text.txt');
dl.click();
}
Inside the function you should be able to do all the modifications you want. If you give more details, I can help you with the replace section of it, but it should be something like the following:
content.replace(regex, substitute);
More information here
Working CodePen

What's the proper way to handle forms in Electron?

The form html and submit event is part of the "renderer".
The submitted data should be available in the main process.
What's the proper way to submit the form and make that data accessible in main.js ?
Should I simply use the "remote" module to pass the data to a function from main.js or is there a better approach?
We use a service (Angular) to process form data in a window. Then notify the remote, if needed.
From your renderer you can send data to the ipc, then in your main.js you catch this event and the passed form data:
// renderer.js
let ipcRenderer = require('electron').ipcRenderer;
ipcRenderer.send('submitForm', formData);
// main.js
ipcMain.on('submitForm', function(event, data) {
// Access form data here
});
You can also send messages back to the renderer from the main.js.
Either sync:
// main.js
ipcMain.on('submitForm', function(event, data) {
// Access form data here
event.returnValue = {"any": "value"};
});
Or async:
// main.js
ipcMain.on('submitForm', function(event, data) {
// Access form data here
event.sender.send('formSubmissionResults', results);
});
// renderer.js
ipcRenderer.on('formSubmissionResults', function(event, args) {
let results = args.body;
});
There are several variations on how to do this, but all are via IPC. 
IPC (inter process communication) is the only way to get data from the render process to the main process, and is event driven. The way this works is that you can use custom defined events which the process listens for and returns something when that event happens.
The example stated by #Adam Eri is a variation on the ipcMain example found in the documentation, but this method is not one size fits all.
The reason for saying that is the matter can quickly become complicated if you are trying to send events via the menu (which typically runs on the main process), or via components through a front end framework like Vue or Angular.
I will give a few examples:
Using Remote with WebContents
To your point, yes you can use electron remote, but for the purposes of forms it is not the recommended approach. Based on the documentation, the point of remote is to 
Use main process modules from the renderer process
tl:dr -This process can cause deadlocks due to its synchronous nature, can cause event object leaks (due to garbage collection), and leads to unexpected results with callbacks.
Further explanation can be had from the documentation but ultimately this is set for using items like dialog and menu in the render process.
index.js (main process)
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require ('path');
const fs = require('fs');
const os = require('os');
let window;
function createWindow(){
window = new BrowserWindow({
show: false
});
window.loadURL(`file://${__dirname}/index.html`);
window.once('ready-to-show', function (){
window.show();
});
window.webContents.openDevTools();
let contents = window.webContents;
window.on('closed', function() {
window = null;
});
}
exports.handleForm = function handleForm(targetWindow, firstname) {
console.log("this is the firstname from the form ->", firstname)
targetWindow.webContents.send('form-received', "we got it");
};
app.on('ready', function(){
createWindow();
});
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Electron App</title>
</head>
<body>
<form action="#" id="ipcForm2">
First name:<br>
<input type="text" name="firstname" id="firstname" value="John">
<br>
Last name:<br>
<input type="text" name="lastname" id="lastname" value="Smith">
<br><br>
<input id="submit" type="submit" value="submit">
</form>
<p id="response"></p>
<script src='renderFile.js'></script>
</body>
</html>
renderFile.js (Render Process)
const { remote, ipcRenderer } = require('electron');
const { handleForm} = remote.require('./index');
const currentWindow = remote.getCurrentWindow();
const submitFormButton = document.querySelector("#ipcForm2");
const responseParagraph = document.getElementById('response')
submitFormButton.addEventListener("submit", function(event){
event.preventDefault(); // stop the form from submitting
let firstname = document.getElementById("firstname").value;
handleForm(currentWindow, firstname)
});
ipcRenderer.on('form-received', function(event, args){
responseParagraph.innerHTML = args
/*
you could choose to submit the form here after the main process completes
and use this as a processing step
*/
});
Traditional IPC
index.js (Main Process)
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require ('path');
const fs = require('fs');
const os = require('os');
let window;
function createWindow(){
window = new BrowserWindow({
show: false
});
window.loadURL(`file://${__dirname}/index.html`);
window.once('ready-to-show', function (){
window.show();
});
window.webContents.openDevTools();
let contents = window.webContents;
window.on('closed', function() {
window = null;
});
}
ipcMain.on('form-submission', function (event, firstname) {
console.log("this is the firstname from the form ->", firstname)
});
app.on('ready', function(){
createWindow();
});
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Electron App</title>
</head>
<body>
<form name="ipcForm" onSubmit="JavaScript:sendForm(event)">
First name:<br>
<input type="text" name="firstname" id="firstname" value="John">
<br>
Last name:<br>
<input type="text" name="lastname" id="lastname" value="Smith">
<br><br>
<input type="submit" value="Submit">
</form>
<script src='renderFile.js'></script>
</body>
</html>
renderFile.js (Render Process)
const ipcRenderer = require('electron').ipcRenderer;
function sendForm(event) {
event.preventDefault() // stop the form from submitting
let firstname = document.getElementById("firstname").value;
ipcRenderer.send('form-submission', firstname)
}
Using WebContents
A possible third option is webContents.executeJavascript to access the renderer process from the main process. This explanation from the remote documentation section.
Summary
As you can see, there are a few options on how to handle forms with Electron. So long as you use IPC, you should be fine; its just how you use it that can get you into trouble. I have shown plain javascript options for handling forms, but there are countless ways to do so. When you bring a front end framework into the mix, it gets even more interesting.
I personally use the traditional IPC approach when I can.
Hope that clears things up for you!
i wouldnt necessarily recommend this way since it may interfere with other functioninality but its a way more concise approach
const str = `
<form action="form://submit" >
<input name="test" >
<button type="submit"> OK </button>
</form>
`
promptWindow.loadURL(`data:text/html;base64,${Buffer.from(str).toString("base64")}`)
promptWindow.webContents.session.protocol.registerStringProtocol("form", e => {
const request = new URL(e.url)
const data = request.searchParams // {test:"inputvalue"}
debugger;
})
Remote is great way to share data. Using global variables and share them with other pages of our electron application. So, based on the following IPC approach, I was able to manage it this way :
1) Add this code in the main.js file :
global.MyGlobalObject = {
variable_1: '12345'
}
2) Use this on your 1st page to update global variable value :
require('electron').remote.getGlobal('MyGlobalObject').variable_1= '4567'
3) Lastly, use something like this on your 2nd page where you'll access the modified global variable and print it :
console.log(require('electron').remote.getGlobal('MyGlobalObject').variable_1)
You can find the same thing in electron's documentation.

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

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.'"',
)
);
}
}

Categories

Resources