This commit is contained in:
2021-07-23 02:36:56 +02:00
commit 4d622c5291
4878 changed files with 1849508 additions and 0 deletions

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Export Selection As ...</name>
<id>fablabchemnitz.de.export_selection_as</id>
<param name="tab" type="notebook">
<page name="tab_settings" gui-text="Settings">
<param name="wrap_transform" type="bool" gui-text="Wrap final document in transform">false</param>
<param name="border_offset" type="float" min="0.000" max="9999.000" precision="3" gui-text="Add border offset around selection">1.000</param>
<param name="export_dir" type="path" mode="folder" gui-text="Location to save exported documents">./inkscape_export/</param>
<param name="opendir" type="bool" gui-text="Open containing output directory after export">false</param>
<param name="dxf_exporter_path" type="path" mode="file" filetypes="py" gui-text="Location of dxf_outlines.py" gui-description="Do not use dxf12_outlines.py! This will try to create R12 DXF files, which will fail!">/usr/share/inkscape/extensions/dxf_outlines.py</param>
<param name="export_svg" type="bool" gui-text="Export as SVG">true</param>
<param name="export_dxf" type="bool" gui-text="Export as DXF R14 file (mm units)">false</param>
<param name="export_pdf" type="bool" gui-text="Export as PDF 1.5">false</param>
<param name="export_png" type="bool" gui-text="Export as PNG">false</param>
<param name="png_dpi" type="float" min="1" max="2400" precision="3" gui-text="PNG DPI (applies for export and replace)" gui-description="default is 96">96</param>
<param name="replace_by_png" type="bool" gui-text="Replace by PNG">false</param>
<param name="newwindow" type="bool" gui-text="Open file in new Inkscape instance">false</param>
<label>Note: If svg/dxf/pdf already existed before, they might get accidently deleted or overwritten. Please take care!</label>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Export Selection As ...</label>
<label>Extension to export the current selection into different formats like SVG, DXF or PDF.</label>
<label>2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/exportselectionas</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">Based on</label>
<label appearance="url">https://github.com/mireq/inkscape-export-selection-as-svg</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect needs-document="true" needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Import/Export/Transfer"/>
</submenu>
</effects-menu>
<menu-tip>Export selection to separate files.</menu-tip>
</effect>
<script>
<command location="inx" interpreter="python">export_selection_as.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,283 @@
#!/usr/bin/env python3
from copy import deepcopy
from pathlib import Path
import logging
import math
import os
import sys
import subprocess
from subprocess import Popen, PIPE
import inkex
from inkex import Rectangle
import inkex.command
from inkex.command import inkscape, inkscape_command
import tempfile
from PIL import Image
import base64
from io import BytesIO
import warnings
warnings.simplefilter('ignore', Image.DecompressionBombWarning)
from lxml import etree
from scour.scour import scourString
logger = logging.getLogger(__name__)
DETACHED_PROCESS = 0x00000008
GROUP_ID = 'export_selection_transform'
class ExportObject(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--tab")
pars.add_argument("--wrap_transform", type=inkex.Boolean, default=False, help="Wrap final document in transform")
pars.add_argument("--border_offset", type=float, default=1.000, help="Add border offset around selection")
pars.add_argument("--export_dir", default="~/inkscape_export/", help="Location to save exported documents")
pars.add_argument("--opendir", type=inkex.Boolean, default=False, help="Open containing output directory after export")
pars.add_argument("--dxf_exporter_path", default="/usr/share/inkscape/extensions/dxf_outlines.py", help="Location of dxf_outlines.py")
pars.add_argument("--export_svg", type=inkex.Boolean, default=False, help="Create a svg file")
pars.add_argument("--export_dxf", type=inkex.Boolean, default=False, help="Create a dxf file")
pars.add_argument("--export_pdf", type=inkex.Boolean, default=False, help="Create a pdf file")
pars.add_argument("--export_png", type=inkex.Boolean, default=False, help="Create a png file")
pars.add_argument("--png_dpi", type=float, default=96, help="PNG DPI (applies for export and replace)")
pars.add_argument("--replace_by_png", type=inkex.Boolean, default=False, help="Replace selection by png export")
pars.add_argument("--newwindow", type=inkex.Boolean, default=False, help="Open file in new Inkscape window")
def openExplorer(self, dir):
if os.name == 'nt':
Popen(["explorer", dir], close_fds=True, creationflags=DETACHED_PROCESS).wait()
else:
Popen(["xdg-open", dir], close_fds=True, start_new_session=True).wait()
def spawnIndependentInkscape(self, file): #function to spawn non-blocking inkscape instance. the inkscape command is available because it is added to ENVIRONMENT when Inkscape main instance is started
if not os.path.exists(file):
inkex.utils.debug("Error. {} does not exist!".format(file))
exit(1)
warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback"
if os.name == 'nt':
Popen(["inkscape", file], close_fds=True, creationflags=DETACHED_PROCESS)
else:
subprocess.Popen(["inkscape", file], start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
warnings.simplefilter("default", ResourceWarning)
def effect(self):
svg_export = self.options.export_svg
extra_param = "--batch-process"
if self.options.export_svg is False and \
self.options.export_dxf is False and \
self.options.export_pdf is False and \
self.options.export_png is False and \
self.options.replace_by_png is False and \
self.options.newwindow is False:
inkex.utils.debug("You must select at least one option to continue!")
return
if self.options.replace_by_png is True:
self.options.border_offset = 0 #override
if not self.svg.selected:
inkex.errormsg("Selection is empty. Please select some objects first!")
return
if self.options.export_dxf is True:
#preflight check for DXF input dir
if not os.path.exists(self.options.dxf_exporter_path):
inkex.utils.debug("Location of dxf_outlines.py does not exist. Please select a proper file and try again.")
exit(1)
export_dir = Path(self.absolute_href(self.options.export_dir))
os.makedirs(export_dir, exist_ok=True)
offset = self.options.border_offset
bbox = inkex.BoundingBox()
selected = self.svg.selected
firstId = selected[0].get('id')
parent = self.svg.getElementById(firstId).getparent()
for elem in selected.values():
transform = inkex.Transform()
parent = elem.getparent()
if parent is not None and isinstance(parent, inkex.ShapeElement):
transform = parent.composed_transform()
try:
bbox += elem.bounding_box(transform)
except Exception:
logger.exception("Bounding box not computed")
logger.info("Skipping bounding box")
transform = elem.composed_transform()
x1, y1 = transform.apply_to_point([0, 0])
x2, y2 = transform.apply_to_point([1, 1])
bbox += inkex.BoundingBox((x1, x2), (y1, y2))
template = self.create_document()
svg_filename = None
group = etree.SubElement(template, '{http://www.w3.org/2000/svg}g')
group.attrib['id'] = GROUP_ID
group.attrib['transform'] = str(inkex.Transform(((1, 0, -bbox.left), (0, 1, -bbox.top))))
for elem in self.svg.selected.values():
if elem.tag == inkex.addNS('image', 'svg'):
continue #skip images
elem_copy = deepcopy(elem)
elem_copy.attrib['transform'] = str(elem.composed_transform())
elem_copy.attrib['style'] = str(elem.composed_style())
group.append(elem_copy)
template.attrib['viewBox'] = f'{-offset} {-offset} {bbox.width + offset * 2} {bbox.height + offset * 2}'
template.attrib['width'] = f'{bbox.width + offset * 2}' + self.svg.unit
template.attrib['height'] = f'{bbox.height + offset * 2}' + self.svg.unit
if svg_filename is None:
filename_base = elem.attrib.get('id', None).replace(os.sep, '_')
if filename_base:
svg_filename = filename_base + '.svg'
if not filename_base: #should never be the case. Inkscape might crash if the id attribute is empty or not existent due to invalid SVG
filename_base = self.svg.get_unique_id("selection")
svg_filename = filename_base + '.svg'
if len(group) == 0:
self.msg("Selection does not contain any vector data.")
exit(1)
template.append(group)
svg_out = os.path.join(tempfile.gettempdir(), svg_filename)
if self.options.wrap_transform is False:
#self.load(inkscape_command(template.tostring(), select=GROUP_ID, verbs=['SelectionUnGroup;FileSave'])) #fails due to new bug
#workaround
self.save_document(template, svg_out) #export recent file
actions_list=[]
actions_list.append("SelectionUnGroup")
actions_list.append("export-type:svg")
actions_list.append("export-filename:{}".format(svg_out))
actions_list.append("export-do")
actions = ";".join(actions_list)
cli_output = inkscape(svg_out, extra_param, actions=actions) #process recent file
if len(cli_output) > 0:
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
self.msg(cli_output)
self.load(svg_out) #reload recent file
template = self.svg
for child in template.getchildren():
if child.tag == '{http://www.w3.org/2000/svg}metadata':
template.remove(child)
self.save_document(template, svg_out) # save one into temp dir to access for dxf/pdf/new window instance
if self.options.export_svg is True:
self.save_document(template, export_dir / svg_filename)
if self.options.opendir is True:
self.openExplorer(export_dir)
if self.options.newwindow is True:
#inkscape(os.path.join(export_dir, svg_filename)) #blocking cmd
self.spawnIndependentInkscape(os.path.join(tempfile.gettempdir(), svg_filename)) #non-blocking
if self.options.export_dxf is True:
#ensure that python command is available #we pass 25.4/96 which stands for unit mm. See inkex.units.UNITS and dxf_outlines.inx
cmd = [
sys.executable, #the path of the python interpreter which is used for this script
self.options.dxf_exporter_path,
'--output=' + os.path.join(export_dir, filename_base + '.dxf'),
r'--units=25.4/96',
os.path.join(tempfile.gettempdir(), svg_filename)
]
proc = Popen(cmd, shell=False, stdout=PIPE, stderr=PIPE)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
inkex.utils.debug("%d %s %s" % (proc.returncode, stdout, stderr))
if self.options.export_pdf is True:
cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions='export-pdf-version:1.5;export-text-to-path;export-filename:{file_name};export-do'.format(file_name=os.path.join(export_dir, filename_base + '.pdf')))
if len(cli_output) > 0:
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
self.msg(cli_output)
if self.options.export_png is True:
png_export=os.path.join(export_dir, filename_base + '.png')
try:
os.remove(png_export)
except OSError as e:
#inkex.utils.debug("Error while deleting previously generated output file " + png_export)
pass
actions_list=[]
actions_list.append("export-background:white")
actions_list.append("export-type:png")
actions_list.append("export-dpi:{}".format(self.options.png_dpi))
actions_list.append("export-filename:{}".format(png_export))
actions_list.append("export-do")
actions = ";".join(actions_list)
cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions=actions)
if len(cli_output) > 0:
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
self.msg(cli_output)
if self.options.replace_by_png is True:
#export to png file to temp
png_export=os.path.join(tempfile.gettempdir(), filename_base + '.png')
try:
os.remove(png_export)
except OSError as e:
#inkex.utils.debug("Error while deleting previously generated output file " + png_export)
pass
actions_list=[]
actions_list.append("export-background:white")
actions_list.append("export-type:png")
actions_list.append("export-dpi:{}".format(self.options.png_dpi))
actions_list.append("export-filename:{}".format(png_export))
actions_list.append("export-do")
actions = ";".join(actions_list)
cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions=actions)
if len(cli_output) > 0:
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
self.msg(cli_output)
#then remove the selection and replace it by png
#self.msg(parent.get('id'))
for elem in selected.values():
elem.delete()
#read png file and get base64 string from it
try:
img = Image.open(png_export)
except Image.DecompressionBombError as e: #we could also increse PIL.Image.MAX_IMAGE_PIXELS = some large int
self.msg("Error. Image is too large. Reduce DPI and try again!")
exit(1)
output_buffer = BytesIO()
img.save(output_buffer, format='PNG')
byte_data = output_buffer.getvalue()
base64_str = base64.b64encode(byte_data).decode('UTF-8')
#finally replace the svg:path(s) with svg:image
imgReplacement = etree.SubElement(Rectangle(), '{http://www.w3.org/2000/svg}image')
imgReplacement.attrib['x'] = str(bbox.left)
imgReplacement.attrib['y'] = str(bbox.top)
imgReplacement.attrib['width'] = str(bbox.width)
imgReplacement.attrib['height'] = str(bbox.height)
imgReplacement.attrib['id'] = firstId
imgReplacement.attrib['{http://www.w3.org/1999/xlink}href'] = "data:image/png;base64,{}".format(base64_str)
parent.append(imgReplacement)
del parent.attrib['transform'] #remove transform
def create_document(self):
document = self.svg.copy()
for child in document.getchildren():
if child.tag == '{http://www.w3.org/2000/svg}defs':
continue
document.remove(child)
return document
def save_document(self, document, filename):
with open(filename, 'wb') as fp:
document = document.tostring()
fp.write(scourString(document).encode('utf8'))
if __name__ == '__main__':
ExportObject().run()