modified: .gitignore
modified: QUICKSTART.md modified: app.py new file: output/.gitkeep modified: static/css/style.css modified: static/js/images-to-pdf.js modified: templates/images_to_pdf.html new file: uploads/.gitkeep
This commit is contained in:
24
.gitignore
vendored
24
.gitignore
vendored
@@ -160,3 +160,27 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
# PDF Web App specific - Ignore upload and output contents but keep directories
|
||||||
|
uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
output/*
|
||||||
|
!output/.gitkeep
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS specific
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ Die App läuft jetzt lokal auf Ihrem Computer. Sie können:
|
|||||||
- ✅ PDFs zusammenführen
|
- ✅ PDFs zusammenführen
|
||||||
- ✅ PDFs zu Bildern konvertieren
|
- ✅ PDFs zu Bildern konvertieren
|
||||||
- ✅ Drag & Drop verwenden
|
- ✅ Drag & Drop verwenden
|
||||||
|
- ✅ **NEU: Bildvorschau mit Rotation** - Bilder anzeigen und drehen
|
||||||
|
- ✅ **NEU: Bildorientierung ändern** - 90°, 180°, 270° Rotation
|
||||||
|
- ✅ **NEU: Verschiedene Vorschaugrößen** - Klein, Mittel, Groß
|
||||||
|
- ✅ **NEU: Bildmodal** - Bilder in voller Größe anzeigen
|
||||||
- ✅ Vollständig offline arbeiten
|
- ✅ Vollständig offline arbeiten
|
||||||
|
|
||||||
## Bei Problemen:
|
## Bei Problemen:
|
||||||
|
|||||||
41
app.py
41
app.py
@@ -112,9 +112,9 @@ def convert_images_to_pdf():
|
|||||||
"""Konvertiert hochgeladene Bilder zu PDF"""
|
"""Konvertiert hochgeladene Bilder zu PDF"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
filenames = data.get('filenames', [])
|
files_data = data.get('files', [])
|
||||||
|
|
||||||
if not filenames:
|
if not files_data:
|
||||||
return jsonify({'error': 'Keine Dateien ausgewählt'}), 400
|
return jsonify({'error': 'Keine Dateien ausgewählt'}), 400
|
||||||
|
|
||||||
# PDF erstellen
|
# PDF erstellen
|
||||||
@@ -125,12 +125,19 @@ def convert_images_to_pdf():
|
|||||||
c = canvas.Canvas(output_path, pagesize=A4)
|
c = canvas.Canvas(output_path, pagesize=A4)
|
||||||
page_width, page_height = A4
|
page_width, page_height = A4
|
||||||
|
|
||||||
for filename in filenames:
|
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)
|
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
# Bild öffnen und Größe anpassen
|
# Bild öffnen und rotieren
|
||||||
with Image.open(file_path) as img:
|
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
|
img_width, img_height = img.size
|
||||||
|
|
||||||
# Seitenverhältnis berechnen
|
# Seitenverhältnis berechnen
|
||||||
@@ -151,8 +158,16 @@ def convert_images_to_pdf():
|
|||||||
x = (page_width - new_width) / 2
|
x = (page_width - new_width) / 2
|
||||||
y = (page_height - new_height) / 2
|
y = (page_height - new_height) / 2
|
||||||
|
|
||||||
# Bild zur PDF hinzufügen
|
# Temporäres rotiertes Bild speichern
|
||||||
c.drawImage(file_path, x, y, width=new_width, height=new_height)
|
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.showPage()
|
||||||
|
|
||||||
c.save()
|
c.save()
|
||||||
@@ -278,6 +293,20 @@ def merge_pdfs():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': f'Fehler beim Zusammenführen: {str(e)}'}), 500
|
return jsonify({'error': f'Fehler beim Zusammenführen: {str(e)}'}), 500
|
||||||
|
|
||||||
|
@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):
|
||||||
|
return "Datei nicht gefunden", 404
|
||||||
|
|
||||||
|
return send_file(file_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Fehler beim Laden der Datei: {str(e)}", 500
|
||||||
|
|
||||||
@app.route('/download/<filename>')
|
@app.route('/download/<filename>')
|
||||||
def download_file(filename):
|
def download_file(filename):
|
||||||
"""Download einer generierten Datei"""
|
"""Download einer generierten Datei"""
|
||||||
|
|||||||
2
output/.gitkeep
Normal file
2
output/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Diese Datei sorgt dafür, dass der output Ordner in Git erhalten bleibt
|
||||||
|
# Der Ordnerinhalt wird durch .gitignore ignoriert
|
||||||
@@ -411,4 +411,101 @@ footer {
|
|||||||
/* File Type Icons */
|
/* File Type Icons */
|
||||||
.file-type-pdf { color: #dc3545; }
|
.file-type-pdf { color: #dc3545; }
|
||||||
.file-type-image { color: #28a745; }
|
.file-type-image { color: #28a745; }
|
||||||
.file-type-zip { color: #ffc107; }
|
.file-type-zip { color: #ffc107; }
|
||||||
|
|
||||||
|
/* Image Preview */
|
||||||
|
.image-preview {
|
||||||
|
max-width: 120px;
|
||||||
|
max-height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview.size-small {
|
||||||
|
max-width: 80px;
|
||||||
|
max-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview.size-medium {
|
||||||
|
max-width: 120px;
|
||||||
|
max-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview.size-large {
|
||||||
|
max-width: 160px;
|
||||||
|
max-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-large {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rotation controls */
|
||||||
|
.rotation-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotation-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image rotation classes */
|
||||||
|
.rotate-0 { transform: rotate(0deg); }
|
||||||
|
.rotate-90 { transform: rotate(90deg); }
|
||||||
|
.rotate-180 { transform: rotate(180deg); }
|
||||||
|
.rotate-270 { transform: rotate(270deg); }
|
||||||
|
|
||||||
|
/* Image modal */
|
||||||
|
.image-modal .modal-body {
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-modal .modal-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File item enhanced */
|
||||||
|
.file-item-enhanced {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item-enhanced:hover {
|
||||||
|
background-color: rgba(0, 123, 255, 0.05);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--box-shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
@@ -122,27 +122,50 @@ function displayFiles() {
|
|||||||
|
|
||||||
function createFileItem(file, index) {
|
function createFileItem(file, index) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'list-group-item file-item';
|
div.className = 'list-group-item file-item-enhanced';
|
||||||
div.dataset.index = index;
|
div.dataset.index = index;
|
||||||
|
|
||||||
|
// Initialize rotation if not set
|
||||||
|
if (!file.rotation) {
|
||||||
|
file.rotation = 0;
|
||||||
|
}
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="file-icon">
|
<div class="d-flex align-items-center">
|
||||||
<i class="${getFileIcon(file.original_name)}"></i>
|
<div class="image-preview-container me-3">
|
||||||
</div>
|
<img src="/uploads/${file.filename}" class="image-preview size-medium rotate-${file.rotation}"
|
||||||
<div class="file-info">
|
alt="${file.original_name}" onclick="showImageModal('${file.filename}', '${file.original_name}', ${index})">
|
||||||
<div class="file-name">${file.original_name}</div>
|
</div>
|
||||||
<div class="file-details">${formatFileSize(file.size)}</div>
|
<div class="image-info">
|
||||||
</div>
|
<div class="file-name fw-bold">${file.original_name}</div>
|
||||||
<div class="file-actions">
|
<div class="file-details text-muted">${formatFileSize(file.size)}</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="moveFileUp(${index})" ${index === 0 ? 'disabled' : ''}>
|
<div class="rotation-controls">
|
||||||
<i class="fas fa-arrow-up"></i>
|
<button type="button" class="btn btn-sm btn-outline-primary rotation-btn"
|
||||||
</button>
|
onclick="rotateImage(${index}, -90)" title="Links drehen">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="moveFileDown(${index})" ${index === imagesToPdfFiles.length - 1 ? 'disabled' : ''}>
|
<i class="fas fa-undo"></i>
|
||||||
<i class="fas fa-arrow-down"></i>
|
</button>
|
||||||
</button>
|
<button type="button" class="btn btn-sm btn-outline-primary rotation-btn"
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeFile(${index})">
|
onclick="rotateImage(${index}, 90)" title="Rechts drehen">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-redo"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<small class="text-muted ms-2">${file.rotation}°</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-controls">
|
||||||
|
<div class="d-flex gap-1 mb-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="moveFileUp(${index})"
|
||||||
|
${index === 0 ? 'disabled' : ''} title="Nach oben">
|
||||||
|
<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' : ''} title="Nach unten">
|
||||||
|
<i class="fas fa-arrow-down"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeFile(${index})" title="Entfernen">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -200,11 +223,16 @@ async function convertToPdf() {
|
|||||||
setLoadingState(convertBtn, true);
|
setLoadingState(convertBtn, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filenames = imagesToPdfFiles.map(file => file.filename);
|
// Include rotation data for each file
|
||||||
|
const filesWithRotation = imagesToPdfFiles.map(file => ({
|
||||||
|
filename: file.filename,
|
||||||
|
rotation: file.rotation || 0,
|
||||||
|
original_name: file.original_name
|
||||||
|
}));
|
||||||
|
|
||||||
const response = await makeRequest('/api/convert-images-to-pdf', {
|
const response = await makeRequest('/api/convert-images-to-pdf', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ filenames })
|
body: JSON.stringify({ files: filesWithRotation })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -264,7 +292,97 @@ function hideResults() {
|
|||||||
errorArea.style.display = 'none';
|
errorArea.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rotateImage(index, degrees) {
|
||||||
|
if (index >= 0 && index < imagesToPdfFiles.length) {
|
||||||
|
imagesToPdfFiles[index].rotation = (imagesToPdfFiles[index].rotation + degrees) % 360;
|
||||||
|
if (imagesToPdfFiles[index].rotation < 0) {
|
||||||
|
imagesToPdfFiles[index].rotation += 360;
|
||||||
|
}
|
||||||
|
displayFiles();
|
||||||
|
showNotification(`Bild um ${degrees}° gedreht.`, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showImageModal(filename, originalName, index) {
|
||||||
|
// Create modal if it doesn't exist
|
||||||
|
let modal = document.getElementById('image-modal');
|
||||||
|
if (!modal) {
|
||||||
|
modal = document.createElement('div');
|
||||||
|
modal.className = 'modal fade image-modal';
|
||||||
|
modal.id = 'image-modal';
|
||||||
|
modal.setAttribute('tabindex', '-1');
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="image-modal-title">${originalName}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<img id="modal-image" src="/uploads/${filename}" class="img-fluid rotate-${imagesToPdfFiles[index].rotation}" alt="${originalName}">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="btn-group me-auto">
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="rotateImageInModal(${index}, -90)">
|
||||||
|
<i class="fas fa-undo me-1"></i>Links drehen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="rotateImageInModal(${index}, 90)">
|
||||||
|
<i class="fas fa-redo me-1"></i>Rechts drehen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
} else {
|
||||||
|
// Update existing modal
|
||||||
|
document.getElementById('image-modal-title').textContent = originalName;
|
||||||
|
const modalImage = document.getElementById('modal-image');
|
||||||
|
modalImage.src = `/uploads/${filename}`;
|
||||||
|
modalImage.alt = originalName;
|
||||||
|
modalImage.className = `img-fluid rotate-${imagesToPdfFiles[index].rotation}`;
|
||||||
|
|
||||||
|
// Update rotation buttons
|
||||||
|
const rotateButtons = modal.querySelectorAll('.btn-outline-primary');
|
||||||
|
rotateButtons[0].onclick = () => rotateImageInModal(index, -90);
|
||||||
|
rotateButtons[1].onclick = () => rotateImageInModal(index, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
const bootstrapModal = new bootstrap.Modal(modal);
|
||||||
|
bootstrapModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateImageInModal(index, degrees) {
|
||||||
|
rotateImage(index, degrees);
|
||||||
|
|
||||||
|
// Update modal image
|
||||||
|
const modalImage = document.getElementById('modal-image');
|
||||||
|
modalImage.className = `img-fluid rotate-${imagesToPdfFiles[index].rotation}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreviewSize() {
|
||||||
|
const previewSize = document.getElementById('preview-size').value;
|
||||||
|
const previews = document.querySelectorAll('.image-preview');
|
||||||
|
|
||||||
|
previews.forEach(preview => {
|
||||||
|
// Remove existing size classes
|
||||||
|
preview.classList.remove('size-small', 'size-medium', 'size-large');
|
||||||
|
// Add new size class
|
||||||
|
preview.classList.add('size-' + previewSize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Global functions for button actions
|
// Global functions for button actions
|
||||||
window.moveFileUp = moveFileUp;
|
window.moveFileUp = moveFileUp;
|
||||||
window.moveFileDown = moveFileDown;
|
window.moveFileDown = moveFileDown;
|
||||||
window.removeFile = removeFile;
|
window.removeFile = removeFile;
|
||||||
|
window.rotateImage = rotateImage;
|
||||||
|
window.showImageModal = showImageModal;
|
||||||
|
window.rotateImageInModal = rotateImageInModal;
|
||||||
|
window.updatePreviewSize = updatePreviewSize;
|
||||||
@@ -131,6 +131,15 @@
|
|||||||
Seitenverhältnis beibehalten
|
Seitenverhältnis beibehalten
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Bildvorschau-Größe:</label>
|
||||||
|
<select id="preview-size" class="form-select" onchange="updatePreviewSize()">
|
||||||
|
<option value="small">Klein (80px)</option>
|
||||||
|
<option value="medium" selected>Mittel (120px)</option>
|
||||||
|
<option value="large">Groß (160px)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
2
uploads/.gitkeep
Normal file
2
uploads/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Diese Datei sorgt dafür, dass der uploads Ordner in Git erhalten bleibt
|
||||||
|
# Der Ordnerinhalt wird durch .gitignore ignoriert
|
||||||
Reference in New Issue
Block a user