mirror of
https://github.com/Gathin23/cleanyourtv.git
synced 2026-11-07 10:46:07 -10:00
158 lines
7.3 KiB
Python
158 lines
7.3 KiB
Python
"""Turns a folder of your own photos into wallpapers the launcher can show on a small TV.
|
|
|
|
python tools/wallpaper_prep.py ~/Pictures/trip /tmp/my-theme/wallpapers
|
|
|
|
Every JPEG, PNG, WebP and (with pillow-heif) HEIC file in the first folder is centre-cropped to 16:9, shrunk to
|
|
1280x720, darkened so the clock, the grid and the dock stay readable, and saved as 01.jpg, 02.jpg, ... in the
|
|
second folder. The originals are never touched: a target that is the source folder (by any spelling or
|
|
link), or an 01.jpg there that is a symlink or hard link to one of your photos, stops the run before
|
|
anything is written. Each picture is kept under --max-kb (300 by default, what
|
|
themes/README.md asks for) by lowering the JPEG quality a step at a time.
|
|
|
|
The darkening is painted into the picture, so wallpaper_dim can stay at 0 and the launcher draws no shade
|
|
layer. The numbers that worked on a Xiaomi Mi Box S (2 GB) are the defaults: brightness 0.25 and a gradient
|
|
that adds up to 60% black toward the bottom, where the dock sits.
|
|
|
|
Needs Pillow; HEIC (iPhone and many Android cameras) also needs pillow-heif:
|
|
|
|
python -m pip install -r tools/requirements-photos.txt
|
|
|
|
Use pictures you have the right to show.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif"}
|
|
|
|
|
|
def parse_size(text):
|
|
try:
|
|
w, h = text.lower().split("x")
|
|
w, h = int(w), int(h)
|
|
except ValueError:
|
|
raise argparse.ArgumentTypeError("size must look like 1280x720")
|
|
if w < 16 or h < 9 or w > 7680 or h > 4320:
|
|
raise argparse.ArgumentTypeError("size must be between 16x9 and 7680x4320")
|
|
return w, h
|
|
|
|
|
|
def source_files(folder):
|
|
"""Picture files directly inside folder, in name order (so the numbering is stable)."""
|
|
names = sorted(n for n in os.listdir(folder) if os.path.splitext(n)[1].lower() in EXTENSIONS)
|
|
return [os.path.join(folder, n) for n in names if os.path.isfile(os.path.join(folder, n))]
|
|
|
|
|
|
def overlapping_outputs(files, target):
|
|
"""Names of the numbered outputs in target that already are, or link to, one of the source files.
|
|
|
|
A symlink or hardlink called 01.jpg in the target would make the save overwrite an original, and
|
|
one pointing at a later photo would do it before that photo's turn, so every planned name is
|
|
checked against every source file up front, by (device, inode), before anything is written.
|
|
"""
|
|
identities = {(info.st_dev, info.st_ino) for info in map(os.stat, files)}
|
|
clashes = []
|
|
for number in range(1, len(files) + 1):
|
|
name = "%02d.jpg" % number
|
|
try:
|
|
info = os.stat(os.path.join(target, name))
|
|
except OSError:
|
|
continue
|
|
if (info.st_dev, info.st_ino) in identities:
|
|
clashes.append(name)
|
|
return clashes
|
|
|
|
|
|
def darken(image, brightness, gradient):
|
|
"""Scale brightness, then blend toward black from top (0) to bottom (gradient)."""
|
|
from PIL import Image, ImageEnhance
|
|
image = ImageEnhance.Brightness(image).enhance(brightness)
|
|
if gradient > 0:
|
|
mask = Image.linear_gradient("L").resize(image.size).point(lambda v: int(v * gradient))
|
|
image = Image.composite(Image.new("RGB", image.size, (0, 0, 0)), image, mask)
|
|
return image
|
|
|
|
|
|
def prepare(path, out_path, size, brightness, gradient, quality, max_kb):
|
|
"""Writes one wallpaper and returns (size in KB, JPEG quality used)."""
|
|
from PIL import Image, ImageOps
|
|
with Image.open(path) as opened:
|
|
image = ImageOps.exif_transpose(opened).convert("RGB")
|
|
image = ImageOps.fit(image, size, Image.LANCZOS, centering=(0.5, 0.5))
|
|
image = darken(image, brightness, gradient)
|
|
q = quality
|
|
while True:
|
|
image.save(out_path, "JPEG", quality=q, optimize=True)
|
|
kb = os.path.getsize(out_path) / 1024
|
|
if kb <= max_kb or q <= 50:
|
|
return round(kb), q
|
|
q -= 6
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description="Resize and darken photos for the launcher's wallpaper folder.")
|
|
parser.add_argument("source", help="folder with your photos")
|
|
parser.add_argument("target", help="folder to write 01.jpg, 02.jpg, ... into (created if missing)")
|
|
parser.add_argument("--size", type=parse_size, default=(1280, 720), help="default 1280x720")
|
|
parser.add_argument("--brightness", type=float, default=0.25, help="0.1 to 1.0, default 0.25")
|
|
parser.add_argument("--gradient", type=float, default=0.6, help="extra black toward the bottom, 0 to 0.9, default 0.6")
|
|
parser.add_argument("--quality", type=int, default=84, help="starting JPEG quality, default 84")
|
|
parser.add_argument("--max-kb", type=int, default=300, help="size limit per picture, default 300")
|
|
args = parser.parse_args(argv)
|
|
|
|
if not 0.1 <= args.brightness <= 1.0:
|
|
parser.error("--brightness must be between 0.1 and 1.0")
|
|
if not 0 <= args.gradient <= 0.9:
|
|
parser.error("--gradient must be between 0 and 0.9")
|
|
if not 30 <= args.quality <= 95:
|
|
parser.error("--quality must be between 30 and 95")
|
|
if not os.path.isdir(args.source):
|
|
parser.error(f"{args.source} is not a folder")
|
|
source_real = os.path.realpath(args.source)
|
|
target_real = os.path.realpath(args.target)
|
|
if target_real == source_real or target_real.startswith(source_real.rstrip(os.sep) + os.sep):
|
|
parser.error("the target must be a different folder, and not inside the source: the originals are never overwritten")
|
|
if os.path.exists(args.target) and os.path.samefile(args.source, args.target):
|
|
parser.error("the target must be a different folder, and not inside the source: the originals are never overwritten")
|
|
|
|
try:
|
|
import PIL # noqa: F401
|
|
except ImportError:
|
|
sys.exit("Pillow is missing: python -m pip install -r tools/requirements-photos.txt")
|
|
try:
|
|
import pillow_heif
|
|
pillow_heif.register_heif_opener()
|
|
except ImportError:
|
|
pillow_heif = None
|
|
|
|
files = source_files(args.source)
|
|
if not files:
|
|
sys.exit(f"no JPEG, PNG, WebP or HEIC pictures in {args.source}")
|
|
clashes = overlapping_outputs(files, args.target)
|
|
if clashes:
|
|
sys.exit(f"{', '.join(clashes)} in {args.target} would overwrite your original photos "
|
|
"(a link or the same file); remove or rename them, or pick another target. Nothing was written.")
|
|
os.makedirs(args.target, exist_ok=True)
|
|
|
|
written = 0
|
|
for number, path in enumerate(files, 1):
|
|
name = os.path.basename(path)
|
|
if os.path.splitext(name)[1].lower() in {".heic", ".heif"} and pillow_heif is None:
|
|
print(f"skipped {name}: HEIC needs pillow-heif (python -m pip install -r tools/requirements-photos.txt)")
|
|
continue
|
|
out_path = os.path.join(args.target, "%02d.jpg" % number)
|
|
try:
|
|
kb, quality = prepare(path, out_path, args.size, args.brightness, args.gradient, args.quality, args.max_kb)
|
|
except Exception as error: # a corrupt picture must not stop the rest
|
|
print(f"skipped {name}: {error}")
|
|
continue
|
|
written += 1
|
|
note = "" if kb <= args.max_kb else f" (over {args.max_kb} KB even at quality {quality})"
|
|
print(f"{os.path.basename(out_path)} <- {name} {kb} KB, quality {quality}{note}")
|
|
print(f"{written} of {len(files)} pictures written to {args.target}")
|
|
return 0 if written else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|