551 lines
20 KiB
Python
551 lines
20 KiB
Python
import os
|
|
import uuid
|
|
from flask import Flask, render_template, request, jsonify, send_file, redirect, url_for, flash
|
|
from werkzeug.utils import secure_filename
|
|
from PIL import Image
|
|
import PyPDF2
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.lib.pagesizes import letter, A4
|
|
from reportlab.lib.utils import ImageReader
|
|
import io
|
|
import zipfile
|
|
import pdf2image
|
|
import tempfile
|
|
import shutil
|
|
from datetime import datetime
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = 'your-secret-key-change-this'
|
|
|
|
# Konfiguration
|
|
UPLOAD_FOLDER = 'uploads'
|
|
OUTPUT_FOLDER = 'output'
|
|
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff'}
|
|
ALLOWED_PDF_EXTENSIONS = {'pdf'}
|
|
MAX_FILE_SIZE = 16 * 1024 * 1024 # 16MB
|
|
|
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
|
app.config['OUTPUT_FOLDER'] = OUTPUT_FOLDER
|
|
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE
|
|
|
|
def allowed_file(filename, extensions):
|
|
"""Überprüft ob die Datei erlaubt ist"""
|
|
return '.' in filename and \
|
|
filename.rsplit('.', 1)[1].lower() in extensions
|
|
|
|
def create_unique_filename(filename):
|
|
"""Erstellt einen eindeutigen Dateinamen"""
|
|
name, ext = os.path.splitext(filename)
|
|
unique_id = str(uuid.uuid4())[:8]
|
|
return f"{name}_{unique_id}{ext}"
|
|
|
|
def cleanup_old_files():
|
|
"""Löscht alte Dateien aus Upload und Output Ordnern"""
|
|
current_time = datetime.now()
|
|
|
|
for folder in [UPLOAD_FOLDER, OUTPUT_FOLDER]:
|
|
if os.path.exists(folder):
|
|
for filename in os.listdir(folder):
|
|
file_path = os.path.join(folder, filename)
|
|
if os.path.isfile(file_path):
|
|
file_time = datetime.fromtimestamp(os.path.getctime(file_path))
|
|
# Lösche Dateien die älter als 1 Stunde sind
|
|
if (current_time - file_time).seconds > 3600:
|
|
os.remove(file_path)
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Hauptseite mit Navigation"""
|
|
cleanup_old_files()
|
|
return render_template('index.html')
|
|
|
|
@app.route('/images-to-pdf')
|
|
def images_to_pdf_page():
|
|
"""Seite für Bilder zu PDF Konvertierung"""
|
|
return render_template('images_to_pdf.html')
|
|
|
|
@app.route('/pdf-tools')
|
|
def pdf_tools_page():
|
|
"""Seite für PDF Tools"""
|
|
return render_template('pdf_tools.html')
|
|
|
|
@app.route('/api/upload-images', methods=['POST'])
|
|
def upload_images():
|
|
"""API Endpunkt für Bilder Upload"""
|
|
try:
|
|
if 'files' not in request.files:
|
|
return jsonify({'error': 'Keine Dateien ausgewählt'}), 400
|
|
|
|
files = request.files.getlist('files')
|
|
|
|
if not files or files[0].filename == '':
|
|
return jsonify({'error': 'Keine Dateien ausgewählt'}), 400
|
|
|
|
uploaded_files = []
|
|
|
|
for file in files:
|
|
if file and allowed_file(file.filename, ALLOWED_IMAGE_EXTENSIONS):
|
|
filename = secure_filename(file.filename)
|
|
unique_filename = create_unique_filename(filename)
|
|
file_path = os.path.join(UPLOAD_FOLDER, unique_filename)
|
|
file.save(file_path)
|
|
uploaded_files.append({
|
|
'filename': unique_filename,
|
|
'original_name': filename,
|
|
'size': os.path.getsize(file_path)
|
|
})
|
|
|
|
if not uploaded_files:
|
|
return jsonify({'error': 'Keine gültigen Bilddateien gefunden'}), 400
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'files': uploaded_files,
|
|
'message': f'{len(uploaded_files)} Dateien erfolgreich hochgeladen'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': f'Fehler beim Upload: {str(e)}'}), 500
|
|
|
|
@app.route('/api/convert-images-to-pdf', methods=['POST'])
|
|
def convert_images_to_pdf():
|
|
"""Konvertiert hochgeladene Bilder zu PDF"""
|
|
try:
|
|
data = request.get_json()
|
|
files_data = data.get('files', [])
|
|
|
|
if not files_data:
|
|
return jsonify({'error': 'Keine Dateien ausgewählt'}), 400
|
|
|
|
# PDF erstellen
|
|
output_filename = f"converted_images_{str(uuid.uuid4())[:8]}.pdf"
|
|
output_path = os.path.join(OUTPUT_FOLDER, output_filename)
|
|
|
|
# Reportlab für PDF Erstellung
|
|
c = canvas.Canvas(output_path, pagesize=A4)
|
|
page_width, page_height = A4
|
|
|
|
for file_data in files_data:
|
|
filename = file_data.get('filename')
|
|
rotation = file_data.get('rotation', 0)
|
|
|
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
|
|
|
if os.path.exists(file_path):
|
|
# Bild öffnen und rotieren
|
|
with Image.open(file_path) as img:
|
|
# Bild rotieren falls nötig
|
|
if rotation != 0:
|
|
img = img.rotate(-rotation, expand=True) # Negative Rotation für korrekte Richtung
|
|
|
|
img_width, img_height = img.size
|
|
|
|
# Seitenverhältnis berechnen
|
|
aspect_ratio = img_width / img_height
|
|
|
|
# Maximale Größe auf der Seite (mit Rändern)
|
|
max_width = page_width - 40
|
|
max_height = page_height - 40
|
|
|
|
if aspect_ratio > 1: # Querformat
|
|
new_width = min(max_width, img_width)
|
|
new_height = new_width / aspect_ratio
|
|
else: # Hochformat
|
|
new_height = min(max_height, img_height)
|
|
new_width = new_height * aspect_ratio
|
|
|
|
# Zentrieren
|
|
x = (page_width - new_width) / 2
|
|
y = (page_height - new_height) / 2
|
|
|
|
# Temporäres rotiertes Bild speichern
|
|
if rotation != 0:
|
|
temp_path = os.path.join(UPLOAD_FOLDER, f"temp_rotated_{filename}")
|
|
img.save(temp_path)
|
|
c.drawImage(temp_path, x, y, width=new_width, height=new_height)
|
|
# Temporäre Datei löschen
|
|
os.remove(temp_path)
|
|
else:
|
|
c.drawImage(file_path, x, y, width=new_width, height=new_height)
|
|
|
|
c.showPage()
|
|
|
|
c.save()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'filename': output_filename,
|
|
'message': 'PDF erfolgreich erstellt'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': f'Fehler bei der Konvertierung: {str(e)}'}), 500
|
|
|
|
@app.route('/api/upload-pdf', methods=['POST'])
|
|
def upload_pdf():
|
|
"""API Endpunkt für PDF Upload"""
|
|
try:
|
|
if 'file' not in request.files:
|
|
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
|
|
|
file = request.files['file']
|
|
|
|
if file.filename == '':
|
|
return jsonify({'error': 'Keine Datei ausgewählt'}), 400
|
|
|
|
if file and allowed_file(file.filename, ALLOWED_PDF_EXTENSIONS):
|
|
filename = secure_filename(file.filename)
|
|
unique_filename = create_unique_filename(filename)
|
|
file_path = os.path.join(UPLOAD_FOLDER, unique_filename)
|
|
file.save(file_path)
|
|
|
|
# PDF Info lesen
|
|
with open(file_path, 'rb') as pdf_file:
|
|
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
|
page_count = len(pdf_reader.pages)
|
|
|
|
# PDF Vorschau erstellen
|
|
print(f"Erstelle Vorschau für: {unique_filename}")
|
|
preview_filename = create_pdf_preview(unique_filename)
|
|
|
|
if preview_filename:
|
|
print(f"Vorschau erfolgreich erstellt: {preview_filename}")
|
|
else:
|
|
print("Vorschau-Erstellung fehlgeschlagen, verwende Fallback")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'filename': unique_filename,
|
|
'original_name': filename,
|
|
'page_count': page_count,
|
|
'size': os.path.getsize(file_path),
|
|
'preview': preview_filename,
|
|
'rotation': 0, # Standardrotation
|
|
'has_preview': preview_filename is not None
|
|
})
|
|
|
|
return jsonify({'error': 'Nur PDF-Dateien sind erlaubt'}), 400
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': f'Fehler beim Upload: {str(e)}'}), 500
|
|
|
|
def create_pdf_preview(pdf_filename):
|
|
"""Erstellt eine Vorschau der ersten PDF-Seite"""
|
|
try:
|
|
pdf_path = os.path.join(UPLOAD_FOLDER, pdf_filename)
|
|
|
|
# Vorschau-Dateiname generieren
|
|
preview_filename = f"preview_{pdf_filename.rsplit('.', 1)[0]}.png"
|
|
preview_path = os.path.join(UPLOAD_FOLDER, preview_filename)
|
|
|
|
# PDF zu Bild konvertieren (nur erste Seite)
|
|
try:
|
|
# Versuche mit pdf2image (benötigt Poppler)
|
|
images = pdf2image.convert_from_path(
|
|
pdf_path,
|
|
first_page=1,
|
|
last_page=1,
|
|
dpi=150,
|
|
fmt='PNG'
|
|
)
|
|
|
|
if images:
|
|
# Bild speichern
|
|
images[0].save(preview_path, 'PNG')
|
|
print(f"PDF-Vorschau erstellt: {preview_filename}")
|
|
return preview_filename
|
|
|
|
except ImportError:
|
|
print("pdf2image nicht verfügbar - PDF-Vorschau übersprungen")
|
|
return None
|
|
except Exception as pdf2image_error:
|
|
print(f"pdf2image Fehler: {str(pdf2image_error)}")
|
|
|
|
# Fallback: Versuche mit PyPDF2 + reportlab eine einfache Vorschau zu erstellen
|
|
try:
|
|
return create_simple_pdf_preview(pdf_path, preview_path, pdf_filename)
|
|
except Exception as fallback_error:
|
|
print(f"Fallback Fehler: {str(fallback_error)}")
|
|
return None
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
print(f"Allgemeiner Fehler beim Erstellen der PDF-Vorschau: {str(e)}")
|
|
return None
|
|
|
|
def create_simple_pdf_preview(pdf_path, preview_path, pdf_filename):
|
|
"""Erstellt eine einfache Text-basierte PDF-Vorschau als Fallback"""
|
|
try:
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
# Lese PDF-Informationen
|
|
with open(pdf_path, 'rb') as pdf_file:
|
|
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
|
page_count = len(pdf_reader.pages)
|
|
|
|
# Versuche Text der ersten Seite zu extrahieren
|
|
first_page_text = ""
|
|
if page_count > 0:
|
|
try:
|
|
first_page_text = pdf_reader.pages[0].extract_text()[:200]
|
|
except:
|
|
first_page_text = "PDF-Inhalt"
|
|
|
|
# Erstelle ein einfaches Vorschaubild
|
|
img = Image.new('RGB', (300, 400), color='white')
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Versuche Standard-Font zu laden
|
|
try:
|
|
font = ImageFont.truetype("arial.ttf", 12)
|
|
title_font = ImageFont.truetype("arial.ttf", 16)
|
|
except:
|
|
font = ImageFont.load_default()
|
|
title_font = ImageFont.load_default()
|
|
|
|
# Zeichne PDF-Vorschau
|
|
draw.rectangle([(10, 10), (290, 390)], outline='black', width=2)
|
|
draw.text((20, 20), f"PDF: {pdf_filename}", fill='black', font=title_font)
|
|
draw.text((20, 50), f"Seiten: {page_count}", fill='gray', font=font)
|
|
|
|
# Text-Vorschau
|
|
if first_page_text:
|
|
# Text umbrechen
|
|
words = first_page_text.split()
|
|
lines = []
|
|
current_line = ""
|
|
for word in words[:50]: # Maximal 50 Wörter
|
|
if len(current_line + word) < 35:
|
|
current_line += word + " "
|
|
else:
|
|
lines.append(current_line.strip())
|
|
current_line = word + " "
|
|
if len(lines) >= 15: # Maximal 15 Zeilen
|
|
break
|
|
if current_line:
|
|
lines.append(current_line.strip())
|
|
|
|
y_pos = 80
|
|
for line in lines:
|
|
draw.text((20, y_pos), line, fill='black', font=font)
|
|
y_pos += 18
|
|
|
|
# Speichere Vorschaubild
|
|
img.save(preview_path, 'PNG')
|
|
print(f"Fallback PDF-Vorschau erstellt: {preview_path}")
|
|
return os.path.basename(preview_path)
|
|
|
|
except Exception as e:
|
|
print(f"Fehler bei Fallback-Vorschau: {str(e)}")
|
|
return None
|
|
|
|
@app.route('/api/pdf-to-images', methods=['POST'])
|
|
def pdf_to_images():
|
|
"""Konvertiert PDF zu Bildern"""
|
|
try:
|
|
data = request.get_json()
|
|
filename = data.get('filename')
|
|
|
|
if not filename:
|
|
return jsonify({'error': 'Keine Datei angegeben'}), 400
|
|
|
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
|
|
|
if not os.path.exists(file_path):
|
|
return jsonify({'error': 'Datei nicht gefunden'}), 404
|
|
|
|
# PDF zu Bildern konvertieren
|
|
images = pdf2image.convert_from_path(file_path, dpi=200)
|
|
|
|
# ZIP-Datei erstellen
|
|
zip_filename = f"pdf_images_{str(uuid.uuid4())[:8]}.zip"
|
|
zip_path = os.path.join(OUTPUT_FOLDER, zip_filename)
|
|
|
|
with zipfile.ZipFile(zip_path, 'w') as zip_file:
|
|
for i, image in enumerate(images):
|
|
img_filename = f"page_{i+1}.png"
|
|
img_buffer = io.BytesIO()
|
|
image.save(img_buffer, format='PNG')
|
|
img_buffer.seek(0)
|
|
zip_file.writestr(img_filename, img_buffer.read())
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'filename': zip_filename,
|
|
'page_count': len(images),
|
|
'message': f'{len(images)} Seiten als Bilder exportiert'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': f'Fehler bei der Konvertierung: {str(e)}'}), 500
|
|
|
|
@app.route('/api/merge-pdfs', methods=['POST'])
|
|
def merge_pdfs():
|
|
"""Fügt mehrere PDFs zusammen"""
|
|
try:
|
|
data = request.get_json()
|
|
files_data = data.get('files', [])
|
|
|
|
if len(files_data) < 2:
|
|
return jsonify({'error': 'Mindestens 2 PDF-Dateien erforderlich'}), 400
|
|
|
|
# PDF Merger erstellen
|
|
merger = PyPDF2.PdfMerger()
|
|
|
|
for file_data in files_data:
|
|
filename = file_data.get('filename')
|
|
rotation = file_data.get('rotation', 0)
|
|
|
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
|
if os.path.exists(file_path):
|
|
if rotation != 0:
|
|
# PDF mit Rotation erstellen
|
|
rotated_pdf_path = create_rotated_pdf(file_path, rotation)
|
|
if rotated_pdf_path:
|
|
merger.append(rotated_pdf_path)
|
|
# Temporäre rotierte PDF löschen nach dem Hinzufügen
|
|
os.remove(rotated_pdf_path)
|
|
else:
|
|
merger.append(file_path) # Fallback ohne Rotation
|
|
else:
|
|
merger.append(file_path)
|
|
|
|
# Zusammengeführte PDF speichern
|
|
output_filename = f"merged_pdf_{str(uuid.uuid4())[:8]}.pdf"
|
|
output_path = os.path.join(OUTPUT_FOLDER, output_filename)
|
|
|
|
with open(output_path, 'wb') as output_file:
|
|
merger.write(output_file)
|
|
|
|
merger.close()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'filename': output_filename,
|
|
'message': f'{len(files_data)} PDFs erfolgreich zusammengeführt'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': f'Fehler beim Zusammenführen: {str(e)}'}), 500
|
|
|
|
def create_rotated_pdf(pdf_path, rotation):
|
|
"""Erstellt eine rotierte Version der PDF"""
|
|
try:
|
|
# Temporären Dateinamen für rotierte PDF erstellen
|
|
temp_filename = f"temp_rotated_{str(uuid.uuid4())[:8]}.pdf"
|
|
temp_path = os.path.join(UPLOAD_FOLDER, temp_filename)
|
|
|
|
with open(pdf_path, 'rb') as input_file:
|
|
pdf_reader = PyPDF2.PdfReader(input_file)
|
|
pdf_writer = PyPDF2.PdfWriter()
|
|
|
|
for page in pdf_reader.pages:
|
|
# Seite rotieren
|
|
rotated_page = page.rotate(rotation)
|
|
pdf_writer.add_page(rotated_page)
|
|
|
|
with open(temp_path, 'wb') as output_file:
|
|
pdf_writer.write(output_file)
|
|
|
|
return temp_path
|
|
|
|
except Exception as e:
|
|
print(f"Fehler beim Rotieren der PDF: {str(e)}")
|
|
return None
|
|
|
|
@app.route('/uploads/<filename>')
|
|
def serve_uploaded_file(filename):
|
|
"""Serviert hochgeladene Dateien für die Vorschau"""
|
|
try:
|
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
|
|
|
if not os.path.exists(file_path):
|
|
print(f"Datei nicht gefunden: {file_path}")
|
|
return "Datei nicht gefunden", 404
|
|
|
|
return send_file(file_path)
|
|
|
|
except Exception as e:
|
|
print(f"Fehler beim Servieren der Datei {filename}: {str(e)}")
|
|
return f"Fehler beim Laden der Datei: {str(e)}", 500
|
|
|
|
@app.route('/debug/pdf-preview/<filename>')
|
|
def debug_pdf_preview(filename):
|
|
"""Debug-Route für PDF-Vorschau-Probleme"""
|
|
try:
|
|
pdf_path = os.path.join(UPLOAD_FOLDER, filename)
|
|
|
|
if not os.path.exists(pdf_path):
|
|
return jsonify({'error': f'PDF nicht gefunden: {pdf_path}'})
|
|
|
|
# Teste PDF-Vorschau-Erstellung
|
|
preview_filename = create_pdf_preview(filename)
|
|
|
|
return jsonify({
|
|
'pdf_file': filename,
|
|
'pdf_exists': os.path.exists(pdf_path),
|
|
'preview_created': preview_filename is not None,
|
|
'preview_filename': preview_filename,
|
|
'preview_path': os.path.join(UPLOAD_FOLDER, preview_filename) if preview_filename else None,
|
|
'preview_exists': os.path.exists(os.path.join(UPLOAD_FOLDER, preview_filename)) if preview_filename else False
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': f'Debug-Fehler: {str(e)}'})
|
|
|
|
@app.route('/debug/check-dependencies')
|
|
def debug_check_dependencies():
|
|
"""Überprüft verfügbare Bibliotheken"""
|
|
deps = {}
|
|
|
|
try:
|
|
import pdf2image
|
|
deps['pdf2image'] = 'verfügbar'
|
|
# Teste Poppler
|
|
try:
|
|
pdf2image.convert_from_bytes(b'dummy')
|
|
except Exception as e:
|
|
if 'poppler' in str(e).lower():
|
|
deps['poppler'] = 'nicht gefunden'
|
|
else:
|
|
deps['poppler'] = 'möglicherweise verfügbar'
|
|
except ImportError:
|
|
deps['pdf2image'] = 'nicht installiert'
|
|
|
|
try:
|
|
import PyPDF2
|
|
deps['PyPDF2'] = 'verfügbar'
|
|
except ImportError:
|
|
deps['PyPDF2'] = 'nicht installiert'
|
|
|
|
try:
|
|
from PIL import Image
|
|
deps['Pillow'] = 'verfügbar'
|
|
except ImportError:
|
|
deps['Pillow'] = 'nicht installiert'
|
|
|
|
return jsonify(deps)
|
|
|
|
@app.route('/download/<filename>')
|
|
def download_file(filename):
|
|
"""Download einer generierten Datei"""
|
|
try:
|
|
file_path = os.path.join(OUTPUT_FOLDER, filename)
|
|
|
|
if not os.path.exists(file_path):
|
|
flash('Datei nicht gefunden', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
return send_file(file_path, as_attachment=True)
|
|
|
|
except Exception as e:
|
|
flash(f'Fehler beim Download: {str(e)}', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
if __name__ == '__main__':
|
|
# Ordner erstellen falls sie nicht existieren
|
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
|
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
|
|
|
|
app.run(debug=True, host='127.0.0.1', port=5000) |