From da1c1322c433cba3a6de755c9f146d1038454095 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Mon, 11 Oct 2021 20:42:02 +0200 Subject: [PATCH] added clip-out extension --- .../fablabchemnitz/clip_out/clip_out.inx | 49 +++ .../fablabchemnitz/clip_out/clip_out.py | 318 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 extensions/fablabchemnitz/clip_out/clip_out.inx create mode 100644 extensions/fablabchemnitz/clip_out/clip_out.py diff --git a/extensions/fablabchemnitz/clip_out/clip_out.inx b/extensions/fablabchemnitz/clip_out/clip_out.inx new file mode 100644 index 00000000..bb4b450e --- /dev/null +++ b/extensions/fablabchemnitz/clip_out/clip_out.inx @@ -0,0 +1,49 @@ + + + Clip Out + fablabchemnitz.de.clip_out + + + false + true + + + + + + + + + + + 96 + + None Selected + + + + + + + + + + path + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/clip_out/clip_out.py b/extensions/fablabchemnitz/clip_out/clip_out.py new file mode 100644 index 00000000..adddeda1 --- /dev/null +++ b/extensions/fablabchemnitz/clip_out/clip_out.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +# +# Copyright (C) [2021] [Matt Cottam], [mpcottam@raincloud.co.uk] +# +# 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. +# +# +# ############################################################################# +# Clip Out - Export multiple clipped images using paths / shapes and a background image +# After setting the options in the main dialogue +# Assign a shortcut in Inkscape Edit>Preferences>Interface>Keyboard to org.inkscape.inklinea.clip_out.noprefs +# For shortcut triggered quick export +# It does require that you have saved +# Your svg file at least once before using ( will not work on an unsaved svg ) +# Requires Inkscape 1.1+ --> +# ############################################################################# +import random + +import inkex +from inkex import command +from pathlib import Path +from datetime import datetime +import tempfile, shutil, os +from lxml import etree + +import time + +conversions = { + 'in': 96.0, + 'pt': 1.3333333333333333, + 'px': 1.0, + 'mm': 3.779527559055118, + 'cm': 37.79527559055118, + 'm': 3779.527559055118, + 'km': 3779527.559055118, + 'Q': 0.94488188976378, + 'pc': 16.0, + 'yd': 3456.0, + 'ft': 1152.0, + '': 1.0, # Default px +} + + +def make_temp_svg(self): + temp_svg_file = tempfile.NamedTemporaryFile(mode='r+', delete='false', suffix='.svg') + # Write the contents of the updated svg to a tempfile to use with command line + my_svg_string = self.svg.root.tostring().decode("utf-8") + temp_svg_file.write(my_svg_string) + return temp_svg_file + + +def inkscape_command_line_export(self, my_temp_svg_filename_path, my_export_path, my_options, export_png_actions): + if Path(my_export_path).is_dir(): + inkex.command.inkscape(my_temp_svg_filename_path, my_options, export_png_actions) + else: + inkex.errormsg('Please Select An Export Folder') + + +def make_image_frame(self, background_image): + found_units = self.svg.unit + + unit_conversion_factor = conversions[found_units] + + bbox_x = background_image.bounding_box().left / unit_conversion_factor + bbox_y = background_image.bounding_box().top / unit_conversion_factor + bbox_width = background_image.bounding_box().width / unit_conversion_factor + bbox_height = background_image.bounding_box().height / unit_conversion_factor + + top_left = str(f'{bbox_x} {bbox_y}') + top_right_x = str(bbox_x + bbox_width) + top_right_y = str(bbox_y) + bottom_right_x = str(bbox_x + bbox_width) + bottom_right_y = str(bbox_y + bbox_height) + bottom_left_x = str(bbox_x) + bottom_left_y = str(bbox_y + bbox_height) + + rect_path = f'M {top_left} L {top_right_x} {top_right_y} L {bottom_right_x} {bottom_right_y} L {bottom_left_x} {bottom_left_y} Z' + + path_id = 'inverse_clip_frame' + str(random.randrange(10000, 99999)) + + parent = self.svg.get_current_layer() + + rect_path_object = etree.SubElement(parent, inkex.addNS('path', 'svg')) + + rect_path_object.attrib['id'] = path_id + + rect_path_object.attrib['d'] = rect_path + + rect_path_object.style['stroke'] = 'black' + rect_path_object.style['stroke-width'] = '1px' + + return rect_path_object.get_id() + + +def command_line_call(self): + # current date and time to time stamp + timestamp = datetime.today().replace(microsecond=0) + timestamp_suffix = str(timestamp.strftime('%Y-%m-%d-%H-%M-%S')) + + # Get export path + my_export_path = self.options.save_path + + # Get name of currently open Inkscape file + my_filename = self.svg.name + + # Check to see if user has saved file at least once + if len(my_filename) < 2: + inkex.errormsg('Please Save Your File First') + return + + # Get png dpi setting + png_dpi = self.options.png_dpi + + # Get crop settings + if self.options.canvas_to_selection == 'true': + canvas_to_selection = 'FitCanvasToSelection;' + is_cropped = 'cropped_' + else: + canvas_to_selection = '' + is_cropped = '' + + # Look at selection list 1st item must be background image + my_objects = self.svg.selected + # Exit if less than 2 objects are selected + if len(my_objects) < 2: + return + if my_objects[-1].TAG != 'image': + inkex.errormsg('Last Selected Object Must Be An Image') + return + + # Clip background image by each object and export to png + + my_background = my_objects[-1] + my_background_id = my_background.get_id() + + # Create a rectangular path same size as image to be clipped used for inverse only + image_frame_id = make_image_frame(self, my_background) + + # my_temp_filename_path = make_temp_svg(my_file_path, my_filename) + my_temp_svg_file = make_temp_svg(self) + my_temp_svg_filename_path = my_temp_svg_file.name + + for my_object in my_objects: + # This loop looks at each clipping object, ignores any image objects + if my_object.TAG != 'image': + my_object_id = my_object.get_id() + + # Build a formatted string for command line actions + + # --batch-process ( or --with-gui ) is required if verbs are used in addition to actions + my_options = '--batch-process' + + my_actions = '--actions=' + + export_png_actions = '' + + # For Positive Clip + if self.options.clip_type_inverse == 'false': + + # Creates individual object clipped files + if self.options.output_set == 'separate' or self.options.output_set == 'master_and_separate': + my_png_export_filename_path = my_export_path + '/' + my_filename.replace('.svg', + '_' + my_object_id + '_' + is_cropped + timestamp_suffix + '.png') + + export_png_actions = my_actions + f'select-by-id:{my_background_id}; \ + SelectionToBack; \ + select-by-id:{my_background_id},{my_object_id}; \ + select-invert; \ + EditDelete; \ + select-all; \ + ObjectSetClipPath; \ + select-all; \ + {canvas_to_selection} \ + export-filename:{my_png_export_filename_path}; \ + export-dpi:{png_dpi}; \ + export-do' + + export_png_actions = export_png_actions.replace(' ', '') + + inkscape_command_line_export(self, my_temp_svg_filename_path, my_export_path, my_options, + export_png_actions) + + # For Inverse Clip + else: + if self.options.output_set == 'separate' or self.options.output_set == 'master_and_separate': + my_png_export_filename_path = my_export_path + '/' + my_filename.replace('.svg', + '_' + my_object_id + '_''inverse_' + is_cropped + timestamp_suffix + '.png') + + export_png_actions = my_actions + f'select-by-id:{image_frame_id},{my_object_id},{my_background_id}; \ + select-invert; \ + EditDelete; \ + select-by-id:{image_frame_id}; \ + SelectionToBack; \ + EditDeselect; \ + select-by-id:{image_frame_id},{my_object_id}; \ + SelectionSymDiff; \ + EditDeselect; \ + select-by-id:{my_background_id}; \ + SelectionToBack; \ + select-all; \ + ObjectSetClipPath; \ + select-all; \ + {canvas_to_selection} \ + export-filename:{my_png_export_filename_path}; \ + export-dpi:{png_dpi}; \ + export-do' + + export_png_actions = export_png_actions.replace(' ', '') + + inkscape_command_line_export(self, my_temp_svg_filename_path, my_export_path, my_options, + export_png_actions) + + if self.options.output_set == 'master_only' or self.options.output_set == 'master_and_separate': + + # Creates a master image with all clipped objects + my_actions = '--actions=' + my_object_id_list = '' + + for my_object in my_objects: + if my_object.TAG != 'image': + # Build select ID string length - Windows has a max cmdline string length of 8192 + my_object_id = my_object.get_id() + my_object_id_list += f'{my_object_id},' + # Remove last comma from id list + my_object_id_list = my_object_id_list.rstrip(',') + + if self.options.clip_type_inverse == 'false': + + my_png_export_filename_path = my_export_path + '/' + my_filename.replace('.svg', + '_' + 'master_' + is_cropped + timestamp_suffix + '.png') + + export_png_actions = my_actions + f' \ + select-by-id:{my_object_id_list},{my_background_id}; \ + select-invert; \ + EditDelete; \ + select-clear; \ + select-by-id:{my_object_id_list}; \ + SelectionCombine; \ + select-clear; \ + select-by-id:{my_background_id}; \ + SelectionToBack; \ + select-clear; \ + select-all; \ + ObjectSetClipPath; \ + select-all; \ + {canvas_to_selection} \ + export-filename:{my_png_export_filename_path}; \ + export-dpi:{png_dpi}; \ + export-do;' + + else: + + my_png_export_filename_path = my_export_path + '/' + my_filename.replace('.svg', + '_' + 'inverse_master_' + is_cropped + timestamp_suffix + '.png') + + export_png_actions = my_actions + f' \ + select-by-id:{my_object_id_list},{my_background_id},{image_frame_id}; \ + select-invert; \ + EditDelete; \ + select-by-id:{my_object_id_list}; \ + SelectionCombine; \ + select-clear; \ + select-by-id:{image_frame_id}; \ + SelectionToBack; \ + select-all; \ + unselect-by-id:{my_background_id}; \ + SelectionSymDiff; \ + select-clear; \ + select-by-id:{my_background_id}; \ + SelectionToBack; \ + select-all; \ + ObjectSetClipPath; \ + select-all; \ + {canvas_to_selection} \ + export-filename:{my_png_export_filename_path}; \ + export-dpi:{png_dpi}; \ + export-do;' + + export_png_actions = export_png_actions.replace(' ', '') + + inkscape_command_line_export(self, my_temp_svg_filename_path, my_export_path, my_options, export_png_actions) + + # Remove rectangular path + image_frame = self.svg.getElementById(image_frame_id) + image_frame.delete() + + # Close temp file + my_temp_svg_file.close() + +class QuickExport(inkex.EffectExtension): + + def add_arguments(self, pars): + pars.add_argument("--clip_type_inverse", default='false') + pars.add_argument("--shape_unit_fix", default='false') + pars.add_argument("--notebook_main", default=0) + pars.add_argument("--output_set", default=0) + pars.add_argument("--canvas_to_selection", default=0) + pars.add_argument("--save_path", default=str(Path.home())) + pars.add_argument("--png_dpi", type=int, default=96) + + def effect(self): + command_line_call(self) + + +if __name__ == '__main__': + QuickExport().run()