How to Place an Image into PDF Files with Dynamic Positions in Laravel (with Javascript)
“A pdf file of a Policy Letter that needs to add a QR Code as an e-sign in the specific place.”
This “story” was written after some discussion with no result (Github, Stackoverflow) for placing an image dynamically/after the last content of a pdf.
While now everything transforming to be digital, including correspondence with PDF files. In this “story”, I would like to share a piece thing of how to place an image file into a pdf file where the position is by our choice, with the study case of:
“A pdf file of a Policy Letter that needs to add a QR Code as an e-sign in the specific place.”
This story would use Laravel, but of course, you could apply this to another tech stack, as long as it uses Javascript. Here is the full steps in a glance:
- Upload PDF File,
- Make a draggable-div,
- Placing QR to PDF.
So, based on that glance, what we must have are:
- Laravel,
- Pdf.js,
- Interact.js,
- TCPDF along with FPDI,
- Some solid understanding of Quantum Mathematics. No, we don’t need that much ;).
Before I start, I already have a fresh-installed Laravel and use this component
<style>
#canvas-container {
position: relative;
width: max-content;
height: max-content;
padding: 0px;
}
.draggable {
display: none;
width: 87px;
height: 87px;
background-color: #9465ab;
touch-action: none;
user-select: none;
text-align: center;
padding: 30px 0;
color: white;
position: absolute;
}
</style>
<body class="antialiased">
<div class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-white">
<div class="max-w-7xl mx-auto p-6 lg:p-8">
<div class="flex justify-center dark:text-white">
<form action="" method="post" enctype="multipart/form-data">
@csrf
<!-- component -->
<div class="flex w-full items-center justify-center bg-grey-lighter">
<label
class="w-64 flex flex-col items-center px-4 py-6 bg-white dark:bg-grey-lighter text-blue rounded-lg shadow-lg tracking-wide uppercase border border-blue cursor-pointer hover:bg-blue hover:text-white">
<svg class="w-8 h-8" fill="black" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z" />
</svg>
<span class="mt-2 text-base dark:text-black leading-normal">Select a file</span>
<input type='file' name="pdf-file" class="hidden" id="document-result" accept=".pdf" required/>
</label>
</div>
{{-- The place for PDF --}}
<div id="canvas-container" class="flex w-full bg-grey-lighter">
<div class="draggable"> QR </div>
<canvas id="pdf-canvas" class="border-solid border-2 dark:border-zinc-50"> ~ PDF ~</canvas>
</div>
{{-- This is some kind of the properties that we need --}}
<input type="hidden" id="stampX" name="stampX">
<input type="hidden" id="stampY" name="stampY">
<input type="hidden" id="canvasHeight" name="canvasHeight">
<input type="hidden" id="canvasWidth" name="canvasWidth">
{{-- btn --}}
<div class="flex w-full items-center justify-center bg-black-#334155">
<button type="submit" class="w-32 mt-3 flex flex-col items-center bg-black-#334155 dark:bg-black-#334155 text-blue rounded-lg shadow-lg tracking-wide uppercase border border-blue cursor-pointer hover:bg-blue hover:text-white">
<span class="mt-2 text-base dark:text-white leading-normal">Submit</span>
</button>
</div>
</form>
</div>
<div class="bottom-0 left-0 right-0 z-40 px-4 py-3 text-center text-white bg-gray-800">
<a href="https://github.com/sponsors/taylorotwell" class="group inline-flex items-center hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="-mt-px mr-1 w-5 h-5 stroke-gray-400 dark:stroke-gray-600 group-hover:stroke-gray-600 dark:group-hover:stroke-gray-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
Sponsor
</a>
Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }})
</div>
</div>
</div>
</body>
Take a look at that code, I have 1 input file with document-result id, and 4 other input hidden that would keep some values(I would explain that after this). We also have a div with canvas-container id that contains 1) a div for QR with display = none, and 2) a canvas for a pdf page. Later on, that QR div will shown on top of a PDF page we have chosen.
And this is the result,

