commit 37cadcc8db60526925a562590944e5c2970793d6 Author: Carsten Graf Date: Thu Jan 15 18:56:52 2026 +0100 V1.0 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a2b703f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +*.md +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a84c32c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:18-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy application files +COPY server.js ./ +COPY html/ ./html/ +COPY css/ ./css/ +COPY js/ ./js/ + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:8080', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start server +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6d2031 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# PDF Unterschrift System + +Ein Zwei-Seiten-System für digitale PDF-Unterschriften mit Echtzeit-Synchronisation. + +## 🎯 Funktionen + +- **Master-Seite (PC)**: PDF hochladen und unterschriebene PDF herunterladen +- **Signatur-Seite (Tablet/Smartphone)**: Permanent offen, zeigt automatisch jede neue PDF mit Vorschau +- **PDF-Vorschau**: Echte PDF-Anzeige auf beiden Seiten mit pdf.js +- **Verschiebbare Unterschrift**: Unterschrift per Drag & Drop positionieren +- **Skalierbare Unterschrift**: Größe der Unterschrift anpassen +- **Automatische Synchronisation**: Keine Session-IDs, keine QR-Codes nötig +- **Echtzeit-Übertragung**: WebSocket-basierte direkte Kommunikation +- **Mehrfachnutzung**: Signatur-Seite einmal öffnen, beliebig viele PDFs unterschreiben + +## 📋 Voraussetzungen + +- Node.js (Version 14 oder höher) +- Ein moderner Webbrowser (Chrome, Firefox, Safari, Edge) + +## 🚀 Installation & Start + +### 1. Dependencies installieren + +```bash +npm install +``` + +### 2. Server starten + +```bash +npm start +``` + +oder + +```bash +node server.js +``` + +Der Server läuft nun auf `http://localhost:8080` + +## 💻 Verwendung + +### Schritt 1: Signatur-Seite öffnen und offen lassen (Tablet/Smartphone) + +1. Öffne im Browser: `http://localhost:8080/signature.html` +2. **WICHTIG**: Lasse diese Seite permanent geöffnet! +3. Die Seite zeigt "Warte auf PDF..." - das ist normal + +### Schritt 2: Master-Seite öffnen (PC) + +1. Öffne im Browser: `http://localhost:8080/master.html` +2. Klicke auf "PDF auswählen" und wähle eine PDF-Datei aus + +### Schritt 3: Automatische Anzeige + +1. Die PDF wird **automatisch** auf der Signatur-Seite angezeigt +2. Du siehst eine **echte Vorschau** des PDFs +3. Keine QR-Codes, keine Links - alles passiert automatisch! + +### Schritt 4: Unterschreiben + +1. Auf der Signatur-Seite: Zeichne deine Unterschrift +2. Klicke auf "Unterschrift senden" +3. Die Unterschrift erscheint automatisch auf der Master-Seite + +### Schritt 5: Unterschrift positionieren + +1. Auf der Master-Seite: Die Unterschrift erscheint als **verschiebbare Box** über dem PDF +2. **Ziehe** die Unterschrift an die gewünschte Position +3. **Skaliere** die Unterschrift mit dem Griff rechts unten +4. **Optional**: Entferne die Unterschrift mit dem × falls nötig (zum Neuzeichnen) +5. **Klicke auf "Unterschrift platzieren & PDF erstellen"** +6. Die Unterschrift wird ins PDF eingefügt und die finale Version wird angezeigt + +### Schritt 6: PDF herunterladen + +- Auf der Master-Seite: Klicke "Unterschriebene PDF herunterladen" +- Die PDF wird mit der Unterschrift an der gewählten Position gespeichert + +### Für die nächste PDF: + +- Lade einfach eine neue PDF auf der Master-Seite hoch +- Sie wird automatisch auf der (noch offenen) Signatur-Seite angezeigt +- Unterschreiben → Positionieren → Platzieren → Download +- **Keine neue Session nötig!** + +## 🌐 Netzwerk-Zugriff + +### Für lokale Nutzung: +- Alle Geräte müssen im selben Netzwerk sein +- Verwende die IP-Adresse deines PCs statt `localhost` + +### IP-Adresse finden: + +**Windows:** +```bash +ipconfig +``` +(Suche nach "IPv4-Adresse") + +**Mac/Linux:** +```bash +ifconfig +``` +oder +```bash +ip addr show +``` + +Dann verwende: `http://[DEINE-IP]:8080/master.html` + +## 📁 Dateien + +``` +pdf-signature-system/ +├── master.html # Master-Seite (PDF hochladen) +├── signature.html # Signatur-Seite (unterschreiben) +├── server.js # WebSocket Server +├── package.json # Node.js Dependencies +└── README.md # Diese Datei +``` + +## 🔧 Technische Details + +### Verwendete Technologien: +- **Frontend**: HTML5, CSS3, JavaScript +- **PDF-Vorschau**: PDF.js (Mozilla) +- **PDF-Verarbeitung**: PDF-Lib (Client-seitig) +- **Kommunikation**: WebSockets +- **Server**: Node.js mit `ws` Package +- **Interaktivität**: Drag & Drop, Resize Handles + +### Architektur: +1. Signatur-Seite öffnen und permanent offen lassen +2. Master-Seite lädt PDF hoch +3. WebSocket-Server leitet PDF direkt an Signatur-Seite weiter +4. Unterschrift wird über WebSocket zurück an Master gesendet +5. Master-Seite fügt Unterschrift ins PDF ein +6. Für nächste PDF: Einfach neue PDF hochladen - alles andere bleibt wie es ist! + +## 🛠️ Anpassungen + +### Standard-Position der Unterschrift ändern: + +In `master.html`, Zeile ~20: +```javascript +let signaturePos = { x: 0.7, y: 0.1 }; // x: 0-1 (links-rechts), y: 0-1 (oben-unten) +``` + +### Standard-Größe der Unterschrift ändern: + +Zeile ~21: +```javascript +let signatureScale = 0.3; // 0.3 = 30% der PDF-Breite +``` + +### Port ändern: + +In `server.js`, Zeile ~158: +```javascript +const PORT = 8080; // Ändere auf gewünschten Port +``` + +Dann auch in beiden HTML-Dateien die WebSocket-URL anpassen. + +## ❗ Troubleshooting + +### "WebSocket Verbindung fehlgeschlagen" +- Stelle sicher, dass der Server läuft +- Prüfe ob Port 8080 verfügbar ist +- Bei Firewall: Erlaube Port 8080 + +### "Warte auf PDF..." bleibt stehen +- Stelle sicher, dass beide Seiten mit dem Server verbunden sind +- Prüfe Browser-Konsole auf Fehlermeldungen +- Server neustarten und beide Seiten neu laden + +### PDF-Vorschau wird nicht angezeigt +- Stelle sicher, dass pdf.js korrekt geladen wurde (siehe Browser-Konsole) +- Versuche eine andere PDF-Datei +- Browser-Cache leeren und Seite neu laden + +### Unterschrift lässt sich nicht verschieben +- Stelle sicher, dass die Unterschrift vollständig geladen ist +- Klicke direkt auf die Unterschrift (nicht auf die Griffe) +- Browser neu laden + +### Unterschrift wird nicht angezeigt +- Prüfe Browser-Konsole auf Fehlermeldungen +- Stelle sicher, dass beide Seiten verbunden sind +- Server neustarten + +### PDF-Download funktioniert nicht +- Warte bis Status "Unterschrift erhalten" angezeigt wird +- Prüfe ob PDF-Lib korrekt geladen wurde +- Browser-Cache leeren + +## 🔒 Sicherheitshinweise + +⚠️ **Wichtig**: Dieses System ist für den lokalen Gebrauch gedacht! + +Für den Produktiveinsatz beachten: +- HTTPS verwenden (WSS statt WS) +- Authentifizierung implementieren +- Session-Timeouts einrichten +- Input-Validierung verstärken +- Rate-Limiting hinzufügen + +## 📝 Lizenz + +MIT License - Frei verwendbar für private und kommerzielle Zwecke + +## 🤝 Support + +Bei Fragen oder Problemen: +1. Prüfe die Konsole (F12 in Browser) +2. Stelle sicher, dass Node.js installiert ist +3. Verifiziere Netzwerk-Verbindung zwischen Geräten + +--- + +Viel Erfolg mit dem PDF Unterschrift System! ✍️📄 diff --git a/css/master.css b/css/master.css new file mode 100644 index 0000000..ae4d0da --- /dev/null +++ b/css/master.css @@ -0,0 +1,328 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background: white; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + overflow: hidden; +} + +.header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 30px; + text-align: center; +} + +.header h1 { + font-size: 2em; + margin-bottom: 10px; +} + +.content { + padding: 30px; +} + +.upload-section { + border: 3px dashed #667eea; + border-radius: 12px; + padding: 40px; + text-align: center; + margin-bottom: 30px; + background: #f8f9ff; + transition: all 0.3s; +} + +.upload-section:hover { + border-color: #764ba2; + background: #f0f2ff; +} + +.upload-section input[type="file"] { + display: none; +} + +.upload-button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 15px 40px; + border: none; + border-radius: 8px; + font-size: 1.1em; + cursor: pointer; + transition: transform 0.2s; +} + +.upload-button:hover:not(:disabled) { + transform: translateY(-2px); +} + +.upload-button:disabled { + opacity: 0.5; + cursor: not-allowed; + background: #ccc; +} + +.status { + margin: 20px 0; + padding: 15px; + border-radius: 8px; + text-align: center; + font-weight: 500; +} + +.status.waiting { + background: #fff3cd; + color: #856404; +} + +.status.ready { + background: #d4edda; + color: #155724; +} + +.status.signed { + background: #d1ecf1; + color: #0c5460; +} + +.connection-status { + text-align: center; + margin: 20px 0; + padding: 20px; + background: #f8f9fa; + border-radius: 12px; +} + +.status-indicator { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 8px; + animation: pulse 2s infinite; +} + +.status-indicator.connected { + background: #28a745; +} + +.status-indicator.disconnected { + background: #dc3545; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.page-navigation { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + margin: 20px 0; + padding: 15px; + background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); + border-radius: 12px; + border: 2px solid #667eea; +} + +.nav-button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 1em; + font-weight: 500; + transition: all 0.2s; +} + +.nav-button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); +} + +.nav-button:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +#pageInfo { + font-weight: bold; + font-size: 1.2em; + color: #667eea; + min-width: 150px; + text-align: center; +} + +.page-selector label { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 8px 12px; + background: white; + border-radius: 6px; + border: 2px solid #667eea; + transition: all 0.2s; +} + +.page-selector label:hover { + background: #667eea15; +} + +.page-selector input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +.pdf-preview { + margin-top: 30px; + border: 2px solid #e0e0e0; + border-radius: 12px; + overflow: hidden; + position: relative; +} + +.pdf-preview.scrollable { + max-height: 70vh; + overflow-y: auto; +} + +.pdf-preview canvas { + width: 100%; + display: block; + margin: 10px 0; +} + +.pdf-page-separator { + text-align: center; + margin: 15px 0; + padding: 8px; + background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); + border-radius: 6px; + font-weight: bold; + color: #667eea; + font-size: 0.9em; +} + +.signature-overlay { + position: absolute; + border: 3px dashed #667eea; + background: rgba(255, 255, 255, 0.9); + padding: 10px; + cursor: move; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + display: none; + z-index: 10; +} + +.signature-overlay.show { + display: block; +} + +.signature-overlay img { + display: block; + max-width: 300px; + height: auto; + pointer-events: none; +} + +.signature-overlay .handle { + position: absolute; + top: -10px; + right: -10px; + background: #667eea; + color: white; + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 18px; +} + +.signature-overlay .resize-handle { + position: absolute; + bottom: 5px; + right: 5px; + width: 20px; + height: 20px; + background: #667eea; + cursor: nwse-resize; + border-radius: 3px; +} + +.signature-controls { + margin-top: 20px; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + display: none; +} + +.signature-controls.show { + display: block; +} + +.signature-controls p { + margin-bottom: 15px; + color: #666; +} + +.download-button { + background: #28a745; + color: white; + padding: 15px 40px; + border: none; + border-radius: 8px; + font-size: 1.1em; + cursor: pointer; + margin-top: 20px; + transition: transform 0.2s; +} + +.download-button:hover { + transform: translateY(-2px); +} + +.download-button:disabled { + background: #ccc; + cursor: not-allowed; +} + +.discard-button { + background: #dc3545; + color: white; + padding: 15px 40px; + border: none; + border-radius: 8px; + font-size: 1.1em; + cursor: pointer; + margin-top: 20px; + transition: transform 0.2s; +} + +.discard-button:hover { + transform: translateY(-2px); + background: #c82333; +} diff --git a/css/signature.css b/css/signature.css new file mode 100644 index 0000000..f8bfb0d --- /dev/null +++ b/css/signature.css @@ -0,0 +1,196 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); + min-height: 100vh; + padding: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.container { + max-width: 800px; + width: 100%; + background: white; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + overflow: hidden; +} + +.header { + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); + color: white; + padding: 30px; + text-align: center; +} + +.header h1 { + font-size: 2em; + margin-bottom: 10px; +} + +.content { + padding: 30px; +} + +.signature-section { + margin: 20px 0; +} + +.signature-pad { + border: 3px solid #11998e; + border-radius: 12px; + cursor: crosshair; + touch-action: none; + display: block; + width: 100%; + background: #fafafa; +} + +.controls { + display: flex; + gap: 15px; + margin-top: 20px; + flex-wrap: wrap; +} + +.button { + padding: 15px 30px; + border: none; + border-radius: 8px; + font-size: 1.1em; + cursor: pointer; + transition: transform 0.2s, opacity 0.2s; + font-weight: 500; +} + +.button:hover { + transform: translateY(-2px); +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.button-clear { + background: #dc3545; + color: white; + flex: 1; +} + +.button-submit { + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); + color: white; + flex: 2; +} + +.info-box { + background: #e7f9f5; + border-left: 4px solid #11998e; + padding: 15px; + margin-bottom: 20px; + border-radius: 4px; +} + +.success-message { + background: #d4edda; + color: #155724; + padding: 20px; + border-radius: 8px; + text-align: center; + display: none; + margin-top: 20px; +} + +.success-message.show { + display: block; +} + +.loading { + text-align: center; + padding: 40px; +} + +.spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #11998e; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; + margin: 0 auto 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-message { + background: #f8d7da; + color: #721c24; + padding: 15px; + border-radius: 8px; + margin-top: 20px; + display: none; +} + +.error-message.show { + display: block; +} + +.pdf-display { + margin: 20px 0; + padding: 20px; + background: #f8f9fa; + border-radius: 12px; + text-align: center; + display: none; +} + +.pdf-display.show { + display: block; +} + +.pdf-display h3 { + margin-bottom: 15px; + color: #11998e; +} + +.pdf-preview-canvas { + max-width: 100%; + border: 2px solid #11998e; + border-radius: 8px; + background: white; + display: block; + margin: 10px auto; +} + +.page-separator { + text-align: center; + margin: 20px 0; + padding: 10px; + background: linear-gradient(135deg, #11998e15 0%, #38ef7d15 100%); + border-radius: 8px; + font-weight: bold; + color: #11998e; +} + +.waiting-message { + text-align: center; + padding: 40px; + color: #666; +} + +.waiting-message .icon { + font-size: 3em; + margin-bottom: 20px; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..72f695a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3.8' + +services: + pdf-signature: + build: + context: . + dockerfile: Dockerfile + container_name: pdf-signature-system + ports: + - "8080:8080" + restart: unless-stopped + environment: + - NODE_ENV=production + networks: + - pdf-signature-network + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:8080', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + +networks: + pdf-signature-network: + driver: bridge diff --git a/html/master.html b/html/master.html new file mode 100644 index 0000000..5ddeb5c --- /dev/null +++ b/html/master.html @@ -0,0 +1,106 @@ + + + + + + Master - PDF Unterschrift System + + + + + +
+
+

