From e4fc012a0b275687dc5547aae6c75a07e9ffa956 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Sun, 9 Jan 2022 22:06:58 +0100 Subject: [PATCH] add raster image perspective --- .../raster_perspective/meta.json | 20 +++ .../raster_perspective/raster_perspective.inx | 16 ++ .../raster_perspective/raster_perspective.py | 166 ++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 extensions/fablabchemnitz/raster_perspective/meta.json create mode 100644 extensions/fablabchemnitz/raster_perspective/raster_perspective.inx create mode 100644 extensions/fablabchemnitz/raster_perspective/raster_perspective.py diff --git a/extensions/fablabchemnitz/raster_perspective/meta.json b/extensions/fablabchemnitz/raster_perspective/meta.json new file mode 100644 index 00000000..ffbbd319 --- /dev/null +++ b/extensions/fablabchemnitz/raster_perspective/meta.json @@ -0,0 +1,20 @@ +[ + { + "name": "Raster Perspective", + "id": "fablabchemnitz.de.raster_perspective", + "path": "raster_perspective", + "dependent_extensions": null, + "original_name": "Perspective", + "original_id": "org.test.filter.imagePerspective", + "license": "GNU GPL v3", + "license_url": "https://github.com/s1291/InkRasterPerspective/blob/master/LICENSE", + "comment": "", + "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/raster_perspective", + "fork_url": "https://github.com/s1291/InkRasterPerspective", + "documentation_url": "https://stadtfabrikanten.org/display/IFM/Raster+Perspective", + "inkscape_gallery_url": null, + "main_authors": [ + "github.com/s1291" + ] + } +] \ No newline at end of file diff --git a/extensions/fablabchemnitz/raster_perspective/raster_perspective.inx b/extensions/fablabchemnitz/raster_perspective/raster_perspective.inx new file mode 100644 index 00000000..9b934c55 --- /dev/null +++ b/extensions/fablabchemnitz/raster_perspective/raster_perspective.inx @@ -0,0 +1,16 @@ + + + Raster Perspective + fablabchemnitz.de.raster_perspective + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/raster_perspective/raster_perspective.py b/extensions/fablabchemnitz/raster_perspective/raster_perspective.py new file mode 100644 index 00000000..ef5e1ba1 --- /dev/null +++ b/extensions/fablabchemnitz/raster_perspective/raster_perspective.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2022 Samir OUCHENE, samirmath01@gmail.com + +import os +import sys + +import io +import inkex +from inkex import Image +from PIL import Image as PIL_Image +import base64 +import numpy + + +try: + from base64 import decodebytes +except ImportError: + from base64 import decodestring as decodebytes + + +class RasterPerspective(inkex.Effect): + def __init__(self): + inkex.Effect.__init__(self) + + @staticmethod + 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(self, node): + """Extract the node as if it were an image.""" + xlink = node.get("xlink:href") + if not xlink.startswith("data:"): + return # Not embedded image data + + 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 = self.mime_to_ext(mimetype) + return decodebytes(data.encode("utf-8")) + + def find_coeffs(self, source_coords, target_coords): + matrix = [] + for s, t in zip(source_coords, target_coords): + matrix.append([t[0], t[1], 1, 0, 0, 0, -s[0] * t[0], -s[0] * t[1]]) + matrix.append([0, 0, 0, t[0], t[1], 1, -s[1] * t[0], -s[1] * t[1]]) + A = numpy.array(matrix, dtype=float) + B = numpy.array(source_coords).reshape(8) + res = numpy.linalg.inv(A.T @ A) @ A.T @ B + return numpy.array(res).reshape(8) + + def effect(self): + the_image_node, envelope_node = self.svg.selection + if str(envelope_node) == "image" and str(the_image_node) == "path": + envelope_node, the_image_node = self.svg.selection #switch + + if str(the_image_node) != "image" and str(envelope_node) != "path": + inkex.utils.debug("Your selection must contain an image and a path with at least 4 points.") + return + img_width, img_height = the_image_node.width, the_image_node.height + + try: + unit_to_vp = self.svg.unit_to_viewport + except AttributeError: + unit_to_vp = self.svg.uutounit + + try: + vp_to_unit = self.svg.viewport_to_unit + except AttributeError: + vp_to_unit = self.svg.unittouu + + img_width = unit_to_vp(img_width) + img_height = unit_to_vp(img_height) + + nodes_pts = list(envelope_node.path.control_points) + node1 = (unit_to_vp(nodes_pts[0][0]), unit_to_vp(nodes_pts[0][1])) + node2 = (unit_to_vp(nodes_pts[1][0]), unit_to_vp(nodes_pts[1][1])) + node3 = (unit_to_vp(nodes_pts[2][0]), unit_to_vp(nodes_pts[2][1])) + node4 = (unit_to_vp(nodes_pts[3][0]), unit_to_vp(nodes_pts[3][1])) + + nodes = [node1, node2, node3, node4] + + xMax = max([node[0] for node in nodes]) + xMin = min([node[0] for node in nodes]) + yMax = max([node[1] for node in nodes]) + yMin = min([node[1] for node in nodes]) + # add some assertions (FIXME) + + img_data = self.extract_image(the_image_node) + orig_image = PIL_Image.open(io.BytesIO(img_data)) + pil_img_size = orig_image.size + scale = pil_img_size[0] / img_width + + coeffs = self.find_coeffs( + [ + (0, 0), + (img_width * scale, 0), + (img_width * scale, img_height * scale), + (0, img_height * scale), + ], + [ + (node1[0] - xMin, node1[1] - yMin), + (node2[0] - xMin, node2[1] - yMin), + (node3[0] - xMin, node3[1] - yMin), + (node4[0] - xMin, node4[1] - yMin), + ], + ) + + W, H = xMax - xMin, yMax - yMin + + final_w, final_h = int(W), int(H) + + # Check if the image has transparency + hasTransparency = orig_image.mode in ("RGBA", "LA") or ( + orig_image.mode == "P" and "transparency" in orig_image.info + ) + + transp_img = orig_image + + # If the original image is not transparent, create a new image with alpha channel + if not hasTransparency: + transp_img = PIL_Image.new("RGBA", orig_image.size) + transp_img.format = "PNG" + transp_img.paste(orig_image) + + image = transp_img.transform( + (final_w, final_h), PIL_Image.PERSPECTIVE, coeffs, PIL_Image.BICUBIC + ) + + obj = inkex.Image() + obj.set("x", vp_to_unit(xMin)) + obj.set("y", vp_to_unit(yMin)) + obj.set("width", vp_to_unit(final_w)) + obj.set("height", vp_to_unit(final_h)) + # embed the transformed image + persp_img_data = io.BytesIO() + image.save(persp_img_data, transp_img.format) + mime = PIL_Image.MIME[transp_img.format] + b64 = base64.b64encode(persp_img_data.getvalue()).decode("utf-8") + uri = f"data:{mime};base64,{b64}" + obj.set("xlink:href", uri) + self.svg.add(obj) + + +RasterPerspective = RasterPerspective() +RasterPerspective.run()