dots/config/inkscape/extensions/org.inkscape.extension.30306/scientific_inkscape/image_helpers.py
2026-06-05 13:11:08 +02:00

675 lines
21 KiB
Python

# Stuff copied from image_extract and image_embed
#!/usr/bin/env python
# coding=utf-8
#
# Copyright (C) 2005,2007 Aaron Spike, aaron@ekips.org
#
# 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.
"""
from __future__ import unicode_literals
import os
import dhelpers # noqa
import inkex
from inkex import Image
try:
from base64 import decodebytes
from base64 import b64encode
except ImportError:
from base64 import decodestring as decodebytes
from base64 import b64encode
def mime_to_ext(mime):
"""Return an extension 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 extract_image(node, save_to):
"""Extract the node as if it were an image."""
xlink = node.get("xlink:href")
if not xlink.startswith("data:"):
return # Not embedded image data
# This call will raise AbortExtension if the document wasn't saved
# and the user is trying to extract them to a relative directory.
# save_to = self.absolute_href(self.options.filepath, default=None)
# Make the target directory if it doesn't exist yet.
if not os.path.isdir(save_to):
os.makedirs(save_to)
try:
data = xlink[5:]
(mimetype, data) = data.split(";", 1)
(base, data) = data.split(",", 1)
except ValueError:
inkex.errormsg("Invalid image format found")
return
if base != "base64":
inkex.errormsg("Can't decode encoding: {}".format(base))
return
file_ext = mime_to_ext(mimetype)
pathwext = os.path.join(save_to, node.get("id") + file_ext)
if os.path.isfile(pathwext):
inkex.errormsg(
"Can't extract image, filename already used: {}".format(pathwext)
)
return
# self.msg('Image extracted to: {}'.format(pathwext))
with open(pathwext, "wb") as fhl:
fhl.write(decodebytes(data.encode("utf-8")))
# absolute for making in-mem cycles work
node.set("xlink:href", os.path.realpath(pathwext))
"""
Embed images so they are base64 encoded data inside the svg.
"""
from inkex.localization import inkex_gettext as _
try:
import urllib.request as urllib
import urllib.parse as urlparse
from base64 import encodebytes
except ImportError:
# python2 compatibility, remove when python3 only.
import urllib
import urlparse
from base64 import encodestring as encodebytes
def embed_image(node, svg_dir):
"""Embed the data of the selected Image Tag element"""
xlink = node.get("xlink:href")
if xlink is not None and xlink[:5] == "data:":
# No need, data already embedded
return
if xlink is None:
inkex.errormsg(
_('Attribute "xlink:href" not set on node {}.'.format(node.get_id()))
)
return
url = urlparse.urlparse(xlink)
href = urllib.url2pathname(url.path)
# Primary location always the filename itself, we allow this
# call to search the user's home folder too.
path = absolute_href2(href or "", svg_dir)
# Backup directory where we can find the image
if not os.path.isfile(path):
path = node.get("sodipodi:absref", path)
if not os.path.isfile(path):
inkex.errormsg(_('File not found "{}". Unable to embed image.').format(path))
return
with open(path, "rb") as handle:
# Don't read the whole file to check the header
file_type = get_type(path, handle.read(10))
handle.seek(0)
if file_type:
# Future: Change encodestring to encodebytes when python3 only
node.set(
"xlink:href",
"data:{};base64,{}".format(
file_type, encodebytes(handle.read()).decode("ascii")
),
)
node.pop("sodipodi:absref")
else:
inkex.errormsg(
_(
"%s is not of type image/png, image/jpeg, "
"image/bmp, image/gif, image/tiff, or image/x-icon"
)
% path
)
def embed_external_image(el, filename):
"""Embed the data of the selected Image Tag element"""
if filename is not None:
with open(filename, "rb") as handle:
# Don't read the whole file to check the header
file_type = get_type(filename, handle.read(10))
handle.seek(0)
if file_type:
# Future: Change encodestring to encodebytes when python3 only
el.set(
"xlink:href",
"data:{};base64,{}".format(
file_type, encodebytes(handle.read()).decode("ascii")
),
)
el.pop("sodipodi:absref")
else:
inkex.errormsg(
_(
"%s is not of type image/png, image/jpeg, "
"image/bmp, image/gif, image/tiff, or image/x-icon"
)
% filename
)
# Check if image is linked or embedded. If linked, check if path is valid
def check_linked(node, svg_dir):
"""Embed the data of the selected Image Tag element"""
xlink = node.get("xlink:href")
if xlink is not None and xlink[:5] == "data:":
return False, None
url = urlparse.urlparse(xlink)
href = urllib.url2pathname(url.path)
path = absolute_href2(href or "", svg_dir)
if not os.path.isfile(path):
path = node.get("sodipodi:absref", path)
fileexists = os.path.isfile(path)
if not (fileexists):
return True, None
else:
return True, path
# Gets the type of an image element
def get_image_type(node, svg_dir):
xlink = node.get("xlink:href")
if not xlink.startswith("data:"):
# Linked image
xlink = node.get("xlink:href")
if xlink is not None and xlink[:5] == "data:":
# No need, data already embedded
return None
if xlink is None:
print(_('Attribute "xlink:href" not set on node {}.'.format(node.get_id())))
return None
url = urlparse.urlparse(xlink)
href = urllib.url2pathname(url.path)
# Primary location always the filename itself, we allow this
# call to search the user's home folder too.
path = absolute_href2(href or "", svg_dir)
# Backup directory where we can find the image
if not os.path.isfile(path):
path = node.get("sodipodi:absref", path)
if not os.path.isfile(path):
print(_('File not found "{}". Unable to embed image.').format(path))
return None
with open(path, "rb") as handle:
# Don't read the whole file to check the header
file_type = get_type(path, handle.read(10))
return file_type
else:
try:
data = xlink[5:]
(mimetype, data) = data.split(";", 1)
(base, data) = data.split(",", 1)
except ValueError:
print("Invalid image format found")
return None
if base != "base64":
print("Can't decode encoding: {}".format(base))
return None
return mimetype
def get_type(path, header):
"""Basic magic header checker, returns mime type"""
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
# ico files lack any magic... therefore we check the filename instead
for ext, mime in (
# official IANA registered MIME is 'image/vnd.microsoft.icon' tho
(".ico", "image/x-icon"),
(".svg", "image/svg+xml"),
):
if path.endswith(ext):
return mime
return None
# if __name__ == '__main__':
# EmbedImage().run()
# Modification to the built-in absolute_href function that doesn't require an
# extension class
def absolute_href2(filename, svg_dir, default="~/"):
"""
Process the filename such that it's turned into an absolute filename
with the working directory being the directory of the loaded svg.
User's home folder is also resolved. So '~/a.png` will be `/home/bob/a.png`
Default is a fallback working directory to use if the svg's filename is not
available, if you set default to None, then the user will be given errors if
there's no working directory available from Inkscape.
"""
filename = os.path.expanduser(filename)
if not os.path.isabs(filename):
filename = os.path.expanduser(filename)
if not os.path.isabs(filename):
filename = os.path.join(svg_dir, filename)
return os.path.realpath(os.path.expanduser(filename))
# Stuff by David Burghoff
try:
from PIL import Image as ImagePIL
hasPIL = True
except:
hasPIL = False
# def remove_alpha(imin):
# background = ImagePIL.new('RGBA', imin.size, (255,255,255))
# alpha_composite = ImagePIL.alpha_composite(background, imin)
# alpha_composite_3 = alpha_composite.convert('RGB')
# return alpha_composite_3
# def remove_alpha(imin, background):
# # background = ImagePIL.new('RGBA', imin.size, (255,255,255))
# alpha_composite = ImagePIL.alpha_composite(background, imin)
# alpha_composite_3 = alpha_composite.convert("RGB")
# return alpha_composite_3
# Convert and crop transparent image to JPG
# Requires the transparent version (imin) as well as the opaque version (opaqueimin)
# def to_jpeg(imin, opaqueimin, imout):
# with ImagePIL.open(imin) as im:
# with ImagePIL.open(opaqueimin) as oim:
# bbox = im.getbbox()
# # # Composite to remove transparent regions
# # compim = remove_alpha(im, imb)
# # Crop to non-transparent region only
# # left,upper,right,lower (left & upper pixel is non-zero corner, right-1 & lower-1 is non-zero corner)
# if bbox is not None:
# oim = oim.crop(bbox)
# bbox = [
# bbox[0] / im.size[0],
# bbox[1] / im.size[1],
# bbox[2] / im.size[0],
# bbox[3] / im.size[1],
# ]
# # normalize to original size
# oim.convert('RGB').save(imout)
# return imout, bbox
def to_jpeg(imin, imout):
with ImagePIL.open(imin) as im:
im.convert("RGB").save(imout)
def crop_image(imin):
with ImagePIL.open(imin) as im:
bbox = im.getbbox()
# left,upper,right,lower (left & upper pixel is non-zero corner, right-1 & lower-1 is non-zero corner)
if bbox is not None:
cropim = im.crop(bbox)
bbox = [
bbox[0] / im.size[0],
bbox[1] / im.size[1],
bbox[2] / im.size[0],
bbox[3] / im.size[1],
]
# normalized to original size
cropim.save(imin)
return imin, bbox
# Extract an embedded image
def extract_image_simple(node, save_to_base):
"""Extract the node as if it were an image."""
xlink = node.get("xlink:href")
data = xlink[5:]
try:
data = xlink[5:]
(mimetype, data) = data.split(";", 1)
(base, data) = data.split(",", 1)
except ValueError:
return None
file_ext = mime_to_ext(mimetype)
pathwext = save_to_base + file_ext
# inkex.utils.debug(pathwext)
with open(pathwext, "wb") as fhl:
fhl.write(decodebytes(data.encode("utf-8")))
return pathwext
# Get the size of an embedded image
def embedded_size(node):
xlink = node.get("xlink:href")
try:
data = xlink[5:]
(mimetype, data) = data.split(";", 1)
(base, data) = data.split(",", 1)
return len(decodebytes(data.encode("utf-8")))
except (ValueError, TypeError):
return None
# Get the data string of an embedded image with the alpha stripped out
# This allows images to be identified after conversion to PDF
def Stripped_Alpha_String(el):
import tempfile
tf = tempfile.NamedTemporaryFile().name
fullpath = extract_image_simple(el, tf)
with ImagePIL.open(fullpath) as im:
newfile = strip_ext(fullpath) + ".png"
return str(im.size)
# im.convert('RGB').save(newfile)
# with open(newfile, "rb") as handle:
# # Don't read the whole file to check the header
# file_type = get_type(newfile, handle.read(10))
# handle.seek(0)
# if file_type:
# return "data:{};base64,{}".format(
# file_type, encodebytes(handle.read()).decode("ascii"))
# return None
def Make_Data_Image(datastr):
data = [ord(c) for c in datastr]
# inkex.utils.debug(data)
import numpy as np
im = ImagePIL.fromarray(np.array([data], dtype="uint8"))
import tempfile
tf = tempfile.NamedTemporaryFile().name
newfile = tf + ".png"
im.save(newfile)
with open(newfile, "rb") as handle:
# Don't read the whole file to check the header
file_type = get_type(newfile, handle.read(10))
handle.seek(0)
if file_type:
return "data:{};base64,{}".format(
file_type, encodebytes(handle.read()).decode("ascii")
)
return None
def Read_Data_Image(imstr):
data = imstr[5:]
(mimetype, data) = data.split(";", 1)
(base, data) = data.split(",", 1)
import io
im = ImagePIL.open(io.BytesIO(decodebytes(data.encode("utf-8"))))
import numpy as np
npa = np.asarray(im)
if len(npa.shape) == 3:
data = npa[:, :, 0]
else:
data = npa
return "".join([chr(v) for v in list(data.ravel())])
import io
def str_to_ImagePIL(imstr):
try:
data = imstr[5:]
(mimetype, data) = data.split(";", 1)
(base, data) = data.split(",", 1)
im = ImagePIL.open(io.BytesIO(decodebytes(data.encode("utf-8"))))
return im
except:
return None
def ImagePIL_to_str(im):
try:
img_byte_arr = io.BytesIO()
im.save(img_byte_arr, format="png")
vals = img_byte_arr.getvalue()
file_type = get_type(None, vals[0:10])
if file_type:
return "data:{};base64,{}".format(
file_type, encodebytes(vals).decode("ascii")
)
except:
return None
# Get just the alpha channel of an image, which can be turned into a mask
# def make_alpha_mask(fin,maskout):
# from PIL import Image, ImageOps
# with Image.open(fin) as im:
# (r,g,b,a)=im.split()
# # (r,g,b)=Image.new('RGB',im.size,'black').split()
# # nim = Image.merge('RGBA',(r,g,b,ImageOps.invert(a)))
# # (r,g,b)=Image.new('RGB',im.size,'black').split()
# nim = Image.merge('L',(a,))
# nim.save(maskout)
# # Get just the alpha channel of an image, which can be turned into a mask
# def make_rgb(fin,rgbout):
# from PIL import Image, ImageOps
# with Image.open(fin) as im:
# (r,g,b,a)=im.split()
# # (r,g,b)=Image.new('RGB',im.size,'black').split()
# # nim = Image.merge('RGBA',(r,g,b,ImageOps.invert(a)))
# # (r,g,b)=Image.new('RGB',im.size,'black').split()
# nim = Image.merge('RGB',(r,g,b))
# nim.save(rgbout)
# Strips image extensions
def strip_ext(fnin):
# strip existing extension
if fnin[-4:].lower() in [".png", ".gif", "jpg", "tif"]:
fnin = fnin[0:-4]
if fnin[-5:].lower() in ["jpeg", "tiff"]:
fnin = fnin[0:-5]
return fnin
# Extracts embedded images or returns the path of linked ones
def extract_img_file(el, svg_dir, newpath):
islinked, validpath = check_linked(el, svg_dir)
if islinked and validpath is not None:
impath = validpath
madenew = False
elif islinked and validpath is None:
impath = None
madenew = False
else:
# inkex.utils.debug(newpath)
extract = extract_image_simple(el, strip_ext(newpath))
if extract is not None:
impath = extract
madenew = True
else:
impath = None
madenew = False
return impath, islinked
# For images with alpha=0 pixels, set the RGB of those pixels based on another
# image. This is usually the same image with a background and with other objects.
# For those pixels, alpha is then set to 1 (out of 255), which prevents the PDF
# renderer from replacing those pixels with black. This avoids the 'gray ring'
# issue that can happen on PDF exports.
def Set_Alpha0_RGB(img, imgref):
im1 = ImagePIL.open(img).convert("RGBA")
im2 = ImagePIL.open(imgref).convert("RGBA")
import numpy as np
d1 = np.asarray(im1)
d2 = np.asarray(im2)
a = d1[:, :, 3]
nd = np.stack(
(
np.where(a == 0, d2[:, :, 0], d1[:, :, 0]),
np.where(a == 0, d2[:, :, 1], d1[:, :, 1]),
np.where(a == 0, d2[:, :, 2], d1[:, :, 2]),
np.where(a == 0, 1 * np.ones_like(a), a),
),
2,
)
ImagePIL.fromarray(nd).save(img)
# inkex.utils.debug(img)
anyalpha0 = np.where(a == 0, True, False).any()
return anyalpha0
# Crop a list of images based on the transparency of the first one
# Returns the normalized bounding box, which we need later
def crop_images(ims_in):
bbox = None
with ImagePIL.open(ims_in[0]) as ref_im:
bbox = ref_im.getbbox()
nsz = ref_im.size
if bbox is not None:
# left,upper,right,lower (left & upper pixel is non-zero corner, right-1 & lower-1 is non-zero corner)
nbbox = [
bbox[0] / nsz[0],
bbox[1] / nsz[1],
bbox[2] / nsz[0],
bbox[3] / nsz[1],
] # normalize to original size
for imf in ims_in:
with ImagePIL.open(imf) as im:
im.crop(bbox).save(imf)
return nbbox
else:
return None
# Get the absolute locations of all linked images when called by an extension
# Needed because the temp file has a different location from the actual one
def get_linked_locations(slf):
llocations = dict()
images = slf.svg.xpath("//svg:image")
for node in images:
xlink = node.get("xlink:href")
if xlink is not None and xlink[:5] != "data:":
try:
import urllib.request as urllib
import urllib.parse as urlparse
except ImportError:
# python2 compatibility, remove when python3 only.
import urllib
import urlparse
url = urlparse.urlparse(xlink)
href = urllib.url2pathname(url.path)
# Look relative to the *temporary* filename instead of the original filename.
try: # v1.2 forward
path = slf.absolute_href(
href or "", cwd=os.path.dirname(slf.options.input_file)
)
except: # pre-v1.2
# Primary location always the filename itself, we allow this
# call to search the user's home folder too.
path = slf.absolute_href(href or "")
# Backup directory where we can find the image
if not os.path.isfile(path):
path = node.get("sodipodi:absref", path)
if os.path.isfile(path):
llocations[node.get_id()] = path
else:
llocations[node.get_id()] = None
return llocations
# Get the absolute locations of all linked images when the absolute path is known
def get_linked_locations_file(fin, svg):
llocations = dict()
images = svg.xpath("//svg:image")
for node in images:
xlink = node.get("xlink:href")
if xlink is not None and xlink[:5] != "data:":
try:
import urllib.request as urllib
import urllib.parse as urlparse
except ImportError:
# python2 compatibility, remove when python3 only.
import urllib
import urlparse
url = urlparse.urlparse(xlink)
href = urllib.url2pathname(url.path)
path = absolute_href2(href or "", os.path.dirname(fin))
# Backup directory where we can find the image
if not os.path.isfile(path):
path = node.get("sodipodi:absref", path)
if os.path.isfile(path):
llocations[node.get_id()] = path
else:
llocations[node.get_id()] = None
return llocations