📄 Master Station

+

PDF hochladen und Unterschrift empfangen

+
+ +
+ + +
+

PDF Dokument auswählen

+

Wähle eine PDF-Datei aus, die unterschrieben werden soll

+ + +

+ ⚠️ Bitte warten Sie, bis die Signatur-Station verbunden ist. +

+
+ + +
+
+ + + + \ No newline at end of file diff --git a/html/signature.html b/html/signature.html new file mode 100644 index 0000000..abb3bb8 --- /dev/null +++ b/html/signature.html @@ -0,0 +1,73 @@ + + + + + + Unterschrift - PDF Signatur + + + + +
+
+

✍️ Unterschriften-Station

+

Bereit für Unterschriften

+
+ +
+
+
+

Verbinde mit Server...

+
+ + + + +
+
+ + + + \ No newline at end of file diff --git a/js/master.js b/js/master.js new file mode 100644 index 0000000..bbe2585 --- /dev/null +++ b/js/master.js @@ -0,0 +1,757 @@ +const { PDFDocument, rgb } = PDFLib; + +// Configure pdf.js worker +pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; + +let originalPdfBytes = null; +let signedPdfBytes = null; +let pdfFileName = ''; +let pdfDoc = null; +let currentPage = null; +let currentPageNum = 1; +let totalPages = 1; +let signatureDataUrl = null; + +// Signature position and size (relative to PDF) +let signaturePos = { x: 0.5, y: 0.1 }; // Relative position (0-1) +let signatureScale = 0.3; + +// WebSocket connection +let ws = null; + +function updateConnectionStatus(isConnected) { + const statusIndicator = document.getElementById('statusIndicator'); + const connectionText = document.getElementById('connectionText'); + const pdfInput = document.getElementById('pdfInput'); + const pdfSelectButton = document.getElementById('pdfSelectButton'); + const pdfSelectHint = document.getElementById('pdfSelectHint'); + + if (isConnected) { + statusIndicator.classList.remove('disconnected'); + statusIndicator.classList.add('connected'); + connectionText.textContent = '✓ Verbunden mit Signatur-Station'; + + // Enable PDF selection + pdfInput.disabled = false; + pdfSelectButton.disabled = false; + pdfSelectHint.style.display = 'none'; + } else { + statusIndicator.classList.remove('connected'); + statusIndicator.classList.add('disconnected'); + connectionText.textContent = '⏳ Warte auf Signatur-Station...'; + + // Disable PDF selection + pdfInput.disabled = true; + pdfSelectButton.disabled = true; + pdfSelectHint.style.display = 'block'; + } +} + +function connectWebSocket() { + // Use current host (works for localhost, IP addresses, and reverse proxy) + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsHost = window.location.hostname; + // If using HTTPS (reverse proxy), use same port as page (usually 443, no port needed) + // If using HTTP directly, use port 8080 + const wsPort = window.location.protocol === 'https:' ? '' : ':8080'; + const wsUrl = `${wsProtocol}//${wsHost}${wsPort}`; + console.log('Verbinde WebSocket zu:', wsUrl); + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log('WebSocket verbunden'); + // Show connection status section + document.getElementById('connectionStatus').style.display = 'block'; + // Register as master + ws.send(JSON.stringify({ + type: 'register_master' + })); + }; + + ws.onmessage = async (event) => { + const data = JSON.parse(event.data); + + if (data.type === 'signature') { + console.log('Unterschrift empfangen'); + signatureDataUrl = data.signature; + await showSignatureOverlay(data.signature); + } else if (data.type === 'signature_station_connected') { + console.log('Signatur-Station verbunden'); + updateConnectionStatus(true); + } else if (data.type === 'signature_station_disconnected') { + console.log('Signatur-Station getrennt'); + updateConnectionStatus(false); + } + }; + + ws.onerror = (error) => { + console.error('WebSocket Fehler:', error); + }; + + ws.onclose = () => { + console.log('WebSocket getrennt'); + setTimeout(connectWebSocket, 1000); + }; +} + +document.getElementById('pdfInput').addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) return; + + // Store filename for download + pdfFileName = file.name.replace('.pdf', ''); + + const arrayBuffer = await file.arrayBuffer(); + + // Store TWO copies - one for rendering, one for pdf-lib + originalPdfBytes = new Uint8Array(arrayBuffer); + const renderCopy = new Uint8Array(arrayBuffer.slice(0)); + + // Create array for sending BEFORE any async operations + const pdfArray = Array.from(originalPdfBytes); + + // Show status section + document.getElementById('uploadSection').style.display = 'none'; + document.getElementById('statusSection').style.display = 'block'; + + // Render PDF preview using the copy + await renderPdfPreview(renderCopy); + + // Send PDF to server using the pre-created array + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'pdf', + pdf: pdfArray + })); + console.log('PDF gesendet, Größe:', pdfArray.length); + + // Send initial page info after a short delay (to ensure signature station received PDF) + setTimeout(() => { + if (ws && ws.readyState === WebSocket.OPEN && totalPages > 0) { + ws.send(JSON.stringify({ + type: 'page_change', + pageNum: 1, + totalPages: totalPages + })); + console.log('Initiale Seiteninformation gesendet: Seite 1 von', totalPages); + } + }, 500); + } +}); + +async function renderPdfPreview(pdfBytes, pageNum = 1) { + try { + // ALWAYS create a fresh copy to prevent detachment + // pdf.js consumes/detaches the buffer during rendering + let safeCopy; + if (pdfBytes instanceof Uint8Array) { + safeCopy = new Uint8Array(pdfBytes.buffer.slice(0)); + } else { + safeCopy = new Uint8Array(pdfBytes); + } + + const loadingTask = pdfjsLib.getDocument({ data: safeCopy }); + pdfDoc = await loadingTask.promise; + totalPages = pdfDoc.numPages; + currentPageNum = pageNum; + + currentPage = await pdfDoc.getPage(pageNum); + + const canvas = document.getElementById('pdfCanvas'); + const ctx = canvas.getContext('2d'); + + const viewport = currentPage.getViewport({ scale: 1.5 }); + canvas.width = viewport.width; + canvas.height = viewport.height; + + const renderContext = { + canvasContext: ctx, + viewport: viewport + }; + + await currentPage.render(renderContext).promise; + + // Update page navigation + updatePageNavigation(); + + console.log('PDF Seite', pageNum, 'erfolgreich gerendert'); + } catch (error) { + console.error('Fehler beim Rendern des PDFs:', error); + alert('Fehler beim Anzeigen der PDF: ' + error.message); + } +} + +async function renderAllPages(pdfBytes) { + try { + // Create a fresh copy + let safeCopy; + if (pdfBytes instanceof Uint8Array) { + safeCopy = new Uint8Array(pdfBytes.buffer.slice(0)); + } else { + safeCopy = new Uint8Array(pdfBytes); + } + + console.log('Rendere ALLE Seiten, Größe:', safeCopy.length, 'bytes'); + + const loadingTask = pdfjsLib.getDocument({ data: safeCopy }); + const doc = await loadingTask.promise; + const numPages = doc.numPages; + + console.log('PDF hat', numPages, 'Seiten'); + + // Get container and clear it + const container = document.getElementById('pdfCanvasContainer'); + container.innerHTML = ''; + + // Make pdf-preview scrollable + const pdfPreview = document.getElementById('pdfPreview'); + pdfPreview.classList.add('scrollable'); + + // Render all pages + for (let pageNum = 1; pageNum <= numPages; pageNum++) { + + // Add page separator + const separator = document.createElement('div'); + separator.className = 'pdf-page-separator'; + separator.textContent = `Seite ${pageNum} von ${numPages}`; + container.appendChild(separator); + + // Create canvas for this page + const canvas = document.createElement('canvas'); + canvas.className = 'pdf-page-canvas'; + canvas.id = `pdf-page-${pageNum}`; + container.appendChild(canvas); + + // Render page + const page = await doc.getPage(pageNum); + const ctx = canvas.getContext('2d'); + + const viewport = page.getViewport({ scale: 1.5 }); + canvas.width = viewport.width; + canvas.height = viewport.height; + + const renderContext = { + canvasContext: ctx, + viewport: viewport + }; + + await page.render(renderContext).promise; + } + + // Hide page navigation after placing + document.getElementById('pageNavigation').style.display = 'none'; + + console.log('Alle', numPages, 'Seiten erfolgreich gerendert'); + } catch (error) { + console.error('Fehler beim Rendern aller Seiten:', error); + alert('Fehler beim Anzeigen der PDF: ' + error.message); + } +} + +function updatePageNavigation() { + const pageNav = document.getElementById('pageNavigation'); + const pageInfo = document.getElementById('pageInfo'); + const prevBtn = document.getElementById('prevPageBtn'); + const nextBtn = document.getElementById('nextPageBtn'); + const pageSelector = document.getElementById('pageSelector'); + const currentPageNumSpan = document.getElementById('currentPageNum'); + + if (totalPages > 1) { + pageNav.style.display = 'flex'; + pageInfo.textContent = `Seite ${currentPageNum} von ${totalPages}`; + prevBtn.disabled = currentPageNum <= 1; + nextBtn.disabled = currentPageNum >= totalPages; + + // Show page selector if signature is visible + if (document.getElementById('signatureOverlay').classList.contains('show')) { + pageSelector.style.display = 'block'; + currentPageNumSpan.textContent = currentPageNum; + } + } else { + pageNav.style.display = 'none'; + pageSelector.style.display = 'none'; + } +} + +async function changePage(direction) { + const newPage = currentPageNum + direction; + if (newPage < 1 || newPage > totalPages) return; + + await renderPdfPreview(originalPdfBytes, newPage); + + // Send page change to signature station + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'page_change', + pageNum: newPage, + totalPages: totalPages + })); + } +} + +function updatePageSelection() { + const currentOnly = document.getElementById('currentPageOnly'); + const allPages = document.getElementById('allPages'); + + // Toggle logic + if (currentOnly.checked && allPages.checked) { + if (event.target === currentOnly) { + allPages.checked = false; + } else { + currentOnly.checked = false; + } + } + + // Ensure at least one is checked + if (!currentOnly.checked && !allPages.checked) { + currentOnly.checked = true; + } +} + +async function showSignatureOverlay(signatureDataUrl) { + const overlay = document.getElementById('signatureOverlay'); + const img = document.getElementById('signatureImage'); + + img.src = signatureDataUrl; + overlay.classList.add('show'); + + document.getElementById('signatureControls').classList.add('show'); + + // Show page selector if multi-page + if (totalPages > 1) { + document.getElementById('pageSelector').style.display = 'block'; + document.getElementById('currentPageNum').textContent = currentPageNum; + } + + document.getElementById('statusMessage').className = 'status signed'; + document.getElementById('statusMessage').textContent = '✅ Unterschrift erhalten! Wähle die Seite(n) und positioniere sie.'; + document.getElementById('downloadButton').disabled = true; + document.getElementById('downloadHint').style.display = 'none'; + + // Position signature initially (use clientWidth for displayed size) + const canvas = document.getElementById('pdfCanvas'); + overlay.style.left = (canvas.clientWidth * signaturePos.x) + 'px'; + overlay.style.top = (canvas.clientHeight * signaturePos.y) + 'px'; + + // Make signature draggable + makeDraggable(overlay); + makeResizable(overlay); +} + +function makeDraggable(element) { + let isDragging = false; + let startX, startY, initialX, initialY; + + element.addEventListener('mousedown', (e) => { + if (e.target.classList.contains('resize-handle') || e.target.classList.contains('handle')) { + return; + } + isDragging = true; + startX = e.clientX; + startY = e.clientY; + initialX = element.offsetLeft; + initialY = element.offsetTop; + element.style.cursor = 'grabbing'; + }); + + document.addEventListener('mousemove', (e) => { + if (!isDragging) return; + + const dx = e.clientX - startX; + const dy = e.clientY - startY; + + element.style.left = (initialX + dx) + 'px'; + element.style.top = (initialY + dy) + 'px'; + }); + + document.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + element.style.cursor = 'move'; + updateSignaturePosition(); + } + }); +} + +function makeResizable(element) { + const resizeHandle = document.getElementById('resizeHandle'); + let isResizing = false; + let startX, startWidth; + + resizeHandle.addEventListener('mousedown', (e) => { + e.stopPropagation(); + isResizing = true; + startX = e.clientX; + startWidth = element.querySelector('img').width; + }); + + document.addEventListener('mousemove', (e) => { + if (!isResizing) return; + + const dx = e.clientX - startX; + const newWidth = Math.max(100, startWidth + dx); + element.querySelector('img').style.maxWidth = newWidth + 'px'; + }); + + document.addEventListener('mouseup', () => { + if (isResizing) { + isResizing = false; + updateSignatureScale(); + } + }); +} + +function updateSignaturePosition() { + const overlay = document.getElementById('signatureOverlay'); + const canvas = document.getElementById('pdfCanvas'); + + signaturePos.x = overlay.offsetLeft / canvas.width; + signaturePos.y = overlay.offsetTop / canvas.height; + console.log('Neue Position:', signaturePos); +} + +function updateSignatureScale() { + const img = document.getElementById('signatureImage'); + const canvas = document.getElementById('pdfCanvas'); + + signatureScale = img.width / canvas.width; + console.log('Neue Skalierung:', signatureScale); +} + +async function addSignatureToPdf() { + try { + if (!originalPdfBytes || originalPdfBytes.length === 0) { + console.error('originalPdfBytes ist nicht verfügbar'); + alert('Fehler: PDF-Daten nicht verfügbar. Bitte laden Sie die PDF erneut hoch.'); + return false; + } + + // Determine which pages to add signature to + const currentOnly = document.getElementById('currentPageOnly').checked; + const allPagesChecked = document.getElementById('allPages').checked; + + let pagesToSign = []; + if (allPagesChecked) { + // All pages + for (let i = 1; i <= totalPages; i++) { + pagesToSign.push(i); + } + } else { + // Current page only + pagesToSign.push(currentPageNum); + } + + console.log('=== FÜGE UNTERSCHRIFTEN HINZU ==='); + console.log('Seiten zum Signieren:', pagesToSign.join(', ')); + + // Load PDF with pdf-lib + // Use ignoreEncryption to handle encrypted PDFs + const pdfLibDoc = await PDFDocument.load(originalPdfBytes, { ignoreEncryption: true }); + const pages = pdfLibDoc.getPages(); + + // Get signature position and size from overlay + const overlay = document.getElementById('signatureOverlay'); + const overlayLeft = overlay.offsetLeft; + const overlayTop = overlay.offsetTop; + const overlayWidth = overlay.querySelector('img').width; + + // Embed signature image once + const signatureImage = await pdfLibDoc.embedPng(signatureDataUrl); + + // Add signature to each selected page + for (const pageNum of pagesToSign) { + const page = pages[pageNum - 1]; // 0-indexed + const { width, height } = page.getSize(); + + console.log('--- Bearbeite Seite', pageNum, '---'); + console.log('Seite Dimensionen:', width, 'x', height); + + // Get canvas display size + const canvas = document.getElementById('pdfCanvas'); + const canvasDisplayWidth = canvas.clientWidth; + const canvasDisplayHeight = canvas.clientHeight; + + const scaleX = width / canvasDisplayWidth; + const scaleY = height / canvasDisplayHeight; + + console.log('Scale Faktoren - X:', scaleX, 'Y:', scaleY); + console.log('Overlay Position:', overlayLeft, overlayTop); + console.log('Overlay Breite:', overlayWidth); + + // Calculate signature dimensions in PDF coordinates + const sigWidth = overlayWidth * scaleX; + const sigHeight = sigWidth * (signatureImage.height / signatureImage.width); + + // Convert position to PDF coordinates (bottom-left origin) + const x = overlayLeft * scaleX; + const y = height - (overlayTop * scaleY) - sigHeight; + + console.log('PDF Position:', x, y, 'Größe:', sigWidth, 'x', sigHeight); + + // Add signature to page + page.drawImage(signatureImage, { + x: x, + y: y, + width: sigWidth, + height: sigHeight, + }); + + console.log('✓ Unterschrift zu Seite', pageNum, 'hinzugefügt'); + } + + // Save PDF + console.log('Speichere PDF...'); + const pdfBytes = await pdfLibDoc.save(); + console.log('PDF gespeichert, Typ:', pdfBytes.constructor.name, 'Größe:', pdfBytes.length); + + if (!pdfBytes || pdfBytes.length === 0) { + console.error('FEHLER: pdfDoc.save() hat leeres Array zurückgegeben!'); + alert('Fehler beim Speichern der PDF. Bitte versuchen Sie es erneut.'); + return false; + } + + // Store as Uint8Array + signedPdfBytes = new Uint8Array(pdfBytes); + console.log('signedPdfBytes gespeichert, Typ:', signedPdfBytes.constructor.name, 'Größe:', signedPdfBytes.length); + console.log('=== UNTERSCHRIFTEN ERFOLGREICH HINZUGEFÜGT ==='); + + return true; + + } catch (error) { + console.error('Fehler beim Hinzufügen der Unterschrift:', error); + console.error('Stack:', error.stack); + alert('Fehler beim Verarbeiten der Unterschrift: ' + error.message); + return false; + } +} + +function removeSignatureOverlay() { + // Only remove the overlay, keep signedPdfBytes intact + document.getElementById('signatureOverlay').classList.remove('show'); + document.getElementById('signatureControls').classList.remove('show'); + signatureDataUrl = null; + console.log('removeSignatureOverlay() aufgerufen - signedPdfBytes:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL'); +} + +function removeSignature() { + // Complete reset including signedPdfBytes + document.getElementById('signatureOverlay').classList.remove('show'); + document.getElementById('signatureControls').classList.remove('show'); + document.getElementById('statusMessage').className = 'status waiting'; + document.getElementById('statusMessage').textContent = 'Warte auf Unterschrift von der Signatur-Station...'; + document.getElementById('downloadButton').disabled = true; + document.getElementById('downloadHint').style.display = 'none'; + signatureDataUrl = null; + console.log('removeSignature() aufgerufen - SETZE signedPdfBytes auf NULL'); + signedPdfBytes = null; +} + +// Place signature button +document.getElementById('placeSignatureButton').addEventListener('click', async () => { + if (!signatureDataUrl) { + console.error('Keine signatureDataUrl vorhanden'); + return; + } + + console.log('=== PLATZIERE UNTERSCHRIFT ==='); + console.log('signatureDataUrl vorhanden:', signatureDataUrl ? 'Ja' : 'Nein'); + console.log('originalPdfBytes vorhanden:', originalPdfBytes ? 'Ja (' + originalPdfBytes.length + ' bytes)' : 'Nein'); + + // Show loading state + const btn = document.getElementById('placeSignatureButton'); + const originalText = btn.textContent; + btn.textContent = '⏳ PDF wird erstellt...'; + btn.disabled = true; + + // Add signature to PDF + const success = await addSignatureToPdf(); + + console.log('addSignatureToPdf Ergebnis:', success); + console.log('signedPdfBytes nach Bearbeitung:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL'); + + if (success && signedPdfBytes) { + // Hide signature overlay (but keep signedPdfBytes!) + removeSignatureOverlay(); + + // Update status + document.getElementById('statusMessage').className = 'status ready'; + document.getElementById('statusMessage').textContent = '✅ PDF wurde erstellt! Bereit zum Download.'; + + // Enable download + document.getElementById('downloadButton').disabled = false; + document.getElementById('downloadHint').style.display = 'block'; + + console.log('Download-Button Status:', document.getElementById('downloadButton').disabled ? 'DEAKTIVIERT' : 'AKTIVIERT'); + console.log('signedPdfBytes für Download:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL'); + + // Send confirmation to signature station that signature was placed + console.log('=== SENDE PLATZIERUNGS-BESTÄTIGUNG ==='); + console.log('WebSocket vorhanden:', ws ? 'Ja' : 'Nein'); + console.log('WebSocket readyState:', ws ? ws.readyState : 'N/A'); + console.log('WebSocket OPEN:', ws && ws.readyState === WebSocket.OPEN ? 'Ja' : 'Nein'); + + if (ws && ws.readyState === WebSocket.OPEN) { + try { + const message = JSON.stringify({ + type: 'signature_placed' + }); + ws.send(message); + console.log('✅ Platzierungs-Bestätigung an Server gesendet'); + } catch (error) { + console.error('❌ Fehler beim Senden der Bestätigung:', error); + } + } else { + console.error('❌ WebSocket nicht verfügbar oder nicht geöffnet - Bestätigung kann nicht gesendet werden'); + } + + // Render the FINAL PDF with signature - show ALL pages + console.log('Rendere finale PDF mit Unterschrift - ALLE Seiten...'); + const renderCopy = new Uint8Array(signedPdfBytes); + await renderAllPages(renderCopy); + console.log('Finale PDF angezeigt - alle Seiten'); + console.log('signedPdfBytes nach Rendering:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL'); + console.log('=== PLATZIERUNG ERFOLGREICH ==='); + } else { + // Reset button on error + console.error('Platzierung fehlgeschlagen! success:', success, 'signedPdfBytes:', signedPdfBytes ? 'vorhanden' : 'NULL'); + btn.textContent = originalText; + btn.disabled = false; + alert('Fehler beim Platzieren der Unterschrift. Bitte versuchen Sie es erneut.'); + } +}); + +document.getElementById('downloadButton').addEventListener('click', async () => { + console.log('=== DOWNLOAD BUTTON GEKLICKT ==='); + console.log('signedPdfBytes:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL/UNDEFINED'); + console.log('signedPdfBytes type:', typeof signedPdfBytes); + + if (!signedPdfBytes || signedPdfBytes.length === 0) { + console.error('FEHLER: signedPdfBytes ist nicht verfügbar!'); + alert('Fehler: Kein signiertes PDF verfügbar. Bitte platzieren Sie die Unterschrift zuerst.'); + return; + } + + try { + console.log('Starte Download, PDF Größe:', signedPdfBytes.length, 'bytes'); + + // Ensure signedPdfBytes is Uint8Array + const pdfData = signedPdfBytes instanceof Uint8Array ? signedPdfBytes : new Uint8Array(signedPdfBytes); + console.log('PDF Daten konvertiert, Typ:', pdfData.constructor.name, 'Größe:', pdfData.length); + + // Download with _signed suffix + const blob = new Blob([pdfData], { type: 'application/pdf' }); + console.log('Blob erstellt, Größe:', blob.size); + + const url = URL.createObjectURL(blob); + console.log('Blob URL erstellt:', url); + + const a = document.createElement('a'); + a.href = url; + a.download = pdfFileName + '_signed.pdf'; + console.log('Download Dateiname:', a.download); + + a.click(); + URL.revokeObjectURL(url); + + console.log('PDF heruntergeladen als:', pdfFileName + '_signed.pdf'); + console.log('=== DOWNLOAD ERFOLGREICH ==='); + + // Reset for next PDF + document.getElementById('uploadSection').style.display = 'block'; + document.getElementById('statusSection').style.display = 'none'; + + // Reset file input + document.getElementById('pdfInput').value = ''; + + // Reset place signature button + const placeBtn = document.getElementById('placeSignatureButton'); + placeBtn.textContent = '✓ Unterschrift platzieren & PDF erstellen'; + placeBtn.disabled = false; + + // Reset page navigation and scrollable state + currentPageNum = 1; + totalPages = 1; + document.getElementById('pageNavigation').style.display = 'none'; + document.getElementById('pageSelector').style.display = 'none'; + document.getElementById('pdfPreview').classList.remove('scrollable'); + + // Reset canvas container to single canvas + const container = document.getElementById('pdfCanvasContainer'); + container.innerHTML = ''; + + removeSignature(); + originalPdfBytes = null; + signedPdfBytes = null; + pdfFileName = ''; + pdfDoc = null; + currentPage = null; + + console.log('Alles zurückgesetzt für nächste PDF'); + } catch (error) { + console.error('Fehler beim Download:', error); + alert('Fehler beim Herunterladen: ' + error.message); + } +}); + +function resetToStart() { + console.log('=== ZURÜCK ZUM ANFANG ==='); + + // Show upload section, hide status section + document.getElementById('uploadSection').style.display = 'block'; + document.getElementById('statusSection').style.display = 'none'; + + // Reset file input + document.getElementById('pdfInput').value = ''; + + // Reset place signature button + const placeBtn = document.getElementById('placeSignatureButton'); + placeBtn.textContent = '✓ Unterschrift platzieren & PDF erstellen'; + placeBtn.disabled = false; + + // Reset download button + document.getElementById('downloadButton').disabled = true; + document.getElementById('downloadHint').style.display = 'none'; + + // Reset page navigation and scrollable state + currentPageNum = 1; + totalPages = 1; + document.getElementById('pageNavigation').style.display = 'none'; + document.getElementById('pageSelector').style.display = 'none'; + document.getElementById('pdfPreview').classList.remove('scrollable'); + + // Reset canvas container to single canvas + const container = document.getElementById('pdfCanvasContainer'); + container.innerHTML = ''; + + // Reset status message + document.getElementById('statusMessage').className = 'status waiting'; + document.getElementById('statusMessage').textContent = 'Warte auf Unterschrift von der Signatur-Station...'; + + // Clear all data + removeSignature(); + originalPdfBytes = null; + signedPdfBytes = null; + pdfFileName = ''; + pdfDoc = null; + currentPage = null; + signatureDataUrl = null; + + console.log('Zurück zum Anfang - bereit für neue PDF'); +} + +// Discard button event listener +document.getElementById('discardButton').addEventListener('click', () => { + if (confirm('Möchten Sie wirklich verwerfen? Alle Änderungen gehen verloren.')) { + // Notify signature station to reset + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'discard' + })); + console.log('Verwerfen-Nachricht an Signatur-Station gesendet'); + } + resetToStart(); + } +}); + +// Connect WebSocket on load +connectWebSocket(); diff --git a/js/signature.js b/js/signature.js new file mode 100644 index 0000000..3bcf3a9 --- /dev/null +++ b/js/signature.js @@ -0,0 +1,300 @@ +// Configure pdf.js worker +pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; + +const canvas = document.getElementById('signatureCanvas'); +const ctx = canvas.getContext('2d'); +let isDrawing = false; +let hasSignature = false; +let ws = null; +let currentPdf = null; +let pdfDoc = null; +let currentPageNum = 1; +let totalPages = 1; + +connectWebSocket(); + +function connectWebSocket() { + // Use current host (works for localhost, IP addresses, and reverse proxy) + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsHost = window.location.hostname; + // If using HTTPS (reverse proxy), use same port as page (usually 443, no port needed) + // If using HTTP directly, use port 8080 + const wsPort = window.location.protocol === 'https:' ? '' : ':8080'; + const wsUrl = `${wsProtocol}//${wsHost}${wsPort}`; + console.log('Verbinde WebSocket zu:', wsUrl); + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log('WebSocket verbunden'); + document.getElementById('loadingSection').style.display = 'none'; + document.getElementById('waitingSection').style.display = 'block'; + + // Register as signature station + ws.send(JSON.stringify({ + type: 'register_signature' + })); + }; + + ws.onmessage = async (event) => { + const data = JSON.parse(event.data); + + if (data.type === 'pdf') { + console.log('PDF empfangen'); + currentPdf = new Uint8Array(data.pdf); + + // Show PDF notification + document.getElementById('waitingSection').style.display = 'none'; + document.getElementById('signatureSection').style.display = 'block'; + document.getElementById('pdfDisplay').classList.add('show'); + + // Render ALL pages of PDF + await renderPdfPreview(currentPdf); + + // Clear previous signature if any + clearSignature(); + } else if (data.type === 'page_change') { + currentPageNum = data.pageNum; + totalPages = data.totalPages; + + // Scroll to the specific page + const pageCanvas = document.getElementById(`page-canvas-${data.pageNum}`); + if (pageCanvas) { + pageCanvas.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } else if (data.type === 'signature_placed') { + console.log('✅ Unterschrift wurde platziert! Reset in 5 Sekunden...'); + console.log('Empfangene Daten:', JSON.stringify(data)); + + // Show immediate feedback + const successMsg = document.getElementById('successMessage'); + successMsg.classList.add('show'); + + // Reset after 5 seconds + setTimeout(() => { + console.log('Auto-Reset wird durchgeführt...'); + + // Clear signature + clearSignature(); + + // Hide success message + document.getElementById('successMessage').classList.remove('show'); + + // Hide PDF and signature section, show waiting + document.getElementById('signatureSection').style.display = 'none'; + document.getElementById('pdfDisplay').classList.remove('show'); + document.getElementById('waitingSection').style.display = 'block'; + + // Reset PDF data + currentPdf = null; + pdfDoc = null; + currentPageNum = 1; + totalPages = 1; + + console.log('Bereit für nächste Unterschrift'); + }, 5000); + } else if (data.type === 'discard') { + console.log('🔄 Verwerfen-Nachricht empfangen, setze zurück...'); + resetSignatureStation(); + } + }; + + ws.onerror = (error) => { + console.error('WebSocket Fehler:', error); + document.getElementById('loadingSection').innerHTML = + '

