mightyscape-1.2/extensions/fablabchemnitz/nextgenerator/nextgenerator.py

252 lines
13 KiB
Python

#!/usr/bin/env python3
# coding=utf-8
#
# NextGenerator - an Inkscape extension to export images with automatically replaced values
# Copyright (C) 2008 Aurélio A. Heckert (original Generator extension in Bash)
# 2019-2021 Maren Hachmann (Python rewrite, update for Inkscape 1.0)
#
# 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 3 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.
#
"""
An Inkscape extension to automatically replace values (text, attribute values)
in an SVG file and to then export the result to various file formats.
This is useful e.g. for generating images for name badges and other similar items.
"""
from __future__ import unicode_literals
import os
import csv
import json
import html
import inkex
from inkex.command import inkscape, call, which
__version__ = '1.6'
class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
"""Generate image files by replacing variables in the current file"""
def add_arguments(self, pars):
pars.add_argument("-c", "--csv_file", type=str, dest="csv_file", help="path to a CSV file")
pars.add_argument("-e", "--extra-vars", help="additional variables to replace and the corresponding columns, in JSON format")
pars.add_argument("-n", "--num_sets", type=int, default="1", help="number of sets in the template")
pars.add_argument("-f", "--format", help="file format to export to: png, pdf, svg, ps, eps")
pars.add_argument("-d", "--dpi", type=int, default="300", help="dpi value for exported raster images")
pars.add_argument("-k", "--limit_output", type=inkex.Boolean, default=False, help="Limit to first output file")
pars.add_argument("-m", "--merge_pdf", type=inkex.Boolean, default=False, help="Merge PDF files into one")
pars.add_argument("-o", "--output_folder", help="path to output folder")
pars.add_argument("-p", "--file_pattern", help="pattern for the output file")
pars.add_argument("-t", "--tab",default="", help="not needed at all")
pars.add_argument("-l", "--helptabs", default="", help="not needed at all")
pars.add_argument("-i", "--id", default="", help="not needed at all")
def effect(self):
# load the attributes that should be replaced in addition to textual values
if self.options.extra_vars == None:
self.options.extra_vars = '{}'
extra_vars = json.loads(self.options.extra_vars)
pdf_files_to_merge = []
# warn user if Ghostscript is not available and they checked the option to merge pdfs
if self.options.merge_pdf:
try:
which('gs')
except inkex.command.CommandNotFound as e:
exit("Cannot merge PDF files, please install Ghostscript (see https://ghostscript.com).")
# load the CSV file
# spaces around commas will be stripped
csv.register_dialect('generator', 'excel', skipinitialspace=True)
with open(self.options.csv_file, newline='', encoding='utf-8-sig') as csvfile:
data = csv.DictReader(csvfile, dialect='generator')
n = 0
if self.options.num_sets == 1:
for row in data:
export_base_name = self.options.file_pattern
self.new_doc = self.document
for i, (key, value) in enumerate(row.items()):
search_string = "%VAR_" + key + "%"
# replace any occurrances of %VAR_my_variable_name% in the SVG file source code
# inkex.utils.debug(value)
self.new_doc = self.new_doc.replace(search_string, html.escape(value))
# build the file name, still without file extension
export_base_name = export_base_name.replace(search_string, value)
#if export_base_name == self.options.file_pattern:
# export_base_name += '_{}'.format(str(n))
for key, svg_cont in extra_vars.items():
if key in row.keys():
# replace any attributes and other SVG content by the values from the CSV file
self.new_doc = self.new_doc.replace(svg_cont, row[key])
else:
inkex.errormsg(_("The replacements in the generated images may be incomplete. Please check your entry '{key}' in the field for the non-text values.").format(key=key))
# clean export base name
export_base_name = "".join([x if x.isalnum() else "_" for x in export_base_name])
res = self.export(export_base_name)
if res != True:
return()
if self.options.format == "pdf" and self.options.merge_pdf:
pdf_files_to_merge.append("{}.pdf".format(export_base_name))
n+=1
if self.options.limit_output is True:
break
if self.options.format == "pdf" and self.options.merge_pdf:
output_file_base = pdf_files_to_merge[0][:-4]
elif self.options.num_sets > 1:
# we need a list to access specific rows and to be able to count it
data = list(data)
# check if user's indication of num_sets is compatible with file
for key in data[0].keys():
num_occurr = self.document.count("%VAR_" + key + "%")
# We ignore keys that don't appear in the document
if num_occurr != 0 and num_occurr != self.options.num_sets:
return inkex.errormsg("There are {0} occurrances of the variable '{1}' in the document, but the number of sets you indicated is {2}. Please make sure that each set contains all variables and that there are just as many sets in your document as you indicate.".format(num_occurr, key, self.options.num_sets))
# abusing negative floor division which rounds to the next lowest number to figure out how many pages we will get
num_exports = -((-len(data))//self.options.num_sets)
# now we hope that the document is properly prepared and the stacking order cycles through datasets - if not, the result will be nonsensical, but we can't know.
output_file_base = "".join([x if x.isalnum() else "_" for x in self.options.file_pattern])
for export_file_num in range(num_exports):
# we only number the export files if there are sets
export_base_name = output_file_base + '_{}'.format(str(export_file_num))
self.new_doc = self.document
for set_num in range(self.options.num_sets):
# number of the data row in the CSV file
n = export_file_num * self.options.num_sets + set_num
if n < len(data):
dataset = data[n]
else:
# no more values available, stop trying to replace them
break
for i, (key, value) in enumerate(dataset.items()):
search_string = "%VAR_" + key + "%"
# replace the next occurrance of %VAR_my_variable_name% in the SVG file source code
self.new_doc = self.new_doc.replace(search_string, html.escape(value), 1)
for key, svg_cont in extra_vars.items():
if key in dataset.keys():
# replace any attributes and other SVG content by the values from the CSV file
self.new_doc = self.new_doc.replace(svg_cont, dataset[key], 1)
else:
inkex.errormsg(_("The replacements in the generated images may be incomplete. Please check your entry '{key}' in the field for the non-text values.").format(key=key))
self.export(export_base_name)
if self.options.format == "pdf" and self.options.merge_pdf:
pdf_files_to_merge.append("{}.pdf".format(export_base_name))
if self.options.limit_output is True:
break
if len(pdf_files_to_merge) > 1:
self.merge_pdfs(pdf_files_to_merge, output_file_base)
def export(self, export_base_name):
export_file_name = '{0}.{1}'.format(export_base_name, self.options.format)
if os.path.exists(self.options.output_folder):
export_file_path = os.path.join(self.options.output_folder, export_file_name)
else:
inkex.errormsg(_("The selected output folder does not exist."))
return False
if self.options.format == 'svg':
# would use this, but it cannot overwrite, nor handle strings for writing...:
# write_svg(self.new_doc, export_file_path)
try:
with open(export_file_path, 'w', encoding='utf-8') as f:
f.write(self.new_doc)
return True
except IOError as e:
print("Couldn't open or write to file (%s)." % e)
return False
else:
actions = {
'png' : 'export-dpi:{dpi};export-filename:{file_name};export-do;file-close'.\
format(dpi=self.options.dpi, file_name=export_file_path),
'pdf' : 'export-dpi:{dpi};export-pdf-version:1.5;export-text-to-path;export-filename:{file_name};export-do;file-close'.\
format(dpi=self.options.dpi, file_name=export_file_path),
'ps' : 'export-dpi:{dpi};export-text-to-path;export-filename:{file_name};export-do;file-close'.\
format(dpi=self.options.dpi, file_name=export_file_path),
'eps' : 'export-dpi:{dpi};export-text-to-path;export-filename:{file_name};export-do;file-close'.\
format(dpi=self.options.dpi, file_name=export_file_path),
}
# create a temporary svg file from our string
temp_svg_name = '{0}.{1}'.format(export_base_name, 'svg')
temp_svg_path = os.path.join(self.tempdir, temp_svg_name)
with open(temp_svg_path, 'w', encoding='utf-8') as f:
f.write(self.new_doc)
# let Inkscape do the exporting
# self.debug(actions[self.options.format])
cli_output = inkscape(temp_svg_path, actions=actions[self.options.format])
if len(cli_output) > 0:
self.debug(_("Inkscape returned the following output when trying to run the file export; the file export may still have worked:"))
self.debug(cli_output)
return False
return True
def merge_pdfs(self, pdf_files_to_merge, output_file_base):
pdf_paths = [os.path.join(self.options.output_folder, f) for f in pdf_files_to_merge]
output_file_name = '{}_all.pdf'.format(output_file_base)
#inkex.utils.debug(pdf_paths)
try:
inkex.command.call('gs', '-q', '-dBATCH', '-dNOPAUSE', '-q', '-sDEVICE=pdfwrite', '-sOutputFile={}'.format(os.path.join(self.options.output_folder, output_file_name)), *pdf_paths)
except inkex.command.ProgramRunError as e:
inkex.utils.debug("PDF files could not be merged for the following reason: {}".format(e))
# don't delete generated files when merging did not work
return()
# delete merged files
for f in pdf_paths:
if os.path.exists(f):
os.remove(f)
def load(self, stream):
return str(stream.read(), 'utf-8')
def save(self, stream):
# must be implemented, but isn't needed.
pass
if __name__ == '__main__':
NextGenerator().run()