From 8cd2622469778cb0bb670b8061ae91da4cdbd98b Mon Sep 17 00:00:00 2001 From: Daniel STAN Date: Wed, 10 Sep 2014 23:18:23 +0200 Subject: [PATCH] impression_hp: first draft --- impression/impression_hp.py | 503 ++++++++++++++++++++++++++++++++++++ impression/livret.py | 31 +++ 2 files changed, 534 insertions(+) create mode 100644 impression/impression_hp.py create mode 100755 impression/livret.py diff --git a/impression/impression_hp.py b/impression/impression_hp.py new file mode 100644 index 00000000..8a8181f8 --- /dev/null +++ b/impression/impression_hp.py @@ -0,0 +1,503 @@ +#!/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 (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'] + + options.append('-o') + # Quelle alimentation papier utiliser ? + 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)) + + + diff --git a/impression/livret.py b/impression/livret.py new file mode 100755 index 00000000..838c0641 --- /dev/null +++ b/impression/livret.py @@ -0,0 +1,31 @@ +#!/bin/bash /usr/scripts/python.sh + +import sys +import math +import itertools + +def booklet_order(n): + page = (lambda k: None if k > n else k) + + recto = 2*int(math.ceil(n/4.)) + verso = recto + 2 + + while recto > 1: + yield page(verso) + yield page(recto-1) + yield page(recto) + yield page(verso-1) + recto -= 2 + verso += 2 + +def pdfjam_order(n): + return ",".join(itertools.imap(lambda k: str(k or '{}'), booklet_order(n))) + +if __name__ == '__main__': + try: + n = int(sys.argv[1]) + except: + print "Veuillez entrer un nombre" + exit(1) + + print pdfjam_order(n)