new file: QUICKSTART.md
new file: README_INSTALL.md new file: requirements.txt new file: static/css/style.css new file: static/js/images-to-pdf.js new file: static/js/main.js new file: static/js/pdf-tools.js new file: templates/pdf_tools.html
This commit is contained in:
69
QUICKSTART.md
Normal file
69
QUICKSTART.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Schnellstart-Anleitung für PDF Editor Web App
|
||||||
|
|
||||||
|
## Sofort loslegen (Windows PowerShell)
|
||||||
|
|
||||||
|
1. **Navigieren Sie zum Projektordner:**
|
||||||
|
```powershell
|
||||||
|
cd "c:\Users\Simon.Speedy\Documents\dev projekte\Test\pdf web app"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Virtuelle Umgebung erstellen:**
|
||||||
|
```powershell
|
||||||
|
python -m venv venv
|
||||||
|
.\venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Abhängigkeiten installieren:**
|
||||||
|
```powershell
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Poppler für Windows installieren:**
|
||||||
|
- Downloaden Sie: https://github.com/oschwartz10612/poppler-windows/releases
|
||||||
|
- Extrahieren Sie nach C:\poppler
|
||||||
|
- Fügen Sie C:\poppler\Library\bin zu den Umgebungsvariablen hinzu
|
||||||
|
|
||||||
|
**ODER per Scoop (einfacher):**
|
||||||
|
```powershell
|
||||||
|
# Falls Scoop nicht installiert ist:
|
||||||
|
# Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
# irm get.scoop.sh | iex
|
||||||
|
|
||||||
|
scoop install poppler
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **App starten:**
|
||||||
|
```powershell
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Browser öffnen:**
|
||||||
|
- Gehen Sie zu: http://127.0.0.1:5000
|
||||||
|
|
||||||
|
## Fertig! 🎉
|
||||||
|
|
||||||
|
Die App läuft jetzt lokal auf Ihrem Computer. Sie können:
|
||||||
|
|
||||||
|
- ✅ Bilder zu PDF konvertieren
|
||||||
|
- ✅ PDFs zusammenführen
|
||||||
|
- ✅ PDFs zu Bildern konvertieren
|
||||||
|
- ✅ Drag & Drop verwenden
|
||||||
|
- ✅ Vollständig offline arbeiten
|
||||||
|
|
||||||
|
## Bei Problemen:
|
||||||
|
|
||||||
|
**Fehler bei pdf2image?**
|
||||||
|
```powershell
|
||||||
|
# Poppler-Pfad manuell setzen (falls automatisch nicht funktioniert)
|
||||||
|
$env:PATH += ";C:\poppler\Library\bin"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Port bereits belegt?**
|
||||||
|
- Ändern Sie in app.py die Zeile: `app.run(debug=True, host='127.0.0.1', port=5001)`
|
||||||
|
|
||||||
|
**Importfehler?**
|
||||||
|
```powershell
|
||||||
|
# Virtuelle Umgebung erneut aktivieren
|
||||||
|
.\venv\Scripts\Activate.ps1
|
||||||
|
pip install --upgrade -r requirements.txt
|
||||||
|
```
|
||||||
243
README_INSTALL.md
Normal file
243
README_INSTALL.md
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
# PDF Editor Web App
|
||||||
|
|
||||||
|
Eine vollständige PDF-Bearbeitungs-Web-Anwendung, die als lokaler Flask-Server läuft. Bietet Funktionen zum Konvertieren von Bildern zu PDF, Zusammenführen von PDFs, Konvertieren von PDFs zu Bildern und vieles mehr.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 🖼️ Bilder zu PDF
|
||||||
|
- **Mehrere Bilder auswählen**: Unterstützt JPG, PNG, GIF, BMP, TIFF
|
||||||
|
- **Drag & Drop Interface**: Einfaches Hochladen durch Ziehen und Ablegen
|
||||||
|
- **Sortierbare Reihenfolge**: Bilder durch Ziehen neu anordnen
|
||||||
|
- **Automatische Größenanpassung**: Bilder werden optimal auf PDF-Seiten angepasst
|
||||||
|
- **Progress Tracking**: Echtzeit-Upload-Fortschritt
|
||||||
|
|
||||||
|
### 🔧 PDF Tools
|
||||||
|
- **PDFs zusammenführen**: Mehrere PDFs zu einer Datei kombinieren
|
||||||
|
- **PDF zu Bildern**: Jede PDF-Seite als PNG/JPEG exportieren
|
||||||
|
- **Sortierbare PDF-Liste**: Reihenfolge beim Zusammenführen anpassen
|
||||||
|
- **ZIP-Export**: Alle extrahierten Bilder in einer ZIP-Datei
|
||||||
|
|
||||||
|
### 🛡️ Sicherheit & Datenschutz
|
||||||
|
- **100% Lokal**: Alle Verarbeitung erfolgt auf Ihrem Computer
|
||||||
|
- **Keine Cloud-Uploads**: Dateien verlassen nie Ihr System
|
||||||
|
- **Automatische Bereinigung**: Temporäre Dateien werden nach 1 Stunde gelöscht
|
||||||
|
- **Sichere Dateinamen**: Automatische Bereinigung und Eindeutigkeit
|
||||||
|
|
||||||
|
### 📱 Benutzerfreundlichkeit
|
||||||
|
- **Responsive Design**: Funktioniert auf Desktop, Tablet und Smartphone
|
||||||
|
- **Moderne UI**: Bootstrap 5 mit Custom CSS
|
||||||
|
- **Intuitive Bedienung**: Selbsterklärende Benutzeroberfläche
|
||||||
|
- **Echtzeit-Feedback**: Progress Bars und Notifications
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
- Python 3.8 oder höher
|
||||||
|
- pip (Python Package Manager)
|
||||||
|
|
||||||
|
### Schritt-für-Schritt Installation
|
||||||
|
|
||||||
|
1. **Repository klonen oder herunterladen**
|
||||||
|
```bash
|
||||||
|
# Falls Sie Git verwenden
|
||||||
|
git clone <repository-url>
|
||||||
|
cd pdf-web-app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Virtuelle Umgebung erstellen (empfohlen)**
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
venv\Scripts\activate
|
||||||
|
|
||||||
|
# macOS/Linux
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Abhängigkeiten installieren**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Poppler installieren (für pdf2image)**
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
- Laden Sie Poppler von [hier](https://github.com/oschwartz10612/poppler-windows/releases) herunter
|
||||||
|
- Extrahieren Sie es nach `C:\poppler`
|
||||||
|
- Fügen Sie `C:\poppler\Library\bin` zu Ihren Umgebungsvariablen hinzu
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
brew install poppler
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ubuntu/Debian:**
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install poppler-utils
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Anwendung starten**
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Browser öffnen**
|
||||||
|
- Öffnen Sie Ihren Browser und gehen Sie zu: `http://127.0.0.1:5000`
|
||||||
|
|
||||||
|
## 📁 Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
pdf-web-app/
|
||||||
|
├── app.py # Haupt-Flask-Anwendung
|
||||||
|
├── requirements.txt # Python-Abhängigkeiten
|
||||||
|
├── README.md # Diese Datei
|
||||||
|
├── templates/ # HTML-Templates
|
||||||
|
│ ├── base.html # Basis-Template
|
||||||
|
│ ├── index.html # Startseite
|
||||||
|
│ ├── images_to_pdf.html # Bilder zu PDF Seite
|
||||||
|
│ └── pdf_tools.html # PDF Tools Seite
|
||||||
|
├── static/ # Statische Dateien
|
||||||
|
│ ├── css/
|
||||||
|
│ │ └── style.css # Custom CSS
|
||||||
|
│ └── js/
|
||||||
|
│ ├── main.js # Haupt-JavaScript
|
||||||
|
│ ├── images-to-pdf.js # Bilder zu PDF Funktionalität
|
||||||
|
│ └── pdf-tools.js # PDF Tools Funktionalität
|
||||||
|
├── uploads/ # Temporäre Upload-Dateien
|
||||||
|
└── output/ # Generierte Ausgabedateien
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Konfiguration
|
||||||
|
|
||||||
|
### Anpassbare Einstellungen in `app.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Maximale Dateigröße (Standard: 16MB)
|
||||||
|
MAX_FILE_SIZE = 16 * 1024 * 1024
|
||||||
|
|
||||||
|
# Unterstützte Bildformate
|
||||||
|
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff'}
|
||||||
|
|
||||||
|
# Server-Konfiguration
|
||||||
|
app.run(debug=True, host='127.0.0.1', port=5000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Verwendung
|
||||||
|
|
||||||
|
### Bilder zu PDF konvertieren
|
||||||
|
|
||||||
|
1. Gehen Sie zur "Bilder zu PDF" Seite
|
||||||
|
2. Ziehen Sie Bilder in den Upload-Bereich oder klicken Sie zum Auswählen
|
||||||
|
3. Sortieren Sie die Bilder durch Ziehen in die gewünschte Reihenfolge
|
||||||
|
4. Klicken Sie auf "Zu PDF konvertieren"
|
||||||
|
5. Laden Sie die fertige PDF-Datei herunter
|
||||||
|
|
||||||
|
### PDFs zusammenführen
|
||||||
|
|
||||||
|
1. Gehen Sie zur "PDF Tools" Seite
|
||||||
|
2. Wählen Sie den "PDFs zusammenführen" Tab
|
||||||
|
3. Laden Sie mindestens 2 PDF-Dateien hoch
|
||||||
|
4. Sortieren Sie die Reihenfolge durch Ziehen
|
||||||
|
5. Klicken Sie auf "PDFs zusammenführen"
|
||||||
|
6. Laden Sie die zusammengeführte PDF herunter
|
||||||
|
|
||||||
|
### PDF zu Bildern konvertieren
|
||||||
|
|
||||||
|
1. Gehen Sie zur "PDF Tools" Seite
|
||||||
|
2. Wählen Sie den "PDF zu Bildern" Tab
|
||||||
|
3. Laden Sie eine PDF-Datei hoch
|
||||||
|
4. Wählen Sie die gewünschten Export-Einstellungen
|
||||||
|
5. Klicken Sie auf "Zu Bildern konvertieren"
|
||||||
|
6. Laden Sie die ZIP-Datei mit allen Bildern herunter
|
||||||
|
|
||||||
|
## 🛠️ Entwicklung
|
||||||
|
|
||||||
|
### Lokale Entwicklung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debug-Modus aktivieren
|
||||||
|
export FLASK_DEBUG=1 # Linux/macOS
|
||||||
|
set FLASK_DEBUG=1 # Windows
|
||||||
|
|
||||||
|
# Anwendung starten
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neue Features hinzufügen
|
||||||
|
|
||||||
|
1. Backend-Logik in `app.py` hinzufügen
|
||||||
|
2. API-Endpunkte erstellen
|
||||||
|
3. Frontend-Funktionalität in entsprechende JS-Datei einfügen
|
||||||
|
4. Templates bei Bedarf erweitern
|
||||||
|
|
||||||
|
## 🐛 Fehlerbehebung
|
||||||
|
|
||||||
|
### Häufige Probleme
|
||||||
|
|
||||||
|
**Problem**: `ImportError: No module named 'pdf2image'`
|
||||||
|
**Lösung**: Stellen Sie sicher, dass Poppler installiert ist (siehe Installation)
|
||||||
|
|
||||||
|
**Problem**: `Permission denied` beim Dateizugriff
|
||||||
|
**Lösung**: Stellen Sie sicher, dass die Upload- und Output-Ordner beschreibbar sind
|
||||||
|
|
||||||
|
**Problem**: Große Dateien werden nicht hochgeladen
|
||||||
|
**Lösung**: Erhöhen Sie `MAX_CONTENT_LENGTH` in der Flask-Konfiguration
|
||||||
|
|
||||||
|
**Problem**: PDF-Konvertierung schlägt fehl
|
||||||
|
**Lösung**: Überprüfen Sie, ob alle Abhängigkeiten korrekt installiert sind
|
||||||
|
|
||||||
|
### Debug-Informationen aktivieren
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In app.py
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Lizenz
|
||||||
|
|
||||||
|
Dieses Projekt steht unter der MIT-Lizenz. Sie können es frei verwenden, modifizieren und verteilen.
|
||||||
|
|
||||||
|
## 🤝 Beitragen
|
||||||
|
|
||||||
|
Beiträge sind willkommen! Bitte:
|
||||||
|
|
||||||
|
1. Forken Sie das Repository
|
||||||
|
2. Erstellen Sie einen Feature-Branch
|
||||||
|
3. Committen Sie Ihre Änderungen
|
||||||
|
4. Pushen Sie zum Branch
|
||||||
|
5. Erstellen Sie einen Pull Request
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
Bei Fragen oder Problemen:
|
||||||
|
|
||||||
|
1. Überprüfen Sie die Fehlerbehebung oben
|
||||||
|
2. Erstellen Sie ein Issue im Repository
|
||||||
|
3. Überprüfen Sie die Console-Logs im Browser (F12)
|
||||||
|
|
||||||
|
## 🔮 Geplante Features
|
||||||
|
|
||||||
|
- [ ] PDF-Passwort-Schutz
|
||||||
|
- [ ] PDF-Metadaten bearbeiten
|
||||||
|
- [ ] Wasserzeichen hinzufügen
|
||||||
|
- [ ] OCR-Texterkennung
|
||||||
|
- [ ] PDF-Komprimierung
|
||||||
|
- [ ] Batch-Verarbeitung
|
||||||
|
- [ ] Dark Mode
|
||||||
|
- [ ] Mehrsprachige Unterstützung
|
||||||
|
|
||||||
|
## 📊 Technologie-Stack
|
||||||
|
|
||||||
|
- **Backend**: Python Flask
|
||||||
|
- **Frontend**: HTML5, CSS3, JavaScript (ES6+)
|
||||||
|
- **UI Framework**: Bootstrap 5
|
||||||
|
- **PDF Processing**: PyPDF2, reportlab, pdf2image
|
||||||
|
- **Image Processing**: Pillow (PIL)
|
||||||
|
- **Icons**: Font Awesome
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Erstellt mit ❤️ für lokale PDF-Bearbeitung**
|
||||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Flask==2.3.3
|
||||||
|
Werkzeug==2.3.7
|
||||||
|
Pillow==10.0.1
|
||||||
|
PyPDF2==3.0.1
|
||||||
|
reportlab==4.0.4
|
||||||
|
pdf2image==1.16.3
|
||||||
|
Jinja2==3.1.2
|
||||||
|
MarkupSafe==2.1.3
|
||||||
|
click==8.1.7
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
blinker==1.6.3
|
||||||
414
static/css/style.css
Normal file
414
static/css/style.css
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
/* PDF Editor Web App - Custom Styles */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #007bff;
|
||||||
|
--secondary-color: #6c757d;
|
||||||
|
--success-color: #28a745;
|
||||||
|
--warning-color: #ffc107;
|
||||||
|
--danger-color: #dc3545;
|
||||||
|
--info-color: #17a2b8;
|
||||||
|
--light-color: #f8f9fa;
|
||||||
|
--dark-color: #343a40;
|
||||||
|
--border-radius: 0.5rem;
|
||||||
|
--box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
--box-shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* General Styles */
|
||||||
|
body {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link {
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.jumbotron {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--box-shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card .card-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card .icon-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: rgba(0, 123, 255, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-icon {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upload Areas */
|
||||||
|
.upload-area {
|
||||||
|
border: 2px dashed #dee2e6;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: #fafbfc;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background-color: rgba(0, 123, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area.dragover {
|
||||||
|
border-color: var(--success-color);
|
||||||
|
background-color: rgba(40, 167, 69, 0.1);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File List */
|
||||||
|
.list-group-item {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-list .list-group-item {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-list .list-group-item:hover {
|
||||||
|
background-color: rgba(0, 123, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(0, 123, 255, 0.1);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-details {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bars */
|
||||||
|
.progress {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: linear-gradient(90deg, var(--primary-color), #0056b3);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, var(--success-color), #1e7e34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: linear-gradient(135deg, var(--warning-color), #e0a800);
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: linear-gradient(135deg, var(--danger-color), #bd2130);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary {
|
||||||
|
border: 2px solid var(--secondary-color);
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary:hover {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #155724;
|
||||||
|
border-left: 4px solid var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #721c24;
|
||||||
|
border-left: 4px solid var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: rgba(23, 162, 184, 0.1);
|
||||||
|
color: #0c5460;
|
||||||
|
border-left: 4px solid var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.nav-pills .nav-link {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-pills .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Headers */
|
||||||
|
.page-header {
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
color: var(--dark-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-control, .form-select {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
|
.spinner-border {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
background-color: white !important;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.jumbotron {
|
||||||
|
padding: 2rem 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jumbotron .display-4 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card .card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--light-color: #343a40;
|
||||||
|
--dark-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #1a1d20;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #2d3338;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
background-color: #343a40;
|
||||||
|
border-color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
background-color: #2d3338;
|
||||||
|
border-color: #495057;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation Classes */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { opacity: 0; transform: translateX(-20px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-in {
|
||||||
|
animation: slideIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.border-dashed {
|
||||||
|
border-style: dashed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-hover {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-hover:hover {
|
||||||
|
box-shadow: var(--box-shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Type Icons */
|
||||||
|
.file-type-pdf { color: #dc3545; }
|
||||||
|
.file-type-image { color: #28a745; }
|
||||||
|
.file-type-zip { color: #ffc107; }
|
||||||
270
static/js/images-to-pdf.js
Normal file
270
static/js/images-to-pdf.js
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
// Images to PDF - JavaScript Functionality
|
||||||
|
|
||||||
|
let imagesToPdfFiles = [];
|
||||||
|
let imagesToPdfSortable = null;
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
let uploadArea, fileInput, fileList, filesContainer, convertBtn, clearBtn;
|
||||||
|
let progressBar, progressText, uploadProgress, resultArea, errorArea;
|
||||||
|
let downloadLink;
|
||||||
|
|
||||||
|
// Initialize when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Get DOM elements
|
||||||
|
uploadArea = document.getElementById('upload-area');
|
||||||
|
fileInput = document.getElementById('file-input');
|
||||||
|
fileList = document.getElementById('file-list');
|
||||||
|
filesContainer = document.getElementById('files-container');
|
||||||
|
convertBtn = document.getElementById('convert-btn');
|
||||||
|
clearBtn = document.getElementById('clear-btn');
|
||||||
|
progressBar = document.getElementById('progress-bar');
|
||||||
|
progressText = document.getElementById('progress-text');
|
||||||
|
uploadProgress = document.getElementById('upload-progress');
|
||||||
|
resultArea = document.getElementById('result-area');
|
||||||
|
errorArea = document.getElementById('error-area');
|
||||||
|
downloadLink = document.getElementById('download-link');
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
setupEventListeners();
|
||||||
|
|
||||||
|
// Setup drag and drop
|
||||||
|
setupDragAndDrop(uploadArea, handleFiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupEventListeners() {
|
||||||
|
// File input change
|
||||||
|
fileInput.addEventListener('change', function(e) {
|
||||||
|
handleFiles(Array.from(e.target.files));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert button
|
||||||
|
convertBtn.addEventListener('click', convertToPdf);
|
||||||
|
|
||||||
|
// Clear button
|
||||||
|
clearBtn.addEventListener('click', clearFiles);
|
||||||
|
|
||||||
|
// Upload area click
|
||||||
|
uploadArea.addEventListener('click', function() {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFiles(files) {
|
||||||
|
// Hide previous results/errors
|
||||||
|
hideResults();
|
||||||
|
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
showError('Keine Dateien ausgewählt.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter valid image files
|
||||||
|
const validFiles = files.filter(file => {
|
||||||
|
const isValid = file.type.startsWith('image/');
|
||||||
|
if (!isValid) {
|
||||||
|
showNotification(`${file.name} ist keine gültige Bilddatei.`, 'warning');
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validFiles.length === 0) {
|
||||||
|
showError('Keine gültigen Bilddateien gefunden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show upload progress
|
||||||
|
showUploadProgress();
|
||||||
|
|
||||||
|
// Upload files
|
||||||
|
uploadImages(validFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImages(files) {
|
||||||
|
try {
|
||||||
|
const response = await uploadFiles(files, '/api/upload-images', updateProgress);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
imagesToPdfFiles = response.files;
|
||||||
|
displayFiles();
|
||||||
|
showNotification(response.message, 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Upload fehlgeschlagen');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Upload-Fehler: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
hideUploadProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(percent) {
|
||||||
|
progressBar.style.width = percent + '%';
|
||||||
|
progressText.textContent = Math.round(percent) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayFiles() {
|
||||||
|
filesContainer.innerHTML = '';
|
||||||
|
|
||||||
|
imagesToPdfFiles.forEach((file, index) => {
|
||||||
|
const fileItem = createFileItem(file, index);
|
||||||
|
filesContainer.appendChild(fileItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show file list
|
||||||
|
fileList.style.display = 'block';
|
||||||
|
|
||||||
|
// Enable convert button
|
||||||
|
convertBtn.disabled = false;
|
||||||
|
|
||||||
|
// Setup sortable
|
||||||
|
setupSortableList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFileItem(file, index) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'list-group-item file-item';
|
||||||
|
div.dataset.index = index;
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="file-icon">
|
||||||
|
<i class="${getFileIcon(file.original_name)}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name">${file.original_name}</div>
|
||||||
|
<div class="file-details">${formatFileSize(file.size)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-actions">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="moveFileUp(${index})" ${index === 0 ? 'disabled' : ''}>
|
||||||
|
<i class="fas fa-arrow-up"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="moveFileDown(${index})" ${index === imagesToPdfFiles.length - 1 ? 'disabled' : ''}>
|
||||||
|
<i class="fas fa-arrow-down"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeFile(${index})">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSortableList() {
|
||||||
|
if (imagesToPdfSortable) {
|
||||||
|
imagesToPdfSortable.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesToPdfSortable = setupSortable(filesContainer, function(evt) {
|
||||||
|
// Update array order
|
||||||
|
const item = imagesToPdfFiles.splice(evt.oldIndex, 1)[0];
|
||||||
|
imagesToPdfFiles.splice(evt.newIndex, 0, item);
|
||||||
|
|
||||||
|
// Refresh display
|
||||||
|
displayFiles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveFileUp(index) {
|
||||||
|
if (index > 0) {
|
||||||
|
[imagesToPdfFiles[index], imagesToPdfFiles[index - 1]] = [imagesToPdfFiles[index - 1], imagesToPdfFiles[index]];
|
||||||
|
displayFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveFileDown(index) {
|
||||||
|
if (index < imagesToPdfFiles.length - 1) {
|
||||||
|
[imagesToPdfFiles[index], imagesToPdfFiles[index + 1]] = [imagesToPdfFiles[index + 1], imagesToPdfFiles[index]];
|
||||||
|
displayFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFile(index) {
|
||||||
|
imagesToPdfFiles.splice(index, 1);
|
||||||
|
|
||||||
|
if (imagesToPdfFiles.length === 0) {
|
||||||
|
clearFiles();
|
||||||
|
} else {
|
||||||
|
displayFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('Datei entfernt.', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertToPdf() {
|
||||||
|
if (imagesToPdfFiles.length === 0) {
|
||||||
|
showError('Keine Dateien zum Konvertieren vorhanden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideResults();
|
||||||
|
setLoadingState(convertBtn, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filenames = imagesToPdfFiles.map(file => file.filename);
|
||||||
|
|
||||||
|
const response = await makeRequest('/api/convert-images-to-pdf', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ filenames })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
showResult(response.filename, response.message);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Konvertierung fehlgeschlagen');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Konvertierungs-Fehler: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoadingState(convertBtn, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFiles() {
|
||||||
|
imagesToPdfFiles = [];
|
||||||
|
filesContainer.innerHTML = '';
|
||||||
|
fileList.style.display = 'none';
|
||||||
|
convertBtn.disabled = true;
|
||||||
|
fileInput.value = '';
|
||||||
|
hideResults();
|
||||||
|
|
||||||
|
if (imagesToPdfSortable) {
|
||||||
|
imagesToPdfSortable.destroy();
|
||||||
|
imagesToPdfSortable = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUploadProgress() {
|
||||||
|
uploadProgress.style.display = 'block';
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressText.textContent = '0%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideUploadProgress() {
|
||||||
|
uploadProgress.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResult(filename, message) {
|
||||||
|
resultArea.style.display = 'block';
|
||||||
|
downloadLink.href = `/download/${filename}`;
|
||||||
|
downloadLink.querySelector('.me-2').nextSibling.textContent = 'PDF herunterladen';
|
||||||
|
|
||||||
|
// Update message if provided
|
||||||
|
if (message) {
|
||||||
|
resultArea.querySelector('p').textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
errorArea.style.display = 'block';
|
||||||
|
document.getElementById('error-message').textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideResults() {
|
||||||
|
resultArea.style.display = 'none';
|
||||||
|
errorArea.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global functions for button actions
|
||||||
|
window.moveFileUp = moveFileUp;
|
||||||
|
window.moveFileDown = moveFileDown;
|
||||||
|
window.removeFile = removeFile;
|
||||||
190
static/js/main.js
Normal file
190
static/js/main.js
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
// PDF Editor - Main JavaScript Functions
|
||||||
|
|
||||||
|
// Global variables
|
||||||
|
let uploadedFiles = [];
|
||||||
|
let sortableInstance = null;
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileIcon(filename) {
|
||||||
|
const extension = filename.toLowerCase().split('.').pop();
|
||||||
|
const iconMap = {
|
||||||
|
'pdf': 'fas fa-file-pdf text-danger',
|
||||||
|
'jpg': 'fas fa-file-image text-success',
|
||||||
|
'jpeg': 'fas fa-file-image text-success',
|
||||||
|
'png': 'fas fa-file-image text-success',
|
||||||
|
'gif': 'fas fa-file-image text-success',
|
||||||
|
'bmp': 'fas fa-file-image text-success',
|
||||||
|
'tiff': 'fas fa-file-image text-success',
|
||||||
|
'zip': 'fas fa-file-archive text-warning'
|
||||||
|
};
|
||||||
|
return iconMap[extension] || 'fas fa-file text-muted';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
// Create notification element
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||||
|
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 400px;';
|
||||||
|
notification.innerHTML = `
|
||||||
|
${message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Auto remove after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.remove();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide loading state
|
||||||
|
function setLoadingState(element, loading = true) {
|
||||||
|
if (loading) {
|
||||||
|
element.disabled = true;
|
||||||
|
const originalText = element.innerHTML;
|
||||||
|
element.dataset.originalText = originalText;
|
||||||
|
element.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Verarbeitung...';
|
||||||
|
} else {
|
||||||
|
element.disabled = false;
|
||||||
|
element.innerHTML = element.dataset.originalText || element.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AJAX helper function
|
||||||
|
async function makeRequest(url, options = {}) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Request failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File upload with progress
|
||||||
|
async function uploadFiles(files, endpoint, progressCallback) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
if (Array.isArray(files)) {
|
||||||
|
files.forEach(file => formData.append('files', file));
|
||||||
|
} else {
|
||||||
|
formData.append('file', files);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
// Upload progress
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable && progressCallback) {
|
||||||
|
const percentComplete = (e.loaded / e.total) * 100;
|
||||||
|
progressCallback(percentComplete);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
resolve(response);
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Invalid JSON response'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Upload failed with status: ${xhr.status}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
reject(new Error('Upload failed'));
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open('POST', endpoint);
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag and drop functionality
|
||||||
|
function setupDragAndDrop(element, callback) {
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||||
|
element.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function preventDefaults(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
['dragenter', 'dragover'].forEach(eventName => {
|
||||||
|
element.addEventListener(eventName, () => {
|
||||||
|
element.classList.add('dragover');
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragleave', 'drop'].forEach(eventName => {
|
||||||
|
element.addEventListener(eventName, () => {
|
||||||
|
element.classList.remove('dragover');
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('drop', (e) => {
|
||||||
|
const files = Array.from(e.dataTransfer.files);
|
||||||
|
callback(files);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortable list functionality
|
||||||
|
function setupSortable(container, onUpdate) {
|
||||||
|
if (typeof Sortable !== 'undefined') {
|
||||||
|
return Sortable.create(container, {
|
||||||
|
animation: 150,
|
||||||
|
ghostClass: 'sortable-ghost',
|
||||||
|
chosenClass: 'sortable-chosen',
|
||||||
|
dragClass: 'sortable-drag',
|
||||||
|
onUpdate: onUpdate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize tooltips
|
||||||
|
function initializeTooltips() {
|
||||||
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializeTooltips();
|
||||||
|
|
||||||
|
// Add fade-in animation to main content
|
||||||
|
const mainContent = document.querySelector('main');
|
||||||
|
if (mainContent) {
|
||||||
|
mainContent.classList.add('fade-in');
|
||||||
|
}
|
||||||
|
});
|
||||||
342
static/js/pdf-tools.js
Normal file
342
static/js/pdf-tools.js
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
// PDF Tools - JavaScript Functionality
|
||||||
|
|
||||||
|
let pdfToolsFiles = [];
|
||||||
|
let mergeSortable = null;
|
||||||
|
let currentPdfFile = null;
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
let mergeUploadArea, mergeFileInput, mergeFileList, mergeFilesContainer, mergePdfsBtn;
|
||||||
|
let splitUploadArea, splitFileInput, splitFileInfo, convertToImagesBtn;
|
||||||
|
let mergeResult, splitResult, mergeDownloadLink, splitDownloadLink;
|
||||||
|
let processingModal, errorArea, errorMessage;
|
||||||
|
|
||||||
|
// Initialize when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializeDOMElements();
|
||||||
|
setupEventListeners();
|
||||||
|
setupDragAndDrop();
|
||||||
|
});
|
||||||
|
|
||||||
|
function initializeDOMElements() {
|
||||||
|
// Merge elements
|
||||||
|
mergeUploadArea = document.getElementById('merge-upload-area');
|
||||||
|
mergeFileInput = document.getElementById('merge-file-input');
|
||||||
|
mergeFileList = document.getElementById('merge-file-list');
|
||||||
|
mergeFilesContainer = document.getElementById('merge-files-container');
|
||||||
|
mergePdfsBtn = document.getElementById('merge-pdfs-btn');
|
||||||
|
mergeResult = document.getElementById('merge-result');
|
||||||
|
mergeDownloadLink = document.getElementById('merge-download-link');
|
||||||
|
|
||||||
|
// Split elements
|
||||||
|
splitUploadArea = document.getElementById('split-upload-area');
|
||||||
|
splitFileInput = document.getElementById('split-file-input');
|
||||||
|
splitFileInfo = document.getElementById('split-file-info');
|
||||||
|
convertToImagesBtn = document.getElementById('convert-to-images-btn');
|
||||||
|
splitResult = document.getElementById('split-result');
|
||||||
|
splitDownloadLink = document.getElementById('split-download-link');
|
||||||
|
|
||||||
|
// Common elements
|
||||||
|
processingModal = new bootstrap.Modal(document.getElementById('processing-modal'));
|
||||||
|
errorArea = document.getElementById('pdf-tools-error');
|
||||||
|
errorMessage = document.getElementById('pdf-tools-error-message');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupEventListeners() {
|
||||||
|
// Merge PDF events
|
||||||
|
mergeFileInput.addEventListener('change', function(e) {
|
||||||
|
handleMergeFiles(Array.from(e.target.files));
|
||||||
|
});
|
||||||
|
|
||||||
|
mergeUploadArea.addEventListener('click', function() {
|
||||||
|
mergeFileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
mergePdfsBtn.addEventListener('click', mergePdfs);
|
||||||
|
|
||||||
|
// Split PDF events
|
||||||
|
splitFileInput.addEventListener('change', function(e) {
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
handleSplitFile(e.target.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
splitUploadArea.addEventListener('click', function() {
|
||||||
|
splitFileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
convertToImagesBtn.addEventListener('click', convertPdfToImages);
|
||||||
|
|
||||||
|
// Tab change events
|
||||||
|
document.querySelectorAll('[data-bs-toggle="pill"]').forEach(tab => {
|
||||||
|
tab.addEventListener('shown.bs.tab', function(e) {
|
||||||
|
hideAllResults();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupDragAndDrop() {
|
||||||
|
// Merge area drag and drop
|
||||||
|
setupDragAndDrop(mergeUploadArea, handleMergeFiles);
|
||||||
|
|
||||||
|
// Split area drag and drop
|
||||||
|
setupDragAndDrop(splitUploadArea, function(files) {
|
||||||
|
if (files.length > 0) {
|
||||||
|
handleSplitFile(files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge PDF functions
|
||||||
|
async function handleMergeFiles(files) {
|
||||||
|
hideAllResults();
|
||||||
|
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
showError('Keine Dateien ausgewählt.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter PDF files
|
||||||
|
const pdfFiles = files.filter(file => file.type === 'application/pdf');
|
||||||
|
|
||||||
|
if (pdfFiles.length === 0) {
|
||||||
|
showError('Keine PDF-Dateien gefunden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfFiles.length < 2) {
|
||||||
|
showError('Mindestens 2 PDF-Dateien erforderlich.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload files one by one
|
||||||
|
pdfToolsFiles = [];
|
||||||
|
|
||||||
|
for (const file of pdfFiles) {
|
||||||
|
try {
|
||||||
|
const response = await uploadSinglePdf(file);
|
||||||
|
if (response.success) {
|
||||||
|
pdfToolsFiles.push(response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Fehler beim Upload von ${file.name}: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayMergeFiles();
|
||||||
|
showNotification(`${pdfToolsFiles.length} PDF-Dateien erfolgreich hochgeladen.`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadSinglePdf(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch('/api/upload-pdf', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayMergeFiles() {
|
||||||
|
mergeFilesContainer.innerHTML = '';
|
||||||
|
|
||||||
|
pdfToolsFiles.forEach((file, index) => {
|
||||||
|
const fileItem = createMergeFileItem(file, index);
|
||||||
|
mergeFilesContainer.appendChild(fileItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
mergeFileList.style.display = 'block';
|
||||||
|
mergePdfsBtn.disabled = pdfToolsFiles.length < 2;
|
||||||
|
|
||||||
|
// Setup sortable
|
||||||
|
setupMergeSortable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMergeFileItem(file, index) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'list-group-item';
|
||||||
|
div.dataset.index = index;
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="file-icon me-3">
|
||||||
|
<i class="fas fa-file-pdf text-danger fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="file-name fw-bold">${file.original_name}</div>
|
||||||
|
<div class="file-details text-muted">
|
||||||
|
${file.page_count} Seiten • ${formatFileSize(file.size)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-actions">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeMergeFile(${index})">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMergeSortable() {
|
||||||
|
if (mergeSortable) {
|
||||||
|
mergeSortable.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeSortable = setupSortable(mergeFilesContainer, function(evt) {
|
||||||
|
const item = pdfToolsFiles.splice(evt.oldIndex, 1)[0];
|
||||||
|
pdfToolsFiles.splice(evt.newIndex, 0, item);
|
||||||
|
displayMergeFiles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMergeFile(index) {
|
||||||
|
pdfToolsFiles.splice(index, 1);
|
||||||
|
|
||||||
|
if (pdfToolsFiles.length === 0) {
|
||||||
|
mergeFileList.style.display = 'none';
|
||||||
|
mergePdfsBtn.disabled = true;
|
||||||
|
} else {
|
||||||
|
displayMergeFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('PDF-Datei entfernt.', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mergePdfs() {
|
||||||
|
if (pdfToolsFiles.length < 2) {
|
||||||
|
showError('Mindestens 2 PDF-Dateien erforderlich.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllResults();
|
||||||
|
processingModal.show();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filenames = pdfToolsFiles.map(file => file.filename);
|
||||||
|
|
||||||
|
const response = await makeRequest('/api/merge-pdfs', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ filenames })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
showMergeResult(response.filename, response.message);
|
||||||
|
showNotification(response.message, 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Zusammenführung fehlgeschlagen');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Fehler beim Zusammenführen: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
processingModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMergeResult(filename, message) {
|
||||||
|
mergeResult.style.display = 'block';
|
||||||
|
mergeDownloadLink.href = `/download/${filename}`;
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
mergeResult.querySelector('p').textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split PDF functions
|
||||||
|
async function handleSplitFile(file) {
|
||||||
|
hideAllResults();
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
showError('Keine Datei ausgewählt.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type !== 'application/pdf') {
|
||||||
|
showError('Nur PDF-Dateien sind erlaubt.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await uploadSinglePdf(file);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
currentPdfFile = response;
|
||||||
|
displaySplitFile();
|
||||||
|
showNotification('PDF-Datei erfolgreich hochgeladen.', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Upload fehlgeschlagen');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Upload-Fehler: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySplitFile() {
|
||||||
|
document.getElementById('split-filename').textContent = currentPdfFile.original_name;
|
||||||
|
document.getElementById('split-file-details').textContent =
|
||||||
|
`${currentPdfFile.page_count} Seiten • ${formatFileSize(currentPdfFile.size)}`;
|
||||||
|
|
||||||
|
splitFileInfo.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertPdfToImages() {
|
||||||
|
if (!currentPdfFile) {
|
||||||
|
showError('Keine PDF-Datei ausgewählt.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllResults();
|
||||||
|
processingModal.show();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await makeRequest('/api/pdf-to-images', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ filename: currentPdfFile.filename })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
showSplitResult(response.filename, response.message);
|
||||||
|
showNotification(response.message, 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Konvertierung fehlgeschlagen');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Konvertierungs-Fehler: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
processingModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSplitResult(filename, message) {
|
||||||
|
splitResult.style.display = 'block';
|
||||||
|
splitDownloadLink.href = `/download/${filename}`;
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
splitResult.querySelector('p').textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
function hideAllResults() {
|
||||||
|
if (mergeResult) mergeResult.style.display = 'none';
|
||||||
|
if (splitResult) splitResult.style.display = 'none';
|
||||||
|
if (errorArea) errorArea.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
errorArea.style.display = 'block';
|
||||||
|
errorMessage.textContent = message;
|
||||||
|
|
||||||
|
// Scroll to error
|
||||||
|
errorArea.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global functions
|
||||||
|
window.removeMergeFile = removeMergeFile;
|
||||||
292
templates/pdf_tools.html
Normal file
292
templates/pdf_tools.html
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}PDF Tools - PDF Editor{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="page-header mb-4">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-tools me-2"></i>PDF Bearbeitungstools
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted">
|
||||||
|
Erweiterte Werkzeuge für die PDF-Bearbeitung: Zusammenführen, teilen, konvertieren.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tools Navigation -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<ul class="nav nav-pills justify-content-center" id="tools-nav" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="merge-tab" data-bs-toggle="pill"
|
||||||
|
data-bs-target="#merge-panel" type="button" role="tab">
|
||||||
|
<i class="fas fa-compress-arrows-alt me-2"></i>PDFs zusammenführen
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="split-tab" data-bs-toggle="pill"
|
||||||
|
data-bs-target="#split-panel" type="button" role="tab">
|
||||||
|
<i class="fas fa-cut me-2"></i>PDF zu Bildern
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="extract-tab" data-bs-toggle="pill"
|
||||||
|
data-bs-target="#extract-panel" type="button" role="tab">
|
||||||
|
<i class="fas fa-file-export me-2"></i>Seiten extrahieren
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="tools-content">
|
||||||
|
<!-- PDF Merge Panel -->
|
||||||
|
<div class="tab-pane fade show active" id="merge-panel" role="tabpanel" aria-labelledby="merge-tab">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-compress-arrows-alt me-2"></i>PDFs zusammenführen
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="merge-upload-area" class="upload-area border-dashed p-4 text-center mb-3">
|
||||||
|
<i class="fas fa-file-pdf fa-3x text-muted mb-3"></i>
|
||||||
|
<h5>PDF-Dateien hier hinziehen oder auswählen</h5>
|
||||||
|
<p class="text-muted mb-3">Mindestens 2 PDF-Dateien erforderlich</p>
|
||||||
|
<input type="file" id="merge-file-input" multiple accept=".pdf" class="d-none">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="document.getElementById('merge-file-input').click()">
|
||||||
|
<i class="fas fa-folder-open me-2"></i>PDF-Dateien auswählen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="merge-file-list" style="display: none;">
|
||||||
|
<h6>PDF-Dateien zum Zusammenführen:</h6>
|
||||||
|
<div id="merge-files-container" class="list-group sortable-list mb-3">
|
||||||
|
<!-- Files will be added here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="merge-pdfs-btn" class="btn btn-success" disabled>
|
||||||
|
<i class="fas fa-compress-arrows-alt me-2"></i>PDFs zusammenführen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="merge-result" style="display: none;" class="mt-3">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-check-circle fa-2x me-3"></i>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-1">PDFs erfolgreich zusammengeführt!</h6>
|
||||||
|
<p class="mb-0">Die PDF-Dateien wurden zu einer Datei zusammengeführt.</p>
|
||||||
|
</div>
|
||||||
|
<a id="merge-download-link" href="#" class="btn btn-success">
|
||||||
|
<i class="fas fa-download me-2"></i>Herunterladen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>Anleitung
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ol class="mb-0">
|
||||||
|
<li class="mb-2">Wählen Sie mindestens 2 PDF-Dateien aus</li>
|
||||||
|
<li class="mb-2">Sortieren Sie die Reihenfolge durch Ziehen</li>
|
||||||
|
<li class="mb-2">Klicken Sie auf "PDFs zusammenführen"</li>
|
||||||
|
<li>Laden Sie die zusammengeführte PDF herunter</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PDF to Images Panel -->
|
||||||
|
<div class="tab-pane fade" id="split-panel" role="tabpanel" aria-labelledby="split-tab">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-cut me-2"></i>PDF zu Bildern konvertieren
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="split-upload-area" class="upload-area border-dashed p-4 text-center mb-3">
|
||||||
|
<i class="fas fa-file-pdf fa-3x text-muted mb-3"></i>
|
||||||
|
<h5>PDF-Datei hier hinziehen oder auswählen</h5>
|
||||||
|
<p class="text-muted mb-3">Jede Seite wird als separates Bild exportiert</p>
|
||||||
|
<input type="file" id="split-file-input" accept=".pdf" class="d-none">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="document.getElementById('split-file-input').click()">
|
||||||
|
<i class="fas fa-folder-open me-2"></i>PDF-Datei auswählen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="split-file-info" style="display: none;" class="mb-3">
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-file-pdf fa-2x text-danger me-3"></i>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 id="split-filename" class="mb-1"></h6>
|
||||||
|
<small id="split-file-details" class="text-muted"></small>
|
||||||
|
</div>
|
||||||
|
<button id="convert-to-images-btn" class="btn btn-warning">
|
||||||
|
<i class="fas fa-images me-2"></i>Zu Bildern konvertieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="split-result" style="display: none;" class="mt-3">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-check-circle fa-2x me-3"></i>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-1">PDF erfolgreich konvertiert!</h6>
|
||||||
|
<p class="mb-0">Alle Seiten wurden als PNG-Bilder exportiert und in einer ZIP-Datei verpackt.</p>
|
||||||
|
</div>
|
||||||
|
<a id="split-download-link" href="#" class="btn btn-success">
|
||||||
|
<i class="fas fa-download me-2"></i>ZIP herunterladen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-cog me-2"></i>Export-Einstellungen
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Bildformat:</label>
|
||||||
|
<select id="export-format" class="form-select">
|
||||||
|
<option value="PNG">PNG (verlustfrei)</option>
|
||||||
|
<option value="JPEG">JPEG (komprimiert)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">DPI (Auflösung):</label>
|
||||||
|
<select id="export-dpi" class="form-select">
|
||||||
|
<option value="150">150 DPI (Standard)</option>
|
||||||
|
<option value="200">200 DPI (Hoch)</option>
|
||||||
|
<option value="300">300 DPI (Sehr hoch)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<small>
|
||||||
|
<i class="fas fa-info-circle me-1"></i>
|
||||||
|
Höhere DPI-Werte ergeben bessere Qualität, aber größere Dateien.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Extract Pages Panel -->
|
||||||
|
<div class="tab-pane fade" id="extract-panel" role="tabpanel" aria-labelledby="extract-tab">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-file-export me-2"></i>Seiten extrahieren
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Diese Funktion wird in einer zukünftigen Version verfügbar sein.
|
||||||
|
Verwenden Sie vorerst die "PDF zu Bildern" Funktion, um einzelne Seiten zu extrahieren.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-roadmap me-2"></i>Geplante Features
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="fas fa-clock text-muted me-2"></i>
|
||||||
|
<small>Seitenbereiche extrahieren</small>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="fas fa-clock text-muted me-2"></i>
|
||||||
|
<small>PDF-Seiten neu ordnen</small>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="fas fa-clock text-muted me-2"></i>
|
||||||
|
<small>Passwort-geschützte PDFs</small>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fas fa-clock text-muted me-2"></i>
|
||||||
|
<small>PDF-Metadaten bearbeiten</small>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Modal -->
|
||||||
|
<div class="modal fade" id="processing-modal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body text-center p-4">
|
||||||
|
<div class="spinner-border text-primary mb-3" role="status">
|
||||||
|
<span class="visually-hidden">Verarbeitung läuft...</span>
|
||||||
|
</div>
|
||||||
|
<h5>PDF wird verarbeitet...</h5>
|
||||||
|
<p class="text-muted mb-0">Bitte warten Sie, während Ihre PDF-Datei verarbeitet wird.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Area -->
|
||||||
|
<div id="pdf-tools-error" style="display: none;" class="mt-4">
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<h6><i class="fas fa-exclamation-triangle me-2"></i>Fehler</h6>
|
||||||
|
<p id="pdf-tools-error-message" class="mb-0"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/pdf-tools.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user