❌ Verbindung zum Server fehlgeschlagen.

'; + }; + + ws.onclose = () => { + console.log('WebSocket getrennt'); + setTimeout(connectWebSocket, 1000); + }; +} + +async function renderPdfPreview(pdfBytes, pageNum = null) { + try { + // Create fresh copy to prevent detachment + const safeCopy = new Uint8Array(pdfBytes.buffer.slice(0)); + + console.log('renderPdfPreview aufgerufen mit', pdfBytes.length, 'bytes'); + const loadingTask = pdfjsLib.getDocument({ data: safeCopy }); + pdfDoc = await loadingTask.promise; + totalPages = pdfDoc.numPages; + + // Update page info + document.getElementById('pageInfoSignature').textContent = `${totalPages} Seite${totalPages > 1 ? 'n' : ''}`; + + // Get container + const container = document.getElementById('pdfPagesContainer'); + container.innerHTML = ''; // Clear existing content + + // Render all pages + for (let pageNum = 1; pageNum <= totalPages; pageNum++) { + + // Add page separator if not first page + if (pageNum > 1) { + const separator = document.createElement('div'); + separator.className = 'page-separator'; + separator.textContent = `Seite ${pageNum}`; + container.appendChild(separator); + } else { + // First page indicator + const separator = document.createElement('div'); + separator.className = 'page-separator'; + separator.textContent = `Seite 1`; + container.appendChild(separator); + } + + // Create canvas for this page + const canvas = document.createElement('canvas'); + canvas.className = 'pdf-preview-canvas'; + canvas.id = `page-canvas-${pageNum}`; + container.appendChild(canvas); + + // Render page + const page = await pdfDoc.getPage(pageNum); + const ctx = canvas.getContext('2d'); + + const viewport = page.getViewport({ scale: 1.2 }); + canvas.width = viewport.width; + canvas.height = viewport.height; + + const renderContext = { + canvasContext: ctx, + viewport: viewport + }; + + await page.render(renderContext).promise; + } + + console.log('Alle', totalPages, 'Seiten erfolgreich gerendert'); + } catch (error) { + console.error('Fehler beim Rendern der PDF Vorschau:', error); + } +} + +// Canvas setup +ctx.strokeStyle = '#000'; +ctx.lineWidth = 2; +ctx.lineCap = 'round'; +ctx.lineJoin = 'round'; + +function startDrawing(e) { + isDrawing = true; + const pos = getPosition(e); + ctx.beginPath(); + ctx.moveTo(pos.x, pos.y); +} + +function draw(e) { + if (!isDrawing) return; + e.preventDefault(); + + const pos = getPosition(e); + ctx.lineTo(pos.x, pos.y); + ctx.stroke(); + + hasSignature = true; + document.getElementById('submitButton').disabled = false; +} + +function stopDrawing() { + isDrawing = false; +} + +function getPosition(e) { + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + if (e.touches && e.touches.length > 0) { + return { + x: (e.touches[0].clientX - rect.left) * scaleX, + y: (e.touches[0].clientY - rect.top) * scaleY + }; + } + + return { + x: (e.clientX - rect.left) * scaleX, + y: (e.clientY - rect.top) * scaleY + }; +} + +// Mouse events +canvas.addEventListener('mousedown', startDrawing); +canvas.addEventListener('mousemove', draw); +canvas.addEventListener('mouseup', stopDrawing); +canvas.addEventListener('mouseout', stopDrawing); + +// Touch events +canvas.addEventListener('touchstart', startDrawing); +canvas.addEventListener('touchmove', draw); +canvas.addEventListener('touchend', stopDrawing); + +// Clear button +document.getElementById('clearButton').addEventListener('click', () => { + clearSignature(); +}); + +function clearSignature() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + hasSignature = false; + document.getElementById('submitButton').disabled = true; + document.getElementById('successMessage').classList.remove('show'); + document.getElementById('errorMessage').classList.remove('show'); +} + +function resetSignatureStation() { + console.log('=== SIGNATUR-STATION ZURÜCKSETZEN ==='); + + // Clear signature canvas + clearSignature(); + + // Hide success and error messages + document.getElementById('successMessage').classList.remove('show'); + document.getElementById('errorMessage').classList.remove('show'); + + // Hide PDF and signature section, show waiting + document.getElementById('signatureSection').style.display = 'none'; + document.getElementById('pdfDisplay').classList.remove('show'); + document.getElementById('waitingSection').style.display = 'block'; + + // Clear PDF pages container + const container = document.getElementById('pdfPagesContainer'); + if (container) { + container.innerHTML = ''; + } + + // Reset PDF data + currentPdf = null; + pdfDoc = null; + currentPageNum = 1; + totalPages = 1; + + console.log('Signatur-Station zurückgesetzt - bereit für neue PDF'); +} + +// Submit button +document.getElementById('submitButton').addEventListener('click', async () => { + if (!hasSignature || !ws || ws.readyState !== WebSocket.OPEN) return; + + try { + // Get signature as data URL + const signatureDataUrl = canvas.toDataURL('image/png'); + + // Send to server (no session ID needed) + ws.send(JSON.stringify({ + type: 'signature', + signature: signatureDataUrl + })); + + // Show success message + document.getElementById('successMessage').classList.add('show'); + document.getElementById('submitButton').disabled = true; + + console.log('Unterschrift gesendet, warte auf Platzierungs-Bestätigung...'); + + } catch (error) { + console.error('Fehler beim Senden:', error); + document.getElementById('errorMessage').classList.add('show'); + } +}); diff --git a/master.html b/master.html new file mode 100644 index 0000000..a747a38 --- /dev/null +++ b/master.html @@ -0,0 +1,97 @@ + + + + + + Master - PDF Unterschrift System + + + + + +
+
+

