2018-08-05 22:37:14 +02:00
|
|
|
|
#!/usr/bin/env python3
|
2012-07-06 14:19:54 +02:00
|
|
|
|
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"""Build a multi screen wallpaper."""
|
2012-07-06 14:19:54 +02:00
|
|
|
|
|
2021-08-30 23:50:19 +02:00
|
|
|
|
# Alternative:
|
|
|
|
|
# curl -s https://bzamayo.com/extras/apple-tv-screensavers.json \
|
|
|
|
|
# | jq -r '.data[].screensavers[].videoURL' \
|
|
|
|
|
# | shuf \
|
|
|
|
|
# | xargs nix run nixpkgs.xwinwrap -c \
|
|
|
|
|
# xwinwrap -b -s -fs -st -sp -nf -ov -- \
|
|
|
|
|
# mpv -wid WID -really-quiet -framedrop=vo --no-audio --panscan=1.0 \
|
|
|
|
|
# -loop-playlist=inf
|
|
|
|
|
|
2021-08-31 17:47:48 +02:00
|
|
|
|
# Alternative:
|
2024-08-15 19:19:13 +02:00
|
|
|
|
# https://moewalls.com/category/pixel-art/
|
|
|
|
|
# We could extract the first frame as a wallpaper, but use the whole video for
|
|
|
|
|
# the lock screen (and pause when the screen is off)
|
2021-08-31 17:47:48 +02:00
|
|
|
|
|
2012-07-06 14:19:54 +02:00
|
|
|
|
import os
|
2021-08-27 00:28:08 +02:00
|
|
|
|
import sys
|
2012-07-06 14:19:54 +02:00
|
|
|
|
import random
|
2021-07-30 14:11:48 +02:00
|
|
|
|
import argparse
|
2015-02-05 09:26:17 +01:00
|
|
|
|
import tempfile
|
2021-08-26 11:32:49 +02:00
|
|
|
|
import itertools
|
2024-12-02 08:28:12 +01:00
|
|
|
|
import functools
|
|
|
|
|
import operator
|
2021-08-27 00:28:08 +02:00
|
|
|
|
import logging
|
|
|
|
|
import logging.handlers
|
2021-08-31 17:38:17 +02:00
|
|
|
|
import inspect
|
2024-08-03 17:56:05 +02:00
|
|
|
|
import xattr
|
2012-07-06 14:19:54 +02:00
|
|
|
|
|
2020-02-06 21:59:29 +01:00
|
|
|
|
from Xlib import display
|
2018-08-05 22:37:14 +02:00
|
|
|
|
from Xlib.ext import randr
|
2021-08-27 00:28:08 +02:00
|
|
|
|
from systemd import journal
|
2022-08-13 18:20:31 +02:00
|
|
|
|
import PIL.Image
|
|
|
|
|
from PIL.Image import Image
|
|
|
|
|
from typing import Optional, NamedTuple
|
2021-08-27 00:28:08 +02:00
|
|
|
|
|
2022-08-13 18:20:31 +02:00
|
|
|
|
# We use typing, but it seems mostly broken with PIL.
|
2021-08-27 00:28:08 +02:00
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("wallpaper")
|
2022-08-13 18:20:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Rectangle(NamedTuple):
|
|
|
|
|
x: int
|
|
|
|
|
y: int
|
|
|
|
|
width: int
|
|
|
|
|
height: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WallpaperPart(NamedTuple):
|
|
|
|
|
rectangle: Rectangle
|
|
|
|
|
image: Image
|
2021-08-27 00:28:08 +02:00
|
|
|
|
|
2021-08-27 08:11:45 +02:00
|
|
|
|
|
2021-09-15 07:26:33 +02:00
|
|
|
|
def get_outputs() -> tuple[list[Rectangle], Image]:
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"""Get physical outputs."""
|
|
|
|
|
# Get display size
|
|
|
|
|
d = display.Display()
|
|
|
|
|
screen = d.screen()
|
|
|
|
|
window = screen.root.create_window(0, 0, 1, 1, 1, screen.root_depth)
|
2022-08-13 18:20:31 +02:00
|
|
|
|
background = PIL.Image.new("RGB", (screen.width_in_pixels, screen.height_in_pixels))
|
2021-08-27 00:28:08 +02:00
|
|
|
|
|
|
|
|
|
# Query randr extension
|
|
|
|
|
outputs = []
|
2023-04-25 11:11:29 +02:00
|
|
|
|
edids = []
|
2021-08-27 00:28:08 +02:00
|
|
|
|
screen_resources = randr.get_screen_resources_current(window)
|
|
|
|
|
for output in screen_resources.outputs:
|
2023-04-25 11:11:29 +02:00
|
|
|
|
# Extract dimension
|
2021-08-27 00:28:08 +02:00
|
|
|
|
output_info = randr.get_output_info(window, output, screen_resources.timestamp)
|
|
|
|
|
if output_info.crtc == 0:
|
2021-08-26 11:32:49 +02:00
|
|
|
|
continue
|
2021-08-27 00:28:08 +02:00
|
|
|
|
crtc_info = randr.get_crtc_info(window, output_info.crtc, output_info.timestamp)
|
2021-08-27 08:11:45 +02:00
|
|
|
|
outputs.append(
|
|
|
|
|
Rectangle(crtc_info.x, crtc_info.y, crtc_info.width, crtc_info.height)
|
|
|
|
|
)
|
2023-04-25 11:11:29 +02:00
|
|
|
|
# Extract EDID
|
|
|
|
|
output_properties = randr.list_output_properties(window, output)
|
|
|
|
|
edid = [0] * 128
|
|
|
|
|
for atom in output_properties._data["atoms"]:
|
|
|
|
|
atom_name = d.get_atom_name(atom)
|
|
|
|
|
if atom_name == "EDID":
|
|
|
|
|
edid = randr.get_output_property(
|
|
|
|
|
window, output, atom, 19, 0, 128
|
|
|
|
|
)._data["value"]
|
|
|
|
|
break
|
|
|
|
|
edids.append(edid)
|
|
|
|
|
|
|
|
|
|
# If for some outputs, EDID is the same, merge them. We assume only
|
|
|
|
|
# horizontal. For some reason, for a Dell Ultrasharp, EDID version and model
|
|
|
|
|
# number is not the same for HDMI and DP. Version is bytes 18-19, while
|
|
|
|
|
# product code are bytes 10-11
|
|
|
|
|
if len(edids) >= 2:
|
|
|
|
|
edids = [edid[:10] + edid[12:18] for edid in edids]
|
|
|
|
|
changed = True
|
|
|
|
|
while changed:
|
|
|
|
|
changed = False
|
|
|
|
|
for i, j in itertools.combinations(range(len(edids)), 2):
|
|
|
|
|
if (
|
|
|
|
|
edids[i] == edids[j]
|
|
|
|
|
and outputs[i].y == outputs[j].y
|
|
|
|
|
and outputs[i].height == outputs[j].height
|
|
|
|
|
and (
|
|
|
|
|
outputs[i].x + outputs[i].width == outputs[j].x
|
|
|
|
|
or outputs[j].x + outputs[j].width == outputs[i].x
|
|
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
logger.debug("merge outputs %s + %s", outputs[i], outputs[j])
|
|
|
|
|
outputs[i] = Rectangle(
|
|
|
|
|
min(outputs[i].x, outputs[j].x),
|
|
|
|
|
outputs[i].y,
|
|
|
|
|
outputs[i].width + outputs[j].width,
|
|
|
|
|
outputs[i].height,
|
|
|
|
|
)
|
|
|
|
|
del edids[j]
|
|
|
|
|
del outputs[j]
|
|
|
|
|
changed = True
|
|
|
|
|
break
|
2021-08-27 00:28:08 +02:00
|
|
|
|
for o in outputs:
|
|
|
|
|
logger.debug("output: %s", o)
|
|
|
|
|
return outputs, background
|
|
|
|
|
|
|
|
|
|
|
2021-09-15 07:26:33 +02:00
|
|
|
|
def get_covering_rectangles(outputs: list[Rectangle]) -> set[tuple[Rectangle, ...]]:
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"""Compute all possible groups of covering boxes for the provided
|
|
|
|
|
outputs. For each group, an output is included in exactly one box.
|
|
|
|
|
|
2021-08-27 08:11:45 +02:00
|
|
|
|
>>> gcr = get_covering_rectangles
|
|
|
|
|
>>> gcr([Rectangle(0, 0, 100, 100)])
|
2021-08-27 00:28:08 +02:00
|
|
|
|
{(Rectangle(x=0, y=0, width=100, height=100),)}
|
2021-08-27 08:11:45 +02:00
|
|
|
|
>>> gcr([Rectangle(0, 0, 100, 100),
|
|
|
|
|
... Rectangle(100, 0, 100, 100)]) # doctest: +NORMALIZE_WHITESPACE
|
|
|
|
|
{(Rectangle(x=0, y=0, width=100, height=100),
|
|
|
|
|
Rectangle(x=100, y=0, width=100, height=100)),
|
|
|
|
|
(Rectangle(x=0, y=0, width=200, height=100),)}
|
|
|
|
|
>>> gcr([Rectangle(0, 0, 100, 100),
|
|
|
|
|
... Rectangle(100, 0, 100, 100),
|
|
|
|
|
... Rectangle(0, 100, 100, 100)]) # doctest: +NORMALIZE_WHITESPACE
|
|
|
|
|
{(Rectangle(x=100, y=0, width=100, height=100),
|
|
|
|
|
Rectangle(x=0, y=100, width=100, height=100),
|
|
|
|
|
Rectangle(x=0, y=0, width=100, height=100)),
|
|
|
|
|
(Rectangle(x=100, y=0, width=100, height=100),
|
|
|
|
|
Rectangle(x=0, y=0, width=100, height=200)),
|
|
|
|
|
(Rectangle(x=0, y=0, width=200, height=100),
|
|
|
|
|
Rectangle(x=0, y=100, width=100, height=100))}
|
2021-10-28 05:46:41 +02:00
|
|
|
|
>>> gcr([Rectangle(0, 0, 2560, 1440),
|
|
|
|
|
... Rectangle(2560, 0, 1920, 1080)]) # doctest: +NORMALIZE_WHITESPACE
|
|
|
|
|
{(Rectangle(x=2560, y=0, width=1920, height=1080),
|
|
|
|
|
Rectangle(x=0, y=0, width=2560, height=1440))}
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
candidates = set()
|
|
|
|
|
for output in outputs:
|
|
|
|
|
candidates.add(output)
|
|
|
|
|
for ooutput in outputs:
|
|
|
|
|
if ooutput == output:
|
|
|
|
|
continue
|
|
|
|
|
if output.x > ooutput.x or output.y > ooutput.y:
|
|
|
|
|
continue
|
|
|
|
|
candidates.add(
|
|
|
|
|
Rectangle(
|
|
|
|
|
output.x,
|
|
|
|
|
output.y,
|
|
|
|
|
ooutput.x - output.x + ooutput.width,
|
|
|
|
|
ooutput.y - output.y + ooutput.height,
|
|
|
|
|
)
|
2021-08-26 11:32:49 +02:00
|
|
|
|
)
|
|
|
|
|
|
2021-08-27 00:28:08 +02:00
|
|
|
|
# Get all rectangle combinations to cover outputs without overlapping
|
|
|
|
|
groups = set()
|
|
|
|
|
for r in range(len(candidates)):
|
|
|
|
|
for candidate in itertools.combinations(candidates, r + 1):
|
|
|
|
|
for output in outputs:
|
|
|
|
|
nb = 0
|
|
|
|
|
for c in candidate:
|
2021-08-27 08:11:45 +02:00
|
|
|
|
if (
|
|
|
|
|
c.x <= output.x < c.x + c.width
|
|
|
|
|
and c.y <= output.y < c.y + c.height
|
2021-10-28 05:46:41 +02:00
|
|
|
|
and output.x + output.width <= c.x + c.width
|
|
|
|
|
and output.y + output.height <= c.y + c.height
|
2021-08-27 08:11:45 +02:00
|
|
|
|
):
|
2021-08-27 00:28:08 +02:00
|
|
|
|
nb += 1
|
|
|
|
|
if nb != 1: # output not contained in a single rectangle
|
|
|
|
|
break
|
|
|
|
|
else:
|
2021-10-28 05:46:41 +02:00
|
|
|
|
# Test for overlap
|
|
|
|
|
overlap = False
|
|
|
|
|
for c1 in candidate:
|
|
|
|
|
for c2 in candidate:
|
|
|
|
|
if c1 == c2:
|
|
|
|
|
continue
|
|
|
|
|
if not (
|
|
|
|
|
c1.x >= c2.x + c2.width
|
|
|
|
|
or c1.x + c1.width <= c2.x
|
|
|
|
|
or c1.y >= c2.y + c2.height
|
|
|
|
|
or c1.y + c1.height <= c2.y
|
|
|
|
|
):
|
|
|
|
|
overlap = True
|
|
|
|
|
if not overlap:
|
|
|
|
|
groups.add(candidate)
|
2021-08-27 00:28:08 +02:00
|
|
|
|
|
|
|
|
|
for g in groups:
|
|
|
|
|
logger.debug("group: %s", g)
|
|
|
|
|
return groups
|
|
|
|
|
|
2021-08-27 08:11:45 +02:00
|
|
|
|
|
2022-08-13 18:20:31 +02:00
|
|
|
|
def get_random_images(directory: str, number: int) -> list[Image]:
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"""Get random images from a directory."""
|
2021-09-15 07:26:33 +02:00
|
|
|
|
image_files = []
|
2024-08-03 17:56:05 +02:00
|
|
|
|
weights = []
|
|
|
|
|
counts = []
|
2021-08-27 00:28:08 +02:00
|
|
|
|
for base, _, files in os.walk(os.path.join(directory)):
|
|
|
|
|
for i in files:
|
|
|
|
|
if os.path.splitext(i)[1].lower() in (".jpg", ".jpeg", ".png", ".webp"):
|
2024-08-03 17:56:05 +02:00
|
|
|
|
filename = os.path.join(base, i)
|
|
|
|
|
image_files.append(filename)
|
|
|
|
|
if options.count_attribute:
|
|
|
|
|
try:
|
|
|
|
|
count = int(
|
2024-08-04 00:07:48 +02:00
|
|
|
|
xattr.getxattr(filename, options.count_attribute).decode()
|
2024-08-03 17:56:05 +02:00
|
|
|
|
)
|
|
|
|
|
except (OSError, ValueError):
|
|
|
|
|
count = 0
|
|
|
|
|
counts.append(count)
|
2024-08-12 00:22:15 +02:00
|
|
|
|
weights = [100 / ((count - min(counts) + 1) ** 3) for count in counts]
|
2024-08-03 17:56:05 +02:00
|
|
|
|
images = [
|
|
|
|
|
PIL.Image.open(image)
|
2024-12-02 08:28:12 +01:00
|
|
|
|
for image in functools.reduce(
|
|
|
|
|
operator.add,
|
|
|
|
|
(random.choices(image_files, weights=weights) for k in range(number)),
|
|
|
|
|
[],
|
|
|
|
|
)
|
2024-08-03 17:56:05 +02:00
|
|
|
|
]
|
2021-08-27 00:28:08 +02:00
|
|
|
|
|
|
|
|
|
for image in images:
|
2021-08-27 08:11:45 +02:00
|
|
|
|
directory_len = len(directory) + 1
|
|
|
|
|
logger.debug("image: %s %s×%s", image.filename[directory_len:], *image.size)
|
2021-08-27 00:28:08 +02:00
|
|
|
|
return images
|
|
|
|
|
|
2021-08-27 08:11:45 +02:00
|
|
|
|
|
2021-09-15 07:26:33 +02:00
|
|
|
|
def get_best_parts(
|
|
|
|
|
groups: set[tuple[Rectangle, ...]],
|
2022-08-13 18:20:31 +02:00
|
|
|
|
images: list[Image],
|
2021-09-15 07:26:33 +02:00
|
|
|
|
ratio_score: int = 100,
|
2024-08-22 08:53:16 +02:00
|
|
|
|
scale_score: int = 20,
|
2024-08-15 18:59:12 +02:00
|
|
|
|
multiple_wallpaper_score: int = -50,
|
2021-09-15 07:26:33 +02:00
|
|
|
|
) -> Optional[list[WallpaperPart]]:
|
2021-08-27 08:11:45 +02:00
|
|
|
|
"""Find optimal association for images for the groups of covering rectangles.
|
|
|
|
|
|
|
|
|
|
>>> gbp = get_best_parts
|
|
|
|
|
>>> gbp([[Rectangle(0, 0, 100, 100)]],
|
2022-08-13 18:20:31 +02:00
|
|
|
|
... [PIL.Image.new("RGB", (100, 100))]) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
2021-08-27 08:11:45 +02:00
|
|
|
|
[WallpaperPart(rectangle=Rectangle(x=0, y=0, width=100, height=100),
|
|
|
|
|
image=<PIL.Image.Image image mode=RGB size=100x100 at ...>)]
|
|
|
|
|
>>> gbp([[Rectangle(0, 0, 100, 100)]],
|
2022-08-13 18:20:31 +02:00
|
|
|
|
... [PIL.Image.new("RGB", (100, 100)),
|
|
|
|
|
... PIL.Image.new("RGB", (100, 200))]) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
2021-08-27 08:11:45 +02:00
|
|
|
|
[WallpaperPart(rectangle=Rectangle(x=0, y=0, width=100, height=100),
|
|
|
|
|
image=<PIL.Image.Image image mode=RGB size=100x100 at ...>)]
|
|
|
|
|
>>> gbp([[Rectangle(0, 0, 100, 100)]],
|
2022-08-13 18:20:31 +02:00
|
|
|
|
... [PIL.Image.new("RGB", (50, 50)),
|
|
|
|
|
... PIL.Image.new("RGB", (100, 200))]) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
2021-08-27 08:11:45 +02:00
|
|
|
|
[WallpaperPart(rectangle=Rectangle(x=0, y=0, width=100, height=100),
|
|
|
|
|
image=<PIL.Image.Image image mode=RGB size=50x50 at ...>)]
|
|
|
|
|
>>> gbp([[Rectangle(0, 0, 100, 100)]],
|
2022-08-13 18:20:31 +02:00
|
|
|
|
... [PIL.Image.new("RGB", (10, 10)),
|
|
|
|
|
... PIL.Image.new("RGB", (100, 200))]) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
2021-08-27 08:11:45 +02:00
|
|
|
|
[WallpaperPart(rectangle=Rectangle(x=0, y=0, width=100, height=100),
|
|
|
|
|
image=<PIL.Image.Image image mode=RGB size=100x200 at ...>)]
|
|
|
|
|
>>> gbp([[Rectangle(0, 0, 100, 100), Rectangle(0, 100, 100, 100)],
|
|
|
|
|
... [Rectangle(0, 0, 200, 100)]],
|
2022-08-13 18:20:31 +02:00
|
|
|
|
... [PIL.Image.new("RGB", (100, 100)),
|
|
|
|
|
... PIL.Image.new("RGB", (200, 100))]) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
2021-08-27 08:11:45 +02:00
|
|
|
|
[WallpaperPart(rectangle=Rectangle(x=0, y=0, width=200, height=100),
|
|
|
|
|
image=<PIL.Image.Image image mode=RGB size=200x100 at ...>)]
|
|
|
|
|
>>> gbp([[Rectangle(0, 0, 100, 100), Rectangle(100, 0, 100, 100)],
|
|
|
|
|
... [Rectangle(0, 0, 200, 100)]],
|
2022-08-13 18:20:31 +02:00
|
|
|
|
... [PIL.Image.new("RGB", (100, 100)),
|
|
|
|
|
... PIL.Image.new("RGB", (100, 100))]) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
2021-08-27 08:11:45 +02:00
|
|
|
|
[WallpaperPart(rectangle=Rectangle(x=0, y=0, width=100, height=100),
|
|
|
|
|
image=<PIL.Image.Image image mode=RGB size=100x100 at ...>),
|
|
|
|
|
WallpaperPart(rectangle=Rectangle(x=100, y=0, width=100, height=100),
|
|
|
|
|
image=<PIL.Image.Image image mode=RGB size=100x100 at ...>)]
|
|
|
|
|
>>> gbp([[Rectangle(0, 0, 1920, 1080), Rectangle(1920, 0, 1920, 1080)],
|
|
|
|
|
... [Rectangle(0, 0, 3840, 1080)]],
|
2022-08-13 18:20:31 +02:00
|
|
|
|
... [PIL.Image.new("RGB", (2560, 1440)),
|
|
|
|
|
... PIL.Image.new("RGB", (3840, 1440))]) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
2021-08-27 08:11:45 +02:00
|
|
|
|
[WallpaperPart(rectangle=Rectangle(x=0, y=0, width=3840, height=1080),
|
|
|
|
|
image=<PIL.Image.Image image mode=RGB size=3840x1440 at ...>)]
|
|
|
|
|
"""
|
2021-08-27 00:28:08 +02:00
|
|
|
|
best_association = None
|
2022-08-13 18:20:31 +02:00
|
|
|
|
best_score: float = 0
|
2021-08-27 00:28:08 +02:00
|
|
|
|
for group in groups:
|
2021-08-27 00:46:15 +02:00
|
|
|
|
associations = [tuple(zip(group, p)) for p in itertools.permutations(images)]
|
|
|
|
|
seen = []
|
|
|
|
|
for association in associations:
|
|
|
|
|
if association in seen:
|
|
|
|
|
continue
|
|
|
|
|
seen.append(association)
|
2022-08-13 18:20:31 +02:00
|
|
|
|
score: float = 0
|
2021-09-15 07:26:33 +02:00
|
|
|
|
association_ = [
|
2021-08-27 08:11:45 +02:00
|
|
|
|
WallpaperPart(rectangle=assoc[0], image=assoc[1])
|
|
|
|
|
for assoc in association
|
|
|
|
|
]
|
2021-09-15 07:26:33 +02:00
|
|
|
|
for assoc in association_:
|
2021-08-27 00:28:08 +02:00
|
|
|
|
# Similar ratio
|
|
|
|
|
oratio = assoc.rectangle.width * 100 // assoc.rectangle.height
|
|
|
|
|
iratio = assoc.image.width * 100 // assoc.image.height
|
|
|
|
|
r = iratio / oratio
|
|
|
|
|
if r > 1:
|
|
|
|
|
r = 1 / r
|
2021-08-31 17:38:17 +02:00
|
|
|
|
score += r * ratio_score
|
2021-08-27 00:28:08 +02:00
|
|
|
|
# Similar scale (when cropped)
|
|
|
|
|
opixels = assoc.rectangle.width * assoc.rectangle.height
|
|
|
|
|
ipixels = assoc.image.width * assoc.image.height * r
|
|
|
|
|
r = ipixels / opixels
|
|
|
|
|
if r >= 1:
|
|
|
|
|
r = 1
|
2021-08-31 17:38:17 +02:00
|
|
|
|
score += r * scale_score
|
2024-08-15 18:59:12 +02:00
|
|
|
|
score += (len(group) - 1) * multiple_wallpaper_score
|
2021-09-15 07:26:33 +02:00
|
|
|
|
logger.debug("association: %s, score %.2f", association_, score)
|
2021-08-27 00:28:08 +02:00
|
|
|
|
if score > best_score or best_association is None:
|
2021-09-15 07:26:33 +02:00
|
|
|
|
best_association = association_
|
2021-08-27 00:28:08 +02:00
|
|
|
|
best_score = score
|
|
|
|
|
|
|
|
|
|
return best_association
|
|
|
|
|
|
|
|
|
|
|
2021-09-15 07:26:33 +02:00
|
|
|
|
def build(background: Image, wallpaper_parts: list[WallpaperPart]) -> None:
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"""Stitch wallpaper into provided background."""
|
|
|
|
|
for part in wallpaper_parts:
|
|
|
|
|
rectangle = part.rectangle
|
|
|
|
|
image = part.image
|
|
|
|
|
|
|
|
|
|
imx, imy = rectangle.width, image.height * rectangle.width // image.width
|
|
|
|
|
if imy < rectangle.height:
|
|
|
|
|
imx, imy = image.width * rectangle.height // image.height, rectangle.height
|
|
|
|
|
if image.size != (imx, imy):
|
2022-08-13 18:20:31 +02:00
|
|
|
|
image = image.resize((imx, imy), PIL.Image.Resampling.LANCZOS)
|
2021-08-27 00:28:08 +02:00
|
|
|
|
image = image.crop(
|
|
|
|
|
(
|
2022-08-13 18:20:31 +02:00
|
|
|
|
(imx - rectangle.width) // 2,
|
|
|
|
|
(imy - rectangle.height) // 2,
|
|
|
|
|
imx - (imx - rectangle.width) // 2,
|
|
|
|
|
imy - (imy - rectangle.height) // 2,
|
2021-08-17 12:51:07 +02:00
|
|
|
|
)
|
2021-07-17 09:13:26 +02:00
|
|
|
|
)
|
2021-08-27 00:28:08 +02:00
|
|
|
|
background.paste(image, (rectangle.x, rectangle.y))
|
|
|
|
|
|
|
|
|
|
|
2021-09-15 07:26:33 +02:00
|
|
|
|
def save(wallpaper: Image, target: str, compression: int) -> None:
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"""Save wallpaper to target."""
|
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
|
|
|
delete=False, dir=os.path.dirname(os.path.realpath(target))
|
|
|
|
|
) as tmp:
|
2021-09-15 07:26:33 +02:00
|
|
|
|
wallpaper.save(tmp, "png", compress_level=compression)
|
2021-08-27 00:28:08 +02:00
|
|
|
|
os.rename(tmp.name, target)
|
|
|
|
|
|
2021-08-27 08:11:45 +02:00
|
|
|
|
|
2021-08-27 00:28:08 +02:00
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
# Parse
|
|
|
|
|
description = sys.modules[__name__].__doc__
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--debug",
|
|
|
|
|
action="store_true",
|
|
|
|
|
default=False,
|
|
|
|
|
help="enable debugging",
|
2021-07-17 09:13:26 +02:00
|
|
|
|
)
|
2021-08-31 17:38:17 +02:00
|
|
|
|
group = parser.add_argument_group("image selection")
|
|
|
|
|
group.add_argument(
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"-d",
|
|
|
|
|
"--directory",
|
|
|
|
|
default=".",
|
|
|
|
|
help="search for images in DIRECTORY",
|
|
|
|
|
)
|
2021-08-31 17:38:17 +02:00
|
|
|
|
group.add_argument(
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"--extra-images",
|
2024-08-20 19:38:46 +02:00
|
|
|
|
default=1,
|
2021-08-27 00:28:08 +02:00
|
|
|
|
metavar="N",
|
2024-08-20 19:38:46 +02:00
|
|
|
|
help="consider N additional images per output to choose the best combination",
|
2021-08-27 00:28:08 +02:00
|
|
|
|
)
|
2024-08-03 17:56:05 +02:00
|
|
|
|
group.add_argument(
|
|
|
|
|
"--count-attribute",
|
|
|
|
|
default="user.count",
|
|
|
|
|
metavar="ATTR",
|
|
|
|
|
help="store number of times an image was used in the provided attribute",
|
|
|
|
|
)
|
2021-08-31 17:38:17 +02:00
|
|
|
|
params = inspect.signature(get_best_parts).parameters
|
|
|
|
|
group.add_argument(
|
|
|
|
|
"--ratio-score",
|
|
|
|
|
default=params["ratio_score"].default,
|
|
|
|
|
help="multiplicative weight applied to ratio matching for score",
|
|
|
|
|
)
|
|
|
|
|
group.add_argument(
|
|
|
|
|
"--scale-score",
|
|
|
|
|
default=params["scale_score"].default,
|
|
|
|
|
help="multiplicative weight applied to pixel matching for score",
|
|
|
|
|
)
|
|
|
|
|
group.add_argument(
|
2024-08-15 18:59:12 +02:00
|
|
|
|
"--multiple-wallpaper-score",
|
|
|
|
|
default=params["multiple_wallpaper_score"].default,
|
|
|
|
|
help="additive weight for each additional wallpaper used",
|
2021-08-31 17:38:17 +02:00
|
|
|
|
)
|
|
|
|
|
group = parser.add_argument_group("image output")
|
|
|
|
|
group.add_argument(
|
|
|
|
|
"-t",
|
|
|
|
|
"--target",
|
|
|
|
|
default="background.png",
|
|
|
|
|
help="write background to FILE",
|
2023-04-25 11:34:53 +02:00
|
|
|
|
metavar="FILE",
|
2021-08-31 17:38:17 +02:00
|
|
|
|
)
|
|
|
|
|
group.add_argument(
|
2021-08-27 00:28:08 +02:00
|
|
|
|
"--compression", default=0, type=int, help="compression level when saving"
|
2021-08-26 11:32:49 +02:00
|
|
|
|
)
|
2023-04-25 11:34:53 +02:00
|
|
|
|
group.add_argument(
|
|
|
|
|
"--outputs",
|
|
|
|
|
default=None,
|
|
|
|
|
help="write number of outputs to FILE",
|
|
|
|
|
metavar="FILE",
|
|
|
|
|
)
|
2021-08-27 00:28:08 +02:00
|
|
|
|
options = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
# Logging
|
|
|
|
|
root = logging.getLogger("")
|
|
|
|
|
root.setLevel(logging.WARNING)
|
|
|
|
|
logger.setLevel(options.debug and logging.DEBUG or logging.INFO)
|
|
|
|
|
if sys.stderr.isatty():
|
|
|
|
|
ch = logging.StreamHandler()
|
|
|
|
|
ch.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
|
|
|
|
|
root.addHandler(ch)
|
|
|
|
|
else:
|
|
|
|
|
root.addHandler(journal.JournalHandler(SYSLOG_IDENTIFIER=logger.name))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
outputs, background = get_outputs()
|
|
|
|
|
candidates = get_covering_rectangles(outputs)
|
2021-08-27 08:11:45 +02:00
|
|
|
|
images = get_random_images(
|
2024-08-20 19:38:46 +02:00
|
|
|
|
options.directory, len(outputs) * (1 + options.extra_images)
|
2021-08-27 08:11:45 +02:00
|
|
|
|
)
|
2021-08-31 17:38:17 +02:00
|
|
|
|
wallpaper_parts = get_best_parts(
|
|
|
|
|
candidates,
|
|
|
|
|
images,
|
|
|
|
|
ratio_score=options.ratio_score,
|
|
|
|
|
scale_score=options.scale_score,
|
2024-08-15 18:59:12 +02:00
|
|
|
|
multiple_wallpaper_score=options.multiple_wallpaper_score,
|
2021-08-31 17:38:17 +02:00
|
|
|
|
)
|
2021-09-15 07:26:33 +02:00
|
|
|
|
assert wallpaper_parts is not None
|
2021-08-27 00:28:08 +02:00
|
|
|
|
for part in wallpaper_parts:
|
2021-08-27 08:11:45 +02:00
|
|
|
|
logger.info(
|
|
|
|
|
"wallpaper: {} ({}×{})".format(
|
|
|
|
|
part.image.filename[(len(options.directory) + 1) :],
|
|
|
|
|
*part.image.size
|
|
|
|
|
)
|
|
|
|
|
)
|
2024-08-03 17:56:05 +02:00
|
|
|
|
if options.count_attribute:
|
|
|
|
|
try:
|
2024-08-04 00:07:48 +02:00
|
|
|
|
count = int(
|
|
|
|
|
xattr.getxattr(part.image.filename, options.count_attribute)
|
|
|
|
|
)
|
2024-08-03 17:56:05 +02:00
|
|
|
|
except (OSError, ValueError):
|
|
|
|
|
count = 0
|
2024-08-04 00:07:48 +02:00
|
|
|
|
xattr.setxattr(
|
2024-08-03 17:56:05 +02:00
|
|
|
|
part.image.filename,
|
|
|
|
|
options.count_attribute,
|
|
|
|
|
bytes(str(count + 1), "ascii"),
|
|
|
|
|
)
|
2021-08-27 00:28:08 +02:00
|
|
|
|
build(background, wallpaper_parts)
|
|
|
|
|
save(background, options.target, options.compression)
|
2023-04-25 11:34:53 +02:00
|
|
|
|
|
|
|
|
|
if options.outputs is not None:
|
|
|
|
|
with open(options.outputs, "w") as f:
|
|
|
|
|
f.write(str(len(outputs)))
|
2021-08-27 00:28:08 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.exception("%s", e)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
sys.exit(0)
|