Last active
March 11, 2019 08:58
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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