📄 Master Station

+

PDF hochladen und Unterschrift empfangen

+
+ +
+
+

PDF Dokument auswählen

+

Wähle eine PDF-Datei aus, die unterschrieben werden soll

+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6b40201 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,43 @@ +{ + "name": "pdf-signature-system", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "pdf-signature-system", + "version": "1.0.0", + "dependencies": { + "ws": "^8.14.2" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "requires": {} + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4a41ca2 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "pdf-signature-system", + "version": "1.0.0", + "description": "Zwei-Seiten PDF Unterschrift System", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "ws": "^8.14.2" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..1c52a18 --- /dev/null +++ b/server.js @@ -0,0 +1,361 @@ +const WebSocket = require('ws'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +// Create HTTP server +const server = http.createServer((req, res) => { + // CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // Serve files + let urlPath = req.url; + + // Remove query string if present + urlPath = urlPath.split('?')[0]; + + // Handle root path + if (urlPath === '/' || urlPath === '/index.html') { + urlPath = '/html/signature.html'; + } + + // Map URL paths to file system paths based on file type + let cleanPath = urlPath.startsWith('/') ? urlPath.substring(1) : urlPath; + + // Route requests to appropriate directories + // Only add directory prefix if path doesn't already have it + if (cleanPath.endsWith('.html')) { + // HTML files from html/ directory + const fileName = path.basename(cleanPath); + if (!cleanPath.startsWith('html/')) { + cleanPath = 'html/' + fileName; + } + } else if (cleanPath.endsWith('.css')) { + // CSS files from css/ directory + const fileName = path.basename(cleanPath); + if (!cleanPath.startsWith('css/')) { + cleanPath = 'css/' + fileName; + } + } else if (cleanPath.endsWith('.js')) { + // JS files from js/ directory + const fileName = path.basename(cleanPath); + if (!cleanPath.startsWith('js/')) { + cleanPath = 'js/' + fileName; + } + } + + // Security: Prevent directory traversal attacks + if (cleanPath.includes('..')) { + res.writeHead(403, { 'Content-Type': 'text/html' }); + res.end('

403 - Zugriff verweigert

', 'utf-8'); + return; + } + + // Convert to filesystem path + const filePath = path.join(process.cwd(), cleanPath); + + // Security: Ensure resolved path is within current directory + const currentDir = process.cwd(); + const resolvedPath = path.resolve(filePath); + + if (!resolvedPath.startsWith(currentDir)) { + res.writeHead(403, { 'Content-Type': 'text/html' }); + res.end('

403 - Zugriff verweigert

', 'utf-8'); + return; + } + + const extname = String(path.extname(filePath)).toLowerCase(); + const mimeTypes = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.pdf': 'application/pdf', + '.ico': 'image/x-icon' + }; + + const contentType = mimeTypes[extname] || 'application/octet-stream'; + + // Log file requests for debugging + console.log(`📄 Request: ${req.url} -> ${filePath} (${contentType})`); + + fs.readFile(filePath, (error, content) => { + if (error) { + if (error.code === 'ENOENT') { + console.log(`❌ 404: Datei nicht gefunden: ${filePath}`); + res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(`

404 - Datei nicht gefunden

Gesuchte Datei: ${filePath}

`, 'utf-8'); + } else { + console.error(`❌ Server Fehler bei ${filePath}:`, error); + res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(`

500 - Server Fehler

${error.code}

`, 'utf-8'); + } + } else { + res.writeHead(200, { 'Content-Type': contentType }); + res.end(content, 'utf-8'); + } + }); +}); + +// Create WebSocket server +const wss = new WebSocket.Server({ server }); + +// Store connections +let masterConnection = null; +let signatureConnection = null; + +wss.on('connection', (ws) => { + console.log('Neue WebSocket Verbindung'); + + ws.on('message', (message) => { + try { + const data = JSON.parse(message); + console.log('📨 Nachricht empfangen:', data.type); + console.log(' Von:', ws === masterConnection ? 'Master' : ws === signatureConnection ? 'Signatur-Station' : 'Unbekannt'); + + switch (data.type) { + case 'register_master': + handleMasterRegister(ws); + break; + + case 'register_signature': + handleSignatureRegister(ws); + break; + + case 'pdf': + handlePdfUpload(data); + break; + + case 'signature': + handleSignature(data); + break; + + case 'page_change': + handlePageChange(data); + break; + + case 'signature_placed': + console.log('🔄 signature_placed Nachricht empfangen, leite weiter...'); + handleSignaturePlaced(); + break; + + case 'discard': + console.log('🔄 discard Nachricht empfangen, leite weiter...'); + handleDiscard(); + break; + + default: + console.log('⚠️ Unbekannter Nachrichtentyp:', data.type); + } + } catch (error) { + console.error('❌ Fehler beim Verarbeiten der Nachricht:', error); + } + }); + + ws.on('close', () => { + console.log('WebSocket Verbindung geschlossen'); + if (ws === masterConnection) { + masterConnection = null; + console.log('Master getrennt'); + } + if (ws === signatureConnection) { + signatureConnection = null; + console.log('Signatur-Station getrennt'); + + // Notify master that signature station disconnected + if (masterConnection && masterConnection.readyState === WebSocket.OPEN) { + masterConnection.send(JSON.stringify({ + type: 'signature_station_disconnected' + })); + console.log('Master über Signatur-Station Trennung informiert'); + } + } + }); +}); + +function handleMasterRegister(ws) { + masterConnection = ws; + console.log('Master registriert'); + + // Notify master about current signature station connection status + if (signatureConnection && signatureConnection.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'signature_station_connected' + })); + console.log('Master über Signatur-Station Verbindung informiert'); + } else { + ws.send(JSON.stringify({ + type: 'signature_station_disconnected' + })); + console.log('Master über fehlende Signatur-Station informiert'); + } +} + +function handleSignatureRegister(ws) { + signatureConnection = ws; + console.log('Signatur-Station registriert'); + + // Notify master that signature station is now connected + if (masterConnection && masterConnection.readyState === WebSocket.OPEN) { + masterConnection.send(JSON.stringify({ + type: 'signature_station_connected' + })); + console.log('Master über Signatur-Station Verbindung informiert'); + } +} + +function handlePdfUpload(data) { + const { pdf } = data; + + if (!signatureConnection || signatureConnection.readyState !== WebSocket.OPEN) { + console.log('Keine Signatur-Station verbunden'); + return; + } + + // Forward PDF to signature station + signatureConnection.send(JSON.stringify({ + type: 'pdf', + pdf: pdf + })); + + console.log('PDF an Signatur-Station weitergeleitet'); +} + +function handleSignature(data) { + const { signature } = data; + + if (!masterConnection || masterConnection.readyState !== WebSocket.OPEN) { + console.log('Keine Master-Verbindung'); + return; + } + + // Send signature to master + masterConnection.send(JSON.stringify({ + type: 'signature', + signature: signature + })); + + console.log('Unterschrift an Master gesendet'); +} + +function handlePageChange(data) { + const { pageNum, totalPages } = data; + + if (!signatureConnection || signatureConnection.readyState !== WebSocket.OPEN) { + console.log('Keine Signatur-Station verbunden'); + return; + } + + // Forward page change to signature station + signatureConnection.send(JSON.stringify({ + type: 'page_change', + pageNum: pageNum, + totalPages: totalPages + })); + + console.log('Seitenwechsel an Signatur-Station weitergeleitet: Seite', pageNum, 'von', totalPages); +} + +function handleSignaturePlaced() { + console.log('handleSignaturePlaced() aufgerufen'); + console.log('signatureConnection vorhanden:', signatureConnection ? 'Ja' : 'Nein'); + console.log('signatureConnection.readyState:', signatureConnection ? signatureConnection.readyState : 'N/A'); + + if (!signatureConnection || signatureConnection.readyState !== WebSocket.OPEN) { + console.log('⚠️ Keine Signatur-Station verbunden - Bestätigung kann nicht gesendet werden'); + return; + } + + try { + // Forward signature placed confirmation to signature station + const message = JSON.stringify({ + type: 'signature_placed' + }); + signatureConnection.send(message); + console.log('✅ Unterschrift-Platzierung-Bestätigung an Signatur-Station gesendet'); + } catch (error) { + console.error('❌ Fehler beim Senden der Bestätigung:', error); + } +} + +function handleDiscard() { + console.log('handleDiscard() aufgerufen'); + console.log('signatureConnection vorhanden:', signatureConnection ? 'Ja' : 'Nein'); + console.log('signatureConnection.readyState:', signatureConnection ? signatureConnection.readyState : 'N/A'); + + if (!signatureConnection || signatureConnection.readyState !== WebSocket.OPEN) { + console.log('⚠️ Keine Signatur-Station verbunden - Verwerfen-Nachricht kann nicht gesendet werden'); + return; + } + + try { + // Forward discard message to signature station + const message = JSON.stringify({ + type: 'discard' + }); + signatureConnection.send(message); + console.log('✅ Verwerfen-Nachricht an Signatur-Station gesendet'); + } catch (error) { + console.error('❌ Fehler beim Senden der Verwerfen-Nachricht:', error); + } +} + +const PORT = 8080; +const HOST = '0.0.0.0'; // Listen on all network interfaces + +server.listen(PORT, HOST, () => { + const os = require('os'); + const networkInterfaces = os.networkInterfaces(); + let ipAddresses = []; + + // Get all IPv4 addresses + Object.keys(networkInterfaces).forEach((interfaceName) => { + networkInterfaces[interfaceName].forEach((iface) => { + if (iface.family === 'IPv4' && !iface.internal) { + ipAddresses.push(iface.address); + } + }); + }); + + console.log(` +╔════════════════════════════════════════════════════════════╗ +║ PDF Unterschrift System gestartet! ║ +╠════════════════════════════════════════════════════════════╝ +║ +║ Lokaler Zugriff: +║ Master-Seite: http://localhost:${PORT}/master.html +║ Signatur-Seite: http://localhost:${PORT}/signature.html +║ `); + + if (ipAddresses.length > 0) { + console.log(`║ +║ Netzwerk-Zugriff (für iPhone/Tablet): `); + ipAddresses.forEach(ip => { + console.log(`║ Master-Seite: http://${ip}:${PORT}/master.html `); + console.log(`║ Signatur-Seite: http://${ip}:${PORT}/signature.html `); + }); + } + + console.log(`║ +║ WICHTIG: Öffne die Signatur-Seite einmal und lasse +║ sie offen. Jede neue PDF wird dort automatisch +║ angezeigt! +║ +║ WebSocket Server läuft auf Port ${PORT} +║ +╚═════════════════════════════════════════════════════════════ + `); +}); \ No newline at end of file