scripts/impression/impression_hp.py
2015-06-19 17:51:13 +02:00

536 lines
16 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
.. codeauthor:: Daniel STAN <dstan@crans.org>
.. codeauthor:: Antoine Durand-Gasselin <adg@crans.org>
Pour envoyer des jobs d'impression à l'imprimante canon via son interface web.
License: GPLv3
"""
# impression_hp.py
#
import sys, os
import logging
import logging.handlers
import BeautifulSoup
import requests
import math
if '/usr/scripts' not in sys.path:
sys.path.append('/usr/scripts')
from base import FichierInvalide, PrintError, SettingsError
from gestion.config import impression as config_impression
from subprocess import Popen, PIPE, check_output
import livret
import lc_ldap.shortcuts
import lc_ldap.crans_utils as crans_utils
## Pour les codes qui se connectent direct à l'imprimante en http
BASE_URL = 'https://imprimante.adm.crans.org'
URL_JOBLIST = BASE_URL + '/hp/device/JobLogReport/Index'
URL_STATE = BASE_URL + '/'
CA = '/etc/ssl/certs/cacert.org.pem'
##
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('DBG_PRINTER', False))
class Option(object):
# Valeur par défaut
value = None
def __str__(self):
return str(self.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 d'un fichier avec l'imprimante HP MFP M880
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._get_adh_write()
self._calcule_prix() # Normalement inutile, mais évite les races
with adh:
adh.solde(-self._prix, u"impression(%d): %s par %s" % (self._jid,self._fichier, self._adh))
adh.save()
del adh
# imprime le document
self._exec_imprime()
_doc = None
def low_tray(self, num):
"""Indique si un bac est vide ou presque vide"""
if self._doc is None:
req = requests.get(URL_STATE, verify=CA)
self._doc = BeautifulSoup.BeautifulSoup(req.text)
try:
tray = self._doc.findAll(attrs={'id': 'TrayBinStatus_%d' % num})[0]
except IndexError:
return False
state = tray.attrMap['class']
if 'morethan40' in state or 'between20and40':
return False
if 'empty' in state:
return True
logger.info('Unknown Tray State: %s' % state)
return False
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 = math.floor(self._settings['copies'].value * ( c_impression +
c_agrafes)) # arrondi et facture
self._prix = float(c_total)/100
return self._prix
@lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5,
constructor=lc_ldap.shortcuts.lc_ldap_admin)
def _get_adh_write(self, ldap):
"""Récupère l'adhérent en lecture-écriture"""
uid = self._adh
if type(uid) in [str, unicode]:
uid = uid.split('@')[-1]
adh = ldap.search(u'uid=%s' % crans_utils.escape(uid), mode='rw')[0]
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()
# 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']
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: # A4, dans Tray3 et Tray4
## On utilise en priorité le Tray 4 (plus gros)
if not self.low_tray(4) or self.low_tray(3):
options.append('InputSlot=Tray4')
else:
options.append('InputSlot=Tray3')
if not self._settings['couleur'].value:
options += ['-o', 'HPColorAsGray=True']
options += ['-o', 'ColorModel=Grayscale']
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 self._settings['livret'].value:
if self._settings['perforation'].value:
v = self._settings['perforation']
options += ['-o', 'HPPunchingOptions=%s' % v]
# Que se passe-t-il si le nom de fichier commence par - ?
# pour éviter cela, on indique la fin des options, avec "--"
options.append('--')
if self._settings['livret'].value:
options.append(self._pdfbook())
else:
options.append(self._fichier)
if not DEBUG:
logger.info("lp " + " ".join(options))
check_output(['lp'] + options)
else:
logger.info("pretend printing (debug): " + " ".join(options))