#!/usr/bin/env python # -*- coding: utf-8 -*- """ .. codeauthor:: Daniel STAN .. codeauthor:: Antoine Durand-Gasselin Pour envoyer des jobs d'impression à l'imprimante canon via son interface web. License: GPLv3 """ # impression_hp.py # # Classe impression pour HP MFP 880 # # Copyright (c) 2006, 2007, 2008, 2009 by Cr@ns (http://www.crans.org) # ############################################################# import sys, os import logging import logging.handlers if '/usr/scripts' not in sys.path: sys.path.append('/usr/scripts') from base import FichierInvalide, SoldeInsuffisant, PrintError, SettingsError from gestion.config import impression as config_impression from subprocess import Popen, PIPE, check_output import livret ## logger = logging.getLogger('impression_hp') logger.setLevel(logging.DEBUG) formatter = logging.Formatter('%(name)s: [%(levelname)s] %(message)s') handler = logging.handlers.SysLogHandler(address = '/dev/log') try: handler.addFormatter(formatter) except AttributeError: handler.formatter = formatter logger.addHandler(handler) DEBUG = bool(os.getenv('DEBUG', False)) DECOUVERT_AUTHORISE = config_impression.decouvert class Option(object): # Valeur par défaut value = None def __str__(self): return str(value) name = "option" pretty_name = u"option quelconque" def parse(self, v): self.value = v class BooleanOption(Option): value = False def parse(self, v): self.value = bool(v) if str(v).lower() == 'false': self.value = False class Couleur(BooleanOption): name = "couleur" pretty_name = u"impression couleur" class Livret(BooleanOption): name = "livret" pretty_name = u"livret piqûres à cheval" def nb(self): return 2*int(self.value) class Duplex(BooleanOption): name = "recto_verso" pretty_name = u"Impression recto verso" class Copies(Option): name = "copies" pretty_name = u"nombre d'exemplaires" value = 1 def parse(self, v): self.value = int(v) class SelectOption(Option): possibilities={ None: u'Aucun', } value = None def parse(self, v): if v == 'None': v = None if v not in self.possibilities: return self.value = v class Agrafage(SelectOption): name = "agrafage" pretty_name = u"agrafage" possibilities = { None : "aucune agrafe", "TopLeft" : u"agrafe en haut à gauche", "TopRight" : u"agrafe en haut à droite", #"BottomLeft" : u"agrafe en bas à gauche", # n'existe pas #"BottomRight" : u"agrafe en bas à droite", # n' existe pas "Left": u"deux agrafes sur le bord gauche", "Right" : u"deux agrafes sur le bord droit", #"Top" : u"deux agrafes sur le bord supérieur", #pas faisable #"Bottom" : u"deux agrafes sur le bord inférieur", #n'existe pas } def nb(self): if not self.value: return 0 if 'Left' in self.value or 'Right' in self.value: return 2 - int('Top' in self.value or 'Bottom' in self.value) return 2 def HP_name(self): if 'Top' in self.value: return '1Staple%sAngled' % (self.value.replace('Top', '')) else: return '2Staples%s' % self.value class Papier(SelectOption): name = "papier" pretty_name = u"format papier" value = 'A4' possibilities = { 'A4' : "Papier A4 ordinaire", 'A3' : "Papier A3 ordinaire", } class Perforation(SelectOption): name = "perforation" pretty_name = u"perforation" possibilities = { None: u"Aucune", "2HolePunchLeft": u"2HolePunchLeft", "2HolePunchRight": u"2HolePunchRight", "2HolePunchTop": u"2HolePunchTop", "2HolePunchBottom": u"2HolePunchBottom", "3HolePunchLeft": u"3HolePunchLeft", "3HolePunchRight": u"3HolePunchRight", "3HolePunchTop": u"3HolePunchTop", "4HolePunchLeft": u"4HolePunchLeft", "4HolePunchRight": u"4HolePunchRight", "4HolePunchTop": u"4HolePunchTop", } # ######################################################## # # CLASSE IMPRESSION # # ######################################################## # # # class impression(object): """impression Un objet impression correspond à un fichier pdf et un adhérent. """ # fichier (chemin) _fichier = "" # adhérent (instance) _adh = None # paramètres _settings_list = [ Agrafage, Papier, Couleur, Duplex, Livret, Copies, Perforation, ] _settings = {} # le prix de l'impression _prix = 0.0 _pages = 0 # le cout de base encre pour une impression en couleurs/n&b # (prix pour papier A4) _base_prix_nb = 0.0 _base_prix_couleurs = 0.0 # Format du pdf, tout droit sorti de pdfinfo # (les dimensions sont donc en pts) _format = '(A4)' _width = 595.28 _height = 841.89 # Jid unique, à définir avant l'impression _jid = 0 def __init__(self, path_to_pdf, adh = None): """impression(path_to_pdf [, adh]) Crée un nouvel objet impression à partir du fichier pdf pointé par path_to_pdf. Si adh ext donné, il peut être soit une instance d'un objet adhérent de crans_ldap soit le login de l'adhérent. Lève l'exception FichierInvalide si le fichier n'existe pas ou si ce n'est pas un pdf. """ # On génère la liste des options self._settings = {cls.name: cls() for cls in self._settings_list} self._fichier = path_to_pdf self._adh = adh # on verifie que le fichier existe if not os.path.isfile(path_to_pdf): raise FichierInvalide, ("Fichier introuvable", path_to_pdf) if not open(path_to_pdf).read().startswith("%PDF"): raise FichierInvalide, ("Le fichier ne semble pas etre un PDF", path_to_pdf) # on compte les pages et on regarde le format pdfinfo = Popen(["pdfinfo",self._fichier],stdout=PIPE,stderr=PIPE).communicate() if pdfinfo[1] <> '': raise FichierInvalide(u"pdfinfo n'arrive pas a lire le fichier (il est peut-etre corrompu ou protege par un mot de passe), https://wiki.crans.org/VieCrans/ImpressionReseau#Format_des_fichiers",path_to_pdf) self._pages = -1 for line in pdfinfo[0].split('\n'): if line.startswith('Pages'): self._pages = int(line.split()[1]) elif line.startswith('Page size'): size = line.split() if len(size) <= 6: self._format = "Unknown" else: self._format = size[6] self._width = float(size[2]) self._height = float(size[4]) # Hack pour mieux reconnaître les formats w = min(self._width,self._height) h = max(self._width,self._height) err = 100 if abs(w - 595) < err and abs(h - 842) < err: self._format = "(A4)" elif abs(w - 842) < err and abs(h - 1180) < err: self._format = "(A3)" if self._pages <= 0: raise FichierInvalide(u"Impossible de lire le nombre de pages",path_to_pdf) if not self._format in ['(A4)','(A3)']: pass #raise FichierInvalide, u"Seuls les formats A3 et A4 sont supportes" # calcule le prix de l'encre tout de suite self._calcule_prix() def _uniq_jid(self): """ Alloue un jid unique """ fname = '/var/impression/jid' ## Maybe need a lock ? try: f = file(fname,'r+') cur = int(f.read())+1 f.seek(0) except (IOError,ValueError): cur = 0 f = file(fname,'w') f.write(str(cur)) f.close() return cur def _pdfbook(self): """Génère un pdf livret équivalent et renvoie le chemin""" newfile = '/tmp/' + self.fileName()[-4] + '-book.pdf' newfile = '/tmp/book.pdf' check_output( ['/usr/bin/pdfjam', self._fichier, livret.pdfjam_order(self._pages), '-o', newfile, ]) return newfile def changeSettings(self, **kw): """changeSettings([keyword=value]) Change les parametres de l'impression, recalcule et renvoie le nouveau prix. """ for name in kw: if name not in self._settings: raise Exception('unknown %s option' % name) self._settings[name].parse(kw[name]) if self._settings['livret'].value: self._settings['recto_verso'].parse(True) self._settings['agrafage'].parse(None) # TODO # Check si l'agrafage et cie sont compatibles avec le nb de pages return self._calcule_prix() def printSettings(self): """printSettings() Affiche les paramètres courants sur la sortie standard """ for k in self._settings: opt = self._settings[k] label = u"%s: %s" % (opt.pretty_name.capitalize(), opt.value) print label.encode('UTF-8') def prix(self): """prix() Renvoie le prix courrant de l'impression """ return self._prix def fileName(self): """fileName() renvoie le nom du fichier pdf (exemple : monPdf.pdf) """ return os.path.basename(self._fichier) def filePath(self): """filePath() renvoie le chemin d'accès au fichier pdf. """ return self._fichier def pages(self): """pages() renvoie le nombre de pages du document (page au sens nombre de faces à imprimer et non le nombre de feuilles) """ return self._pages def imprime(self): """imprime() imprime le document pdf. débite l'adhérent si adhérent il y a. (si il a été indiqué à l'initialisation de l'objet) """ self._jid = self._uniq_jid() # debite l'adhérent si adherent il y a if not DEBUG and (self._adh != None): adh = self._adh.split('@') if len(adh) > 1: adh = adh[1:] adh = self._get_adh(adh[0]) self._calcule_prix() # Normalement inutile, mais évite les races if (self._prix > (adh.solde() - DECOUVERT_AUTHORISE)): raise SoldeInsuffisant adh.solde(-self._prix, "impression(%d): %s par %s" % (self._jid,self._fichier,self._adh)) adh.save() del adh # imprime le document self._exec_imprime() def _calcule_prix(self): faces = self._pages if self._settings['livret'].value: feuilles = int((faces+3)/4) faces = 2 * feuilles elif self._settings['recto_verso'].value: feuilles = int(faces/2.+0.5) else: feuilles = faces if (self._settings['papier'].value == "A3"): c_papier = config_impression.c_a3 pages = 2*faces else: pages = faces c_papier = config_impression.c_a4 if self._settings['couleur'].value: c_impression = c_papier * feuilles + config_impression.c_face_couleur * pages else: c_impression = c_papier * feuilles + config_impression.c_face_nb * pages # Cout des agrafes nb_agrafes = self._settings['agrafage'].nb() + self._settings['livret'].nb() c_agrafes = nb_agrafes * config_impression.c_agrafe c_total = int(self._settings['copies'].value * ( c_impression + c_agrafes ) + 0.5) # arrondi et facture self._prix = float(c_total)/100 return self._prix def _get_adh(self, adh): if type(adh) == str: #from ldap_crans_test import crans_ldap from gestion.ldap_crans import CransLdap adh = CransLdap().getProprio(adh, 'w') return adh ## ################################# ## ## fonction qui imprime pour de vrai ## ## ################################# ## ## def _exec_imprime(self): """ Envoie l'impression a l'imprimante avec les parametres actuels """ if (self._adh != None): logger.info('Impression(%d) [%s] : %s' % (self._jid, self._adh, self._fichier)) else: logger.info("Impression(%d) : %s" % (self._jid, self._fichier)) # Création de la liste d'options pour lp options = list() # TODO que se passe-t-il si le fichier commence par -o ou -- ? if self._settings['livret'].value: options.append(self._pdfbook()) else: options.append(self._fichier) # Pour spécifier l'imprimante options += ['-d', 'MFPM880'] # Pour spécifier un jobname de la forme jid:adh:nom_du_fichier jobname = '%d:%s:%s' % (self._jid, self._adh, self._fichier.split('/')[-1].replace("\"","\\\"")) options += ['-t', jobname] #Pour le nombre de copies et specifie non assemblee options += ['-n', str(self._settings['copies'].value)] #Et on ne les veux pas assemblées (sic) options += ['-o', 'Collate=True'] # Pour donner le login de l'adherent (TODO: useful ?) options += ['-U', str(self._adh)] if self._settings['papier'].value == 'A4': options += ['-o', 'PageSize=A4'] else: options += ['-o', 'pdf-expand', '-o', 'pdf-paper=841x1190', '-o', 'PageSize=A3'] # Toujours resize (pour éviter que l'imprimante ne deadlock) options += ['-o', 'fit-to-page'] # Quelle alimentation papier utiliser ? options.append('-o') if self._settings['papier'].value == 'A3': options.append('InputSlot=Tray1') elif self._settings['livret'].value: options.append('InputSlot=Tray2') else: options.append('InputSlot=Tray3') if not self._settings['couleur'].value: options += ['-o', 'HPColorAsGray=True'] duplex = self._settings['recto_verso'].value or\ self._settings['livret'].value paysage = self._width > self._height # mode paysage: on indique que c'est landscape (ou livret) if paysage: options += ['-o', 'landscape'] # Livret: 2 pages par côté, et on inverse le bord duplex if self._settings['livret'].value: options += ['-o', 'number-up=2'] paysage = not paysage if duplex: if paysage: options += ['-o', 'sides=two-sided-short-edge'] else: options += ['-o', 'sides=two-sided-long-edge'] if not duplex: options += ['-o', 'sides=one-sided'] if self._settings['livret'].value: options += ['-o', 'HPStaplerOptions=FoldStitch'] elif self._settings['agrafage'].value: v = self._settings['agrafage'].HP_name() options += ['-o', 'HPStaplerOptions=%s' % v] if not DEBUG: logger.info("lp " + " ".join(options)) check_output(['lp'] + options) else: logger.info("pretend printing (debug): " + " ".join(options))