2022-10-10 03:43:34 +02:00
#!/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.
#
2023-11-26 21:11:13 +01:00
2022-10-10 03:43:34 +02:00
"""
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
2023-11-26 21:11:13 +01:00
import inkex
from inkex . command import inkscape , call , which
__version__ = ' 1.6 '
2022-10-10 03:43:34 +02:00
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 " )
2023-11-26 21:11:13 +01:00
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 " )
2022-10-10 03:43:34 +02:00
pars . add_argument ( " -o " , " --output_folder " , help = " path to output folder " )
pars . add_argument ( " -p " , " --file_pattern " , help = " pattern for the output file " )
2023-11-26 21:11:13 +01:00
pars . add_argument ( " -t " , " --tab " , default = " " , help = " not needed at all " )
2022-10-10 03:43:34 +02:00
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 )
2023-11-26 21:11:13 +01:00
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). " )
2022-10-10 03:43:34 +02:00
# 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 ' )
2023-11-26 21:11:13 +01:00
n = 0
2022-10-10 03:43:34 +02:00
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
2023-11-26 21:11:13 +01:00
# inkex.utils.debug(value)
2022-10-10 03:43:34 +02:00
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 )
2023-11-26 21:11:13 +01:00
#if export_base_name == self.options.file_pattern:
# export_base_name += '_{}'.format(str(n))
2022-10-10 03:43:34 +02:00
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 :
2023-11-26 21:11:13 +01:00
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 ]
2022-10-10 03:43:34 +02:00
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.
2023-11-26 21:11:13 +01:00
output_file_base = " " . join ( [ x if x . isalnum ( ) else " _ " for x in self . options . file_pattern ] )
2022-10-10 03:43:34 +02:00
for export_file_num in range ( num_exports ) :
# we only number the export files if there are sets
2023-11-26 21:11:13 +01:00
export_base_name = output_file_base + ' _ {} ' . format ( str ( export_file_num ) )
2022-10-10 03:43:34 +02:00
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 )
2023-11-26 21:11:13 +01:00
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 )
2022-10-10 03:43:34 +02:00
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 :
2023-11-26 21:11:13 +01:00
inkex . errormsg ( _ ( " The selected output folder does not exist. " ) )
2022-10-10 03:43:34 +02:00
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)
2023-11-26 21:11:13 +01:00
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
2022-10-10 03:43:34 +02:00
else :
actions = {
2023-11-26 21:11:13 +01:00
' png ' : ' export-dpi: {dpi} ;export-filename: {file_name} ;export-do;file-close ' . \
2022-10-10 03:43:34 +02:00
format ( dpi = self . options . dpi , file_name = export_file_path ) ,
2023-11-26 21:11:13 +01:00
' pdf ' : ' export-dpi: {dpi} ;export-pdf-version:1.5;export-text-to-path;export-filename: {file_name} ;export-do;file-close ' . \
2022-10-10 03:43:34 +02:00
format ( dpi = self . options . dpi , file_name = export_file_path ) ,
2023-11-26 21:11:13 +01:00
' ps ' : ' export-dpi: {dpi} ;export-text-to-path;export-filename: {file_name} ;export-do;file-close ' . \
2022-10-10 03:43:34 +02:00
format ( dpi = self . options . dpi , file_name = export_file_path ) ,
2023-11-26 21:11:13 +01:00
' eps ' : ' export-dpi: {dpi} ;export-text-to-path;export-filename: {file_name} ;export-do;file-close ' . \
2022-10-10 03:43:34 +02:00
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 )
2023-11-26 21:11:13 +01:00
with open ( temp_svg_path , ' w ' , encoding = ' utf-8 ' ) as f :
2022-10-10 03:43:34 +02:00
f . write ( self . new_doc )
2023-11-26 21:11:13 +01:00
2022-10-10 03:43:34 +02:00
# 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
2023-11-26 21:11:13 +01:00
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 )
2022-10-10 03:43:34 +02:00
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__ ' :
2023-11-26 21:11:13 +01:00
NextGenerator ( ) . run ( )