Als Vertiefungsthema 2 habe ich eine Web-Applikation gewählt.
Für mein Vertiefungsthema habe ich eine Web-Applikation entwickelt. Zunächst möchte ich den Hintergrund dazu erläutern: In den Vorlesungen mache ich mir gerne direkte Notizen auf die Skripte. Da diese jedoch oftmals sehr wenig Platz bieten, habe ich in der Vergangenheit ein Python-Programm erstellt, das jeder Seite eines PDF-Dokuments einen weißen Rand hinzufügt. Nun habe ich mir überlegt, dieses Tool auch anderen zur Verfügung zu stellen. Gleichzeitig erleichtert es mir selbst die Nutzung. Statt das Programm jedes Mal zu suchen und auszuführen – wobei es manchmal auch Probleme mit den PDF-Dateien gab – kann ich nun einfach das Dokument hochladen und herunterladen. Da ich bereits ein Skript habe, das einige Anforderungen erfüllt, werde ich dieses Skript für das Backend verwenden und es um weitere Funktionen erweitern, wie variable Seitenränder und die Möglichkeit für den Benutzer, die Größe des Randes individuell anzupassen.
Mein Portfolio basiert auf einem statischen Export einer React-App. Daher ist es mir leider nicht möglich, einen solchen Prozess im Hintergrund durchzuführen. Dieser Prozess läuft daher auf einem separaten Server.
Der Aufbau meines Vertiefungsthemas gestaltet sich wie folgt:
1. HTML-Seite für das Hochladen eines PDFs
2. Upload und Verarbeitung
3. Speicherung und Download
Mein Vertiefungsthema besteht aus vier verschiedenen Programmen:
1. server.py – Bearbeitung der PDF-Dokumente
2. housekeeping.py – öscht alte Dokumente aus dem öffentlichen Ordner
3. index.html – Benutzeroberfläche für den User
4. script.js – Vorschau der PDF-Dokumente und Interaktion zwischen index.html und server.py
Das Skript server.py ist für die Bearbeitung der PDF-Dokumente zuständig. Es empfängt das hochgeladene PDF, fügt den gewünschten Rand hinzu und speichert das bearbeitete PDF im öffentlichen Ordner. Hier sind die wesentlichen Funktionen des Skripts:
from flask import Flask, request, jsonify, session, redirect, url_for
from werkzeug.utils import secure_filename
from PyPDF2 import PdfReader, PdfWriter
from PyPDF2 import Transformation
from tqdm import tqdm
import os
import time
app = Flask(__name__)
UPLOAD_FOLDER = '/var/www/html/public'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def generate_unique_identifier():
return str(int(time.time()))
def add_margin_to_pdf(input_file, output_file, margins):
try:
with open(input_file, 'rb') as f:
p = PdfReader(f)
writer = PdfWriter()
number_of_pages = len(p.pages)
for i in tqdm(range(number_of_pages)):
page = p.pages[i]
mediabox = page.mediabox
original_width = mediabox.upper_right[0] - mediabox.lower_left[0]
original_height = mediabox.upper_right[1] - mediabox.lower_left[1]
new_width = original_width + margins['left_margin'] + margins['right_margin']
new_height = original_height + margins['top_margin'] + margins['bottom_margin']
new_page = writer.add_blank_page(width=new_width, height=new_height)
tx = margins['left_margin']
ty = margins['bottom_margin']
page.merge_page(new_page)
new_page.merge_page(page)
transformation = Transformation().translate(tx, ty)
new_page.add_transformation(transformation)
with open(output_file, 'wb') as f_out:
writer.write(f_out)
except Exception as e:
print('Error processing PDF: %s' % str(e))
raise
def delete_file(filepath):
try:
os.remove(filepath)
except Exception as e:
app.logger.error('Error deleting file: %s', str(e))
raise
@app.route('/api/upload', methods=['POST'])
def upload_file():
try:
if 'pdf_file' not in request.files:
return jsonify({'error': 'No PDF file uploaded'}), 400
pdf_file = request.files['pdf_file']
if pdf_file.filename == '':
return jsonify({'error': 'No PDF file selected'}), 400
if pdf_file and pdf_file.filename.endswith('.pdf'):
filename = secure_filename(pdf_file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
pdf_file.save(filepath)
top_margin_enabled = request.form.get('top_margin_enabled') == 'true'
bottom_margin_enabled = request.form.get('bottom_margin_enabled') == 'true'
left_margin_enabled = request.form.get('left_margin_enabled') == 'true'
right_margin_enabled = request.form.get('right_margin_enabled') == 'true'
margins = {
'top_margin': int(request.form.get('top_margin', 0)) if top_margin_enabled else 0,
'bottom_margin': int(request.form.get('bottom_margin', 0)) if bottom_margin_enabled else 0,
'left_margin': int(request.form.get('left_margin', 0)) if left_margin_enabled else 0,
'right_margin': int(request.form.get('right_margin', 0)) if right_margin_enabled else 0,
}
output_filename = f"Modified_{generate_unique_identifier()}_{filename}"
output_filepath = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
add_margin_to_pdf(filepath, output_filepath, margins)
session['download_pressed'] = False
return jsonify({'download_url': f'/downloads/{output_filename}'}), 200
else:
return jsonify({'error': 'Invalid file format, only PDF files are supported'}), 400
except Exception as e:
app.logger.error('Error uploading file: %s', str(e))
return jsonify({'error': 'An error occurred while processing the request'}), 500
@app.route('/download-complete', methods=['GET'])
def download_complete():
try:
if session.get('download_pressed', False):
filename = session.pop('file_to_delete', None)
if filename:
delete_file(filename)
return jsonify({'message': 'File deleted successfully'}), 200
else:
return jsonify({'error': 'No file to delete'}), 404
else:
return jsonify({'error': 'Download button not pressed'}), 400
except Exception as e:
app.logger.error('Error deleting file: %s', str(e))
return jsonify({'error': 'An error occurred while deleting the file'}), 500
if __name__ == '__main__':
app.run(debug=True, port=3000)
Das Skript housekeeping.py ist für die Verwaltung der gespeicherten Dokumente verantwortlich. Es löscht alte Dokumente aus dem öffentlichen Ordner, um Speicherplatz freizugeben und die Übersichtlichkeit zu wahren. Die Hauptfunktionen umfassen:
import os
import time
from datetime import datetime, timedelta
DIRECTORY_PATH = "/var/www/html/public"
LOG_FILE = "deletion_log.txt"
def log_action(action, file_path=""):
with open(LOG_FILE, 'a') as log:
if file_path:
log.write(f"{datetime.now()} - {action}: {file_path}
")
else:
log.write(f"{datetime.now()} - {action}
")
def clean_directory(path):
now = datetime.now()
cutoff_time = now - timedelta(hours=24)
for root, dirs, files in os.walk(path, topdown=False):
for name in files:
file_path = os.path.join(root, name)
file_mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_mtime < cutoff_time:
if not name.endswith(".txt"):
# Due to log file
os.remove(file_path)
log_action("Deleted file", file_path)
print(f"Deleted file: {file_path}")
for name in dirs:
dir_path = os.path.join(root, name)
if not os.listdir(dir_path):
os.rmdir(dir_path)
log_action("Deleted empty directory", dir_path)
print(f"Deleted empty directory: {dir_path}")
def run_periodically():
while True:
log_action("Script executed")
clean_directory(DIRECTORY_PATH)
time.sleep(300)
if __name__ == "__main__":
run_periodically()
Die Datei index.html stellt die Benutzeroberfläche für die Web-Applikation bereit. Sie enthält ein Formular zum Hochladen von PDF-Dokumenten und Eingabefelder für die Konfiguration der Seitenränder. Zu den wichtigsten Elementen gehören:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>PDF Upload</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
<style>
.container {
max-width: 800px;
margin: auto;
}
.input-group {
margin-bottom: 1.5rem;
}
.pdf-preview-container {
border: 1px solid #ccc;
margin-top: 1.5rem;
}
.pdf-preview {
width: 100%;
height: 500px;
}
.processing-banner {
background-color: #4CAF50;
color: white;
text-align: center;
padding: 10px;
margin-top: 1.5rem;
display: none;
}
.hidden {
display: none;
}
.centered-button-container {
display: flex;
justify-content: center;
}
.bottom-margin {
margin-bottom: 600px;
}
.margin-table {
width: 100%;
}
.margin-table td {
padding: 8px;
}
.margin-table input[type="number"] {
width: 100%;
}
</style>
</head>
<body class="bg-gray-900 flex items-center justify-center h-screen">
<div id="mainContainer" class="container bg-black opacity-80 text-white p-8 rounded-lg shadow-lg">
<i class="fas fa-times text-xl text-gray-400 reset-icon" id="resetBtn"></i>
<h1 class="text-2xl font-bold mb-4 text-red-600">PDF-Dokument hochladen</h1>
<p class="mb-4">Laden Sie Ihr PDF-Dokument hoch, um es zu speichern.</p>
<input type="file" id="pdfFile" accept="application/pdf" class="block w-full text-sm text-gray-900 bg-gray-300 rounded-lg border border-gray-300 cursor-pointer focus:outline-none focus:border-transparent p-2.5 mb-4">
<div class="pdf-preview-container hidden">
<canvas id="pdfPreview" class="pdf-preview"></canvas>
</div>
<div class="mt-8 border border-white p-4">
<h2 class="text-xl font-bold mb-4 text-red-600">Seiten mit zusätzlichem Rand</h2>
<table class="margin-table">
<tr>
<td><label for="topMargin">Oben:</label></td>
<td><input type="number" id="topMargin" class="text-sm text-gray-900 bg-gray-300 rounded-lg border border-gray-300 cursor-pointer focus:outline-none focus:border-transparent p-2.5" value="200"></td>
<td>
<input type="checkbox" id="topCheckbox">
<label for="topCheckbox" class="ml-1">Zusätzlichen Rand hinzufügen</label>
</td>
</tr>
<tr>
<td><label for="leftMargin">Links:</label></td>
<td><input type="number" id="leftMargin" class="text-sm text-gray-900 bg-gray-300 rounded-lg border border-gray-300 cursor-pointer focus:outline-none focus:border-transparent p-2.5" value="200"></td>
<td>
<input type="checkbox" id="leftCheckbox">
<label for="leftCheckbox" class="ml-1">Zusätzlichen Rand hinzufügen</label>
</td>
</tr>
<tr>
<td><label for="rightMargin">Rechts:</label></td>
<td><input type="number" id="rightMargin" class="text-sm text-gray-900 bg-gray-300 rounded-lg border border-gray-300 cursor-pointer focus:outline-none focus:border-transparent p-2.5" value="200"></td>
<td>
<input type="checkbox" id="rightCheckbox">
<label for="rightCheckbox" class="ml-1">Zusätzlichen Rand hinzufügen</label>
</td>
</tr>
<tr>
<td><label for="bottomMargin">Unten:</label></td>
<td><input type="number" id="bottomMargin" class="text-sm text-gray-900 bg-gray-300 rounded-lg border border-gray-300 cursor-pointer focus:outline-none focus:border-transparent p-2.5" value="200"></td>
<td>
<input type="checkbox" id="bottomCheckbox">
<label for="bottomCheckbox" class="ml-1">Zusätzlichen Rand hinzufügen</label>
</td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox" id="selectAllCheckbox" class="mr-2">
<label for="selectAllCheckbox">Alle Seiten auswählen</label>
</td>
<td>
<input type="number" id="allMargin" class="text-sm text-gray-900 bg-gray-300 rounded-lg border border-gray-300 cursor-pointer focus:outline-none focus:border-transparent p-2.5 hidden" value="200">
</td>
</tr>
</table>
</div>
<button id="uploadBtn" class="bg-red-600 hover:bg-red-800 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline w-full mt-8">
Hochladen
</button>
<div id="processingBanner" class="processing-banner">
Verarbeitung läuft...
</div>
<div class="mt-4">
<div class="bg-gray-300 w-full rounded-full h-2">
<div id="progressBar" class="bg-green-500 h-2 rounded-full hidden"></div>
</div>
<p id="statusText" class="text-center mt-2"></p>
</div>
<div class="mt-4 centered-button-container">
<a id="downloadHtmlBtn" class="hidden bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mt-8" download>Download Modified PDF</a>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script src="script.js"></script>
<script>
document.getElementById('selectAllCheckbox').addEventListener('change', function() {
const allMarginInput = document.getElementById('allMargin');
if (this.checked) {
allMarginInput.classList.remove('hidden');
} else {
allMarginInput.classList.add('hidden');
}
});
</script>
</body>
</html>
Das Skript script.js steuert die Interaktion zwischen der Benutzeroberfläche (index.html) und dem Backend (server.py). Es enthält Funktionen zur Vorschau des PDFs, zum Hochladen der Datei und zur Anzeige des Fortschritts. Wichtige Funktionen sind:
document.addEventListener('DOMContentLoaded', function () {
const uploadButton = document.getElementById('uploadBtn');
const fileInput = document.getElementById('pdfFile');
const progressBar = document.getElementById('progressBar');
const statusText = document.getElementById('statusText');
const pdfPreviewContainer = document.querySelector('.pdf-preview-container');
const pdfPreviewCanvas = document.getElementById('pdfPreview');
const resetBtn = document.getElementById('resetBtn');
const topMarginInput = document.getElementById('topMargin');
const bottomMarginInput = document.getElementById('bottomMargin');
const leftMarginInput = document.getElementById('leftMargin');
const rightMarginInput = document.getElementById('rightMargin');
const topMarginCheckbox = document.getElementById('topCheckbox');
const bottomMarginCheckbox = document.getElementById('bottomCheckbox');
const leftMarginCheckbox = document.getElementById('leftCheckbox');
const rightMarginCheckbox = document.getElementById('rightCheckbox');
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const allMarginInput = document.getElementById('allMargin');
const downloadHtmlBtn = document.getElementById('downloadHtmlBtn');
const processingBanner = document.getElementById('processingBanner');
const mainContainer = document.getElementById('mainContainer');
let pdfDoc = null;
let pageNum = 1;
let numPages = 0;
let progressInterval;
function renderPage(pageNum) {
pdfDoc.getPage(pageNum).then(function(page) {
const scale = 1.5;
const viewport = page.getViewport({scale: scale});
const context = pdfPreviewCanvas.getContext('2d');
pdfPreviewCanvas.height = viewport.height;
pdfPreviewCanvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport
};
page.render(renderContext);
});
}
function disableUploadButton() {
uploadButton.disabled = true;
}
function enableUploadButton() {
uploadButton.disabled = false;
}
function showProcessingBanner() {
processingBanner.style.display = 'block';
}
function hideProcessingBanner() {
processingBanner.style.display = 'none';
}
function showProgressBar() {
progressBar.classList.remove('hidden');
}
function hideProgressBar() {
progressBar.classList.add('hidden');
}
function resetProgressBar() {
progressBar.style.width = '0%';
hideProgressBar();
}
function startContinuousProgressBar() {
let progress = 0;
showProgressBar();
progressInterval = setInterval(() => {
progress += 1;
if (progress > 100) {
progress = 0;
}
progressBar.style.width = progress + '%';
}, 100);
}
function stopContinuousProgressBar() {
clearInterval(progressInterval);
progressBar.style.width = '100%';
}
function getMargins() {
if (selectAllCheckbox.checked) {
const allMargin = allMarginInput.value;
return {
top: allMargin,
bottom: allMargin,
left: allMargin,
right: allMargin,
topEnabled: true,
bottomEnabled: true,
leftEnabled: true,
rightEnabled: true
};
} else {
return {
top: topMarginInput.value,
bottom: bottomMarginInput.value,
left: leftMarginInput.value,
right: rightMarginInput.value,
topEnabled: topMarginCheckbox.checked,
bottomEnabled: bottomMarginCheckbox.checked,
leftEnabled: leftMarginCheckbox.checked,
rightEnabled: rightMarginCheckbox.checked
};
}
}
function resetUI() {
statusText.textContent = '';
resetProgressBar();
downloadHtmlBtn.classList.add('hidden');
mainContainer.classList.remove('bottom-margin');
}
function scrollToDownloadButton() {
downloadHtmlBtn.scrollIntoView({ behavior: 'smooth' });
}
uploadButton.addEventListener('click', function() {
disableUploadButton();
showProcessingBanner();
resetProgressBar();
const file = fileInput.files[0];
const formData = new FormData();
formData.append('pdf_file', file);
const margins = getMargins();
formData.append('top_margin', margins.top);
formData.append('bottom_margin', margins.bottom);
formData.append('left_margin', margins.left);
formData.append('right_margin', margins.right);
formData.append('top_margin_enabled', margins.topEnabled);
formData.append('bottom_margin_enabled', margins.bottomEnabled);
formData.append('left_margin_enabled', margins.leftEnabled);
formData.append('right_margin_enabled', margins.rightEnabled);
if (!file) {
statusText.textContent = 'Bitte wählen Sie eine Datei aus. (Version 1.0)';
enableUploadButton();
hideProcessingBanner();
return;
}
statusText.textContent = '';
startContinuousProgressBar();
fetch('/api/upload', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.json();
})
.then(data => {
stopContinuousProgressBar();
statusText.textContent = 'Datei erfolgreich hochgeladen!';
if (data.download_url) {
downloadHtmlBtn.href = data.download_url;
downloadHtmlBtn.classList.remove('hidden');
mainContainer.classList.add('bottom-margin'); // Add margin when button is shown
scrollToDownloadButton();
}
enableUploadButton();
hideProcessingBanner();
})
.catch(error => {
stopContinuousProgressBar();
statusText.textContent = "Fehler: $ {error.message}";
enableUploadButton();
hideProcessingBanner();
});
});
resetBtn.addEventListener('click', function() {
fileInput.value = '';
statusText.textContent = '';
resetProgressBar();
pdfPreviewContainer.classList.add('hidden');
downloadHtmlBtn.classList.add('hidden');
mainContainer.classList.remove('bottom-margin');
});
fileInput.addEventListener('change', function() {
const file = fileInput.files[0];
if (file) {
resetUI();
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(function(pdf) {
pdfDoc = pdf;
numPages = pdf.numPages;
renderPage(pageNum);
pdfPreviewContainer.classList.remove('hidden');
});
};
fileReader.readAsArrayBuffer(file);
}
});
selectAllCheckbox.addEventListener('change', function() {
if (this.checked) {
allMarginInput.classList.remove('hidden');
} else {
allMarginInput.classList.add('hidden');
}
});
});
Durch diese Struktur ist die Web-Applikation benutzerfreundlich und effizient, sowohl für mich als auch für andere Benutzer, die von den zusätzlichen Funktionen profitieren können.
Insgesamt ermöglicht die Web-Applikation den Benutzern, PDF-Dokumente mit individuellen Seitenrändern zu versehen und herunterzuladen. Die klare Benutzeroberfläche und die einfache Bedienung machen die Anwendung benutzerfreundlich und effizient. Durch die Kombination von Frontend und Backend-Technologien wird eine nahtlose Interaktion zwischen Benutzer und System gewährleistet.
Die Entwicklung der Web-Applikation hat mir wertvolle Einblicke in die Gestaltung und Umsetzung von Softwarelösungen gegeben. Ich konnte meine Kenntnisse in den Bereichen Web-Entwicklung, API-Integration und Benutzeroberflächen verbessern und praktische Erfahrungen sammeln. Die Anwendung meines Vertiefungsthemas zeigt, wie Technologie eingesetzt werden kann, um Probleme zu lösen und den Alltag zu erleichtern.