diff options
Diffstat (limited to '')
-rwxr-xr-x | share/extensions/image_extract.py | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/share/extensions/image_extract.py b/share/extensions/image_extract.py new file mode 100755 index 0000000..4454f63 --- /dev/null +++ b/share/extensions/image_extract.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python +# coding=utf-8 +# +# Copyright (C) 2005 Aaron Spike, aaron@ekips.org +# 2022 Jonathan Neuhauser, jonathan.neuhauser@outlook.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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +""" +Extract embedded images. +""" + +import math +import os +from pathlib import Path +from typing import Iterable +from base64 import decodebytes + +import inkex +from inkex.localization import inkex_gettext as _, inkex_ngettext as ngettext + + +class ExtractImage(inkex.EffectExtension): + """Extract images and save to filenames""" + + def add_arguments(self, pars): + pars.add_argument( + "-s", + "--selectedonly", + type=inkex.Boolean, + help="Extract only selected images", + default=True, + ) + pars.add_argument( + "-l", + "--linkextracted", + type=inkex.Boolean, + help="Replace image data with link to image", + default=True, + ) + pars.add_argument( + "--directory", + default="./images/", + help="Location to save the images. " + "If the directory starts with ./, the filename is interpreted " + "relative to the location of the opened file.", + ) + pars.add_argument("--basename", default="", help="Optional file name prefix.") + pars.add_argument( + "--filepath", + default="", + help="Path to a new file. If given, --basename and --directory " + "options are ignored.", + ) + + def __init__(self): + super().__init__() + self.errcount = 0 + + def message(self, elem, message, error=True): + """Write an error message""" + inkex.errormsg(elem.get_id() + ": " + message) + if error: + self.errcount += 1 + + def effect(self): + self.errcount = 0 + + elems: Iterable[inkex.BaseElement] = ( + self.svg.selection.filter(inkex.Image) + if self.options.selectedonly + else self.svg.xpath("//svg:image") + ) + if len(elems) == 0: + return + + filename, directory = self.process_options() + + counter = 1 + for __, elem in enumerate(elems): + data, file_ext = self.prepare(elem) + if data is None: + continue + + # If no filename is set, use id + cname = filename + if cname.strip() == "": + cname = elem.get_id() + elif len(elems) > 1: + # if more than one element is selected and a common filename is used, + # insert ID + while True: + suffix = "_" + str(counter).rjust(int(math.log10(len(elems))) + 1) + if os.path.isfile( + os.path.join(directory, cname + suffix + file_ext) + ): + counter += 1 + else: + cname = cname + suffix + break + + pathwext = os.path.join(directory, cname + file_ext) + if self.save_image(elem, data, pathwext): + + # absolute for making in-mem cycles work + if self.options.linkextracted: + elem.set("xlink:href", Path(os.path.realpath(pathwext)).as_uri()) + counter += 1 + + if self.errcount > 0: + inkex.errormsg( + ngettext( + "{} error occurred", "{} errors occurred.", self.errcount + ).format(self.errcount) + ) + + def process_options(self): + """Prepare directory and base filename, independent of particular images""" + # First case: Extension called from the context menu + if self.options.filepath.strip() != "": + directory, filename = os.path.split(self.options.filepath) + filename, __ = os.path.splitext(filename) + elif self.options.directory.strip() != "": + # If the extension is called from the + # Effects menu, directory is passed and can be absolute or relative + directory = self.options.directory + filename = os.path.splitext(self.options.basename)[0] + + # create the directory if it doesn't exist + directory = self.absolute_href(directory) + try: + os.makedirs(directory, exist_ok=True) + except OSError: + raise inkex.AbortExtension( + _("Unable to create directory {}.").format(directory) + ) + + return filename, directory + + @staticmethod + def mime_to_ext(mime): + """Return a file extension (incl. leading dot) based on the mime type""" + # Most extensions are automatic (i.e. extension is same as minor part of mime type) + part = mime.split("/", 1)[1].split("+")[0] + return "." + { + # These are the non-matching ones. + "svg+xml": "svg", + "jpeg": "jpg", + "icon": "ico", + }.get(part, part) + + def prepare(self, node): + """Check if we can process the data attribute""" + xlink = node.get("xlink:href") + if not xlink.startswith("data:"): + self.message( + node, _("Unable to extract image, is it maybe already linked?") + ) + return None, None # Not embedded image data + + try: + data = xlink[5:] + (mimetype, data) = data.split(";", 1) + (base, data) = data.split(",", 1) + file_ext = self.mime_to_ext(mimetype) + except (ValueError, IndexError): + self.message(node, _("Invalid image format found.")) + return None, None + + if base != "base64": + self.message(node, _("Unable to decode encoding {}.").format(base)) + return None, None + + return data, file_ext + + def save_image(self, node, data, pathwext): + """Save the image contained in the base64-encoded string data to pathwext. + + Returns whether the operation succeded.""" + + if os.path.isfile(pathwext): + self.message( + node, + _("Unable to extract image, file {} already exists.").format(pathwext), + ) + return False + + try: + with open(pathwext, "wb") as fhl: + fhl.write(decodebytes(data.encode("utf-8"))) + except (OSError, ValueError): + self.message(node, _("Unable to write to {}").format(pathwext)) + return False + + self.message(node, _("Image extracted to: {}").format(pathwext), False) + + return True + + +if __name__ == "__main__": + ExtractImage().run() |