Rearrangement of pages when creating paginated pdf using html2canvas (async) - javascript

I have a dynamic page which depending on some information will create more or less sections. I want to loop through those, make a canvas out of them and bring those sections to a pdf.
I had the page previously running with this code, but i had to change the sections.
var pagina = 0;
var paginas_totales = parseInt('<?= $pagina ?>');
var doc = new jsPDF({
orientation: "landscape",
unit: "mm",
format: [297, 238]
});
$('.generate').on('click', function() {
nextStep();
});
function generaDescarga() {
pagina++;
if(pagina == paginas_totales) {
doc.save('Report <?= html_entity_decode($dataSampling['samp_name']) ?>.pdf');
window.scrollTo(0,0)
} else {
nextStep()
}
}
var j = -1;
function nextStep(){
j++;
if(j >= paginas_totales) return;
$('#loader .contenedor_loader span').text(`Creando página ${j + 1} / ${paginas_totales}`)
//Fix imagen cut
document.querySelector(".page-"+j).scrollIntoView()
setTimeout(() => {
html2canvas(
document.querySelector(".page-"+j),
{
// allowTaint: true,
backgroundColor:"#0d3451",
scale: 2,
userCORS: true,
onrendered: function(canvas) {
var imgData = canvas.toDataURL("image/jpeg", 1.0);
doc.addImage(imgData, 'JPEG', 0, 0);
if (j != paginas_totales - 1) {
doc.addPage();
}
generaDescarga();
}
}
).then(canvas => {// document.body.appendChild(canvas)}
);
}, 10);
}
From here i got plenty of errors, most of them because the asyncronity of the library of html2canvas where i am struggling.
My actual code looks like this
var paginas_totales = parseInt('<?= $pagina ?>');
rendered =0;
$('.generate').on('click', function() {
showLoading();
var doc = new jsPDF({orientation: "landscape",unit: "pt",format: [1125, 900]});
//Generation of empty structure
for (let i = 1; i < paginas_totales; i++) {doc.addPage();}
doc.save('Report <?= $dataSampling['samp_name'] ?>.pdf');
for (let i = 0; i <paginas_totales ; i++) {
var div_renderizar = document.querySelector(".page-"+i);
if (div_renderizar != null){
generateCanvas( doc, i, div_renderizar ,function() {
rendered++;
if (rendered >= paginas_totales){
doc.save('Report.pdf');
hideLoading();
}
});
}else{
console.log("Failing page "+i);
hideLoading();
}
}
});
function generateCanvas(doc, i, div_renderizar, callback){
html2canvas(
div_renderizar,
{ allowTaint: true,
scale: 2,
userCORS: true
}
).then(function (canvas){
let imgData = canvas.toDataURL("image/jpeg", 1.0);
doc.setPage(i);
doc.addImage(imgData, 'JPEG', 0, 0);
}).catch( error => {
console.error("Falló generateCanvas=> "+error);
console.log(new Error().stack);
hideLoading();
});
callback();
}
Where im getting blank pages in the pdf.
At this point i've already managed to get a pdf with all pages but i get it in a random* order, which i guess is due the asyncronity of the library (lighter pages go first)
I've tried adding "async" and "await".
I've updated the libraries to a CDN for both html2canvas and jsPDF
I've tried lots of tutorials like
https://solveforum.com/forums/threads/solved-html2canvas-and-jspdf-rendering-multiple-divs-blank-pdfs.197029/
https://www.freakyjolly.com/html2canvas-multipage-pdf-tutorial/
but most of them focus on static page or pagination depending on height
I've tried different approaches suggested on other SO pages like
HTML2Canvas creating multiple divs
html2canvas screenshot keeps turning up blank (this one because it was generating blank pages)
But the ORDER on the pages go crazy and the log isn't helping, is just telling me that the error is in this function, or in the .min external file, but not what i need.
So what i need is
Create pdf object
Loop for elements like $(".page-"+i) from 0 to last index
Add page for element
Create canvas for that element and insert that IN ORDER into the pdf
Download/Preview the results
The html structure is working perfect, having some pdfs being correctly generated proves this.
Both html2canvas and jsPDF are correctly installed/configured
The issue im sure its between point 2 and 4.
Hope its clear enough. Thank you for all help, really stuck on this, i have tried a lot of approaches, but i wanted to be readable.

Related

trying to modify the js function so that it gives the same output as it was when called the other js function

I am exporting the content on the webpage to the PDF file, for this i have used jsPDF API and i could able to get it work but now i want to use html2PDF as it resolves few issues which were faced when using jsPDF API.
I have written the function $scope.exportUsingJSPDF which is called when the button Export Using JSPDF is clicked. Similarly i want to implement the function $scope.exportUsingHTML2PDF which uses html2PDF API but could not succeed. Any inputs on how to modify $scope.exportUsingHTML2PDF so that it iterates the divs and shows the div content as shown when invoked using $scope.exportUsingJSPDF by clicking Export using JSPDF API.
Complete online example: https://plnkr.co/edit/454HUFF3rmLlkXLCQkbx?p=preview
js code:
//trying to implement the below function same as $scope.exportUsingJSPDF, so
// that when user click on Export using HTML2PDF button, it exports the content to the PDF and generaes the PDF.
$scope.exportUsingHTML2PDF = function(){
var pdf = new jsPDF('l', 'pt', 'a4');
var pdfName = 'test.pdf';
pdf.canvas.height = 72 * 11;
pdf.canvas.width = 72 * 8.5;
html2pdf(document.getElementByClassName("myDivClass"), pdf, function(pdf){
pdf.save(pdfName);
});
}
$scope.exportUsingJSPDF = function() {
var pdf = new jsPDF('p','pt','a4');
var pdfName = 'test.pdf';
var options = { pagesplit: true};
var $divs = $('.myDivClass') //jQuery object of all the myDivClass divs
var numRecursionsNeeded = $divs.length -1; //the number of times we need to call addHtml (once per div)
var currentRecursion=0;
//Found a trick for using addHtml more than once per pdf. Call addHtml in the callback function of addHtml recursively.
function recursiveAddHtmlAndSave(currentRecursion, totalRecursions){
//Once we have done all the divs save the pdf
if(currentRecursion==totalRecursions){
pdf.save(pdfName);
}else{
currentRecursion++;
pdf.addPage();
//$('.myDivClass')[currentRecursion] selects one of the divs out of the jquery collection as a html element
//addHtml requires an html element. Not a string like fromHtml.
pdf.fromHTML($('.myDivClass')[currentRecursion], 15, 20, options, function(){
console.log(currentRecursion);
recursiveAddHtmlAndSave(currentRecursion, totalRecursions)
});
}
}
pdf.fromHTML($('.myDivClass')[currentRecursion], 15, 20, options, function(){
recursiveAddHtmlAndSave(currentRecursion, numRecursionsNeeded);
});
}
PS: I was trying to modify $scope.exportUsingHTML2PDF so that it gives the same output as generated when clicked on "Export using JSPDF" button which calls the function $scope.exportUsingJSPDF.
The problem lies with your function using exportUsingHTML2PDF, the error is that you need to pass in the html to the function of html2PDF. Manage the page css on the basis of your need.
EDIT: You have wrong library. Please check html2pdf.js library within the plunker
Working plunker: html2pdf
$scope.exportUsingHTML2PDF = function() {
var element = document.getElementById('element-to-print');
html2pdf(element, {
margin: 1,
filename: 'myfile.pdf',
image: {
type: 'jpeg',
quality: 0.98
},
html2canvas: {
dpi: 192,
letterRendering: true
},
jsPDF: {
unit: 'in',
format: 'letter',
orientation: 'portrait'
}
});
}
With JSPDF and HTML2PDF, you have to get used to two fundamentally different coding styles:
JSPDF: imperative (javascript statements)
HTML2PDF: declarative (directives embedded in HTML)
So for page breaks:
JSPDF: pdf.addPage();
HTML2PDF: <div class="html2pdf__page-break"></div>
That should work, however HTML2PDF is buggy and gives a "Supplied data is not a JPEG" error when <div class="html2pdf__page-break"></div> is included (at least it does so for me, in Plunkr), despite being totally what the documentation tells us to do.
I haven't got time to debug it. You'll need to do some research. Someone will have posted a solution somewhere on the web.

Issue with image load recursive chain on slow network/mobile

So basically I have a page with a few sections. Each sections contains 5-30 image icons that are fairly small in size but large enough that I want to manipulate the load order of them.
I'm using a library called collagePlus which allows me to give it a list of elements which it will collage into a nice image grid. The idea here is to start at the first section of images, load the images, display the grid, then move on to the next section of images all the way to the end. Once we reach the end I pass a callback which initializes a gallery library I am using called fancybox which simply makes all the images interactive when clicked(but does not modify the icons state/styles).
var fancyCollage = new function() { /* A mixed usage of fancybox.js and collagePlus.js */
var collageOpts = {
'targetHeight': 200,
'fadeSpeed': 2000,
'allowPartialLastRow': true
};
// This is just for the case that the browser window is resized
var resizeTimer = null;
$(window).bind('resize', function() {
resetCollage(); // resize all collages
});
// Here we apply the actual CollagePlus plugin
var collage = function(elems) {
if (!elems)
elems = $('.Collage');
elems.removeWhitespace().collagePlus(collageOpts);
};
var resetCollage = function(elems) {
// hide all the images until we resize them
$('.Collage .Image_Wrapper').css("opacity", 0);
// set a timer to re-apply the plugin
if (resizeTimer) clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
collage(elems);
}, 200);
};
var setFancyBox = function() {
$(".covers").fancybox({/*options*/});
};
this.init = function(opts) {
if (opts != null) {
if (opts.height) {
collageOpts.targetHeight = opts.height;
}
}
$(document).ready(function() {
// some recursive functional funk
// basically goes through each section then each image in each section and loads the image and recurses onto the next image or section
function loadImage(images, imgIndex, sections, sectIndex, callback) {
if (sectIndex == sections.length) {
return callback();
}
if (imgIndex == images.length) {
var c = sections.eq(sectIndex);
collage(c);
images = sections.eq(sectIndex + 1).find("img.preload");
return loadImage(images, 0, sections, sectIndex + 1, callback);
}
var src = images.eq(imgIndex).data("src");
var img = new Image();
img.onload = img.onerror = function() {
images[imgIndex].src = src; // once the image is loaded set the UI element's source
loadImage(images, imgIndex + 1, sections, sectIndex, callback)
};
img.src = src; // load the image in the background
}
var firstImgList = $(".Collage").eq(0).find("img.preload");
loadImage(firstImgList, 0, $(".Collage"), 0, setFancyBox);
});
}
}
From my galleries I then call the init function.
It seems like my recursive chain being triggered by img.onload or img.onerror is not working properly if the images take a while to load(on slow networks or mobile). I'm not sure what I'm missing here so if anyone can chip in that would be great!
If it isn't clear what is going wrong from the code I posted you can see a live example here: https://www.yuvalboss.com/albums/olympic-traverse-august-2017
It works quite well on my desktop, but on my Nexus 5x it does not work and seems like the finally few collage calls are not happening. I've spent too long on this now so opening this up to see if I can get some help. Thanks everyone!
Whooooo I figured it out!
Was getting this issue which I'm still unsure about what it means
[Violation] Forced reflow while executing JavaScript took 43ms
Moved this into the callback that happens only once all images are loaded
$(window).bind('resize', function() {
resetCollage(); // resize all collages
});
For some reason it was getting called early even if the browser never resized causing collage to get called when no elements existed yet.
If anyone has any informative input as to why I was getting this js violation would be great to know so I can make a better fix but for now this works :):):)

