Add Docker support and improve PDF conversion

Features:
- Dockerfile for containerized deployment
- Docker Compose configuration with health checks
- Automated database initialization via entrypoint script
- Quick start script for easy Docker deployment
- Comprehensive Docker deployment documentation (DOCKER.md)
- Complete README with installation and usage instructions
- .dockerignore for optimized image builds

Improvements:
- PDF conversion now preserves orientation (portrait/landscape)
- PDF rendering at 300 DPI for sharp quality
- Maintains aspect ratio during conversion
- Compact media library view with image thumbnails
- Better media preview with scrollable gallery

Docker Features:
- Multi-stage build for smaller images
- Non-root user for security
- Health checks for container monitoring
- Volume mounts for persistent data
- Production-ready Gunicorn configuration
- Support for Redis caching (optional)
This commit is contained in:
DigiServer Developer
2025-11-17 21:05:49 +02:00
parent 2e3e181bb2
commit 2db0033bc0
9 changed files with 827 additions and 42 deletions

View File

@@ -308,12 +308,11 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
log_action('info', f'Converting PDF to images: {filename}')
# Convert PDF pages to images at Full HD resolution
# Convert PDF pages to images at high DPI for quality
images = convert_from_path(
filepath,
dpi=150, # Good quality for Full HD display
fmt='png',
size=(1920, 1080) # Direct Full HD output
dpi=300, # 300 DPI for sharp rendering
fmt='png'
)
if not images:
@@ -323,18 +322,34 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
base_filename = Path(filename).stem
upload_folder = os.path.dirname(filepath)
# Save each page directly as PNG
# Save each page with proper aspect ratio preservation
converted_files = []
for idx, image in enumerate(images, start=1):
# Create filename for this page
page_filename = f"{base_filename}_page{idx:03d}.png"
page_filepath = os.path.join(upload_folder, page_filename)
# Save the image directly without additional optimization
image.save(page_filepath, 'PNG', optimize=True)
# Determine orientation and resize maintaining aspect ratio
width, height = image.size
is_portrait = height > width
# Define Full HD dimensions based on orientation
if is_portrait:
# Portrait: max height 1920, max width 1080 (rotated Full HD)
max_size = (1080, 1920)
else:
# Landscape: max width 1920, max height 1080 (standard Full HD)
max_size = (1920, 1080)
# Resize maintaining aspect ratio (thumbnail maintains ratio)
from PIL import Image as PILImage
image.thumbnail(max_size, PILImage.Resampling.LANCZOS)
# Save the optimized image
image.save(page_filepath, 'PNG', optimize=True, quality=95)
converted_files.append((page_filepath, page_filename))
log_action('info', f'Converted PDF page {idx}/{len(images)}: {page_filename}')
log_action('info', f'Converted PDF page {idx}/{len(images)} ({width}x{height} -> {image.size[0]}x{image.size[1]}): {page_filename}')
log_action('info', f'PDF converted successfully: {len(images)} pages from {filename}')

View File

@@ -205,6 +205,14 @@
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.media-thumbnail {
background: #f0f0f0;
}
body.dark-mode .media-thumbnail {
background: #1a202c;
}
.media-icon {
font-size: 48px;
margin-bottom: 10px;
@@ -334,55 +342,57 @@
<div class="card-header">
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 24px; height: 24px; filter: brightness(0) invert(1);">
Upload Media
Media Library
</h2>
</div>
<div style="text-align: center; padding: 40px 20px;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 96px; height: 96px; opacity: 0.5; margin-bottom: 20px;">
<h3 style="margin-bottom: 15px;">Upload Media Files</h3>
<p style="color: #6c757d; margin-bottom: 25px;">
Upload images, videos, and PDFs to your media library.<br>
Assign them to playlists during or after upload.
</p>
<a href="{{ url_for('content.upload_media_page') }}" class="btn btn-success" style="padding: 15px 40px; font-size: 16px; display: inline-flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
Go to Upload Page
<!-- Compact Upload Section -->
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 8px; margin-bottom: 20px;">
<a href="{{ url_for('content.upload_media_page') }}" class="btn btn-success" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 16px; height: 16px; filter: brightness(0) invert(1);">
Upload New Media
</a>
</div>
<!-- Media Library Preview -->
<hr style="margin: 25px 0;">
<h3 style="margin-bottom: 15px;">Media Library ({{ media_files|length }} files)</h3>
<div class="media-library">
<!-- Media Library with Thumbnails -->
<h3 style="margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between;">
<span>📚 Available Media ({{ media_files|length }})</span>
</h3>
<div class="media-library" style="max-height: 500px; overflow-y: auto;">
{% if media_files %}
{% for media in media_files[:12] %}
{% for media in media_files %}
<div class="media-item" title="{{ media.filename }}">
<div class="media-icon">
{% if media.content_type == 'image' %}
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="Image" style="width: 48px; height: 48px; opacity: 0.5;">
{% elif media.content_type == 'video' %}
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="Video" style="width: 48px; height: 48px; opacity: 0.5;">
{% elif media.content_type == 'pdf' %}
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="PDF" style="width: 48px; height: 48px; opacity: 0.5;">
{% else %}
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="File" style="width: 48px; height: 48px; opacity: 0.5;">
{% endif %}
</div>
<div class="media-name">{{ media.filename[:20] }}...</div>
{% if media.content_type == 'image' %}
<div class="media-thumbnail" style="width: 100%; height: 100px; overflow: hidden; border-radius: 6px; margin-bottom: 8px; background: #f0f0f0; display: flex; align-items: center; justify-content: center;">
<img src="{{ url_for('static', filename='uploads/' + media.filename) }}"
alt="{{ media.filename }}"
style="max-width: 100%; max-height: 100%; object-fit: cover;"
onerror="this.style.display='none'; this.parentElement.innerHTML='<span style=\'font-size: 48px;\'>📷</span>'">
</div>
{% elif media.content_type == 'video' %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
🎥
</div>
{% elif media.content_type == 'pdf' %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
📄
</div>
{% else %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
📁
</div>
{% endif %}
<div class="media-name" style="font-size: 11px; line-height: 1.3;">{{ media.filename[:25] }}{% if media.filename|length > 25 %}...{% endif %}</div>
<div style="font-size: 10px; color: #999; margin-top: 4px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
</div>
{% endfor %}
{% else %}
<div style="text-align: center; padding: 20px; color: #999;">
<div style="text-align: center; padding: 40px; color: #999; grid-column: 1 / -1;">
<div style="font-size: 48px; margin-bottom: 10px;">📭</div>
<p>No media files yet. Upload your first file!</p>
</div>
{% endif %}
</div>
{% if media_files|length > 12 %}
<p style="text-align: center; margin-top: 15px; color: #999;">
+ {{ media_files|length - 12 }} more files
</p>
{% endif %}
</div>
</div>