summaryrefslogtreecommitdiffstats
path: root/share/extensions/other/clipart/import_web_image.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
commitcca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch)
tree146f39ded1c938019e1ed42d30923c2ac9e86789 /share/extensions/other/clipart/import_web_image.py
parentInitial commit. (diff)
downloadinkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.tar.xz
inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'share/extensions/other/clipart/import_web_image.py')
-rwxr-xr-xshare/extensions/other/clipart/import_web_image.py395
1 files changed, 395 insertions, 0 deletions
diff --git a/share/extensions/other/clipart/import_web_image.py b/share/extensions/other/clipart/import_web_image.py
new file mode 100755
index 0000000..5c8c8d8
--- /dev/null
+++ b/share/extensions/other/clipart/import_web_image.py
@@ -0,0 +1,395 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright 2021 Martin Owens <doctormo@gmail.com>
+# Copyright 2022 Simon Duerr <dev@simonduerr.eu>
+#
+# 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/>
+#
+"""
+Import images from the internet, inkscape extension (GUI)
+"""
+
+__version__ = '1.0'
+__pkgname__ = 'inkscape-import-web-image'
+
+import os
+import sys
+import logging
+import warnings
+warnings.filterwarnings("ignore")
+
+from collections import defaultdict
+from base64 import encodebytes
+from appdirs import user_cache_dir
+
+import inkex
+from inkex import Style
+from inkex.gui import GtkApp, Window, IconView, asyncme
+from inkex.gui.pixmap import PixmapManager, SizeFilter, PadFilter, OverlayFilter
+from inkex.elements import (
+ load_svg, Image, Defs, NamedView, Metadata,
+ SvgDocumentElement, StyleElement
+)
+from gi.repository import Gtk
+
+from import_sources import RemoteSource, RemoteFile, RemotePage
+
+SOURCES = os.path.join(os.path.dirname(__file__), 'sources')
+LICENSES = os.path.join(os.path.dirname(__file__), 'licenses')
+CACHE_DIR = user_cache_dir('inkscape-import-web-image', 'Inkscape')
+
+class LicenseOverlay(OverlayFilter):
+ pixmaps = PixmapManager(LICENSES)
+
+ def get_overlay(self, item=None, manager=None):
+ if item is None: # Default image
+ return None
+ return self.pixmaps.get(item.get_overlay())
+
+
+class ResultsIconView(IconView):
+ """The search results shown as icons"""
+ def get_markup(self, item):
+ return item.string
+
+ def get_icon(self, item):
+ return item.icon
+
+ def setup(self):
+ self._list.set_markup_column(1)
+ self._list.set_pixbuf_column(2)
+ crt, crp = self._list.get_cells()
+ self.crt_notify = crt.connect('notify', self.keep_size)
+ super().setup()
+
+ def keep_size(self, crt, *args):
+ """Hack Gtk to keep cells smaller"""
+ crt.handler_block(self.crt_notify)
+ crt.set_property('width', 150)
+ crt.handler_unblock(self.crt_notify)
+
+
+class ImporterWindow(Window):
+ """The window that is in the glade file"""
+ name = 'import_web_image'
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.widget('dl-searching').hide()
+
+ # Add each of the source services from their plug-in modules
+ self.source = self.widget('service_list')
+ self.source_model = self.source.get_model()
+ self.source_model.clear()
+
+ RemoteSource.load(SOURCES)
+ pix = PixmapManager(SOURCES, size=150)
+ for x, (key, source) in enumerate(RemoteSource.sources.items()):
+ if not source.is_enabled:
+ continue
+ # We add them in GdkPixbuf, string, string format (see glade file)
+ self.source_model.append([pix.get(source.icon), source.name, key])
+ if source.is_default:
+ self.source.set_active(x)
+
+ pixmaps = PixmapManager(CACHE_DIR, filters=[
+ SizeFilter(size=150),
+ PadFilter(size=(0, 150)),
+ LicenseOverlay(position=(0.5, 1))
+ ])
+ self.select_func = self.gapp.kwargs['select']
+ self.results = ResultsIconView(self.widget('results'), pixmaps)
+ self.pool = ResultsIconView(self.widget('pool'), pixmaps)
+ self.multiple = False
+
+ self.widget("perm-nocopyright").set_message_type(Gtk.MessageType.WARNING)
+
+ def select_image(self, widget):
+ """Callback when clicking on an image in the result list"""
+ for item_path in self.widget("results").get_selected_items():
+ item_iter = self.results._model.get_iter(item_path)
+ item = self.results._model[item_iter][0]
+ self.show_license(item)
+ self.update_btn_import()
+
+ def show_license(self, item):
+ """Display the current item's license information"""
+ self.widget("perms").foreach(lambda w: w.hide())
+ info = item.license_info
+ for mod in info['modules']:
+ self.widget("perm-" + mod).show()
+
+ def get_selected_source(self):
+ """Return the selected source class"""
+ _iter = self.source.get_active_iter()
+ key = self.source_model[_iter][2]
+ return RemoteSource.sources[key](CACHE_DIR)
+
+ def img_multiple(self, widget):
+ """Enable multi-selection"""
+ widget.hide()
+ self.widget('btn-single').show()
+ self.widget('multiple-box').show()
+ self.widget('multiple-buttons').show()
+ self.multiple = True
+ self.update_btn_import()
+
+ def img_single(self, widget):
+ """Enable single-selection"""
+ widget.hide()
+ self.widget('btn-multiple').show()
+ self.widget('multiple-box').hide()
+ self.widget('multiple-buttons').hide()
+ is_selected = bool(list(self.results.get_selected_items()))
+ self.pool.clear()
+ self.multiple = False
+ self.update_btn_import()
+
+ def img_add(self, widget=None):
+ """Add an item from the search to the multi-pool"""
+ self.pool.add(self.results.get_selected_items())
+ self.update_btn_import()
+
+ def img_remove(self, widget=None):
+ """Remove any selected items from the multi-pool"""
+ for item in self.pool.get_selected_items():
+ self.pool.remove_item(item)
+ self.update_btn_import()
+
+ def update_btn_import(self):
+ """Set the import button to enabled or disabled"""
+ enabled = False
+ if self.multiple:
+ enabled = bool(list(self.pool))
+ else:
+ enabled = bool(list(self.results.get_selected_items()))
+ self.widget('btn-import').set_sensitive(enabled)
+
+ def result_activate(self, widget=None):
+ """Search results double click"""
+ if self.multiple:
+ self.img_add()
+ else:
+ self.img_import()
+
+ def img_import(self, widget=None):
+ """Apply the selected image and quit"""
+ items = []
+ if not self.multiple:
+ items.extend(self.results.get_selected_items())
+ else:
+ items.extend([item for item,*_ in self.pool])
+
+ to_exit = True
+ for item in items:
+ self.select_func(item.get_file())
+ # XXX This pagination control is not good. Replace it with normal controls.
+ #elif isinstance(item, RemotePage):
+ if to_exit:
+ self.exit()
+
+ def search(self, widget):
+ """Remote search activation"""
+ query = widget.get_text()
+ if len(query) > 2:
+ self.search_clear()
+ self.search_started()
+ self.async_search(query)
+
+ @asyncme.run_or_none
+ def async_search(self, query):
+ """Asyncronous searching in PyPI"""
+ for resource in self.get_selected_source().file_search(query):
+ self.add_search_result(resource)
+ self.search_finished()
+
+ @asyncme.mainloop_only
+ def add_search_result(self, resource):
+ """Adding things to Gtk must be done in mainloop"""
+ if isinstance(resource, RemotePage):
+ return self.set_next_page(resource)
+
+ self.results.add_item(resource)
+
+ def search_clear(self, widget=None):
+ """Remove previous search"""
+ self.results.clear()
+ self.update_btn_import()
+ self.next_page_item = None
+ self.widget('btn-next-page').hide()
+
+ def search_started(self):
+ """Set widgets to stun"""
+ self.widget('dl-search').set_sensitive(False)
+ self.widget('dl-searching').start()
+ self.widget('dl-searching').show()
+
+ @asyncme.mainloop_only
+ def search_finished(self):
+ """After everything, finish the search"""
+ self.widget('dl-search').set_sensitive(True)
+ self.widget('dl-searching').hide()
+ self.widget('dl-searching').stop()
+
+ def set_next_page(self, item):
+ self.next_page_item = item
+ self.widget('btn-next-page').show()
+
+ def show_next_page(self, widget=None):
+ item = self.next_page_item
+ if item:
+ self.search_clear()
+ self.search_started()
+ self.async_next_page(item)
+
+ @asyncme.run_or_none
+ def async_next_page(self, item):
+ for resource in item.get_next_page():
+ self.add_search_result(resource)
+ self.search_finished()
+
+
+class App(GtkApp):
+ """Load the inkscape extensions glade file and attach to window"""
+ glade_dir = os.path.join(os.path.dirname(__file__))
+ app_name = 'inkscape-import-web-image'
+ windows = [ImporterWindow]
+
+class ImportWebImage(inkex.EffectExtension):
+ """Import an svg from the internet"""
+ selected_filename = None
+
+ def merge_defs(self, defs):
+ """Add all the items in defs to the self.svg.defs"""
+ target = self.svg.defs
+ for child in defs:
+ if isinstance(child, StyleElement):
+ continue # Already appled in merge_stylesheets()
+ target.append(child)
+
+ def merge_stylesheets(self, svg):
+ """Because we don't want conflicting style-sheets (classes, ids, etc) we scrub them"""
+ elems = defaultdict(list)
+ # 1. Find all styles, and all elements that apply to them
+ for sheet in svg.getroot().stylesheets:
+ for style in sheet:
+ xpath = style.to_xpath()
+ for elem in svg.xpath(xpath):
+ elems[elem].append(style)
+ # 1b. Clear possibly conflicting attributes
+ if '@id' in xpath:
+ elem.set_random_id()
+ if '@class' in xpath:
+ elem.set('class', None)
+ # 2. Apply each style cascade sequentially
+ for elem, styles in elems.items():
+ output = Style()
+ for style in styles:
+ output += style
+ elem.style = output + elem.style
+
+ def import_svg(self, new_svg):
+ """Import an svg file into the current document"""
+ self.merge_stylesheets(new_svg)
+ for child in new_svg.getroot():
+ if isinstance(child, SvgDocumentElement):
+ yield from self.import_svg(child)
+ elif isinstance(child, StyleElement):
+ continue # Already applied in merge_stylesheets()
+ elif isinstance(child, Defs):
+ self.merge_defs(child)
+ elif isinstance(child, (NamedView, Metadata)):
+ pass
+ else:
+ yield child
+
+ def import_from_file(self, filename):
+ if not filename or not os.path.isfile(filename):
+ return
+ with open(filename, 'rb') as fhl:
+ head = fhl.read(100)
+ if b'<?xml' in head or b'<svg' in head:
+ new_svg = load_svg(head + fhl.read())
+ # Add each object to the container
+ objs = list(self.import_svg(new_svg))
+
+ if len(objs) == 1 and isinstance(objs[0], inkex.Group):
+ # Prevent too many groups, if item aready has one.
+ container = objs[0]
+ else:
+ # Make a new group to contain everything
+ container = inkex.Group()
+ for child in objs:
+ container.append(child)
+
+ # Retain the original filename as a group label
+ container.label = os.path.basename(filename)
+ # Apply unit transformation to keep things the same sizes.
+ container.transform.add_scale(self.svg.unittouu(1.0) \
+ / new_svg.getroot().unittouu(1.0))
+
+
+ else:
+ container = self.import_raster(filename, fhl)
+
+ self.svg.get_current_layer().append(container)
+
+ # Make sure that none of the new content is a layer.
+ for child in container.descendants():
+ if isinstance(child, inkex.Group):
+ child.set("inkscape:groupmode", None)
+
+ def effect(self):
+ def select_func(filename):
+ self.import_from_file(filename)
+
+ App(start_loop=True, select=select_func)
+
+ def import_raster(self, filename, handle):
+ """Import a raster image"""
+ # Don't read the whole file to check the header
+ handle.seek(0)
+ file_type = self.get_type(filename, handle.read(10))
+ handle.seek(0)
+
+ if file_type:
+ # Future: Change encodestring to encodebytes when python3 only
+ node = Image()
+ node.label = os.path.basename(filename)
+ node.set('xlink:href', 'data:{};base64,{}'.format(
+ file_type, encodebytes(handle.read()).decode('ascii')))
+ return node
+
+ @staticmethod
+ def get_type(path, header):
+ """Basic magic header checker, returns mime type"""
+ # Taken from embedimage.py
+ for head, mime in (
+ (b'\x89PNG', 'image/png'),
+ (b'\xff\xd8', 'image/jpeg'),
+ (b'BM', 'image/bmp'),
+ (b'GIF87a', 'image/gif'),
+ (b'GIF89a', 'image/gif'),
+ (b'MM\x00\x2a', 'image/tiff'),
+ (b'II\x2a\x00', 'image/tiff'),
+ ):
+ if header.startswith(head):
+ return mime
+ return None
+
+
+if __name__ == '__main__':
+ ImportWebImage().run()