I would like to merge all images within DIV and allow user to download (js powered)

I'm working on a client's web design and development powered by Wordpress and Woocommerce, the plugin is great but there's a few functions I wish it had. The main one is giving the ability for the user to save the preview of their configuration.
The plug-in is 'Woocommerce Visual Products Configurator':
It allows buyers to build their own item by selecting from a series of different attributes for different parts of an item. Ex, choosing different types of Soles for a shoe, as well as being able to choose from a series of laces for the same shoe.
My Issue: The configuration images are layered on top of each other as the user selects their options so any normal "save as" function will just save the top image.
I have succesfully managed to combine and save the images using html2canvas-Data URL() but the way it generates the screenshots means the quality becomes very poor.
My thoughts are to merge all the images within the "vpc-preview" DIV then force the download.
Hunting through the functions of the plugin I found this:
function merge_pictures($images, $path = false, $url = false) {
$tmp_dir = uniqid();
$upload_dir = wp_upload_dir();
$generation_path = $upload_dir["basedir"] . "/VPC";
$generation_url = $upload_dir["baseurl"] . "/VPC";
if (wp_mkdir_p($generation_path)) {
$output_file_path = $generation_path . "/$tmp_dir.png";
$output_file_url = $generation_url . "/$tmp_dir.png";
foreach ($images as $imgs) {
list($width, $height) = getimagesize($imgs);
$img = imagecreatefrompng($imgs);
imagealphablending($img, true);
imagesavealpha($img, true);
if (isset($output_img)) {
imagecopy($output_img, $img, 0, 0, 0, 0, 1000, 500);
} else {
$output_img = $img;
imagealphablending($output_img, true);
imagesavealpha($output_img, true);
imagecopymerge($output_img, $img, 10, 12, 0, 0, 0, 0, 100);
}
}
imagepng($output_img, $output_file_path);
imagedestroy($output_img);
if ($path)
return $output_file_path;
if ($url)
return $output_file_url;
} else
return false;
}
However the function isn't called anywhere. There's also a couple of "save" buttons that are commented out which makes me wonder if they removed from a previous build.
Ideally I'd like the user to be able to instantly share their creation to facebook but thought this would be a good start.
Any ideas would be greatly appreciated!
Thanks!
UPDATE:
I've managed to use the following code to output an alert of the url's of each of the config images. Useless for the user but at least I know it's targeting exactly what I need.
function img_find() {
var imgs = document.querySelectorAll('#vpc-preview img');
var imgSrcs = [];
for (var i = 0; i < imgs.length; i++) {
imgSrcs.push(imgs[i].src);
}
return alert(imgSrcs);
}
Any suggestions on how to manipulate this and merge the corresponding image of each URL then force the download?
Cick to see Fiddle
UPDATE 2: The following fiddle lets users upload images and then merges them into a single photo for download. Unfortunately my coding skills aren't good enough to manipulate this into using the SRC urls of images within certain DIV and to Merge all the photos on top of each other.
Fiddle
function addToCanvas(img) {
// resize canvas to fit the image
// height should be the max width of the images added, since we rotate -90 degree
// width is just a sum of all images' height
canvas.height = max(lastHeight, img.width);
canvas.width = lastWidth + img.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (lastImage) {
ctx.drawImage(lastImage, 0, canvas.height - lastImage.height);
}
ctx.rotate(270 * Math.PI / 180); // rotate the canvas to the specified degrees
ctx.drawImage(img, -canvas.height, lastWidth);
lastImage = new Image();
lastImage.src = canvas.toDataURL();
lastWidth += img.height;
lastHeight = canvas.height;
imagesLoaded += 1;
}
Based on this question/answer, I would take a look at node-canvas.
What I have done in the past is use a #print css file to only capture the divs needed. Very simple concept but works well.
After looking at your link, that is exactly the situations I use it in..online designers. It is a little more difficult keeping your layers responsively aligned, but in the end I feel your codes become cleaner and it works well across devices and OS'.
If you dont want PDF and a simple preview and download than html2image.js is perfect.
$(document).ready(function(){
var element = $("#html-content-holder"); // global variable
var getCanvas; // global variable
$("#btn-Preview-Image").on('click', function () {
html2canvas(element, {
onrendered: function (canvas) {
$("#previewImage").append(canvas);
getCanvas = canvas;
}
});
});
$("#btn-Convert-Html2Image").on('click', function () {
var imgageData = getCanvas.toDataURL("image/png");
// Now browser starts downloading it instead of just showing it
var newData = imgageData.replace(/^data:image\/png/, "data:application/octet-stream");
$("#btn-Convert-Html2Image").attr("download", "your_pic_name.png").attr("href", newData);
});
});
<script src="https://github.com/niklasvh/html2canvas/releases/download/0.4.1/html2canvas.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="html-content-holder" style="background-color: #F0F0F1; color: #00cc65; width: 500px;
padding-left: 25px; padding-top: 10px;">
Place any code here
</div>
<input id="btn-Preview-Image" type="button" value="Preview"/>
<a id="btn-Convert-Html2Image" href="#">Download</a>
<br/>
<h3>Preview :</h3>
<div id="previewImage">
</div>
For High quality you want to use rasterizeHTML.js
Found here:
https://github.com/cburgmer/rasterizeHTML.js/blob/master/examples/retina.html