Now, let’s start:
- Upload PDF File
After setting up the UI, we will install pdf.js by Mozilla using this cdn,
<script type="module" src='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.269/pdf.min.mjs'></script>
<script type="module" src='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.269/pdf.worker.min.mjs'></script>
This pdf.js and worker.pdf.js would be used for loading the pdf file to a canvas, but if you prefer another package I thought it would be okay, as soon as the pdf loaded to the canvas. Now add this after the script of pdfjs,
<script>
document.querySelector("#document-result").addEventListener("change", async function(e){
var file = e.target.files[0]
if(file.type != "application/pdf"){
alert(file.name, "is not a pdf file.")
return
}
var fileReader = new FileReader();
fileReader.onload = async function() {
var typedarray = new Uint8Array(this.result);
const loadingTask = pdfjsLib.getDocument(typedarray);
loadingTask.promise.then(pdf => {
// you can now use *pdf* here
pdf.getPage(pdf.numPages).then(function(page) {
// you can now use *page* here
var scale = 1.5;
var viewport = page.getViewport({scale: scale});
var canvas = document.getElementById('pdf-canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
document.getElementById('canvasHeight').value = viewport.height;
document.getElementById('canvasWidth').value = viewport.width;
page.render({
canvasContext: canvas.getContext('2d'),
viewport: viewport
});
// Remove class border-2
canvas.classList.remove('border-2');
});
});
};
fileReader.readAsArrayBuffer(file);
document.getElementsByClassName('draggable')[0].style.display = 'block';
})
</script>
This script will listen for changes in the file input, and do the following things:
- Read the file using FileReader (the reading process is at the bottom by readAsArrayBuffer()),
- When the file is loaded, convert the file to a TypedArray (represents an array of 8-bit unsigned integers),
- Load the pdf’s TypedArray to the pdfjs (promise),
- Load the page with pdf.getPage(pdf.numPages). pdf.getPage accepts a page number, and numPages would be the total number of the pages (starting from 1), so it would load the last page of the pdf,
- After that, we set the scale for the viewport of the PDF page and placed it on a canvas,
- We also save the canvas’s height and width to the hidden input. This input would also sent to the backend for processing the stamp image,
- Then render the pdf,
- In the last, we set the draggable to be displayed (since we set the display to none before).
Now let’s try choosing a pdf file,

That’s my pdf, and an irremovable QR Box (div). And as you can see, we’ll place that Box above my name (ArKa).
2. Make a draggable-div
We will use interact.js to make the QR div draggable. Add this cdn code to load interact.js,
<script src="https://cdn.jsdelivr.net/npm/interactjs@1.10.20/dist/interact.min.js"></script>
And also,
<script>
const position = { x: 0, y: 0 }
interact('.draggable').draggable({
listeners: {
move (event) {
position.x += event.dx
position.y += event.dy
event.target.style.transform =
`translate(${position.x}px, ${position.y}px)`
},
end (event) {
var style = window.getComputedStyle(event.target);
var matrix = new WebKitCSSMatrix(style.transform);
console.log(matrix.m41, matrix.m42)
document.getElementById('stampX').value = matrix.m41;
document.getElementById('stampY').value = matrix.m42;
}
},
inertia: true,
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true
})
],
})
</script>
This code would make any element with a draggable class become draggable. The listener will listen for the move and end (stop) event of the draggable-div. The move will transform the position of the draggable-div, while the end will keep the position’s value to the stampX and stampY (the other 2 hidden inputs we created before). The inertia makes it nice-to-see, while in modifiers we modify the interaction to only its parent, check their docs for more information.
In this way, the QR div already becomes a draggable-div before we choose a pdf file, but it is still hidden as the display is none. Now, let’s try to refresh the page and choose a pdf file again. And look,

There’s no difference, but you could drag that purple QR. Now try to set it to a position where you want,

Well, it’s looking good, that QR position would later replaced by an image (as a QR). Now open your browser developer mode, check the console then move your QR,

We could see that we have a coordinate of our QR, containing 2 value of X and Y. These value, along with the pdf and its Width and Height, will be sent to the server/backend to be processed. Now, create a POST route and controller to handle this process (I know you can do it).
Before we step too far, let’s check what we sent,

*Right, I do it in controller ;).
Now we have these values.

3. Placing QR to PDF
Before we continue, I would install a QR Package to generate a QR. It’s up to you to generate it or use any image you want. I’m also using Setassign/Fpdi, along with TCPDF (If you prefer MPDF, DOMPDF, or any other package it’s okay I guess) for placing pdf.
First of all, let’s get the value from the request. Since we set the scale to 1.5 when rendering PDF to a canvas, we have to re-scale the value to 1,

After getting the values, we have to prepare the QR or any other file you want to set to those coordinates. I will generate a QR with this,

Here we’ll create a QR Image with that random name TheArKa-*.png. Please align with your needs. Now what we have to do is load our uploaded file, and set it to the TCPDF,

You could always store that file first to get the original file, but I will use it directly and set it to PDF. After that, we’ll iterate for every page of the PDF,

Take a look, the $template and $size come from the uploaded PDF, while in the beginning we already have another size from Frontend, but wait, these values are in a px unit. Is that a problem? Of course, because TCPDF would return the size in cm. So here we have to make the $canvasWidth and $canvasHeight (from Frontend) equal with $size[‘width’] and $size[‘height’] (from Backend), consecutively. Also, the coordination of the QR should be equal ($realXPosition and $realYPosition). The reduction percentage above uses the following formula,

With that formula, I got about After all those countings, and since I wanna place the QR on the last page of the PDF, I have that if statement where it’s only applicable when the page is on the last page. Then we’ll place the QR based on the $realXPosition and $realYPosition. See, there’s a 20, 20 value, where it’s means the size of the QR would be.

This is a little bit tricky since we previously set draggable-div to 87px, but now we wrote this as 20,

Remember that TCPDF uses cm as a unit value? So basically, with the above formula, we’ll get a difference in percent between Canvas and PDF Size,

It’s reduced by about 64.72%. Now, If we divide 87 by 1.5 (for the scale) we get 58, then we subtract 64.72% from it, so the result is approximately 20.46.
With these calculations, we also could make the size of draggable-div to be resizable. Well, 20.46 is not 100% accurate with 20, but it’s an art for retrying all the way, right :D.
For the last, we could just return the stamped pdf,

And here’s the result,

Isn’t it good enough, is it?
For those of you who wanna try this, I deployed the app here. Since we dunno each other, don’t upload your necessary document ;).
Note: Setasign/Fpdi is only applicable for PDF Version 1.4 for its Free Version. So for a workaround, you have to convert your uploaded file first to 1.4 using Ghostscript. This is indeed very unfortunate, but most of the PDF content is available in version 1.4.
Full Source Code here.