- Status: Accepted
- Datum: 2026-05-22
- Betrifft: PDF-Export, Lehrer-Workflow, GoodNotes-Integration
- Abhängigkeit: ADR-0001 Amendment — Inline-Bibliotheken erlaubt
Amendment (2026-05-28): Der ursprüngliche Splitting-Algorithmus orientierte sich an manuellen
.page-break-hint-Markern und zerteilte zu lange Abschnitte stur in 297-mm-Stücke. In der Praxis erzeugte das schlechte Umbrüche: Karten wurden mitten durchgeschnitten und Marker hinterließen halbleere Seiten. Der Algorithmus wurde durch eine block-bewusste Paginierung ersetzt (siehe „Export-Mechanik" unten). Die Marker sind für den PDF-Export nicht mehr nötig; sie steuern weiterhin nur den nativen Druck (ADR-0023).
Browser-natives Drucken (Strg/Cmd + P → „Als PDF speichern") funktioniert mit dem überarbeiteten Druck-CSS (ADR-0023) inzwischen ordentlich. Es bleibt aber ein Pferdefuß:
Canvas-Inhalte werden vom Browser-Druck nicht zuverlässig wiedergegeben. Mal werden sie als gerasterte Bilder gedruckt, mal sind sie leer, mal merkwürdig skaliert. Bei Onepagern mit Stift-Eingabe (Profil digitales Arbeitsheft) ist das ein Killer.
Vorbild Turtle-Arbeitsblatt: Dort gab es einen pixelgenauen PDF-Export via html2canvas (rendert das ganze DOM in ein Canvas) + jsPDF (verpackt Canvas-Bilder in eine PDF-Datei). Ergebnis: PDF sieht 1:1 aus wie der Bildschirm — inklusive aller Canvas-Zeichnungen. Schüler:innen können das PDF in GoodNotes importieren und dort weiterarbeiten.
Diese Mechanik will ich verfügbar machen — aber nur optional, weil sie zwei externe Bibliotheken benötigt.
PDF-Export ist ein optionales Modul, das per Snippet in einen Onepager kopiert wird, wenn er gebraucht wird. Es wird nicht standardmäßig im templates/onepager-boilerplate.html mitgeliefert, weil die Bibliotheken zusammen ca. 400 KB minified wiegen — zu viel Overhead für Onepager, die kein PDF brauchen.
| Lib | Version | Größe (min) | Lizenz |
|---|---|---|---|
| html2canvas | 1.4.1 | ~47 KB | MIT |
| jsPDF | 2.5.1 | ~350 KB (umd.min) | MIT |
Beide unter MIT — mit unserer CC-BY-4.0 (Repository-Lizenz) und der Verwendung in pädagogischem Material kompatibel. Lizenz-Header beider Libs muss erhalten bleiben (steht so im jeweiligen Minified-Code).
Per ADR-0001 Amendment ist das Inline-Einbinden externer Bibliotheken zulässig, solange die Datei single-file bleibt:
<!-- ===== PDF-EXPORT-LIBS (inline, ca. 400 KB) ===== -->
<script>/*! html2canvas v1.4.1 — MIT License — https://html2canvas.hertzen.com */
(function(global, factory) { ... }) /* ~47 KB minified */
</script>
<script>/*! jsPDF v2.5.1 — MIT License — https://github.com/parallax/jsPDF */
(function(global, factory) { ... }) /* ~350 KB minified */
</script>
<!-- ===== /PDF-EXPORT-LIBS ===== -->Die echten Inline-Codes stehen in templates/snippets/pdf-export-snippet.html als kopierbarer Block. Die Lehrkraft (oder KI) holt sich die Libs einmal per curl und fügt sie als großen Block in den Onepager ein.
Der naive Ansatz (das gerenderte Canvas in 297-mm-Stücke schneiden) zerschneidet Aufgaben-Karten an beliebigen Stellen. Stattdessen paginiert das Modul block-bewusst: Umbrüche dürfen nur in den Lücken zwischen unzerschneidbaren Blöcken liegen, und jede Seite wird so voll wie möglich gepackt. Zusätzlich hält jede Seite 2,5 cm Rand auf allen vier Seiten ein.
Seitenrand (25 mm rundum): Statt randlos zu platzieren, rendert das Modul die .page während des Exports auf Inhaltsbreite contentWmm = 210 − 2·25 = 160 mm (CSS-Regel body.pdf-rendering .page { width:160mm; padding:0 }, Kopfzeile ohne Bleed) und setzt jedes Seitenbild per addImage an Position (25, 25) mit Breite 160 mm. Die nutzbare Höhe pro Seite ist entsprechend contentHmm = 247 mm. Weil in echter Inhaltsbreite gerendert wird, bleiben die pt-Schriftgrößen physikalisch korrekt (kein horizontales Stauchen). Der native Strg/Cmd+P-Druck behält seinen 18-mm-@page-Rand (ADR-0023); die 25 mm gelten nur für den PDF-Export.
- html2canvas rendert das
.page-Element (auf 160 mm Inhaltsbreite) in ein Canvas - Unzerschneidbare Blöcke einsammeln: alle Treffer von
atomSel(.box, .material-panel, .worked-example, .aufgabe, .quiz-frage, .ich-kann-row, figure, table, h2, h3, p), reduziert auf die äußersten Elemente (ein.aufgabe-Container statt seiner inneren<p>), mit ihren Y-Positionen pergetBoundingClientRect() - Seiten packen: vom Seitenanfang Blöcke aufnehmen, bis der nächste über das 247-mm-Limit (Inhaltshöhe) ragen würde → genau davor (in der Lücke) trennen. So wird kein Block zerschnitten und es entstehen keine halbleeren Seiten
- Verwaiste Überschrift vermeiden: stünde eine
h2/h3allein am Seitenende, wird sie auf die nächste Seite gezogen - Notfall: ein einzelner Block, der höher als die Inhaltshöhe (247 mm) ist, wird hart getrennt — das sollte durch kleinere Bausteine vermieden werden
Damit landen Aufgaben-Karten nie über zwei Seiten verteilt, ohne dass Autor:innen manuelle Marker setzen müssen.
CSS-Begleitregel: body.pdf-rendering .page-break-hint { border-top-color: transparent !important; } und body.pdf-rendering .page-break-hint::after { display: none !important; } — damit die Marker (die weiterhin im DOM stehen und den nativen Druck steuern) im PDF nicht als gestrichelter Strich oder als „↓ neue A4-Seite ↓"-Text auftauchen.
Vollständiger Code: siehe templates/snippets/pdf-export-snippet.html.
// 1. html2canvas (scale=2; .page wurde per CSS auf contentWmm=160mm gesetzt)
const canvas = await html2canvas(target, { scale: 2, ... });
const marginMm = 25;
const contentWmm = 210 - 2*marginMm; // 160
const contentHmm = 297 - 2*marginMm; // 247
const mmPerPx = contentWmm / canvas.width;
const pagePx = Math.floor(contentHmm / mmPerPx); // Inhaltshöhe in Canvas-Pixeln
const realScale = canvas.height / target.height; // CSS-Pixel → Canvas-Pixel
// 2. Äußerste, unzerschneidbare Blöcke einsammeln (Karten, Boxen, Überschriften …)
let atoms = [...target.querySelectorAll(atomSel)].filter(el => el.getClientRects().length);
atoms = atoms.filter(el => !atoms.some(o => o !== el && o.contains(el)));
const items = atoms.map(el => {
const r = el.getBoundingClientRect();
return { top: (r.top - targetTop) * realScale, bottom: (r.bottom - targetTop) * realScale,
heading: /^H[1-4]$/.test(el.tagName) };
}).sort((a, b) => a.top - b.top);
// 3. Seiten packen — nur in Lücken zwischen Blöcken trennen
const pages = [];
let start = 0;
while (start < canvas.height - 1) {
const limit = start + pagePx;
let k = 0;
while (k < items.length && items[k].bottom <= limit + 0.5) k++; // erster überstehender Block
let end;
if (k >= items.length) end = canvas.height; // Rest passt
else if (items[k].top > start + 1) {
end = Math.min(items[k].top, limit); // vor Block / am Limit trennen
for (let p = k - 1; p >= 0 && items[p].heading && items[p].top > start + 1; p--)
end = items[p].top; // Überschrift nicht verwaisen
} else end = limit; // Block > Seite → Notfall-Schnitt
if (end <= start) end = Math.min(limit, canvas.height);
pages.push({ start, end });
start = end;
}
// 4. Pro Seite einen Bild-Ausschnitt in den 2,5cm-Rahmen setzen:
// pdf.addImage(img, 'JPEG', marginMm, marginMm, contentWmm, hPx*mmPerPx) + pdf.addPageDer Rendervorgang dauert auf älteren Geräten ein paar Sekunden — ein einfaches Modal mit Fortschritts-Anzeige verhindert „Hängt die App?"-Gefühl:
<div id="pdf-progress" class="modal" hidden>
<div class="modal__panel">
<h2>📄 PDF wird erstellt …</h2>
<p>Einen Moment Geduld. Bei vielen Zeichnungen kann das ein paar Sekunden dauern.</p>
<div class="progress-bar"><div class="progress-fill" id="pdf-progress-fill"></div></div>
</div>
</div>Modal wird zu Beginn von exportPDF() geöffnet, am Ende geschlossen.
Bevor die Lehrkraft auf „PDF speichern" klickt, sollte sie im A4-Vorschau-Modus geprüft haben, dass das Layout passt. Beim Klick auf „PDF speichern" wird vorübergehend die A4-Vorschau aktiviert, falls noch nicht:
async function exportPDF() {
const wasPreview = document.body.classList.contains('a4-preview');
if (!wasPreview) setA4Preview(true);
await new Promise(r => requestAnimationFrame(r)); // Layout sich setzen lassen
// … html2canvas-Aufruf …
if (!wasPreview) setA4Preview(false);
}Eine PDF-Datei kann in GoodNotes (iPad) importiert werden — sie wird zu einer beschriftbaren Seite. Der Workflow für Lehrer:innen:
- Onepager fertig bauen
?layout=a4einschalten, prüfen- „PDF speichern" klicken
- PDF in GoodNotes importieren
- Mit Apple Pencil annotieren, verteilen
Für Schüler:innen:
- Im Onepager arbeiten (auch mit Canvas-Zeichnungen)
- „PDF speichern" klicken
- PDF in GoodNotes weiter beschriften (z. B. eigene Anmerkungen, Markierungen)
- Oder per AirDrop, Mail, Teams abgeben
| Format | Wann | Eigenschaft |
|---|---|---|
| JSON (ADR-0003) | Wieder-Import, Backup, Geräte-Wechsel | Maschinen-lesbar, kompakt, nur State |
| HTML (ADR-0020) | Übergabe an Lehrkraft oder Eltern, später wieder bearbeiten | Doppelklick öffnet, voll interaktiv |
| PDF (dieses ADR) | Druck, GoodNotes-Integration, Archivierung | Pixelgenau, statisch, ausdruckbar |
Drei Wege — drei Zwecke. Sie konkurrieren nicht, ergänzen einander.
- Browser-natives Drucken reicht: Verworfen — Canvas-Inhalte unzuverlässig im Druck.
- PDF-Lib selbst schreiben: Verworfen — PDF-Format ist komplex, Vektorisierung von Canvas extrem aufwendig.
- html2pdf.js (Wrapper-Lib): Wrappt html2canvas + jsPDF. Spart Code, aber weniger Kontrolle über Render-Optionen. Eine valide Variante — könnte als Sub-Variante dokumentiert werden, wir bleiben aber bei html2canvas + jsPDF separat.
- Server-Side-PDF (Puppeteer, weasyprint): Bräuchte Backend. Verstößt gegen ADR-0001 Kern. Verworfen.
- CDN-Link statt Inline: Würde ADR-0001 verletzen. Verworfen.
- PDF-Modul standardmäßig im Boilerplate: Verworfen — 400 KB extra für Onepager, die kein PDF brauchen, ist Verschwendung. Daher optional per Snippet.
Positiv:
- Pixelgenaues PDF inkl. Canvas-Zeichnungen
- GoodNotes-tauglich (iPad)
- Single-File-Prinzip durch Inline-Libs erhalten
- Offline-fähig (CDN-frei)
- Lehrer:innen können Klassensätze als PDF archivieren
Negativ / Trade-offs:
- 400 KB extra pro Onepager mit PDF-Modul (in Bytes — beim Hosting irrelevant, beim mobilen Erst-Laden spürbar bei langsamer Verbindung)
- html2canvas hat bekannte Schwächen: CSS-Gradienten, einige Filter, oder dynamische Inhalte werden nicht perfekt gerendert
- Bei sehr großen Onepagern (viele Seiten) kann der Render einige Sekunden dauern
- jsPDF erzeugt JPEG-eingebettete PDFs — bei Texten verlustbehaftet (bei
quality 0.92aber kaum sichtbar) - Lizenz-Header der Libs müssen erhalten bleiben
Folgewirkungen für künftige Onepager:
- Bei Bedarf: Snippet aus
templates/snippets/pdf-export-snippet.htmleinkopieren - Vorher überlegen: Brauche ich wirklich PDF, oder reicht HTML-Export?
- Wenn ja: einmal
curlfür die Libs, dann Block in HTML einfügen - Im A4-Vorschau-Modus prüfen, bevor PDF erstellt wird