Black image bug when exporting highcharts in Firefox

I am using highcharts to display several graphs on a webpage which display fine.
I have an export function that tries to combine the charts into a pdf. I am getting the svg of the chart and converting it to a jpeg image to be included in a pdf created by jsPDF.
Here is the code I am using to generate the images:
if ($('.chart').length > 0) {
var chartSVG = $('.chart').highcharts().getSVG(),
chartImg = new Image();
chartImg.src = "data:image/svg+xml;base64," + window.btoa(unescape(encodeURIComponent(chartSVG)));
var chartCanvas = document.createElement("canvas");
chartCanvas.width = 600;
chartCanvas.height = 400;
chartCanvas.getContext("2d").drawImage(chartImg, 0, 0, 600, 400);
var chartImgData = chartCanvas.toDataURL("image/jpeg");
}
This works perfectly in Chrome but in Firefox it just returns a black image.
Does anyone know what might be going wrong or has seen a similar issue?
Thanks for your help.
UPDATE
I've updated the code but now no image is appended to the pdf document, either in Chrome or Firefox.
if ($('.sales').length > 0) {
var chartSVG = $('.sales').highcharts().getSVG(),
chartImg = new Image();
chartImg.onload = function () {
var chartCanvas = document.createElement("canvas");
chartCanvas.width = 600;
chartCanvas.height = 400;
chartCanvas.getContext("2d").drawImage(chartImg, 0, 0, 600, 400);
var chartImgData = chartCanvas.toDataURL("image/jpeg");
}
chartImg.src = "data:image/svg+xml;base64," + window.btoa(unescape(encodeURIComponent(chartSVG)));
}
Not sure if I have the code in the correct place.
If I log 'chartImgData' to the console, both browsers generate a dataURI, but Firefox's version differs to Chromes.
UPDATE
Fixed the issue with black images. Now i'm struggling with how to return multiple images - how to nest multiple callbacks or is there another way?
Example: jsfiddle.net/wmuk489c/2
SOLVED
Thanks for your help #RobertLangson. fiddle updated with final working code should anyone need it: http://jsfiddle.net/wmuk489c/3/
FURTHER ISSUES:
My charts are dynamic and so may not always be present. I need to get an image from each graph that exists. If the graph does not exist, the 'getSVG' function fails, see example: http://jsfiddle.net/wmuk489c/4/
How should the img.onload work if the chart doesn't exist? The first chart in the callback may not be present either, so how would this work? Is there a better way to get the images?
setting chartImg.src causes an asynchronous load so you then need to do this...
chartImg.onload = function() {
var chartCanvas = document.createElement("canvas");
chartCanvas.width = 600;
chartCanvas.height = 400;
chartCanvas.getContext("2d").drawImage(chartImg, 0, 0, 600, 400);
var chartImgData = chartCanvas.toDataURL("image/jpeg");
doc.addImage(chartImgData, 'JPEG', 0, 0, 200, 100);
// You can only do this bit after you've added the image so it needs
// to be in the callback too
doc.save('test.pdf');
}
chartImg.src = ...
You've a race condition otherwise and I imagine you just happen to get away with it with the Chrome browser on your PC.
Here's your fiddle fixed.

