No Description

printing.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. #!/usr/bin/env python
  2. #-*- coding:utf-8 -*-
  3. # vim: set ts=4 sts=4
  4. # For now the internal nametag templates will be hardcoded. At least a [default]
  5. # section is required; if a named section is missing the HTML will be ignored,
  6. # otherwise the following sections and files are allowed:
  7. # - [parent]
  8. # - [report]
  9. # - [volunteer]
  10. # TODO: respect locale's date/time format when doing substitution. Hard coded for now.
  11. # TODO: abstract barcode generation in themes. Hard coded for now.
  12. # TODO: a bunch of caching and restructuring.
  13. # TODO: code for multiple copies, if needed.
  14. # TODO: speed up batch printing with multi-page patched wkhtmltopdf?
  15. """Handles generation of HTML for nametags, saving/reading printer config, etc"""
  16. import os
  17. import sys
  18. import re
  19. import platform
  20. import logging
  21. import subprocess
  22. import tempfile
  23. import datetime
  24. from configobj import ConfigObj
  25. #from codebar import codebar
  26. PRINT_MODE = 'pdf'
  27. # Platforms using the CUPS printing system (UNIX):
  28. unix = ['Linux', 'linux2', 'Darwin']
  29. wkhtmltopdf = '/usr/bin/wkhtmltopdf' #path to wkhtmltopdf binary
  30. #TODO: Option to select native or builtin wkhtmltopdf
  31. #TODO: Determine whether to use ~/.taxidi/resources/ (config.ini in ~/.taxidi/)
  32. # or $PWD/resources/ (config.ini in pwd).
  33. script_path = os.path.dirname(os.path.realpath(__file__))
  34. nametags = os.path.join(script_path, 'resources', 'nametag') #path where html can be found.
  35. #For distribution this may be moved to /usr/share/taxidi/ in UNIX, but should be
  36. #copied to user's ~/.taxidi/ as write access is needed.
  37. lpr = '/usr/bin/lp' #path to LPR program (CUPS/Unix only).
  38. class Printer:
  39. def __init__(self, local=False):
  40. self.log = logging.getLogger(__name__)
  41. if local:
  42. self.con = _DummyPrinter()
  43. else:
  44. if platform.system() in unix:
  45. self.log.info("System is type UNIX. Using CUPS.")
  46. self.con = _CUPS()
  47. elif platform.system() == 'win32':
  48. self.log.info("System is type WIN32. Using GDI.")
  49. #TODO implement win32 printing code
  50. else:
  51. self.log.warning("Unsupported platform. Printing by preview only.")
  52. def buildArguments(self, config, section='default'):
  53. """Returns list of arguments to pass to wkhtmltopdf. Requires config
  54. dictionary and section name to read (if None reads [default]).
  55. Works all in lowercase. If section name does not exist, will use
  56. values from [default] instead.
  57. """
  58. try: #attempt to read passed section. If invalid, use default instead.
  59. self.log.debug('Attempting to read print configuration for section {0}...'
  60. .format(section))
  61. piece = config[section]
  62. except KeyError:
  63. self.log.warn('No section for {0}: using default instead.'.format(section))
  64. piece = config['default']
  65. args = []
  66. if len(piece) == 0:
  67. return [] #No options in section
  68. for arg in piece.keys():
  69. if arg.lower() == 'zoom':
  70. args.append('--zoom')
  71. args.append(piece[arg])
  72. elif arg.lower() == 'size':
  73. args.append('-s')
  74. args.append(piece[arg])
  75. elif arg.lower() == 'height':
  76. args.append('--page-height')
  77. args.append(piece[arg])
  78. elif arg.lower() == 'width':
  79. args.append('--page-width')
  80. args.append(piece[arg])
  81. elif arg.lower() == 'left':
  82. args.append('--margin-left')
  83. args.append(piece[arg])
  84. elif arg.lower() == 'right':
  85. args.append('--margin-right')
  86. args.append(piece[arg])
  87. elif arg.lower() == 'top':
  88. args.append('--margin-top')
  89. args.append(piece[arg])
  90. elif arg.lower() == 'bottom':
  91. args.append('--margin-bottom')
  92. args.append(piece[arg])
  93. elif arg.lower() == 'orientation':
  94. args.append('--orientation')
  95. args.append(piece[arg].lower())
  96. else:
  97. self.log.warning("Unexpected key encountered in {0}: {1} = {2}"
  98. .format(section, arg, piece[arg]))
  99. return args
  100. def writePdf(self, args, html, copies=1, collate=True):
  101. """
  102. Calls wkhtmltopdf and generates a pdf. Accepts args as a list, path
  103. to html file, and returns path to temporary file. Temp file
  104. should be unlinked when no longer needed.
  105. Also accepts copies=1, collate=True
  106. """
  107. #Build arguments
  108. if copies < 0: copies = 1
  109. if copies != 1:
  110. args.append("--copies")
  111. args.append(copies)
  112. if collate:
  113. args.append("--collate")
  114. if type(html) == list:
  115. args += html
  116. else:
  117. args.append(html)
  118. #create temp file to write to
  119. out = tempfile.NamedTemporaryFile(delete=False)
  120. out.close()
  121. args.append(out.name) #append output file name
  122. self.log.debug('Calling {0} with arguments <'.format(wkhtmltopdf))
  123. self.log.debug('{0} >'.format(args))
  124. if args[0] != wkhtmltopdf: args.insert(0, wkhtmltopdf) #prepend program name
  125. ret = subprocess.check_call(args)
  126. if ret != 0:
  127. self.log.error('Called process error:')
  128. self.log.error('Program returned exit code {0}'.
  129. format(ret))
  130. return 0
  131. self.log.debug('Generated pdf {0}'.format(out.name))
  132. return out.name
  133. def preview(self, fileName):
  134. """Opens a file using the default application. (Print preview)"""
  135. if os.name == 'posix':
  136. if sys.platform == 'darwin': #OS X
  137. self.log.debug('Attempting to preview file {0} via open'
  138. .format(fileName))
  139. ret = subprocess.call(['/usr/bin/open', fileName])
  140. if ret != 0:
  141. self.log.error('open returned non-zero exit code {0}'.format(ret))
  142. return 1
  143. return 0
  144. elif sys.platform == 'linux2': #linux
  145. try: #attempt to use evince-previewer instead
  146. self.log.debug('Attempting to preview file {0} with evince.'
  147. .format(fileName))
  148. ret=subprocess.Popen(('/usr/bin/evince-previewer', fileName))
  149. if ret != 0:
  150. self.log.error('evince returned non-zero exit code {0}'
  151. .format(ret))
  152. return 1
  153. return 0
  154. except OSError:
  155. self.log.debug('evince unavailable.')
  156. self.log.debug('Attempting to preview file {0} via xdg-open'
  157. .format(fileName))
  158. ret = subprocess.call(('/usr/bin/xdg-open', fileName))
  159. if ret != 0:
  160. self.log.error('xdg-open returned non-zero exit code {0}'.format(ret))
  161. return 1
  162. return 0
  163. else:
  164. self.log.warning('Unable to determine default viewer for POSIX platform {0}'.format(sys.platform))
  165. self.log.warning('Please file a bug and attach a copy of this log.')
  166. if os.name == 'nt': #UNTESTED
  167. self.log.debug('Attempting to preview file {0} via win32.startfile'
  168. .format(fileName))
  169. os.startfile(filepath)
  170. return 0
  171. def printout(self, filename, printer=None, orientation=None):
  172. if os.name == 'posix' or os.name == 'mac': #use CUPS/lpr
  173. self.con.printout(filename, printer, orientation)
  174. #---- printing proxy methods -----
  175. def listPrinters(self):
  176. """Returns list of names of available system printers (proxy)."""
  177. return self.con.listPrinters()
  178. def getPrinters(self):
  179. """
  180. Returns dictionary of printer name, description, location, and URI (proxy)
  181. """
  182. return self.con.getPrinters()
  183. class Nametag:
  184. def __init__(self, barcode=False):
  185. """
  186. Format nametags with data using available HTML templates. Will
  187. fetch barcode encoding options from global config; otherwise accepts
  188. barcode=False as the initialization argument.
  189. """
  190. #TODO: source config options for barcode, etc. from global config.
  191. self.barcodeEnable = barcode
  192. self.log = logging.getLogger(__name__)
  193. #Setup and compile regex replacements:
  194. self.date_re = re.compile(r'%DATE%', re.IGNORECASE) #date
  195. self.time_re = re.compile(r'%TIME%', re.IGNORECASE) #time
  196. self.room_re = re.compile(r'%ROOM%', re.IGNORECASE) #room
  197. self.first_re = re.compile(r'%FIRST%', re.IGNORECASE) #name
  198. self.name_re = re.compile(r'%NAME%', re.IGNORECASE) #name
  199. self.last_re = re.compile(r'%LAST%', re.IGNORECASE) #surname
  200. self.medical_re = re.compile(r'%MEDICAL%', re.IGNORECASE) #medical
  201. self.code_re = re.compile(r'%CODE%', re.IGNORECASE) #paging code
  202. self.secure_re = re.compile(r'%S%', re.IGNORECASE) #security code
  203. self.title_re = re.compile(r'%TITLE%', re.IGNORECASE) #security code
  204. self.number_re = re.compile(r'%NUMBER%', re.IGNORECASE) #security code
  205. self.level_re = re.compile(r'%LEVEL%', re.IGNORECASE) #security code
  206. self.age_re = re.compile(r'%AGE%', re.IGNORECASE) #age
  207. self.medicalIcon_re = re.compile(
  208. r"window\.onload\s=\shide\('medical'\)\;", re.IGNORECASE)
  209. self.volunteerIcon_re = re.compile(
  210. r"window\.onload\s=\shide\('volunteer'\)\;", re.IGNORECASE)
  211. def listTemplates(self, directory=nametags):
  212. """Returns a list of the installed nametag themes"""
  213. #TODO: add more stringent validation against config and html.
  214. directory = os.path.abspath(directory)
  215. self.log.debug("Searching for html templates in {0}".format(directory))
  216. try:
  217. resource = os.listdir(directory)
  218. except OSError as e:
  219. logger.error('({0})'.format(e))
  220. return []
  221. themes = []
  222. #Remove any files from themes[] that are not directories:
  223. for i in resource:
  224. if os.path.isdir(os.path.join(directory, i)):
  225. themes.append(i)
  226. valid = []
  227. #Check that each folder has the corresponding .conf file:
  228. for i in themes:
  229. if os.path.isfile(self._getTemplateFile(i)):
  230. valid.append(i)
  231. valid.sort()
  232. del resource, themes
  233. self.log.debug("Found templates {0}".format(valid))
  234. return valid
  235. def _getTemplateFile(self, theme, directory=nametags):
  236. """
  237. Returns full path to .conf file for an installed template pack and
  238. check it exists.
  239. """
  240. directory = os.path.abspath(directory)
  241. path = os.path.join(directory, theme, '{0}.conf'.format(theme))
  242. #TODO: more stringent checks (is there a matching HTML file?)
  243. if os.path.isfile(path):
  244. return path
  245. else:
  246. self.log.warning("{0} does not exist or is not file.".format(path))
  247. def _getTemplatePath(self, theme, directory=nametags):
  248. """Returns absolute path only of template pack"""
  249. return os.path.join(directory, theme)
  250. def readConfig(self, theme):
  251. """
  252. Reads the configuration for a specified template pack. Returns
  253. dictionary.
  254. """
  255. inifile = self._getTemplateFile(theme)
  256. self.log.info("Reading template configuration from '{0}'"
  257. .format(inifile))
  258. config = ConfigObj(inifile)
  259. try: #check for default section
  260. default = config['default']
  261. except KeyError as e:
  262. self.log.error(e)
  263. self.log.error("{0} contains no [default] section and is invalid."
  264. .format(inifile))
  265. raise KeyError("{0} contains no [default] section and is invalid."
  266. .format(inifile))
  267. del default
  268. return config
  269. def nametag(self, template='apis', name='', number='', title='', level='', age='', barcode=False):
  270. #def nametag(self, template='default', room='', first='', last='', medical='',
  271. # code='', secure='', barcode=True):
  272. #Check that theme specified is valid:
  273. if template != 'default':
  274. themes = self.listTemplates()
  275. if template not in themes:
  276. self.log.error("Bad theme specified. Using default instead.")
  277. template = 'default'
  278. #Read in the HTML from template.
  279. try:
  280. directory = self._getTemplatePath(template)
  281. except KeyError:
  282. self.log.error("Unable to process template '{0}'. Aborting."
  283. .format(template))
  284. return None
  285. #read in the HTML
  286. self.log.debug('Generating nametag with nametag()')
  287. self.log.debug('Reading {0}...'.format(os.path.join(directory,
  288. 'default.html')))
  289. f = open(os.path.join(directory, 'default.html'))
  290. html = f.read()
  291. f.close()
  292. if len(html) == 0: #template file is empty: use default instead.
  293. self.log.warn('HTML template file {0} is blank.'.format(
  294. os.path.join(directory, 'default.html')))
  295. #generate barcode of secure code, code128:
  296. if barcode:
  297. try:
  298. if secure == '':
  299. codebar.gen('code128', os.path.join(directory,
  300. 'default-secure.png'), code)
  301. else:
  302. codebar.gen('code128', os.path.join(directory,
  303. 'default-secure.png'), secure)
  304. except NotImplementedError as e:
  305. self.log.error('Unable to generate barcode: {0}'.format(e))
  306. html = html.replace(u'default-secure.png', u'white.gif')
  307. else:
  308. #replace barcode image with white.gif
  309. html = html.decode('utf-8').replace(u'default-secure.png', u'white.gif')
  310. self.log.debug("Disabled barcode.")
  311. #get the current date/time
  312. now = datetime.datetime.now()
  313. #Perform substitutions:
  314. #html = self.date_re.sub(now.strftime("%a %d %b %Y"), html)
  315. html = self.date_re.sub(now.strftime("%a<br>%Y-%m-%d"), html)
  316. html = self.time_re.sub(now.strftime("%H:%M:%S"), html)
  317. #Fix for if database returns None instead of empty string:
  318. html = self.name_re.sub(name.encode('utf-8'), html.encode('utf-8'))
  319. html = self.level_re.sub(str(level), html)
  320. html = self.title_re.sub(str(title), html)
  321. html = self.number_re.sub(str(number), html)
  322. html = self.age_re.sub(str(age), html)
  323. return html
  324. class _DummyPrinter:
  325. def __init__(self):
  326. self.con = None
  327. def listPrinters(self):
  328. return []
  329. def getPrinters(self):
  330. return {}
  331. def returnDefault(self):
  332. return ''
  333. def printout(self, filename, printer=None, orientation=None):
  334. raise PrinterError('No printer system available')
  335. class _CUPS:
  336. def __init__(self):
  337. self.log = logging.getLogger(__name__)
  338. self.log.info("Connecting to CUPS server on localhost...")
  339. try:
  340. import cups
  341. except ImportError as e:
  342. self.log.error("CUPS module not available. Is CUPS installed? {0}".format(e))
  343. self.con = cups.Connection()
  344. def listPrinters(self):
  345. """
  346. Returns a list of the names of available system printers.
  347. """
  348. return self.con.getPrinters().keys()
  349. def getPrinters(self):
  350. """
  351. Returns dictionary of printer name, description, location, and URI.
  352. """
  353. a = dict()
  354. printers = self.con.getPrinters()
  355. for item in printers:
  356. info = printers[item]['printer-info']
  357. location = printers[item]['printer-location']
  358. uri = printers[item]['device-uri']
  359. a[item] = { 'info' : info, 'location' : location, 'uri' : uri}
  360. return a
  361. def getDefault(self):
  362. """Determines the user's default system or personal printer."""
  363. return self.con.getDests()[None, None].name
  364. def printout(self, filename, printer=None, orientation=None):
  365. if printer != None: #set printer; if none, then use default (leave argument blank)
  366. if printer not in self.listPrinters():
  367. raise PrinterError('Specified printer is not available on this system')
  368. printArgs = ['-P', printer]
  369. printArgs = []
  370. else:
  371. printArgs = []
  372. if orientation: #set orientation option
  373. if orientation not in ['landscape', 'portrait']:
  374. raise PrinterError('Bad orientation specification: {0}'.format(orientation))
  375. #printArgs.append('-o')
  376. #printArgs.append(orientation)
  377. try: #see if file exists
  378. f = open(filename)
  379. except IOError:
  380. raise PrinterError('The specified file does not exist: {0}'.format(filename))
  381. else:
  382. f.close()
  383. printArgs.append("-o")
  384. printArgs.append("2.00x1.00\"")
  385. printArgs.append(filename) #append file to print.
  386. ret = subprocess.check_call([lpr,] + printArgs)
  387. if ret != 0:
  388. raise PrinterError('{0} exited non-zero ({1}). Error spooling.'.format(lpr, ret))
  389. class PrinterError(Exception):
  390. def __init__(self, value=''):
  391. if value == '':
  392. self.error = 'Generic spooling error.'
  393. else:
  394. self.error = value
  395. def __str__(self):
  396. return repr(self.error)
  397. class Main:
  398. def __init__(self, local=False):
  399. self.log = logging.getLogger(__name__)
  400. self.con = Printer(False)
  401. self.tag = Nametag(True)
  402. self.section = ''
  403. def nametag(self, theme='apis', name='', number='', title='', level='', barcode=False, section='default'):
  404. #def nametag(self, theme='default', room='', first='', last='', medical='',
  405. # code='', secure='', barcode=True, section='default'):
  406. """Note: section= not fully implemented in Nametag.nametag method"""
  407. self.section = section
  408. self.conf = self.tag.readConfig(theme) #theme
  409. self.args = self.con.buildArguments(self.conf, section) #section
  410. stuff = self.tag.nametag(name=name, number=number, title=title, template=theme, level=level)
  411. temp_path = self.tag._getTemplatePath(theme)
  412. html = tempfile.NamedTemporaryFile(delete=False, dir=temp_path, suffix='.html')
  413. html.write(stuff.encode('utf-8'))
  414. html.close()
  415. self.pdf = self.con.writePdf(self.args, html.name)
  416. os.unlink(html.name)
  417. return self.pdf
  418. def nametags(self, tags, theme='apis', section='default'):
  419. self.section = section
  420. self.conf = self.tag.readConfig(theme) #theme
  421. self.args = self.con.buildArguments(self.conf, section) #section
  422. html_files = []
  423. for data in tags:
  424. stuff = self.tag.nametag(
  425. name=data['name'], number=data['number'],
  426. title=data['title'], template=theme, level=data['level'],
  427. age=data['age']
  428. )
  429. temp_path = self.tag._getTemplatePath(theme)
  430. html = tempfile.NamedTemporaryFile(delete=False, dir=temp_path, suffix='.html')
  431. html.write(stuff)
  432. html.close()
  433. html_files.append(html.name)
  434. self.pdf = self.con.writePdf(self.args, html_files)
  435. for tmpname in html_files:
  436. #os.unlink(tmpname)
  437. pass
  438. return self.pdf
  439. def preview(self, filename=None):
  440. if filename == None:
  441. filename = self.pdf
  442. self.con.preview(filename)
  443. def printout(self, filename=None, printer=None, orientation=None):
  444. if filename == None:
  445. filename = self.pdf
  446. if orientation == None:
  447. orientation = self.conf[self.section]['orientation']
  448. self.con.printout(filename, printer, orientation)
  449. def cleanup(self, trash=None, *dt):
  450. if trash != None:
  451. for item in trash:
  452. os.unlink(item)
  453. else:
  454. os.unlink(self.pdf)
  455. self.section = ''
  456. if __name__ == '__main__':
  457. tags = [
  458. { 'name' : "Barkley Woofington", 'number' : "S-6969", 'level' : "Top Dog", 'title' : '' , 'age' : 20},
  459. { 'name' : "Rechner Foxer", 'number' : "S-0001", 'level' : "Foxo", 'title' : '' , 'age' : 12}
  460. ]
  461. con = Main(False)
  462. con.nametags(tags, theme='apis')
  463. #con.nametag(theme='apis', name="Some Kind Of Horse", number="S-0000", title="Staff", level="Player")
  464. print(con.pdf)
  465. con.preview()
  466. #con.printout(printer="LabelWriter-450-Turbo")
  467. raw_input(">")
  468. con.cleanup()