Files
2026-01-07 19:14:21 +01:00

254 lines
8.7 KiB
Python

#!/usr/bin/env python3
import os
import glob
import shutil
import subprocess
import yaml
import sys
from PIL import Image
# --- 1. Load Configuration ---
def load_config(config_path="config.yml"):
if not os.path.exists(config_path):
print(f"❌ Error: Configuration file '{config_path}' not found.")
sys.exit(1)
with open(config_path, 'r') as file:
return yaml.safe_load(file)
# --- 2. System Checks ---
def check_dependencies():
tools = ["ffmpeg", "pdftoppm"]
missing = []
for tool in tools:
if not shutil.which(tool):
missing.append(tool)
if missing:
print(f"❌ Error: Missing required tools: {', '.join(missing)}")
print(" Please install them: apt install ffmpeg poppler-utils")
sys.exit(1)
def main():
print("🚀 Starting PDF to Scroll Video Converter...")
# Load settings
cfg = load_config()
INPUT_DIR = cfg['input_folder']
CACHE_DIR = cfg['cache_folder']
OUTPUT_FILE = cfg['output_filename']
WIDTH = cfg['resolution']['width']
HEIGHT = cfg['resolution']['height']
SEC_PER_SLIDE = cfg['timing']['seconds_per_slide']
FPS = cfg['timing']['fps']
check_dependencies()
# Clean and create directories
if os.path.exists(CACHE_DIR):
shutil.rmtree(CACHE_DIR)
os.makedirs(CACHE_DIR)
# Find PDFs
pdf_files = sorted(glob.glob(os.path.join(INPUT_DIR, "*.pdf")))
if not pdf_files:
print("❌ No PDF files found in the input folder.")
sys.exit(1)
print(f"📂 Found {len(pdf_files)} PDF(s). Processing...")
# --- Step 1: Convert PDFs to Images ---
global_counter = 0
for pdf in pdf_files:
print(f" 📄 Processing: {os.path.basename(pdf)}")
cmd = f"pdftoppm -png -r 150 \"{pdf}\" \"{CACHE_DIR}/temp\""
subprocess.run(cmd, shell=True, check=True,
stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
temp_files = sorted(glob.glob(os.path.join(CACHE_DIR, "temp-*.png")))
for temp_img in temp_files:
new_name = os.path.join(CACHE_DIR, f"slide_{global_counter:05d}.png")
os.rename(temp_img, new_name)
global_counter += 1
# --- Step 2: Process Images ---
print("📐 Processing images...")
all_images = sorted(glob.glob(os.path.join(CACHE_DIR, "slide_*.png")))
processed_dir = os.path.join(CACHE_DIR, "processed")
os.makedirs(processed_dir, exist_ok=True)
# Resize all images to target width
processed_images = []
total_height = 0
for i, img_path in enumerate(all_images):
img = Image.open(img_path)
# Resize to target width while maintaining aspect ratio
aspect_ratio = img.height / img.width
new_height = int(WIDTH * aspect_ratio)
img_resized = img.resize((WIDTH, new_height), Image.LANCZOS)
output_path = os.path.join(processed_dir, f"frame_{i:05d}.png")
img_resized.save(output_path, "PNG", optimize=True)
processed_images.append(output_path)
total_height += new_height
img.close()
if (i + 1) % 10 == 0:
print(f" ... Processed {i + 1}/{len(all_images)} images")
total_slides = len(processed_images)
total_duration = total_slides * SEC_PER_SLIDE
print(f"\n📊 Statistics:")
print(f" Total Slides: {total_slides}")
print(f" Combined Height: {total_height:,}px")
print(f" Video Duration: {total_duration} seconds")
print(f" Viewport: {WIDTH}x{HEIGHT}px")
# --- Step 3: Create Stitched Image in Chunks ---
print("\n🧩 Creating scrollable canvas...")
# For very long documents, we need to split into manageable chunks
MAX_CHUNK_HEIGHT = 65000 # Safe limit for PNG
chunk_images = []
current_chunk = []
current_chunk_height = 0
chunk_num = 0
for img_path in processed_images:
with Image.open(img_path) as img:
img_height = img.height
# Check if adding this image would exceed chunk limit
if current_chunk_height + img_height > MAX_CHUNK_HEIGHT and current_chunk:
# Save current chunk
chunk_path = os.path.join(CACHE_DIR, f"chunk_{chunk_num:03d}.png")
# Stitch current chunk
total_h = sum(Image.open(p).height for p in current_chunk)
canvas = Image.new('RGB', (WIDTH, total_h), (255, 255, 255))
y_offset = 0
for chunk_img_path in current_chunk:
with Image.open(chunk_img_path) as ci:
canvas.paste(ci, (0, y_offset))
y_offset += ci.height
canvas.save(chunk_path, "PNG", optimize=True)
canvas.close()
chunk_images.append(chunk_path)
print(f" ... Created chunk {chunk_num + 1} ({current_chunk_height:,}px)")
# Reset for next chunk
current_chunk = []
current_chunk_height = 0
chunk_num += 1
current_chunk.append(img_path)
current_chunk_height += img_height
# Save final chunk
if current_chunk:
chunk_path = os.path.join(CACHE_DIR, f"chunk_{chunk_num:03d}.png")
total_h = sum(Image.open(p).height for p in current_chunk)
canvas = Image.new('RGB', (WIDTH, total_h), (255, 255, 255))
y_offset = 0
for chunk_img_path in current_chunk:
with Image.open(chunk_img_path) as ci:
canvas.paste(ci, (0, y_offset))
y_offset += ci.height
canvas.save(chunk_path, "PNG", optimize=True)
canvas.close()
chunk_images.append(chunk_path)
print(f" ... Created chunk {chunk_num + 1} ({current_chunk_height:,}px)")
# --- Step 4: Create Video from Chunks ---
print("\n🎥 Encoding video with smooth scroll effect...")
if len(chunk_images) == 1:
# Simple case: one chunk
big_image = chunk_images[0]
ffmpeg_cmd = (
f"ffmpeg -y -v error -stats "
f"-loop 1 -i \"{big_image}\" "
f"-vf \"crop={WIDTH}:{HEIGHT}:0:'min(ih-oh,(ih-oh)*t/{total_duration})',format=yuv420p\" "
f"-t {total_duration} "
f"-c:v libx264 -preset medium -crf 23 -r {FPS} "
f"\"{OUTPUT_FILE}\""
)
result = os.system(ffmpeg_cmd)
else:
# Multiple chunks: process each and concatenate
print(f" Processing {len(chunk_images)} chunks...")
chunk_videos = []
time_per_chunk = total_duration / len(chunk_images)
for i, chunk_path in enumerate(chunk_images):
chunk_video = os.path.join(CACHE_DIR, f"chunk_video_{i:03d}.mp4")
ffmpeg_cmd = (
f"ffmpeg -y -v error -stats "
f"-loop 1 -i \"{chunk_path}\" "
f"-vf \"crop={WIDTH}:{HEIGHT}:0:'min(ih-oh,(ih-oh)*t/{time_per_chunk})',format=yuv420p\" "
f"-t {time_per_chunk} "
f"-c:v libx264 -preset medium -crf 23 -r {FPS} "
f"\"{chunk_video}\""
)
print(f" ... Encoding chunk {i + 1}/{len(chunk_images)}")
result = os.system(ffmpeg_cmd)
if result != 0:
print(f"❌ Error encoding chunk {i + 1}")
sys.exit(1)
chunk_videos.append(chunk_video)
# Concatenate chunks
print(" ... Concatenating chunks...")
concat_file = os.path.join(CACHE_DIR, "concat_list.txt")
with open(concat_file, 'w') as f:
for cv in chunk_videos:
f.write(f"file '{os.path.abspath(cv)}'\n")
ffmpeg_cmd = (
f"ffmpeg -y -v error -stats "
f"-f concat -safe 0 -i \"{concat_file}\" "
f"-c copy "
f"\"{OUTPUT_FILE}\""
)
result = os.system(ffmpeg_cmd)
if result != 0:
print("❌ Error during ffmpeg encoding.")
sys.exit(1)
print("\n" + "="*60)
print(f"✅ SUCCESS! Video saved as: {OUTPUT_FILE}")
print(f" Duration: {total_duration}s | Resolution: {WIDTH}x{HEIGHT}")
print(f" Frames: {total_duration * FPS:,.0f} | Size: {os.path.getsize(OUTPUT_FILE) / 1024 / 1024:.1f} MB")
print("="*60)
# Optional: cleanup
print("\n💡 Tip: Delete the './tmp' folder to free up disk space")
if __name__ == "__main__":
main()