JavaScript waiting until an image is fully loaded before continuing script

I've been looking around a lot of JavaScript answers but I haven't found one that really answers my problem yet. What I'm trying to do is load an image, grab the pixel data, perform an analysis, and then load another image to repeat the process.
My problem is that I can't preload all of the images because this script has to be able to work on large amounts of images and preloading could be too resource heavy. So I'm stuck trying to load a new image each time through a loop, but I'm stuck with a race condition between the image loading and the script drawing it to the canvas's context. At least I'm pretty sure that's what is happening because the script will work fine with the images precached (for example if I refresh after loading the page previously).
As you'll see there are several lines of code commented out because I'm incredibly new to JavaScript and they weren't working the way I thought they would, but I didn't want to forget about them if I needed the functionality later.
This is the snippet of code that I believe is giving rise to the problem:
EDIT: So I got my function to work after following a suggestion
function myFunction(imageURLarray) {
var canvas = document.getElementById('imagecanvas');
console.log("Canvas Grabbed");
if (!canvas || !canvas.getContext) {
return;
}
var context = canvas.getContext('2d');
if (!context || !context.putImageData) {
return;
}
window.loadedImageCount = 0;
loadImages(context, canvas.width, canvas.height, imageURLarray, 0);
}
function loadImages(context, width, height, imageURLarray, currentIndex) {
if (imageURLarray.length == 0 || imageURLarray.length == currentIndex) {
return false;
}
if (typeof currentIndex == 'undefined') {
currentIndex = 0;
}
var currentimage = new Image();
currentimage.src = imageURLarray[currentIndex];
var tempindex = currentIndex;
currentimage.onload = function(e) {
// Function code here
window.loadedImageCount++;
if (loadedImageCount == imageURLarray.length) {
// Function that happens after all images are loaded here
}
}
currentIndex++;
loadImages(context, width, height, imageURLarray, currentIndex);
return;
}
Maybe this will help:
currentimage.onload = function(e){
// code, run after image load
}
If it is necessary to wait for the image to load, the following code will load the next image (currentIndex is your "img" variable):
var loadImages = function(imageURLarray, currentIndex){
if (imageURLarray.length == 0 || imageURLarray.length == currentIndex) return false;
if (typeof currentIndex == 'undefined'){
currentIndex = 0;
}
// your top code
currentimage.onload = function(e){
// code, run after image load
loadImages(imageURLArray, currentIndex++);
}
}
Instead of a "for" loop, use for example this function:
loadImages(imageURLarray);
Maybe try this:
http://jsfiddle.net/jnt9f/
Setting onload handler before setting img src will make sure the onload event be fired even the image is cached
var $imgs = $(),i=0;
for (var img = 0; img < imageURLarray.length; img++) {
$imgs = $imgs.add('<img/>');
}
var fctn = (function fctn(i){
$imgs.eq(i).on('load',function(){
//do some stuff
//...
fctn(++i);
}).attr('src',imageURLarray[i]);
})(0);
Actually...a lot of developers are pointing here to detect when images are done loading after a jQuery event..
https://github.com/desandro/imagesloaded
If you can determine when the event triggers your images to load (for example, adding an Id or class onto the page right before your images begin to load), then you should be able to blend that in with this plug-in on github.
Good Luck!

Categories

Resources