2014-07-19 08:40:52 +02:00
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import argparse
|
|
|
|
|
import errno
|
|
|
|
|
import time
|
2025-06-27 09:01:51 -06:00
|
|
|
import subprocess
|
2014-07-19 08:40:52 +02:00
|
|
|
from PIL import Image
|
|
|
|
|
from multiprocessing import Pool
|
|
|
|
|
from multiprocessing import Value
|
2025-06-27 09:01:51 -06:00
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
import io
|
|
|
|
|
import struct
|
2014-07-19 08:40:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class State(object):
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.counter = Value('i', 0)
|
|
|
|
|
self.start_ticks = Value('d', time.process_time())
|
|
|
|
|
|
|
|
|
|
def increment(self, n=1):
|
|
|
|
|
with self.counter.get_lock():
|
|
|
|
|
self.counter.value += n
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def value(self):
|
|
|
|
|
return self.counter.value
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def start(self):
|
|
|
|
|
return self.start_ticks.value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init(args):
|
|
|
|
|
global state
|
|
|
|
|
state = args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
args = parse_args()
|
|
|
|
|
state = State()
|
|
|
|
|
|
|
|
|
|
files = find_files(args.directory)
|
|
|
|
|
|
|
|
|
|
with Pool(processes=4, initializer=init, initargs=(state, )) as pool:
|
|
|
|
|
pool.map(process_file, files)
|
|
|
|
|
|
|
|
|
|
print("{0} files processed in total.".format(state.value))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_args():
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
description="Create thumbnails for Synology Photo Station.")
|
|
|
|
|
parser.add_argument("--directory", required=True,
|
|
|
|
|
help="Directory to generate thumbnails for. "
|
|
|
|
|
"Subdirectories will always be processed.")
|
|
|
|
|
|
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_files(dir):
|
2025-06-27 09:01:51 -06:00
|
|
|
# Only process formats that Synology doesn't handle well
|
|
|
|
|
# Exclude common images (jpg, png, gif, etc.) since NAS handles them fine
|
|
|
|
|
valid_exts = ('mp4', 'webm', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'm4v', 'ts',
|
|
|
|
|
'psd', 'blend')
|
2014-07-19 08:40:52 +02:00
|
|
|
valid_exts_re = "|".join(
|
|
|
|
|
map((lambda ext: ".*\\.{0}$".format(ext)), valid_exts))
|
|
|
|
|
|
|
|
|
|
for root, dirs, files in os.walk(dir):
|
|
|
|
|
for name in files:
|
|
|
|
|
if re.match(valid_exts_re, name, re.IGNORECASE) \
|
2025-06-27 09:01:51 -06:00
|
|
|
and not name.startswith('SYNOPHOTO_THUMB') \
|
|
|
|
|
and not name.startswith('SYNOVIDEO_VIDEO_SCREENSHOT'):
|
2014-07-19 08:40:52 +02:00
|
|
|
yield os.path.join(root, name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_progress():
|
|
|
|
|
global state
|
|
|
|
|
state.increment(1)
|
|
|
|
|
processed = state.value
|
|
|
|
|
if processed % 10 == 0:
|
2025-06-27 09:01:51 -06:00
|
|
|
elapsed = float(time.process_time() - state.start)
|
|
|
|
|
rate = float(processed) / elapsed if elapsed > 0 else float(processed)
|
2014-07-19 08:40:52 +02:00
|
|
|
print("{0} files processed so far, averaging {1:.2f} files per second."
|
2025-06-27 09:01:51 -06:00
|
|
|
.format(processed, rate))
|
2014-07-19 08:40:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_file(file_path):
|
|
|
|
|
print(file_path)
|
|
|
|
|
|
|
|
|
|
(dir, filename) = os.path.split(file_path)
|
|
|
|
|
thumb_dir = os.path.join(dir, 'eaDir_tmp', filename)
|
|
|
|
|
ensure_directory_exists(thumb_dir)
|
|
|
|
|
|
2025-06-27 09:01:51 -06:00
|
|
|
# Determine file type and process accordingly
|
|
|
|
|
file_ext = os.path.splitext(filename)[1].lower()
|
|
|
|
|
|
|
|
|
|
if file_ext in ['.mp4', '.webm', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.m4v', '.ts']:
|
|
|
|
|
create_video_thumbnails(file_path, thumb_dir)
|
|
|
|
|
elif file_ext in ['.psd']:
|
|
|
|
|
create_psd_thumbnails(file_path, thumb_dir)
|
|
|
|
|
elif file_ext in ['.blend']:
|
|
|
|
|
create_blend_thumbnails(file_path, thumb_dir)
|
|
|
|
|
else:
|
|
|
|
|
# Standard image processing
|
|
|
|
|
create_thumbnails(file_path, thumb_dir)
|
2014-07-19 08:40:52 +02:00
|
|
|
|
|
|
|
|
print_progress()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_directory_exists(path):
|
|
|
|
|
try:
|
|
|
|
|
os.makedirs(path)
|
|
|
|
|
except OSError as exception:
|
|
|
|
|
if exception.errno != errno.EEXIST:
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
2025-06-27 09:01:51 -06:00
|
|
|
def create_video_thumbnails(source_path, dest_dir):
|
2025-06-29 18:15:35 -06:00
|
|
|
"""Generate video thumbnails using Windows provider first, then FFmpeg fallback"""
|
|
|
|
|
# Try Windows thumbnail extraction first (much faster!)
|
|
|
|
|
windows_thumb = extract_windows_thumbnail(source_path)
|
|
|
|
|
if windows_thumb:
|
|
|
|
|
generate_synology_thumbnails(windows_thumb, dest_dir, include_video_screenshot=True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Fallback to FFmpeg if Windows extraction fails
|
|
|
|
|
print(f"Windows thumbnail extraction failed for {source_path}, using FFmpeg...")
|
|
|
|
|
create_video_thumbnails_ffmpeg(source_path, dest_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_video_thumbnails_ffmpeg(source_path, dest_dir):
|
|
|
|
|
"""Generate video thumbnails using FFmpeg (fallback method)"""
|
2025-06-27 09:01:51 -06:00
|
|
|
to_generate = (('SYNOVIDEO_VIDEO_SCREENSHOT.jpg', 1280),
|
|
|
|
|
('SYNOPHOTO_THUMB_XL.jpg', 1280),
|
|
|
|
|
('SYNOPHOTO_THUMB_B.jpg', 640),
|
|
|
|
|
('SYNOPHOTO_THUMB_M.jpg', 320),
|
|
|
|
|
('SYNOPHOTO_THUMB_PREVIEW.jpg', 160),
|
|
|
|
|
('SYNOPHOTO_THUMB_S.jpg', 120))
|
|
|
|
|
|
|
|
|
|
for thumb in to_generate:
|
|
|
|
|
thumb_path = os.path.join(dest_dir, thumb[0])
|
|
|
|
|
if os.path.exists(thumb_path):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Use FFmpeg to extract a frame from the video at 10% duration
|
|
|
|
|
cmd = [
|
|
|
|
|
'ffmpeg', '-i', source_path,
|
|
|
|
|
'-ss', '00:00:05', # Seek to 5 seconds
|
|
|
|
|
'-vframes', '1',
|
|
|
|
|
'-vf', f'scale={thumb[1]}:{thumb[1]}:force_original_aspect_ratio=decrease',
|
|
|
|
|
'-y', # Overwrite output file
|
|
|
|
|
thumb_path
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
print(f"Warning: FFmpeg failed for {source_path}: {result.stderr}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
print("Warning: FFmpeg not found. Video thumbnails will be skipped.")
|
|
|
|
|
print("Install FFmpeg: https://ffmpeg.org/download.html")
|
|
|
|
|
break
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error processing video {source_path}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_psd_thumbnails(source_path, dest_dir):
|
2025-06-29 18:15:35 -06:00
|
|
|
"""Generate PSD thumbnails using Windows/Icaros provider first, then psd-tools fallback"""
|
|
|
|
|
# Try Windows thumbnail extraction first (uses Icaros or built-in PSD support)
|
|
|
|
|
windows_thumb = extract_windows_thumbnail(source_path)
|
|
|
|
|
if windows_thumb:
|
|
|
|
|
generate_synology_thumbnails(windows_thumb, dest_dir)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Fallback to psd-tools if Windows extraction fails
|
|
|
|
|
print(f"Windows thumbnail extraction failed for {source_path}, using psd-tools...")
|
|
|
|
|
create_psd_thumbnails_direct(source_path, dest_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_psd_thumbnails_direct(source_path, dest_dir):
|
|
|
|
|
"""Generate PSD thumbnails using PIL with PSD support (fallback method)"""
|
2025-06-27 09:01:51 -06:00
|
|
|
try:
|
|
|
|
|
# Try to open PSD file - requires pillow-psd plugin
|
|
|
|
|
from psd_tools import PSDImage
|
|
|
|
|
psd = PSDImage.open(source_path)
|
|
|
|
|
pil_image = psd.composite()
|
|
|
|
|
|
|
|
|
|
to_generate = (('SYNOPHOTO_THUMB_XL.jpg', 1280),
|
|
|
|
|
('SYNOPHOTO_THUMB_B.jpg', 640),
|
|
|
|
|
('SYNOPHOTO_THUMB_M.jpg', 320),
|
|
|
|
|
('SYNOPHOTO_THUMB_PREVIEW.jpg', 160),
|
|
|
|
|
('SYNOPHOTO_THUMB_S.jpg', 120))
|
|
|
|
|
|
|
|
|
|
for thumb in to_generate:
|
|
|
|
|
thumb_path = os.path.join(dest_dir, thumb[0])
|
|
|
|
|
if os.path.exists(thumb_path):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
pil_image.thumbnail((thumb[1], thumb[1]), Image.LANCZOS)
|
|
|
|
|
# Convert to RGB if needed for JPEG
|
|
|
|
|
if pil_image.mode == 'RGBA':
|
|
|
|
|
pil_image = pil_image.convert('RGB')
|
|
|
|
|
pil_image.save(thumb_path, 'JPEG')
|
|
|
|
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
print("Warning: psd-tools not installed. Install with: pip install psd-tools")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error processing PSD {source_path}: {e}")
|
|
|
|
|
|
|
|
|
|
|
2025-06-29 18:15:35 -06:00
|
|
|
def generate_synology_thumbnails(image, dest_dir, include_video_screenshot=False):
|
|
|
|
|
"""Generate all required Synology thumbnail sizes from a PIL image"""
|
|
|
|
|
to_generate = [
|
|
|
|
|
('SYNOPHOTO_THUMB_XL.jpg', 1280),
|
|
|
|
|
('SYNOPHOTO_THUMB_B.jpg', 640),
|
|
|
|
|
('SYNOPHOTO_THUMB_M.jpg', 320),
|
|
|
|
|
('SYNOPHOTO_THUMB_PREVIEW.jpg', 160),
|
|
|
|
|
('SYNOPHOTO_THUMB_S.jpg', 120)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Add video screenshot for video files
|
|
|
|
|
if include_video_screenshot:
|
|
|
|
|
to_generate.insert(0, ('SYNOVIDEO_VIDEO_SCREENSHOT.jpg', 1280))
|
|
|
|
|
|
|
|
|
|
for thumb_name, thumb_size in to_generate:
|
|
|
|
|
thumb_path = os.path.join(dest_dir, thumb_name)
|
|
|
|
|
if os.path.exists(thumb_path):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Create a copy for thumbnailing
|
|
|
|
|
img_copy = image.copy()
|
|
|
|
|
img_copy.thumbnail((thumb_size, thumb_size), Image.LANCZOS)
|
|
|
|
|
|
|
|
|
|
# Convert to RGB if needed for JPEG
|
|
|
|
|
if img_copy.mode == 'RGBA':
|
|
|
|
|
img_copy = img_copy.convert('RGB')
|
|
|
|
|
|
|
|
|
|
img_copy.save(thumb_path, 'JPEG', quality=85)
|
|
|
|
|
|
|
|
|
|
|
2025-06-27 09:01:51 -06:00
|
|
|
def extract_blend_thumbnail(blend_path):
|
2025-06-29 18:15:35 -06:00
|
|
|
"""Extract thumbnail from .blend file using Windows thumbnail provider or direct extraction."""
|
|
|
|
|
|
|
|
|
|
# Option 1: Try using Windows thumbnail extraction first (fastest)
|
|
|
|
|
try:
|
|
|
|
|
temp_thumb = extract_windows_thumbnail(blend_path)
|
|
|
|
|
if temp_thumb:
|
|
|
|
|
return temp_thumb
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Option 2: Direct .blend file parsing (more reliable)
|
|
|
|
|
try:
|
|
|
|
|
return extract_blend_thumbnail_direct(blend_path)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Failed to extract .blend thumbnail: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
2025-06-27 09:01:51 -06:00
|
|
|
|
2025-06-29 18:15:35 -06:00
|
|
|
def extract_windows_thumbnail(file_path):
|
|
|
|
|
"""Extract thumbnail from adjacent Thumbs.db file."""
|
|
|
|
|
try:
|
|
|
|
|
# Look for Thumbs.db in the same directory
|
|
|
|
|
directory = os.path.dirname(file_path)
|
|
|
|
|
filename = os.path.basename(file_path)
|
|
|
|
|
thumbs_db_path = os.path.join(directory, 'Thumbs.db')
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(thumbs_db_path):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return extract_from_thumbs_db(thumbs_db_path, filename)
|
|
|
|
|
|
|
|
|
|
except Exception:
|
2025-06-27 09:01:51 -06:00
|
|
|
return None
|
2025-06-29 18:15:35 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_from_thumbs_db(thumbs_db_path, target_filename):
|
|
|
|
|
"""Extract specific file thumbnail from Thumbs.db"""
|
|
|
|
|
try:
|
|
|
|
|
import olefile
|
|
|
|
|
|
|
|
|
|
if not olefile.isOleFile(thumbs_db_path):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
ole = olefile.OleFileIO(thumbs_db_path)
|
|
|
|
|
|
|
|
|
|
# Get list of streams
|
|
|
|
|
all_streams = ole.listdir()
|
|
|
|
|
|
|
|
|
|
# Thumbs.db stores thumbnails with their filename as the stream name
|
|
|
|
|
# Try different variations of the filename
|
|
|
|
|
stream_names = []
|
|
|
|
|
base_name = os.path.splitext(target_filename)[0]
|
|
|
|
|
|
|
|
|
|
# Common patterns for thumbnail stream names in Thumbs.db
|
|
|
|
|
stream_names.extend([
|
|
|
|
|
target_filename,
|
|
|
|
|
target_filename.lower(),
|
|
|
|
|
target_filename.upper(),
|
|
|
|
|
base_name,
|
|
|
|
|
base_name.lower(),
|
|
|
|
|
base_name.upper(),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
# First try filename-based matching (older Thumbs.db format)
|
|
|
|
|
for stream_name in ole.listdir():
|
|
|
|
|
stream_path = '/'.join(stream_name) if isinstance(stream_name, list) else stream_name
|
|
|
|
|
|
|
|
|
|
# Check if this stream corresponds to our target file
|
|
|
|
|
for candidate in stream_names:
|
|
|
|
|
if candidate in stream_path or stream_path.endswith(candidate):
|
|
|
|
|
img = extract_thumbnail_from_stream(ole, stream_name)
|
|
|
|
|
if img:
|
|
|
|
|
ole.close()
|
|
|
|
|
return img
|
|
|
|
|
|
|
|
|
|
# If filename matching failed, try hash-based extraction (newer format)
|
|
|
|
|
for stream_name in ole.listdir():
|
|
|
|
|
stream_path = '/'.join(stream_name) if isinstance(stream_name, list) else stream_name
|
|
|
|
|
|
|
|
|
|
# Look for thumbnail streams (usually start with size like "256_")
|
|
|
|
|
if stream_path.startswith(('256_', '96_', '32_', 'Thumbnail')):
|
|
|
|
|
img = extract_thumbnail_from_stream(ole, stream_name)
|
|
|
|
|
if img:
|
|
|
|
|
ole.close()
|
|
|
|
|
return img
|
|
|
|
|
ole.close()
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
print("Warning: olefile not installed. Install with: pip install olefile")
|
|
|
|
|
return None
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Debug: Thumbs.db extraction error: {e}")
|
2025-06-27 09:01:51 -06:00
|
|
|
return None
|
2025-06-29 18:15:35 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_thumbnail_from_stream(ole, stream_name):
|
|
|
|
|
"""Helper function to extract thumbnail from a Thumbs.db stream"""
|
|
|
|
|
try:
|
|
|
|
|
# Use the original list format as returned by ole.listdir()
|
|
|
|
|
with ole.openstream(stream_name) as stream:
|
|
|
|
|
# Read the full stream data to analyze the header structure
|
|
|
|
|
stream.seek(0)
|
|
|
|
|
full_data = stream.read()
|
|
|
|
|
|
|
|
|
|
# Try different header offsets for Thumbs.db format
|
|
|
|
|
# Common offsets: 12, 16, 20, 24, 32
|
|
|
|
|
for offset in [12, 16, 20, 24, 32]:
|
|
|
|
|
try:
|
|
|
|
|
thumbnail_data = full_data[offset:]
|
|
|
|
|
if len(thumbnail_data) > 0:
|
|
|
|
|
img = Image.open(io.BytesIO(thumbnail_data))
|
|
|
|
|
return img
|
|
|
|
|
except Exception:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# If standard offsets fail, try to find JPEG/PNG signatures
|
|
|
|
|
# Look for JPEG signature (FF D8 FF)
|
|
|
|
|
jpeg_start = full_data.find(b'\xff\xd8\xff')
|
|
|
|
|
if jpeg_start != -1:
|
|
|
|
|
try:
|
|
|
|
|
img = Image.open(io.BytesIO(full_data[jpeg_start:]))
|
|
|
|
|
return img
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Look for PNG signature (89 50 4E 47)
|
|
|
|
|
png_start = full_data.find(b'\x89PNG')
|
|
|
|
|
if png_start != -1:
|
|
|
|
|
try:
|
|
|
|
|
img = Image.open(io.BytesIO(full_data[png_start:]))
|
|
|
|
|
return img
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_blend_thumbnail_direct(blend_path):
|
|
|
|
|
"""Direct extraction of embedded thumbnail from .blend file."""
|
|
|
|
|
import struct
|
|
|
|
|
|
|
|
|
|
with open(blend_path, 'rb') as f:
|
|
|
|
|
# Read header
|
|
|
|
|
header = f.read(12)
|
|
|
|
|
if not header.startswith(b'BLENDER'):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# Determine architecture and endianness
|
|
|
|
|
pointer_size = 8 if header[7:8] == b'-' else 4
|
|
|
|
|
endian = '<' if header[8:9] == b'v' else '>'
|
|
|
|
|
|
|
|
|
|
# Find the largest embedded preview image
|
|
|
|
|
best_preview = None
|
|
|
|
|
best_size = 0
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
# Read block header
|
|
|
|
|
block_header = f.read(16 + pointer_size)
|
|
|
|
|
if len(block_header) < 16 + pointer_size:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
code = block_header[:4]
|
|
|
|
|
size = struct.unpack(endian + 'I', block_header[4:8])[0]
|
|
|
|
|
|
|
|
|
|
if code == b'PREV':
|
|
|
|
|
# Read preview block data
|
|
|
|
|
preview_data = f.read(size)
|
|
|
|
|
if len(preview_data) < size:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# Look for PNG signature
|
|
|
|
|
png_start = preview_data.find(b'\x89PNG\r\n\x1a\n')
|
|
|
|
|
if png_start != -1:
|
|
|
|
|
# Find the end of this PNG
|
|
|
|
|
png_end = preview_data.find(b'IEND', png_start)
|
|
|
|
|
if png_end != -1:
|
|
|
|
|
png_end += 8 # Include IEND + CRC
|
|
|
|
|
png_data = preview_data[png_start:png_end]
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
img = Image.open(io.BytesIO(png_data))
|
|
|
|
|
img_size = img.size[0] * img.size[1]
|
|
|
|
|
|
|
|
|
|
# Keep the largest preview
|
|
|
|
|
if img_size > best_size:
|
|
|
|
|
best_preview = img.copy()
|
|
|
|
|
best_size = img_size
|
|
|
|
|
except Exception:
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
# Skip this block
|
|
|
|
|
f.seek(size, 1)
|
|
|
|
|
|
|
|
|
|
return best_preview
|
2025-06-27 09:01:51 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_blend_thumbnails(source_path, dest_dir):
|
|
|
|
|
"""Extract embedded Blender thumbnail and generate Synology thumbnails."""
|
|
|
|
|
img = extract_blend_thumbnail(source_path)
|
|
|
|
|
if img is None:
|
|
|
|
|
print(f"No embedded thumbnail in {source_path}")
|
|
|
|
|
return
|
2014-07-19 08:40:52 +02:00
|
|
|
|
2025-06-29 18:15:35 -06:00
|
|
|
generate_synology_thumbnails(img, dest_dir)
|
2014-07-19 08:40:52 +02:00
|
|
|
|
2025-06-27 09:01:51 -06:00
|
|
|
|
|
|
|
|
def create_thumbnails(source_path, dest_dir):
|
2025-06-29 18:15:35 -06:00
|
|
|
"""Image thumbnail creation using Windows provider first, then PIL fallback"""
|
|
|
|
|
# Try Windows thumbnail extraction first
|
|
|
|
|
windows_thumb = extract_windows_thumbnail(source_path)
|
|
|
|
|
if windows_thumb:
|
|
|
|
|
generate_synology_thumbnails(windows_thumb, dest_dir)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Fallback to PIL for basic image processing
|
|
|
|
|
create_thumbnails_pil(source_path, dest_dir)
|
2025-06-27 09:01:51 -06:00
|
|
|
|
|
|
|
|
|
2025-06-29 18:15:35 -06:00
|
|
|
def create_thumbnails_pil(source_path, dest_dir):
|
|
|
|
|
"""Original PIL-based image thumbnail creation (fallback method)"""
|
|
|
|
|
try:
|
|
|
|
|
im = Image.open(source_path)
|
|
|
|
|
generate_synology_thumbnails(im, dest_dir)
|
2025-06-27 09:01:51 -06:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error processing image {source_path}: {e}")
|
2014-07-19 08:40:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|