add latest features to nextgen

This commit is contained in:
Mario Voigt 2023-11-26 21:11:13 +01:00
parent 37e0135daa
commit 8b89f6ebfd
2 changed files with 92 additions and 26 deletions

View File

@ -8,7 +8,7 @@
<param gui-text="CSV file:" gui-description="A file with comma-separated values, one line per exported file. The first line contains the column names. Put quotation marks around your variables if they contain a comma." name="csv_file" type="path" mode="file" filetypes="csv">/path/to/file.csv</param>
<spacer />
<label>Non-text values to replace (see Help tab):</label>
<param name="extra-vars" type="string" gui-text="" />
<param name="extra-vars" type="string" gui-text="&#8291;"></param>
<param gui-text="Number of sets in the template:" gui-description="How many datasets fit into one template file - e.g. if you have an A4 page with 8 visitor badges for 8 different persons which use the same variable names, choose '8'. See 'Help' tab for more info." name="num_sets" type="int" min="1" max="10000">1</param>
<spacer />
<separator />
@ -23,7 +23,11 @@
</param>
<param gui-text="DPI (for PNG and filters):" gui-description="The resolution for your exported raster images" name="dpi" type="int" min="1" max="10000">300</param>
<param gui-text="File name pattern:" gui-description="The pattern for the names of the generated files. If 'Number of sets' is 1, it should contain at least one unique column variable (in the form of '%VAR_my_variable_name%'), or a unique combination of column variables, so new files won't overwrite those that have already been generated. Do not include the file extension, it will be added automatically. If 'Number of sets' is greater than 1, use any name you like. The exported files will be numbered automatically." name="file_pattern" type="string">%VAR_my_variable_name%</param>
<param gui-text="Merge output PDF files into one PDF file" gui-description="When outputting to PDF format, merge the resulting files into a single, multi-page PDF file. Requires ghostscript (https://ghostscript.com) to be installed."
name="merge_pdf" type="bool">false</param>
<param gui-text="Save in:" gui-description="The name of the folder where the generated images should be saved" name="output_folder" type="path" mode="folders">/tmp</param>
<param gui-text="Limit to first output file" gui-description="This is for testing purpose only. Savely check the output before creating hundreds of files."
name="limit_output" type="bool">false</param>
</page>
<page name="help" gui-text="Help">
<param name="helptabs" type="notebook">
@ -47,7 +51,7 @@ Also make sure to use a variable only exactly once per set. If you do not adhere
</page>
<page name="about" gui-text="About">
<label appearance="header">NextGenerator</label>
<label indent="1">Version 1.1.1</label>
<label indent="1">Version 1.6</label>
<spacer />
<label xml:space="preserve">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.
@ -56,6 +60,7 @@ This extension is a Python rewrite of the Generator bash script extension by Aur
</param>
<effect needs-live-preview="false">
<object-type>all</object-type>
<menu-tip>Automatically replace values and export the result.</menu-tip>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Import/Export/Transfer" />

View File

@ -19,6 +19,7 @@
# 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.
@ -31,12 +32,12 @@ from __future__ import unicode_literals
import os
import csv
import json
import time #for debugging purposes
import inkex
import html
from inkex.command import inkscape
__version__ = '1.2'
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"""
@ -47,10 +48,12 @@ class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
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("-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")
@ -62,6 +65,15 @@ class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
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
@ -70,6 +82,7 @@ class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
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:
@ -78,17 +91,30 @@ class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
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)
if self.export(export_base_name) != True:
return
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
@ -105,9 +131,12 @@ class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
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 = "".join([x if x.isalnum() else "_" for x in self.options.file_pattern]) + '_{}'.format(str(export_file_num))
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):
@ -133,6 +162,15 @@ class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
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)
@ -140,35 +178,39 @@ class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
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.")
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)
with open(export_file_path, 'w') as f:
f.write(self.new_doc)
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;FileClose'.\
'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;FileClose'.\
'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;FileClose'.\
'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;FileClose'.\
'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)
#inkex.utils.debug("temp_svg_path=" + temp_svg_path)
with open(temp_svg_path, 'w') as f:
with open(temp_svg_path, 'w', encoding='utf-8') as f:
f.write(self.new_doc)
#inkex.utils.debug("self.new_doc=" + 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])
@ -177,7 +219,26 @@ class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
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
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')