366 lines
11 KiB
Python
366 lines
11 KiB
Python
|
#! /usr/bin/python
|
||
|
# coding=utf-8
|
||
|
|
||
|
# Generator-Python - a Inkscape extension to generate end-use files
|
||
|
# from a model
|
||
|
# Copyright (C) 2017 Johannes Matschke
|
||
|
# Based on the Generator extension by Aurélio A. Heckert
|
||
|
#
|
||
|
# 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.
|
||
|
|
||
|
# Version 1.0
|
||
|
|
||
|
import sys
|
||
|
import os
|
||
|
import subprocess
|
||
|
import platform
|
||
|
import distutils.spawn
|
||
|
import tempfile
|
||
|
import shutil
|
||
|
import argparse
|
||
|
import csv
|
||
|
import ctypes
|
||
|
|
||
|
|
||
|
class SimpleObject:
|
||
|
pass
|
||
|
|
||
|
|
||
|
class MyArgumentParser(argparse.ArgumentParser):
|
||
|
def error(self, message):
|
||
|
# use "argument" instead of "--argument"
|
||
|
message = message.replace(' --', ' ')
|
||
|
Show_error_and_exit('Argument error', message)
|
||
|
|
||
|
|
||
|
def Is_executable(filename):
|
||
|
return distutils.spawn.find_executable(filename) is not None
|
||
|
|
||
|
|
||
|
if platform.system() != 'Windows' and Is_executable('zenity'):
|
||
|
class ProgressBar():
|
||
|
def __init__(self, title, text):
|
||
|
self.__title = title
|
||
|
self.__text = text
|
||
|
self.__active = False
|
||
|
|
||
|
def __enter__(self):
|
||
|
self.__devnull = open(os.devnull, 'w')
|
||
|
self.__proc = subprocess.Popen(
|
||
|
[
|
||
|
'zenity',
|
||
|
'--progress',
|
||
|
'--title={0}'.format(self.__title),
|
||
|
'--text={0}'.format(self.__text),
|
||
|
'--auto-close',
|
||
|
'--width=400'
|
||
|
], stdin=subprocess.PIPE, stdout=self.__devnull,
|
||
|
stderr=self.__devnull)
|
||
|
self.__active = True
|
||
|
return self
|
||
|
|
||
|
def __exit__(self, *args):
|
||
|
if self.__active:
|
||
|
self.__close()
|
||
|
|
||
|
def Set_percent(self, p):
|
||
|
if self.is_active:
|
||
|
self.__proc.stdin.write(str(p) + '\n')
|
||
|
|
||
|
@property
|
||
|
def is_active(self):
|
||
|
if self.__active and self.__proc.poll() is not None:
|
||
|
self.__close()
|
||
|
return self.__active
|
||
|
|
||
|
def __close(self):
|
||
|
if not self.__proc.stdin.closed:
|
||
|
self.__proc.stdin.close()
|
||
|
if not self.__devnull.closed:
|
||
|
self.__devnull.close()
|
||
|
self.__active = False
|
||
|
else:
|
||
|
class ProgressBar():
|
||
|
def __init__(self, title, text):
|
||
|
pass
|
||
|
|
||
|
def __enter__(self):
|
||
|
return self
|
||
|
|
||
|
def __exit__(self, *args):
|
||
|
pass
|
||
|
|
||
|
def Set_percent(self, p):
|
||
|
pass
|
||
|
|
||
|
@property
|
||
|
def is_active(self):
|
||
|
return True
|
||
|
|
||
|
|
||
|
if platform.system() == 'Windows':
|
||
|
def Show_info(title, message):
|
||
|
MB_ICONINFORMATION = 0x40
|
||
|
ctypes.windll.user32.MessageBoxA(0, message, title, MB_ICONINFORMATION)
|
||
|
|
||
|
def Show_question(title, message):
|
||
|
MB_ICONQUESTION = 0x20
|
||
|
MB_YESNO = 0x4
|
||
|
IDYES = 6
|
||
|
return ctypes.windll.user32.MessageBoxA(
|
||
|
0, message, title, MB_ICONQUESTION | MB_YESNO) == IDYES
|
||
|
|
||
|
def Show_error_and_exit(title, message):
|
||
|
MB_ICONERROR = 0x10
|
||
|
ctypes.windll.user32.MessageBoxA(0, message, title, MB_ICONERROR)
|
||
|
sys.exit(1)
|
||
|
|
||
|
elif Is_executable('zenity'):
|
||
|
def Show_info(title, message):
|
||
|
Zenity('info', title, message)
|
||
|
|
||
|
def Show_question(title, message):
|
||
|
return Zenity('question', title, message) == 0
|
||
|
|
||
|
def Show_error_and_exit(title, message):
|
||
|
Zenity('error', title, message)
|
||
|
sys.exit(1)
|
||
|
|
||
|
def Zenity(mode, title, message):
|
||
|
return Call_no_output(
|
||
|
[
|
||
|
'zenity',
|
||
|
'--{0}'.format(mode),
|
||
|
'--title={0}'.format(title),
|
||
|
'--text={0}'.format(message)
|
||
|
])
|
||
|
else:
|
||
|
def Show_info(title, message):
|
||
|
pass
|
||
|
|
||
|
def Show_question(title, message):
|
||
|
return None
|
||
|
|
||
|
def Show_error_and_exit(title, message):
|
||
|
sys.stderr.write(title + '\n' + message + '\n')
|
||
|
sys.exit(1)
|
||
|
|
||
|
|
||
|
def Get_command_line_arguments():
|
||
|
parser = MyArgumentParser()
|
||
|
parser.add_argument(
|
||
|
'--vartype', choices=['COLUMN', 'NAME'], required=True, type=str.upper,
|
||
|
help='How to refer to columns')
|
||
|
parser.add_argument(
|
||
|
'--extravars', default='',
|
||
|
help='Extra textual values to be replaced')
|
||
|
parser.add_argument(
|
||
|
'--datafile', required=True, help='path of CSV data file')
|
||
|
parser.add_argument(
|
||
|
'--format',
|
||
|
choices=['PDF', 'SVG', 'PS', 'EPS', 'PNG', 'JPG'],
|
||
|
default='PDF', type=str.upper, help='Export format')
|
||
|
parser.add_argument(
|
||
|
'--dpi', default='90', help='DPI (for PNG and JPG)')
|
||
|
parser.add_argument(
|
||
|
'--output', required=True, help='Output pattern')
|
||
|
parser.add_argument(
|
||
|
'--preview', choices=['TRUE', 'FALSE'], default='FALSE', type=str.upper,
|
||
|
help='Preview (only first page)')
|
||
|
parser.add_argument(
|
||
|
'--specialchars', choices=['TRUE', 'FALSE'], default='TRUE', type=str.upper,
|
||
|
help='Handle special XML characters')
|
||
|
parser.add_argument('infile', help='SVG input file')
|
||
|
|
||
|
args = parser.parse_known_args()[0]
|
||
|
return args
|
||
|
|
||
|
|
||
|
def Generate(replacements):
|
||
|
template = globaldata.template
|
||
|
destfile = globaldata.args.output
|
||
|
|
||
|
for search, replace in replacements:
|
||
|
template = template.replace(search, replace)
|
||
|
destfile = destfile.replace(search, replace)
|
||
|
|
||
|
tmp_svg = os.path.join(globaldata.tempdir, 'temp.svg')
|
||
|
|
||
|
with open(tmp_svg, 'wb') as f:
|
||
|
f.write(template)
|
||
|
|
||
|
if globaldata.args.format == 'SVG':
|
||
|
shutil.move(tmp_svg, destfile)
|
||
|
elif globaldata.args.format == 'JPG':
|
||
|
tmp_png = os.path.join(globaldata.tempdir, 'temp.png')
|
||
|
Ink_render(tmp_svg, tmp_png, 'PNG')
|
||
|
Png_to_jpg(tmp_png, destfile)
|
||
|
else:
|
||
|
Ink_render(tmp_svg, destfile, globaldata.args.format)
|
||
|
|
||
|
return destfile
|
||
|
|
||
|
|
||
|
def Ink_render(infile, outfile, format):
|
||
|
Call_or_die(
|
||
|
[
|
||
|
'inkscape',
|
||
|
'--without-gui',
|
||
|
'--export-{0}={1}'.format(
|
||
|
format.lower(), outfile),
|
||
|
'--export-dpi={0}'.format(globaldata.args.dpi),
|
||
|
infile
|
||
|
],
|
||
|
'Inkscape Converting Error')
|
||
|
|
||
|
|
||
|
def Png_to_jpg(pngfile, jpgfile):
|
||
|
if platform.system() == 'Windows':
|
||
|
Show_error_and_exit(
|
||
|
'JPG Export', 'JPG Export is not available on Windows.')
|
||
|
elif Is_executable('convert'):
|
||
|
Call_or_die(
|
||
|
[
|
||
|
'convert',
|
||
|
'PNG:' + pngfile,
|
||
|
'JPG:' + jpgfile
|
||
|
],
|
||
|
'ImageMagick Converting Error')
|
||
|
else:
|
||
|
Show_error_and_exit(
|
||
|
'JPG export', 'JPG export is not available because '
|
||
|
'Image Magick is not installed.')
|
||
|
|
||
|
|
||
|
def Prepare_data(data_old):
|
||
|
data_new = []
|
||
|
if len(globaldata.args.extravars) == 0:
|
||
|
extra_value_info = []
|
||
|
else:
|
||
|
extra_value_info = \
|
||
|
[x.split('=>', 2) for x in globaldata.args.extravars.split('|')]
|
||
|
|
||
|
for row in data_old:
|
||
|
row_new = []
|
||
|
for key, value in row:
|
||
|
if key == '':
|
||
|
continue
|
||
|
if globaldata.args.specialchars == 'TRUE':
|
||
|
value = value.replace('&', '&')
|
||
|
value = value.replace('<', '<')
|
||
|
value = value.replace('>', '>')
|
||
|
value = value.replace('"', '"')
|
||
|
value = value.replace("'", ''')
|
||
|
row_new.append(('%VAR_'+key+'%', value))
|
||
|
for search_replace in extra_value_info:
|
||
|
if len(search_replace) != 2:
|
||
|
Show_error_and_exit(
|
||
|
'Extra vars error',
|
||
|
'There is something wrong with your extra vars '
|
||
|
'parameter')
|
||
|
if search_replace[1] == key:
|
||
|
row_new.append((search_replace[0], value))
|
||
|
data_new.append(row_new)
|
||
|
|
||
|
return data_new
|
||
|
|
||
|
|
||
|
def Open_file_viewer(filename):
|
||
|
if platform.system() == 'Windows':
|
||
|
os.startfile(filename)
|
||
|
elif Is_executable('xdg-open'):
|
||
|
Call_no_output(['xdg-open', filename])
|
||
|
else:
|
||
|
Show_error_and_exit(
|
||
|
'No preview', 'Preview is not available because '
|
||
|
'"xdg-open" is not installed.')
|
||
|
|
||
|
|
||
|
def Call_no_output(args):
|
||
|
with open(os.devnull, 'w') as devnull:
|
||
|
return subprocess.call(args, stdout=devnull, stderr=devnull)
|
||
|
|
||
|
|
||
|
def Call_or_die(args, error_title):
|
||
|
try:
|
||
|
subprocess.check_output(args, stderr=subprocess.STDOUT)
|
||
|
except subprocess.CalledProcessError as error:
|
||
|
Show_error_and_exit(error_title, error.output)
|
||
|
|
||
|
|
||
|
def Process_csv_file(csvfile):
|
||
|
if globaldata.args.vartype == 'COLUMN':
|
||
|
csvdata = [[(str(x), y) for x, y in enumerate(row, start=1)]
|
||
|
for row in csv.reader(csvfile)]
|
||
|
else:
|
||
|
csvdata = [row.items() for row in csv.DictReader(csvfile)]
|
||
|
|
||
|
csvdata = Prepare_data(csvdata)
|
||
|
|
||
|
count = sum(1 for i in csvdata)
|
||
|
if count == 0:
|
||
|
Show_error_and_exit(
|
||
|
'Empty CSV file', 'There are no data sets in your CSV file.')
|
||
|
|
||
|
if globaldata.args.preview == 'TRUE':
|
||
|
for row in csvdata:
|
||
|
varlist = '\n'.join(
|
||
|
[key for key, value in row])
|
||
|
Show_info(
|
||
|
'Generator Variables',
|
||
|
'The replaceable text, based on your configuration and on '
|
||
|
'the CSV are:\n{0}'.format(varlist))
|
||
|
|
||
|
new_file = Generate(row)
|
||
|
Open_file_viewer(new_file)
|
||
|
break
|
||
|
else: # no preview
|
||
|
with ProgressBar('Generator', 'Generating...') as progress:
|
||
|
for num, row in enumerate(csvdata, start=1):
|
||
|
Generate(row)
|
||
|
if not progress.is_active:
|
||
|
break
|
||
|
progress.Set_percent(num * 100 / count)
|
||
|
|
||
|
|
||
|
try:
|
||
|
globaldata = SimpleObject()
|
||
|
globaldata.args = Get_command_line_arguments()
|
||
|
globaldata.tempdir = tempfile.mkdtemp()
|
||
|
|
||
|
if not os.path.isfile(globaldata.args.datafile):
|
||
|
Show_error_and_exit(
|
||
|
'File not found', 'The CSV file "{0}" does not '
|
||
|
'exist.'.format(globaldata.args.datafile))
|
||
|
|
||
|
if (not os.path.splitext(globaldata.args.output)[1][1:].upper() ==
|
||
|
globaldata.args.format):
|
||
|
if Show_question(
|
||
|
'Output file extension',
|
||
|
'Your output pattern has a file extension '
|
||
|
'diferent from the export format.\n\nDo you want to '
|
||
|
'add the file extension?'):
|
||
|
globaldata.args.output += '.' + \
|
||
|
globaldata.args.format.lower()
|
||
|
|
||
|
outdir = os.path.dirname(globaldata.args.output)
|
||
|
if outdir != '' and not os.path.exists(outdir):
|
||
|
os.makedirs(outdir)
|
||
|
|
||
|
with open(globaldata.args.infile, 'rb') as f:
|
||
|
globaldata.template = f.read()
|
||
|
|
||
|
with open(globaldata.args.datafile, 'rb') as csvfile:
|
||
|
Process_csv_file(csvfile)
|
||
|
|
||
|
finally:
|
||
|
shutil.rmtree(globaldata.tempdir)
|