bundle: update (2026-01-18)

This commit is contained in:
2026-01-18 04:47:51 +00:00
parent 822898e4a3
commit 7597f2f156
223 changed files with 31167 additions and 0 deletions

View File

@@ -0,0 +1,360 @@
#
# Copyright 2011-2022 Martin Owens <doctormo@geek-2.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
"""
Provides wrappers for pixmap access.
"""
import os
import logging
from typing import List
from collections.abc import Iterable
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
import cairo
ICON_THEME = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
BILINEAR = GdkPixbuf.InterpType.BILINEAR
HYPER = GdkPixbuf.InterpType.HYPER
SIZE_ASPECT = 0
SIZE_ASPECT_GROW = 1
SIZE_ASPECT_CROP = 2
SIZE_STRETCH = 3
class PixmapLoadError(ValueError):
"""Failed to load a pixmap"""
class PixmapFilter: # pylint: disable=too-few-public-methods
"""Base class for filtering the pixmaps in a manager's output.
required - List of values required for this filter.
Use:
class Foo(PixmapManager):
filters = [ PixmapFilterFoo ]
"""
required: List[str] = []
optional: List[str] = []
def __init__(self, **kwargs):
self.enabled = True
for key in self.required:
if key not in kwargs:
self.enabled = False
else:
setattr(self, key, kwargs[key])
for key in self.optional:
if key in kwargs:
setattr(self, key, kwargs[key])
def filter(self, img, **kwargs):
"""Run filter, replace this methodwith your own"""
raise NotImplementedError(
"Please add 'filter' method to your PixmapFilter class %s."
% type(self).__name__
)
@staticmethod
def to_size(dat):
"""Tries to calculate a size that will work for the data"""
if isinstance(dat, (int, float)):
return (dat, dat)
if isinstance(dat, Iterable) and len(dat) >= 2:
return (dat[0], dat[1])
return None
class OverlayFilter(PixmapFilter):
"""Adds an overlay to output images, overlay can be any name that
the owning pixmap manager can find.
overlay : Name of overlay image
position : Location of the image:
0 - Full size (1 to 1 overlay, default)
(x,y) - Percentage from one end to the other position 0-1
alpha : Blending alpha, 0 - 255
"""
optional = ["position", "overlay", "alpha"]
def __init__(self, *args, **kwargs):
self.position = (0, 0)
self.overlay = None
self.alpha = 255
super().__init__(*args, **kwargs)
self.pad_x, self.pad_y = self.to_size(self.position)
def get_overlay(self, **kwargs):
if "manager" not in kwargs:
raise ValueError("PixmapManager must be provided when adding an overlay.")
return kwargs["manager"].get(
kwargs.get("overlay", None) or self.overlay, no_overlay=True
)
def filter(self, img, no_overlay=False, **kwargs):
# Recursion protection
if no_overlay:
return img
overlay = self.get_overlay(**kwargs)
if overlay:
img = img.copy()
(x, y, width, height) = self.set_position(overlay, img)
overlay.composite(
img, x, y, width, height, x, y, 1, 1, BILINEAR, self.alpha
)
return img
def set_position(self, overlay, img):
"""Sets the position of img on the given width and height"""
img_w, img_h = img.get_width(), img.get_height()
ovl_w, ovl_h = overlay.get_width(), overlay.get_height()
return (
max([0, (img_w - ovl_w) * self.pad_x]),
max([0, (img_h - ovl_h) * self.pad_y]),
min([ovl_w, img_w]),
min([ovl_h, img_h]),
)
class SizeFilter(PixmapFilter):
"""Resizes images to a certain size:
resize_mode - Way in which the size is calculated
0 - Best Aspect, don't grow
1 - Best Aspect, grow
2 - Cropped Aspect
3 - Stretch
"""
required = ["size"]
optional = ["resize_mode"]
def __init__(self, *args, **kwargs):
self.size = None
self.resize_mode = SIZE_ASPECT
super().__init__(*args, **kwargs)
self.img_w, self.img_h = self.to_size(self.size) or (0, 0)
def aspect(self, img_w, img_h):
"""Get the aspect ratio of the image resized"""
if self.resize_mode == SIZE_STRETCH:
return (self.img_w, self.img_h)
if (
self.resize_mode == SIZE_ASPECT
and img_w < self.img_w
and img_h < self.img_h
):
return (img_w, img_h)
(pcw, pch) = (self.img_w / img_w, self.img_h / img_h)
factor = (
max(pcw, pch) if self.resize_mode == SIZE_ASPECT_CROP else min(pcw, pch)
)
return (int(img_w * factor), int(img_h * factor))
def filter(self, img, **kwargs):
if self.size is not None:
(width, height) = self.aspect(img.get_width(), img.get_height())
return img.scale_simple(width, height, HYPER)
return img
class PadFilter(SizeFilter):
"""Add padding to the image to make it a standard size"""
optional = ["padding"]
def __init__(self, *args, **kwargs):
self.size = None
self.padding = 0.5
super().__init__(*args, **kwargs)
self.pad_x, self.pad_y = self.to_size(self.padding)
def filter(self, img, **kwargs):
(width, height) = (img.get_width(), img.get_height())
if width < self.img_w or height < self.img_h:
target = GdkPixbuf.Pixbuf.new(
img.get_colorspace(),
True,
img.get_bits_per_sample(),
max([width, self.img_w]),
max([height, self.img_h]),
)
target.fill(0x0) # Transparent black
x = (target.get_width() - width) * self.pad_x
y = (target.get_height() - height) * self.pad_y
img.composite(target, x, y, width, height, x, y, 1, 1, BILINEAR, 255)
return target
return img
class PixmapManager:
"""Manage a set of cached pixmaps, returns the default image
if it can't find one or the missing image if that's available."""
missing_image = "image-missing"
default_image = "application-default-icon"
icon_theme = ICON_THEME
theme_size = 32
filters: List[type] = []
pixmap_dir = None
def __init__(self, location="", **kwargs):
self.location = location
if self.pixmap_dir and not os.path.isabs(location):
self.location = os.path.join(self.pixmap_dir, location)
self.loader_size = PixmapFilter.to_size(kwargs.pop("load_size", None))
# Add any instance specified filters first
self._filters = []
for item in kwargs.get("filters", []) + self.filters:
if isinstance(item, PixmapFilter):
self._filters.append(item)
elif callable(item):
# Now add any class specified filters with optional kwargs
self._filters.append(item(**kwargs))
self.cache = {}
self.get_pixmap(self.default_image)
def get(self, *args, **kwargs):
"""Get a pixmap of any kind"""
return self.get_pixmap(*args, **kwargs)
def get_missing_image(self):
"""Get a missing image when other images aren't found"""
return self.get(self.missing_image)
@staticmethod
def data_is_file(data):
"""Test the file to see if it's a filename or not"""
return isinstance(data, str) and "<svg" not in data
def get_pixmap(self, data, **kwargs):
"""
There are three types of images this might return.
1. A named gtk-image such as "gtk-stop"
2. A file on the disk such as "/tmp/a.png"
3. Data as either svg or binary png
All pixmaps are cached for multiple use.
"""
if "manager" not in kwargs:
kwargs["manager"] = self
if not data:
if not self.default_image:
return None
data = self.default_image
key = data[-30:] # bytes or string
if not key in self.cache:
# load the image from data or a filename/theme icon
img = None
try:
if self.data_is_file(data):
img = self.load_from_name(data)
else:
img = self.load_from_data(data)
except PixmapLoadError as err:
logging.warning(str(err))
return self.get_missing_image()
if isinstance(img, Gtk.IconPaintable):
# Temporary porting hack: rasterise iconpaintable to pixbuf
# https://discourse.gnome.org/t/convert-symbolic-icon-to-gdktexture-gdkpixbuf/29324/3
w = img.get_intrinsic_width()
h = img.get_intrinsic_height()
snapshot = Gtk.Snapshot()
img.snapshot(snapshot, w, h)
node = snapshot.to_node()
surface = cairo.ImageSurface(cairo.Format.ARGB32, w, h)
ctx = cairo.Context(surface)
node.draw(ctx)
img = Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h)
if img is not None:
self.cache[key] = self.apply_filters(img, **kwargs)
return self.cache[key]
def apply_filters(self, img, **kwargs):
"""Apply all the filters to the given image"""
for lens in self._filters:
if lens.enabled:
img = lens.filter(img, **kwargs)
return img
def load_from_data(self, data):
"""Load in memory picture file (jpeg etc)"""
# This doesn't work yet, returns None *shrug*
loader = GdkPixbuf.PixbufLoader()
if self.loader_size:
loader.set_size(*self.loader_size)
try:
if isinstance(data, str):
data = data.encode("utf-8")
loader.write(data)
loader.close()
except GLib.GError as err:
raise PixmapLoadError(f"Failed to load pixbuf from data: {err}")
return loader.get_pixbuf()
def load_from_name(self, name):
"""Load a pixbuf from a name, filename or theme icon name"""
pixmap_path = self.pixmap_path(name)
if os.path.exists(pixmap_path):
try:
return GdkPixbuf.Pixbuf.new_from_file(pixmap_path)
except RuntimeError as msg:
raise PixmapLoadError(f"Failed to load pixmap '{pixmap_path}', {msg}")
elif (
self.icon_theme and "/" not in name and "." not in name and "<" not in name
):
return self.theme_pixmap(name, size=self.theme_size)
raise PixmapLoadError(f"Failed to find pixmap '{name}' in {self.location}")
def theme_pixmap(self, name, size=32):
"""Internal user: get image from gnome theme"""
size = size or 32
if not self.icon_theme.has_icon(name):
name = "image-missing"
return self.icon_theme.lookup_icon(name, [], size, 1, Gtk.TextDirection.NONE, 0)
def pixmap_path(self, name):
"""Returns the pixmap path based on stored location"""
for filename in (
name,
os.path.join(self.location, f"{name}.svg"),
os.path.join(self.location, f"{name}.png"),
):
if os.path.exists(filename) and os.path.isfile(filename):
return name
return os.path.join(self.location, name)