Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Last active March 11, 2019 08:58
Show Gist options
  • Select an option

  • Save ssokolow/cd9dc27aa2db42e0a290 to your computer and use it in GitHub Desktop.

Select an option

Save ssokolow/cd9dc27aa2db42e0a290 to your computer and use it in GitHub Desktop.
set_background.py: A script to hack desktop-spanning background support into KDE 4.x
[Desktop Entry]
# IMPORTANT: THIS FILE IS A ONE-OFF HACK THAT I WROTE FOR MYSELF AND I AM
# SHARING IT AS AN EXAMPLE. I DO NOT GUARANTEE THAT IT WILL WORK AS-IS ON
# YOUR DESKTOP.
#
# Requirements:
# - Geeqie
# - Zenity
# - ~/bin/set_background.py
#
# Instructions:
# 1. `xdg-desktop-menu install geeqie-prepare-background.desktop`
# 2. `mkdir -p ~/.local/share/images/background`
Version=1.0
Type=Application
Name=Prepare Desktop Background
# call the helper script
Exec=sh -c 'python ~/bin/set_background.py --gravity $(zenity --list --title="Select Gravity" --column="Gravity" --text="Select Background Alignment" --hide-header --height=300 top-left top-center top-right middle-left middle-center middle-right bottom-left bottom-middle bottom-right | cut -d\| -f1) %F ~/.local/share/images/background'
# Desktop files that are usable only in Geeqie should be marked like this:
Categories=X-Geeqie;
OnlyShowIn=X-Geeqie;
# Show in "Edit" menu
X-Geeqie-Menu-Path=EditMenu
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Simple PyQt5 script to chop up multi-monitor backgrounds for KDE."""
from __future__ import (absolute_import, division, print_function,
with_statement, unicode_literals)
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__version__ = "0.2.1"
__license__ = "MIT"
IMG_EXTS = [
'.bmp', '.gif',
'.jpg', '.jpe', '.jpeg',
'.png', '.pbm', '.pgm', '.ppm',
'.xbm', '.xpm',
'.svg'
]
import logging
log = logging.getLogger(__name__)
import errno, os, random
try:
from PyQt5.QtCore import QRect, QPoint, Qt
from PyQt5.QtGui import QImage
from PyQt5.QtWidgets import QApplication
except ImportError:
from PyQt4.QtCore import QRect, QPoint, Qt
from PyQt4.QtGui import QApplication, QImage
V_GRAV = {
'top': Qt.AlignTop,
'middle': Qt.AlignVCenter,
'bottom': Qt.AlignBottom
}
H_GRAV = {
'left': Qt.AlignLeft,
'center': Qt.AlignHCenter,
'right': Qt.AlignRight
}
GRAVITIES = {'%s-%s' % (x, y): V_GRAV[x] | H_GRAV[y]
for x in V_GRAV for y in H_GRAV}
def get_desktop_geom():
"""Get the box we must upscale to fill and the pieces to crop out"""
app = QApplication([])
deskwid = app.desktop()
desktop = QRect(0, 0, deskwid.width(), deskwid.height())
log.debug("Desktop rectangle: %s", desktop)
monitors = [deskwid.screenGeometry(x)
for x in range(deskwid.screenCount())]
log.debug("Monitor rectangles: %s", monitors)
return desktop, monitors
def write_backgrounds(image, monitors, outdir):
"""Given a QImage, a list of QRects, and a target directory, crop out
a bunch of numbered PNG files.
"""
for idx, geom in enumerate(monitors):
# We can't cleanly force a refresh in plasma, so we must instead
# provide single-image sets to Plasma's randomizer so it can notice
# the changes
outparent = os.path.join(outdir, '%d' % idx)
if not os.path.exists(outparent):
log.debug("Creating directory: %s", outparent)
os.makedirs(outparent)
outpath = os.path.join(outparent, 'piece.png')
log.info("Extracting %s and writing as %s", geom, outpath)
if not image.copy(geom).save(outpath):
log.warning("Failed to write image!")
def apply_gravity(image, tgt_rect, gravity):
"""Crop an image to a target rect based on a given alignment"""
# Verticals
if gravity & Qt.AlignTop:
tgt_rect.moveTop(0)
if gravity & Qt.AlignVCenter:
tgt_rect.moveCenter(QPoint(tgt_rect.center().x(),
int(image.height() / 2)))
elif gravity & Qt.AlignBottom:
tgt_rect.moveBottom(image.height())
# Horizontals
if gravity & Qt.AlignLeft:
tgt_rect.moveLeft(0)
if gravity & Qt.AlignHCenter:
tgt_rect.moveCenter(QPoint(int(image.width() / 2),
tgt_rect.center().y()))
elif gravity & Qt.AlignRight:
tgt_rect.moveRight(image.width())
log.debug("Cropping to %s", tgt_rect)
return image.copy(tgt_rect)
def set_background(img_path, outdir, gravity=0):
"""Given a path, [re]generate output files matching the monitors."""
if not os.path.exists(outdir):
raise IOError(errno.ENOENT, "Destination directory does not exist",
outdir)
if not os.path.isdir(outdir):
raise IOError(errno.ENOTDIR, "Destination path is not a directory",
outdir)
if not os.access(outdir, os.W_OK):
raise IOError(errno.EACCES, "Destination directory not writable",
outdir)
if not os.access(img_path, os.R_OK):
raise IOError(errno.EACCES, "Source file not readable",
outdir)
log.debug("Loading image: %s", img_path)
infile = QImage(img_path)
if infile.isNull():
raise ValueError("Could not load image: %s", img_path)
desktop, monitors = get_desktop_geom()
log.debug("Image size before: %s", infile.size())
infile = infile.scaled(desktop.size(),
Qt.KeepAspectRatioByExpanding,
Qt.SmoothTransformation)
log.debug("Image size after fitting to desktop with "
"KeepAspectRatioByExpanding: %s", infile.size())
infile = apply_gravity(infile, desktop, gravity)
write_backgrounds(infile, monitors, outdir)
def is_image(path):
"""Determine whether the given path represents a potential background"""
return os.path.splitext(path)[1].lower() in IMG_EXTS
def pick_recursive(roots):
"""Recursively traverse a list of paths and pick an image file."""
log.debug("Picking random file from: %s", roots)
potentials = []
for root in roots:
if os.path.isfile(root):
potentials.append(root)
elif os.path.isdir(root):
for path, _, files in os.walk(root):
for fname in files:
fpath = os.path.join(path, fname)
if is_image(fpath):
potentials.append(fpath)
return random.choice(potentials)
def main():
"""The main entry point, compatible with setuptools entry points."""
from argparse import ArgumentParser
parser = ArgumentParser(
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0])
parser.add_argument('--version', action='version',
version='%%(prog)s v%s' % __version__)
parser.add_argument('-v', '--verbose', action="count", default=2,
help="Increase verbosity. Use twice for extra effect")
parser.add_argument('-q', '--quiet', action="count", default=0,
help="Decrease verbosity. Use twice for extra effect")
parser.add_argument('--gravity', default='top-left', choices=GRAVITIES,
help="Choose how to align the image before cropping if"
" its aspect ratio doesn't match the desktop.")
parser.add_argument('--randomize', action="store_true", default=False,
help="Randomly select an image from the given paths")
parser.add_argument('paths', nargs='+')
parser.add_argument('outpath')
args = parser.parse_args()
# Set up clean logging to stderr
log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING,
logging.INFO, logging.DEBUG]
args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1)
args.verbose = max(args.verbose, 0)
logging.basicConfig(level=log_levels[args.verbose],
format='%(levelname)s: %(message)s')
if args.randomize:
args.paths = [pick_recursive(args.paths)]
if args.paths:
path = args.paths[0]
if os.path.isfile(path):
log.debug("Requested alignment/gravity: %s", args.gravity)
try:
set_background(args.paths[0], args.outpath,
GRAVITIES[args.gravity])
except IOError as err:
log.error(err)
else:
log.error("Not a valid file: %s", path)
if __name__ == '__main__':
main()
# vim: set sw=4 sts=4 expandtab :
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment