mirror of
https://github.com/vincentbernat/i3wm-configuration.git
synced 2025-06-27 20:18:49 +02:00
wallpaper: refactor a bit wallpaper module
This commit is contained in:
parent
453ea504f3
commit
fadf06d1b9
1 changed files with 204 additions and 143 deletions
253
bin/wallpaper
253
bin/wallpaper
|
@ -1,77 +1,72 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Build a multi screen wallpaper
|
||||
|
||||
# First argument is the directory where the wallpapers can be
|
||||
# found. We use xinerama to know the dimension of each screen.
|
||||
"""Build a multi screen wallpaper."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import argparse
|
||||
import tempfile
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
||||
from Xlib import display
|
||||
from Xlib.ext import randr
|
||||
|
||||
from PIL import Image
|
||||
from systemd import journal
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--directory",
|
||||
default=".",
|
||||
help="search for images in DIRECTORY",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--target",
|
||||
default="background.png",
|
||||
help="write background to FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extra-images",
|
||||
default=3,
|
||||
metavar="N",
|
||||
help="consider N additional images to choose the best combination",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--compression", default=0, type=int, help="compression level when saving"
|
||||
)
|
||||
options = parser.parse_args()
|
||||
background = None
|
||||
|
||||
# Get display size
|
||||
d = display.Display()
|
||||
screen = d.screen()
|
||||
window = screen.root.create_window(0, 0, 1, 1, 1, screen.root_depth)
|
||||
background = Image.new("RGB", (screen.width_in_pixels, screen.height_in_pixels))
|
||||
logger = logging.getLogger("wallpaper")
|
||||
Rectangle = collections.namedtuple("Rectangle", ["x", "y", "width", "height"])
|
||||
WallpaperPart = collections.namedtuple("WallpaperPart", ["rectangle", "image"])
|
||||
|
||||
# Query randr extension
|
||||
Output = collections.namedtuple("Output", ["x", "y", "width", "height"])
|
||||
outputs = []
|
||||
screen_resources = randr.get_screen_resources_current(window)
|
||||
for output in screen_resources.outputs:
|
||||
def get_outputs():
|
||||
"""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)
|
||||
background = Image.new("RGB", (screen.width_in_pixels, screen.height_in_pixels))
|
||||
|
||||
# Query randr extension
|
||||
outputs = []
|
||||
screen_resources = randr.get_screen_resources_current(window)
|
||||
for output in screen_resources.outputs:
|
||||
output_info = randr.get_output_info(window, output, screen_resources.timestamp)
|
||||
if output_info.crtc == 0:
|
||||
continue
|
||||
crtc_info = randr.get_crtc_info(window, output_info.crtc, output_info.timestamp)
|
||||
outputs.append(Output(crtc_info.x, crtc_info.y, crtc_info.width, crtc_info.height))
|
||||
if not outputs:
|
||||
outputs.append(Output(0, 0, (background.width, background.height)))
|
||||
outputs.append(Rectangle(crtc_info.x, crtc_info.y, crtc_info.width, crtc_info.height))
|
||||
|
||||
# Get all possible rectangles from outputs
|
||||
candidates = []
|
||||
for output in outputs:
|
||||
candidates.append(output)
|
||||
for o in outputs:
|
||||
logger.debug("output: %s", o)
|
||||
return outputs, background
|
||||
|
||||
|
||||
def get_covering_rectangles(outputs):
|
||||
"""Compute all possible groups of covering boxes for the provided
|
||||
outputs. For each group, an output is included in exactly one box.
|
||||
|
||||
>>> get_covering_rectangles([Rectangle(0, 0, 100, 100)])
|
||||
{(Rectangle(x=0, y=0, width=100, height=100),)}
|
||||
>>> get_covering_rectangles([Rectangle(0, 0, 100, 100), Rectangle(100, 0, 100, 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=200, height=100),)}
|
||||
>>> get_covering_rectangles([Rectangle(0, 0, 100, 100), Rectangle(100, 0, 100, 100), Rectangle(0, 100, 100, 100)])
|
||||
{(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))}
|
||||
"""
|
||||
|
||||
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.append(
|
||||
Output(
|
||||
candidates.add(
|
||||
Rectangle(
|
||||
output.x,
|
||||
output.y,
|
||||
ooutput.x - output.x + ooutput.width,
|
||||
|
@ -79,9 +74,9 @@ for output in outputs:
|
|||
)
|
||||
)
|
||||
|
||||
# Get all rectangle combinations to cover outputs without overlapping
|
||||
groups = []
|
||||
for r in range(len(candidates)):
|
||||
# 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
|
||||
|
@ -91,79 +86,145 @@ for r in range(len(candidates)):
|
|||
if nb != 1: # output not contained in a single rectangle
|
||||
break
|
||||
else:
|
||||
groups.append(candidate)
|
||||
groups.add(candidate)
|
||||
|
||||
# Get random images
|
||||
images = []
|
||||
for base, _, files in os.walk(os.path.join(options.directory)):
|
||||
for g in groups:
|
||||
logger.debug("group: %s", g)
|
||||
return groups
|
||||
|
||||
def get_random_images(directory, number):
|
||||
"""Get random images from a directory."""
|
||||
images = []
|
||||
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"):
|
||||
images.append(os.path.join(base, i))
|
||||
images = random.sample(images, len(outputs) * options.extra_images)
|
||||
images = [Image.open(image) for image in images]
|
||||
images = random.sample(images, number)
|
||||
images = [Image.open(image) for image in images]
|
||||
|
||||
# Find optimal combination for images
|
||||
best_association = None
|
||||
best_score = 0
|
||||
for group in groups:
|
||||
for image in images:
|
||||
logger.debug("image: %s %s×%s", image.filename[(len(directory) + 1) :], *image.size)
|
||||
return images
|
||||
|
||||
def get_best_parts(groups, images):
|
||||
"""Find optimal association for images for the groups of covering rectangles."""
|
||||
best_association = None
|
||||
best_score = 0
|
||||
for group in groups:
|
||||
for association in (zip(group, p) for p in itertools.permutations(images)):
|
||||
score = 0
|
||||
association = list(association)
|
||||
for output, image in association:
|
||||
association = [WallpaperPart(rectangle=assoc[0], image=assoc[1]) for assoc in association]
|
||||
for assoc in association:
|
||||
# Similar ratio
|
||||
oratio = output.width * 100 // output.height
|
||||
iratio = image.width * 100 // image.height
|
||||
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
|
||||
score += r * 100
|
||||
# Similar scale (when cropped)
|
||||
opixels = output.width * output.height
|
||||
ipixels = image.width * image.height * r
|
||||
opixels = assoc.rectangle.width * assoc.rectangle.height
|
||||
ipixels = assoc.image.width * assoc.image.height * r
|
||||
r = ipixels / opixels
|
||||
if r >= 1:
|
||||
r = 1
|
||||
score += r * 50
|
||||
score /= len(group)
|
||||
logger.debug("association: %s, score %.2f", association, score)
|
||||
if score > best_score or best_association is None:
|
||||
best_association = association
|
||||
best_score = score
|
||||
|
||||
print(
|
||||
"wallpaper: {}".format(
|
||||
" + ".join(
|
||||
"`{}` ({}×{})".format(
|
||||
image.filename[(len(options.directory) + 1) :], *image.size
|
||||
)
|
||||
for image in [couple[1] for couple in best_association]
|
||||
)
|
||||
)
|
||||
)
|
||||
return best_association
|
||||
|
||||
for couple in best_association:
|
||||
output = couple[0]
|
||||
image = couple[1]
|
||||
|
||||
# Find the right size for the screen
|
||||
imx, imy = output.width, image.height * output.width // image.width
|
||||
if imy < output.height:
|
||||
imx, imy = image.width * output.height // image.height, output.height
|
||||
def build(background, wallpaper_parts):
|
||||
"""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):
|
||||
image = image.resize((imx, imy), Image.LANCZOS)
|
||||
image = image.crop(
|
||||
(
|
||||
(imx - output.width) / 2,
|
||||
(imy - output.height) / 2,
|
||||
imx - (imx - output.width) / 2,
|
||||
imy - (imy - output.height) / 2,
|
||||
(imx - rectangle.width) / 2,
|
||||
(imy - rectangle.height) / 2,
|
||||
imx - (imx - rectangle.width) / 2,
|
||||
imy - (imy - rectangle.height) / 2,
|
||||
)
|
||||
)
|
||||
background.paste(image, (output.x, output.y))
|
||||
background.paste(image, (rectangle.x, rectangle.y))
|
||||
|
||||
# Save
|
||||
assert background, "Don't know the size of the display area"
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, dir=os.path.dirname(os.path.realpath(options.target))
|
||||
) as tmp:
|
||||
background.save(tmp, "png", compress_level=options.compression)
|
||||
os.rename(tmp.name, options.target)
|
||||
|
||||
def save(wallpaper, target, compression):
|
||||
"""Save wallpaper to target."""
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, dir=os.path.dirname(os.path.realpath(target))
|
||||
) as tmp:
|
||||
background.save(tmp, "png", compress_level=compression)
|
||||
os.rename(tmp.name, target)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Parse
|
||||
description = sys.modules[__name__].__doc__
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="enable debugging",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--directory",
|
||||
default=".",
|
||||
help="search for images in DIRECTORY",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--target",
|
||||
default="background.png",
|
||||
help="write background to FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extra-images",
|
||||
default=3,
|
||||
metavar="N",
|
||||
help="consider N additional images to choose the best combination",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--compression", default=0, type=int, help="compression level when saving"
|
||||
)
|
||||
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)
|
||||
images = get_random_images(options.directory, len(outputs) + options.extra_images)
|
||||
wallpaper_parts = get_best_parts(candidates, images)
|
||||
for part in wallpaper_parts:
|
||||
logger.info("wallpaper: {} ({}×{})".format(
|
||||
part.image.filename[(len(options.directory) + 1) :],
|
||||
*part.image.size
|
||||
))
|
||||
build(background, wallpaper_parts)
|
||||
save(background, options.target, options.compression)
|
||||
except Exception as e:
|
||||
logger.exception("%s", e)
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue