#!/usr/bin/env python3 # coding=utf-8 # # Copyright (C) 2022 Mikael Nordin, hej@mic.ke # # 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. # """ Description of this extension """ import gi import math import io import inkex import base64 from inkex import Image as IImage from PIL import Image gi.require_version("Gtk", "3.0") gi.require_version("Gdk", "3.0") from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, Gio # noqa: E402 class GeoReferencerPoint(): def __init__( self, x: float, y: float, to_x: float = 0, to_y: float = 0 ) -> None: self.x: float = x self.y: float = y self.to_x: float = to_x self.to_y: float = to_y class GeoReferencer(): def __init__(self, window) -> None: self.points: list[GeoReferencerPoint] = list() self.window = window if 'pilimage' in self.window: self.img = self.window.pilimage def add_point(self, point: GeoReferencerPoint) -> None: self.points.append(point) def add_img(self, img) -> None: self.img = img def do_transform(self): if len(self.points) < 2: self.window.message_box( "Not enough points for georeferencing available.") return None ratio = get_scale_factor(self.points[0], self.points[1]) angle = get_rotation_angle(self.points[0], self.points[1]) width = int(self.img.width * ratio) height = int(self.img.height * ratio) self.window.message_box( "w: {}, h: {}, s: {}, a: {}".format(width, height, ratio, angle)) resized = self.img.resize( (width, height), Image.Resampling.LANCZOS) rotated = resized.rotate(angle) return rotated class GeoReferencerWindow(Gtk.Window): def __init__(self, pilimage=None) -> None: super().__init__(title="Georeferencer") if pilimage: self.add_image(pilimage) self.georeferencer: GeoReferencer = GeoReferencer(self) self.set_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK) self.box: Gtk.Box = Gtk.Box() self.add(self.box) self.button: Gtk.Button = Gtk.Button(label="Georeference") self.button.connect("clicked", self.on_button_clicked) self.box.pack_start(self.button, True, True, 0) self.event_box: Gtk.EventBox = Gtk.EventBox() self.event_box.set_above_child(above_child=True) self.event_box.connect('motion-notify-event', self.on_motion_notify) self.event_box.connect('enter-notify-event', self.on_motion_notify) self.event_box.connect('button-press-event', self.on_mouse_click) self.box.pack_start(self.event_box, True, True, 0) self.connect("destroy", Gtk.main_quit) self.show_all() def message_box(self, message: str): dialog = Gtk.MessageDialog(transient_for=self, modal=False, destroy_with_parent=True, message_type=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK, text=message) dialog.show_all() dialog.run() dialog.destroy() def on_button_clicked(self, widget) -> None: gtkimage = pil_to_gtk(self.georeferencer.do_transform()) self.event_box.remove(self.gtkimage) self.gtkimage = gtkimage self.event_box.add(self.gtkimage) self.gtkimage.show_all() self.queue_draw() # self.destroy() def on_motion_notify(self, widget, event) -> None: widget.set_tooltip_text("x:{}, y:{}".format(event.x, event.y)) def on_mouse_click(self, widget, event) -> None: dialog = Gtk.MessageDialog(transient_for=self, modal=True, destroy_with_parent=True, message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.OK_CANCEL, text="Set new coordinates") point_box = dialog.get_content_area() x_label = Gtk.Label() y_label = Gtk.Label() x_label.set_text("x:") y_label.set_text("y:") x_entry = Gtk.Entry() y_entry = Gtk.Entry() point_box.pack_start(x_label, True, True, 0) point_box.pack_start(x_entry, True, True, 0) point_box.pack_start(y_label, True, True, 0) point_box.pack_start(y_entry, True, True, 0) dialog.show_all() response = dialog.run() if response == Gtk.ResponseType.OK: if x_entry.get_text() and y_entry.get_text(): to_x = float(x_entry.get_text()) to_y = float(y_entry.get_text()) point = GeoReferencerPoint(event.x, event.y, to_x, to_y) self.georeferencer.add_point(point) dialog.destroy() def add_image(self, pilimage, mimetype): self.pilimage = pilimage self.georeferencer.add_img(pilimage) self.gtkimage = pil_to_gtk(pilimage, mimetype, window=self) self.event_box.add(self.gtkimage) self.event_box.show_all() def pil_to_gtk(pilimage: Image, mimetype, window: GeoReferencerWindow = None): if window: msgfunc = window.message_box else: msgfunc = print glibbytes = GLib.Bytes.new(pilimage.tobytes()) if mimetype == "image/png": glibdata = glibbytes.get_data() gdkpixbuf = GdkPixbuf.Pixbuf.new_from_data( glibdata, GdkPixbuf.Colorspace.RGB, True, 8, pilimage.width, pilimage.height, len( pilimage.getbands( ))*pilimage.width, None, None) elif mimetype == "image/jpeg": glibstream = Gio.MemoryInputStream.new_from_bytes(glibbytes) gdkpixbuf = GdkPixbuf.Pixbuf.new_from_stream(glibstream, None) else: msgfunc("Unknown mimetype, can only handle jpg and png") gtkimage = Gtk.Image() gtkimage.set_from_pixbuf(gdkpixbuf) return gtkimage def get_scale_factor(a: GeoReferencerPoint, b: GeoReferencerPoint) -> float: mag_v = ((b.x - a.x)*(b.x - a.x)) + ((b.y - a.y)*(b.y - a.y)) mag_w = ((b.to_x - a.to_x)*(b.to_x - a.to_x)) + \ ((b.to_y - a.to_y)*(b.to_y - a.to_y)) return mag_w/mag_v def get_dot_product(a: GeoReferencerPoint, b: GeoReferencerPoint) -> float: a1 = b.x-a.x a2 = b.to_x - a.to_x b1 = b.y - a.y b2 = b.to_y - a.to_y return (a1*b1) + (a2*b2) def get_rotation_angle(a: GeoReferencerPoint, b: GeoReferencerPoint) -> float: dot = get_dot_product(a, b) mag_v = ((b.x - a.x)*(b.x - a.x)) + ((b.y - a.y)*(b.y - a.y)) mag_w = ((b.to_x - a.to_x)*(b.to_x - a.to_x)) + \ ((b.to_y - a.to_y)*(b.to_y - a.to_y)) return (((dot/mag_v) / mag_w)*(180/math.pi)) class CoordinatesExtension(inkex.EffectExtension): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.window: GeoReferencerWindow = GeoReferencerWindow() self.image_mimetype = str() def effect(self) -> None: images = self.svg.selection.get(IImage) pilimages = list() for elem in images: xlink = elem.get("xlink:href") if xlink is not None and xlink[:5] == "data:": data = xlink[5:] (mimetype, data) = data.split(";", 1) self.image_mimetype = mimetype (base, data) = data.split(",", 1) if base != "base64": self.window.message_box( "Unable to decode encoding {}.").format(base) return else: imgdata = base64.b64decode(data) pilimage = Image.open(io.BytesIO(imgdata)) pilimages.append(pilimage) if len(pilimages) == 0: self.window.message_box("Could not find image in selection") self.window.show_all() self.window.destroy() else: breakout = False for pilimage in pilimages: try: self.window.add_image(pilimage, mimetype) breakout = True except gi.repository.GLib.GError as e: self.window.message_box( "Could not load image type: {}, error was: {}".format( mimetype, e)) breakout = False if breakout: break if breakout: self.window.show_all() Gtk.main() else: self.window.destroy() if __name__ == '__main__': CoordinatesExtension().run()