bundle: update (2026-01-18)
This commit is contained in:
360
extensions/km-hatch/deps/inkex/gui/pixmap.py
Normal file
360
extensions/km-hatch/deps/inkex/gui/pixmap.py
Normal 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)
|
||||
Reference in New Issue
Block a user