aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+studip@gmail.com>2025-09-29 10:57:21 +0200
committerJan-Hendrik Willms <tleilax+studip@gmail.com>2025-09-29 10:57:21 +0200
commiteddcbad656fbdefed1bfbc182ae1b7a5bd3b7e63 (patch)
treeb35f2e424b27aee5a1a8dca7eb6ffd443e9be93f
parent637ac5d73a0b2f83aa9d36d4c6bbe9c4b44df3af (diff)
fix pdf generation from questionnaires, fixes #3224
Closes #3224 Merge request studip/studip!4496
-rw-r--r--app/views/questionnaire/evaluate.php2
-rw-r--r--package-lock.json8
-rw-r--r--package.json1
-rw-r--r--resources/assets/javascripts/lib/questionnaire.js160
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
<?= \Studip\LinkButton::create(_("Starten"), URLHelper::getURL("dispatch.php/questionnaire/start/".$questionnaire->getId())) ?>
<? endif ?>
<? if ($questionnaire->resultsVisible()) : ?>
- <?= \Studip\LinkButton::create(_('PDF exportieren'), '#', ['onclick' => "STUDIP.Questionnaire.exportEvaluationAsPDF(); return false;"]) ?>
+ <?= \Studip\LinkButton::create(_('PDF exportieren'), '#', ['onclick' => "STUDIP.Questionnaire.exportEvaluationAsPDF(this.closest('article.studip')); return false;"]) ?>
<? endif ?>
<? if ($questionnaire->isEditable() && $questionnaire->isRunning()) : ?>
<?= \Studip\LinkButton::create(_("Beenden"), URLHelper::getURL("dispatch.php/questionnaire/stop/".$questionnaire->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) {