From eddcbad656fbdefed1bfbc182ae1b7a5bd3b7e63 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms Date: Mon, 29 Sep 2025 10:57:21 +0200 Subject: fix pdf generation from questionnaires, fixes #3224 Closes #3224 Merge request studip/studip!4496 --- app/views/questionnaire/evaluate.php | 2 +- package-lock.json | 8 +- package.json | 1 + resources/assets/javascripts/lib/questionnaire.js | 160 +++++++++++----------- 4 files changed, 88 insertions(+), 83 deletions(-) diff --git a/app/views/questionnaire/evaluate.php b/app/views/questionnaire/evaluate.php index 1130e73..56c1ab7 100644 --- a/app/views/questionnaire/evaluate.php +++ b/app/views/questionnaire/evaluate.php @@ -76,7 +76,7 @@ if (isset($filtered[$questionnaire->getId()]) && $filtered[$questionnaire->getId getId())) ?> resultsVisible()) : ?> - "STUDIP.Questionnaire.exportEvaluationAsPDF(); return false;"]) ?> + "STUDIP.Questionnaire.exportEvaluationAsPDF(this.closest('article.studip')); return false;"]) ?> isEditable() && $questionnaire->isRunning()) : ?> getId())) ?> diff --git a/package-lock.json b/package-lock.json index 9a8d6dd..ca1b69c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,7 @@ "focus-trap-vue": "^4.0.3", "globals": "^15.13.0", "highlight.js": "10.5.0", + "html2canvas": "^1.4.1", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "jest-junit": "^16.0.0", @@ -5236,7 +5237,6 @@ "version": "1.0.2", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 0.6.0" } @@ -6187,7 +6187,6 @@ "version": "2.1.0", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -8622,9 +8621,10 @@ }, "node_modules/html2canvas": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -13196,7 +13196,6 @@ "version": "1.0.3", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -13807,7 +13806,6 @@ "version": "1.0.2", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } diff --git a/package.json b/package.json index d8e5133..a86f219 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "focus-trap-vue": "^4.0.3", "globals": "^15.13.0", "highlight.js": "10.5.0", + "html2canvas": "^1.4.1", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "jest-junit": "^16.0.0", diff --git a/resources/assets/javascripts/lib/questionnaire.js b/resources/assets/javascripts/lib/questionnaire.js index 157c003..2c48d88 100644 --- a/resources/assets/javascripts/lib/questionnaire.js +++ b/resources/assets/javascripts/lib/questionnaire.js @@ -226,96 +226,102 @@ const Questionnaire = { } }, + async exportEvaluationAsPDF(container) { + const [html2canvas, jsPDF] = await Promise.all([ + import('html2canvas').then(m => m.default), + import('jspdf').then(m => m.default), + ]); - exportEvaluationAsPDF: function () { - window.scrollTo(0, 0); - const html2canvas = import('html2canvas'); - const jsPDF = import('jspdf'); - jsPDF.then(function (jsPDF) { - let pdfExporter = jsPDF.default; - html2canvas.then(function (canvas) { - let canvasCreator = canvas.default; + const pdf = new jsPDF({ + orientation: 'portrait' + }); - let pdf = new pdfExporter({ - orientation: 'portrait' - }); + const results = container.querySelector('.questionnaire_results'); - $(".questionnaire_results").addClass('print-view'); + results.classList.add('print-view'); - let title = $(".questionnaire_results").data('title'); - let formattedDate = new Intl.DateTimeFormat(String.locale, { - year: "numeric", - month: "long", - day: "numeric", + const title = results.dataset.title; + const splitTitle = pdf.splitTextToSize(title, 180); - hour: "numeric", - minute: "numeric" - }).format(new Date()); - let splitTitle = pdf.splitTextToSize(title, 180); + const formattedDate = new Intl.DateTimeFormat(String.locale, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric' + }).format(new Date()); - let count_questions = $(".questionnaire_results .question").length; - let questions_rendered = 0; - let canvasses = []; + const questions = results.querySelectorAll('.question'); - $(".questionnaire_results .question").each(function (index) { - canvasCreator(this, {logging: false}).then(canvas => { - canvasses[index] = canvas; - questions_rendered++; - if (questions_rendered === count_questions) { - //then all renders are finished: - let height_sum = 0; - for (let i = 0; i < count_questions; i++) { - if (i === 0) { - height_sum += 15; - } - let imgData = canvasses[i].toDataURL('image/png'); + const canvasses = await Promise.all( + Array.from(questions).map(element => { + element.querySelectorAll('svg.ct-chart-bar').forEach(svg => { + // Remove xmlns attribute from all children of the svg + svg.querySelectorAll('[xmlns]').forEach(node => { + node.removeAttribute('xmlns'); + }); - let height = Math.floor(160 / canvasses[i].width * canvasses[i].height); - if (height_sum + height > 240 && height < 240) { - pdf.addPage(); - height_sum = 15; - } - pdf.addImage(imgData, 'JPEG', - 25, - 20 + height_sum, - 160, - height, - 'image_' + i, - 'NONE', + // Set width and height as attribute, not as style + svg.setAttribute('width', svg.getBoundingClientRect().width); + svg.setAttribute('height', svg.getBoundingClientRect().height); + svg.style.width = null; + svg.style.height = null; + }); - ); - height_sum += height + 10; - } + return html2canvas(element, { + allowTaint: false, + foreignObjectRendering: false, + useCORS: true, + logging: false + }) + }) + ); - const pages = pdf.internal.getNumberOfPages(); + //then all renders are finished: + let height_sum = 15; + canvasses.forEach((canvas, index) => { + let height = Math.floor(160 / canvas.width * canvas.height); + if (height_sum + height > 240 && height < 240) { + pdf.addPage(); + height_sum = 15; + } + pdf.addImage( + canvas.toDataURL('image/png'), + 'JPEG', + 25, + 20 + height_sum, + 160, + height, + 'image_' + index, + 'FAST', + ); + height_sum += height + 10; + }) - for (let i = 1; i <= pages; i++) { - let pageSize = pdf.internal.pageSize; - let pageHeight = pageSize.getHeight(); - pdf.setPage(i); - pdf.setFontSize(16); - pdf.text(splitTitle, 25, 20); - pdf.setFontSize(8); - pdf.text( - String(formattedDate), - 30, - pageHeight - 8 - ) - pdf.text( - String(i) + ' / ' + String(pages), - pageSize.getWidth() - 30, - pageHeight - 8 - ); - } - pdf.save(title + '.pdf'); - } - }); - }); - $(".questionnaire_results").removeClass('print-view'); - }) - }); + const pages = pdf.internal.getNumberOfPages(); + + for (let i = 1; i <= pages; i++) { + let pageSize = pdf.internal.pageSize; + let pageHeight = pageSize.getHeight(); + pdf.setPage(i); + pdf.setFontSize(16); + pdf.text(splitTitle, 25, 20); + pdf.setFontSize(8); + pdf.text( + String(formattedDate), + 30, + pageHeight - 8 + ) + pdf.text( + String(i) + ' / ' + String(pages), + pageSize.getWidth() - 30, + pageHeight - 8 + ); + } + pdf.save(title + '.pdf'); + results.classList.remove('print-view'); }, addDelayedInit(el, data, isAjax, isMultiple) { -- cgit v1.0