diff --git a/.gitignore b/.gitignore index 6fde163..d3853ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ *.pyc *.ini *.log +*.swp ======= docs/_build +build +dist +Admin_CFDI.egg-info diff --git a/README.rst b/README.rst index 9c40779..11787bc 100644 --- a/README.rst +++ b/README.rst @@ -2,13 +2,13 @@ admin-cfdi ========== :Autores: - Universo Libre A.C., Python Cabal + Ver archivo contributors.txt :Fecha: 07/12/2015 -:Versión: - 0.2.8 +:Ultima Versión: + 0.3.0 Descripción @@ -27,7 +27,7 @@ Pequeño sistema para administrar CFDIs; facturas electrónicas de México. Entr Requerimientos -------------- -* Python 3.4 +* Python 3.2+ * Tk si usas Linux. Si usas Windows, ya lo integra Python. * Firefox para la automatización de la descarga del SAT. * Selenium para la automatización de la descarga del SAT. @@ -37,23 +37,16 @@ Requerimientos Instalación ----------- -Si tienes instalado correctamente Python 3.4, puedes instalar con Pip. +Si tienes instalado correctamente Python 3.2+, puedes instalar con Pip. GNU & LInux ########### :: - sudo pip install selenium pygubu - python admincfdi.py + sudo python setup.py install + admin-cfdi -ArchLinux -_________ - -:: - - sudo pip install selenium pygubu - python admincfdi.py Linux Mint __________ @@ -61,8 +54,8 @@ __________ :: sudo apt-get install python3-pip python3-tk - sudo pip3 install selenium pygubu - python3 admincfdi.py + sudo python setup.py install + admin-cfdi Windows @@ -72,7 +65,7 @@ Si usas Windows, asegúrate de abrir el script con el ejecutable pythonw.exe loc :: - pip install selenium pygubu + python setup.py install Ligas diff --git a/admincfdi.py b/admin-cfdi old mode 100644 new mode 100755 similarity index 78% rename from admincfdi.py rename to admin-cfdi index b2d7f1a..8552d75 --- a/admincfdi.py +++ b/admin-cfdi @@ -14,12 +14,9 @@ import tkinter as tk import pygubu from selenium import webdriver -from pyutil import Util -from pyutil import Mail -from pyutil import LibO -from pyutil import APP_LIBO -from pyutil import CFDIPDF -from values import Global +from admincfdi.pyutil import Util, Mail, LibO, CFDIPDF, DescargaSAT +from admincfdi.values import Global +from admincfdi.pyutil import LIBO class Application(pygubu.TkApplication): @@ -139,14 +136,13 @@ def _create_ui(self): combo = self._get_object('combo_end_second') self.util.combo_values(combo, minutes, 59) - if not APP_LIBO: - self._config('radio_ods', {'state': 'disabled'}) - self._config('radio_json', {'state': 'disabled'}) - self._config('button_select_template_json', {'state': 'disabled'}) - self._set('mail_port', 993) self._get_object('check_ssl').invoke() self._focus_set('text_rfc') + + if not LIBO: + self._config('radio_ods', {'state': 'disabled'}) + self.pb = self._get_object('progressbar') return def _center_window(self, root): @@ -283,10 +279,10 @@ def radio_template_type(self, event): radio = 'radio_ods' button = 'button_select_template_ods' self._config(button, {'state': 'normal'}) - self._config('button_select_template_json', {'state': 'normal'}) + self._config('button_select_template_csv', {'state': 'normal'}) if opt == 1: - radio = 'radio_json' - button = 'button_select_template_json' + radio = 'radio_csv' + button = 'button_select_template_csv' self._config(radio, {'bg': self.g.COLORS['DEFAULT']}) self._config(button, {'state': 'disabled'}) return @@ -389,235 +385,93 @@ def combo_month_click(self, event): self.util.combo_values(combo, days, 0) return + def msg_user(self, msg): + self._set('msg_user', msg, True) + + def progress(self, value, maximum): + pb = self._get_object('progressbar') + pb['value'] = value + pb['maximum'] = maximum + self.parent.update_idletasks() + def button_download_sat_click(self): ok, data = self._validate_download_sat() if not ok: return - self._download_sat(data) - return - - def _download_sat(self, data): - self._set('msg_user', 'Abriendo Firefox...', True) - page_query = self.g.SAT['page_receptor'] - if data['type_invoice'] == 1: - page_query = self.g.SAT['page_emisor'] - # To prevent download dialog - profile = webdriver.FirefoxProfile() - profile.set_preference( - 'browser.download.folderList', 2) - profile.set_preference( - 'browser.download.manager.showWhenStarting', False) - profile.set_preference( - 'browser.helperApps.alwaysAsk.force', False) - profile.set_preference( - 'browser.helperApps.neverAsk.saveToDisk', - 'text/xml, application/octet-stream, application/xml') - profile.set_preference( - 'browser.download.dir', data['user_sat']['target_sat']) - # mrE - desactivar telemetry - profile.set_preference( - 'toolkit.telemetry.prompted', 2) - profile.set_preference( - 'toolkit.telemetry.rejected', True) - profile.set_preference( - 'toolkit.telemetry.enabled', False) - profile.set_preference( - 'datareporting.healthreport.service.enabled', False) - profile.set_preference( - 'datareporting.healthreport.uploadEnabled', False) - profile.set_preference( - 'datareporting.healthreport.service.firstRun', False) - profile.set_preference( - 'datareporting.healthreport.logging.consoleEnabled', False) - profile.set_preference( - 'datareporting.policy.dataSubmissionEnabled', False) - profile.set_preference( - 'datareporting.policy.dataSubmissionPolicyResponseType', 'accepted-info-bar-dismissed') - #profile.set_preference( - # 'datareporting.policy.dataSubmissionPolicyAccepted'; False) # este me marca error, why? - #oculta la gran flecha animada al descargar - profile.set_preference( - 'browser.download.animateNotifications', False) + descarga = DescargaSAT( + status_callback=self.msg_user, download_callback=self.progress) + profile = descarga.get_firefox_profile(data['carpeta_destino']) try: - pb = self._get_object('progressbar') - browser = webdriver.Firefox(profile) - self._set('msg_user', 'Conectando...', True) - browser.get(self.g.SAT['page_init']) - txt = browser.find_element_by_name(self.g.SAT['user']) - txt.send_keys(data['user_sat']['user_sat']) - txt = browser.find_element_by_name(self.g.SAT['password']) - txt.send_keys(data['user_sat']['password']) - txt.submit() - self.util.sleep(3) - self._set('msg_user', 'Conectado...', True) - browser.get(page_query) - self.util.sleep(5) - self._set('msg_user', 'Buscando...', True) - if data['type_search'] == 1: - txt = browser.find_element_by_id(self.g.SAT['uuid']) - txt.click() - txt.send_keys(data['search_uuid']) - else: - # Descargar por fecha - opt = browser.find_element_by_id(self.g.SAT['date']) - opt.click() - self.util.sleep() - if data['search_rfc']: - if data['type_search'] == 1: - txt = browser.find_element_by_id(self.g.SAT['receptor']) - else: - txt = browser.find_element_by_id(self.g.SAT['emisor']) - txt.send_keys(data['search_rfc']) - # Emitidas - if data['type_invoice'] == 1: - year = int(data['search_year']) - month = int(data['search_month']) - dates = self.util.get_dates(year, month) - txt = browser.find_element_by_id(self.g.SAT['date_from']) - arg = "document.getElementsByName('{}')[0]." \ - "removeAttribute('disabled');".format( - self.g.SAT['date_from_name']) - browser.execute_script(arg) - txt.send_keys(dates[0]) - txt = browser.find_element_by_id(self.g.SAT['date_to']) - arg = "document.getElementsByName('{}')[0]." \ - "removeAttribute('disabled');".format( - self.g.SAT['date_to_name']) - browser.execute_script(arg) - txt.send_keys(dates[1]) - # Recibidas - else: - arg = "document.getElementById('{}').value={};".format( - self.g.SAT['year'], data['search_year']) - browser.execute_script(arg) - arg = "document.getElementById('{}').value={};".format( - self.g.SAT['month'], data['search_month']) - browser.execute_script(arg) - - if data['search_day'] != '00': - arg = "document.getElementById('{}').value='{}';".format( - self.g.SAT['day'], data['search_day']) - browser.execute_script(arg) - self.util.sleep() - - #Establece hora de inicio y fin - values = ( - 'start_hour', 'start_minute', 'start_second', - 'end_hour', 'end_minute', 'end_second' - ) - for v in values: - arg = "document.getElementById('{}').value={};".format( - self.g.SAT[v], data[v]) - browser.execute_script(arg) - self.util.sleep() - - browser.find_element_by_id(self.g.SAT['submit']).click() - sec = 3 - if data['type_invoice'] != 1 and data['search_day'] == '00': - sec = 15 - self.util.sleep(sec) - # Bug del SAT - if data['type_invoice'] != 1 and data['search_day'] != '00': - arg = "document.getElementById('{}').value='{}';".format( - self.g.SAT['day'], data['search_day']) - browser.execute_script(arg) - self.util.sleep(2) - browser.find_element_by_id(self.g.SAT['submit']).click() - self.util.sleep(sec) - elif data['type_invoice'] == 2 and data['sat_month']: - return self._download_sat_month(data, browser) - - try: - found = True - content = browser.find_elements_by_class_name( - self.g.SAT['subtitle']) - for c in content: - if self.g.SAT['found'] in c.get_attribute('innerHTML') \ - and c.is_displayed(): - found = False - break - except Exception as e: - print (str(e)) - - if found: - docs = browser.find_elements_by_name(self.g.SAT['download']) - t = len(docs) - pb['maximum'] = t - pb.start() - for i, v in enumerate(docs): - msg = 'Factura {} de {}'.format(i+1, t) - pb['value'] = i + 1 - self._set('msg_user', msg, True) - download = self.g.SAT['page_cfdi'].format( - v.get_attribute('onclick').split("'")[1]) - browser.get(download) - pb['value'] = 0 - pb.stop() - self.util.sleep() - else: - self._set('msg_user', 'Sin facturas...', True) + descarga.connect(profile, rfc=data['rfc'], ciec=data['ciec']) + docs = descarga.search( + facturas_emitidas=data['facturas_emitidas'], + uuid=data['uuid'], + rfc_emisor=data['rfc_emisor'], + año=data['año'], + mes=data['mes'], + día=data['día'], + hora_inicial=data['start_hour'], + minuto_inicial=data['start_minute'], + segundo_inicial=data['start_second'], + hora_final=data['end_hour'], + minuto_final=data['end_minute'], + segundo_final=data['end_second'], + mes_completo_por_día=data['mes_completo_por_día']) + descarga.download(docs) except Exception as e: - print (e) + self.g.LOG.error("Descarga SAT: \n" + str(e)) finally: - pb['value'] = 0 - pb.stop() - try: - self._set('msg_user', 'Desconectando...', True) - link = browser.find_element_by_partial_link_text('Cerrar Sesi') - link.click() - except: - pass - finally: - browser.close() - self._set('msg_user', 'Desconectado...') - return - - def _download_sat_month(self, data, browser): - year = int(data['search_year']) - month = int(data['search_month']) - days_month = self.util.get_days(year, month) + 1 - days = ['%02d' % x for x in range(1, days_month)] - for d in days: - combo = browser.find_element_by_id(self.g.SAT['day']) - sb = combo.get_attribute('sb') - combo = browser.find_element_by_id('sbToggle_{}'.format(sb)) - combo.click() - self.util.sleep(2) - if data['search_month'] == d: - links = browser.find_elements_by_link_text(d) - for l in links: - p = l.find_element_by_xpath( - '..').find_element_by_xpath('..') - sb2 = p.get_attribute('id') - if sb in sb2: - link = l - break - else: - link = browser.find_element_by_link_text(d) - link.click() - self.util.sleep(2) - browser.find_element_by_id(self.g.SAT['submit']).click() - self.util.sleep(3) - docs = browser.find_elements_by_name(self.g.SAT['download']) - if docs: - t = len(docs) - pb = self._get_object('progressbar') - pb['maximum'] = t - pb.start() - for i, v in enumerate(docs): - msg = 'Factura {} de {}'.format(i+1, t) - pb['value'] = i + 1 - self._set('msg_user', msg, True) - download = self.g.SAT['page_cfdi'].format( - v.get_attribute('onclick').split("'")[1]) - browser.get(download) - self.util.sleep() - pb['value'] = 0 - pb.stop() - self.util.sleep() + descarga.disconnect() return def _validate_download_sat(self): + '''Valida requisitos y crea datos para descarga + + Las validaciones son: + + - Al menos una tríada RFC, CIEC y carpeta + destino ha sido registrada + - Una tríada RFC, CIEC y carpeta destino + está seleccionada + - La UUID no es nula y tiene 36 caracteres, + si se seleccionó *Buscar por folio + fiscal (UUID)* + - El RFC del emisor tiene 12 o 13 + caracteres, si se proporciona + + Si falta alguna de estas condiciones, + se abre un diálogo con un texto informativo + y un botón 'OK' en el ambiente gráfico + y se regresa (False, {}) + + Si las validaciones pasan, se construye un + diccionario ``data`` con estas llaves y valores + requeridos por la API de descarga: + + - user_sat: un diccionario con el + RFC, la CIEC y la carpeta destino + que se seleccionaron, con las llaves + ``user_sat``, ``password`` y ``target_sat``. + - facturas_emitidas: True si la selección hecha en + *Tipo de consulta* es 1 facturas emitidas, False + si la selección es 2 facturas recibidas + - search_uuid: el valor llenado para *UUID*, + es cadena vacía por omisión + - search_rfc: el valor llenado en *RFC Emisor*, + es cadena vacía por omisión + - search_year: el valor seleccionado en *Año* + como cadena, es el año en curso por omisión + - search_month: el valor seleccionado en *Mes* + como cadena, es el mes en curso por omisión + - search_day: el valor seleccionado en *Día* + como cadena, es '00' por omisión o si sat_month + es verdadero. + - sat_month: Representa la caja a la izquierda de + *Descargar mes completo por día*. + + La función regresa (True, data) + ''' + data = {} if not self.users_sat: msg = 'Agrega un RFC y contraseña a consultar' @@ -651,20 +505,19 @@ def _validate_download_sat(self): self.util.msgbox(msg) return False, data sat_month = self._get('sat_month') - if sat_month: - search_day = '00' - else: - search_day = self._get('search_day') + search_day = self._get('search_day') + user = self.users_sat[current_user] data = { - 'user_sat': self.users_sat[current_user], - 'type_invoice': self._get('type_invoice'), - 'type_search': opt, - 'search_uuid': uuid, - 'search_rfc': rfc, - 'search_year': self._get('search_year'), - 'search_month': self._get('search_month'), - 'search_day': search_day, - 'sat_month': sat_month, + 'facturas_emitidas': self._get('type_invoice') == 1, + 'rfc': user['user_sat'], + 'ciec': user['password'], + 'carpeta_destino': user['target_sat'], + 'uuid': uuid, + 'rfc_emisor': rfc, + 'año': self._get('search_year'), + 'mes': self._get('search_month'), + 'día': search_day, + 'mes_completo_por_día': sat_month, 'start_hour': self._get('start_hour'), 'start_minute': self._get('start_minute'), 'start_second': self._get('start_second'), @@ -1074,20 +927,20 @@ def button_select_folder_source_pdf_click(self): def button_select_folder_target_pdf_click(self): folder = self.util.get_folder(self.parent) if folder: - self._set('target_pdf', folder) + self._set('pdf_target', folder) return def button_select_template_ods_click(self): file_name = self.util.get_file(self.parent, ext=self.g.EXT_ODS) if file_name: self._set('template_ods', file_name) - self._set('template_json') + self._set('template_csv') return - def button_select_template_json_click(self): - file_name = self.util.get_file(self.parent, ext=self.g.EXT_JSON) + def button_select_template_csv_click(self): + file_name = self.util.get_file(self.parent, ext=self.g.EXT_CSV) if file_name: - self._set('template_json', file_name) + self._set('template_csv', file_name) self._set('template_ods') return @@ -1104,7 +957,7 @@ def button_save_pdf_user_click(self): 'pdf_source', 'pdf_target', 'template_ods', - 'template_json' + 'template_csv' ) for v in var: self._set(v) @@ -1162,13 +1015,13 @@ def _validate_pdf_user(self): self.util.msgbox(msg) return False, data template_ods = self.util.path_config(template_ods) - template_json = self._get('template_json') - if template_json: - if not self.util.validate_dir(template_json): - msg = 'No se encontró la plantilla JSON' + template_csv = self._get('template_csv') + if template_csv: + if not self.util.validate_dir(template_csv): + msg = 'No se encontró la plantilla CSV' self.util.msgbox(msg) return False, data - template_json = self.util.path_config(template_json) + template_csv = self.util.path_config(template_csv) template_type = self._get('template_type') if template_type == 1: @@ -1177,8 +1030,8 @@ def _validate_pdf_user(self): self.util.msgbox(msg) return False, data elif template_type == 2: - if not template_json: - msg = 'Selecciona una plantilla JSON' + if not template_csv: + msg = 'Selecciona una plantilla CSV' self.util.msgbox(msg) return False, data @@ -1186,7 +1039,7 @@ def _validate_pdf_user(self): data['pdf_source'] = self.util.path_config(pdf_source) data['pdf_target'] = self.util.path_config(pdf_target) data['template_ods'] = template_ods - data['template_json'] = template_json + data['template_csv'] = template_csv #~ data['pdf_print'] = self._get('pdf_print') return True, data @@ -1199,7 +1052,7 @@ def button_delete_pdf_user_click(self): 'pdf_source', 'pdf_target', 'template_ods', - 'template_json', + 'template_csv', ) for v in var: self._set(v, self.users_pdf[sel][v]) @@ -1219,38 +1072,53 @@ def button_generate_pdf_click(self): if not ok: return - files = data['files'] - del data['files'] - total = len(files) + if data['pdf_user']['template_csv']: + self._csv_to_pdf(data) + return - libo = LibO() - if libo.SM is None: - del libo - msg = 'No fue posible iniciar LibreOffice, asegurate de que ' \ - 'este correctamente instalado e intentalo de nuevo' - self.util.msgbox(msg) - return False, data + self._ods_to_pdf(data) + return - pb = self._get_object('progressbar') - pb['maximum'] = total - pb.start() + def _ods_to_pdf(self, data): + files = data['files'] + total = len(files) + self.pb['maximum'] = total + self.pb.start() j = 0 for i, f in enumerate(files): - pb['value'] = i + 1 + self.pb['value'] = i + 1 msg = 'Archivo {} de {}'.format(i + 1, total) self._set('msg_user', msg) self.parent.update_idletasks() - if self._make_pdf(f, libo, data['pdf_user']): + if self._make_pdf(f, data['libo'], data['pdf_user']): j += 1 - del libo - pb['value'] = 0 - pb.stop() + del data['libo'] + self.pb['value'] = 0 + self.pb.stop() msg = 'Archivos XML encontrados: {}\n' msg += 'Archivos PDF generados: {}' msg = msg.format(total, j) self.util.msgbox(msg) return + def _csv_to_pdf(self, data): + files = data['files'] + user = data['pdf_user'] + total = len(files) + self.pb['maximum'] = total + self.pb.start() + for i, f in enumerate(files): + args = (self.g.SCRIPTS['CFDI2PDF'], '-a', f, '-o', + user['pdf_source'], '-d', user['pdf_target'], '-p', user['template_csv']) + self.util.call(args) + self.pb['value'] = i + 1 + msg = 'Archivo {} de {}'.format(i + 1, total) + self._set('msg_user', msg) + self.parent.update_idletasks() + self.pb['value'] = 0 + self.pb.stop() + return + def _make_pdf(self, path, libo, data): xml = self.util.parse(path) if xml is None: @@ -1258,8 +1126,8 @@ def _make_pdf(self, path, libo, data): source = data['pdf_source'] target = data['pdf_target'] template = data['template_ods'] - if data['template_json']: - template = data['template_json'] + if data['template_csv']: + template = data['template_csv'] info = self.util.get_path_info(path) path_pdf = self.util.join(target, info[0][len(source)+1:]) self.util.makedirs(path_pdf) @@ -1278,36 +1146,46 @@ def _make_pdf(self, path, libo, data): return False def _validate_pdf(self): + data = {} pdf_user = self._get('current_user_pdf') if not pdf_user: msg = 'Selecciona un usuario primero' self.util.msgbox(msg) return False, {} - data = self.users_pdf[pdf_user] - if not self.util.validate_dir(data['pdf_source'], 'r'): + pdf_user = self.users_pdf[pdf_user] + if not self.util.validate_dir(pdf_user['pdf_source'], 'r'): msg = 'No tienes derechos de lectura en el directorio origen' self.util.msgbox(msg) return False, {} - files = self.util.get_files(data['pdf_source']) + files = self.util.get_files(pdf_user['pdf_source']) if not files: msg = 'No se encontraron archivos XML en el directorio origen' self.util.msgbox(msg) return False, {} - if not self.util.validate_dir(data['pdf_target'], 'w'): + if not self.util.validate_dir(pdf_user['pdf_target'], 'w'): msg = 'No tienes derechos de escritura en el directorio destino' self.util.msgbox(msg) return False, {} - template_ods = data['template_ods'] - if data['template_ods']: - if not self.util.exists(data['template_ods']): + data['pdf_user'] = pdf_user + data['files'] = files + data['libo'] = None + if pdf_user['template_csv']: + msg = 'Se van a generar {} PDFs\n\n¿Estas seguro de continuar?' + if not self.util.question(msg.format(len(files))): + return False, {} + return True, data + + #~ template_ods = pdf_user['template_ods'] + if pdf_user['template_ods']: + if not self.util.exists(pdf_user['template_ods']): msg = 'No se encontró una plantilla en la ruta establecida' self.util.msgbox(msg) return False, {} - ext = self.util.get_path_info(data['template_ods'], 3) + ext = self.util.get_path_info(pdf_user['template_ods'], 3) if ext != self.g.EXT_ODS: msg = 'La plantilla no es un archivo ODS de Calc' self.util.msgbox(msg) @@ -1318,25 +1196,12 @@ def _validate_pdf(self): msg = 'No fue posible iniciar LibreOffice, asegurate de que ' \ 'este correctamente instalado e intentalo de nuevo' self.util.msgbox(msg) - return False, data - doc = libo.doc_open(data['template_ods']) - if doc is None: - del libo - msg = 'No fue posible abrir la plantilla, asegurate de que ' \ - 'la ruta sea correcta' - self.util.msgbox(msg) - return False, data - doc.dispose() - del libo - else: # ToDO CSV - return False, {} + return False, {} msg = 'Se van a generar {} PDFs\n\n¿Estas seguro de continuar?' if not self.util.question(msg.format(len(files))): return False, {} - - data['pdf_user'] = data - data['files'] = files + data['libo'] = libo return True, data def button_select_folder_report_source_click(self): diff --git a/admin-cfdi.pyw b/admin-cfdi.pyw new file mode 100755 index 0000000..e948acf --- /dev/null +++ b/admin-cfdi.pyw @@ -0,0 +1,6 @@ +import sys +import os.path + + +ac_path = os.path.join(sys.path[0], "admin-cfdi") +exec(open(ac_path, encoding='utf-8').read()) diff --git a/admincfdi.log b/admincfdi.log deleted file mode 100644 index e69de29..0000000 diff --git a/__init__.py b/admincfdi/__init__.py similarity index 100% rename from __init__.py rename to admincfdi/__init__.py diff --git a/bin/cadena3.2.xslt b/admincfdi/bin/cadena3.2.xslt similarity index 100% rename from bin/cadena3.2.xslt rename to admincfdi/bin/cadena3.2.xslt diff --git a/bin/cfdi_3.2.xsd b/admincfdi/bin/cfdi_3.2.xsd similarity index 100% rename from bin/cfdi_3.2.xsd rename to admincfdi/bin/cfdi_3.2.xsd diff --git a/bin/cfdi_3.2.xslt b/admincfdi/bin/cfdi_3.2.xslt similarity index 84% rename from bin/cfdi_3.2.xslt rename to admincfdi/bin/cfdi_3.2.xslt index 5c6a0d4..89c26d1 100644 --- a/bin/cfdi_3.2.xslt +++ b/admincfdi/bin/cfdi_3.2.xslt @@ -406,6 +406,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ||| diff --git a/bin/get_certificado.xslt b/admincfdi/bin/get_certificado.xslt similarity index 93% rename from bin/get_certificado.xslt rename to admincfdi/bin/get_certificado.xslt index e003d1b..2eb2f8a 100644 --- a/bin/get_certificado.xslt +++ b/admincfdi/bin/get_certificado.xslt @@ -51,6 +51,10 @@ + + + + diff --git a/bin/get_sello.xslt b/admincfdi/bin/get_sello.xslt similarity index 82% rename from bin/get_sello.xslt rename to admincfdi/bin/get_sello.xslt index 65d0165..af45a03 100644 --- a/bin/get_sello.xslt +++ b/admincfdi/bin/get_sello.xslt @@ -4,7 +4,7 @@ - + diff --git a/bin/get_sello_sat.xslt b/admincfdi/bin/get_sello_sat.xslt similarity index 100% rename from bin/get_sello_sat.xslt rename to admincfdi/bin/get_sello_sat.xslt diff --git a/bin/iconv.dll b/admincfdi/bin/iconv.dll similarity index 100% rename from bin/iconv.dll rename to admincfdi/bin/iconv.dll diff --git a/bin/libeay32.dll b/admincfdi/bin/libeay32.dll similarity index 100% rename from bin/libeay32.dll rename to admincfdi/bin/libeay32.dll diff --git a/bin/libexslt.dll b/admincfdi/bin/libexslt.dll similarity index 100% rename from bin/libexslt.dll rename to admincfdi/bin/libexslt.dll diff --git a/bin/libxml2.dll b/admincfdi/bin/libxml2.dll similarity index 100% rename from bin/libxml2.dll rename to admincfdi/bin/libxml2.dll diff --git a/bin/libxslt.dll b/admincfdi/bin/libxslt.dll similarity index 100% rename from bin/libxslt.dll rename to admincfdi/bin/libxslt.dll diff --git a/bin/openssl.exe b/admincfdi/bin/openssl.exe similarity index 100% rename from bin/openssl.exe rename to admincfdi/bin/openssl.exe diff --git a/bin/ssleay32.dll b/admincfdi/bin/ssleay32.dll similarity index 100% rename from bin/ssleay32.dll rename to admincfdi/bin/ssleay32.dll diff --git a/bin/timbre.xslt b/admincfdi/bin/timbre.xslt similarity index 100% rename from bin/timbre.xslt rename to admincfdi/bin/timbre.xslt diff --git a/bin/timbre_1.0.xslt b/admincfdi/bin/timbre_1.0.xslt similarity index 100% rename from bin/timbre_1.0.xslt rename to admincfdi/bin/timbre_1.0.xslt diff --git a/bin/xsltproc.exe b/admincfdi/bin/xsltproc.exe similarity index 100% rename from bin/xsltproc.exe rename to admincfdi/bin/xsltproc.exe diff --git a/bin/zlib1.dll b/admincfdi/bin/zlib1.dll similarity index 100% rename from bin/zlib1.dll rename to admincfdi/bin/zlib1.dll diff --git a/cer_pac/00001000000103834451.cer b/admincfdi/cer_pac/00001000000103834451.cer similarity index 100% rename from cer_pac/00001000000103834451.cer rename to admincfdi/cer_pac/00001000000103834451.cer diff --git a/cer_pac/00001000000104731997.cer b/admincfdi/cer_pac/00001000000104731997.cer similarity index 100% rename from cer_pac/00001000000104731997.cer rename to admincfdi/cer_pac/00001000000104731997.cer diff --git a/cer_pac/00001000000104750010.cer b/admincfdi/cer_pac/00001000000104750010.cer similarity index 100% rename from cer_pac/00001000000104750010.cer rename to admincfdi/cer_pac/00001000000104750010.cer diff --git a/cer_pac/00001000000104871381.cer b/admincfdi/cer_pac/00001000000104871381.cer similarity index 100% rename from cer_pac/00001000000104871381.cer rename to admincfdi/cer_pac/00001000000104871381.cer diff --git a/cer_pac/00001000000104888042.cer b/admincfdi/cer_pac/00001000000104888042.cer similarity index 100% rename from cer_pac/00001000000104888042.cer rename to admincfdi/cer_pac/00001000000104888042.cer diff --git a/cer_pac/00001000000200005634.cer b/admincfdi/cer_pac/00001000000200005634.cer similarity index 100% rename from cer_pac/00001000000200005634.cer rename to admincfdi/cer_pac/00001000000200005634.cer diff --git a/cer_pac/00001000000200011997.cer b/admincfdi/cer_pac/00001000000200011997.cer similarity index 100% rename from cer_pac/00001000000200011997.cer rename to admincfdi/cer_pac/00001000000200011997.cer diff --git a/cer_pac/00001000000200365214.cer b/admincfdi/cer_pac/00001000000200365214.cer similarity index 100% rename from cer_pac/00001000000200365214.cer rename to admincfdi/cer_pac/00001000000200365214.cer diff --git a/cer_pac/00001000000200795916.cer b/admincfdi/cer_pac/00001000000200795916.cer similarity index 100% rename from cer_pac/00001000000200795916.cer rename to admincfdi/cer_pac/00001000000200795916.cer diff --git a/cer_pac/00001000000201345662.cer b/admincfdi/cer_pac/00001000000201345662.cer similarity index 100% rename from cer_pac/00001000000201345662.cer rename to admincfdi/cer_pac/00001000000201345662.cer diff --git a/cer_pac/00001000000201345708.cer b/admincfdi/cer_pac/00001000000201345708.cer similarity index 100% rename from cer_pac/00001000000201345708.cer rename to admincfdi/cer_pac/00001000000201345708.cer diff --git a/cer_pac/00001000000201395217.cer b/admincfdi/cer_pac/00001000000201395217.cer similarity index 100% rename from cer_pac/00001000000201395217.cer rename to admincfdi/cer_pac/00001000000201395217.cer diff --git a/cer_pac/00001000000201455572.cer b/admincfdi/cer_pac/00001000000201455572.cer similarity index 100% rename from cer_pac/00001000000201455572.cer rename to admincfdi/cer_pac/00001000000201455572.cer diff --git a/cer_pac/00001000000201614141.cer b/admincfdi/cer_pac/00001000000201614141.cer similarity index 100% rename from cer_pac/00001000000201614141.cer rename to admincfdi/cer_pac/00001000000201614141.cer diff --git a/cer_pac/00001000000201629292.cer b/admincfdi/cer_pac/00001000000201629292.cer similarity index 100% rename from cer_pac/00001000000201629292.cer rename to admincfdi/cer_pac/00001000000201629292.cer diff --git a/cer_pac/00001000000201748120.cer b/admincfdi/cer_pac/00001000000201748120.cer similarity index 100% rename from cer_pac/00001000000201748120.cer rename to admincfdi/cer_pac/00001000000201748120.cer diff --git a/cer_pac/00001000000202241710.cer b/admincfdi/cer_pac/00001000000202241710.cer similarity index 100% rename from cer_pac/00001000000202241710.cer rename to admincfdi/cer_pac/00001000000202241710.cer diff --git a/cer_pac/00001000000202453260.cer b/admincfdi/cer_pac/00001000000202453260.cer similarity index 100% rename from cer_pac/00001000000202453260.cer rename to admincfdi/cer_pac/00001000000202453260.cer diff --git a/cer_pac/00001000000202638162.cer b/admincfdi/cer_pac/00001000000202638162.cer similarity index 100% rename from cer_pac/00001000000202638162.cer rename to admincfdi/cer_pac/00001000000202638162.cer diff --git a/cer_pac/00001000000202639096.cer b/admincfdi/cer_pac/00001000000202639096.cer similarity index 100% rename from cer_pac/00001000000202639096.cer rename to admincfdi/cer_pac/00001000000202639096.cer diff --git a/cer_pac/00001000000202639521.cer b/admincfdi/cer_pac/00001000000202639521.cer similarity index 100% rename from cer_pac/00001000000202639521.cer rename to admincfdi/cer_pac/00001000000202639521.cer diff --git a/cer_pac/00001000000202693892.cer b/admincfdi/cer_pac/00001000000202693892.cer similarity index 100% rename from cer_pac/00001000000202693892.cer rename to admincfdi/cer_pac/00001000000202693892.cer diff --git a/cer_pac/00001000000202695775.cer b/admincfdi/cer_pac/00001000000202695775.cer similarity index 100% rename from cer_pac/00001000000202695775.cer rename to admincfdi/cer_pac/00001000000202695775.cer diff --git a/cer_pac/00001000000202700691.cer b/admincfdi/cer_pac/00001000000202700691.cer similarity index 100% rename from cer_pac/00001000000202700691.cer rename to admincfdi/cer_pac/00001000000202700691.cer diff --git a/cer_pac/00001000000202771790.cer b/admincfdi/cer_pac/00001000000202771790.cer similarity index 100% rename from cer_pac/00001000000202771790.cer rename to admincfdi/cer_pac/00001000000202771790.cer diff --git a/cer_pac/00001000000202772539.cer b/admincfdi/cer_pac/00001000000202772539.cer similarity index 100% rename from cer_pac/00001000000202772539.cer rename to admincfdi/cer_pac/00001000000202772539.cer diff --git a/cer_pac/00001000000202809550.cer b/admincfdi/cer_pac/00001000000202809550.cer similarity index 100% rename from cer_pac/00001000000202809550.cer rename to admincfdi/cer_pac/00001000000202809550.cer diff --git a/cer_pac/00001000000202864285.cer b/admincfdi/cer_pac/00001000000202864285.cer similarity index 100% rename from cer_pac/00001000000202864285.cer rename to admincfdi/cer_pac/00001000000202864285.cer diff --git a/cer_pac/00001000000202864530.cer b/admincfdi/cer_pac/00001000000202864530.cer similarity index 100% rename from cer_pac/00001000000202864530.cer rename to admincfdi/cer_pac/00001000000202864530.cer diff --git a/cer_pac/00001000000202864883.cer b/admincfdi/cer_pac/00001000000202864883.cer similarity index 100% rename from cer_pac/00001000000202864883.cer rename to admincfdi/cer_pac/00001000000202864883.cer diff --git a/cer_pac/00001000000202865018.cer b/admincfdi/cer_pac/00001000000202865018.cer similarity index 100% rename from cer_pac/00001000000202865018.cer rename to admincfdi/cer_pac/00001000000202865018.cer diff --git a/cer_pac/00001000000202905407.cer b/admincfdi/cer_pac/00001000000202905407.cer similarity index 100% rename from cer_pac/00001000000202905407.cer rename to admincfdi/cer_pac/00001000000202905407.cer diff --git a/cer_pac/00001000000203015571.cer b/admincfdi/cer_pac/00001000000203015571.cer similarity index 100% rename from cer_pac/00001000000203015571.cer rename to admincfdi/cer_pac/00001000000203015571.cer diff --git a/cer_pac/00001000000203051706.cer b/admincfdi/cer_pac/00001000000203051706.cer similarity index 100% rename from cer_pac/00001000000203051706.cer rename to admincfdi/cer_pac/00001000000203051706.cer diff --git a/cer_pac/00001000000203082087.cer b/admincfdi/cer_pac/00001000000203082087.cer similarity index 100% rename from cer_pac/00001000000203082087.cer rename to admincfdi/cer_pac/00001000000203082087.cer diff --git a/cer_pac/00001000000203092957.cer b/admincfdi/cer_pac/00001000000203092957.cer similarity index 100% rename from cer_pac/00001000000203092957.cer rename to admincfdi/cer_pac/00001000000203092957.cer diff --git a/cer_pac/00001000000203093174.cer b/admincfdi/cer_pac/00001000000203093174.cer similarity index 100% rename from cer_pac/00001000000203093174.cer rename to admincfdi/cer_pac/00001000000203093174.cer diff --git a/cer_pac/00001000000203159220.cer b/admincfdi/cer_pac/00001000000203159220.cer similarity index 100% rename from cer_pac/00001000000203159220.cer rename to admincfdi/cer_pac/00001000000203159220.cer diff --git a/cer_pac/00001000000203159375.cer b/admincfdi/cer_pac/00001000000203159375.cer similarity index 100% rename from cer_pac/00001000000203159375.cer rename to admincfdi/cer_pac/00001000000203159375.cer diff --git a/cer_pac/00001000000203191015.cer b/admincfdi/cer_pac/00001000000203191015.cer similarity index 100% rename from cer_pac/00001000000203191015.cer rename to admincfdi/cer_pac/00001000000203191015.cer diff --git a/cer_pac/00001000000203220518.cer b/admincfdi/cer_pac/00001000000203220518.cer similarity index 100% rename from cer_pac/00001000000203220518.cer rename to admincfdi/cer_pac/00001000000203220518.cer diff --git a/cer_pac/00001000000203220546.cer b/admincfdi/cer_pac/00001000000203220546.cer similarity index 100% rename from cer_pac/00001000000203220546.cer rename to admincfdi/cer_pac/00001000000203220546.cer diff --git a/cer_pac/00001000000203253077.cer b/admincfdi/cer_pac/00001000000203253077.cer similarity index 100% rename from cer_pac/00001000000203253077.cer rename to admincfdi/cer_pac/00001000000203253077.cer diff --git a/cer_pac/00001000000203285726.cer b/admincfdi/cer_pac/00001000000203285726.cer similarity index 100% rename from cer_pac/00001000000203285726.cer rename to admincfdi/cer_pac/00001000000203285726.cer diff --git a/cer_pac/00001000000203285735.cer b/admincfdi/cer_pac/00001000000203285735.cer similarity index 100% rename from cer_pac/00001000000203285735.cer rename to admincfdi/cer_pac/00001000000203285735.cer diff --git a/cer_pac/00001000000203292609.cer b/admincfdi/cer_pac/00001000000203292609.cer similarity index 100% rename from cer_pac/00001000000203292609.cer rename to admincfdi/cer_pac/00001000000203292609.cer diff --git a/cer_pac/00001000000203312933.cer b/admincfdi/cer_pac/00001000000203312933.cer similarity index 100% rename from cer_pac/00001000000203312933.cer rename to admincfdi/cer_pac/00001000000203312933.cer diff --git a/cer_pac/00001000000203352843.cer b/admincfdi/cer_pac/00001000000203352843.cer similarity index 100% rename from cer_pac/00001000000203352843.cer rename to admincfdi/cer_pac/00001000000203352843.cer diff --git a/cer_pac/00001000000203392777.cer b/admincfdi/cer_pac/00001000000203392777.cer similarity index 100% rename from cer_pac/00001000000203392777.cer rename to admincfdi/cer_pac/00001000000203392777.cer diff --git a/cer_pac/00001000000203430011.cer b/admincfdi/cer_pac/00001000000203430011.cer similarity index 100% rename from cer_pac/00001000000203430011.cer rename to admincfdi/cer_pac/00001000000203430011.cer diff --git a/cer_pac/00001000000203495276.cer b/admincfdi/cer_pac/00001000000203495276.cer similarity index 100% rename from cer_pac/00001000000203495276.cer rename to admincfdi/cer_pac/00001000000203495276.cer diff --git a/cer_pac/00001000000203495475.cer b/admincfdi/cer_pac/00001000000203495475.cer similarity index 100% rename from cer_pac/00001000000203495475.cer rename to admincfdi/cer_pac/00001000000203495475.cer diff --git a/cer_pac/00001000000203631919.cer b/admincfdi/cer_pac/00001000000203631919.cer similarity index 100% rename from cer_pac/00001000000203631919.cer rename to admincfdi/cer_pac/00001000000203631919.cer diff --git a/cer_pac/00001000000300091673.cer b/admincfdi/cer_pac/00001000000300091673.cer similarity index 100% rename from cer_pac/00001000000300091673.cer rename to admincfdi/cer_pac/00001000000300091673.cer diff --git a/cer_pac/00001000000300171291.cer b/admincfdi/cer_pac/00001000000300171291.cer similarity index 100% rename from cer_pac/00001000000300171291.cer rename to admincfdi/cer_pac/00001000000300171291.cer diff --git a/cer_pac/00001000000300171326.cer b/admincfdi/cer_pac/00001000000300171326.cer similarity index 100% rename from cer_pac/00001000000300171326.cer rename to admincfdi/cer_pac/00001000000300171326.cer diff --git a/cer_pac/00001000000300209963.cer b/admincfdi/cer_pac/00001000000300209963.cer similarity index 100% rename from cer_pac/00001000000300209963.cer rename to admincfdi/cer_pac/00001000000300209963.cer diff --git a/cer_pac/00001000000300250292.cer b/admincfdi/cer_pac/00001000000300250292.cer similarity index 100% rename from cer_pac/00001000000300250292.cer rename to admincfdi/cer_pac/00001000000300250292.cer diff --git a/cer_pac/00001000000300392385.cer b/admincfdi/cer_pac/00001000000300392385.cer similarity index 100% rename from cer_pac/00001000000300392385.cer rename to admincfdi/cer_pac/00001000000300392385.cer diff --git a/cer_pac/00001000000300407877.cer b/admincfdi/cer_pac/00001000000300407877.cer similarity index 100% rename from cer_pac/00001000000300407877.cer rename to admincfdi/cer_pac/00001000000300407877.cer diff --git a/cer_pac/00001000000300439968.cer b/admincfdi/cer_pac/00001000000300439968.cer similarity index 100% rename from cer_pac/00001000000300439968.cer rename to admincfdi/cer_pac/00001000000300439968.cer diff --git a/cer_pac/00001000000300494998.cer b/admincfdi/cer_pac/00001000000300494998.cer similarity index 100% rename from cer_pac/00001000000300494998.cer rename to admincfdi/cer_pac/00001000000300494998.cer diff --git a/cer_pac/00001000000300627194.cer b/admincfdi/cer_pac/00001000000300627194.cer similarity index 100% rename from cer_pac/00001000000300627194.cer rename to admincfdi/cer_pac/00001000000300627194.cer diff --git a/cer_pac/00001000000300716418.cer b/admincfdi/cer_pac/00001000000300716418.cer similarity index 100% rename from cer_pac/00001000000300716418.cer rename to admincfdi/cer_pac/00001000000300716418.cer diff --git a/cer_pac/00001000000300716428.cer b/admincfdi/cer_pac/00001000000300716428.cer similarity index 100% rename from cer_pac/00001000000300716428.cer rename to admincfdi/cer_pac/00001000000300716428.cer diff --git a/cer_pac/00001000000300774022.cer b/admincfdi/cer_pac/00001000000300774022.cer similarity index 100% rename from cer_pac/00001000000300774022.cer rename to admincfdi/cer_pac/00001000000300774022.cer diff --git a/cer_pac/00001000000300915978.cer b/admincfdi/cer_pac/00001000000300915978.cer similarity index 100% rename from cer_pac/00001000000300915978.cer rename to admincfdi/cer_pac/00001000000300915978.cer diff --git a/cer_pac/00001000000300969660.cer b/admincfdi/cer_pac/00001000000300969660.cer similarity index 100% rename from cer_pac/00001000000300969660.cer rename to admincfdi/cer_pac/00001000000300969660.cer diff --git a/cer_pac/00001000000301021501.cer b/admincfdi/cer_pac/00001000000301021501.cer similarity index 100% rename from cer_pac/00001000000301021501.cer rename to admincfdi/cer_pac/00001000000301021501.cer diff --git a/cer_pac/00001000000301032322.cer b/admincfdi/cer_pac/00001000000301032322.cer similarity index 100% rename from cer_pac/00001000000301032322.cer rename to admincfdi/cer_pac/00001000000301032322.cer diff --git a/cer_pac/00001000000301062628.cer b/admincfdi/cer_pac/00001000000301062628.cer similarity index 100% rename from cer_pac/00001000000301062628.cer rename to admincfdi/cer_pac/00001000000301062628.cer diff --git a/cer_pac/00001000000301083052.cer b/admincfdi/cer_pac/00001000000301083052.cer similarity index 100% rename from cer_pac/00001000000301083052.cer rename to admincfdi/cer_pac/00001000000301083052.cer diff --git a/cer_pac/00001000000301100488.cer b/admincfdi/cer_pac/00001000000301100488.cer similarity index 100% rename from cer_pac/00001000000301100488.cer rename to admincfdi/cer_pac/00001000000301100488.cer diff --git a/cer_pac/00001000000301160463.cer b/admincfdi/cer_pac/00001000000301160463.cer similarity index 100% rename from cer_pac/00001000000301160463.cer rename to admincfdi/cer_pac/00001000000301160463.cer diff --git a/admincfdi/cer_pac/00001000000301205071.cer b/admincfdi/cer_pac/00001000000301205071.cer new file mode 100644 index 0000000..b97bfe6 Binary files /dev/null and b/admincfdi/cer_pac/00001000000301205071.cer differ diff --git a/admincfdi/cer_pac/00001000000301251152.cer b/admincfdi/cer_pac/00001000000301251152.cer new file mode 100644 index 0000000..d094d92 Binary files /dev/null and b/admincfdi/cer_pac/00001000000301251152.cer differ diff --git a/cer_pac/00001000000301280594.cer b/admincfdi/cer_pac/00001000000301280594.cer similarity index 100% rename from cer_pac/00001000000301280594.cer rename to admincfdi/cer_pac/00001000000301280594.cer diff --git a/cer_pac/00001000000301567711.cer b/admincfdi/cer_pac/00001000000301567711.cer similarity index 100% rename from cer_pac/00001000000301567711.cer rename to admincfdi/cer_pac/00001000000301567711.cer diff --git a/cer_pac/00001000000301634628.cer b/admincfdi/cer_pac/00001000000301634628.cer similarity index 100% rename from cer_pac/00001000000301634628.cer rename to admincfdi/cer_pac/00001000000301634628.cer diff --git a/cer_pac/00001000000301751173.cer b/admincfdi/cer_pac/00001000000301751173.cer similarity index 100% rename from cer_pac/00001000000301751173.cer rename to admincfdi/cer_pac/00001000000301751173.cer diff --git a/cer_pac/00001000000301914249.cer b/admincfdi/cer_pac/00001000000301914249.cer similarity index 100% rename from cer_pac/00001000000301914249.cer rename to admincfdi/cer_pac/00001000000301914249.cer diff --git a/cer_pac/00001000000301927035.cer b/admincfdi/cer_pac/00001000000301927035.cer similarity index 100% rename from cer_pac/00001000000301927035.cer rename to admincfdi/cer_pac/00001000000301927035.cer diff --git a/cer_pac/00001000000301949314.cer b/admincfdi/cer_pac/00001000000301949314.cer similarity index 100% rename from cer_pac/00001000000301949314.cer rename to admincfdi/cer_pac/00001000000301949314.cer diff --git a/cer_pac/00001000000304339685.cer b/admincfdi/cer_pac/00001000000304339685.cer similarity index 100% rename from cer_pac/00001000000304339685.cer rename to admincfdi/cer_pac/00001000000304339685.cer diff --git a/cer_pac/00001000000304691381.cer b/admincfdi/cer_pac/00001000000304691381.cer similarity index 100% rename from cer_pac/00001000000304691381.cer rename to admincfdi/cer_pac/00001000000304691381.cer diff --git a/cer_pac/20001000000100005868.cer b/admincfdi/cer_pac/20001000000100005868.cer similarity index 100% rename from cer_pac/20001000000100005868.cer rename to admincfdi/cer_pac/20001000000100005868.cer diff --git a/img/calc.gif b/admincfdi/img/calc.gif similarity index 100% rename from img/calc.gif rename to admincfdi/img/calc.gif diff --git a/img/calc.png b/admincfdi/img/calc.png similarity index 100% rename from img/calc.png rename to admincfdi/img/calc.png diff --git a/img/csv.gif b/admincfdi/img/csv.gif similarity index 100% rename from img/csv.gif rename to admincfdi/img/csv.gif diff --git a/img/csv.png b/admincfdi/img/csv.png similarity index 100% rename from img/csv.png rename to admincfdi/img/csv.png diff --git a/img/delete.gif b/admincfdi/img/delete.gif similarity index 100% rename from img/delete.gif rename to admincfdi/img/delete.gif diff --git a/img/delete.png b/admincfdi/img/delete.png similarity index 100% rename from img/delete.png rename to admincfdi/img/delete.png diff --git a/img/down.gif b/admincfdi/img/down.gif similarity index 100% rename from img/down.gif rename to admincfdi/img/down.gif diff --git a/img/down.png b/admincfdi/img/down.png similarity index 100% rename from img/down.png rename to admincfdi/img/down.png diff --git a/img/exit.gif b/admincfdi/img/exit.gif similarity index 100% rename from img/exit.gif rename to admincfdi/img/exit.gif diff --git a/img/exit.png b/admincfdi/img/exit.png similarity index 100% rename from img/exit.png rename to admincfdi/img/exit.png diff --git a/img/favicon.gif b/admincfdi/img/favicon.gif similarity index 100% rename from img/favicon.gif rename to admincfdi/img/favicon.gif diff --git a/img/favicon.png b/admincfdi/img/favicon.png similarity index 100% rename from img/favicon.png rename to admincfdi/img/favicon.png diff --git a/img/folder.gif b/admincfdi/img/folder.gif similarity index 100% rename from img/folder.gif rename to admincfdi/img/folder.gif diff --git a/img/folder.png b/admincfdi/img/folder.png similarity index 100% rename from img/folder.png rename to admincfdi/img/folder.png diff --git a/img/pdf.gif b/admincfdi/img/pdf.gif similarity index 100% rename from img/pdf.gif rename to admincfdi/img/pdf.gif diff --git a/img/pdf.png b/admincfdi/img/pdf.png similarity index 100% rename from img/pdf.png rename to admincfdi/img/pdf.png diff --git a/img/report.gif b/admincfdi/img/report.gif similarity index 100% rename from img/report.gif rename to admincfdi/img/report.gif diff --git a/img/report.png b/admincfdi/img/report.png similarity index 100% rename from img/report.png rename to admincfdi/img/report.png diff --git a/img/save.gif b/admincfdi/img/save.gif similarity index 100% rename from img/save.gif rename to admincfdi/img/save.gif diff --git a/img/save.png b/admincfdi/img/save.png similarity index 100% rename from img/save.png rename to admincfdi/img/save.png diff --git a/img/xml.gif b/admincfdi/img/xml.gif similarity index 100% rename from img/xml.gif rename to admincfdi/img/xml.gif diff --git a/img/xml.png b/admincfdi/img/xml.png similarity index 100% rename from img/xml.png rename to admincfdi/img/xml.png diff --git a/pyutil.py b/admincfdi/pyutil.py similarity index 53% rename from pyutil.py rename to admincfdi/pyutil.py index bbeceed..841794f 100644 --- a/pyutil.py +++ b/admincfdi/pyutil.py @@ -15,7 +15,6 @@ import sys import re import csv -import glob import json import ftplib import time @@ -35,7 +34,16 @@ from tkinter.filedialog import askdirectory from tkinter.filedialog import askopenfilename from tkinter import messagebox -from pysimplesoap.client import SoapClient, SoapFault +from requests import Request, Session +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException +from fpdf import FPDF +from admincfdi.values import Global + + try: from subprocess import DEVNULL except ImportError: @@ -45,40 +53,63 @@ WIN = 'win32' MAC = 'darwin' LINUX = 'linux' -APP_LIBO = True +LIBO = True if sys.platform == WIN: try: from win32com.client import Dispatch - except ImportError as e: - APP_LIBO = False + except ImportError: + LIBO = False elif sys.platform == LINUX: - import uno - from com.sun.star.beans import PropertyValue - from com.sun.star.beans.PropertyState import DIRECT_VALUE - from com.sun.star.awt import Size + try: + import uno + from com.sun.star.beans import PropertyValue + from com.sun.star.beans.PropertyState import DIRECT_VALUE + from com.sun.star.awt import Size + except ImportError: + LIBO = False class SAT(object): _webservice = 'https://consultaqr.facturaelectronica.sat.gob.mx/' \ - 'consultacfdiservice.svc?wsdl' - - def __init__(self): + 'consultacfdiservice.svc' + _soap = """ + + + + + + ?re={emisor_rfc}&rr={receptor_rfc}&tt={total}&id={uuid} + + + + """ + + def __init__(self, log): + self.log = log self.error = '' self.msg = '' def get_estatus(self, data): + data = self._soap.format(**data).encode('utf-8') + headers = { + 'SOAPAction': '"http://tempuri.org/IConsultaCFDIService/Consulta"', + 'Content-length': len(data), + 'Content-type': 'text/xml; charset="UTF-8"' + } + s = Session() + req = Request('POST', self._webservice, data=data, headers=headers) + prepped = req.prepare() try: - args = '?re={emisor_rfc}&rr={receptor_rfc}&tt={total}&id={uuid}' - client = SoapClient(wsdl = self._webservice) - fac = args.format(**data) - res = client.Consulta(fac) - if 'ConsultaResult' in res: - self.msg = res['ConsultaResult']['Estado'] - return True - return False - except SoapFault as sf: - self.error = sf.faultstring + response = s.send(prepped, timeout=5) + tree = ET.fromstring(response.text) + self.msg = tree[0][0][0][1].text + return True + except Exception as e: + self.log.error(str(e)) return False @@ -311,6 +342,9 @@ class Util(object): def __init__(self): self.OS = sys.platform + def call(self, *arg): + return subprocess.call(*arg) + def get_folder(self, parent): return askdirectory(parent=parent) @@ -334,6 +368,10 @@ def get_path_info(self, path, index=-1): else: return data[index] + def replace_extension(self, path, new_ext): + path, _, name, _ = self.get_path_info(path) + return self.join(path, name + new_ext) + def path_join(self, *paths): return os.path.join(*paths) @@ -343,11 +381,12 @@ def path_config(self, path): target = path.replace('/', '\\') return target - def get_files(self, path, ext='*.xml'): - files = [] - for folder,_,_ in os.walk(path): - files.extend(glob.glob(os.path.join(folder, ext.lower()))) - return tuple(files) + def get_files(self, path, ext='xml'): + xmls = [] + for folder, _, files in os.walk(path): + pattern = re.compile('\.{}'.format(ext), re.IGNORECASE) + xmls += [os.path.join(folder,f) for f in files if pattern.search(f)] + return tuple(xmls) def join(self, *paths): return os.path.join(*paths) @@ -399,6 +438,22 @@ def sleep(self, sec=1): time.sleep(sec) return + def lee_credenciales(self, ruta_archivo): + '''Lee un renglón de ruta_archivo, regresa status, rfc, pwd + + status es 'Ok', o un mensaje de error''' + + try: + f = open(ruta_archivo) + except FileNotFoundError: + return 'Archivo no encontrado: ' + ruta_archivo, '', '' + row = f.readline().strip() + fields = row.split() + if not len(fields) == 2: + return 'No contiene dos campos: ' + ruta_archivo, '', '' + rfc, pwd = fields + return 'Ok', rfc, pwd + def load_config(self, path): data = {} try: @@ -422,10 +477,13 @@ def save_config(self, path, key, value): def now(self): return datetime.now() - def get_dates(self, year, month): - days = calendar.monthrange(year, month)[1] - d1 = '01/{:02d}/{}'.format(month, year) - d2 = '{}/{:02d}/{}'.format(days, month, year) + def get_dates(self, year, month, day=0): + if day: + d1 = d2 = '{:02d}/{:02d}/{}'.format(day, month, year) + else: + days = calendar.monthrange(year, month)[1] + d1 = '01/{:02d}/{}'.format(month, year) + d2 = '{}/{:02d}/{}'.format(days, month, year) return d1, d2 def get_days(self, year, month): @@ -509,9 +567,7 @@ def get_name(self, path, PRE, format1='', format2=''): if not 'serie' in data: data['serie'] = '' data['fecha'] = data['fecha'].partition('T')[0] - if 'folio' in data: - data['folio'] = int(data['folio']) - else: + if not 'folio' in data: data['folio'] = 0 node = xml.find('{}Emisor'.format(pre)) data['emisor_rfc'] = node.attrib['rfc'] @@ -671,7 +727,7 @@ def get_info_report(self, path, options, g): 'total': data['total'], 'uuid': data['UUID'] } - sat = SAT() + sat = SAT(g.LOG) if sat.get_estatus(data_sat): info.append(sat.msg) else: @@ -1653,3 +1709,1124 @@ def _format_date(self, date_string): return date.strftime('{}, %d de {} de %Y'.format( d[date.weekday()], m[date.month])) +class visibility_of_either(object): + """ An expectation for checking that one of two elements, + located by locator 1 and locator2, is visible. + Visibility means that the element is not only + displayed but also has a height and width that is + greater than 0. + returns the WebElement that is visible. + """ + def __init__(self, locator1, locator2): + self.locator1 = locator1 + self.locator2 = locator2 + + def __call__(self, driver): + element1 = driver.find_element(*self.locator1) + element2 = driver.find_element(*self.locator2) + return _element_if_visible(element1) or \ + _element_if_visible(element2) + +def _element_if_visible(element): + return element if element.is_displayed() else False + + +class DescargaSAT(object): + + def __init__(self, status_callback=print, + download_callback=print): + self.g = Global() + self.util = Util() + self.status = status_callback + self.progress = download_callback + self.browser = None + + def get_firefox_profile(self, carpeta_destino): + 'Devuelve un perfil para Firefox' + + # To prevent download dialog + profile = webdriver.FirefoxProfile() + profile.set_preference( + 'browser.download.folderList', 2) + profile.set_preference( + 'browser.download.manager.showWhenStarting', False) + profile.set_preference( + 'browser.helperApps.alwaysAsk.force', False) + profile.set_preference( + 'browser.helperApps.neverAsk.saveToDisk', + 'text/xml, application/octet-stream, application/xml') + profile.set_preference( + 'browser.download.dir', carpeta_destino) + # mrE - desactivar telemetry + profile.set_preference( + 'toolkit.telemetry.prompted', 2) + profile.set_preference( + 'toolkit.telemetry.rejected', True) + profile.set_preference( + 'toolkit.telemetry.enabled', False) + profile.set_preference( + 'datareporting.healthreport.service.enabled', False) + profile.set_preference( + 'datareporting.healthreport.uploadEnabled', False) + profile.set_preference( + 'datareporting.healthreport.service.firstRun', False) + profile.set_preference( + 'datareporting.healthreport.logging.consoleEnabled', False) + profile.set_preference( + 'datareporting.policy.dataSubmissionEnabled', False) + profile.set_preference( + 'datareporting.policy.dataSubmissionPolicyResponseType', 'accepted-info-bar-dismissed') + #profile.set_preference( + # 'datareporting.policy.dataSubmissionPolicyAccepted'; False) # este me marca error, why? + #oculta la gran flecha animada al descargar + profile.set_preference( + 'browser.download.animateNotifications', False) + return profile + + def connect(self, profile, rfc='', ciec=''): + 'Lanza navegador y hace login en el portal del SAT' + + self.status('Abriendo Firefox...') + browser = webdriver.Firefox(profile) + self.browser = browser + self.status('Conectando...') + browser.get(self.g.SAT['page_init']) + txt = browser.find_element_by_name(self.g.SAT['user']) + txt.send_keys(rfc) + txt = browser.find_element_by_name(self.g.SAT['password']) + txt.send_keys(ciec) + txt.submit() + wait = WebDriverWait(browser, 10) + try: + wait.until(EC.title_contains('NetIQ Access')) + self.status('Conectado') + return True + except TimeoutException: + self.status('No conectado') + return False + + def disconnect(self): + 'Cierra la sesión y el navegador' + + if self.browser: + try: + self.status('Desconectando...') + link = self.browser.find_element_by_partial_link_text('Cerrar Sesi') + link.click() + except: + pass + finally: + self.browser.close() + self.status('Desconectado...') + self.browser = None + + def search(self, + facturas_emitidas=False, + uuid='', + rfc_emisor='', + año=None, + mes=None, + día='00', + hora_inicial='0', + minuto_inicial='0', + segundo_inicial='0', + hora_final='23', + minuto_final='59', + segundo_final='59', + mes_completo_por_día=False): + 'Busca y regresa los resultados' + + if self.browser: + browser = self.browser + + page_query = self.g.SAT['page_receptor'] + if facturas_emitidas: + page_query = self.g.SAT['page_emisor'] + + browser.get(page_query) + wait = WebDriverWait(browser, 15) + wait.until(EC.title_contains('Buscar CFDI')) + self.status('Buscando...') + if uuid: + txt = browser.find_element_by_id(self.g.SAT['uuid']) + txt.click() + txt.send_keys(uuid) + else: + # Descargar por fecha + opt = browser.find_element_by_id(self.g.SAT['date']) + opt.click() + wait.until(EC.staleness_of(opt)) + if facturas_emitidas: + txt = wait.until(EC.element_to_be_clickable( + (By.ID, self.g.SAT['receptor']))) + else: + txt = wait.until(EC.element_to_be_clickable( + (By.ID, self.g.SAT['emisor']))) + if rfc_emisor: + txt.send_keys(rfc_emisor) + + hora_inicial = int(hora_inicial) + minuto_inicial = int(minuto_inicial) + segundo_inicial = int(segundo_inicial) + hora_final = int(hora_final) + minuto_final = int(minuto_final) + segundo_final = int(segundo_final) + + # Emitidas + if facturas_emitidas: + year = int(año) + month = int(mes) + day = int(día) + if not mes_completo_por_día and day: + dates = self.util.get_dates(year, month, day) + else: + dates = self.util.get_dates(year, month) + txt = browser.find_element_by_id(self.g.SAT['date_from']) + arg = "document.getElementsByName('{}')[0]." \ + "removeAttribute('disabled');".format( + self.g.SAT['date_from_name']) + browser.execute_script(arg) + txt.send_keys(dates[0]) + txt = browser.find_element_by_id(self.g.SAT['date_to']) + arg = "document.getElementsByName('{}')[0]." \ + "removeAttribute('disabled');".format( + self.g.SAT['date_to_name']) + browser.execute_script(arg) + txt.send_keys(dates[1]) + + # Hay que seleccionar también la hora, minuto y segundo + arg = "document.getElementById('{}')." \ + "value={};".format( + self.g.SAT['hour'], hora_final) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value={};".format( + self.g.SAT['minute'], minuto_final) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value={};".format( + self.g.SAT['second'], segundo_final) + browser.execute_script(arg) + if mes_completo_por_día: + return self._download_sat_month_emitidas(dates[1], day) + # Recibidas + else: + arg = "document.getElementById('{}')." \ + "value={};".format( + self.g.SAT['year'], año) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value={};".format( + self.g.SAT['month'], mes) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value='{}';".format( + self.g.SAT['day'], día) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value='{}';".format( + self.g.SAT['start_hour'], hora_inicial) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value='{}';".format( + self.g.SAT['start_minute'], minuto_inicial) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value='{}';".format( + self.g.SAT['start_second'], segundo_inicial) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value='{}';".format( + self.g.SAT['end_hour'], hora_final) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value='{}';".format( + self.g.SAT['end_minute'], minuto_final) + browser.execute_script(arg) + arg = "document.getElementById('{}')." \ + "value='{}';".format( + self.g.SAT['end_second'], segundo_final) + browser.execute_script(arg) + + results_table = browser.find_element_by_id( + self.g.SAT['resultados']) + browser.find_element_by_id(self.g.SAT['submit']).click() + wait.until(EC.staleness_of(results_table)) + wait.until(visibility_of_either( + (By.ID, self.g.SAT['resultados']), + (By.ID, self.g.SAT['noresultados']))) + if not facturas_emitidas and mes_completo_por_día: + return self._download_sat_month(año, mes, browser) + + results = wait.until(visibility_of_either( + (By.ID, self.g.SAT['resultados']), + (By.ID, self.g.SAT['noresultados']))) + if results.get_attribute('id') == self.g.SAT['resultados']: + wait.until(EC.element_to_be_clickable( + (By.NAME, self.g.SAT['download']))) + docs = browser.find_elements_by_name(self.g.SAT['download']) + return docs + else: + self.status('Sin facturas...') + return [] + + def download(self, docs): + 'Descarga los resultados' + if docs is None: + self.status('No se encontraron documentos') + return + if self.browser: + t = len(docs) + for i, v in enumerate(docs): + msg = 'Factura {} de {}'.format(i+1, t) + self.progress(i + 1, t) + self.status(msg) + download = self.g.SAT['page_cfdi'].format( + v.get_attribute('onclick').split("'")[1]) + self.browser.get(download) + self.progress(0, t) + self.util.sleep() + return + + def _download_sat_month(self, año, mes, browser): + '''Descarga CFDIs del SAT a una carpeta local + + Todos los CFDIs recibidos del mes selecionado''' + + year = int(año) + month = int(mes) + days_month = self.util.get_days(year, month) + 1 + days = ['%02d' % x for x in range(1, days_month)] + for d in days: + combo = browser.find_element_by_id(self.g.SAT['day']) + sb = combo.get_attribute('sb') + combo = browser.find_element_by_id('sbToggle_{}'.format(sb)) + combo.click() + self.util.sleep(2) + if mes == d: + links = browser.find_elements_by_link_text(d) + for l in links: + p = l.find_element_by_xpath( + '..').find_element_by_xpath('..') + sb2 = p.get_attribute('id') + if sb in sb2: + link = l + break + else: + link = browser.find_element_by_link_text(d) + link.click() + self.util.sleep(2) + browser.find_element_by_id(self.g.SAT['submit']).click() + self.util.sleep(3) + docs = browser.find_elements_by_name(self.g.SAT['download']) + if docs: + t = len(docs) + for i, v in enumerate(docs): + msg = 'Factura {} de {}'.format(i+1, t) + self.progress(i + 1, t) + self.status(msg) + download = self.g.SAT['page_cfdi'].format( + v.get_attribute('onclick').split("'")[1]) + browser.get(download) + self.util.sleep() + self.progress(0, t) + self.util.sleep() + return + + def _download_sat_month_emitidas(self, date_end, day_init): + '''Descarga CFDIs del SAT a una carpeta local + + Todos los CFDIs emitidos del mes selecionado + La interfaz en el SAT para facturas emitidas es diferente que para las + recibidas, es necesario buscar otros controles. + ''' + if day_init > 0: + day_init -= 1 + last_day = int(date_end.split('/')[0]) + for day in range(day_init, last_day): + current = '{:02d}'.format(day + 1) + date_end[2:] + print ("Día: ", current) + arg = "document.getElementsByName('{}')[0].removeAttribute(" \ + "'disabled');".format(self.g.SAT['date_from_name']) + self.browser.execute_script(arg) + arg = "document.getElementsByName('{}')[0].removeAttribute(" \ + "'disabled');".format(self.g.SAT['date_to_name']) + self.browser.execute_script(arg) + date_from = self.browser.find_element_by_id(self.g.SAT['date_from']) + date_to = self.browser.find_element_by_id(self.g.SAT['date_to']) + date_from.clear() + date_to.clear() + date_from.send_keys(current) + date_to.send_keys(current) + self.browser.find_element_by_id(self.g.SAT['submit']).click() + self.util.sleep(5) + docs = self.browser.find_elements_by_name(self.g.SAT['download']) + if docs: + print ("\tDocumentos: ", len(docs)) + else: + print ("\tDocumentos: 0") + self.download(docs) + return + + +class CSVPDF(FPDF): + """ + Genera un PDF a partir de un CSV + """ + G = Global() + TITULO1 = { + '2.0': 'Datos CFD', + '2.2': 'Datos CFD', + '3.0': 'Datos CFDI', + '3.2': 'Datos CFDI' + } + TITULO2 = { + '2.0': 'Año Aprobación:', + '2.2': 'Año Aprobación:', + '3.0': 'Serie CSD SAT:', + '3.2': 'Serie CSD SAT:' + } + TITULO3 = { + '2.0': 'N° Aprobación:', + '2.2': 'N° Aprobación:', + '3.0': 'Folio Fiscal:', + '3.2': 'Folio Fiscal:' + } + SPACE = 8 + LIMIT_MARGIN = 260 + DECIMALES = 2 + + def __init__(self, path_xml, path_template='', status_callback=print): + super().__init__(format='Letter') + self.status = status_callback + try: + self.xml = ET.parse(path_xml).getroot() + self.status('Generando: {}'.format(path_xml)) + except Exception as e: + self.xml = None + self.status('Error al parsear: {}'.format(path_xml)) + self.status(str(e)) + self.compress = False + self.error = '' + self.set_auto_page_break(True, margin=15) + self.SetRightMargin = 15 + self.SetTopMargin = 5 + self.set_draw_color(50, 50, 50) + self.set_title('Factura Libre') + self.set_author('www.facturalibre.net') + self.line_width = 0.1 + self.alias_nb_pages() + self.pos_size = {'x': 0, 'y': 0, 'w': 0, 'h': 0} + self.y_detalle = 80 + self.data = {} + self.elements = {} + decimales = self.DECIMALES + self.COLOR_RED = self._color(255, 0, 0) + if self.xml: + self.version = self.xml.attrib['version'] + #~ self.cadena = self._get_cadena(path_xml) + self._parse_csv(path_template) + + try: + decimales = len(self.xml.attrib['total'].split('.')[1]) + except IndexError: + decimales = self.DECIMALES + if decimales == 1: + decimales = self.DECIMALES + + self.currency = '{0:,.%sf}' % decimales + self.monedas = { + 'peso': ('peso', '$', 'm.n.'), + 'pesos': ('peso', '$', 'm.n.'), + 'mxn': ('peso', '$', 'm.n.'), + 'mxp': ('peso', '$', 'm.n.'), + 'euro': ('euro', '€', '€'), + 'euros': ('euro', '€', '€'), + 'dolar': ('dolar', '$', 'usd'), + 'dolares': ('dolar', '$', 'usd'), + 'usd': ('dolar', '$', 'usd'), + } + self.timbre = '' + + def make_pdf(self): + self.add_page() + self._set_detalle(self.G.PREFIX[self.version]) + self._set_totales(self.G.PREFIX[self.version]) + self._set_comprobante2(self.G.PREFIX[self.version]) + return + + def header(self): + self._set_emisor(self.G.PREFIX[self.version]) + self._set_receptor(self.G.PREFIX[self.version]) + self._set_comprobante(self.G.PREFIX[self.version]) + self._set_encabezados_detalle() + pass + + def footer(self): + self.set_y(-15) + self.set_font('Helvetica', '', 8) + self.set_text_color(*self._rgb(0)) + self.cell(0, 10, 'Página %s de {nb}' % self.page_no(), 0, 0, 'R') + self.set_x(10) + s = 'Factura elaborada con software libre ' + self.cell(0, 10, s, 0, 0, 'L') + self.set_x(10 + self.get_string_width(s)) + self.set_text_color(*self._rgb(6291456)) + self.cell(0, 10, 'www.facturalibre.net', 0, 0, 'L', + link='www.facturalibre.net') + + def _verify_margin(self, h=0): + mb = self.y + h + if mb > self.LIMIT_MARGIN: + self.add_page() + self.set_y(self.y_detalle) + return + + def _set_leyendas(self, pre): + com = self.xml.find('%sComplemento' % pre) + if com is None: + return + nodo = com.find('%sLeyendasFiscales' % self.G.PREFIX['LEYENDAS']) + if nodo is None: + return + for leyenda in list(nodo): + if 'disposicionFiscal' in leyenda.attrib: + self.elements['fiscal1_titulo']['y'] = self.y + self.elements['fiscal1']['y'] = self.y + self._write_text('fiscal1_titulo') + self._write_text('fiscal1', leyenda.attrib['disposicionFiscal']) + if 'norma' in leyenda.attrib: + self.elements['fiscal2_titulo']['y'] = self.y + self.elements['fiscal2']['y'] = self.y + self._write_text('fiscal2_titulo') + self._write_text('fiscal2', leyenda.attrib['norma']) + self.elements['fiscal_leyenda']['y'] = self.y + 5 + self._write_text('fiscal_leyenda', leyenda.attrib['textoLeyenda']) + self.ln(self.SPACE) + return + + def _set_donataria(self, pre): + com = self.xml.find('%sComplemento' % pre) + if com is None: return + nodo = com.find('%sDonatarias' % self.G.PREFIX['DONATARIA']) + if nodo is None: return + self.elements['dona_aut_titulo']['y'] = self.y + self.elements['dona_aut']['y'] = self.y + self._write_text('dona_aut_titulo') + self._write_text('dona_aut', nodo.attrib['noAutorizacion']) + self.elements['dona_fecha_titulo']['y'] = self.y + self.elements['dona_fecha']['y'] = self.y + self._write_text('dona_fecha_titulo') + self._write_text('dona_fecha', nodo.attrib['fechaAutorizacion']) + self.elements['dona_leyenda']['y'] = self.y + 5 + self._write_text('dona_leyenda', nodo.attrib['leyenda']) + self.ln(self.SPACE) + return + + def _set_comprobante2(self, pre): + fields = ( + ('Moneda', 'Moneda'), + ('TipoCambio', u'Tipo de cambio'), + ('formaDePago', u'Forma de pago'), + ('condicionesDePago', u'Condiciones de pago'), + ('metodoDePago', u'Método de pago'), + ('NumCtaPago', u'Cuenta de pago'), + ) + self._verify_margin(20) + self._set_donataria(pre) + self._verify_margin(20) + self._set_leyendas(pre) + self._verify_margin(20) + for f in fields: + if f[0] in self.xml.attrib: + self._write_otros(f[1], self.xml.attrib[f[0]]) + self.ln(self.SPACE) + if self.timbre: + qr_data = '?re=%s&rr=%s&tt=%s&id=%s' % ( + self.data['emisor_rfc'], + self.data['receptor_rfc'], + '%017.06f' % float(self.xml.attrib['total']), + self.timbre['UUID'] + ) + path = self._get_cbb(qr_data) + if os.path.exists(path): + self._verify_margin(self.elements['qr_cbb']['h']) + self.image( + path, + self.elements['qr_cbb']['x'], + self.y, + self.elements['qr_cbb']['w'], + self.elements['qr_cbb']['h'] + ) + os.unlink(path) + sello_emisor = self.timbre['selloCFD'] + sello_sat = self.timbre['selloSAT'] + self.elements['sello_cfd_titulo']['y'] = self.y + self.elements['sello_cfd_titulo']['text'] += 'I' + self._write_text('sello_cfd_titulo') + self.elements['sello_cfd']['y'] = self.y + 4 + self._write_text('sello_cfd', sello_emisor) + self.elements['sello_sat_titulo']['y'] = self.y + 1 + self._write_text('sello_sat_titulo') + self.elements['sello_sat']['y'] = self.y + 4 + self._write_text('sello_sat', sello_sat) + self.elements['fecha_titulo']['y'] = self.y + 1 + self._write_text('fecha_titulo') + self.elements['fecha']['y'] = self.y + self._write_text('fecha', self.timbre['FechaTimbrado']) + self.elements['leyenda']['text'] += 'I' + else: + sello_emisor = self.xml.attrib['sello'] + self.elements['sello_cfd_titulo']['x'] = 10 + self.elements['sello_cfd_titulo']['y'] = self.y + self._write_text('sello_cfd_titulo') + self.elements['sello_cfd']['x'] = 10 + self.elements['sello_cfd']['y'] = self.y + 4 + self.elements['sello_cfd']['w'] = 195 + self.elements['sello_cfd']['multiline'] = 0 + self._write_text('sello_cfd', sello_emisor) + self.elements['cadena_titulo']['text'] = 'Cadena original del CFD' + self._verify_margin(10) + self.elements['cadena_titulo']['y'] = self.y + 5 + self._write_text('cadena_titulo') + self.elements['cadena']['y'] = self.y + 4 + self._write_text('cadena', self._get_cadena()) + #~ self._verify_margin(10) + self.elements['leyenda']['y'] = self.y + 5 + self._write_text('leyenda') + return + + def _get_cbb(self, data): + scale = 10 + f = tempfile.NamedTemporaryFile(suffix='.png', delete=False) + path = f.name + code = pyqrcode.QRCode(data, mode='binary') + code.png(path, scale) + return path + + def _set_totales(self, pre): + moneda = ('peso', '$', 'm.n.') + if 'Moneda' in self.xml.attrib: + if self.xml.attrib['Moneda'].lower() in self.monedas: + moneda = self.monedas[self.xml.attrib['Moneda'].lower()] + else: + moneda = (self.xml.attrib['Moneda'], '', '') + importe = '%s %s' % ( + moneda[1], + self.currency.format(float(self.xml.attrib['subTotal'])) + ) + self._verify_margin(20) + self._write_importe('Subtotal', importe) + + if 'descuento' in self.xml.attrib: + if 'motivoDescuento' in self.xml.attrib: + self.elements['motivo_descuento']['y'] = self.y + self.elements['motivo_titulo']['y'] = self.y + self._write_text('motivo_titulo') + self._write_text( + 'motivo_descuento', self.xml.attrib['motivoDescuento']) + importe = '%s %s' % ( + moneda[1], + self.currency.format(float(self.xml.attrib['descuento'])) + ) + self._write_importe('Descuento', importe) + + imp = self.xml.find('%sImpuestos' % pre) + if imp is not None: + nodo = imp.find('%sTraslados' % pre) + if nodo is not None: + for n in list(nodo): + title = '%s %s %%' % (n.attrib['impuesto'], n.attrib['tasa']) + importe = '%s %s' % ( + moneda[1], + self.currency.format(float(n.attrib['importe'])) + ) + self._write_importe(title, importe) + nodo = imp.find('%sRetenciones' % pre) + if nodo is not None: + for n in list(nodo): + title = u'Retención %s' % n.attrib['impuesto'] + importe = '%s %s' % ( + moneda[1], + self.currency.format(float(n.attrib['importe'])) + ) + self._write_importe(title, importe) + + com = self.xml.find('%sComplemento' % pre) + if com is not None: + otros = com.find('%sImpuestosLocales' % self.G.PREFIX['IMP_LOCAL']) + if otros is not None: + for otro in list(otros): + if otro.tag == '%sRetencionesLocales' % self.G.PREFIX['IMP_LOCAL']: + name = 'ImpLocRetenido' + tasa = 'TasadeRetencion' + else: + name = 'ImpLocTrasladado' + tasa = 'TasadeTraslado' + title = u'%s %s %%' % (otro.attrib[name], otro.attrib[tasa]) + importe = '%s %s' % ( + moneda[1], + self.currency.format(float(otro.attrib['Importe'])) + ) + self._write_importe(title, importe) + + if 'totalImpuestosTrasladados' in imp.attrib: + importe = '%s %s' % ( + moneda[1], + self.currency.format(float(imp.attrib['totalImpuestosTrasladados'])) + ) + self.elements['imp_tras_titulo']['y'] = self.y + self.elements['imp_trasladado']['y'] = self.y + self._write_text('imp_tras_titulo') + self._write_text('imp_trasladado', importe) + if 'totalImpuestosRetenidos' in imp.attrib: + importe = '%s %s' % ( + moneda[1], + self.currency.format(float(imp.attrib['totalImpuestosRetenidos'])) + ) + self.elements['imp_rete_titulo']['y'] = self.y + self.elements['imp_retenido']['y'] = self.y + self._write_text('imp_rete_titulo') + self._write_text('imp_retenido', importe) + + total = float(self.xml.attrib['total']) + importe = '%s %s' % ( + moneda[1], + self.currency.format(total) + ) + self._write_importe('Total', importe) + self.ln() + letras = '-(%s/100 %s)-' % ( + NumerosLetras().to_letters(total, moneda[0]), + moneda[2] + ) + self._verify_margin(20) + self.elements['en_letras']['y'] = self.y + self._write_text('en_letras', letras.upper()) + self.ln(self.SPACE) + return + + def _write_otros(self, title, value): + self.elements['otros_titulo']['text'] = title + self.elements['otros_titulo']['y'] = self.y + self.elements['otros']['y'] = self.y + self._write_text('otros_titulo') + self._write_text('otros', value) + self.y += 3 + return + + def _write_importe(self, title, importe): + self.elements['subtotal_titulo']['text'] = title + self.elements['subtotal_titulo']['y'] = self.y + self.elements['subtotal']['y'] = self.y + self._write_text('subtotal_titulo') + self._write_text('subtotal', importe) + self.y += 4.5 + return + + def _set_detalle(self, pre): + conceptos = self.xml.find('%sConceptos' % pre) + for c in list(conceptos): + clave = '' + if 'noIdentificacion' in c.attrib: + clave = c.attrib['noIdentificacion'] + self._write_text('clave', clave) + unidad = 'No aplica' + if 'unidad' in c.attrib: + unidad = c.attrib['unidad'] + self._write_text('unidad', unidad) + importe = self.currency.format(float(c.attrib['cantidad'])) + self._write_text('cantidad', importe) + importe = self.currency.format(float(c.attrib['valorUnitario'])) + if c.attrib['valorUnitario'].startswith('-'): + self.elements['pu']['foreground'] = self.COLOR_RED + else: + self.elements['pu']['foreground'] = 0 + self._write_text('pu', importe) + importe = self.currency.format(float(c.attrib['importe'])) + if c.attrib['importe'].startswith('-'): + self.elements['importe']['foreground'] = self.COLOR_RED + else: + self.elements['importe']['foreground'] = 0 + self._write_text('importe', importe) + page = self.page_no() + descripcion = self._get_descripcion(c, pre) + self._write_text('descripcion', descripcion) + if self.page_no() > page: + new_y = self.y_detalle + else: + new_y = self.y + self.elements['clave']['y'] = new_y + self.elements['descripcion']['y'] = new_y + self.elements['unidad']['y'] = new_y + self.elements['cantidad']['y'] = new_y + self.elements['pu']['y'] = new_y + self.elements['importe']['y'] = new_y + self.line(10, self.y, 205, self.y) + self.ln() + return + + def _get_descripcion(self, nodo, pre): + s = nodo.attrib['descripcion'] + for n in list(nodo): + if n.tag == '%sInformacionAduanera' % pre: + s += u'\nAduana: %s\nFecha: %s Número: %s' % ( + n.attrib['aduana'], + n.attrib['fecha'], + n.attrib['numero'] + ) + elif n.tag == '%sCuentaPredial' % pre: + s += u'\n\nCuenta Predial Número: %s' % n.attrib['numero'] + elif n.tag == '%sParte' % pre: + serie = '' + if 'noIdentificacion' in n.attrib: + serie = n.attrib['noIdentificacion'] + s += u'\n\n Cantidad: %s Serie: %s\n %s' % ( + n.attrib['cantidad'], + serie, + n.attrib['descripcion'] + ) + for n2 in list(n): + if n2.tag == '%sInformacionAduanera' % pre: + s += u'\n Aduana: %s\n Fecha: %s Número: %s' % ( + n2.attrib['aduana'], + n2.attrib['fecha'], + n2.attrib['numero'] + ) + info = nodo.find('%sComplementoConcepto' % pre) + if info is None: + return s + info = info.find('{}instEducativas'.format(self.G.PREFIX['IEDU'])) + if info is not None: + s1 = '' + if 'nombreAlumno' in info.attrib: + s += '\n\nAlumno: %s' % info.attrib['nombreAlumno'] + if 'CURP' in info.attrib: + s += '\nCURP: %s' % info.attrib['CURP'] + if 'nivelEducativo' in info.attrib: + s1 = '\nAcuerdo de incorporación ante la SEP %s' % info.attrib['nivelEducativo'] + if 'autRVOE' in info.attrib: + if s1: + s1 = '%s %s' % (s1, info.attrib['autRVOE']) + else: + s1 = '\nAcuerdo de incorporación ante la SEP No: %s' % info.attrib['autRVOE'] + s += s1 + if 'rfcPago' in info.attrib: + s += '\nRFC de pago: %s' % info.attrib['rfcPago'] + return s + + def _set_encabezados_detalle(self): + self._write_text('clave_titulo') + self._write_text('descripcion_titulo') + self._write_text('unidad_titulo') + self._write_text('cantidad_titulo') + self._write_text('pu_titulo') + self._write_text('importe_titulo') + self.ln() + return + + def _set_comprobante(self, pre): + if 'LugarExpedicion' in self.xml.attrib: + lugar = '%s, ' % self.xml.attrib['LugarExpedicion'] + else: + lugar = self.data['expedicion'] + fecha = self.xml.attrib['fecha'].split('T') + date = datetime.strptime(fecha[0], '%Y-%m-%d') + lugar += date.strftime('%A, %d de %B del %Y') # .decode('utf-8') + self._write_text('cfdi_fecha', lugar) + self._write_text('cfdi_hora', fecha[1]) + self._write_text('cfdi_titulo1') + self._write_text('cfdi_titulo2', self.TITULO2[self.version]) + self._write_text('cfdi_titulo3', self.TITULO3[self.version]) + self._write_text('cfdi_titulo4') + self._write_text('cfdi_titulo5') + self._write_text('cfdi_titulo', self.TITULO1[self.version]) + self._write_text('cfdi_regimen', self.data['regimen']) + self.timbre = self._get_timbre(self.G.PREFIX[self.version]) + csdsat = '' + uuid = '' + if self.timbre: + csdsat = self.timbre['noCertificadoSAT'] + uuid = self.timbre['UUID'].upper() + else: + if 'anoAprobacion' in self.xml.attrib: + csdsat = self.xml.attrib['anoAprobacion'] + if 'noAprobacion' in self.xml.attrib: + uuid = self.xml.attrib['noAprobacion'] + folio = '' + if 'serie' in self.xml.attrib: + folio += self.xml.attrib['serie'] + if 'folio' in self.xml.attrib: + if folio: + folio += '-%s' % self.xml.attrib['folio'] + else: + folio = self.xml.attrib['folio'] + self._write_text('cfdi_csd', self.xml.attrib['noCertificado']) + self._write_text('cfdi_csdsat', csdsat) + self._write_text('cfdi_uuid', uuid) + self._write_text('cfdi_tipo', self.xml.attrib['tipoDeComprobante'].upper()) + self._write_text('cfdi_folio', folio) + return + + def _set_receptor(self, pre): + self._write_text('receptor_titulo') + receptor = self.xml.find('%sReceptor' % pre) + name_receptor = 'Sin nombre' + if 'nombre' in receptor.attrib: + name_receptor = receptor.attrib['nombre'] + self._write_text('receptor_nombre', name_receptor) + self._write_text('receptor_rfc', receptor.attrib['rfc']) + self.data['receptor_rfc'] = receptor.attrib['rfc'] + dir_fiscal = receptor.find('%sDomicilio' % pre) + domicilio1 = '' + if dir_fiscal is not None: + if 'calle' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['calle'] + if 'noExterior' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['noExterior'] + if 'noInterior' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['noInterior'] + domicilio2 = '' + if 'colonia' in dir_fiscal.attrib: + if dir_fiscal.attrib['colonia'].strip().lower().startswith('col.'): + domicilio2 += '%s, ' % dir_fiscal.attrib['colonia'] + else: + domicilio2 += 'Col. %s, ' % dir_fiscal.attrib['colonia'] + if 'codigoPostal' in dir_fiscal.attrib: + domicilio2 += 'C.P. %s ' % dir_fiscal.attrib['codigoPostal'] + domicilio3 ='' + if 'municipio' in dir_fiscal.attrib: + domicilio3 += '%s, ' % dir_fiscal.attrib['municipio'] + if 'estado' in dir_fiscal.attrib: + domicilio3 += '%s, ' % dir_fiscal.attrib['estado'] + if 'pais' in dir_fiscal.attrib: + domicilio3 += '%s ' % dir_fiscal.attrib['pais'] + domicilio4 = '' + if 'localidad' in dir_fiscal.attrib: + domicilio4 += 'Localidad: %s, ' % dir_fiscal.attrib['localidad'] + if 'referencia' in dir_fiscal.attrib: + domicilio4 += 'Referencia: %s' % dir_fiscal.attrib['referencia'] + self._write_text('receptor_direccion1', domicilio1) + self._write_text('receptor_direccion2', domicilio2) + self._write_text('receptor_direccion3', domicilio3) + self._write_text('receptor_direccion4', domicilio4) + return + + def _set_emisor(self, pre): + emisor = self.xml.find('%sEmisor' % pre) + dir_fiscal = emisor.find('%sExpedidoEn' % pre) + lugar = '' + if dir_fiscal is not None: + self.elements['emisor_logo']['x'] = 10 + self.elements['emisor_nombre']['x'] = 45 + self.elements['emisor_rfc']['x'] = 45 + self.elements['emisor_direccion1']['x'] = 45 + self.elements['emisor_direccion2']['x'] = 45 + self.elements['emisor_direccion3']['x'] = 45 + self.elements['emisor_direccion4']['x'] = 45 + self.elements['emisor_nombre']['w'] = 75 + self.elements['emisor_rfc']['w'] = 75 + self.elements['emisor_direccion1']['w'] = 75 + self.elements['emisor_direccion2']['w'] = 75 + self.elements['emisor_direccion3']['w'] = 75 + self.elements['emisor_direccion4']['w'] = 75 + domicilio1 = '' + if 'calle' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['calle'] + if 'noExterior' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['noExterior'] + if 'noInterior' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['noInterior'] + domicilio2 = '' + if 'colonia' in dir_fiscal.attrib: + if dir_fiscal.attrib['colonia'].strip().lower().startswith('col.'): + domicilio2 += '%s, ' % dir_fiscal.attrib['colonia'] + else: + domicilio2 += 'Col. %s, ' % dir_fiscal.attrib['colonia'] + if 'codigoPostal' in dir_fiscal.attrib: + domicilio2 += 'C.P. %s ' % dir_fiscal.attrib['codigoPostal'] + domicilio3 ='' + if 'municipio' in dir_fiscal.attrib: + domicilio3 += '%s, ' % dir_fiscal.attrib['municipio'] + lugar += '%s, ' % dir_fiscal.attrib['municipio'] + if 'estado' in dir_fiscal.attrib: + domicilio3 += '%s, ' % dir_fiscal.attrib['estado'] + lugar += '%s, ' % dir_fiscal.attrib['estado'] + if 'pais' in dir_fiscal.attrib: + domicilio3 += '%s ' % dir_fiscal.attrib['pais'] + domicilio4 = '' + if 'localidad' in dir_fiscal.attrib: + domicilio4 += '%s, ' % dir_fiscal.attrib['localidad'] + if 'referencia' in dir_fiscal.attrib: + domicilio4 += '%s' % dir_fiscal.attrib['referencia'] + self._write_text('expedido_titulo') + self._write_text('expedido_direccion1', domicilio1) + self._write_text('expedido_direccion2', domicilio2) + self._write_text('expedido_direccion3', domicilio3) + self._write_text('expedido_direccion4', domicilio4) + + path_logo = 'logos/{}.png'.format(emisor.attrib['rfc'].lower()) + self.data['emisor_rfc'] = emisor.attrib['rfc'] + if os.path.exists(path_logo): + self.image( + path_logo, + self.elements['emisor_logo']['x'], + self.elements['emisor_logo']['y'], + self.elements['emisor_logo']['w'], + self.elements['emisor_logo']['h'] + ) + regimen = emisor.find('%sRegimenFiscal' % pre) + if regimen is None: + regimen = '' + else: + regimen = regimen.attrib['Regimen'] + dir_fiscal = emisor.find('%sDomicilioFiscal' % pre) + lugar2 = '' + domicilio1 = '' + domicilio2 = '' + domicilio3 = '' + domicilio4 = '' + if not dir_fiscal is None: + if 'calle' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['calle'] + if 'noExterior' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['noExterior'] + if 'noInterior' in dir_fiscal.attrib: + domicilio1 += '%s ' % dir_fiscal.attrib['noInterior'] + + if 'colonia' in dir_fiscal.attrib: + if dir_fiscal.attrib['colonia'].strip().lower().startswith('col.'): + domicilio2 += '%s, ' % dir_fiscal.attrib['colonia'] + else: + domicilio2 += 'Col. %s, ' % dir_fiscal.attrib['colonia'] + if 'codigoPostal' in dir_fiscal.attrib: + domicilio2 += 'C.P. %s ' % dir_fiscal.attrib['codigoPostal'] + + if 'municipio' in dir_fiscal.attrib: + domicilio3 += '%s, ' % dir_fiscal.attrib['municipio'] + lugar2 += '%s, ' % dir_fiscal.attrib['municipio'] + if 'estado' in dir_fiscal.attrib: + domicilio3 += '%s, ' % dir_fiscal.attrib['estado'] + lugar2 += '%s, ' % dir_fiscal.attrib['estado'] + if 'pais' in dir_fiscal.attrib: + domicilio3 += '%s ' % dir_fiscal.attrib['pais'] + + if 'localidad' in dir_fiscal.attrib: + domicilio4 += '%s, ' % dir_fiscal.attrib['localidad'] + if 'referencia' in dir_fiscal.attrib: + domicilio4 += '%s' % dir_fiscal.attrib['referencia'] + name_emisor = 'Sin nombre' + if 'nombre' in emisor.attrib: + name_emisor = emisor.attrib['nombre'] + self._write_text('emisor_nombre', name_emisor) + self._write_text('emisor_rfc', emisor.attrib['rfc']) + self._write_text('emisor_direccion1', domicilio1) + self._write_text('emisor_direccion2', domicilio2) + self._write_text('emisor_direccion3', domicilio3) + self._write_text('emisor_direccion4', domicilio4) + if not lugar: + lugar = lugar2 + self.data['expedicion'] = lugar + self.data['regimen'] = regimen + return + + def _rgb(self, col): + return (col // 65536), (col // 256 % 256), (col % 256) + + def _color(self, r, g, b): + return int('%02x%02x%02x' % (r, g, b), 16) + + def _write_text(self, k, s=''): + if not k in self.elements: return + self.pos_size['x'] = self.elements[k]['x'] + self.pos_size['y'] = self.elements[k]['y'] + self.pos_size['w'] = self.elements[k]['w'] + self.pos_size['h'] = self.elements[k]['h'] + self.set_font( + self.elements[k]['font'], + self.elements[k]['style'], + self.elements[k]['size'] + ) + self.set_text_color(*self._rgb(self.elements[k]['foreground'])) + self.set_fill_color(*self._rgb(self.elements[k]['background'])) + self.set_xy(self.pos_size['x'], self.pos_size['y']) + if self.elements[k]['border'] == '0' or \ + self.elements[k]['border'] == '1': + border = eval(self.elements[k]['border']) + else: + border = self.elements[k]['border'] + if not s: + s = self.elements[k]['text'] + if self.elements[k]['multiline']: + self.multi_cell( + self.pos_size['w'], + self.pos_size['h'], + s, + border, + self.elements[k]['align'], + bool(self.elements[k]['background']) + ) + else: + self._ajust_text(s, self.elements[k]['size']) + self.cell( + self.pos_size['w'], + self.pos_size['h'], + s, + border, + 0, + self.elements[k]['align'], + bool(self.elements[k]['background']) + ) + return + + def _ajust_text(self, s, size): + if not isinstance(s, str): + s = str(s) + s = s.encode('ascii', 'replace') + while True: + if self.get_string_width(s) < (self.pos_size['w']-0.8): + break + else: + size -= 1 + self.set_font_size(size) + return + + def _get_timbre(self, pre): + com = self.xml.find('%sComplemento' % pre) + if com is None: return {} + timbre = com.find('%sTimbreFiscalDigital' % self.G.PREFIX['TIMBRE']) + if timbre is None: + return {} + else: + return timbre.attrib + + def _get_cadena(self): + return self.G.CADENA.format(**self.timbre) + + def _parse_csv(self, template='', emisor_rfc=''): + "Parse template format csv file and create elements dict" + keys = ('x', 'y', 'w', 'h', 'font', 'size', 'style', 'foreground', + 'background', 'align', 'priority', 'multiline', 'border', 'text') + self.elements = {} + if template: + path_template = template + else: + path_template = '{}/{}.csv'.format( + self.G.PATHS['TEMPLATE'], + emisor_rfc.lower() + ) + if not os.path.exists(path_template): + path_template = '{}/default.csv'.format(self.G.PATHS['TEMPLATE']) + reader = csv.reader(open(path_template, 'r'), delimiter=',') + for row in reader: + new_list = [] + for v in row[1:]: + if v == (''): + new_list.append('') + elif v.startswith("'"): + new_list.append(v[1:-1]) + else: + new_list.append(eval(v.strip())) + self.elements[row[0][1:-1]] = dict(zip(keys, new_list)) + return + diff --git a/admincfdi/template/default.csv b/admincfdi/template/default.csv new file mode 100644 index 0000000..ce81a64 --- /dev/null +++ b/admincfdi/template/default.csv @@ -0,0 +1,77 @@ +'headers','x','y','w','h','font','size','style','foreground','background','align','priority','multiline','border', 'text' +'emisor_logo',30,5,35,30,'',0,'',0,0,'',0,0,'' +'emisor_nombre',85,5,100,4,'Helvetica',10,'B',6291456,14803425,'C',0,0,'0','' +'emisor_rfc',85,9,100,4,'Helvetica',10,'B',6291456,14803425,'C',0,0,'0','' +'emisor_direccion1',85,13,100,4,'Helvetica',9,'B',0,0,'C',0,0,'0','' +'emisor_direccion2',85,17,100,4,'Helvetica',9,'B',0,0,'C',0,0,'0','' +'emisor_direccion3',85,21,100,4,'Helvetica',9,'B',0,0,'C',0,0,'0','' +'emisor_direccion4',85,25,100,4,'Helvetica',9,'B',0,0,'C',0,0,'0','' +'expedido_titulo',130,5,75,4,'Helvetica',7,'B',6291456,14803425,'C',0,0,'0','Expedido En' +'expedido_direccion1',130,9,75,4,'Helvetica',8,'B',0,0,'C',0,0,'0','' +'expedido_direccion2',130,13,75,4,'Helvetica',8,'B',0,0,'C',0,0,'0','' +'expedido_direccion3',130,17,75,4,'Helvetica',8,'B',0,0,'C',0,0,'0','' +'expedido_direccion4',130,21,75,4,'Helvetica',8,'B',0,0,'C',0,0,'0','' +'receptor_titulo',10,40,100,4,'Helvetica',7,'B',6291456,14803425,'C',0,0,'0','Receptor' +'receptor_nombre',15,45,95,4,'Helvetica',10,'B',6291456,0,'L',0,0,'0','' +'receptor_rfc',15,49,95,4,'Helvetica',10,'B',6291456,0,'L',0,0,'0','' +'receptor_direccion1',15,53,100,4,'Helvetica',9,'B',0,0,'L',0,0,'0','' +'receptor_direccion2',15,57,100,4,'Helvetica',9,'B',0,0,'L',0,0,'0','' +'receptor_direccion3',15,61,100,4,'Helvetica',9,'B',0,0,'L',0,0,'0','' +'receptor_direccion4',15,65,100,4,'Helvetica',8,'B',0,0,'L',0,0,'0','' +'cfdi_titulo',120,40,85,4,'Helvetica',7,'B',6291456,14803425,'C',0,0,'0','' +'cfdi_fecha',10,35,170,4,'Helvetica',8,'',0,0,'R',0,0,'0','' +'cfdi_hora',180,35,20,4,'Helvetica',6,'B',0,0,'R',0,0,'0','' +'cfdi_titulo1',120,44,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','Serie CSD Emisor' +'cfdi_titulo2',120,48,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','' +'cfdi_titulo3',120,52,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','' +'cfdi_titulo4',120,56,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','Tipo de Comprobante' +'cfdi_titulo5',120,60,30,4,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','Folio' +'cfdi_regimen',120,64,85,4,'Helvetica',7,'B',6291456,14803425,'C',0,0,'0','' +'cfdi_csd',150,44,55,4,'Helvetica',7,'',0,0,'C',0,0,'0','' +'cfdi_csdsat',150,48,55,4,'Helvetica',7,'',0,0,'C',0,0,'0','' +'cfdi_uuid',150,52,55,4,'Helvetica',7,'',0,0,'C',0,0,'0','' +'cfdi_tipo',150,56,55,4,'Helvetica',7,'',0,0,'C',0,0,'0','' +'cfdi_folio',150,60,55,4,'Helvetica',12,'B',16711680,0,'C',0,0,'0','' +'clave_titulo',10,70,20,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Clave' +'descripcion_titulo',30,70,85,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Descripcion' +'unidad_titulo',115,70,15,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Unidad' +'cantidad_titulo',130,70,20,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Cantidad' +'pu_titulo',150,70,25,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'R','Valor Unitario' +'importe_titulo',175,70,30,3,'Helvetica',7,'B',6291456,14803425,'C',0,0,'','Importe' +'clave',10,73,20,3,'Helvetica',7,'',0,0,'C',0,0,'T','Sin' +'descripcion',30,73,85,3,'Helvetica',7,'',0,0,'J',0,1,'T','' +'unidad',115,73,15,3,'Helvetica',7,'',0,0,'C',0,0,'T','' +'cantidad',130,73,20,3,'Helvetica',7,'',0,0,'R',0,0,'T','' +'pu',150,73,25,3,'Helvetica',7,'',0,0,'R',0,0,'T','' +'importe',175,73,30,3,'Helvetica',7,'',0,0,'R',0,0,'T','' +'subtotal_titulo',150,0,25,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Subtotal' +'subtotal',175,0,30,4,'Helvetica',8,'B',0,0,'R',0,0,'0','' +'motivo_titulo',10,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Motivo del descuento' +'motivo_descuento',50,0,95,4,'Helvetica',8,'',0,0,'L',0,0,'0','' +'imp_tras_titulo',10,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Total impuestos trasladados' +'imp_rete_titulo',80,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Total impuestos retenidos' +'imp_trasladado',50,0,25,4,'Helvetica',8,'B',0,0,'R',0,0,'0','' +'imp_retenido',120,0,25,4,'Helvetica',8,'B',0,0,'R',0,0,'0','' +'en_letras',10,0,195,4,'Helvetica',7,'B',0,0,'C',0,0,'0','' +'otros_titulo',10,0,25,3,'Helvetica',7,'B',6291456,14803425,'R',0,0,'0','' +'otros',35,0,40,3,'Helvetica',7,'',0,0,'L',0,0,'0','' +'dona_aut_titulo',10,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','No. Autorizacion' +'dona_fecha_titulo',80,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Fecha de Autorizacion' +'dona_aut',50,0,25,4,'Helvetica',8,'B',0,0,'L',0,0,'0','' +'dona_fecha',120,0,25,4,'Helvetica',8,'B',0,0,'L',0,0,'0','' +'dona_leyenda',10,0,195,4,'Helvetica',6,'',6291456,14803425,'J',0,0,'0','' +'fiscal1_titulo',10,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Disposicion Fiscal' +'fiscal2_titulo',80,0,40,4,'Helvetica',8,'B',6291456,14803425,'R',0,0,'0','Norma' +'fiscal1',50,0,25,4,'Helvetica',8,'B',0,0,'C',0,0,'0','' +'fiscal2',120,0,85,4,'Helvetica',8,'B',0,0,'L',0,0,'0','' +'fiscal_leyenda',10,0,195,4,'Helvetica',6,'',6291456,14803425,'J',0,1,'0','' +'qr_cbb',10,0,30,30,'',0,'',0,0,'',0,0,'' +'sello_cfd_titulo',45,0,160,4,'Helvetica',6,'B',0,0,'L',0,0,'0','Sello Digital del CFD' +'sello_cfd',45,0,160,4,'Helvetica',6,'',6291456,14803425,'J',0,1,'0','' +'sello_sat_titulo',45,0,160,4,'Helvetica',6,'B',0,0,'L',0,0,'0','Sello del SAT' +'sello_sat',45,0,160,4,'Helvetica',6,'',6291456,14803425,'J',0,1,'0','' +'fecha_titulo',45,0,35,4,'Helvetica',6,'B',0,0,'L',0,0,'0','Fecha y hora de certificacion:' +'fecha',80,0,50,4,'Helvetica',6,'',0,0,'L',0,0,'0','' +'cadena_titulo',10,0,195,4,'Helvetica',6,'B',0,0,'L',0,0,'0','Cadena original del complemento de certificacion digital del SAT' +'cadena',10,0,195,4,'Helvetica',6,'',6291456,14803425,'J',0,1,'0','' +'leyenda',65.5,0,80,4,'Helvetica',6,'',6291456,14803425,'C',0,0,'0','Este documento es una representacion impresa de CFD' \ No newline at end of file diff --git a/admincfdi/template/plantilla_factura.ods b/admincfdi/template/plantilla_factura.ods new file mode 100644 index 0000000..6f42094 Binary files /dev/null and b/admincfdi/template/plantilla_factura.ods differ diff --git a/admincfdi/tests/test_pyutil.py b/admincfdi/tests/test_pyutil.py new file mode 100644 index 0000000..5962e5b --- /dev/null +++ b/admincfdi/tests/test_pyutil.py @@ -0,0 +1,274 @@ +import unittest + + +class LeeCredenciales(unittest.TestCase): + + def setUp(self): + from unittest.mock import Mock + from admincfdi import pyutil + + self.onefile = Mock() + self.onefile.readline.return_value = 'rfc pwd' + pyutil.open = Mock(return_value=self.onefile) + + def tearDown(self): + from admincfdi import pyutil + + del pyutil.open + + def test_file_not_found(self): + from admincfdi import pyutil + + pyutil.open.side_effect = FileNotFoundError + util = pyutil.Util() + status, rfc, pwd = util.lee_credenciales('ruta') + self.assertEqual('Archivo no encontrado: ruta', status) + + def test_not_two_fields(self): + from admincfdi import pyutil + + self.onefile.readline.return_value = '' + util = pyutil.Util() + status, rfc, pwd = util.lee_credenciales('ruta') + self.assertEqual('No contiene dos campos: ruta', status) + + def test_success(self): + from admincfdi import pyutil + + util = pyutil.Util() + status, rfc, pwd = util.lee_credenciales('ruta') + self.assertEqual('Ok', status) + + def test_surplus_whitespace_is_ok(self): + from admincfdi import pyutil + + self.onefile.readline.return_value = ' \t rfc \t pwd \r\n' + util = pyutil.Util() + status, rfc, pwd = util.lee_credenciales('ruta') + self.assertEqual('rfc', rfc) + self.assertEqual('pwd', pwd) + + +class DescargaSAT(unittest.TestCase): + + def setUp(self): + import time + from unittest.mock import Mock + from selenium import webdriver + from admincfdi import pyutil + + self.webdriver = pyutil.webdriver + pyutil.webdriver = Mock() + pyutil.webdriver.FirefoxProfile = webdriver.FirefoxProfile + + self.WebDriverWait = pyutil.WebDriverWait + self.wait = Mock() + pyutil.WebDriverWait = Mock(return_value=self.wait) + + self.sleep = time.sleep + time.sleep = Mock() + + self.status = Mock() + self.transfer = Mock() + + pyutil.print = Mock() + + def tearDown(self): + import time + from admincfdi import pyutil + + pyutil.webdriver = self.webdriver + pyutil.WebDriverWait = self.WebDriverWait + time.sleep = self.sleep + del pyutil.print + + def test_get_firefox_profile(self): + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + descarga = DescargaSAT() + profile = descarga.get_firefox_profile('carpeta_destino') + self.assertIsInstance(profile, webdriver.FirefoxProfile) + + def test_connect(self): + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + profile = descarga.connect(profile, rfc='x', ciec='y') + self.assertEqual(3, self.status.call_count) + + def test_connect_fail(self): + from unittest.mock import Mock + from admincfdi.pyutil import DescargaSAT + from admincfdi.pyutil import WebDriverWait + from selenium import webdriver + from selenium.common.exceptions import TimeoutException + + self.wait.until.side_effect = TimeoutException + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + profile = descarga.connect(profile, rfc='x', ciec='y') + self.assertRaises(TimeoutException) + + def test_disconnect_not_connected(self): + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + descarga = DescargaSAT(status_callback=self.status) + descarga.disconnect() + self.assertEqual(0, self.status.call_count) + + def test_disconnect(self): + from unittest.mock import Mock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = Mock() + descarga.disconnect() + self.assertEqual(2, self.status.call_count) + + def test_search_not_connected(self): + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + results = descarga.search(uuid='uuid') + self.assertEqual(0, len(results)) + + def test_search_uuid(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = MagicMock() + results = descarga.search(uuid='uuid', día='00') + self.assertEqual(0, len(results)) + + def test_search_facturas_emitidas(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = MagicMock() + results = descarga.search(facturas_emitidas=True, + año=1, mes=1) + self.assertEqual(0, len(results)) + + def test_search_rfc_emisor(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = MagicMock() + results = descarga.search(rfc_emisor='x') + self.assertEqual(0, len(results)) + + def test_search_not_found(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.g.SAT['found'] = 'no' + descarga.browser = MagicMock() + c = MagicMock() + descarga.browser.find_elements_by_class_name.return_value = [c] + c.get_attribute.return_value = 'x no x' + c.is_displayed.return_value = True + results = descarga.search() + self.assertEqual(0, len(results)) + + def test_search_not_found_exception(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = MagicMock() + c = MagicMock() + descarga.browser.find_elements_by_class_name.side_effect = Exception() + results = descarga.search() + self.assertEqual(0, len(results)) + + def test_search_mes_eq_día(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = MagicMock() + link = MagicMock() + descarga.browser.find_elements_by_link_text.return_value = [link] + combo = descarga.browser.find_element_by_id.return_value + combo.get_attribute.return_value = 'sb' + r = link.find_element_by_xpath.return_value + p = r.find_element_by_xpath.return_value + p.get_attribute.return_value = 'sb2' + results = descarga.search(día='01', mes='01') + self.assertEqual(0, len(results)) + + def test_search_minuto_final_cero(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = MagicMock() + results = descarga.search(día='01', mes='02', minuto_final='0') + + expected = "document.getElementById('ctl00_MainContent_CldFecha_DdlMinutoFin').value='0';" + descarga.browser.execute_script.assert_any_call(expected) + + def test_search_mes_completo_por_día(self): + from unittest.mock import MagicMock, Mock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = MagicMock() + descarga._download_sat_month = Mock(return_value=[]) + results = descarga.search(día='00', + mes_completo_por_día=True) + self.assertEqual(0, len(results)) + + def test_search_mes_ne_día(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status) + descarga.browser = MagicMock() + results = descarga.search(día='01', mes='02') + self.assertEqual(0, len(results)) + + def test_download(self): + from unittest.mock import MagicMock + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + descarga = DescargaSAT(status_callback=self.status, + download_callback=self.transfer) + descarga.browser = MagicMock() + docs = [MagicMock()] + descarga.download(docs) + + +if __name__ == '__main__': + unittest.main() diff --git a/ui/mainwindow.ui b/admincfdi/ui/mainwindow.ui similarity index 99% rename from ui/mainwindow.ui rename to admincfdi/ui/mainwindow.ui index 744a58f..1de975a 100644 --- a/ui/mainwindow.ui +++ b/admincfdi/ui/mainwindow.ui @@ -1688,9 +1688,9 @@ - + {Sans} 10 {} - JSON + CSV 2 int:template_type @@ -1733,11 +1733,11 @@ - + {Sans} 10 {} #ff5500 readonly - string:template_json + string:template_csv 36 @@ -1787,8 +1787,8 @@ - - button_select_template_json_click + + button_select_template_csv_click csv.gif 1 diff --git a/values.py b/admincfdi/values.py similarity index 91% rename from values.py rename to admincfdi/values.py index 28adbe5..2d5d695 100644 --- a/values.py +++ b/admincfdi/values.py @@ -23,7 +23,7 @@ class Global(object): OS = sys.platform MAIN = 'mainwindow' TITLE = 'Admin CFDI - Factura Libre' - CWD = os.getcwd() + CWD = os.path.dirname(__file__) PATHS = { 'current': CWD, 'img': os.path.join(CWD, 'img'), @@ -37,6 +37,11 @@ class Global(object): 'XSLT_SELLO_SAT': os.path.join(CWD, 'bin', 'get_sello_sat.xslt'), 'XSLT_CADENA': os.path.join(CWD, 'bin', 'cfdi_{}.xslt'), 'XSLT_TIMBRE': os.path.join(CWD, 'bin', 'timbre_1.0.xslt'), + 'TEMPLATE': os.path.join(CWD, 'template'), + 'USER': os.path.expanduser('~') + } + SCRIPTS = { + 'CFDI2PDF': os.path.abspath(os.path.join(CWD, '..', 'cfdi2pdf')), } EXT_XML = '.xml' EXT_ODS = '.ods' @@ -53,7 +58,7 @@ class Global(object): FILES = { 'main': os.path.join(PATHS['ui'], 'mainwindow.ui'), 'config': os.path.join(PATHS['ui'], 'config.ini'), - 'log': os.path.join(CWD, 'admincfdi.log'), + 'log': os.path.join(PATHS['USER'], 'admincfdi.log'), } FILE_NAME = '{serie}{folio:06d}_{fecha}_{receptor_rfc}' CADENA = '||{version}|{UUID}|{FechaTimbrado}|{selloCFD}|{noCertificadoSAT}||' @@ -147,6 +152,7 @@ class Global(object): 'IMP_LOCAL': '{http://www.sat.gob.mx/implocal}', 'IEDU': '{http://www.sat.gob.mx/iedu}', 'DONATARIA': '{http://www.sat.gob.mx/donat}', + 'LEYENDAS': '{http://www.sat.gob.mx/leyendasFiscales}', } page_init = 'https://cfdiau.sat.gob.mx/nidp/app/login?id=SATUPCFDiCon&' \ 'sid=0&option=credential&sid=0' @@ -165,18 +171,23 @@ class Global(object): 'year': 'DdlAnio', 'month': 'ctl00_MainContent_CldFecha_DdlMes', 'day': 'ctl00_MainContent_CldFecha_DdlDia', + 'hour': 'ctl00_MainContent_CldFechaFinal2_DdlHora', + 'minute': 'ctl00_MainContent_CldFechaFinal2_DdlMinuto', + 'second': 'ctl00_MainContent_CldFechaFinal2_DdlSegundo', 'submit': 'ctl00_MainContent_BtnBusqueda', 'download': 'BtnDescarga', 'emisor': 'ctl00_MainContent_TxtRfcReceptor', 'receptor': 'ctl00_MainContent_TxtRfcReceptor', 'uuid': 'ctl00_MainContent_TxtUUID', - 'combos': 'sbToggle', + 'combos': 'sbToggle_{}', 'found': 'No existen registros que cumplan con los criterios de', 'subtitle': 'subtitle', 'page_init': page_init, 'page_cfdi': page_cfdi, 'page_receptor': page_cfdi.format('ConsultaReceptor.aspx'), 'page_emisor': page_cfdi.format('ConsultaEmisor.aspx'), + 'resultados': 'ctl00_MainContent_PnlResultados', + 'noresultados': 'ctl00_MainContent_PnlNoResultados', 'start_hour': 'ctl00_MainContent_CldFecha_DdlHora', 'start_minute': 'ctl00_MainContent_CldFecha_DdlMinuto', 'start_second': 'ctl00_MainContent_CldFecha_DdlSegundo', diff --git a/cfdi2pdf b/cfdi2pdf new file mode 100755 index 0000000..1ccbea9 --- /dev/null +++ b/cfdi2pdf @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +import argparse +import sys + +from admincfdi.pyutil import CSVPDF +from admincfdi.pyutil import Util + +def process_command_line_arguments(): + parser = argparse.ArgumentParser( + description='Crea un PDF desde una plantilla CSV') + + help = 'Archivo XML origen.' + parser.add_argument('-a', '--archivo-xml', help=help, default='') + + help = 'Directorio origen.' + parser.add_argument('-o', '--directorio-origen', help=help, default='') + + help = 'Directorio destino.' + parser.add_argument('-d', '--directorio-destino', help=help, default='') + + help = 'Plantilla CSV.' + parser.add_argument('-p', '--plantilla', help=help, default='') + + args = parser.parse_args() + return args + + +def main(): + ext_pdf = '.pdf' + args = process_command_line_arguments() + util = Util() + + if args.archivo_xml and util.exists(args.archivo_xml): + if args.directorio_destino: + info = util.get_path_info(args.archivo_xml) + path_pdf = util.join(args.directorio_destino, info[0][len(args.directorio_origen)+1:]) + util.makedirs(path_pdf) + path_pdf = util.join(path_pdf, info[2] + ext_pdf) + else: + path_pdf = util.replace_extension(args.archivo_xml, ext_pdf) + pdf = CSVPDF(args.archivo_xml, args.plantilla) + if pdf.xml: + pdf.make_pdf() + pdf.output(path_pdf, 'F') + sys.exit(0) + if args.directorio_origen: + files = util.get_files(args.directorio_origen) + for f in files: + path_pdf = util.replace_extension(f, ext_pdf) + pdf = CSVPDF(f, args.plantilla) + if pdf.xml: + pdf.make_pdf() + pdf.output(path_pdf, 'F') + +if __name__ == '__main__': + main() diff --git a/cfdi2pdf.cmd b/cfdi2pdf.cmd new file mode 100644 index 0000000..ce6bcfe --- /dev/null +++ b/cfdi2pdf.cmd @@ -0,0 +1,4 @@ +@echo off +set PYTHONIOENCODING=utf-8 +set c2p_path="%~d0\%~p0cfdi2pdf" +py %c2p_path% %* diff --git a/contributors.txt b/contributors.txt new file mode 100644 index 0000000..10033b5 --- /dev/null +++ b/contributors.txt @@ -0,0 +1,5 @@ +Mauricio Baeza +Miguel Trujillo +Patricio Paez +Sergio Gutierrez +Jonathan Lopez diff --git a/descarga-cfdi b/descarga-cfdi new file mode 100755 index 0000000..5b8eabe --- /dev/null +++ b/descarga-cfdi @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +#! coding: utf-8 + +# 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, 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 MERCHANTIBILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. + + +import argparse +import time +import datetime +import os +import getpass +import sys + +from admincfdi.pyutil import DescargaSAT +from admincfdi.pyutil import Util + + +def process_command_line_arguments(): + parser = argparse.ArgumentParser(description='Descarga CFDIs del SAT a una carpeta local') + + default_archivo_credenciales = 'credenciales.conf' + help = 'Archivo con credenciales para el SAT. ' \ + 'RFC y CIEC en el primer renglón y ' \ + 'separadas por un espacio. ' \ + 'El predeterminado es %(default)s' + parser.add_argument('--archivo-de-credenciales', + help=help, default=default_archivo_credenciales) + + help = 'Solicitar credenciales para el SAT al inicio. ' + parser.add_argument('--solicitar-credenciales', + action='store_const', const=True, + help=help, default=False) + + home_path = os.environ.get('HOME') + if not home_path: + home_path = os.environ.get('HOMEPATH', '') + default_carpeta_destino = os.path.join( + home_path, 'cfdi-descarga') + help = 'Carpeta local para guardar los CFDIs descargados ' \ + 'El predeterminado es %(default)s' + parser.add_argument('--carpeta-destino', + help=help, default=default_carpeta_destino) + + help = 'Descargar facturas emitidas. ' \ + 'Por omisión se descargan facturas recibidas' + parser.add_argument('--facturas-emitidas', + action='store_const', const=True, + help=help, default=False) + + help = 'UUID. Por omisión no se usa en la búsqueda. ' \ + 'Esta opción tiene precedencia sobre las demás ' \ + 'opciones de búsqueda.' + parser.add_argument('--uuid', + help=help, default='') + + help = 'RFC del emisor. Por omisión no se usa en la búsqueda.' + parser.add_argument('--rfc-emisor', + help=help, default='') + + today = datetime.date.today() + help = 'Año. El valor por omisión es el año en curso' + parser.add_argument('--año', + help=help, default=str(today.year)) + + help = 'Mes. El valor por omisión es el mes en curso' + parser.add_argument('--mes', + help=help, default='{:02d}'.format(today.month)) + + help = 'Día. Por omisión no se usa en la búsqueda.' + parser.add_argument('--día', + help=help, default='00') + + help = 'Hora inicial. Por omisión es 0.' + parser.add_argument('--hora-inicial', + help=help, default='0') + + help = 'Minuto inicial. Por omisión es 0.' + parser.add_argument('--minuto-inicial', + help=help, default='0') + + help = 'Segundo inicial. Por omisión es 0.' + parser.add_argument('--segundo-inicial', + help=help, default='0') + + help = 'Hora final. Por omisión es 23.' + parser.add_argument('--hora-final', + help=help, default='23') + + help = 'Minuto final. Por omisión es 59.' + parser.add_argument('--minuto-final', + help=help, default='59') + + help = 'Segundo final. Por omisión es 59.' + parser.add_argument('--segundo-final', + help=help, default='59') + + help = 'Mes completo por día. Por omisión no se usa en la búsqueda.' + parser.add_argument('--mes-completo-por-día', action='store_const', const=True, + help=help, default=False) + + args=parser.parse_args() + return args + +def main(): + + args = process_command_line_arguments() + if args.solicitar_credenciales: + rfc = input('RFC: ') + pwd = getpass.getpass('CIEC: ') + else: + util = Util() + status, rfc, pwd = util.lee_credenciales(args.archivo_de_credenciales) + if status != 'Ok': + print(status) + sys.exit(1) + descarga = DescargaSAT() + profile = descarga.get_firefox_profile(args.carpeta_destino) + try: + descarga.connect(profile, rfc=rfc, ciec=pwd) + docs = descarga.search(facturas_emitidas= args.facturas_emitidas, + uuid=args.uuid, + rfc_emisor=args.rfc_emisor, + año=args.año, + mes=args.mes, + día=args.día, + hora_inicial=args.hora_inicial, + minuto_inicial=args.minuto_inicial, + segundo_inicial=args.segundo_inicial, + hora_final=args.hora_final, + minuto_final=args.minuto_final, + segundo_final=args.segundo_final, + mes_completo_por_día=args.mes_completo_por_día) + descarga.download(docs) + except Exception as e: + print (e) + finally: + descarga.disconnect() + + +if __name__ == '__main__': + main() diff --git a/descarga-cfdi.cmd b/descarga-cfdi.cmd new file mode 100755 index 0000000..7cdb918 --- /dev/null +++ b/descarga-cfdi.cmd @@ -0,0 +1,4 @@ +@echo off +set PYTHONIOENCODING=utf-8 +set dc_path="%~d0\%~p0descarga-cfdi" +py %dc_path% %* diff --git a/docs/conf.py b/docs/conf.py index 685c8b7..ff9065b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ @@ -29,7 +29,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [] +extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -45,7 +45,7 @@ # General information about the project. project = 'admin-cfdi' -copyright = '2015, Mauricio Baeza' +copyright = '2015, Python Cabal' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -111,7 +111,7 @@ # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -html_title = "Documentación de {} v{}".format(project, release) +html_title = "Documentacion de {} v{}".format(project, release) # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = html_title diff --git a/docs/devel.rst b/docs/devel.rst index d01bdf1..c2c89fb 100644 --- a/docs/devel.rst +++ b/docs/devel.rst @@ -1,3 +1,325 @@ ========== Desarrollo ========== +Este capítulo contiene información útil para quienes desean +desarrollar aplicaciones que trabajen con CFDIs, para lo +cual pueden usar una o más de las clases disponibles dentro +del paquete `admincfdi`. + + +Estructura +========== + +El paquete `admincfdi` incluye los siguientes módulos: + +- `pyutil` Tiene varias clases que implementan + las funcionalidades usadas por las aplicaciones. + +- `values` Tiene la clase Global que centraliza + valores que se usan en los otros módulos. Por + ejemplo, las URLs y valores id de la página web + de CFDIs del SAT están en el atributo SAT, + es un diccionario que es usado + en la descarga de CFDIs. + +admin-cfdi +========== + +El botón ``Descargar`` está ligado al método +:func:`admin-cfdi.Application.button_download_sat_click` +de la aplicación, que ejecuta +estos dos métodos: + +- :func:`admin-cfdi.Application._validate_download_sat` + +- :func:`admin-cfdi.Application._download_sat` + +Descarga de facturas del SAT +============================ + +El proceso de descarga mediante la aplicación de CFDIs +del SAT consiste en estos pasos: + +#. Conectar +#. Buscar +#. Descargar +#. Desconectar + +Los detalles de cada paso: + +#. Conectar + + - Lanzar el navegador + - Navegar a la página de login de CFDIs + - Llenar el usuario y la contraseña (RFC y CIEC) + - Enviar los datos al servidor + - Esperar la respuesta + - En caso de éxito, se carga una página con el título + *NetIQ Access Manager* + - En caso de falla, un elemento ``div`` con id ``xacerror`` + deja de estar oculto y muestra su texto que empieza + con *El RFC o contraseña son incorrectos.* + +#. Buscar + + - Navegar a la página de búsqueda de facturas emitidas, + o a la de facturas recibidas + - Esperar a que el título cambie a *Buscar CFDI* + - Llenar los datos de la búsqueda + - Si la búsqueda es por UUID, llenar el UUID en + el input con id ``ctl00_MainContent_TxtUUID``. + - Si la búsqueda es por fecha: + - Hacer clic en el botón de radio a la izquierda + de Fecha de Emisión con id + ``ctl00_MainContent_RdoFechas``. + - Esperar a que el input a la derecha de RFC Emisor + con id ``ctl00_MainContent_TxtRfcReceptor`` + esté habilitado y se pueda hacer clic en él. + - Si se buscan facturas emitidas: + - Habilitar los inputs con id + + - ``ctl00_MainContent_CldFechaInicial2_Calendario_text`` + - ``ctl00_MainContent_CldFechaFinal2_Calendario_text`` + + y asignar valor de fecha inicial y fecha final de emisión + usando formato ``dd/mm/aaaa`` + - Asignar a los selects no visibles de tiempo final con ids + + - ``ctl00_MainContent_CldFechaFinal2_DdlHora`` + - ``ctl00_MainContent_CldFechaFinal2_DdlMinuto`` + - ``ctl00_MainContent_CldFechaFinal2_DdlSegundo`` + + una cadena con un valor en el rango que corresponde + respectivamente: 1 a 23, 1 a 59 y 1 a 59. + No usar ceros a la izquierda para valores menores + a 10: usar 5, no 05. Por omisión hay que llenar cada + select con el valor máximo correspondiente. + - Se se buscan facturas recibidas: + - Asignar a los selects no visibles con ids + + - ``DdlAnio`` + - ``ctl00_MainContent_CldFecha_DdlMes`` + - ``ctl00_MainContent_CldFecha_DdlDia`` + - ``ctl00_MainContent_CldFecha_DdlHora`` + - ``ctl00_MainContent_CldFecha_DdlMinuto`` + - ``ctl00_MainContent_CldFecha_DdlSegundo`` + - ``ctl00_MainContent_CldFecha_DdlHoraFin`` + - ``ctl00_MainContent_CldFecha_DdlMinutoFin`` + - ``ctl00_MainContent_CldFecha_DdlSegundoFin`` + + los valores de los parámetros año, mes, día, + hora_inicial, minuto_inicial, segundo_inicial, + hora_final, minuto_final y segundo_final respectivamente. + Los valores de horas, minutos y segundos + no deben llevar 0 a la izquierda. El valor de + día requiere 0 a la izquierda para valores menores + a 10. + + - Enviar la forma de búsqueda al servidor mediante método POST, los + datos que se envían se muestran más bajo. + + - Esperar a que no sea visible el elemento div de los + resultados, o el botón mismo de enviar + - Esperar a que uno de los dos div con id + ``ctl00_MainContent_PnlResultados`` o id + ``ctl00_MainContent_PnlNoResultados`` esté + visible. + - Si el div con id ``ctl00_MainContent_PnlResultados`` + es visible: + + - Esperar que un elemento con name ``BtnDescarga`` + se le pueda hacer clic + - Encontrar la lista todos los elementos con name + ``BtnDescarga``. Son los íconos + de descarga a la izquierda en cada renglón. + + - La lista de resultados está paginada en 500 elementos. + Si los + resultados son más de 500, una opción es dividir + la búsqueda en dos o más búsquedas + en las que se agregan criterios: La búsqueda de un + mes se puede dividir en búsquedas por día; la + búsqueda de un día puede dividirse en búsquedas en + un rango de horas en ese día. + + + +#. Descargar + + - Iterar en cada elemento de la lista + de resultados: + + - Concatenar la URL base + de CFDIs con el valor del atributo ``onclick`` + del elemento + - Hacer la solicitud GET a esta URL + +#. Desconectar + - Cerrar la sesión + - Cerrar el navegador. Este paso se realiza + a pesar de que ocurra una falla en el paso + anterior. + +En caso de alguna falla en los primeros tres pasos, +la aplicación debe realizar el paso 4. + +Los datos que se envían por la forma de búsqueda de facturas recibidas: + + - ctl00$ScriptManager1=ctl00$MainContent$UpnlBusqueda|ctl00$MainContent$BtnBusqueda + - __CSRFTOKEN=%2FwEFJGNjZmIzNzZmLTE4OWUtNDQwNS1iNmZiLWU2NWE4MDQ0Y2EwZA%3D%3D + - ctl00$MainContent$TxtUUID= + - ctl00$MainContent$FiltroCentral=RdoFechas + - ctl00$MainContent$CldFecha$DdlAnio=2014 + - ctl00$MainContent$CldFecha$DdlMes=1 + - ctl00$MainContent$CldFecha$DdlDia=0 + - ctl00$MainContent$CldFecha$DdlHora=0 + - ctl00$MainContent$CldFecha$DdlMinuto=0 + - ctl00$MainContent$CldFecha$DdlSegundo=0 + - ctl00$MainContent$CldFecha$DdlHoraFin=23 + - ctl00$MainContent$CldFecha$DdlMinutoFin=59 + - ctl00$MainContent$CldFecha$DdlSegundoFin=59 + - ctl00$MainContent$TxtRfcReceptor= + - ctl00$MainContent$DdlEstadoComprobante=-1 + - ctl00$MainContent$hfInicialBool=false + - ctl00$MainContent$ddlComplementos=-1 + - __EVENTTARGET= + - __EVENTARGUMENT= + - __LASTFOCUS= + - __VIEWSTATE= + - __VIEWSTATEGENERATOR=FE9DB3F4 + - __VIEWSTATEENCRYPTED= + - __ASYNCPOST=true + - ctl00$MainContent$BtnBusqueda=Buscar CFDI + +Los datos que se envían por la forma de búsqueda de facturas emitidas: + + - ctl00$ScriptManager1=ctl00$MainContent$UpnlBusqueda|ctl00$MainContent$BtnBusqueda + - __CSRFTOKEN=%2FwEFJGNjZmIzNzZmLTE4OWUtNDQwNS1iNmZiLWU2NWE4MDQ0Y2EwZA%3D%3D + - ctl00$MainContent$TxtUUID= + - ctl00$MainContent$FiltroCentral=RdoFechas + - ctl00$MainContent$hfInicial=2015 + - ctl00$MainContent$hfInicialBool=false + - ctl00$MainContent$CldFechaInicial2$Calendario_text=18/05/2015 + - ctl00$MainContent$CldFechaInicial2$DdlHora=0 + - ctl00$MainContent$CldFechaInicial2$DdlMinuto=0 + - ctl00$MainContent$CldFechaInicial2$DdlSegundo=0 + - ctl00$MainContent$hfFinal=2015 + - ctl00$MainContent$CldFechaFinal2$Calendario_text=19/05/2015 + - ctl00$MainContent$CldFechaFinal2$DdlHora=23 + - ctl00$MainContent$CldFechaFinal2$DdlMinuto=59 + - ctl00$MainContent$CldFechaFinal2$DdlSegundo=59 + - ctl00$MainContent$TxtRfcReceptor= + - ctl00$MainContent$DdlEstadoComprobante=-1 + - ctl00$MainContent$ddlComplementos=-1 + - ctl00$MainContent$hfDatos= + - ctl00$MainContent$hfFlag= + - ctl00$MainContent$hfAux= + - __EVENTTARGET= + - __EVENTARGUMENT= + - __LASTFOCUS= + - __VIEWSTATE= + - __VIEWSTATEGENERATOR=3D1378D8 + - __VIEWSTATEENCRYPTED= + - __ASYNCPOST=true + - ctl00$MainContent$BtnBusqueda=Buscar CFDI + + + +API +=== +El módulo :mod:`admincfdi.pyutil` provee varias clases, las cuales +pueden ser usadas por las aplicaciones. En las siguientes +secciones se explican y dan ejemplos de uso cada una de estas clases. + + +SAT +--- + +ValidCFDI +--------- + +Util +---- + +Mail +---- + +LibO +---- + +NumerosLetras +------------- + +CFDIPDF +------- + +DescargaSAT +----------- +Lleva a cabo al descarga de CFDIs del sitio del SAT. Para descargar +un conjunto de CFDIs con ciertos criterios de búsqueda, se +utilizan los siguientes pasos: + +#. Instanciar :class:`~admincfdi.pyutil.DescargaSAT`:: + + descarga = DescargaSAT() + +#. Crear un perfil de Firefox:: + + profile = descarga.get_firefox_profile(carpeta_destino) + +#. Conectar al sitio del SAT, lanzando Firefox:: + + descarga.connect(profile, rfc=rfc, ciec=pwd) + +#. Realizar una búsqueda, guardando la lista de resultados + obtenida:: + + docs = descarga.search(facturas_emitidas=facturas_emitidas, + uuid=uuid, + rfc_emisor=rfc_emisor, + año=año, + mes=mes, + día=día, + mes_completo_por_día=mes_completo_por_día) + +#. Descargar los CFDIs:: + + descarga.download(docs) + +#. Desconectar la sesión del sitio del SAT y terminar + Firefox:: + + descarga.disconnect() + +Los pasos 4. de búsqueda y 5. de descarga pueden repetirse, si +se desean descargar dos o más conjuntos de CFDIs con diferentes +criterios de búsqueda, manteniendo la sesión original abierta. + +Como ejemplo, a continuación se muestra el uso de los +pasos en las aplicaciones ``admin-cfdi`` y ``descarga-cfdi`` +que son parte del proyecto:: + + descarga = DescargaSAT() + profile = descarga.get_firefox_profile(args.carpeta_destino) + try: + descarga.connect(profile, rfc=rfc, ciec=pwd) + docs = descarga.search(facturas_emitidas= args.facturas_emitidas, + uuid=args.uuid, + rfc_emisor=args.rfc_emisor, + año=args.año, + mes=args.mes, + día=args.día, + mes_completo_por_día=args.mes_completo_por_día) + descarga.download(docs) + except Exception as e: + print (e) + finally: + descarga.disconnect() + +Las cláusulas ``try/except/finally`` son para manejar alguna +excepción que ocurra en cualquiera de los pasos, y garantizar +que en cualquier caso se hace la desconexión de la sesión +y se termina Firefox. + +CSVPDF +------ diff --git a/docs/glosario.rst b/docs/glosario.rst index 495a527..8bb6c80 100644 --- a/docs/glosario.rst +++ b/docs/glosario.rst @@ -8,7 +8,7 @@ Glosario Un administrador de documentos CFDI CFDI - Certificado de Firma Digital Integrada + Comprobante Fiscal Digital por Internet SAT Servicio de Administración Tributaria diff --git a/docs/img/admin-cfdi-ventana-ppal.png b/docs/img/admin-cfdi-ventana-ppal.png new file mode 100644 index 0000000..704128b Binary files /dev/null and b/docs/img/admin-cfdi-ventana-ppal.png differ diff --git a/docs/img/ejemplo-pdf.png b/docs/img/ejemplo-pdf.png new file mode 100644 index 0000000..52f49da Binary files /dev/null and b/docs/img/ejemplo-pdf.png differ diff --git a/docs/index.rst b/docs/index.rst index e7f8d95..89c368d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,8 +12,10 @@ Contents: :maxdepth: 2 intro + install uso devel + reference glosario diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..d4de5c1 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,66 @@ +=========== +Instalación +=========== + +Para instalar :term:`admin-cfdi` descarga la ultima versión de producción desde +`Github`_ e instala con el comando. + +:: + + sudo python setup.py install + +Si lo prefieres usa un entorno virtual. + +#. Para `LinuxMint`_ + + * Crea el entorno virtual + + :: + + pyvenv-3.4 test_admin --without-pip + + * Activalo + + :: + + cd test_admin/ + source bin/activate + + * Instala pip + + :: + + wget https://bootstrap.pypa.io/get-pip.py + python get-pip.py + + * Instala :term:`admin-cfdi` + + :: + + python setup.py install + +#. Para `ArchLinux`_ + + * Crea el entorno virtual + + :: + + pyvenv test_admin + + * Activalo + + :: + + cd test_admin/ + source bin/activate + + * Instala :term:`admin-cfdi` + + :: + + python setup.py install + + +.. _Github: https://github.com/LinuxCabal/admin-cfdi +.. _LinuxMint: http://linuxmint.com/ +.. _ArchLinux: https://www.archlinux.org/ diff --git a/docs/intro.rst b/docs/intro.rst index b0d6ce9..5e82383 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -2,6 +2,79 @@ Introducción ============ -:term:`admin-cfdi` es una aplicación para descargar -y administrar documentos :term:`CFDI` emitidos por el -:term:`SAT`. +:term:`admin-cfdi` es una aplicación de escritorio desarrollada en `Python`_ para descargar +documentos :term:`CFDI` (facturas electrónicas) directamente del :term:`SAT`, +permite también, descargar CFDIs desde correos electrónicos, validarlos y administrarlos. + +.. _Python: http://python.org/ + +.. image:: img/admin-cfdi-ventana-ppal.png + :align: center + :width: 714 + :height: 762 + :scale: 50 % + :alt: Haz clic para agrandar + +`admin-cfdi` es `software libre`_ bajo la licencia `GNU GPL 3`_ y corre en GNU/Linux y +otros sistemas operativos. +Se tienen también dos aplicaciones de línea de comando que permiten +automatizar operaciones mediante un script: + +.. _software libre: https://www.gnu.org/philosophy/free-sw.es.html +.. _GNU GPL 3: https://www.gnu.org/licenses/gpl.html + +``descarga-cfdi`` descarga CFDIs que cumplan determinado criterio, por +ejemplo todos los CFDIs recibidos en el mes de enero de 2015:: + + descarga-cfdi --año 2015 --mes 01 + + Abriendo Firefox... + Conectando... + Conectado... + Buscando... + Factura 1 de 13 + Factura 2 de 13 + Factura 3 de 13 + Factura 4 de 13 + Factura 5 de 13 + Factura 6 de 13 + Factura 7 de 13 + Factura 8 de 13 + Factura 9 de 13 + Factura 10 de 13 + Factura 11 de 13 + Factura 12 de 13 + Factura 13 de 13 + Desconectando... + Desconectado.. + +Los CFDIs se guardan por omisión en la carpeta `cfdi-descarga`:: + + 12FB2D4B-CAE0-41CF-B344-13FE5135C773.xml 5A5108B2-2171-49B0-86D4-539DD205786A.xml CB969AF4-0E13-441B-9CC7-0AA11831317F.xml + 1FBFA93D-F171-0B0E-CF71-4216C214E66F.xml 61F50926-7C47-4269-B612-3777881050A4.xml F1ABE4CE-9444-4F77-A3E5-57A6559F6CB3.xml + 2968F314-90D6-4000-BBA5-E17988F2870F.xml 79FE35B0-636E-4163-8BA2-38E053E97E4C.xml FF31423C-E1BC-4A3D-9A7B-472FFE9988F9.xml + 2CF33F44-2E2A-4F4C-904C-6213D3E8F12C.xml + +``cfdi2pdf`` convierte los CFDIs de una carpeta origen a formato PDF:: + + cfdi2pdf -o cfdi-descarga/ -d cfdi-pdf/ + + Generando: ../cfdi-descarga/2CF33F44-2E2A-4F4C-904C-6213D3E8F12C.xml + Generando: ../cfdi-descarga/79FE35B0-636E-4163-8BA2-38E053E97E4C.xml + Generando: ../cfdi-descarga/61F50926-7C47-4269-B612-3777881050A4.xml + Generando: ../cfdi-descarga/1FBFA93D-F171-0B0E-CF71-4216C214E66F.xml + Generando: ../cfdi-descarga/F1ABE4CE-9444-4F77-A3E5-57A6559F6CB3.xml + Generando: ../cfdi-descarga/2968F314-90D6-4000-BBA5-E17988F2870F.xml + Generando: ../cfdi-descarga/FF31423C-E1BC-4A3D-9A7B-472FFE9988F9.xml + Generando: ../cfdi-descarga/CB969AF4-0E13-441B-9CC7-0AA11831317F.xml + Generando: ../cfdi-descarga/5A5108B2-2171-49B0-86D4-539DD205786A.xml + Generando: ../cfdi-descarga/12FB2D4B-CAE0-41CF-B344-13FE5135C773.xml + +Un ejemplo de los archivos PDF generados: + +.. image:: img/ejemplo-pdf.png + :align: center + :width: 1366 + :height: 768 + :scale: 50 % + :alt: Haz clic para agrandar diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 0000000..7ff63dc --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,27 @@ +========== +Referencia +========== + +admincfdi +--------- + +.. automodule:: admincfdi + :members: + :undoc-members: + :private-members: + +pyutil +------ + +.. automodule:: admincfdi.pyutil + :members: + :undoc-members: + :private-members: + +values +------ + +.. automodule:: admincfdi.values + :members: + :undoc-members: + :private-members: diff --git a/docs/uso.rst b/docs/uso.rst index 93e46dc..cb7e177 100644 --- a/docs/uso.rst +++ b/docs/uso.rst @@ -1,3 +1,131 @@ === Uso === + +Aplicaciones +------------ +Admincfdi incluye las siguientes aplicaciones: + +- `admin-cfdi` + +- `descarga-cfdi` + +- `cfdi2pdf` + +`admin-cfdi` es una aplicación gráfica, `descarga-cfdi` +y `cfdi2pdf` son aplicaciones de línea de comando. + +admin-cfdi +========== + +La descarga de los archivos XML del sitio web del SAT se +maneja en la primera pestaña de la interfase gráfica. + +Primeramente el usuario debe llenar +datos y/o seleccionar opciones en estos tres apartados: + +- Datos de acceso +- Tipo de consulta +- Opciones de búsqueda + +El proceso de la descarga se inicia mediante el botón +``Descargar``. + +descarga-cfdi +============= + +El avance del proceso se indica al usuario mediante +textos cortos que se muestran en una línea de estado +de la interfase gráfica, en esta secuencia:: + + Abriendo Firefox... + Conectando... + Conectado... + Buscando... + Factura 1 de 12 + Factura 2 de 12 + Factura 3 de 12 + Factura 4 de 12 + Factura 5 de 12 + Factura 6 de 12 + Factura 7 de 12 + Factura 8 de 12 + Factura 9 de 12 + Factura 10 de 12 + Factura 11 de 12 + Factura 12 de 12 + Desconectando... + Desconectado... + + +Pruebas funcionales de descarga del SAT +--------------------------------------- +Estas pruebas sirven para varios propósitos: + +- Saber si el sitio del SAT esta funcionando + normalmente, + +- Saber si nuestra conexión entre la PC + y el sito del SAT está funcionando y si + su desempeño es el esperado, + +- Saber si el sitio del SAT cambió su + funcionamiento del tal forma que sea + necesario actualizar la librería de + descarga de admincfdi. + +Las pruebas realizan descargas mediante +varios modos de búsqueda y validan +que la cantidad de archivos descargados +sea la esperada. No requieren interacción +mientas corren. + +Es necesario crear un archivo de credenciales y un archivo de +configuración para las pruebas. El archivo de configuración +especifica los criterios de cada búsqueda. Este es un ejemplo:: + + [uuid] + uuid=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + expected=1 + + [rfc_emisor] + rfc_emisor=XXXXXXXXXXXX + año=2014 + mes=09 + día=26 + expected=1 + + [año_mes_día] + año=2014 + mes=09 + día=26 + expected=1 + + [mes_completo_por_día] + año=2014 + mes=09 + expected=5 + +Se necesitan estas cuatro secciones. Hay que ajustar los +valores para que la cantidad de CFDIs no sea muy grande. La +variable ``expected`` se ajusta a la cantidad de CFDIs que se +descargan, para las credenciales que se utilicen. + +Para ejecutar:: + + python functional_DescargaSAT.py + .... + ---------------------------------------------------------------------- + Ran 4 tests in 254.376s + +Agregar el parámetro ``-v`` para tener un renglón por +cada prueba que se ejecuta:: + + python functional_DescargaSAT.py -v + test_año_mes_día (__main__.DescargaSAT) ... ok + test_mes_completo (__main__.DescargaSAT) ... ok + test_rfc (__main__.DescargaSAT) ... ok + test_uuid (__main__.DescargaSAT) ... ok + + ---------------------------------------------------------------------- + Ran 4 tests in 254.376s diff --git a/functional_DescargaSAT.conf.sample b/functional_DescargaSAT.conf.sample new file mode 100644 index 0000000..83f31cc --- /dev/null +++ b/functional_DescargaSAT.conf.sample @@ -0,0 +1,21 @@ +[uuid] +uuid=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +expected=1 + +[rfc_emisor] +rfc_emisor=XXXXXXXXXXXX +año=2014 +mes=09 +día=26 +expected=1 + +[año_mes_día] +año=2014 +mes=09 +día=26 +expected=1 + +[mes_completo_por_día] +año=2014 +mes=09 +expected=5 diff --git a/functional_DescargaSAT.py b/functional_DescargaSAT.py new file mode 100755 index 0000000..8273567 --- /dev/null +++ b/functional_DescargaSAT.py @@ -0,0 +1,275 @@ +import unittest + + +class DescargaSAT(unittest.TestCase): + + def setUp(self): + import configparser + from admincfdi.pyutil import Util + + self.config = configparser.ConfigParser() + self.config.read('functional_DescargaSAT.conf' ) + + util = Util() + status, self.rfc, self.ciec = util.lee_credenciales('credenciales.conf') + self.assertEqual('Ok', status) + + def test_connect_disconnect(self): + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + + def no_op(*args): + pass + + descarga = DescargaSAT(status_callback=no_op) + status = descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + self.assertTrue(status) + descarga.disconnect() + + def test_connect_fail(self): + from admincfdi.pyutil import DescargaSAT + from selenium import webdriver + + profile = webdriver.FirefoxProfile() + + def no_op(*args): + pass + + descarga = DescargaSAT(status_callback=no_op) + status = descarga.connect(profile, rfc='x', ciec='y') + self.assertFalse(status) + descarga.browser.close() + + def test_search_uuid(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['uuid'] + uuid = seccion['uuid'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + profile = descarga.get_firefox_profile('destino') + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + result = descarga.search(uuid=uuid, + día='00') + descarga.disconnect() + self.assertEqual(expected, len(result)) + + def test_uuid(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['uuid'] + uuid = seccion['uuid'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + with tempfile.TemporaryDirectory() as tempdir: + destino = os.path.join(tempdir, 'cfdi-descarga') + profile = descarga.get_firefox_profile(destino) + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + docs = descarga.search(uuid=uuid, día='00') + descarga.download(docs) + descarga.disconnect() + self.assertEqual(expected, len(os.listdir(destino))) + + def test_rfc(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['rfc_emisor'] + rfc_emisor = seccion['rfc_emisor'] + año = seccion['año'] + mes = seccion['mes'] + día = seccion['día'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + with tempfile.TemporaryDirectory() as tempdir: + destino = os.path.join(tempdir, 'cfdi-descarga') + profile = descarga.get_firefox_profile(destino) + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + docs = descarga.search(año=año, mes=mes, día=día, + rfc_emisor=rfc_emisor) + descarga.download(docs) + descarga.disconnect() + self.assertEqual(expected, len(os.listdir(destino))) + + def test_año_mes_día(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['año_mes_día'] + año = seccion['año'] + mes = seccion['mes'] + día = seccion['día'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + with tempfile.TemporaryDirectory() as tempdir: + destino = os.path.join(tempdir, 'cfdi-descarga') + profile = descarga.get_firefox_profile(destino) + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + docs = descarga.search(año=año, mes=mes, día=día) + descarga.download(docs) + descarga.disconnect() + self.assertEqual(expected, len(os.listdir(destino))) + + def test_hora_minuto_inicial(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['hora_minuto_inicial'] + año = seccion['año'] + mes = seccion['mes'] + día = seccion['día'] + hora_inicial = seccion['hora_inicial'] + minuto_inicial = seccion['minuto_inicial'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + with tempfile.TemporaryDirectory() as tempdir: + destino = os.path.join(tempdir, 'cfdi-descarga') + profile = descarga.get_firefox_profile(destino) + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + docs = descarga.search(año=año, mes=mes, día=día, + hora_inicial=hora_inicial, + minuto_inicial=minuto_inicial) + descarga.download(docs) + descarga.disconnect() + self.assertEqual(expected, len(os.listdir(destino))) + + def test_hora_minuto_final(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['hora_minuto_final'] + año = seccion['año'] + mes = seccion['mes'] + día = seccion['día'] + hora_final = seccion['hora_final'] + minuto_final = seccion['minuto_final'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + with tempfile.TemporaryDirectory() as tempdir: + destino = os.path.join(tempdir, 'cfdi-descarga') + profile = descarga.get_firefox_profile(destino) + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + docs = descarga.search(año=año, mes=mes, día=día, + hora_final=hora_final, minuto_final=minuto_final) + descarga.download(docs) + descarga.disconnect() + self.assertEqual(expected, len(os.listdir(destino))) + + def test_segundo_inicial_final(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['segundo_inicial_final'] + año = seccion['año'] + mes = seccion['mes'] + día = seccion['día'] + hora_inicial = seccion['hora_inicial'] + minuto_inicial = seccion['minuto_inicial'] + segundo_inicial = seccion['segundo_inicial'] + hora_final = seccion['hora_final'] + minuto_final = seccion['minuto_final'] + segundo_final = seccion['segundo_final'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + with tempfile.TemporaryDirectory() as tempdir: + destino = os.path.join(tempdir, 'cfdi-descarga') + profile = descarga.get_firefox_profile(destino) + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + docs = descarga.search(año=año, mes=mes, día=día, + hora_inicial=hora_inicial, minuto_inicial=minuto_inicial, + segundo_inicial=segundo_inicial, + hora_final=hora_final, minuto_final=minuto_final, + segundo_final=segundo_final) + descarga.download(docs) + descarga.disconnect() + self.assertEqual(expected, len(os.listdir(destino))) + + def test_mes_completo(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['mes_completo_por_día'] + año = seccion['año'] + mes = seccion['mes'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + with tempfile.TemporaryDirectory() as tempdir: + destino = os.path.join(tempdir, 'cfdi-descarga') + profile = descarga.get_firefox_profile(destino) + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + docs = descarga.search(año=año, mes=mes, día='00', + mes_completo_por_día=True) + descarga.download(docs) + descarga.disconnect() + self.assertEqual(expected, len(os.listdir(destino))) + + def test_emitidas(self): + import os + import tempfile + from admincfdi.pyutil import DescargaSAT + + def no_op(*args): + pass + + seccion = self.config['emitidas'] + año = seccion['año'] + mes = seccion['mes'] + expected = int(seccion['expected']) + descarga = DescargaSAT(status_callback=no_op, + download_callback=no_op) + with tempfile.TemporaryDirectory() as tempdir: + destino = os.path.join(tempdir, 'cfdi-descarga') + profile = descarga.get_firefox_profile(destino) + descarga.connect(profile, rfc=self.rfc, ciec=self.ciec) + docs = descarga.search(año=año, mes=mes, día='00', + facturas_emitidas=True) + descarga.disconnect() + self.assertEqual(expected, len(docs)) + + +if __name__ == '__main__': + unittest.main() diff --git a/pwd.sample b/pwd.sample new file mode 100644 index 0000000..273bf02 --- /dev/null +++ b/pwd.sample @@ -0,0 +1,3 @@ +rfc ciec +# escribe en el renglón de arriba tu RFC y tu CIEC +# separados por un espacio diff --git a/pyqrcode/__init__.py b/pyqrcode/__init__.py deleted file mode 100644 index 97978b8..0000000 --- a/pyqrcode/__init__.py +++ /dev/null @@ -1,322 +0,0 @@ -"""This module is used to create QR Codes. It is designed to be as simple and -as possible. It does this by using sane defaults and autodetection to make -creating a QR Code very simple. - -It is recommended that you use the :func:`pyqrcode.create` function to build the -QRCode object. This results in cleaner looking code. - -Examples: - >>> import pyqrcode - >>> import sys - >>> url = pyqrcode.create('http://uca.edu') - >>> url.svg(sys.stdout, scale=1) - >>> url.svg('uca.svg', scale=4) - >>> number = pyqrcode.create(123456789012345) - >>> number.png('big-number.png') -""" -import pyqrcode.tables -import pyqrcode.builder as builder - -def create(content, error='H', version=None, mode=None): - """When creating a QR code only the content to be encoded is required, - all the other properties of the code will be guessed based on the - contents given. This function will return a :class:`QRCode` object. - - Unless you are familiar with QR code's inner workings - it is recommended that you just specify the content and nothing else. - However, there are cases where you may want to specify the various - properties of the created code manually, this is what the other - parameters do. Below, you will find a lengthy explanation of what - each parameter is for. Note, the parameter names and values are taken - directly from the standards. You may need to familiarize yourself - with the terminology of QR codes for the names to make sense. - - The *error* parameter sets the error correction level of the code. There - are four levels defined by the standard. The first is level 'L' which - allows for 7% of the code to be corrected. Second, is level 'M' which - allows for 15% of the code to be corrected. Next, is level 'Q' which - is the most common choice for error correction, it allow 25% of the - code to be corrected. Finally, there is the highest level 'H' which - allows for 30% of the code to be corrected. There are several ways to - specify this parameter, you can use an upper or lower case letter, - a float corresponding to the percentage of correction, or a string - containing the percentage. See tables.modes for all the possible - values. By default this parameter is set to 'H' which is the highest - possible error correction, but it has the smallest available data - capacity. - - The *version* parameter specifies the size and data capacity of the - code. Versions are any integer between 1 and 40. Where version 1 is - the smallest QR code, and version 40 is the largest. If this parameter - is left unspecified, then the contents and error correction level will - be used to guess the smallest possible QR code version that the - content will fit inside of. You may want to specify this parameter - for consistency when generating several QR codes with varying amounts - of data. That way all of the generated codes would have the same size. - - The *mode* parameter specifies how the contents will be encoded. By - default, the best possible encoding for the contents is guessed. There - are four possible encoding methods. First, is 'numeric' which is - used to encode integer numbers. Next, is 'alphanumeric' which is - used to encode some ASCII characters. This mode uses only a limited - set of characters. Most problematic is that it can only use upper case - English characters, consequently, the content parameter will be - subjected to str.upper() before encoding. See tables.ascii_codes for - a complete list of available characters. We then have 'binary' encoding - which just encodes the bytes directly into the QR code (this encoding - is the least efficient). Finally, there is 'kanji' encoding (i.e. - Japanese characters), this encoding is unimplemented at this time. - """ - return QRCode(content, error, version, mode) - -class QRCode: - """This class represents a QR code. To use this class simply give the - constructor a string representing the data to be encoded, it will then - build a code in memory. You can then save it in various formats. Note, - codes can be written out as PNG files but this requires the PyPNG module. - You can find the PyPNG module at http://packages.python.org/pypng/. - - Examples: - >>> from pyqrcode import QRCode - >>> import sys - >>> url = QRCode('http://uca.edu') - >>> url.svg(sys.stdout, scale=1) - >>> url.svg('uca.svg', scale=4) - >>> number = QRCode(123456789012345) - >>> number.png('big-number.png') - - .. note:: - For what all of the parameters do, see the :func:`pyqrcode.create` - function. - """ - def __init__(self, content, error='H', version=None, mode=None): - - #Coerce the content into a string - self.data = str(content) - - #Check that the passed in error level is valid - try: - self.error = tables.error_level[str(error).upper()] - except: - raise ValueError('The error parameter is not one of ' - '"L", "M", "Q", or "H."') - - #Guess the mode of the code, this will also be used for - #error checking - guessed_content_type = self._detect_content_type() - - #Force a passed in mode to be lowercase - if mode: - mode = mode.lower() - - #Check that the mode parameter is compatible with the contents - if not mode: - #Use the guessed mode - self.mode = guessed_content_type - self.mode_num = tables.modes[self.mode] - elif guessed_content_type == 'binary' and \ - tables.modes[mode] != tables.modes['binary']: - #Binary is only guessed as a last resort, if the - #passed in mode is not binary the data won't encode - raise ValueError('The content provided cannot be encoded with ' - 'the mode {}, it can only be encoded as ' - 'binary.'.format(mode)) - elif tables.modes[mode] == tables.modes['numeric'] and \ - guessed_content_type != 'numeric': - #If numeric encoding is requested make sure the data can - #be encoded in that format - raise ValueError('The content cannot be encoded as numeric.') - else: - #The data should encode with the passed in mode - self.mode = mode - self.mode_num = tables.modes[self.mode] - - #Guess the "best" version - self.version = self._pick_best_fit() - - #If the user supplied a version, then check that it has - #sufficient data capacity for the contents passed in - if version: - if version >= self.version: - self.version = version - else: - raise ValueError('The data will not fit inside a version {} ' - 'code with the given encoding and error ' - 'level (the code must be at least a ' - 'version {}).'.format(version, self.version)) - - #Build the QR code - self.builder = builder.QRCodeBuilder(data=content, - version=self.version, - mode=self.mode, - error=self.error) - - #Save the code for easier reference - self.code = self.builder.code - - def __str__(self): - return repr(self) - - def __repr__(self): - return 'QRCode(content=\'{}\', error=\'{}\', version={}, mode=\'{}\')'.format( - self.data, self.error, self.version, self.mode) - - - def _detect_content_type(self): - """This method tries to auto-detect the type of the data. It first - tries to see if the data is a valid integer, in which case it returns - numeric. Next, it tests the data to see if it is 'alphanumeric.' QR - Codes use a special table with very limited range of ASCII characters. - The code's data is tested to make sure it fits inside this limited - range. If all else fails, the data is determined to be of type - 'binary.' - - Note, encoding 'kanji' is not yet implemented. - """ - #See if the data is an integer - try: - test = int(self.data) - return 'numeric' - except: - #Data is not numeric, this is not an error - pass - - #See if that data is alphanumeric based on the standards - #special ASCII table - valid_characters = tables.ascii_codes.keys() - if all(map(lambda x: x in valid_characters, self.data.upper())): - return 'alphanumeric' - - #All of the tests failed. The content can only be binary. - return 'binary' - - def _pick_best_fit(self): - """This method return the smallest possible QR code version number - that will fit the specified data with the given error level. - """ - for version in range(1,41): - #Get the maximum possible capacity - capacity = tables.data_capacity[version][self.error][self.mode_num] - - #Check the capacity - if (self.mode_num == tables.modes['binary'] and \ - capacity >= len(self.data.encode('latin1'))) or \ - capacity >= len(self.data): - return version - - raise ValueError('The data will not fit in any QR code version ' - 'with the given encoding and error level.') - - def get_png_size(self, scale): - """This is method helps users determine what *scale* to use when - creating a PNG of this QR code. It is meant mostly to be used in the - console to help the user determine the pixel size of the code - using various scales. - - This method will return an integer representing the width and height of - the QR code in pixels, as if it was drawn using the given *scale*. - Because QR codes are square, the number represents both dimensions. - - Example: - >>> code = pyqrcode.QRCode("I don't like spam!") - >>> print(code.get_png_size(1)) - 31 - >>> print(code.get_png_size(5)) - 155 - """ - return builder._get_png_size(self.version, scale) - - def png(self, file, scale=1, module_color=None, background=None): - """This method writes the QR code out as an PNG image. The resulting - PNG has a bit depth of 1. The file parameter is used to specify where - to write the image to. It can either be an writable stream or a - file path. - - .. note:: - This method depends on the pypng module to actually create the - PNG file. - - This method will write the given *file* out as a PNG file. The file - can be either a string file path, or a writable stream. - - The *scale* parameter sets how large to draw a single module. By - default one pixel is used to draw a single module. This may make the - code too small to be read efficiently. Increasing the scale will make - the code larger. Only integer scales are usable. This method will - attempt to coerce the parameter into an integer (e.g. 2.5 will become 2, - and '3' will become 3). - - The *module_color* parameter sets what color to use for the encoded - modules (the black part on most QR codes). The *background* parameter - sets what color to use for the background (the white part on most - QR codes). If either parameter is set, then both must be - set or a ValueError is raised. Colors should be specified as either - a list or a tuple of length 3 or 4. The components of the list must - be integers between 0 and 255. The first three member give the RGB - color. The fourth member gives the alpha component, where 0 is - transparent and 255 is opaque. Note, many color - combinations are unreadable by scanners, so be careful. - - - - Example: - >>> code = pyqrcode.create('Are you suggesting coconuts migrate?') - >>> code.png('swallow.png', scale=5) - >>> code.png('swallow.png', scale=5, - module_color=(0x66, 0x33, 0x0), #Dark brown - background=(0xff, 0xff, 0xff, 0x88)) #50% transparent white - """ - builder._png(self.code, self.version, file, scale, - module_color, background) - - def svg(self, file, scale=1, module_color='#000000', background=None): - """This method writes the QR code out as an SVG document. The - code is drawn by drawing only the modules corresponding to a 1. They - are drawn using a line, such that contiguous modules in a row - are drawn with a single line. - - The *file* parameter is used to specify where to write the document - to. It can either be a writable stream or a file path. - - The *scale* parameter sets how large to draw - a single module. By default one pixel is used to draw a single - module. This may make the code too small to be read efficiently. - Increasing the scale will make the code larger. Unlike the png() method, - this method will accept fractional scales (e.g. 2.5). - - Note, three things are done to make the code more appropriate for - embedding in a HTML document. The "white" part of the code is actually - transparent. The code itself has a class of "pyqrcode". The lines - making up the QR code have a class "pyqrline". These should make the - code easier to style using CSS. - - You can also set the colors directly using the *module_color* and - *background* parameters. The *module_color* parameter sets what color to - use for the data modules (the black part on most QR codes). The - *background* parameter sets what color to use for the background (the - white part on most QR codes). The parameters can be set to any valid - SVG or HTML color. If the background is set to None, then no background - will be drawn, i.e. the background will be transparent. Note, many color - combinations are unreadable by scanners, so be careful. - - Example: - >>> code = pyqrcode.create('Hello. Uhh, can we have your liver?') - >>> code.svg('live-organ-transplants.svg', 3.6) - >>> code.svg('live-organ-transplants.svg', scale=4, - module_color='brown', background='0xFFFFFF') - """ - builder._svg(self.code, self.version, file, scale, - module_color, background) - - def text(self): - """This method returns a string based representation of the QR code. - The data modules are represented by 1's and the background modules are - represented by 0's. The main purpose of this method is to allow a user - to write their own renderer. - - Example: - >>> code = pyqrcode.create('Example') - >>> text = code.text() - >>> print(text) - """ - return builder._text(self.code) diff --git a/pyqrcode/builder.py b/pyqrcode/builder.py deleted file mode 100644 index 96ac93f..0000000 --- a/pyqrcode/builder.py +++ /dev/null @@ -1,1089 +0,0 @@ -"""This module does the actual generation of the QR codes. The QRCodeBuilder -builds the code. While the various output methods draw the code into a file. -""" -import pyqrcode.tables as tables -import io -import sys -import itertools - -class QRCodeBuilder: - """This class generates a QR code based on the standard. It is meant to - be used internally, not by users!!! - - This class implements the tutorials found at: - - * http://www.thonky.com/qr-code-tutorial/ - - * http://www.matchadesign.com/blog/qr-code-demystified-part-6/ - - This class also uses the standard, which can be read online at: - http://raidenii.net/files/datasheets/misc/qr_code.pdf - - Test codes were tested against: - http://zxing.org/w/decode.jspx - - Also, reference codes were generated at: - http://www.morovia.com/free-online-barcode-generator/qrcode-maker.php - - QR code Debugger: - http://qrlogo.kaarposoft.dk/qrdecode.html - """ - def __init__(self, data, version, mode, error): - """See :py:class:`pyqrcode.QRCode` for information on the parameters.""" - - #Set what data we are going to use to generate - #the QR code - if isinstance(data, bytes): - self.data = data.decode('utf-8') - else: - self.data = data - - #Check that the user passed in a valid mode - if mode in tables.modes.keys(): - self.mode = tables.modes[mode] - else: - raise LookupError('{} is not a valid mode.'.format(mode)) - - #Check that the user passed in a valid error level - if error in tables.error_level.keys(): - self.error = tables.error_level[error] - else: - raise LookupError('{} is not a valid error ' - 'level.'.format(error)) - - if 1 <= version <= 40: - self.version = version - else: - raise ValueError("The version must between 1 and 40.") - - #Look up the proper row for error correction code words - self.error_code_words = tables.eccwbi[version][self.error] - - #This property will hold the binary string as it is built - self.buffer = io.StringIO() - - #Create the binary data block - self.add_data() - - #Create the actual QR code - self.make_code() - - def grouper(self, n, iterable, fillvalue=None): - """This generator yields a set of tuples, where the - iterable is broken into n sized chunks. If the - iterable is not evenly sized then fillvalue will - be appended to the last tuple to make up the difference. - - This function is copied from the standard docs on - itertools. - """ - args = [iter(iterable)] * n - return itertools.zip_longest(*args, fillvalue=fillvalue) - - def binary_string(self, data, length): - """This method returns a string of length n that is the binary - representation of the given data. This function is used to - basically create bit fields of a given size. - """ - return '{{:0{}b}}'.format(length).format(int(data)) - - def get_data_length(self): - """QR codes contain a "data length" field. This method creates this - field. A binary string representing the appropriate length is - returned. - """ - - #The "data length" field varies by the type of code and its mode. - #discover how long the "data length" field should be. - if 1 <= self.version <= 9: - max_version = 9 - elif 10 <= self.version <= 26: - max_version = 26 - elif 27 <= self.version <= 40: - max_version = 40 - - data_length = tables.data_length_field[max_version][self.mode] - - length_string = self.binary_string(len(self.data), data_length) - - if len(length_string) > data_length: - raise ValueError('The supplied data will not fit ' - 'within this version of a QRCode.') - return length_string - - def encode(self): - """This method encodes the data into a binary string using - the appropriate algorithm specified by the mode. - """ - if self.mode == tables.modes['alphanumeric']: - encoded = self.encode_alphanumeric() - elif self.mode == tables.modes['numeric']: - encoded = self.encode_numeric() - elif self.mode == tables.modes['bytes']: - encoded = self.encode_bytes() - else: - raise ValueError('This mode is not yet implemented.') - - bits = self.terminate_bits(encoded) - if bits is not None: - encoded += bits - - return encoded - - def encode_alphanumeric(self): - """This method encodes the QR code's data if its mode is - alphanumeric. It returns the data encoded as a binary string. - """ - #Convert the string to upper case - self.data = self.data.upper() - - #Change the data such that it uses a QR code ascii table - ascii = [] - for char in self.data: - ascii.append(tables.ascii_codes[char]) - - #Now perform the algorithm that will make the ascii into bit fields - with io.StringIO() as buf: - for (a,b) in self.grouper(2, ascii): - if b is not None: - buf.write(self.binary_string((45*a)+b, 11)) - else: - #This occurs when there is an odd number - #of characters in the data - buf.write(self.binary_string(a, 6)) - - #Return the binary string - return buf.getvalue() - - def encode_numeric(self): - """This method encodes the QR code's data if its mode is - numeric. It returns the data encoded as a binary string. - """ - with io.StringIO() as buf: - #Break the number into groups of three digits - for triplet in self.grouper(3, self.data): - number = '' - for digit in triplet: - #Only build the string if digit is not None - if digit: - number = ''.join([number, digit]) - else: - break - - #If the number is one digits, make a 4 bit field - if len(number) == 1: - bin = self.binary_string(number, 4) - - #If the number is two digits, make a 7 bit field - elif len(number) == 2: - bin = self.binary_string(number, 7) - - #Three digit numbers use a 10 bit field - else: - bin = self.binary_string(number, 10) - - buf.write(bin) - return buf.getvalue() - - def encode_bytes(self): - """This method encodes the QR code's data if its mode is - 8 bit mode. It returns the data encoded as a binary string. - """ - with io.StringIO() as buf: - for char in self.data.encode('latin1'): - buf.write('{{:0{}b}}'.format(8).format(char)) - return buf.getvalue() - - - def add_data(self): - """This function properly constructs a QR code's data string. It takes - into account the interleaving pattern required by the standard. - """ - #Encode the data into a QR code - self.buffer.write(self.binary_string(self.mode, 4)) - self.buffer.write(self.get_data_length()) - self.buffer.write(self.encode()) - - #delimit_words and add_words can return None - add_bits = self.delimit_words() - if add_bits: - self.buffer.write(add_bits) - - fill_bytes = self.add_words() - if fill_bytes: - self.buffer.write(fill_bytes) - - #Get a numeric representation of the data - data = [int(''.join(x),2) - for x in self.grouper(8, self.buffer.getvalue())] - - #This is the error information for the code - error_info = tables.eccwbi[self.version][self.error] - - #This will hold our data blocks - data_blocks = [] - - #This will hold our error blocks - error_blocks = [] - - #Some codes have the data sliced into two different sized blocks - #for example, first two 14 word sized blocks, then four 15 word - #sized blocks. This means that slicing size can change over time. - data_block_sizes = [error_info[2]] * error_info[1] - if error_info[3] != 0: - data_block_sizes.extend([error_info[4]] * error_info[3]) - - #For every block of data, slice the data into the appropriate - #sized block - current_byte = 0 - for n_data_blocks in data_block_sizes: - data_blocks.append(data[current_byte:current_byte+n_data_blocks]) - current_byte += n_data_blocks - - if current_byte < len(data): - raise ValueError('Too much data for this code version.') - - #DEBUG CODE!!!! - #Print out the data blocks - #print('Data Blocks:\n{}'.format(data_blocks)) - - #Calculate the error blocks - for n, block in enumerate(data_blocks): - error_blocks.append(self.make_error_block(block, n)) - - #DEBUG CODE!!!! - #Print out the error blocks - #print('Error Blocks:\n{}'.format(error_blocks)) - - #Buffer we will write our data blocks into - data_buffer = io.StringIO() - - #Add the data blocks - #Write the buffer such that: block 1 byte 1, block 2 byte 1, etc. - largest_block = max(error_info[2], error_info[4])+error_info[0] - for i in range(largest_block): - for block in data_blocks: - if i < len(block): - data_buffer.write(self.binary_string(block[i], 8)) - - #Add the error code blocks. - #Write the buffer such that: block 1 byte 1, block 2 byte 2, etc. - for i in range(error_info[0]): - for block in error_blocks: - data_buffer.write(self.binary_string(block[i], 8)) - - self.buffer = data_buffer - - def terminate_bits(self, payload): - """This method adds zeros to the end of the encoded data so that the - encoded data is of the correct length. It returns a binary string - containing the bits to be added. - """ - data_capacity = tables.data_capacity[self.version][self.error][0] - - if len(payload) > data_capacity: - raise ValueError('The supplied data will not fit ' - 'within this version of a QR code.') - - #We must add up to 4 zeros to make up for any shortfall in the - #length of the data field. - if len(payload) == data_capacity: - return None - elif len(payload) <= data_capacity-4: - bits = self.binary_string(0,4) - else: - #Make up any shortfall need with less than 4 zeros - bits = self.binary_string(0, data_capacity - len(payload)) - - return bits - - def delimit_words(self): - """This method takes the existing encoded binary string - and returns a binary string that will pad it such that - the encoded string contains only full bytes. - """ - bits_short = 8 - (len(self.buffer.getvalue()) % 8) - - #The string already falls on an byte boundary do nothing - if bits_short == 8: - return None - else: - return self.binary_string(0, bits_short) - - def add_words(self): - """The data block must fill the entire data capacity of the QR code. - If we fall short, then we must add bytes to the end of the encoded - data field. The value of these bytes are specified in the standard. - """ - - data_blocks = len(self.buffer.getvalue()) // 8 - total_blocks = tables.data_capacity[self.version][self.error][0] // 8 - needed_blocks = total_blocks - data_blocks - - if needed_blocks == 0: - return None - - #This will return item1, item2, item1, item2, etc. - block = itertools.cycle(['11101100', '00010001']) - - #Create a string of the needed blocks - return ''.join([next(block) for x in range(needed_blocks)]) - - def _fix_exp(self, exponent): - """Makes sure the exponent ranges from 0 to 255.""" - #return (exponent % 256) + (exponent // 256) - return exponent % 255 - - def make_error_block(self, block, block_number): - """This function constructs the error correction block of the - given data block. This is *very complicated* process. To - understand the code you need to read: - - * http://www.thonky.com/qr-code-tutorial/part-2-error-correction/ - * http://www.matchadesign.com/blog/qr-code-demystified-part-4/ - """ - #Get the error information from the standards table - error_info = tables.eccwbi[self.version][self.error] - - #This is the number of 8-bit words per block - if block_number < error_info[1]: - code_words_per_block = error_info[2] - else: - code_words_per_block = error_info[4] - - #This is the size of the error block - error_block_size = error_info[0] - - #Copy the block as the message polynomial coefficients - mp_co = block[:] - - #Add the error blocks to the message polynomial - mp_co.extend([0] * (error_block_size)) - - #Get the generator polynomial - generator = tables.generator_polynomials[error_block_size] - - #This will hold the temporary sum of the message coefficient and the - #generator polynomial - gen_result = [0] * len(generator) - - #Go through every code word in the block - for i in range(code_words_per_block): - #Get the first coefficient from the message polynomial - coefficient = mp_co.pop(0) - - #Skip coefficients that are zero - if coefficient == 0: - continue - else: - #Turn the coefficient into an alpha exponent - alpha_exp = tables.galois_antilog[coefficient] - - #Add the alpha to the generator polynomial - for n in range(len(generator)): - gen_result[n] = alpha_exp + generator[n] - if gen_result[n] > 255: - gen_result[n] = gen_result[n] % 255 - - #Convert the alpha notation back into coefficients - gen_result[n] = tables.galois_log[gen_result[n]] - - #XOR the sum with the message coefficients - mp_co[n] = gen_result[n] ^ mp_co[n] - - #Pad the end of the error blocks with zeros if needed - if len(mp_co) < code_words_per_block: - mp_co.extend([0] * (code_words_per_block - len(mp_co))) - - return mp_co - - def make_code(self): - """This method returns the best possible QR code.""" - from copy import deepcopy - - #Get the size of the underlying matrix - matrix_size = tables.version_size[self.version] - - #Create a template matrix we will build the codes with - row = [' ' for x in range(matrix_size)] - template = [deepcopy(row) for x in range(matrix_size)] - - #Add mandatory information to the template - self.add_detection_pattern(template) - self.add_position_pattern(template) - self.add_version_pattern(template) - - #Create the various types of masks of the template - self.masks = self.make_masks(template) - - self.best_mask = self.choose_best_mask() - self.code = self.masks[self.best_mask] - - def add_detection_pattern(self, m): - """This method add the detection patterns to the QR code. This lets - the scanner orient the pattern. It is required for all QR codes. - The detection pattern consists of three boxes located at the upper - left, upper right, and lower left corners of the matrix. Also, two - special lines called the timing pattern is also necessary. Finally, - a single black pixel is added just above the lower left black box. - """ - - #Draw outer black box - for i in range(7): - inv = -(i+1) - for j in [0,6,-1,-7]: - m[j][i] = 1 - m[i][j] = 1 - m[inv][j] = 1 - m[j][inv] = 1 - - #Draw inner white box - for i in range(1, 6): - inv = -(i+1) - for j in [1, 5, -2, -6]: - m[j][i] = 0 - m[i][j] = 0 - m[inv][j] = 0 - m[j][inv] = 0 - - #Draw inner black box - for i in range(2, 5): - for j in range(2, 5): - inv = -(i+1) - m[i][j] = 1 - m[inv][j] = 1 - m[j][inv] = 1 - - #Draw white border - for i in range(8): - inv = -(i+1) - for j in [7, -8]: - m[i][j] = 0 - m[j][i] = 0 - m[inv][j] = 0 - m[j][inv] = 0 - - #To keep the code short, it draws an extra box - #in the lower right corner, this removes it. - for i in range(-8, 0): - for j in range(-8, 0): - m[i][j] = ' ' - - #Add the timing pattern - bit = itertools.cycle([1,0]) - for i in range(8, (len(m)-8)): - b = next(bit) - m[i][6] = b - m[6][i] = b - - #Add the extra black pixel - m[-8][8] = 1 - - def add_position_pattern(self, m): - """This method draws the position adjustment patterns onto the QR - Code. All QR code versions larger than one require these special boxes - called position adjustment patterns. - """ - #Version 1 does not have a position adjustment pattern - if self.version == 1: - return - - #Get the coordinates for where to place the boxes - coordinates = tables.position_adjustment[self.version] - - #Get the max and min coordinates to handle special cases - min_coord = coordinates[0] - max_coord = coordinates[-1] - - #Draw a box at each intersection of the coordinates - for i in coordinates: - for j in coordinates: - #Do not draw these boxes because they would - #interfere with the detection pattern - if (i == min_coord and j == min_coord) or \ - (i == min_coord and j == max_coord) or \ - (i == max_coord and j == min_coord): - continue - - #Center black pixel - m[i][j] = 1 - - #Surround the pixel with a white box - for x in [-1,1]: - m[i+x][j+x] = 0 - m[i+x][j] = 0 - m[i][j+x] = 0 - m[i-x][j+x] = 0 - m[i+x][j-x] = 0 - - #Surround the white box with a black box - for x in [-2,2]: - for y in [0,-1,1]: - m[i+x][j+x] = 1 - m[i+x][j+y] = 1 - m[i+y][j+x] = 1 - m[i-x][j+x] = 1 - m[i+x][j-x] = 1 - - def add_version_pattern(self, m): - """For QR codes with a version 7 or higher, a special pattern - specifying the code's version is required. - - For further information see: - http://www.thonky.com/qr-code-tutorial/format-version-information/#example-of-version-7-information-string - """ - if self.version < 7: - return - - #Get the bit fields for this code's version - #We will iterate across the string, the bit string - #needs the least significant digit in the zero-th position - field = iter(tables.version_pattern[self.version][::-1]) - - #Where to start placing the pattern - start = len(m)-11 - - #The version pattern is pretty odd looking - for i in range(6): - #The pattern is three modules wide - for j in range(start, start+3): - bit = int(next(field)) - - #Bottom Left - m[i][j] = bit - - #Upper right - m[j][i] = bit - - def make_masks(self, template): - """This method generates all seven masks so that the best mask can - be determined. The template parameter is a code matrix that will - server as the base for all the generated masks. - """ - from copy import deepcopy - - nmasks = len(tables.mask_patterns) - masks = [''] * nmasks - count = 0 - - for n in range(nmasks): - cur_mask = deepcopy(template) - masks[n] = cur_mask - - #Add the type pattern bits to the code - self.add_type_pattern(cur_mask, tables.type_bits[self.error][n]) - - #Get the mask pattern - pattern = tables.mask_patterns[n] - - #This will read the 1's and 0's one at a time - bits = iter(self.buffer.getvalue()) - - #These will help us do the up, down, up, down pattern - row_start = itertools.cycle([len(cur_mask)-1, 0]) - row_stop = itertools.cycle([-1,len(cur_mask)]) - direction = itertools.cycle([-1, 1]) - - #The data pattern is added using pairs of columns - for column in range(len(cur_mask)-1, 0, -2): - - #The vertical timing pattern is an exception to the rules, - #move the column counter over by one - if column <= 6: - column = column - 1 - - #This will let us fill in the pattern - #right-left, right-left, etc. - column_pair = itertools.cycle([column, column-1]) - - #Go through each row in the pattern moving up, then down - for row in range(next(row_start), next(row_stop), - next(direction)): - - #Fill in the right then left column - for i in range(2): - col = next(column_pair) - - #Go to the next column if we encounter a - #preexisting pattern (usually an alignment pattern) - if cur_mask[row][col] != ' ': - continue - - #Some versions don't have enough bits. You then fill - #in the rest of the pattern with 0's. These are - #called "remainder bits." - try: - bit = int(next(bits)) - except: - bit = 0 - - - #If the pattern is True then flip the bit - if pattern(row, col): - cur_mask[row][col] = bit ^ 1 - else: - cur_mask[row][col] = bit - - #DEBUG CODE!!! - #Save all of the masks as png files - #for i, m in enumerate(masks): - # _png(m, self.version, 'mask-{}.png'.format(i), 5) - - return masks - - def choose_best_mask(self): - """This method returns the index of the "best" mask as defined by - having the lowest total penalty score. The penalty rules are defined - by the standard. The mask with the lowest total score should be the - easiest to read by optical scanners. - """ - self.scores = [] - for n in range(len(self.masks)): - self.scores.append([0,0,0,0]) - - #Score penalty rule number 1 - #Look for five consecutive squares with the same color. - #Each one found gets a penalty of 3 + 1 for every - #same color square after the first five in the row. - for (n, mask) in enumerate(self.masks): - current = mask[0][0] - counter = 0 - total = 0 - - #Examine the mask row wise - for row in range(0,len(mask)): - counter = 0 - for col in range(0,len(mask)): - bit = mask[row][col] - - if bit == current: - counter += 1 - else: - if counter >= 5: - total += (counter - 5) + 3 - counter = 1 - current = bit - if counter >= 5: - total += (counter - 5) + 3 - - #Examine the mask column wise - for col in range(0,len(mask)): - counter = 0 - for row in range(0,len(mask)): - bit = mask[row][col] - - if bit == current: - counter += 1 - else: - if counter >= 5: - total += (counter - 5) + 3 - counter = 1 - current = bit - if counter >= 5: - total += (counter - 5) + 3 - - self.scores[n][0] = total - - #Score penalty rule 2 - #This rule will add 3 to the score for each 2x2 block of the same - #colored pixels there are. - for (n, mask) in enumerate(self.masks): - count = 0 - #Don't examine the 0th and Nth row/column - for i in range(0, len(mask)-1): - for j in range(0, len(mask)-1): - if mask[i][j] == mask[i+1][j] and \ - mask[i][j] == mask[i][j+1] and \ - mask[i][j] == mask[i+1][j+1]: - count += 1 - - self.scores[n][1] = count * 3 - - #Score penalty rule 3 - #This rule looks for 1011101 within the mask prefixed - #and/or suffixed by four zeros. - patterns = [[0,0,0,0,1,0,1,1,1,0,1], - [1,0,1,1,1,0,1,0,0,0,0],] - #[0,0,0,0,1,0,1,1,1,0,1,0,0,0,0]] - - for (n, mask) in enumerate(self.masks): - nmatches = 0 - - for i in range(len(mask)): - for j in range(len(mask)): - for pattern in patterns: - match = True - k = j - #Look for row matches - for p in pattern: - if k >= len(mask) or mask[i][k] != p: - match = False - break - k += 1 - if match: - nmatches += 1 - - match = True - k = j - #Look for column matches - for p in pattern: - if k >= len(mask) or mask[k][i] != p: - match = False - break - k += 1 - if match: - nmatches += 1 - - - self.scores[n][2] = nmatches * 40 - - #Score the last rule, penalty rule 4. This rule measures how close - #the pattern is to being 50% black. The further it deviates from - #this this ideal the higher the penalty. - for (n, mask) in enumerate(self.masks): - nblack = 0 - for row in mask: - nblack += sum(row) - - total_pixels = len(mask)**2 - ratio = nblack / total_pixels - percent = (ratio * 100) - 50 - self.scores[n][3] = int((abs(int(percent)) / 5) * 10) - - - #Calculate the total for each score - totals = [0] * len(self.scores) - for i in range(len(self.scores)): - for j in range(len(self.scores[i])): - totals[i] += self.scores[i][j] - - #DEBUG CODE!!! - #Prints out a table of scores - #print('Rule Scores\n 1 2 3 4 Total') - #for i in range(len(self.scores)): - # print(i, end='') - # for s in self.scores[i]: - # print('{: >6}'.format(s), end='') - # print('{: >7}'.format(totals[i])) - #print('Mask Chosen: {}'.format(totals.index(min(totals)))) - - #The lowest total wins - return totals.index(min(totals)) - - def add_type_pattern(self, m, type_bits): - """This will add the pattern to the QR code that represents the error - level and the type of mask used to make the code. - """ - field = iter(type_bits) - for i in range(7): - bit = int(next(field)) - - #Skip the timing bits - if i < 6: - m[8][i] = bit - else: - m[8][i+1] = bit - - if -8 < -(i+1): - m[-(i+1)][8] = bit - - for i in range(-8,0): - bit = int(next(field)) - - m[8][i] = bit - - i = -i - #Skip timing column - if i > 6: - m[i][8] = bit - else: - m[i-1][8] = bit - -############################################################################## -############################################################################## -# -# Output Functions -# -############################################################################## -############################################################################## - -def _get_file(file, mode): - """This method returns the file parameter if it is an open writable - stream. Otherwise it treats the file parameter as a file path and - opens it with the given mode. It is used by the svg and png methods - to interpret the file parameter. - """ - import os.path - #See if the file parameter is a stream - if not isinstance(file, io.IOBase): - #If it is not a stream open a the file path - return open(os.path.abspath(file), mode) - elif not file.writable(): - raise ValueError('Stream is not writable.') - else: - return file - -def _get_png_size(version, scale): - """See: QRCode.get_png_size - - This function was abstracted away from QRCode to allow for the output of - QR codes during the build process, i.e. for debugging. It works - just the same except you must specify the code's version. This is needed - to calculate the PNG's size. - """ - #Formula: scale times number of modules plus the border on each side - return (scale * tables.version_size[version]) + (2 * scale) - -def _text(code): - """This method returns a text based representation of the QR code. - This is useful for debugging purposes. - """ - buf = io.StringIO() - - border_row = '0' * (len(code[0]) + 2) - - buf.write(border_row) - buf.write('\n') - for row in code: - buf.write('0') - for bit in row: - if bit == 1: - buf.write('1') - elif bit == 0: - buf.write('0') - #This is for debugging unfinished QR codes, - #unset pixels will be spaces. - else: - buf.write(' ') - buf.write('0\n') - - buf.write(border_row) - - return buf.getvalue() - -def _svg(code, version, file, scale=1, module_color='black', background=None): - """This method writes the QR code out as an SVG document. The - code is drawn by drawing only the modules corresponding to a 1. They - are drawn using a line, such that contiguous modules in a row - are drawn with a single line. The file parameter is used to - specify where to write the document to. It can either be an writable - stream or a file path. The scale parameter is sets how large to draw - a single module. By default one pixel is used to draw a single - module. This may make the code to small to be read efficiently. - Increasing the scale will make the code larger. This method will accept - fractional scales (e.g. 2.5). - """ - #This is the template for the svg line. It is placed here so it - #does not need to be recreated for each call to line(). - line_template = ''' - ''' - - def line(x1, y1, x2, y2, color): - """This sub-function draws the modules. It attempts to draw them - as a single line per row, rather than as individual rectangles. - It uses the l variable as a template t - """ - return line_template.format(x1+scale, y1+scale, x2+scale, y2+scale, - color, scale) - - file = _get_file(file, 'w') - - #Write the document header - file.write(""" - - - - QR code - """.format((tables.version_size[version]*scale)+(2*scale))) - - #Draw a background rectangle if necessary - if background: - file.write(""" - - """.format((tables.version_size[version]*scale)+(2*scale), - background)) - - #This will hold the current row number - rnumber = 0 - - #The current "color," used to define starting a line and ending a line. - color = 'black' - - #Loop through each row of the code - for row in code: - colnumber = 0 #Reset column number - start_column = 0 #Reset the starting_column number - - #Examine every bit in the row - for bit in row: - #Set the color of the bit - if bit == 1: - new_color = 'black' - elif bit == 0: - new_color = 'white' - - #DEBUG CODE!! - #In unfinished QR codes, unset pixels will be red - #else: - #new_color = 'red' - - #When the color changes then draw a line - if new_color != color: - #Don't draw the white background - if color != 'white': - file.write(line(start_column, rnumber, - colnumber, rnumber, color)) - - #Move the next line's starting color and number - start_column = colnumber - color = new_color - - #Accumulate the column - colnumber += scale - - #End the row by drawing out the accumulated line - #if it is not the background - if color != 'white': - file.write(line(start_column, rnumber, - colnumber, rnumber, color)) - - #Set row number - rnumber += scale - - - #Close the document - file.write("\n") - -def _png(code, version, file, scale=1, module_color=None, background=None): - """See: pyqrcode.QRCode.png() - - This function was abstracted away from QRCode to allow for the output of - QR codes during the build process, i.e. for debugging. It works - just the same except you must specify the code's version. This is needed - to calculate the PNG's size. - - This method will write the given file out as a PNG file. Note, it - depends on the PyPNG module to do this. - """ - from . import png - - #Coerce scale parameter into an integer - try: - scale = int(scale) - except ValueError: - raise ValueError('The scale parameter must be an integer') - - def scale_code(code): - """To perform the scaling we need to inflate the number of bits. - The PNG library expects all of the bits when it draws the PNG. - Effectively, we double, tripple, etc. the number of columns and - the number of rows. - """ - #This is the row to show up at the top and bottom border - border_module = [1] * scale - border_row = [1] * _get_png_size(version, scale) - border = [border_row] * scale - - - #This is one row's worth of each possible module - #PNG's use 0 for black and 1 for white, this is the - #reverse of the QR standard - black = [0] * scale - white = [1] * scale - - #This will hold the final PNG's bits - bits = [] - - #Add scale rows before the code as a border, - #as per the standard - bits.extend(border) - - #Add each row of the to the final PNG bits - for row in code: - tmp_row = [] - - #Add one all white module to the beginning - #to create the vertical border - tmp_row.extend(border_module) - - #Go through each bit in the code - for item in row: - #Add one scaled module - if item == 0: - tmp_row.extend(white) - else: - tmp_row.extend(black) - - #Add one all white module to the end - #to create the vertical border - tmp_row.extend(border_module) - - #Copy each row scale times - for n in range(scale): - bits.append(tmp_row) - - #Add the bottom border - bits.extend(border) - - return bits - - def png_pallete_color(color): - """This creates a palette color from a list or tuple. The list or - tuple must be of length 3 (for rgb) or 4 (for rgba). The values - must be between 0 and 255. Note rgb colors will be given an added - alpha component set to 255. - - The pallete color is represented as a list, this is what is returned. - """ - if color: - rgba = [] - if not (3 <= len(color) <= 4): - raise ValueError('Colors must be a list or tuple of length ' - ' 3 or 4. You passed in ' - '"{}".'.format(color)) - - for c in color: - c = int(c) - if 0 <= c <= 255: - rgba.append(int(c)) - else: - raise ValueError('Color components must be between ' - ' 0 and 255') - - #Make all all colors have an alpha channel - if len(rgba) == 3: - rgba.append(255) - - return rgba - - #If the user passes in one parameter, then they must pass in both or neither - #Note, this is a logical xor - if (not module_color) != (not background): - raise ValueError('If you specify either the black or white parameter, ' - 'then you must specify both.') - - #Create the pallete, or set greyscale to True - if module_color: - palette = [png_pallete_color(module_color), - png_pallete_color(background)] - greyscale = False - else: - palette = None - greyscale = True - - #The size of the PNG - size = _get_png_size(version, scale) - - #We need to increase the size of the code to match up to the - #scale parameter. - code = scale_code(code) - - #Write out the PNG - with _get_file(file, 'wb') as f: - w = png.Writer(width=size, height=size, greyscale=greyscale, - palette=palette, bitdepth=1) - - w.write(f, code) diff --git a/pyqrcode/png.py b/pyqrcode/png.py deleted file mode 100644 index fc1f469..0000000 --- a/pyqrcode/png.py +++ /dev/null @@ -1,3838 +0,0 @@ -#!/usr/bin/env python - -# png.py - PNG encoder/decoder in pure Python -# -# Copyright (C) 2006 Johann C. Rocholl -# Portions Copyright (C) 2009 David Jones -# And probably portions Copyright (C) 2006 Nicko van Someren -# -# Original concept by Johann C. Rocholl. -# -# LICENCE (MIT) -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# Changelog (recent first): -# 2009-03-11 David: interlaced bit depth < 8 (writing). -# 2009-03-10 David: interlaced bit depth < 8 (reading). -# 2009-03-04 David: Flat and Boxed pixel formats. -# 2009-02-26 David: Palette support (writing). -# 2009-02-23 David: Bit-depths < 8; better PNM support. -# 2006-06-17 Nicko: Reworked into a class, faster interlacing. -# 2006-06-17 Johann: Very simple prototype PNG decoder. -# 2006-06-17 Nicko: Test suite with various image generators. -# 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support. -# 2006-06-15 Johann: Scanline iterator interface for large input files. -# 2006-06-09 Johann: Very simple prototype PNG encoder. - -# Incorporated into Bangai-O Development Tools by drj on 2009-02-11 from -# http://trac.browsershots.org/browser/trunk/pypng/lib/png.py?rev=2885 - -# Incorporated into pypng by drj on 2009-03-12 from -# //depot/prj/bangaio/master/code/png.py#67 - - -""" -Pure Python PNG Reader/Writer - -This Python module implements support for PNG images (see PNG -specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads -and writes PNG files with all allowable bit depths (1/2/4/8/16/24/32/48/64 -bits per pixel) and colour combinations: greyscale (1/2/4/8/16 bit); RGB, -RGBA, LA (greyscale with alpha) with 8/16 bits per channel; colour mapped -images (1/2/4/8 bit). Adam7 interlacing is supported for reading and -writing. A number of optional chunks can be specified (when writing) -and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``. - -For help, type ``import png; help(png)`` in your python interpreter. - -A good place to start is the :class:`Reader` and :class:`Writer` classes. - -Requires Python 2.3. Limited support is available for Python 2.2, but -not everything works. Best with Python 2.4 and higher. Installation is -trivial, but see the ``README.txt`` file (with the source distribution) -for details. - -This file can also be used as a command-line utility to convert -`Netpbm `_ PNM files to PNG, and the reverse conversion from PNG to -PNM. The interface is similar to that of the ``pnmtopng`` program from -Netpbm. Type ``python png.py --help`` at the shell prompt -for usage and a list of options. - -A note on spelling and terminology ----------------------------------- - -Generally British English spelling is used in the documentation. So -that's "greyscale" and "colour". This not only matches the author's -native language, it's also used by the PNG specification. - -The major colour models supported by PNG (and hence by PyPNG) are: -greyscale, RGB, greyscale--alpha, RGB--alpha. These are sometimes -referred to using the abbreviations: L, RGB, LA, RGBA. In this case -each letter abbreviates a single channel: *L* is for Luminance or Luma or -Lightness which is the channel used in greyscale images; *R*, *G*, *B* stand -for Red, Green, Blue, the components of a colour image; *A* stands for -Alpha, the opacity channel (used for transparency effects, but higher -values are more opaque, so it makes sense to call it opacity). - -A note on formats ------------------ - -When getting pixel data out of this module (reading) and presenting -data to this module (writing) there are a number of ways the data could -be represented as a Python value. Generally this module uses one of -three formats called "flat row flat pixel", "boxed row flat pixel", and -"boxed row boxed pixel". Basically the concern is whether each pixel -and each row comes in its own little tuple (box), or not. - -Consider an image that is 3 pixels wide by 2 pixels high, and each pixel -has RGB components: - -Boxed row flat pixel:: - - list([R,G,B, R,G,B, R,G,B], - [R,G,B, R,G,B, R,G,B]) - -Each row appears as its own list, but the pixels are flattened so that -three values for one pixel simply follow the three values for the previous -pixel. This is the most common format used, because it provides a good -compromise between space and convenience. PyPNG regards itself as -at liberty to replace any sequence type with any sufficiently compatible -other sequence type; in practice each row is an array (from the array -module), and the outer list is sometimes an iterator rather than an -explicit list (so that streaming is possible). - -Flat row flat pixel:: - - [R,G,B, R,G,B, R,G,B, - R,G,B, R,G,B, R,G,B] - -The entire image is one single giant sequence of colour values. -Generally an array will be used (to save space), not a list. - -Boxed row boxed pixel:: - - list([ (R,G,B), (R,G,B), (R,G,B) ], - [ (R,G,B), (R,G,B), (R,G,B) ]) - -Each row appears in its own list, but each pixel also appears in its own -tuple. A serious memory burn in Python. - -In all cases the top row comes first, and for each row the pixels are -ordered from left-to-right. Within a pixel the values appear in the -order, R-G-B-A (or L-A for greyscale--alpha). - -There is a fourth format, mentioned because it is used internally, -is close to what lies inside a PNG file itself, and has some support -from the public API. This format is called packed. When packed, -each row is a sequence of bytes (integers from 0 to 255), just as -it is before PNG scanline filtering is applied. When the bit depth -is 8 this is essentially the same as boxed row flat pixel; when the -bit depth is less than 8, several pixels are packed into each byte; -when the bit depth is 16 (the only value more than 8 that is supported -by the PNG image format) each pixel value is decomposed into 2 bytes -(and `packed` is a misnomer). This format is used by the -:meth:`Writer.write_packed` method. It isn't usually a convenient -format, but may be just right if the source data for the PNG image -comes from something that uses a similar format (for example, 1-bit -BMPs, or another PNG file). - -And now, my famous members --------------------------- -""" - -# http://www.python.org/doc/2.2.3/whatsnew/node5.html - - -__version__ = "0.0.16" - -from array import array -from functools import reduce -try: # See :pyver:old - import itertools -except: - pass -import math -# http://www.python.org/doc/2.4.4/lib/module-operator.html -import operator -import struct -import sys -import zlib -# http://www.python.org/doc/2.4.4/lib/module-warnings.html -import warnings -try: - import pyximport - pyximport.install() - import cpngfilters as pngfilters -except ImportError: - pass - - -__all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array'] - - -# The PNG signature. -# http://www.w3.org/TR/PNG/#5PNG-file-signature -_signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10) - -_adam7 = ((0, 0, 8, 8), - (4, 0, 8, 8), - (0, 4, 4, 8), - (2, 0, 4, 4), - (0, 2, 2, 4), - (1, 0, 2, 2), - (0, 1, 1, 2)) - -def group(s, n): - # See - # http://www.python.org/doc/2.6/library/functions.html#zip - return list(zip(*[iter(s)]*n)) - -def isarray(x): - """Same as ``isinstance(x, array)`` except on Python 2.2, where it - always returns ``False``. This helps PyPNG work on Python 2.2. - """ - - try: - return isinstance(x, array) - except: - return False - -try: # see :pyver:old - array.tostring -except: - def tostring(row): - l = len(row) - return struct.pack('%dB' % l, *row) -else: - def tostring(row): - """Convert row of bytes to string. Expects `row` to be an - ``array``. - """ - return row.tostring() - -# Conditionally convert to bytes. Works on Python 2 and Python 3. -try: - bytes('', 'ascii') - def strtobytes(x): return bytes(x, 'iso8859-1') - def bytestostr(x): return str(x, 'iso8859-1') -except: - strtobytes = str - bytestostr = str - -def interleave_planes(ipixels, apixels, ipsize, apsize): - """ - Interleave (colour) planes, e.g. RGB + A = RGBA. - - Return an array of pixels consisting of the `ipsize` elements of data - from each pixel in `ipixels` followed by the `apsize` elements of data - from each pixel in `apixels`. Conventionally `ipixels` and - `apixels` are byte arrays so the sizes are bytes, but it actually - works with any arrays of the same type. The returned array is the - same type as the input arrays which should be the same type as each other. - """ - - itotal = len(ipixels) - atotal = len(apixels) - newtotal = itotal + atotal - newpsize = ipsize + apsize - # Set up the output buffer - # See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356 - out = array(ipixels.typecode) - # It's annoying that there is no cheap way to set the array size :-( - out.extend(ipixels) - out.extend(apixels) - # Interleave in the pixel data - for i in range(ipsize): - out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize] - for i in range(apsize): - out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize] - return out - -def check_palette(palette): - """Check a palette argument (to the :class:`Writer` class) for validity. - Returns the palette as a list if okay; raises an exception otherwise. - """ - - # None is the default and is allowed. - if palette is None: - return None - - p = list(palette) - if not (0 < len(p) <= 256): - raise ValueError("a palette must have between 1 and 256 entries") - seen_triple = False - for i,t in enumerate(p): - if len(t) not in (3,4): - raise ValueError( - "palette entry %d: entries must be 3- or 4-tuples." % i) - if len(t) == 3: - seen_triple = True - if seen_triple and len(t) == 4: - raise ValueError( - "palette entry %d: all 4-tuples must precede all 3-tuples" % i) - for x in t: - if int(x) != x or not(0 <= x <= 255): - raise ValueError( - "palette entry %d: values must be integer: 0 <= x <= 255" % i) - return p - -class Error(Exception): - prefix = 'Error' - def __str__(self): - return self.prefix + ': ' + ' '.join(self.args) - -class FormatError(Error): - """Problem with input file format. In other words, PNG file does - not conform to the specification in some way and is invalid. - """ - - prefix = 'FormatError' - -class ChunkError(FormatError): - prefix = 'ChunkError' - - -class Writer: - """ - PNG encoder in pure Python. - """ - - def __init__(self, width=None, height=None, - size=None, - greyscale=False, - alpha=False, - bitdepth=8, - palette=None, - transparent=None, - background=None, - gamma=None, - compression=None, - interlace=False, - bytes_per_sample=None, # deprecated - planes=None, - colormap=None, - maxval=None, - chunk_limit=2**20): - """ - Create a PNG encoder object. - - Arguments: - - width, height - Image size in pixels, as two separate arguments. - size - Image size (w,h) in pixels, as single argument. - greyscale - Input data is greyscale, not RGB. - alpha - Input data has alpha channel (RGBA or LA). - bitdepth - Bit depth: from 1 to 16. - palette - Create a palette for a colour mapped image (colour type 3). - transparent - Specify a transparent colour (create a ``tRNS`` chunk). - background - Specify a default background colour (create a ``bKGD`` chunk). - gamma - Specify a gamma value (create a ``gAMA`` chunk). - compression - zlib compression level: 0 (none) to 9 (more compressed); default: -1 or None. - interlace - Create an interlaced image. - chunk_limit - Write multiple ``IDAT`` chunks to save memory. - - The image size (in pixels) can be specified either by using the - `width` and `height` arguments, or with the single `size` - argument. If `size` is used it should be a pair (*width*, - *height*). - - `greyscale` and `alpha` are booleans that specify whether - an image is greyscale (or colour), and whether it has an - alpha channel (or not). - - `bitdepth` specifies the bit depth of the source pixel values. - Each source pixel value must be an integer between 0 and - ``2**bitdepth-1``. For example, 8-bit images have values - between 0 and 255. PNG only stores images with bit depths of - 1,2,4,8, or 16. When `bitdepth` is not one of these values, - the next highest valid bit depth is selected, and an ``sBIT`` - (significant bits) chunk is generated that specifies the original - precision of the source image. In this case the supplied pixel - values will be rescaled to fit the range of the selected bit depth. - - The details of which bit depth / colour model combinations the - PNG file format supports directly, are somewhat arcane - (refer to the PNG specification for full details). Briefly: - "small" bit depths (1,2,4) are only allowed with greyscale and - colour mapped images; colour mapped images cannot have bit depth - 16. - - For colour mapped images (in other words, when the `palette` - argument is specified) the `bitdepth` argument must match one of - the valid PNG bit depths: 1, 2, 4, or 8. (It is valid to have a - PNG image with a palette and an ``sBIT`` chunk, but the meaning - is slightly different; it would be awkward to press the - `bitdepth` argument into service for this.) - - The `palette` option, when specified, causes a colour mapped image - to be created: the PNG colour type is set to 3; greyscale - must not be set; alpha must not be set; transparent must - not be set; the bit depth must be 1,2,4, or 8. When a colour - mapped image is created, the pixel values are palette indexes - and the `bitdepth` argument specifies the size of these indexes - (not the size of the colour values in the palette). - - The palette argument value should be a sequence of 3- or - 4-tuples. 3-tuples specify RGB palette entries; 4-tuples - specify RGBA palette entries. If both 4-tuples and 3-tuples - appear in the sequence then all the 4-tuples must come - before all the 3-tuples. A ``PLTE`` chunk is created; if there - are 4-tuples then a ``tRNS`` chunk is created as well. The - ``PLTE`` chunk will contain all the RGB triples in the same - sequence; the ``tRNS`` chunk will contain the alpha channel for - all the 4-tuples, in the same sequence. Palette entries - are always 8-bit. - - If specified, the `transparent` and `background` parameters must - be a tuple with three integer values for red, green, blue, or - a simple integer (or singleton tuple) for a greyscale image. - - If specified, the `gamma` parameter must be a positive number - (generally, a float). A ``gAMA`` chunk will be created. Note that - this will not change the values of the pixels as they appear in - the PNG file, they are assumed to have already been converted - appropriately for the gamma specified. - - The `compression` argument specifies the compression level to - be used by the ``zlib`` module. Values from 1 to 9 specify - compression, with 9 being "more compressed" (usually smaller - and slower, but it doesn't always work out that way). 0 means - no compression. -1 and ``None`` both mean that the default - level of compession will be picked by the ``zlib`` module - (which is generally acceptable). - - If `interlace` is true then an interlaced image is created - (using PNG's so far only interace method, *Adam7*). This does not - affect how the pixels should be presented to the encoder, rather - it changes how they are arranged into the PNG file. On slow - connexions interlaced images can be partially decoded by the - browser to give a rough view of the image that is successively - refined as more image data appears. - - .. note :: - - Enabling the `interlace` option requires the entire image - to be processed in working memory. - - `chunk_limit` is used to limit the amount of memory used whilst - compressing the image. In order to avoid using large amounts of - memory, multiple ``IDAT`` chunks may be created. - """ - - # At the moment the `planes` argument is ignored; - # its purpose is to act as a dummy so that - # ``Writer(x, y, **info)`` works, where `info` is a dictionary - # returned by Reader.read and friends. - # Ditto for `colormap`. - - # A couple of helper functions come first. Best skipped if you - # are reading through. - - def isinteger(x): - try: - return int(x) == x - except: - return False - - def check_color(c, which): - """Checks that a colour argument for transparent or - background options is the right form. Also "corrects" bare - integers to 1-tuples. - """ - - if c is None: - return c - if greyscale: - try: - l = len(c) - except TypeError: - c = (c,) - if len(c) != 1: - raise ValueError("%s for greyscale must be 1-tuple" % - which) - if not isinteger(c[0]): - raise ValueError( - "%s colour for greyscale must be integer" % - which) - else: - if not (len(c) == 3 and - isinteger(c[0]) and - isinteger(c[1]) and - isinteger(c[2])): - raise ValueError( - "%s colour must be a triple of integers" % - which) - return c - - if size: - if len(size) != 2: - raise ValueError( - "size argument should be a pair (width, height)") - if width is not None and width != size[0]: - raise ValueError( - "size[0] (%r) and width (%r) should match when both are used." - % (size[0], width)) - if height is not None and height != size[1]: - raise ValueError( - "size[1] (%r) and height (%r) should match when both are used." - % (size[1], height)) - width,height = size - del size - - if width <= 0 or height <= 0: - raise ValueError("width and height must be greater than zero") - if not isinteger(width) or not isinteger(height): - raise ValueError("width and height must be integers") - # http://www.w3.org/TR/PNG/#7Integers-and-byte-order - if width > 2**32-1 or height > 2**32-1: - raise ValueError("width and height cannot exceed 2**32-1") - - if alpha and transparent is not None: - raise ValueError( - "transparent colour not allowed with alpha channel") - - if bytes_per_sample is not None: - warnings.warn('please use bitdepth instead of bytes_per_sample', - DeprecationWarning) - if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2): - raise ValueError( - "bytes per sample must be .125, .25, .5, 1, or 2") - bitdepth = int(8*bytes_per_sample) - del bytes_per_sample - if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth: - raise ValueError("bitdepth (%r) must be a postive integer <= 16" % - bitdepth) - - self.rescale = None - if palette: - if bitdepth not in (1,2,4,8): - raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8") - if transparent is not None: - raise ValueError("transparent and palette not compatible") - if alpha: - raise ValueError("alpha and palette not compatible") - if greyscale: - raise ValueError("greyscale and palette not compatible") - else: - # No palette, check for sBIT chunk generation. - if alpha or not greyscale: - if bitdepth not in (8,16): - targetbitdepth = (8,16)[bitdepth > 8] - self.rescale = (bitdepth, targetbitdepth) - bitdepth = targetbitdepth - del targetbitdepth - else: - assert greyscale - assert not alpha - if bitdepth not in (1,2,4,8,16): - if bitdepth > 8: - targetbitdepth = 16 - elif bitdepth == 3: - targetbitdepth = 4 - else: - assert bitdepth in (5,6,7) - targetbitdepth = 8 - self.rescale = (bitdepth, targetbitdepth) - bitdepth = targetbitdepth - del targetbitdepth - - if bitdepth < 8 and (alpha or not greyscale and not palette): - raise ValueError( - "bitdepth < 8 only permitted with greyscale or palette") - if bitdepth > 8 and palette: - raise ValueError( - "bit depth must be 8 or less for images with palette") - - transparent = check_color(transparent, 'transparent') - background = check_color(background, 'background') - - # It's important that the true boolean values (greyscale, alpha, - # colormap, interlace) are converted to bool because Iverson's - # convention is relied upon later on. - self.width = width - self.height = height - self.transparent = transparent - self.background = background - self.gamma = gamma - self.greyscale = bool(greyscale) - self.alpha = bool(alpha) - self.colormap = bool(palette) - self.bitdepth = int(bitdepth) - self.compression = compression - self.chunk_limit = chunk_limit - self.interlace = bool(interlace) - self.palette = check_palette(palette) - - self.color_type = 4*self.alpha + 2*(not greyscale) + 1*self.colormap - assert self.color_type in (0,2,3,4,6) - - self.color_planes = (3,1)[self.greyscale or self.colormap] - self.planes = self.color_planes + self.alpha - # :todo: fix for bitdepth < 8 - self.psize = (self.bitdepth/8) * self.planes - - def make_palette(self): - """Create the byte sequences for a ``PLTE`` and if necessary a - ``tRNS`` chunk. Returned as a pair (*p*, *t*). *t* will be - ``None`` if no ``tRNS`` chunk is necessary. - """ - - p = array('B') - t = array('B') - - for x in self.palette: - p.extend(x[0:3]) - if len(x) > 3: - t.append(x[3]) - p = tostring(p) - t = tostring(t) - if t: - return p,t - return p,None - - def write(self, outfile, rows): - """Write a PNG image to the output file. `rows` should be - an iterable that yields each row in boxed row flat pixel format. - The rows should be the rows of the original image, so there - should be ``self.height`` rows of ``self.width * self.planes`` values. - If `interlace` is specified (when creating the instance), then - an interlaced PNG file will be written. Supply the rows in the - normal image order; the interlacing is carried out internally. - - .. note :: - - Interlacing will require the entire image to be in working memory. - """ - - if self.interlace: - fmt = 'BH'[self.bitdepth > 8] - a = array(fmt, itertools.chain(*rows)) - return self.write_array(outfile, a) - else: - nrows = self.write_passes(outfile, rows) - if nrows != self.height: - raise ValueError( - "rows supplied (%d) does not match height (%d)" % - (nrows, self.height)) - - def write_passes(self, outfile, rows, packed=False): - """ - Write a PNG image to the output file. - - Most users are expected to find the :meth:`write` or - :meth:`write_array` method more convenient. - - The rows should be given to this method in the order that - they appear in the output file. For straightlaced images, - this is the usual top to bottom ordering, but for interlaced - images the rows should have already been interlaced before - passing them to this function. - - `rows` should be an iterable that yields each row. When - `packed` is ``False`` the rows should be in boxed row flat pixel - format; when `packed` is ``True`` each row should be a packed - sequence of bytes. - - """ - - # http://www.w3.org/TR/PNG/#5PNG-file-signature - outfile.write(_signature) - - # http://www.w3.org/TR/PNG/#11IHDR - write_chunk(outfile, 'IHDR', - struct.pack("!2I5B", self.width, self.height, - self.bitdepth, self.color_type, - 0, 0, self.interlace)) - - # See :chunk:order - # http://www.w3.org/TR/PNG/#11gAMA - if self.gamma is not None: - write_chunk(outfile, 'gAMA', - struct.pack("!L", int(round(self.gamma*1e5)))) - - # See :chunk:order - # http://www.w3.org/TR/PNG/#11sBIT - if self.rescale: - write_chunk(outfile, 'sBIT', - struct.pack('%dB' % self.planes, - *[self.rescale[0]]*self.planes)) - - # :chunk:order: Without a palette (PLTE chunk), ordering is - # relatively relaxed. With one, gAMA chunk must precede PLTE - # chunk which must precede tRNS and bKGD. - # See http://www.w3.org/TR/PNG/#5ChunkOrdering - if self.palette: - p,t = self.make_palette() - write_chunk(outfile, 'PLTE', p) - if t: - # tRNS chunk is optional. Only needed if palette entries - # have alpha. - write_chunk(outfile, 'tRNS', t) - - # http://www.w3.org/TR/PNG/#11tRNS - if self.transparent is not None: - if self.greyscale: - write_chunk(outfile, 'tRNS', - struct.pack("!1H", *self.transparent)) - else: - write_chunk(outfile, 'tRNS', - struct.pack("!3H", *self.transparent)) - - # http://www.w3.org/TR/PNG/#11bKGD - if self.background is not None: - if self.greyscale: - write_chunk(outfile, 'bKGD', - struct.pack("!1H", *self.background)) - else: - write_chunk(outfile, 'bKGD', - struct.pack("!3H", *self.background)) - - # http://www.w3.org/TR/PNG/#11IDAT - if self.compression is not None: - compressor = zlib.compressobj(self.compression) - else: - compressor = zlib.compressobj() - - # Choose an extend function based on the bitdepth. The extend - # function packs/decomposes the pixel values into bytes and - # stuffs them onto the data array. - data = array('B') - if self.bitdepth == 8 or packed: - extend = data.extend - elif self.bitdepth == 16: - # Decompose into bytes - def extend(sl): - fmt = '!%dH' % len(sl) - data.extend(array('B', struct.pack(fmt, *sl))) - else: - # Pack into bytes - assert self.bitdepth < 8 - # samples per byte - spb = int(8/self.bitdepth) - def extend(sl): - a = array('B', sl) - # Adding padding bytes so we can group into a whole - # number of spb-tuples. - l = float(len(a)) - extra = math.ceil(l / float(spb))*spb - l - a.extend([0]*int(extra)) - # Pack into bytes - l = group(a, spb) - l = [reduce(lambda x,y: - (x << self.bitdepth) + y, e) for e in l] - data.extend(l) - if self.rescale: - oldextend = extend - factor = \ - float(2**self.rescale[1]-1) / float(2**self.rescale[0]-1) - def extend(sl): - oldextend([int(round(factor*x)) for x in sl]) - - # Build the first row, testing mostly to see if we need to - # changed the extend function to cope with NumPy integer types - # (they cause our ordinary definition of extend to fail, so we - # wrap it). See - # http://code.google.com/p/pypng/issues/detail?id=44 - enumrows = enumerate(rows) - del rows - - # First row's filter type. - data.append(0) - # :todo: Certain exceptions in the call to ``.next()`` or the - # following try would indicate no row data supplied. - # Should catch. - i,row = next(enumrows) - try: - # If this fails... - extend(row) - except: - # ... try a version that converts the values to int first. - # Not only does this work for the (slightly broken) NumPy - # types, there are probably lots of other, unknown, "nearly" - # int types it works for. - def wrapmapint(f): - return lambda sl: f(list(map(int, sl))) - extend = wrapmapint(extend) - del wrapmapint - extend(row) - - for i,row in enumrows: - # Add "None" filter type. Currently, it's essential that - # this filter type be used for every scanline as we do not - # mark the first row of a reduced pass image; that means we - # could accidentally compute the wrong filtered scanline if - # we used "up", "average", or "paeth" on such a line. - data.append(0) - extend(row) - if len(data) > self.chunk_limit: - compressed = compressor.compress(tostring(data)) - if len(compressed): - # print >> sys.stderr, len(data), len(compressed) - write_chunk(outfile, 'IDAT', compressed) - # Because of our very witty definition of ``extend``, - # above, we must re-use the same ``data`` object. Hence - # we use ``del`` to empty this one, rather than create a - # fresh one (which would be my natural FP instinct). - del data[:] - if len(data): - compressed = compressor.compress(tostring(data)) - else: - compressed = '' - flushed = compressor.flush() - if len(compressed) or len(flushed): - # print >> sys.stderr, len(data), len(compressed), len(flushed) - write_chunk(outfile, 'IDAT', compressed + flushed) - # http://www.w3.org/TR/PNG/#11IEND - write_chunk(outfile, 'IEND') - return i+1 - - def write_array(self, outfile, pixels): - """ - Write an array in flat row flat pixel format as a PNG file on - the output file. See also :meth:`write` method. - """ - - if self.interlace: - self.write_passes(outfile, self.array_scanlines_interlace(pixels)) - else: - self.write_passes(outfile, self.array_scanlines(pixels)) - - def write_packed(self, outfile, rows): - """ - Write PNG file to `outfile`. The pixel data comes from `rows` - which should be in boxed row packed format. Each row should be - a sequence of packed bytes. - - Technically, this method does work for interlaced images but it - is best avoided. For interlaced images, the rows should be - presented in the order that they appear in the file. - - This method should not be used when the source image bit depth - is not one naturally supported by PNG; the bit depth should be - 1, 2, 4, 8, or 16. - """ - - if self.rescale: - raise Error("write_packed method not suitable for bit depth %d" % - self.rescale[0]) - return self.write_passes(outfile, rows, packed=True) - - def convert_pnm(self, infile, outfile): - """ - Convert a PNM file containing raw pixel data into a PNG file - with the parameters set in the writer object. Works for - (binary) PGM, PPM, and PAM formats. - """ - - if self.interlace: - pixels = array('B') - pixels.fromfile(infile, - (self.bitdepth/8) * self.color_planes * - self.width * self.height) - self.write_passes(outfile, self.array_scanlines_interlace(pixels)) - else: - self.write_passes(outfile, self.file_scanlines(infile)) - - def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile): - """ - Convert a PPM and PGM file containing raw pixel data into a - PNG outfile with the parameters set in the writer object. - """ - pixels = array('B') - pixels.fromfile(ppmfile, - (self.bitdepth/8) * self.color_planes * - self.width * self.height) - apixels = array('B') - apixels.fromfile(pgmfile, - (self.bitdepth/8) * - self.width * self.height) - pixels = interleave_planes(pixels, apixels, - (self.bitdepth/8) * self.color_planes, - (self.bitdepth/8)) - if self.interlace: - self.write_passes(outfile, self.array_scanlines_interlace(pixels)) - else: - self.write_passes(outfile, self.array_scanlines(pixels)) - - def file_scanlines(self, infile): - """ - Generates boxed rows in flat pixel format, from the input file - `infile`. It assumes that the input file is in a "Netpbm-like" - binary format, and is positioned at the beginning of the first - pixel. The number of pixels to read is taken from the image - dimensions (`width`, `height`, `planes`) and the number of bytes - per value is implied by the image `bitdepth`. - """ - - # Values per row - vpr = self.width * self.planes - row_bytes = vpr - if self.bitdepth > 8: - assert self.bitdepth == 16 - row_bytes *= 2 - fmt = '>%dH' % vpr - def line(): - return array('H', struct.unpack(fmt, infile.read(row_bytes))) - else: - def line(): - scanline = array('B', infile.read(row_bytes)) - return scanline - for y in range(self.height): - yield line() - - def array_scanlines(self, pixels): - """ - Generates boxed rows (flat pixels) from flat rows (flat pixels) - in an array. - """ - - # Values per row - vpr = self.width * self.planes - stop = 0 - for y in range(self.height): - start = stop - stop = start + vpr - yield pixels[start:stop] - - def array_scanlines_interlace(self, pixels): - """ - Generator for interlaced scanlines from an array. `pixels` is - the full source image in flat row flat pixel format. The - generator yields each scanline of the reduced passes in turn, in - boxed row flat pixel format. - """ - - # http://www.w3.org/TR/PNG/#8InterlaceMethods - # Array type. - fmt = 'BH'[self.bitdepth > 8] - # Value per row - vpr = self.width * self.planes - for xstart, ystart, xstep, ystep in _adam7: - if xstart >= self.width: - continue - # Pixels per row (of reduced image) - ppr = int(math.ceil((self.width-xstart)/float(xstep))) - # number of values in reduced image row. - row_len = ppr*self.planes - for y in range(ystart, self.height, ystep): - if xstep == 1: - offset = y * vpr - yield pixels[offset:offset+vpr] - else: - row = array(fmt) - # There's no easier way to set the length of an array - row.extend(pixels[0:row_len]) - offset = y * vpr + xstart * self.planes - end_offset = (y+1) * vpr - skip = self.planes * xstep - for i in range(self.planes): - row[i::self.planes] = \ - pixels[offset+i:end_offset:skip] - yield row - -def write_chunk(outfile, tag, data=strtobytes('')): - """ - Write a PNG chunk to the output file, including length and - checksum. - """ - - # http://www.w3.org/TR/PNG/#5Chunk-layout - outfile.write(struct.pack("!I", len(data))) - tag = strtobytes(tag) - outfile.write(tag) - outfile.write(data) - checksum = zlib.crc32(tag) - checksum = zlib.crc32(data, checksum) - checksum &= 2**32-1 - outfile.write(struct.pack("!I", checksum)) - -def write_chunks(out, chunks): - """Create a PNG file by writing out the chunks.""" - - out.write(_signature) - for chunk in chunks: - write_chunk(out, *chunk) - -def filter_scanline(type, line, fo, prev=None): - """Apply a scanline filter to a scanline. `type` specifies the - filter type (0 to 4); `line` specifies the current (unfiltered) - scanline as a sequence of bytes; `prev` specifies the previous - (unfiltered) scanline as a sequence of bytes. `fo` specifies the - filter offset; normally this is size of a pixel in bytes (the number - of bytes per sample times the number of channels), but when this is - < 1 (for bit depths < 8) then the filter offset is 1. - """ - - assert 0 <= type < 5 - - # The output array. Which, pathetically, we extend one-byte at a - # time (fortunately this is linear). - out = array('B', [type]) - - def sub(): - ai = -fo - for x in line: - if ai >= 0: - x = (x - line[ai]) & 0xff - out.append(x) - ai += 1 - def up(): - for i,x in enumerate(line): - x = (x - prev[i]) & 0xff - out.append(x) - def average(): - ai = -fo - for i,x in enumerate(line): - if ai >= 0: - x = (x - ((line[ai] + prev[i]) >> 1)) & 0xff - else: - x = (x - (prev[i] >> 1)) & 0xff - out.append(x) - ai += 1 - def paeth(): - # http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth - ai = -fo # also used for ci - for i,x in enumerate(line): - a = 0 - b = prev[i] - c = 0 - - if ai >= 0: - a = line[ai] - c = prev[ai] - p = a + b - c - pa = abs(p - a) - pb = abs(p - b) - pc = abs(p - c) - if pa <= pb and pa <= pc: Pr = a - elif pb <= pc: Pr = b - else: Pr = c - - x = (x - Pr) & 0xff - out.append(x) - ai += 1 - - if not prev: - # We're on the first line. Some of the filters can be reduced - # to simpler cases which makes handling the line "off the top" - # of the image simpler. "up" becomes "none"; "paeth" becomes - # "left" (non-trivial, but true). "average" needs to be handled - # specially. - if type == 2: # "up" - type = 0 - elif type == 3: - prev = [0]*len(line) - elif type == 4: # "paeth" - type = 1 - if type == 0: - out.extend(line) - elif type == 1: - sub() - elif type == 2: - up() - elif type == 3: - average() - else: # type == 4 - paeth() - return out - - -def from_array(a, mode=None, info={}): - """Create a PNG :class:`Image` object from a 2- or 3-dimensional array. - One application of this function is easy PIL-style saving: - ``png.from_array(pixels, 'L').save('foo.png')``. - - .. note : - - The use of the term *3-dimensional* is for marketing purposes - only. It doesn't actually work. Please bear with us. Meanwhile - enjoy the complimentary snacks (on request) and please use a - 2-dimensional array. - - Unless they are specified using the *info* parameter, the PNG's - height and width are taken from the array size. For a 3 dimensional - array the first axis is the height; the second axis is the width; - and the third axis is the channel number. Thus an RGB image that is - 16 pixels high and 8 wide will use an array that is 16x8x3. For 2 - dimensional arrays the first axis is the height, but the second axis - is ``width*channels``, so an RGB image that is 16 pixels high and 8 - wide will use a 2-dimensional array that is 16x24 (each row will be - 8*3==24 sample values). - - *mode* is a string that specifies the image colour format in a - PIL-style mode. It can be: - - ``'L'`` - greyscale (1 channel) - ``'LA'`` - greyscale with alpha (2 channel) - ``'RGB'`` - colour image (3 channel) - ``'RGBA'`` - colour image with alpha (4 channel) - - The mode string can also specify the bit depth (overriding how this - function normally derives the bit depth, see below). Appending - ``';16'`` to the mode will cause the PNG to be 16 bits per channel; - any decimal from 1 to 16 can be used to specify the bit depth. - - When a 2-dimensional array is used *mode* determines how many - channels the image has, and so allows the width to be derived from - the second array dimension. - - The array is expected to be a ``numpy`` array, but it can be any - suitable Python sequence. For example, a list of lists can be used: - ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``. The exact - rules are: ``len(a)`` gives the first dimension, height; - ``len(a[0])`` gives the second dimension; ``len(a[0][0])`` gives the - third dimension, unless an exception is raised in which case a - 2-dimensional array is assumed. It's slightly more complicated than - that because an iterator of rows can be used, and it all still - works. Using an iterator allows data to be streamed efficiently. - - The bit depth of the PNG is normally taken from the array element's - datatype (but if *mode* specifies a bitdepth then that is used - instead). The array element's datatype is determined in a way which - is supposed to work both for ``numpy`` arrays and for Python - ``array.array`` objects. A 1 byte datatype will give a bit depth of - 8, a 2 byte datatype will give a bit depth of 16. If the datatype - does not have an implicit size, for example it is a plain Python - list of lists, as above, then a default of 8 is used. - - The *info* parameter is a dictionary that can be used to specify - metadata (in the same style as the arguments to the - :class:``png.Writer`` class). For this function the keys that are - useful are: - - height - overrides the height derived from the array dimensions and allows - *a* to be an iterable. - width - overrides the width derived from the array dimensions. - bitdepth - overrides the bit depth derived from the element datatype (but - must match *mode* if that also specifies a bit depth). - - Generally anything specified in the - *info* dictionary will override any implicit choices that this - function would otherwise make, but must match any explicit ones. - For example, if the *info* dictionary has a ``greyscale`` key then - this must be true when mode is ``'L'`` or ``'LA'`` and false when - mode is ``'RGB'`` or ``'RGBA'``. - """ - - # We abuse the *info* parameter by modifying it. Take a copy here. - # (Also typechecks *info* to some extent). - info = dict(info) - - # Syntax check mode string. - bitdepth = None - try: - mode = mode.split(';') - if len(mode) not in (1,2): - raise Error() - if mode[0] not in ('L', 'LA', 'RGB', 'RGBA'): - raise Error() - if len(mode) == 2: - try: - bitdepth = int(mode[1]) - except: - raise Error() - except Error: - raise Error("mode string should be 'RGB' or 'L;16' or similar.") - mode = mode[0] - - # Get bitdepth from *mode* if possible. - if bitdepth: - if info.get('bitdepth') and bitdepth != info['bitdepth']: - raise Error("mode bitdepth (%d) should match info bitdepth (%d)." % - (bitdepth, info['bitdepth'])) - info['bitdepth'] = bitdepth - - # Fill in and/or check entries in *info*. - # Dimensions. - if 'size' in info: - # Check width, height, size all match where used. - for dimension,axis in [('width', 0), ('height', 1)]: - if dimension in info: - if info[dimension] != info['size'][axis]: - raise Error( - "info[%r] shhould match info['size'][%r]." % - (dimension, axis)) - info['width'],info['height'] = info['size'] - if 'height' not in info: - try: - l = len(a) - except: - raise Error( - "len(a) does not work, supply info['height'] instead.") - info['height'] = l - # Colour format. - if 'greyscale' in info: - if bool(info['greyscale']) != ('L' in mode): - raise Error("info['greyscale'] should match mode.") - info['greyscale'] = 'L' in mode - if 'alpha' in info: - if bool(info['alpha']) != ('A' in mode): - raise Error("info['alpha'] should match mode.") - info['alpha'] = 'A' in mode - - planes = len(mode) - if 'planes' in info: - if info['planes'] != planes: - raise Error("info['planes'] should match mode.") - - # In order to work out whether we the array is 2D or 3D we need its - # first row, which requires that we take a copy of its iterator. - # We may also need the first row to derive width and bitdepth. - a,t = itertools.tee(a) - row = next(t) - del t - try: - row[0][0] - threed = True - testelement = row[0] - except: - threed = False - testelement = row - if 'width' not in info: - if threed: - width = len(row) - else: - width = len(row) // planes - info['width'] = width - - # Not implemented yet - assert not threed - - if 'bitdepth' not in info: - try: - dtype = testelement.dtype - # goto the "else:" clause. Sorry. - except: - try: - # Try a Python array.array. - bitdepth = 8 * testelement.itemsize - except: - # We can't determine it from the array element's - # datatype, use a default of 8. - bitdepth = 8 - else: - # If we got here without exception, we now assume that - # the array is a numpy array. - if dtype.kind == 'b': - bitdepth = 1 - else: - bitdepth = 8 * dtype.itemsize - info['bitdepth'] = bitdepth - - for thing in 'width height bitdepth greyscale alpha'.split(): - assert thing in info - return Image(a, info) - -# So that refugee's from PIL feel more at home. Not documented. -fromarray = from_array - -class Image: - """A PNG image. - You can create an :class:`Image` object from an array of pixels by calling - :meth:`png.from_array`. It can be saved to disk with the - :meth:`save` method.""" - def __init__(self, rows, info): - """ - .. note :: - - The constructor is not public. Please do not call it. - """ - - self.rows = rows - self.info = info - - def save(self, file): - """Save the image to *file*. If *file* looks like an open file - descriptor then it is used, otherwise it is treated as a - filename and a fresh file is opened. - - In general, you can only call this method once; after it has - been called the first time and the PNG image has been saved, the - source data will have been streamed, and cannot be streamed - again. - """ - - w = Writer(**self.info) - - try: - file.write - def close(): pass - except: - file = open(file, 'wb') - def close(): file.close() - - try: - w.write(file, self.rows) - finally: - close() - -class _readable: - """ - A simple file-like interface for strings and arrays. - """ - - def __init__(self, buf): - self.buf = buf - self.offset = 0 - - def read(self, n): - r = self.buf[self.offset:self.offset+n] - if isarray(r): - r = r.tostring() - self.offset += n - return r - - -class Reader: - """ - PNG decoder in pure Python. - """ - - def __init__(self, _guess=None, **kw): - """ - Create a PNG decoder object. - - The constructor expects exactly one keyword argument. If you - supply a positional argument instead, it will guess the input - type. You can choose among the following keyword arguments: - - filename - Name of input file (a PNG file). - file - A file-like object (object with a read() method). - bytes - ``array`` or ``string`` with PNG data. - - """ - if ((_guess is not None and len(kw) != 0) or - (_guess is None and len(kw) != 1)): - raise TypeError("Reader() takes exactly 1 argument") - - # Will be the first 8 bytes, later on. See validate_signature. - self.signature = None - self.transparent = None - # A pair of (len,type) if a chunk has been read but its data and - # checksum have not (in other words the file position is just - # past the 4 bytes that specify the chunk type). See preamble - # method for how this is used. - self.atchunk = None - - if _guess is not None: - if isarray(_guess): - kw["bytes"] = _guess - elif isinstance(_guess, str): - kw["filename"] = _guess - elif hasattr(_guess, 'read'): - kw["file"] = _guess - - if "filename" in kw: - self.file = open(kw["filename"], "rb") - elif "file" in kw: - self.file = kw["file"] - elif "bytes" in kw: - self.file = _readable(kw["bytes"]) - else: - raise TypeError("expecting filename, file or bytes array") - - - def chunk(self, seek=None, lenient=False): - """ - Read the next PNG chunk from the input file; returns a - (*type*,*data*) tuple. *type* is the chunk's type as a string - (all PNG chunk types are 4 characters long). *data* is the - chunk's data content, as a string. - - If the optional `seek` argument is - specified then it will keep reading chunks until it either runs - out of file or finds the type specified by the argument. Note - that in general the order of chunks in PNGs is unspecified, so - using `seek` can cause you to miss chunks. - - If the optional `lenient` argument evaluates to True, - checksum failures will raise warnings rather than exceptions. - """ - - self.validate_signature() - - while True: - # http://www.w3.org/TR/PNG/#5Chunk-layout - if not self.atchunk: - self.atchunk = self.chunklentype() - length,type = self.atchunk - self.atchunk = None - data = self.file.read(length) - if len(data) != length: - raise ChunkError('Chunk %s too short for required %i octets.' - % (type, length)) - checksum = self.file.read(4) - if len(checksum) != 4: - raise ValueError('Chunk %s too short for checksum.', tag) - if seek and type != seek: - continue - verify = zlib.crc32(strtobytes(type)) - verify = zlib.crc32(data, verify) - # Whether the output from zlib.crc32 is signed or not varies - # according to hideous implementation details, see - # http://bugs.python.org/issue1202 . - # We coerce it to be positive here (in a way which works on - # Python 2.3 and older). - verify &= 2**32 - 1 - verify = struct.pack('!I', verify) - if checksum != verify: - # print repr(checksum) - (a, ) = struct.unpack('!I', checksum) - (b, ) = struct.unpack('!I', verify) - message = "Checksum error in %s chunk: 0x%08X != 0x%08X." % (type, a, b) - if lenient: - warnings.warn(message, RuntimeWarning) - else: - raise ChunkError(message) - return type, data - - def chunks(self): - """Return an iterator that will yield each chunk as a - (*chunktype*, *content*) pair. - """ - - while True: - t,v = self.chunk() - yield t,v - if t == 'IEND': - break - - def undo_filter(self, filter_type, scanline, previous): - """Undo the filter for a scanline. `scanline` is a sequence of - bytes that does not include the initial filter type byte. - `previous` is decoded previous scanline (for straightlaced - images this is the previous pixel row, but for interlaced - images, it is the previous scanline in the reduced image, which - in general is not the previous pixel row in the final image). - When there is no previous scanline (the first row of a - straightlaced image, or the first row in one of the passes in an - interlaced image), then this argument should be ``None``. - - The scanline will have the effects of filtering removed, and the - result will be returned as a fresh sequence of bytes. - """ - - # :todo: Would it be better to update scanline in place? - # Yes, with the Cython extension making the undo_filter fast, - # updating scanline inplace makes the code 3 times faster - # (reading 50 images of 800x800 went from 40s to 16s) - result = scanline - - if filter_type == 0: - return result - - if filter_type not in (1,2,3,4): - raise FormatError('Invalid PNG Filter Type.' - ' See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .') - - # Filter unit. The stride from one pixel to the corresponding - # byte from the previous previous. Normally this is the pixel - # size in bytes, but when this is smaller than 1, the previous - # byte is used instead. - fu = max(1, self.psize) - - # For the first line of a pass, synthesize a dummy previous - # line. An alternative approach would be to observe that on the - # first line 'up' is the same as 'null', 'paeth' is the same - # as 'sub', with only 'average' requiring any special case. - if not previous: - previous = array('B', [0]*len(scanline)) - - def sub(): - """Undo sub filter.""" - - ai = 0 - # Loops starts at index fu. Observe that the initial part - # of the result is already filled in correctly with - # scanline. - for i in range(fu, len(result)): - x = scanline[i] - a = result[ai] - result[i] = (x + a) & 0xff - ai += 1 - - def up(): - """Undo up filter.""" - - for i in range(len(result)): - x = scanline[i] - b = previous[i] - result[i] = (x + b) & 0xff - - def average(): - """Undo average filter.""" - - ai = -fu - for i in range(len(result)): - x = scanline[i] - if ai < 0: - a = 0 - else: - a = result[ai] - b = previous[i] - result[i] = (x + ((a + b) >> 1)) & 0xff - ai += 1 - - def paeth(): - """Undo Paeth filter.""" - - # Also used for ci. - ai = -fu - for i in range(len(result)): - x = scanline[i] - if ai < 0: - a = c = 0 - else: - a = result[ai] - c = previous[ai] - b = previous[i] - p = a + b - c - pa = abs(p - a) - pb = abs(p - b) - pc = abs(p - c) - if pa <= pb and pa <= pc: - pr = a - elif pb <= pc: - pr = b - else: - pr = c - result[i] = (x + pr) & 0xff - ai += 1 - - # Call appropriate filter algorithm. Note that 0 has already - # been dealt with. - (None, - pngfilters.undo_filter_sub, - pngfilters.undo_filter_up, - pngfilters.undo_filter_average, - pngfilters.undo_filter_paeth)[filter_type](fu, scanline, previous, result) - return result - - def deinterlace(self, raw): - """ - Read raw pixel data, undo filters, deinterlace, and flatten. - Return in flat row flat pixel format. - """ - - # print >> sys.stderr, ("Reading interlaced, w=%s, r=%s, planes=%s," + - # " bpp=%s") % (self.width, self.height, self.planes, self.bps) - # Values per row (of the target image) - vpr = self.width * self.planes - - # Make a result array, and make it big enough. Interleaving - # writes to the output array randomly (well, not quite), so the - # entire output array must be in memory. - fmt = 'BH'[self.bitdepth > 8] - a = array(fmt, [0]*vpr*self.height) - source_offset = 0 - - for xstart, ystart, xstep, ystep in _adam7: - # print >> sys.stderr, "Adam7: start=%s,%s step=%s,%s" % ( - # xstart, ystart, xstep, ystep) - if xstart >= self.width: - continue - # The previous (reconstructed) scanline. None at the - # beginning of a pass to indicate that there is no previous - # line. - recon = None - # Pixels per row (reduced pass image) - ppr = int(math.ceil((self.width-xstart)/float(xstep))) - # Row size in bytes for this pass. - row_size = int(math.ceil(self.psize * ppr)) - for y in range(ystart, self.height, ystep): - filter_type = raw[source_offset] - source_offset += 1 - scanline = raw[source_offset:source_offset+row_size] - source_offset += row_size - recon = self.undo_filter(filter_type, scanline, recon) - # Convert so that there is one element per pixel value - flat = self.serialtoflat(recon, ppr) - if xstep == 1: - assert xstart == 0 - offset = y * vpr - a[offset:offset+vpr] = flat - else: - offset = y * vpr + xstart * self.planes - end_offset = (y+1) * vpr - skip = self.planes * xstep - for i in range(self.planes): - a[offset+i:end_offset:skip] = \ - flat[i::self.planes] - return a - - def iterboxed(self, rows): - """Iterator that yields each scanline in boxed row flat pixel - format. `rows` should be an iterator that yields the bytes of - each row in turn. - """ - - def asvalues(raw): - """Convert a row of raw bytes into a flat row. Result may - or may not share with argument""" - - if self.bitdepth == 8: - return raw - if self.bitdepth == 16: - raw = tostring(raw) - return array('H', struct.unpack('!%dH' % (len(raw)//2), raw)) - assert self.bitdepth < 8 - width = self.width - # Samples per byte - spb = 8//self.bitdepth - out = array('B') - mask = 2**self.bitdepth - 1 - shifts = list(map(self.bitdepth.__mul__, reversed(list(range(spb))))) - for o in raw: - out.extend([mask&(o>>i) for i in shifts]) - return out[:width] - - return map(asvalues, rows) - - def serialtoflat(self, bytes, width=None): - """Convert serial format (byte stream) pixel data to flat row - flat pixel. - """ - - if self.bitdepth == 8: - return bytes - if self.bitdepth == 16: - bytes = tostring(bytes) - return array('H', - struct.unpack('!%dH' % (len(bytes)//2), bytes)) - assert self.bitdepth < 8 - if width is None: - width = self.width - # Samples per byte - spb = 8//self.bitdepth - out = array('B') - mask = 2**self.bitdepth - 1 - shifts = list(map(self.bitdepth.__mul__, reversed(list(range(spb))))) - l = width - for o in bytes: - out.extend([(mask&(o>>s)) for s in shifts][:l]) - l -= spb - if l <= 0: - l = width - return out - - def iterstraight(self, raw): - """Iterator that undoes the effect of filtering, and yields each - row in serialised format (as a sequence of bytes). Assumes input - is straightlaced. `raw` should be an iterable that yields the - raw bytes in chunks of arbitrary size.""" - - # length of row, in bytes - rb = self.row_bytes - a = array('B') - # The previous (reconstructed) scanline. None indicates first - # line of image. - recon = None - for some in raw: - a.extend(some) - while len(a) >= rb + 1: - filter_type = a[0] - scanline = a[1:rb+1] - del a[:rb+1] - recon = self.undo_filter(filter_type, scanline, recon) - yield recon - if len(a) != 0: - # :file:format We get here with a file format error: when the - # available bytes (after decompressing) do not pack into exact - # rows. - raise FormatError( - 'Wrong size for decompressed IDAT chunk.') - assert len(a) == 0 - - def validate_signature(self): - """If signature (header) has not been read then read and - validate it; otherwise do nothing. - """ - - if self.signature: - return - self.signature = self.file.read(8) - if self.signature != _signature: - raise FormatError("PNG file has invalid signature.") - - def preamble(self, lenient=False): - """ - Extract the image metadata by reading the initial part of the PNG - file up to the start of the ``IDAT`` chunk. All the chunks that - precede the ``IDAT`` chunk are read and either processed for - metadata or discarded. - - If the optional `lenient` argument evaluates to True, - checksum failures will raise warnings rather than exceptions. - """ - - self.validate_signature() - - while True: - if not self.atchunk: - self.atchunk = self.chunklentype() - if self.atchunk is None: - raise FormatError( - 'This PNG file has no IDAT chunks.') - if self.atchunk[1] == 'IDAT': - return - self.process_chunk(lenient=lenient) - - def chunklentype(self): - """Reads just enough of the input to determine the next - chunk's length and type, returned as a (*length*, *type*) pair - where *type* is a string. If there are no more chunks, ``None`` - is returned. - """ - - x = self.file.read(8) - if not x: - return None - if len(x) != 8: - raise FormatError( - 'End of file whilst reading chunk length and type.') - length,type = struct.unpack('!I4s', x) - type = bytestostr(type) - if length > 2**31-1: - raise FormatError('Chunk %s is too large: %d.' % (type,length)) - return length,type - - def process_chunk(self, lenient=False): - """Process the next chunk and its data. This only processes the - following chunk types, all others are ignored: ``IHDR``, - ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``. - - If the optional `lenient` argument evaluates to True, - checksum failures will raise warnings rather than exceptions. - """ - - type, data = self.chunk(lenient=lenient) - if type == 'IHDR': - # http://www.w3.org/TR/PNG/#11IHDR - if len(data) != 13: - raise FormatError('IHDR chunk has incorrect length.') - (self.width, self.height, self.bitdepth, self.color_type, - self.compression, self.filter, - self.interlace) = struct.unpack("!2I5B", data) - - # Check that the header specifies only valid combinations. - if self.bitdepth not in (1,2,4,8,16): - raise Error("invalid bit depth %d" % self.bitdepth) - if self.color_type not in (0,2,3,4,6): - raise Error("invalid colour type %d" % self.color_type) - # Check indexed (palettized) images have 8 or fewer bits - # per pixel; check only indexed or greyscale images have - # fewer than 8 bits per pixel. - if ((self.color_type & 1 and self.bitdepth > 8) or - (self.bitdepth < 8 and self.color_type not in (0,3))): - raise FormatError("Illegal combination of bit depth (%d)" - " and colour type (%d)." - " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." - % (self.bitdepth, self.color_type)) - if self.compression != 0: - raise Error("unknown compression method %d" % self.compression) - if self.filter != 0: - raise FormatError("Unknown filter method %d," - " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." - % self.filter) - if self.interlace not in (0,1): - raise FormatError("Unknown interlace method %d," - " see http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods ." - % self.interlace) - - # Derived values - # http://www.w3.org/TR/PNG/#6Colour-values - colormap = bool(self.color_type & 1) - greyscale = not (self.color_type & 2) - alpha = bool(self.color_type & 4) - color_planes = (3,1)[greyscale or colormap] - planes = color_planes + alpha - - self.colormap = colormap - self.greyscale = greyscale - self.alpha = alpha - self.color_planes = color_planes - self.planes = planes - self.psize = float(self.bitdepth)/float(8) * planes - if int(self.psize) == self.psize: - self.psize = int(self.psize) - self.row_bytes = int(math.ceil(self.width * self.psize)) - # Stores PLTE chunk if present, and is used to check - # chunk ordering constraints. - self.plte = None - # Stores tRNS chunk if present, and is used to check chunk - # ordering constraints. - self.trns = None - # Stores sbit chunk if present. - self.sbit = None - elif type == 'PLTE': - # http://www.w3.org/TR/PNG/#11PLTE - if self.plte: - warnings.warn("Multiple PLTE chunks present.") - self.plte = data - if len(data) % 3 != 0: - raise FormatError( - "PLTE chunk's length should be a multiple of 3.") - if len(data) > (2**self.bitdepth)*3: - raise FormatError("PLTE chunk is too long.") - if len(data) == 0: - raise FormatError("Empty PLTE is not allowed.") - elif type == 'bKGD': - try: - if self.colormap: - if not self.plte: - warnings.warn( - "PLTE chunk is required before bKGD chunk.") - self.background = struct.unpack('B', data) - else: - self.background = struct.unpack("!%dH" % self.color_planes, - data) - except struct.error: - raise FormatError("bKGD chunk has incorrect length.") - elif type == 'tRNS': - # http://www.w3.org/TR/PNG/#11tRNS - self.trns = data - if self.colormap: - if not self.plte: - warnings.warn("PLTE chunk is required before tRNS chunk.") - else: - if len(data) > len(self.plte)/3: - # Was warning, but promoted to Error as it - # would otherwise cause pain later on. - raise FormatError("tRNS chunk is too long.") - else: - if self.alpha: - raise FormatError( - "tRNS chunk is not valid with colour type %d." % - self.color_type) - try: - self.transparent = \ - struct.unpack("!%dH" % self.color_planes, data) - except struct.error: - raise FormatError("tRNS chunk has incorrect length.") - elif type == 'gAMA': - try: - self.gamma = struct.unpack("!L", data)[0] / 100000.0 - except struct.error: - raise FormatError("gAMA chunk has incorrect length.") - elif type == 'sBIT': - self.sbit = data - if (self.colormap and len(data) != 3 or - not self.colormap and len(data) != self.planes): - raise FormatError("sBIT chunk has incorrect length.") - - def read(self, lenient=False): - """ - Read the PNG file and decode it. Returns (`width`, `height`, - `pixels`, `metadata`). - - May use excessive memory. - - `pixels` are returned in boxed row flat pixel format. - - If the optional `lenient` argument evaluates to True, - checksum failures will raise warnings rather than exceptions. - """ - - def iteridat(): - """Iterator that yields all the ``IDAT`` chunks as strings.""" - while True: - try: - type, data = self.chunk(lenient=lenient) - except ValueError as e: - raise ChunkError(e.args[0]) - if type == 'IEND': - # http://www.w3.org/TR/PNG/#11IEND - break - if type != 'IDAT': - continue - # type == 'IDAT' - # http://www.w3.org/TR/PNG/#11IDAT - if self.colormap and not self.plte: - warnings.warn("PLTE chunk is required before IDAT chunk") - yield data - - def iterdecomp(idat): - """Iterator that yields decompressed strings. `idat` should - be an iterator that yields the ``IDAT`` chunk data. - """ - - # Currently, with no max_length paramter to decompress, this - # routine will do one yield per IDAT chunk. So not very - # incremental. - d = zlib.decompressobj() - # Each IDAT chunk is passed to the decompressor, then any - # remaining state is decompressed out. - for data in idat: - # :todo: add a max_length argument here to limit output - # size. - yield array('B', d.decompress(data)) - yield array('B', d.flush()) - - self.preamble(lenient=lenient) - raw = iterdecomp(iteridat()) - - if self.interlace: - raw = array('B', itertools.chain(*raw)) - arraycode = 'BH'[self.bitdepth>8] - # Like :meth:`group` but producing an array.array object for - # each row. - pixels = map(lambda *row: array(arraycode, row), - *[iter(self.deinterlace(raw))]*self.width*self.planes) - else: - pixels = self.iterboxed(self.iterstraight(raw)) - meta = dict() - for attr in 'greyscale alpha planes bitdepth interlace'.split(): - meta[attr] = getattr(self, attr) - meta['size'] = (self.width, self.height) - for attr in 'gamma transparent background'.split(): - a = getattr(self, attr, None) - if a is not None: - meta[attr] = a - if self.plte: - meta['palette'] = self.palette() - return self.width, self.height, pixels, meta - - - def read_flat(self): - """ - Read a PNG file and decode it into flat row flat pixel format. - Returns (*width*, *height*, *pixels*, *metadata*). - - May use excessive memory. - - `pixels` are returned in flat row flat pixel format. - - See also the :meth:`read` method which returns pixels in the - more stream-friendly boxed row flat pixel format. - """ - - x, y, pixel, meta = self.read() - arraycode = 'BH'[meta['bitdepth']>8] - pixel = array(arraycode, itertools.chain(*pixel)) - return x, y, pixel, meta - - def palette(self, alpha='natural'): - """Returns a palette that is a sequence of 3-tuples or 4-tuples, - synthesizing it from the ``PLTE`` and ``tRNS`` chunks. These - chunks should have already been processed (for example, by - calling the :meth:`preamble` method). All the tuples are the - same size: 3-tuples if there is no ``tRNS`` chunk, 4-tuples when - there is a ``tRNS`` chunk. Assumes that the image is colour type - 3 and therefore a ``PLTE`` chunk is required. - - If the `alpha` argument is ``'force'`` then an alpha channel is - always added, forcing the result to be a sequence of 4-tuples. - """ - - if not self.plte: - raise FormatError( - "Required PLTE chunk is missing in colour type 3 image.") - plte = group(array('B', self.plte), 3) - if self.trns or alpha == 'force': - trns = array('B', self.trns or '') - trns.extend([255]*(len(plte)-len(trns))) - plte = list(map(operator.add, plte, group(trns, 1))) - return plte - - def asDirect(self): - """Returns the image data as a direct representation of an - ``x * y * planes`` array. This method is intended to remove the - need for callers to deal with palettes and transparency - themselves. Images with a palette (colour type 3) - are converted to RGB or RGBA; images with transparency (a - ``tRNS`` chunk) are converted to LA or RGBA as appropriate. - When returned in this format the pixel values represent the - colour value directly without needing to refer to palettes or - transparency information. - - Like the :meth:`read` method this method returns a 4-tuple: - - (*width*, *height*, *pixels*, *meta*) - - This method normally returns pixel values with the bit depth - they have in the source image, but when the source PNG has an - ``sBIT`` chunk it is inspected and can reduce the bit depth of - the result pixels; pixel values will be reduced according to - the bit depth specified in the ``sBIT`` chunk (PNG nerds should - note a single result bit depth is used for all channels; the - maximum of the ones specified in the ``sBIT`` chunk. An RGB565 - image will be rescaled to 6-bit RGB666). - - The *meta* dictionary that is returned reflects the `direct` - format and not the original source image. For example, an RGB - source image with a ``tRNS`` chunk to represent a transparent - colour, will have ``planes=3`` and ``alpha=False`` for the - source image, but the *meta* dictionary returned by this method - will have ``planes=4`` and ``alpha=True`` because an alpha - channel is synthesized and added. - - *pixels* is the pixel data in boxed row flat pixel format (just - like the :meth:`read` method). - - All the other aspects of the image data are not changed. - """ - - self.preamble() - - # Simple case, no conversion necessary. - if not self.colormap and not self.trns and not self.sbit: - return self.read() - - x,y,pixels,meta = self.read() - - if self.colormap: - meta['colormap'] = False - meta['alpha'] = bool(self.trns) - meta['bitdepth'] = 8 - meta['planes'] = 3 + bool(self.trns) - plte = self.palette() - def iterpal(pixels): - for row in pixels: - row = list(map(plte.__getitem__, row)) - yield array('B', itertools.chain(*row)) - pixels = iterpal(pixels) - elif self.trns: - # It would be nice if there was some reasonable way of doing - # this without generating a whole load of intermediate tuples. - # But tuples does seem like the easiest way, with no other way - # clearly much simpler or much faster. (Actually, the L to LA - # conversion could perhaps go faster (all those 1-tuples!), but - # I still wonder whether the code proliferation is worth it) - it = self.transparent - maxval = 2**meta['bitdepth']-1 - planes = meta['planes'] - meta['alpha'] = True - meta['planes'] += 1 - typecode = 'BH'[meta['bitdepth']>8] - def itertrns(pixels): - for row in pixels: - # For each row we group it into pixels, then form a - # characterisation vector that says whether each pixel - # is opaque or not. Then we convert True/False to - # 0/maxval (by multiplication), and add it as the extra - # channel. - row = group(row, planes) - opa = list(map(it.__ne__, row)) - opa = list(map(maxval.__mul__, opa)) - opa = list(zip(opa)) # convert to 1-tuples - yield array(typecode, - itertools.chain(*list(map(operator.add, row, opa)))) - pixels = itertrns(pixels) - targetbitdepth = None - if self.sbit: - sbit = struct.unpack('%dB' % len(self.sbit), self.sbit) - targetbitdepth = max(sbit) - if targetbitdepth > meta['bitdepth']: - raise Error('sBIT chunk %r exceeds bitdepth %d' % - (sbit,self.bitdepth)) - if min(sbit) <= 0: - raise Error('sBIT chunk %r has a 0-entry' % sbit) - if targetbitdepth == meta['bitdepth']: - targetbitdepth = None - if targetbitdepth: - shift = meta['bitdepth'] - targetbitdepth - meta['bitdepth'] = targetbitdepth - def itershift(pixels): - for row in pixels: - yield list(map(shift.__rrshift__, row)) - pixels = itershift(pixels) - return x,y,pixels,meta - - def asFloat(self, maxval=1.0): - """Return image pixels as per :meth:`asDirect` method, but scale - all pixel values to be floating point values between 0.0 and - *maxval*. - """ - - x,y,pixels,info = self.asDirect() - sourcemaxval = 2**info['bitdepth']-1 - del info['bitdepth'] - info['maxval'] = float(maxval) - factor = float(maxval)/float(sourcemaxval) - def iterfloat(): - for row in pixels: - yield list(map(factor.__mul__, row)) - return x,y,iterfloat(),info - - def _as_rescale(self, get, targetbitdepth): - """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`.""" - - width,height,pixels,meta = get() - maxval = 2**meta['bitdepth'] - 1 - targetmaxval = 2**targetbitdepth - 1 - factor = float(targetmaxval) / float(maxval) - meta['bitdepth'] = targetbitdepth - def iterscale(): - for row in pixels: - yield [int(round(x*factor)) for x in row] - if maxval == targetmaxval: - return width, height, pixels, meta - else: - return width, height, iterscale(), meta - - def asRGB8(self): - """Return the image data as an RGB pixels with 8-bits per - sample. This is like the :meth:`asRGB` method except that - this method additionally rescales the values so that they - are all between 0 and 255 (8-bit). In the case where the - source image has a bit depth < 8 the transformation preserves - all the information; where the source image has bit depth - > 8, then rescaling to 8-bit values loses precision. No - dithering is performed. Like :meth:`asRGB`, an alpha channel - in the source image will raise an exception. - - This function returns a 4-tuple: - (*width*, *height*, *pixels*, *metadata*). - *width*, *height*, *metadata* are as per the :meth:`read` method. - - *pixels* is the pixel data in boxed row flat pixel format. - """ - - return self._as_rescale(self.asRGB, 8) - - def asRGBA8(self): - """Return the image data as RGBA pixels with 8-bits per - sample. This method is similar to :meth:`asRGB8` and - :meth:`asRGBA`: The result pixels have an alpha channel, *and* - values are rescaled to the range 0 to 255. The alpha channel is - synthesized if necessary (with a small speed penalty). - """ - - return self._as_rescale(self.asRGBA, 8) - - def asRGB(self): - """Return image as RGB pixels. RGB colour images are passed - through unchanged; greyscales are expanded into RGB - triplets (there is a small speed overhead for doing this). - - An alpha channel in the source image will raise an - exception. - - The return values are as for the :meth:`read` method - except that the *metadata* reflect the returned pixels, not the - source image. In particular, for this method - ``metadata['greyscale']`` will be ``False``. - """ - - width,height,pixels,meta = self.asDirect() - if meta['alpha']: - raise Error("will not convert image with alpha channel to RGB") - if not meta['greyscale']: - return width,height,pixels,meta - meta['greyscale'] = False - typecode = 'BH'[meta['bitdepth'] > 8] - def iterrgb(): - for row in pixels: - a = array(typecode, [0]) * 3 * width - for i in range(3): - a[i::3] = row - yield a - return width,height,iterrgb(),meta - - def asRGBA(self): - """Return image as RGBA pixels. Greyscales are expanded into - RGB triplets; an alpha channel is synthesized if necessary. - The return values are as for the :meth:`read` method - except that the *metadata* reflect the returned pixels, not the - source image. In particular, for this method - ``metadata['greyscale']`` will be ``False``, and - ``metadata['alpha']`` will be ``True``. - """ - - width,height,pixels,meta = self.asDirect() - if meta['alpha'] and not meta['greyscale']: - return width,height,pixels,meta - typecode = 'BH'[meta['bitdepth'] > 8] - maxval = 2**meta['bitdepth'] - 1 - maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width - def newarray(): - return array(typecode, maxbuffer) - - if meta['alpha'] and meta['greyscale']: - # LA to RGBA - def convert(): - for row in pixels: - # Create a fresh target row, then copy L channel - # into first three target channels, and A channel - # into fourth channel. - a = newarray() - pngfilters.convert_la_to_rgba(row, a) - yield a - elif meta['greyscale']: - # L to RGBA - def convert(): - for row in pixels: - a = newarray() - pngfilters.convert_l_to_rgba(row, a) - yield a - else: - assert not meta['alpha'] and not meta['greyscale'] - # RGB to RGBA - def convert(): - for row in pixels: - a = newarray() - pngfilters.convert_rgb_to_rgba(row, a) - yield a - meta['alpha'] = True - meta['greyscale'] = False - return width,height,convert(),meta - - -# === Legacy Version Support === - -# :pyver:old: PyPNG works on Python versions 2.3 and 2.2, but not -# without some awkward problems. Really PyPNG works on Python 2.4 (and -# above); it works on Pythons 2.3 and 2.2 by virtue of fixing up -# problems here. It's a bit ugly (which is why it's hidden down here). -# -# Generally the strategy is one of pretending that we're running on -# Python 2.4 (or above), and patching up the library support on earlier -# versions so that it looks enough like Python 2.4. When it comes to -# Python 2.2 there is one thing we cannot patch: extended slices -# http://www.python.org/doc/2.3/whatsnew/section-slices.html. -# Instead we simply declare that features that are implemented using -# extended slices will not work on Python 2.2. -# -# In order to work on Python 2.3 we fix up a recurring annoyance involving -# the array type. In Python 2.3 an array cannot be initialised with an -# array, and it cannot be extended with a list (or other sequence). -# Both of those are repeated issues in the code. Whilst I would not -# normally tolerate this sort of behaviour, here we "shim" a replacement -# for array into place (and hope no-ones notices). You never read this. -# -# In an amusing case of warty hacks on top of warty hacks... the array -# shimming we try and do only works on Python 2.3 and above (you can't -# subclass array.array in Python 2.2). So to get it working on Python -# 2.2 we go for something much simpler and (probably) way slower. -try: - array('B').extend([]) - array('B', array('B')) -except: - # Expect to get here on Python 2.3 - try: - class _array_shim(array): - true_array = array - def __new__(cls, typecode, init=None): - super_new = super(_array_shim, cls).__new__ - it = super_new(cls, typecode) - if init is None: - return it - it.extend(init) - return it - def extend(self, extension): - super_extend = super(_array_shim, self).extend - if isinstance(extension, self.true_array): - return super_extend(extension) - if not isinstance(extension, (list, str)): - # Convert to list. Allows iterators to work. - extension = list(extension) - return super_extend(self.true_array(self.typecode, extension)) - array = _array_shim - except: - # Expect to get here on Python 2.2 - def array(typecode, init=()): - if type(init) == str: - return list(map(ord, init)) - return list(init) - -# Further hacks to get it limping along on Python 2.2 -try: - enumerate -except: - def enumerate(seq): - i=0 - for x in seq: - yield i,x - i += 1 - -try: - reversed -except: - def reversed(l): - l = list(l) - l.reverse() - for x in l: - yield x - -try: - itertools -except: - class _dummy_itertools: - pass - itertools = _dummy_itertools() - def _itertools_imap(f, seq): - for x in seq: - yield f(x) - itertools.imap = _itertools_imap - def _itertools_chain(*iterables): - for it in iterables: - for element in it: - yield element - itertools.chain = _itertools_chain - - -# === Support for users without Cython === - -try: - pngfilters -except: - class pngfilters(object): - def undo_filter_sub(filter_unit, scanline, previous, result): - """Undo sub filter.""" - - ai = 0 - # Loops starts at index fu. Observe that the initial part - # of the result is already filled in correctly with - # scanline. - for i in range(filter_unit, len(result)): - x = scanline[i] - a = result[ai] - result[i] = (x + a) & 0xff - ai += 1 - undo_filter_sub = staticmethod(undo_filter_sub) - - def undo_filter_up(filter_unit, scanline, previous, result): - """Undo up filter.""" - - for i in range(len(result)): - x = scanline[i] - b = previous[i] - result[i] = (x + b) & 0xff - undo_filter_up = staticmethod(undo_filter_up) - - def undo_filter_average(filter_unit, scanline, previous, result): - """Undo up filter.""" - - ai = -filter_unit - for i in range(len(result)): - x = scanline[i] - if ai < 0: - a = 0 - else: - a = result[ai] - b = previous[i] - result[i] = (x + ((a + b) >> 1)) & 0xff - ai += 1 - undo_filter_average = staticmethod(undo_filter_average) - - def undo_filter_paeth(filter_unit, scanline, previous, result): - """Undo Paeth filter.""" - - # Also used for ci. - ai = -filter_unit - for i in range(len(result)): - x = scanline[i] - if ai < 0: - a = c = 0 - else: - a = result[ai] - c = previous[ai] - b = previous[i] - p = a + b - c - pa = abs(p - a) - pb = abs(p - b) - pc = abs(p - c) - if pa <= pb and pa <= pc: - pr = a - elif pb <= pc: - pr = b - else: - pr = c - result[i] = (x + pr) & 0xff - ai += 1 - undo_filter_paeth = staticmethod(undo_filter_paeth) - - def convert_la_to_rgba(row, result): - for i in range(3): - result[i::4] = row[0::2] - result[3::4] = row[1::2] - convert_la_to_rgba = staticmethod(convert_la_to_rgba) - - def convert_l_to_rgba(row, result): - """Convert a grayscale image to RGBA. This method assumes the alpha - channel in result is already correctly initialized.""" - for i in range(3): - result[i::4] = row - convert_l_to_rgba = staticmethod(convert_l_to_rgba) - - def convert_rgb_to_rgba(row, result): - """Convert an RGB image to RGBA. This method assumes the alpha - channel in result is already correctly initialized.""" - for i in range(3): - result[i::4] = row[i::3] - convert_rgb_to_rgba = staticmethod(convert_rgb_to_rgba) - - -# === Internal Test Support === - -# This section comprises the tests that are internally validated (as -# opposed to tests which produce output files that are externally -# validated). Primarily they are unittests. - -# Note that it is difficult to internally validate the results of -# writing a PNG file. The only thing we can do is read it back in -# again, which merely checks consistency, not that the PNG file we -# produce is valid. - -# Run the tests from the command line: -# python -c 'import png;png.runTest()' -# If you have nose installed you can use that: -# nosetests png.py - -# (For an in-memory binary file IO object) We use BytesIO where -# available, otherwise we use StringIO, but name it BytesIO. -try: - from io import BytesIO -except: - from io import StringIO as BytesIO -import tempfile -# http://www.python.org/doc/2.4.4/lib/module-unittest.html -import unittest - - -def runTest(): - unittest.main(__name__) - -def topngbytes(name, rows, x, y, **k): - """Convenience function for creating a PNG file "in memory" as a - string. Creates a :class:`Writer` instance using the keyword arguments, - then passes `rows` to its :meth:`Writer.write` method. The resulting - PNG file is returned as a string. `name` is used to identify the file for - debugging. - """ - - import os - - print(name) - f = BytesIO() - w = Writer(x, y, **k) - w.write(f, rows) - if os.environ.get('PYPNG_TEST_TMP'): - w = open(name, 'wb') - w.write(f.getvalue()) - w.close() - return f.getvalue() - -def _redirect_io(inp, out, f): - """Calls the function `f` with ``sys.stdin`` changed to `inp` - and ``sys.stdout`` changed to `out`. They are restored when `f` - returns. This function returns whatever `f` returns. - """ - - import os - - try: - oldin,sys.stdin = sys.stdin,inp - oldout,sys.stdout = sys.stdout,out - x = f() - finally: - sys.stdin = oldin - sys.stdout = oldout - if os.environ.get('PYPNG_TEST_TMP') and hasattr(out,'getvalue'): - name = mycallersname() - if name: - w = open(name+'.png', 'wb') - w.write(out.getvalue()) - w.close() - return x - -def mycallersname(): - """Returns the name of the caller of the caller of this function - (hence the name of the caller of the function in which - "mycallersname()" textually appears). Returns None if this cannot - be determined.""" - - # http://docs.python.org/library/inspect.html#the-interpreter-stack - import inspect - - frame = inspect.currentframe() - if not frame: - return None - frame_,filename_,lineno_,funname,linelist_,listi_ = ( - inspect.getouterframes(frame)[2]) - return funname - -def seqtobytes(s): - """Convert a sequence of integers to a *bytes* instance. Good for - plastering over Python 2 / Python 3 cracks. - """ - - return strtobytes(''.join(chr(x) for x in s)) - -class Test(unittest.TestCase): - # This member is used by the superclass. If we don't define a new - # class here then when we use self.assertRaises() and the PyPNG code - # raises an assertion then we get no proper traceback. I can't work - # out why, but defining a new class here means we get a proper - # traceback. - class failureException(Exception): - pass - - def helperLN(self, n): - mask = (1 << n) - 1 - # Use small chunk_limit so that multiple chunk writing is - # tested. Making it a test for Issue 20. - w = Writer(15, 17, greyscale=True, bitdepth=n, chunk_limit=99) - f = BytesIO() - w.write_array(f, array('B', list(map(mask.__and__, list(range(1, 256)))))) - r = Reader(bytes=f.getvalue()) - x,y,pixels,meta = r.read() - self.assertEqual(x, 15) - self.assertEqual(y, 17) - self.assertEqual(list(itertools.chain(*pixels)), - list(map(mask.__and__, list(range(1,256))))) - def testL8(self): - return self.helperLN(8) - def testL4(self): - return self.helperLN(4) - def testL2(self): - "Also tests asRGB8." - w = Writer(1, 4, greyscale=True, bitdepth=2) - f = BytesIO() - w.write_array(f, array('B', list(range(4)))) - r = Reader(bytes=f.getvalue()) - x,y,pixels,meta = r.asRGB8() - self.assertEqual(x, 1) - self.assertEqual(y, 4) - for i,row in enumerate(pixels): - self.assertEqual(len(row), 3) - self.assertEqual(list(row), [0x55*i]*3) - def testP2(self): - "2-bit palette." - a = (255,255,255) - b = (200,120,120) - c = (50,99,50) - w = Writer(1, 4, bitdepth=2, palette=[a,b,c]) - f = BytesIO() - w.write_array(f, array('B', (0,1,1,2))) - r = Reader(bytes=f.getvalue()) - x,y,pixels,meta = r.asRGB8() - self.assertEqual(x, 1) - self.assertEqual(y, 4) - self.assertEqual(list(map(list, pixels)), list(map(list, [a, b, b, c]))) - def testPtrns(self): - "Test colour type 3 and tRNS chunk (and 4-bit palette)." - a = (50,99,50,50) - b = (200,120,120,80) - c = (255,255,255) - d = (200,120,120) - e = (50,99,50) - w = Writer(3, 3, bitdepth=4, palette=[a,b,c,d,e]) - f = BytesIO() - w.write_array(f, array('B', (4, 3, 2, 3, 2, 0, 2, 0, 1))) - r = Reader(bytes=f.getvalue()) - x,y,pixels,meta = r.asRGBA8() - self.assertEqual(x, 3) - self.assertEqual(y, 3) - c = c+(255,) - d = d+(255,) - e = e+(255,) - boxed = [(e,d,c),(d,c,a),(c,a,b)] - flat = [itertools.chain(*row) for row in boxed] - self.assertEqual(list(map(list, pixels)), list(map(list, flat))) - def testRGBtoRGBA(self): - "asRGBA8() on colour type 2 source.""" - # Test for Issue 26 - # Also test that Reader can take a "file-like" object. - r = Reader(BytesIO(_pngsuite['basn2c08'])) - x,y,pixels,meta = r.asRGBA8() - # Test the pixels at row 9 columns 0 and 1. - row9 = list(pixels)[9] - self.assertEqual(list(row9[0:8]), - [0xff, 0xdf, 0xff, 0xff, 0xff, 0xde, 0xff, 0xff]) - def testLtoRGBA(self): - "asRGBA() on grey source.""" - # Test for Issue 60 - r = Reader(bytes=_pngsuite['basi0g08']) - x,y,pixels,meta = r.asRGBA() - row9 = list(list(pixels)[9]) - self.assertEqual(row9[0:8], - [222, 222, 222, 255, 221, 221, 221, 255]) - def testCtrns(self): - "Test colour type 2 and tRNS chunk." - # Test for Issue 25 - r = Reader(bytes=_pngsuite['tbrn2c08']) - x,y,pixels,meta = r.asRGBA8() - # I just happen to know that the first pixel is transparent. - # In particular it should be #7f7f7f00 - row0 = list(pixels)[0] - self.assertEqual(tuple(row0[0:4]), (0x7f, 0x7f, 0x7f, 0x00)) - def testAdam7read(self): - """Adam7 interlace reading. - Specifically, test that for images in the PngSuite that - have both an interlaced and straightlaced pair that both - images from the pair produce the same array of pixels.""" - for candidate in _pngsuite: - if not candidate.startswith('basn'): - continue - candi = candidate.replace('n', 'i') - if candi not in _pngsuite: - continue - print('adam7 read', candidate) - straight = Reader(bytes=_pngsuite[candidate]) - adam7 = Reader(bytes=_pngsuite[candi]) - # Just compare the pixels. Ignore x,y (because they're - # likely to be correct?); metadata is ignored because the - # "interlace" member differs. Lame. - straight = straight.read()[2] - adam7 = adam7.read()[2] - self.assertEqual(list(map(list, straight)), list(map(list, adam7))) - def testAdam7write(self): - """Adam7 interlace writing. - For each test image in the PngSuite, write an interlaced - and a straightlaced version. Decode both, and compare results. - """ - # Not such a great test, because the only way we can check what - # we have written is to read it back again. - - for name,bytes in list(_pngsuite.items()): - # Only certain colour types supported for this test. - if name[3:5] not in ['n0', 'n2', 'n4', 'n6']: - continue - it = Reader(bytes=bytes) - x,y,pixels,meta = it.read() - pngi = topngbytes('adam7wn'+name+'.png', pixels, - x=x, y=y, bitdepth=it.bitdepth, - greyscale=it.greyscale, alpha=it.alpha, - transparent=it.transparent, - interlace=False) - x,y,ps,meta = Reader(bytes=pngi).read() - it = Reader(bytes=bytes) - x,y,pixels,meta = it.read() - pngs = topngbytes('adam7wi'+name+'.png', pixels, - x=x, y=y, bitdepth=it.bitdepth, - greyscale=it.greyscale, alpha=it.alpha, - transparent=it.transparent, - interlace=True) - x,y,pi,meta = Reader(bytes=pngs).read() - self.assertEqual(list(map(list, ps)), list(map(list, pi))) - def testPGMin(self): - """Test that the command line tool can read PGM files.""" - def do(): - return _main(['testPGMin']) - s = BytesIO() - s.write(strtobytes('P5 2 2 3\n')) - s.write(strtobytes('\x00\x01\x02\x03')) - s.flush() - s.seek(0) - o = BytesIO() - _redirect_io(s, o, do) - r = Reader(bytes=o.getvalue()) - x,y,pixels,meta = r.read() - self.assertTrue(r.greyscale) - self.assertEqual(r.bitdepth, 2) - def testPAMin(self): - """Test that the command line tool can read PAM file.""" - def do(): - return _main(['testPAMin']) - s = BytesIO() - s.write(strtobytes('P7\nWIDTH 3\nHEIGHT 1\nDEPTH 4\nMAXVAL 255\n' - 'TUPLTYPE RGB_ALPHA\nENDHDR\n')) - # The pixels in flat row flat pixel format - flat = [255,0,0,255, 0,255,0,120, 0,0,255,30] - asbytes = seqtobytes(flat) - s.write(asbytes) - s.flush() - s.seek(0) - o = BytesIO() - _redirect_io(s, o, do) - r = Reader(bytes=o.getvalue()) - x,y,pixels,meta = r.read() - self.assertTrue(r.alpha) - self.assertTrue(not r.greyscale) - self.assertEqual(list(itertools.chain(*pixels)), flat) - def testLA4(self): - """Create an LA image with bitdepth 4.""" - bytes = topngbytes('la4.png', [[5, 12]], 1, 1, - greyscale=True, alpha=True, bitdepth=4) - sbit = Reader(bytes=bytes).chunk('sBIT')[1] - self.assertEqual(sbit, strtobytes('\x04\x04')) - def testPal(self): - """Test that a palette PNG returns the palette in info.""" - r = Reader(bytes=_pngsuite['basn3p04']) - x,y,pixels,info = r.read() - self.assertEqual(x, 32) - self.assertEqual(y, 32) - self.assertTrue('palette' in info) - def testPalWrite(self): - """Test metadata for paletted PNG can be passed from one PNG - to another.""" - r = Reader(bytes=_pngsuite['basn3p04']) - x,y,pixels,info = r.read() - w = Writer(**info) - o = BytesIO() - w.write(o, pixels) - o.flush() - o.seek(0) - r = Reader(file=o) - _,_,_,again_info = r.read() - # Same palette - self.assertEqual(again_info['palette'], info['palette']) - def testPalExpand(self): - """Test that bitdepth can be used to fiddle with pallete image.""" - r = Reader(bytes=_pngsuite['basn3p04']) - x,y,pixels,info = r.read() - pixels = [list(row) for row in pixels] - info['bitdepth'] = 8 - w = Writer(**info) - o = BytesIO() - w.write(o, pixels) - o.flush() - o.seek(0) - r = Reader(file=o) - _,_,again_pixels,again_info = r.read() - # Same pixels - again_pixels = [list(row) for row in again_pixels] - self.assertEqual(again_pixels, pixels) - - def testPNMsbit(self): - """Test that PNM files can generates sBIT chunk.""" - def do(): - return _main(['testPNMsbit']) - s = BytesIO() - s.write(strtobytes('P6 8 1 1\n')) - for pixel in range(8): - s.write(struct.pack(' 0xff: - fmt = fmt + 'H' - else: - fmt = fmt + 'B' - for row in pixels: - file.write(struct.pack(fmt, *row)) - file.flush() - -def color_triple(color): - """ - Convert a command line colour value to a RGB triple of integers. - FIXME: Somewhere we need support for greyscale backgrounds etc. - """ - if color.startswith('#') and len(color) == 4: - return (int(color[1], 16), - int(color[2], 16), - int(color[3], 16)) - if color.startswith('#') and len(color) == 7: - return (int(color[1:3], 16), - int(color[3:5], 16), - int(color[5:7], 16)) - elif color.startswith('#') and len(color) == 13: - return (int(color[1:5], 16), - int(color[5:9], 16), - int(color[9:13], 16)) - -def _add_common_options(parser): - """Call *parser.add_option* for each of the options that are - common between this PNG--PNM conversion tool and the gen - tool. - """ - parser.add_option("-i", "--interlace", - default=False, action="store_true", - help="create an interlaced PNG file (Adam7)") - parser.add_option("-t", "--transparent", - action="store", type="string", metavar="#RRGGBB", - help="mark the specified colour as transparent") - parser.add_option("-b", "--background", - action="store", type="string", metavar="#RRGGBB", - help="save the specified background colour") - parser.add_option("-g", "--gamma", - action="store", type="float", metavar="value", - help="save the specified gamma value") - parser.add_option("-c", "--compression", - action="store", type="int", metavar="level", - help="zlib compression level (0-9)") - return parser - -def _main(argv): - """ - Run the PNG encoder with options from the command line. - """ - - # Parse command line arguments - from optparse import OptionParser - import re - version = '%prog ' + re.sub(r'( ?\$|URL: |Rev:)', '', __version__) - parser = OptionParser(version=version) - parser.set_usage("%prog [options] [imagefile]") - parser.add_option('-r', '--read-png', default=False, - action='store_true', - help='Read PNG, write PNM') - parser.add_option("-a", "--alpha", - action="store", type="string", metavar="pgmfile", - help="alpha channel transparency (RGBA)") - _add_common_options(parser) - - (options, args) = parser.parse_args(args=argv[1:]) - - # Convert options - if options.transparent is not None: - options.transparent = color_triple(options.transparent) - if options.background is not None: - options.background = color_triple(options.background) - - # Prepare input and output files - if len(args) == 0: - infilename = '-' - infile = sys.stdin - elif len(args) == 1: - infilename = args[0] - infile = open(infilename, 'rb') - else: - parser.error("more than one input file") - outfile = sys.stdout - if sys.platform == "win32": - import msvcrt, os - msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - - if options.read_png: - # Encode PNG to PPM - png = Reader(file=infile) - width,height,pixels,meta = png.asDirect() - write_pnm(outfile, width, height, pixels, meta) - else: - # Encode PNM to PNG - format, width, height, depth, maxval = \ - read_pnm_header(infile, ('P5','P6','P7')) - # When it comes to the variety of input formats, we do something - # rather rude. Observe that L, LA, RGB, RGBA are the 4 colour - # types supported by PNG and that they correspond to 1, 2, 3, 4 - # channels respectively. So we use the number of channels in - # the source image to determine which one we have. We do not - # care about TUPLTYPE. - greyscale = depth <= 2 - pamalpha = depth in (2,4) - supported = [2**x-1 for x in range(1,17)] - try: - mi = supported.index(maxval) - except ValueError: - raise NotImplementedError( - 'your maxval (%s) not in supported list %s' % - (maxval, str(supported))) - bitdepth = mi+1 - writer = Writer(width, height, - greyscale=greyscale, - bitdepth=bitdepth, - interlace=options.interlace, - transparent=options.transparent, - background=options.background, - alpha=bool(pamalpha or options.alpha), - gamma=options.gamma, - compression=options.compression) - if options.alpha: - pgmfile = open(options.alpha, 'rb') - format, awidth, aheight, adepth, amaxval = \ - read_pnm_header(pgmfile, 'P5') - if amaxval != '255': - raise NotImplementedError( - 'maxval %s not supported for alpha channel' % amaxval) - if (awidth, aheight) != (width, height): - raise ValueError("alpha channel image size mismatch" - " (%s has %sx%s but %s has %sx%s)" - % (infilename, width, height, - options.alpha, awidth, aheight)) - writer.convert_ppm_and_pgm(infile, pgmfile, outfile) - else: - writer.convert_pnm(infile, outfile) - - -if __name__ == '__main__': - try: - _main(sys.argv) - except Error as e: - print(e, file=sys.stderr) diff --git a/pyqrcode/tables.py b/pyqrcode/tables.py deleted file mode 100644 index 518688c..0000000 --- a/pyqrcode/tables.py +++ /dev/null @@ -1,746 +0,0 @@ -'''This module lists out all of the tables needed to create a QR code. -If you are viewing this in the HTML documentation, I recommend reading the -actual file instead. The formating for the tables is much more readable. -''' - -#: This defines the QR Code's 'mode' which sets what -#: type of code it is along with its size. -modes = {'numeric':1, - '1':1, - 1:1, - 'alphanumeric':2, - 'alfanumeric':2, - 'text':2, - '2':2, - 2:2, - 'binary':4, - 'bin':4, - 'byte':4, - 'bytes':4, - '4':4, - 4:4, - 'japanese':8, - 'kanji':8, - '8':8, - 8:8} - -#: This defines the amount of error correction. The dictionary -#: allows the user to specify this in several ways. -error_level = {'L':'L', 'l':'L', '7%':'L', .7:'L', - 'M':'M', 'm':'M', '15%':'M', .15:'M', - 'Q':'Q', 'q':'Q', '25%':'Q', .25:'Q', - 'H':'H', 'h':'H', '30%':'H', .30:'H'} - -#: This is a dictionary holds how long the "data length" field is for -#: each version and mode of the QR Code. -data_length_field = {9:{1:10, 2:9, 4:8, 8:8}, - 26:{1:12, 2:11, 4:16, 8:10}, - 40:{1:14, 2:13, 4:16, 8:12}} - -#: QR Codes uses a unique ASCII-like table for the 'alphanumeric' mode. -#: This is a dictionary representing that unique table, where the -#: keys are the possible characters in the data and the values -#: are the character's numeric representation. -ascii_codes = {'0':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, - '8':8, '9':9, 'A':10, 'B':11, 'C':12, 'D':13, 'E':14, - 'F':15, 'G':16, 'H':17, 'I':18, 'J':19, 'K':20, 'L':21, - 'M':22, 'N':23, 'O':24, 'P':25, 'Q':26, 'R':27, 'S':28, - 'T':29, 'U':30, 'V':31, 'W':32, 'X':33, 'Y':34, 'Z':35, - ' ':36, '$':37, '%':38, '*':39, '+':40, '-':41, '.':42, - '/':43, ':':44} - -#: This array specifies the size of a QR Code in pixels. These numbers are -#: defined in the standard. The indexes correspond to the QR Code's -#: version number. This array was taken from: -#: -#: http://www.denso-wave.com/qrcode/vertable1-e.html -version_size = [None, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, - 61, 65, 69, 73, 77, 81, 85, 89, 93, 97, - 101, 105, 109, 113, 117, 121, 125, 129, 133, 137, - 141, 145, 149, 153, 157, 161, 165, 169, 173, 177] - -#: This dictionary lists the data capacity for all possible QR Codes. -#: This dictionary is organized where the first key corresponds to the -#: QR Code version number. The next key corresponds to the error -#: correction level, see error. The final key corresponds to -#: the mode number, see modes. The zero mode number represents the -#: possible "data bits." This table was taken from: -#: -#: http://www.denso-wave.com/qrcode/vertable1-e.html -data_capacity = { - 1: { - "L":{0:152,1:41,2:25,4:17,8:10,}, - "M":{0:128,1:34,2:20,4:14,8:8,}, - "Q":{0:104,1:27,2:16,4:11,8:7,}, - "H":{0:72,1:17,2:10,4:7,8:4,}}, - 2: { - "L":{0:272,1:77,2:47,4:32,8:20,}, - "M":{0:224,1:63,2:38,4:26,8:16,}, - "Q":{0:176,1:48,2:29,4:20,8:12,}, - "H":{0:128,1:34,2:20,4:14,8:8,}}, - 3: { - "L":{0:440,1:127,2:77,4:53,8:32,}, - "M":{0:352,1:101,2:61,4:42,8:26,}, - "Q":{0:272,1:77,2:47,4:32,8:20,}, - "H":{0:208,1:58,2:35,4:24,8:15,}}, - 4: { - "L":{0:640,1:187,2:114,4:78,8:48,}, - "M":{0:512,1:149,2:90,4:62,8:38,}, - "Q":{0:384,1:111,2:67,4:46,8:28,}, - "H":{0:288,1:82,2:50,4:34,8:21,}}, - 5: { - "L":{0:864,1:255,2:154,4:106,8:65,}, - "M":{0:688,1:202,2:122,4:84,8:52,}, - "Q":{0:496,1:144,2:87,4:60,8:37,}, - "H":{0:368,1:106,2:64,4:44,8:27,}}, - 6: { - "L":{0:1088,1:322,2:195,4:134,8:82,}, - "M":{0:864,1:255,2:154,4:106,8:65,}, - "Q":{0:608,1:178,2:108,4:74,8:45,}, - "H":{0:480,1:139,2:84,4:58,8:36,}}, - 7: { - "L":{0:1248,1:370,2:224,4:154,8:95,}, - "M":{0:992,1:293,2:178,4:122,8:75,}, - "Q":{0:704,1:207,2:125,4:86,8:53,}, - "H":{0:528,1:154,2:93,4:64,8:39,}}, - 8: { - "L":{0:1552,1:461,2:279,4:192,8:118,}, - "M":{0:1232,1:365,2:221,4:152,8:93,}, - "Q":{0:880,1:259,2:157,4:108,8:66,}, - "H":{0:688,1:202,2:122,4:84,8:52,}}, - 9: { - "L":{0:1856,1:552,2:335,4:230,8:141,}, - "M":{0:1456,1:432,2:262,4:180,8:111,}, - "Q":{0:1056,1:312,2:189,4:130,8:80,}, - "H":{0:800,1:235,2:143,4:98,8:60,}}, - 10: { - "L":{0:2192,1:652,2:395,4:271,8:167,}, - "M":{0:1728,1:513,2:311,4:213,8:131,}, - "Q":{0:1232,1:364,2:221,4:151,8:93,}, - "H":{0:976,1:288,2:174,4:119,8:74,}}, - 11: { - "L":{0:2592,1:772,2:468,4:321,8:198,}, - "M":{0:2032,1:604,2:366,4:251,8:155,}, - "Q":{0:1440,1:427,2:259,4:177,8:109,}, - "H":{0:1120,1:331,2:200,4:137,8:85,}}, - 12: { - "L":{0:2960,1:883,2:535,4:367,8:226,}, - "M":{0:2320,1:691,2:419,4:287,8:177,}, - "Q":{0:1648,1:489,2:296,4:203,8:125,}, - "H":{0:1264,1:374,2:227,4:155,8:96,}}, - 13: { - "L":{0:3424,1:1022,2:619,4:425,8:262,}, - "M":{0:2672,1:796,2:483,4:331,8:204,}, - "Q":{0:1952,1:580,2:352,4:241,8:149,}, - "H":{0:1440,1:427,2:259,4:177,8:109,}}, - 14: { - "L":{0:3688,1:1101,2:667,4:458,8:282,}, - "M":{0:2920,1:871,2:528,4:362,8:223,}, - "Q":{0:2088,1:621,2:376,4:258,8:159,}, - "H":{0:1576,1:468,2:283,4:194,8:120,}}, - 15: { - "L":{0:4184,1:1250,2:758,4:520,8:320,}, - "M":{0:3320,1:991,2:600,4:412,8:254,}, - "Q":{0:2360,1:703,2:426,4:292,8:180,}, - "H":{0:1784,1:530,2:321,4:220,8:136,}}, - 16: { - "L":{0:4712,1:1408,2:854,4:586,8:361,}, - "M":{0:3624,1:1082,2:656,4:450,8:277,}, - "Q":{0:2600,1:775,2:470,4:322,8:198,}, - "H":{0:2024,1:602,2:365,4:250,8:154,}}, - 17: { - "L":{0:5176,1:1548,2:938,4:644,8:397,}, - "M":{0:4056,1:1212,2:734,4:504,8:310,}, - "Q":{0:2936,1:876,2:531,4:364,8:224,}, - "H":{0:2264,1:674,2:408,4:280,8:173,}}, - 18: { - "L":{0:5768,1:1725,2:1046,4:718,8:442,}, - "M":{0:4504,1:1346,2:816,4:560,8:345,}, - "Q":{0:3176,1:948,2:574,4:394,8:243,}, - "H":{0:2504,1:746,2:452,4:310,8:191,}}, - 19: { - "L":{0:6360,1:1903,2:1153,4:792,8:488,}, - "M":{0:5016,1:1500,2:909,4:624,8:384,}, - "Q":{0:3560,1:1063,2:644,4:442,8:272,}, - "H":{0:2728,1:813,2:493,4:338,8:208,}}, - 20: { - "L":{0:6888,1:2061,2:1249,4:858,8:528,}, - "M":{0:5352,1:1600,2:970,4:666,8:410,}, - "Q":{0:3880,1:1159,2:702,4:482,8:297,}, - "H":{0:3080,1:919,2:557,4:382,8:235,}}, - 21: { - "L":{0:7456,1:2232,2:1352,4:929,8:572,}, - "M":{0:5712,1:1708,2:1035,4:711,8:438,}, - "Q":{0:4096,1:1224,2:742,4:509,8:314,}, - "H":{0:3248,1:969,2:587,4:403,8:248,}}, - 22: { - "L":{0:8048,1:2409,2:1460,4:1003,8:618,}, - "M":{0:6256,1:1872,2:1134,4:779,8:480,}, - "Q":{0:4544,1:1358,2:823,4:565,8:348,}, - "H":{0:3536,1:1056,2:640,4:439,8:270,}}, - 23: { - "L":{0:8752,1:2620,2:1588,4:1091,8:672,}, - "M":{0:6880,1:2059,2:1248,4:857,8:528,}, - "Q":{0:4912,1:1468,2:890,4:611,8:376,}, - "H":{0:3712,1:1108,2:672,4:461,8:284,}}, - 24: { - "L":{0:9392,1:2812,2:1704,4:1171,8:721,}, - "M":{0:7312,1:2188,2:1326,4:911,8:561,}, - "Q":{0:5312,1:1588,2:963,4:661,8:407,}, - "H":{0:4112,1:1228,2:744,4:511,8:315,}}, - 25: { - "L":{0:10208,1:3057,2:1853,4:1273,8:784,}, - "M":{0:8000,1:2395,2:1451,4:997,8:614,}, - "Q":{0:5744,1:1718,2:1041,4:715,8:440,}, - "H":{0:4304,1:1286,2:779,4:535,8:330,}}, - 26: { - "L":{0:10960,1:3283,2:1990,4:1367,8:842,}, - "M":{0:8496,1:2544,2:1542,4:1059,8:652,}, - "Q":{0:6032,1:1804,2:1094,4:751,8:462,}, - "H":{0:4768,1:1425,2:864,4:593,8:365,}}, - 27: { - "L":{0:11744,1:3514,2:2132,4:1465,8:902,}, - "M":{0:9024,1:2701,2:1637,4:1125,8:692,}, - "Q":{0:6464,1:1933,2:1172,4:805,8:496,}, - "H":{0:5024,1:1501,2:910,4:625,8:385,}}, - 28: { - "L":{0:12248,1:3669,2:2223,4:1528,8:940,}, - "M":{0:9544,1:2857,2:1732,4:1190,8:732,}, - "Q":{0:6968,1:2085,2:1263,4:868,8:534,}, - "H":{0:5288,1:1581,2:958,4:658,8:405,}}, - 29: { - "L":{0:13048,1:3909,2:2369,4:1628,8:1002,}, - "M":{0:10136,1:3035,2:1839,4:1264,8:778,}, - "Q":{0:7288,1:2181,2:1322,4:908,8:559,}, - "H":{0:5608,1:1677,2:1016,4:698,8:430,}}, - 30: { - "L":{0:13880,1:4158,2:2520,4:1732,8:1066,}, - "M":{0:10984,1:3289,2:1994,4:1370,8:843,}, - "Q":{0:7880,1:2358,2:1429,4:982,8:604,}, - "H":{0:5960,1:1782,2:1080,4:742,8:457,}}, - 31: { - "L":{0:14744,1:4417,2:2677,4:1840,8:1132,}, - "M":{0:11640,1:3486,2:2113,4:1452,8:894,}, - "Q":{0:8264,1:2473,2:1499,4:1030,8:634,}, - "H":{0:6344,1:1897,2:1150,4:790,8:486,}}, - 32: { - "L":{0:15640,1:4686,2:2840,4:1952,8:1201,}, - "M":{0:12328,1:3693,2:2238,4:1538,8:947,}, - "Q":{0:8920,1:2670,2:1618,4:1112,8:684,}, - "H":{0:6760,1:2022,2:1226,4:842,8:518,}}, - 33: { - "L":{0:16568,1:4965,2:3009,4:2068,8:1273,}, - "M":{0:13048,1:3909,2:2369,4:1628,8:1002,}, - "Q":{0:9368,1:2805,2:1700,4:1168,8:719,}, - "H":{0:7208,1:2157,2:1307,4:898,8:553,}}, - 34: { - "L":{0:17528,1:5253,2:3183,4:2188,8:1347,}, - "M":{0:13800,1:4134,2:2506,4:1722,8:1060,}, - "Q":{0:9848,1:2949,2:1787,4:1228,8:756,}, - "H":{0:7688,1:2301,2:1394,4:958,8:590,}}, - 35: { - "L":{0:18448,1:5529,2:3351,4:2303,8:1417,}, - "M":{0:14496,1:4343,2:2632,4:1809,8:1113,}, - "Q":{0:10288,1:3081,2:1867,4:1283,8:790,}, - "H":{0:7888,1:2361,2:1431,4:983,8:605,}}, - 36: { - "L":{0:19472,1:5836,2:3537,4:2431,8:1496,}, - "M":{0:15312,1:4588,2:2780,4:1911,8:1176,}, - "Q":{0:10832,1:3244,2:1966,4:1351,8:832,}, - "H":{0:8432,1:2524,2:1530,4:1051,8:647,}}, - 37: { - "L":{0:20528,1:6153,2:3729,4:2563,8:1577,}, - "M":{0:15936,1:4775,2:2894,4:1989,8:1224,}, - "Q":{0:11408,1:3417,2:2071,4:1423,8:876,}, - "H":{0:8768,1:2625,2:1591,4:1093,8:673,}}, - 38: { - "L":{0:21616,1:6479,2:3927,4:2699,8:1661,}, - "M":{0:16816,1:5039,2:3054,4:2099,8:1292,}, - "Q":{0:12016,1:3599,2:2181,4:1499,8:923,}, - "H":{0:9136,1:2735,2:1658,4:1139,8:701,}}, - 39: { - "L":{0:22496,1:6743,2:4087,4:2809,8:1729,}, - "M":{0:17728,1:5313,2:3220,4:2213,8:1362,}, - "Q":{0:12656,1:3791,2:2298,4:1579,8:972,}, - "H":{0:9776,1:2927,2:1774,4:1219,8:750,}}, - 40: { - "L":{0:23648,1:7089,2:4296,4:2953,8:1817,}, - "M":{0:18672,1:5596,2:3391,4:2331,8:1435,}, - "Q":{0:13328,1:3993,2:2420,4:1663,8:1024,}, - "H":{0:10208,1:3057,2:1852,4:1273,8:784,}} -} - -#: This table defines the "Error Correction Code Words and Block Information." -#: The table lists the number of error correction words that are required -#: to be generated for each version and error correction level. The table -#: is accessed by first using the version number as a key and then the -#: error level. The array values correspond to these columns from the source -#: table: -#: -#: +----------------------------+ -#: |0 | EC Code Words Per Block | -#: +----------------------------+ -#: |1 | Block 1 Count | -#: +----------------------------+ -#: |2 | Block 1 Data Code Words | -#: +----------------------------+ -#: |3 | Block 2 Count | -#: +----------------------------+ -#: |4 | Block 2 Data Code Words | -#: +----------------------------+ -#: -#: This table was taken from: -#: -#: http://www.thonky.com/qr-code-tutorial/error-correction-table/ -eccwbi = { - 1: { - 'L': [ 7, 1, 19, 0, 0, ], - 'M': [10, 1, 16, 0, 0, ], - 'Q': [13, 1, 13, 0, 0, ], - 'H': [17, 1, 9, 0, 0, ], - }, - 2: { - 'L': [10, 1, 34, 0, 0, ], - 'M': [16, 1, 28, 0, 0, ], - 'Q': [22, 1, 22, 0, 0, ], - 'H': [28, 1, 16, 0, 0, ], - }, - 3: { - 'L': [15, 1, 55, 0, 0, ], - 'M': [26, 1, 44, 0, 0, ], - 'Q': [18, 2, 17, 0, 0, ], - 'H': [22, 2, 13, 0, 0, ], - }, - 4: { - 'L': [20, 1, 80, 0, 0, ], - 'M': [18, 2, 32, 0, 0, ], - 'Q': [26, 2, 24, 0, 0, ], - 'H': [16, 4, 9, 0, 0, ], - }, - 5: { - 'L': [26, 1, 108, 0, 0, ], - 'M': [24, 2, 43, 0, 0, ], - 'Q': [18, 2, 15, 2, 16, ], - 'H': [22, 2, 11, 2, 12, ], - }, - 6: { - 'L': [18, 2, 68, 0, 0, ], - 'M': [16, 4, 27, 0, 0, ], - 'Q': [24, 4, 19, 0, 0, ], - 'H': [28, 4, 15, 0, 0, ], - }, - 7: { - 'L': [20, 2, 78, 0, 0, ], - 'M': [18, 4, 31, 0, 0, ], - 'Q': [18, 2, 14, 4, 15, ], - 'H': [26, 4, 13, 1, 14, ], - }, - 8: { - 'L': [24, 2, 97, 0, 0, ], - 'M': [22, 2, 38, 2, 39, ], - 'Q': [22, 4, 18, 2, 19, ], - 'H': [26, 4, 14, 2, 15, ], - }, - 9: { - 'L': [30, 2, 116, 0, 0, ], - 'M': [22, 3, 36, 2, 37, ], - 'Q': [20, 4, 16, 4, 17, ], - 'H': [24, 4, 12, 4, 13, ], - }, - 10: { - 'L': [18, 2, 68, 2, 69, ], - 'M': [26, 4, 43, 1, 44, ], - 'Q': [24, 6, 19, 2, 20, ], - 'H': [28, 6, 15, 2, 16, ], - }, - 11: { - 'L': [20, 4, 81, 0, 0, ], - 'M': [30, 1, 50, 4, 51, ], - 'Q': [28, 4, 22, 4, 23, ], - 'H': [24, 3, 12, 8, 13, ], - }, - 12: { - 'L': [24, 2, 92, 2, 93, ], - 'M': [22, 6, 36, 2, 37, ], - 'Q': [26, 4, 20, 6, 21, ], - 'H': [28, 7, 14, 4, 15, ], - }, - 13: { - 'L': [26, 4, 107, 0, 0, ], - 'M': [22, 8, 37, 1, 38, ], - 'Q': [24, 8, 20, 4, 21, ], - 'H': [22, 12, 11, 4, 12, ], - }, - 14: { - 'L': [30, 3, 115, 1, 116, ], - 'M': [24, 4, 40, 5, 41, ], - 'Q': [20, 11, 16, 5, 17, ], - 'H': [24, 11, 12, 5, 13, ], - }, - 15: { - 'L': [22, 5, 87, 1, 88, ], - 'M': [24, 5, 41, 5, 42, ], - 'Q': [30, 5, 24, 7, 25, ], - 'H': [24, 11, 12, 7, 13, ], - }, - 16: { - 'L': [24, 5, 98, 1, 99, ], - 'M': [28, 7, 45, 3, 46, ], - 'Q': [24, 15, 19, 2, 20, ], - 'H': [30, 3, 15, 13, 16, ], - }, - 17: { - 'L': [28, 1, 107, 5, 108, ], - 'M': [28, 10, 46, 1, 47, ], - 'Q': [28, 1, 22, 15, 23, ], - 'H': [28, 2, 14, 17, 15, ], - }, - 18: { - 'L': [30, 5, 120, 1, 121, ], - 'M': [26, 9, 43, 4, 44, ], - 'Q': [28, 17, 22, 1, 23, ], - 'H': [28, 2, 14, 19, 15, ], - }, - 19: { - 'L': [28, 3, 113, 4, 114, ], - 'M': [26, 3, 44, 11, 45, ], - 'Q': [26, 17, 21, 4, 22, ], - 'H': [26, 9, 13, 16, 14, ], - }, - 20: { - 'L': [28, 3, 107, 5, 108, ], - 'M': [26, 3, 41, 13, 42, ], - 'Q': [30, 15, 24, 5, 25, ], - 'H': [28, 15, 15, 10, 16, ], - }, - 21: { - 'L': [28, 4, 116, 4, 117, ], - 'M': [26, 17, 42, 0, 0, ], - 'Q': [28, 17, 22, 6, 23, ], - 'H': [30, 19, 16, 6, 17, ], - }, - 22: { - 'L': [28, 2, 111, 7, 112, ], - 'M': [28, 17, 46, 0, 0, ], - 'Q': [30, 7, 24, 16, 25, ], - 'H': [24, 34, 13, 0, 0, ], - }, - 23: { - 'L': [30, 4, 121, 5, 122, ], - 'M': [28, 4, 47, 14, 48, ], - 'Q': [30, 11, 24, 14, 25, ], - 'H': [30, 16, 15, 14, 16, ], - }, - 24: { - 'L': [30, 6, 117, 4, 118, ], - 'M': [28, 6, 45, 14, 46, ], - 'Q': [30, 11, 24, 16, 25, ], - 'H': [30, 30, 16, 2, 17, ], - }, - 25: { - 'L': [26, 8, 106, 4, 107, ], - 'M': [28, 8, 47, 13, 48, ], - 'Q': [30, 7, 24, 22, 25, ], - 'H': [30, 22, 15, 13, 16, ], - }, - 26: { - 'L': [28, 10, 114, 2, 115, ], - 'M': [28, 19, 46, 4, 47, ], - 'Q': [28, 28, 22, 6, 23, ], - 'H': [30, 33, 16, 4, 17, ], - }, - 27: { - 'L': [30, 8, 122, 4, 123, ], - 'M': [28, 22, 45, 3, 46, ], - 'Q': [30, 8, 23, 26, 24, ], - 'H': [30, 12, 15, 28, 16, ], - }, - 28: { - 'L': [30, 3, 117, 10, 118, ], - 'M': [28, 3, 45, 23, 46, ], - 'Q': [30, 4, 24, 31, 25, ], - 'H': [30, 11, 15, 31, 16, ], - }, - 29: { - 'L': [30, 7, 116, 7, 117, ], - 'M': [28, 21, 45, 7, 46, ], - 'Q': [30, 1, 23, 37, 24, ], - 'H': [30, 19, 15, 26, 16, ], - }, - 30: { - 'L': [30, 5, 115, 10, 116, ], - 'M': [28, 19, 47, 10, 48, ], - 'Q': [30, 15, 24, 25, 25, ], - 'H': [30, 23, 15, 25, 16, ], - }, - 31: { - 'L': [30, 13, 115, 3, 116, ], - 'M': [28, 2, 46, 29, 47, ], - 'Q': [30, 42, 24, 1, 25, ], - 'H': [30, 23, 15, 28, 16, ], - }, - 32: { - 'L': [30, 17, 115, 0, 0, ], - 'M': [28, 10, 46, 23, 47, ], - 'Q': [30, 10, 24, 35, 25, ], - 'H': [30, 19, 15, 35, 16, ], - }, - 33: { - 'L': [30, 17, 115, 1, 116, ], - 'M': [28, 14, 46, 21, 47, ], - 'Q': [30, 29, 24, 19, 25, ], - 'H': [30, 11, 15, 46, 16, ], - }, - 34: { - 'L': [30, 13, 115, 6, 116, ], - 'M': [28, 14, 46, 23, 47, ], - 'Q': [30, 44, 24, 7, 25, ], - 'H': [30, 59, 16, 1, 17, ], - }, - 35: { - 'L': [30, 12, 121, 7, 122, ], - 'M': [28, 12, 47, 26, 48, ], - 'Q': [30, 39, 24, 14, 25, ], - 'H': [30, 22, 15, 41, 16, ], - }, - 36: { - 'L': [30, 6, 121, 14, 122, ], - 'M': [28, 6, 47, 34, 48, ], - 'Q': [30, 46, 24, 10, 25, ], - 'H': [30, 2, 15, 64, 16, ], - }, - 37: { - 'L': [30, 17, 122, 4, 123, ], - 'M': [28, 29, 46, 14, 47, ], - 'Q': [30, 49, 24, 10, 25, ], - 'H': [30, 24, 15, 46, 16, ], - }, - 38: { - 'L': [30, 4, 122, 18, 123, ], - 'M': [28, 13, 46, 32, 47, ], - 'Q': [30, 48, 24, 14, 25, ], - 'H': [30, 42, 15, 32, 16, ], - }, - 39: { - 'L': [30, 20, 117, 4, 118, ], - 'M': [28, 40, 47, 7, 48, ], - 'Q': [30, 43, 24, 22, 25, ], - 'H': [30, 10, 15, 67, 16, ], - }, - 40: { - 'L': [30, 19, 118, 6, 119, ], - 'M': [28, 18, 47, 31, 48, ], - 'Q': [30, 34, 24, 34, 25, ], - 'H': [30, 20, 15, 61, 16, ], - }, -} - -#: This table lists all of the generator polynomials used by QR Codes. -#: They are indexed by the number of "ECC Code Words" (see table above). -#: This table is taken from: -#: -#: http://www.matchadesign.com/blog/qr-code-demystified-part-4/ -generator_polynomials = { - 7:[87, 229, 146, 149, 238, 102, 21], - 10:[251, 67, 46, 61, 118, 70, 64, 94, 32, 45], - 13:[74, 152, 176, 100, 86, 100, 106, 104, 130, 218, 206, 140, 78], - 15:[8, 183, 61, 91, 202, 37, 51, 58, 58, 237, 140, 124, 5, 99, 105], - 16:[120, 104, 107, 109, 102, 161, 76, 3, 91, 191, 147, 169, 182, 194, - 225, 120], - 17:[43, 139, 206, 78, 43, 239, 123, 206, 214, 147, 24, 99, 150, 39, - 243, 163, 136], - 18:[215, 234, 158, 94, 184, 97, 118, 170, 79, 187, 152, 148, 252, 179, - 5, 98, 96, 153], - 20:[17, 60, 79, 50, 61, 163, 26, 187, 202, 180, 221, 225, 83, 239, 156, - 164, 212, 212, 188, 190], - 22:[210, 171, 247, 242, 93, 230, 14, 109, 221, 53, 200, 74, 8, 172, 98, - 80, 219, 134, 160, 105, 165, 231], - 24:[229, 121, 135, 48, 211, 117, 251, 126, 159, 180, 169, 152, 192, 226, - 228, 218, 111, 0, 117, 232, 87, 96, 227, 21], - 26:[173, 125, 158, 2, 103, 182, 118, 17, 145, 201, 111, 28, 165, 53, 161, - 21, 245, 142, 13, 102, 48, 227, 153, 145, 218, 70], - 28:[168, 223, 200, 104, 224, 234, 108, 180, 110, 190, 195, 147, 205, 27, - 232, 201, 21, 43, 245, 87, 42, 195, 212, 119, 242, 37, 9, 123], - 30:[41, 173, 145, 152, 216, 31, 179, 182, 50, 48, 110, 86, 239, 96, 222, - 125, 42, 173, 226, 193, 224, 130, 156, 37, 251, 216, 238, 40, 192, - 180] -} - -#: This table contains the log and values used in GF(256) arithmetic. -#: They are used to generate error correction codes for QR Codes. -#: This table is taken from: -#: -#: vhttp://www.thonky.com/qr-code-tutorial/log-antilog-table/ -galois_log = [ - 1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, 76, 152, - 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192, 157, 39, 78, - 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 70, 140, 5, - 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, 95, 190, 97, - 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240, 253, 231, 211, - 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, 217, 175, - 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206, 129, 31, - 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 133, - 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 168, 77, - 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 230, 209, - 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, 227, - 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 130, - 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, 81, - 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 18, - 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 44, - 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1,] - -#: This table contains the antilog and values used in GF(256) arithmetic. -#: They are used to generate error correction codes for QR Codes. -#: This table is taken from: -#: -#: http://www.thonky.com/qr-code-tutorial/log-antilog-table/ -galois_antilog = [ - None, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100, - 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, 138, - 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 29, 181, - 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, 191, - 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 54, 208, - 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, 30, 66, - 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 202, 94, - 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, 112, - 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 227, 165, - 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 55, 63, - 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 242, - 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 31, 45, - 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 108, - 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 203, - 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215, 79, - 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175,] - -#: This table contains the coordinates for the position adjustment patterns. -#: The index of the table corresponds to the QR Code's version number. -#: This table is taken from: -#: -#: http://www.thonky.com/qr-code-tutorial/part-3-mask-pattern/ -position_adjustment = [ - None, #There is not version 0 - None, #Version 1 does not need adjustment - [6, 18, ], - [6, 22, ], - [6, 26, ], - [6, 30, ], - [6, 34, ], - [6, 22, 38, ], - [6, 24, 42, ], - [6, 26, 46, ], - [6, 28, 50, ], - [6, 30, 54, ], - [6, 32, 58, ], - [6, 34, 62, ], - [6, 26, 46, 66, ], - [6, 26, 48, 70, ], - [6, 26, 50, 74, ], - [6, 30, 54, 78, ], - [6, 30, 56, 82, ], - [6, 30, 58, 86, ], - [6, 34, 62, 90, ], - [6, 28, 50, 72, 94, ], - [6, 26, 50, 74, 98, ], - [6, 30, 54, 78, 102, ], - [6, 28, 54, 80, 106, ], - [6, 32, 58, 84, 110, ], - [6, 30, 58, 86, 114, ], - [6, 34, 62, 90, 118, ], - [6, 26, 50, 74, 98, 122, ], - [6, 30, 54, 78, 102, 126, ], - [6, 26, 52, 78, 104, 130, ], - [6, 30, 56, 82, 108, 134, ], - [6, 34, 60, 86, 112, 138, ], - [6, 30, 58, 86, 114, 142, ], - [6, 34, 62, 90, 118, 146, ], - [6, 30, 54, 78, 102, 126, 150, ], - [6, 24, 50, 76, 102, 128, 154, ], - [6, 28, 54, 80, 106, 132, 158, ], - [6, 32, 58, 84, 110, 136, 162, ], - [6, 26, 54, 82, 110, 138, 166, ], - [6, 30, 58, 86, 114, 142, 170, ], -] - -#: This table specifies the bit pattern to be added to a QR Code's -#: image to specify what version the code is. Note, this pattern -#: is not used for versions 1-6. This table is taken from: -#: -#: http://www.thonky.com/qr-code-tutorial/part-3-mask-pattern/ -version_pattern = [None,None,None,None,None,None,None, #0-6 - '000111110010010100', '001000010110111100', '001001101010011001', - '001010010011010011', '001011101111110110', '001100011101100010', - '001101100001000111', '001110011000001101', '001111100100101000', - '010000101101111000', '010001010001011101', '010010101000010111', - '010011010100110010', '010100100110100110', '010101011010000011', - '010110100011001001', '010111011111101100', '011000111011000100', - '011001000111100001', '011010111110101011', '011011000010001110', - '011100110000011010', '011101001100111111', '011110110101110101', - '011111001001010000', '100000100111010101', '100001011011110000', - '100010100010111010', '100011011110011111', '100100101100001011', - '100101010000101110', '100110101001100100', '100111010101000001', - '101000110001101001' -] - -#: This table contains the bit fields needed to specify the error code level and -#: mask pattern used by a QR Code. This table is take from: -#: -#: http://www.thonky.com/qr-code-tutorial/part-3-mask-pattern/ -type_bits = { - 'L':{ - 0:'111011111000100', - 1:'111001011110011', - 2:'111110110101010', - 3:'111100010011101', - 4:'110011000101111', - 5:'110001100011000', - 6:'110110001000001', - 7:'110100101110110', - }, - 'M':{ - 0:'101010000010010', - 1:'101000100100101', - 2:'101111001111100', - 3:'101101101001011', - 4:'100010111111001', - 5:'100000011001110', - 6:'100111110010111', - 7:'100101010100000', - }, - 'Q':{ - 0:'011010101011111', - 1:'011000001101000', - 2:'011111100110001', - 3:'011101000000110', - 4:'010010010110100', - 5:'010000110000011', - 6:'010111011011010', - 7:'010101111101101', - }, - 'H':{ - 0:'001011010001001', - 1:'001001110111110', - 2:'001110011100111', - 3:'001100111010000', - 4:'000011101100010', - 5:'000001001010101', - 6:'000110100001100', - 7:'000100000111011', - }, -} - -#: This table contains *functions* to compute whether to change current bit when -#: creating the masks. All of the functions in the table return a boolean value. -#: A True result means you should add the bit to the QR Code exactly as is. A -#: False result means you should add the opposite bit. This table was taken -#: from: -#: -#: http://www.thonky.com/qr-code-tutorial/mask-patterns/ -mask_patterns = [ - lambda row,col: (row + col) % 2 == 0, - lambda row,col: row % 2 == 0, - lambda row,col: col % 3 == 0, - lambda row,col: (row + col) % 3 == 0, - lambda row,col: ((row // 2) + (col // 3)) % 2 == 0, - lambda row,col: ((row * col) % 2) + ((row * col) % 3) == 0, - lambda row,col: (((row * col) % 2) + ((row * col) % 3)) % 2 == 0, - lambda row,col: (((row + col) % 2) + ((row * col) % 3)) % 2 == 0] diff --git a/pysimplesoap/__init__.py b/pysimplesoap/__init__.py deleted file mode 100644 index 271cba3..0000000 --- a/pysimplesoap/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""PySimpleSOAP""" - - -__author__ = "Mariano Reingart" -__author_email__ = "reingart@gmail.com" -__copyright__ = "Copyright (C) 2013 Mariano Reingart" -__license__ = "LGPL 3.0" -__version__ = "1.11" - -TIMEOUT = 30 - - -from . import client, server, simplexml, transport diff --git a/pysimplesoap/client.py b/pysimplesoap/client.py deleted file mode 100644 index ae06884..0000000 --- a/pysimplesoap/client.py +++ /dev/null @@ -1,713 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation; either version 3, 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 MERCHANTIBILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# for more details. - -"""Pythonic simple SOAP Client implementation""" - -from __future__ import unicode_literals -import sys -if sys.version > '3': - unicode = str - -try: - import cPickle as pickle -except ImportError: - import pickle -import hashlib -import logging -import os -import tempfile - -from . import __author__, __copyright__, __license__, __version__, TIMEOUT -from .simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, OrderedDict -from .transport import get_http_wrapper, set_http_wrapper, get_Http -# Utility functions used throughout wsdl_parse, moved aside for readability -from .helpers import fetch, sort_dict, make_key, process_element, \ - postprocess_element, get_message, preprocess_schema, \ - get_local_name, get_namespace_prefix, TYPE_MAP - - -log = logging.getLogger(__name__) - - -class SoapFault(RuntimeError): - def __init__(self, faultcode, faultstring): - self.faultcode = faultcode - self.faultstring = faultstring - RuntimeError.__init__(self, faultcode, faultstring) - - def __str__(self): - return self.__unicode__().encode('ascii', 'ignore') - - def __unicode__(self): - return '%s: %s' % (self.faultcode, self.faultstring) - - def __repr__(self): - return "SoapFault(%s, %s)" % (repr(self.faultcode), - repr(self.faultstring)) - - -# soap protocol specification & namespace -soap_namespaces = dict( - soap11='http://schemas.xmlsoap.org/soap/envelope/', - soap='http://schemas.xmlsoap.org/soap/envelope/', - soapenv='http://schemas.xmlsoap.org/soap/envelope/', - soap12='http://www.w3.org/2003/05/soap-env', - soap12env="http://www.w3.org/2003/05/soap-envelope", -) - - -class SoapClient(object): - """Simple SOAP Client (simil PHP)""" - def __init__(self, location=None, action=None, namespace=None, - cert=None, exceptions=True, proxy=None, ns=None, - soap_ns=None, wsdl=None, wsdl_basedir='', cache=False, cacert=None, - sessions=False, soap_server=None, timeout=TIMEOUT, - http_headers=None, trace=False, - ): - """ - :param http_headers: Additional HTTP Headers; example: {'Host': 'ipsec.example.com'} - """ - self.certssl = cert - self.keyssl = None - self.location = location # server location (url) - self.action = action # SOAP base action - self.namespace = namespace # message - self.exceptions = exceptions # lanzar execpiones? (Soap Faults) - self.xml_request = self.xml_response = '' - self.http_headers = http_headers or {} - self.wsdl_basedir = wsdl_basedir - - # shortcut to print all debugging info and sent / received xml messages - if trace: - logging.basicConfig(level=logging.DEBUG) - - if not soap_ns and not ns: - self.__soap_ns = 'soap' # 1.1 - elif not soap_ns and ns: - self.__soap_ns = 'soapenv' # 1.2 - else: - self.__soap_ns = soap_ns - - # SOAP Server (special cases like oracle, jbossas6 or jetty) - self.__soap_server = soap_server - - # SOAP Header support - self.__headers = {} # general headers - self.__call_headers = None # OrderedDict to be marshalled for RPC Call - - # check if the Certification Authority Cert is a string and store it - if cacert and cacert.startswith('-----BEGIN CERTIFICATE-----'): - fd, filename = tempfile.mkstemp() - f = os.fdopen(fd, 'w+b', -1) - log.debug("Saving CA certificate to %s" % filename) - f.write(cacert) - cacert = filename - f.close() - self.cacert = cacert - - # Create HTTP wrapper - Http = get_Http() - self.http = Http(timeout=timeout, cacert=cacert, proxy=proxy, sessions=sessions) - - # namespace prefix, None to use xmlns attribute or False to not use it: - self.__ns = ns - if not ns: - self.__xml = """ -<%(soap_ns)s:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema" - xmlns:%(soap_ns)s="%(soap_uri)s"> -<%(soap_ns)s:Header/> -<%(soap_ns)s:Body> - <%(method)s xmlns="%(namespace)s"> - - -""" - else: - self.__xml = """ -<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" xmlns:%(ns)s="%(namespace)s"> -<%(soap_ns)s:Header/> -<%(soap_ns)s:Body> - <%(ns)s:%(method)s> - - -""" - - # parse wsdl url - self.services = wsdl and self.wsdl_parse(wsdl, cache=cache) - self.service_port = None # service port for late binding - - def __getattr__(self, attr): - """Return a pseudo-method that can be called""" - if not self.services: # not using WSDL? - return lambda self=self, *args, **kwargs: self.call(attr, *args, **kwargs) - else: # using WSDL: - return lambda *args, **kwargs: self.wsdl_call(attr, *args, **kwargs) - - def call(self, method, *args, **kwargs): - """Prepare xml request and make SOAP call, returning a SimpleXMLElement. - - If a keyword argument called "headers" is passed with a value of a - SimpleXMLElement object, then these headers will be inserted into the - request. - """ - #TODO: method != input_message - # Basic SOAP request: - xml = self.__xml % dict(method=method, # method tag name - namespace=self.namespace, # method ns uri - ns=self.__ns, # method ns prefix - soap_ns=self.__soap_ns, # soap prefix & uri - soap_uri=soap_namespaces[self.__soap_ns]) - request = SimpleXMLElement(xml, namespace=self.__ns and self.namespace, - prefix=self.__ns) - - request_headers = kwargs.pop('headers', None) - - # serialize parameters - if kwargs: - parameters = list(kwargs.items()) - else: - parameters = args - if parameters and isinstance(parameters[0], SimpleXMLElement): - # merge xmlelement parameter ("raw" - already marshalled) - if parameters[0].children() is not None: - for param in parameters[0].children(): - getattr(request, method).import_node(param) - elif parameters: - # marshall parameters: - use_ns = None if self.__soap_server == "jetty" else True - for k, v in parameters: # dict: tag=valor - getattr(request, method).marshall(k, v, ns=use_ns) - elif not self.__soap_server in ('oracle',) or self.__soap_server in ('jbossas6',): - # JBossAS-6 requires no empty method parameters! - delattr(request("Body", ns=list(soap_namespaces.values()),), method) - - # construct header and parameters (if not wsdl given) except wsse - if self.__headers and not self.services: - self.__call_headers = dict([(k, v) for k, v in self.__headers.items() - if not k.startswith('wsse:')]) - # always extract WS Security header and send it - if 'wsse:Security' in self.__headers: - #TODO: namespaces too hardwired, clean-up... - header = request('Header', ns=list(soap_namespaces.values()),) - k = 'wsse:Security' - v = self.__headers[k] - header.marshall(k, v, ns=False, add_children_ns=False) - header(k)['xmlns:wsse'] = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' - # - if self.__call_headers: - header = request('Header', ns=list(soap_namespaces.values()),) - for k, v in self.__call_headers.items(): - ##if not self.__ns: - ## header['xmlns'] - if isinstance(v, SimpleXMLElement): - # allows a SimpleXMLElement to be constructed and inserted - # rather than a dictionary. marshall doesn't allow ns: prefixes - # in dict key names - header.import_node(v) - else: - header.marshall(k, v, ns=self.__ns, add_children_ns=False) - if request_headers: - header = request('Header', ns=list(soap_namespaces.values()),) - for subheader in request_headers.children(): - header.import_node(subheader) - - self.xml_request = request.as_xml() - self.xml_response = self.send(method, self.xml_request) - response = SimpleXMLElement(self.xml_response, namespace=self.namespace, - jetty=self.__soap_server in ('jetty',)) - if self.exceptions and response("Fault", ns=list(soap_namespaces.values()), error=False): - raise SoapFault(unicode(response.faultcode), unicode(response.faultstring)) - return response - - def send(self, method, xml): - """Send SOAP request using HTTP""" - if self.location == 'test': return - # location = '%s' % self.location #?op=%s" % (self.location, method) - location = str(self.location) - - if self.services: - soap_action = str(self.action) - else: - soap_action = str(self.action) + method - - headers = { - 'Content-type': 'text/xml; charset="UTF-8"', - 'Content-length': str(len(xml)), - 'SOAPAction': '"%s"' % soap_action - } - headers.update(self.http_headers) - log.info("POST %s" % location) - log.debug('\n'.join(["%s: %s" % (k, v) for k, v in headers.items()])) - log.debug(xml) - - response, content = self.http.request( - location, 'POST', body=xml, headers=headers) - self.response = response - self.content = content - - log.debug('\n'.join(["%s: %s" % (k, v) for k, v in response.items()])) - log.debug(content) - return content - - def get_operation(self, method): - # try to find operation in wsdl file - soap_ver = self.__soap_ns.startswith('soap12') and 'soap12' or 'soap11' - if not self.service_port: - for service_name, service in self.services.items(): - for port_name, port in [port for port in service['ports'].items()]: - if port['soap_ver'] == soap_ver: - self.service_port = service_name, port_name - break - else: - raise RuntimeError('Cannot determine service in WSDL: ' - 'SOAP version: %s' % soap_ver) - else: - port = self.services[self.service_port[0]]['ports'][self.service_port[1]] - if not self.location: - self.location = port['location'] - operation = port['operations'].get(method) - if not operation: - raise RuntimeError('Operation %s not found in WSDL: ' - 'Service/Port Type: %s' % - (method, self.service_port)) - return operation - - def wsdl_call(self, method, *args, **kwargs): - """Pre and post process SOAP call, input and output parameters using WSDL""" - soap_uri = soap_namespaces[self.__soap_ns] - operation = self.get_operation(method) - - # get i/o type declarations: - input = operation['input'] - output = operation['output'] - header = operation.get('header') - if 'action' in operation: - self.action = operation['action'] - - if 'namespace' in operation: - self.namespace = operation['namespace'] - - # construct header and parameters - if header: - self.__call_headers = sort_dict(header, self.__headers) - method, params = self.wsdl_call_get_params(method, input, *args, **kwargs) - - # call remote procedure - response = self.call(method, *params) - # parse results: - resp = response('Body', ns=soap_uri).children().unmarshall(output) - return resp and list(resp.values())[0] # pass Response tag children - - def wsdl_call_get_params(self, method, input, *args, **kwargs): - """Build params from input and args/kwargs""" - params = inputname = inputargs = None - all_args = {} - if input: - inputname = list(input.keys())[0] - inputargs = input[inputname] - - if input and args: - # convert positional parameters to named parameters: - d = {} - for idx, arg in enumerate(args): - key = list(inputargs.keys())[idx] - if isinstance(arg, dict): - if key in arg: - d[key] = arg[key] - else: - raise KeyError('Unhandled key %s. use client.help(method)') - else: - d[key] = arg - all_args.update({inputname: d}) - - if input and (kwargs or all_args): - if kwargs: - all_args.update({inputname: kwargs}) - valid, errors, warnings = self.wsdl_validate_params(input, all_args) - if not valid: - raise ValueError('Invalid Args Structure. Errors: %s' % errors) - params = list(sort_dict(input, all_args).values())[0].items() - if self.__soap_server == 'axis': - # use the operation name - method = method - else: - # use the message (element) name - method = inputname - #elif not input: - #TODO: no message! (see wsmtxca.dummy) - else: - params = kwargs and kwargs.items() - - return (method, params) - - def wsdl_validate_params(self, struct, value): - """Validate the arguments (actual values) for the parameters structure. - Fail for any invalid arguments or type mismatches.""" - errors = [] - warnings = [] - valid = True - - # Determine parameter type - if type(struct) == type(value): - typematch = True - if not isinstance(struct, dict) and isinstance(value, dict): - typematch = True # struct can be an OrderedDict - else: - typematch = False - - if struct == str: - struct = unicode # fix for py2 vs py3 string handling - - if not isinstance(struct, (list, dict, tuple)) and struct in TYPE_MAP.keys(): - try: - struct(value) # attempt to cast input to parameter type - except: - valid = False - errors.append('Type mismatch for argument value. parameter(%s): %s, value(%s): %s' % (type(struct), struct, type(value), value)) - - elif isinstance(struct, list) and len(struct) == 1 and not isinstance(value, list): - # parameter can have a dict in a list: [{}] indicating a list is allowed, but not needed if only one argument. - next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct[0], value) - if not next_valid: - valid = False - errors.extend(next_errors) - warnings.extend(next_warnings) - - # traverse tree - elif isinstance(struct, dict): - if struct and value: - for key in value: - if key not in struct: - valid = False - errors.append('Argument key %s not in parameter. parameter: %s, args: %s' % (key, struct, value)) - else: - next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct[key], value[key]) - if not next_valid: - valid = False - errors.extend(next_errors) - warnings.extend(next_warnings) - for key in struct: - if key not in value: - warnings.append('Parameter key %s not in args. parameter: %s, value: %s' % (key, struct, value)) - elif struct and not value: - warnings.append('parameter keys not in args. parameter: %s, args: %s' % (struct, value)) - elif not struct and value: - valid = False - errors.append('Args keys not in parameter. parameter: %s, args: %s' % (struct, value)) - else: - pass - elif isinstance(struct, list): - struct_list_value = struct[0] - for item in value: - next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct_list_value, item) - if not next_valid: - valid = False - errors.extend(next_errors) - warnings.extend(next_warnings) - elif not typematch: - valid = False - errors.append('Type mismatch. parameter(%s): %s, value(%s): %s' % (type(struct), struct, type(value), value)) - - return (valid, errors, warnings) - - def help(self, method): - """Return operation documentation and invocation/returned value example""" - operation = self.get_operation(method) - input = operation.get('input') - input = input and input.values() and list(input.values())[0] - if isinstance(input, dict): - input = ", ".join("%s=%s" % (k, repr(v)) for k, v in input.items()) - elif isinstance(input, list): - input = repr(input) - output = operation.get('output') - if output: - output = list(operation['output'].values())[0] - headers = operation.get('headers') or None - return "%s(%s)\n -> %s:\n\n%s\nHeaders: %s" % ( - method, - input or '', - output and output or '', - operation.get('documentation', ''), - headers, - ) - - def wsdl_parse(self, url, cache=False): - """Parse Web Service Description v1.1""" - - log.debug('Parsing wsdl url: %s' % url) - # Try to load a previously parsed wsdl: - force_download = False - if cache: - # make md5 hash of the url for caching... - filename_pkl = '%s.pkl' % hashlib.md5(url.encode('utf-8')).hexdigest() - if isinstance(cache, str): - filename_pkl = os.path.join(cache, filename_pkl) - if os.path.exists(filename_pkl): - log.debug('Unpickle file %s' % (filename_pkl, )) - f = open(filename_pkl, 'rb') - pkl = pickle.load(f) - f.close() - # sanity check: - if pkl['version'][:-1] != __version__.split(' ')[0][:-1] or pkl['url'] != url: - import warnings - warnings.warn('version or url mismatch! discarding cached wsdl', RuntimeWarning) - log.debug('Version: %s %s' % (pkl['version'], __version__)) - log.debug('URL: %s %s' % (pkl['url'], url)) - force_download = True - else: - self.namespace = pkl['namespace'] - self.documentation = pkl['documentation'] - return pkl['services'] - - soap_ns = { - 'http://schemas.xmlsoap.org/wsdl/soap/': 'soap11', - 'http://schemas.xmlsoap.org/wsdl/soap12/': 'soap12', - } - wsdl_uri = 'http://schemas.xmlsoap.org/wsdl/' - xsd_uri = 'http://www.w3.org/2001/XMLSchema' - xsi_uri = 'http://www.w3.org/2001/XMLSchema-instance' - - # always return an unicode object: - REVERSE_TYPE_MAP['string'] = str - - # Open uri and read xml: - xml = fetch(url, self.http, cache, force_download, self.wsdl_basedir) - # Parse WSDL XML: - wsdl = SimpleXMLElement(xml, namespace=wsdl_uri) - - # Extract useful data: - self.namespace = None - self.documentation = unicode(wsdl('documentation', error=False)) or '' - - # some wsdl are splitted down in several files, join them: - imported_wsdls = {} - for element in wsdl.children() or []: - if element.get_local_name() in ('import'): - wsdl_namespace = element['namespace'] - wsdl_location = element['location'] - if wsdl_location is None: - log.warning('WSDL location not provided for %s!' % wsdl_namespace) - continue - if wsdl_location in imported_wsdls: - log.warning('WSDL %s already imported!' % wsdl_location) - continue - imported_wsdls[wsdl_location] = wsdl_namespace - log.debug('Importing wsdl %s from %s' % (wsdl_namespace, wsdl_location)) - # Open uri and read xml: - xml = fetch(wsdl_location, self.http, cache, force_download, self.wsdl_basedir) - # Parse imported XML schema (recursively): - imported_wsdl = SimpleXMLElement(xml, namespace=xsd_uri) - # merge the imported wsdl into the main document: - wsdl.import_node(imported_wsdl) - # warning: do not process schemas to avoid infinite recursion! - - - # detect soap prefix and uri (xmlns attributes of ) - xsd_ns = None - soap_uris = {} - for k, v in wsdl[:]: - if v in soap_ns and k.startswith('xmlns:'): - soap_uris[get_local_name(k)] = v - if v == xsd_uri and k.startswith('xmlns:'): - xsd_ns = get_local_name(k) - - services = {} - bindings = {} # binding_name: binding - operations = {} # operation_name: operation - port_type_bindings = {} # port_type_name: binding - messages = {} # message: element - elements = {} # element: type def - - for service in wsdl.service: - service_name = service['name'] - if not service_name: - continue # empty service? - serv = services.setdefault(service_name, {'ports': {}}) - serv['documentation'] = service['documentation'] or '' - for port in service.port: - binding_name = get_local_name(port['binding']) - operations[binding_name] = {} - address = port('address', ns=list(soap_uris.values()), error=False) - location = address and address['location'] or None - soap_uri = address and soap_uris.get(address.get_prefix()) - soap_ver = soap_uri and soap_ns.get(soap_uri) - bindings[binding_name] = {'name': binding_name, - 'service_name': service_name, - 'location': location, - 'soap_uri': soap_uri, - 'soap_ver': soap_ver, } - serv['ports'][port['name']] = bindings[binding_name] - - for binding in wsdl.binding: - binding_name = binding['name'] - soap_binding = binding('binding', ns=list(soap_uris.values()), error=False) - transport = soap_binding and soap_binding['transport'] or None - port_type_name = get_local_name(binding['type']) - bindings[binding_name].update({ - 'port_type_name': port_type_name, - 'transport': transport, 'operations': {}, - }) - if port_type_name not in port_type_bindings: - port_type_bindings[port_type_name] = [] - port_type_bindings[port_type_name].append(bindings[binding_name]) - for operation in binding.operation: - op_name = operation['name'] - op = operation('operation', ns=list(soap_uris.values()), error=False) - action = op and op['soapAction'] - d = operations[binding_name].setdefault(op_name, {}) - bindings[binding_name]['operations'][op_name] = d - d.update({'name': op_name}) - d['parts'] = {} - # input and/or ouput can be not present! - input = operation('input', error=False) - body = input and input('body', ns=list(soap_uris.values()), error=False) - d['parts']['input_body'] = body and body['parts'] or None - output = operation('output', error=False) - body = output and output('body', ns=list(soap_uris.values()), error=False) - d['parts']['output_body'] = body and body['parts'] or None - header = input and input('header', ns=list(soap_uris.values()), error=False) - d['parts']['input_header'] = header and {'message': header['message'], 'part': header['part']} or None - header = output and output('header', ns=list(soap_uris.values()), error=False) - d['parts']['output_header'] = header and {'message': header['message'], 'part': header['part']} or None - if action: - d['action'] = action - - # check axis2 namespace at schema types attributes (europa.eu checkVat) - if "http://xml.apache.org/xml-soap" in dict(wsdl[:]).values(): - # get the sub-namespace in the first schema element (see issue 8) - schema = wsdl.types('schema', ns=xsd_uri) - attrs = dict(schema[:]) - self.namespace = attrs.get('targetNamespace', self.namespace) - - imported_schemas = {} - global_namespaces = {} - - # process current wsdl schema: - for schema in wsdl.types('schema', ns=xsd_uri): - preprocess_schema(schema, imported_schemas, elements, xsd_uri, self.__soap_server, self.http, cache, force_download, self.wsdl_basedir, global_namespaces=global_namespaces) - - postprocess_element(elements) - - for message in wsdl.message: - for part in message('part', error=False) or []: - element = {} - element_name = part['element'] - if not element_name: - # some implementations (axis) uses type instead - element_name = part['type'] - type_ns = get_namespace_prefix(element_name) - type_uri = wsdl.get_namespace_uri(type_ns) - if type_uri == xsd_uri: - element_name = get_local_name(element_name) - fn = REVERSE_TYPE_MAP.get(element_name, None) - element = {part['name']: fn} - # emulate a true Element (complexType) - list(messages.setdefault((message['name'], None), {message['name']: OrderedDict()}).values())[0].update(element) - else: - element_name = get_local_name(element_name) - fn = elements.get(make_key(element_name, 'element', type_uri)) - if not fn: - # some axis servers uses complexType for part messages - fn = elements.get(make_key(element_name, 'complexType', type_uri)) - element = {message['name']: {part['name']: fn}} - else: - element = {element_name: fn} - messages[(message['name'], part['name'])] = element - - for port_type in wsdl.portType: - port_type_name = port_type['name'] - - for binding in port_type_bindings.get(port_type_name, []): - for operation in port_type.operation: - op_name = operation['name'] - op = operations[binding['name']][op_name] - op['documentation'] = unicode(operation('documentation', error=False)) or '' - if binding['soap_ver']: - #TODO: separe operation_binding from operation (non SOAP?) - if operation('input', error=False): - input_msg = get_local_name(operation.input['message']) - input_header = op['parts'].get('input_header') - if input_header: - header_msg = get_local_name(input_header.get('message')) - header_part = get_local_name(input_header.get('part')) - # warning: some implementations use a separate message! - header = get_message(messages, header_msg or input_msg, header_part) - else: - header = None # not enought info to search the header message: - op['input'] = get_message(messages, input_msg, op['parts'].get('input_body')) - op['header'] = header - try: - ns_uri = list(op['input'].values())[0].namespace - except AttributeError: - # TODO: fix if no parameters parsed or "variants" - ns = get_namespace_prefix(operation.input['message']) - ns_uri = operation.get_namespace_uri(ns) - if ns_uri: - op['namespace'] = ns_uri - else: - op['input'] = None - op['header'] = None - if operation('output', error=False): - output_msg = get_local_name(operation.output['message']) - op['output'] = get_message(messages, output_msg, op['parts'].get('output_body')) - else: - op['output'] = None - - # dump the full service/port/operation map - #log.debug(pprint.pformat(services)) - - # Save parsed wsdl (cache) - if cache: - f = open(filename_pkl, "wb") - pkl = { - 'version': __version__.split(' ')[0], - 'url': url, - 'namespace': self.namespace, - 'documentation': self.documentation, - 'services': services, - } - pickle.dump(pkl, f) - f.close() - - return services - - def __setitem__(self, item, value): - """Set SOAP Header value - this header will be sent for every request.""" - self.__headers[item] = value - - def close(self): - """Finish the connection and remove temp files""" - self.http.close() - if self.cacert.startswith(tempfile.gettempdir()): - log.debug('removing %s' % self.cacert) - os.unlink(self.cacert) - - -def parse_proxy(proxy_str): - """Parses proxy address user:pass@host:port into a dict suitable for httplib2""" - proxy_dict = {} - if proxy_str is None: - return - if '@' in proxy_str: - user_pass, host_port = proxy_str.split('@') - else: - user_pass, host_port = '', proxy_str - if ':' in host_port: - host, port = host_port.split(':') - proxy_dict['proxy_host'], proxy_dict['proxy_port'] = host, int(port) - if ':' in user_pass: - proxy_dict['proxy_user'], proxy_dict['proxy_pass'] = user_pass.split(':') - return proxy_dict - - -if __name__ == '__main__': - pass diff --git a/pysimplesoap/helpers.py b/pysimplesoap/helpers.py deleted file mode 100644 index 434a886..0000000 --- a/pysimplesoap/helpers.py +++ /dev/null @@ -1,465 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation; either version 3, 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 MERCHANTIBILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# for more details. - -"""Pythonic simple SOAP Client helpers""" - - -from __future__ import unicode_literals -import sys -if sys.version > '3': - basestring = unicode = str - -import datetime -from decimal import Decimal -import os -import logging -import hashlib -import warnings - -try: - import urllib2 - from urlparse import urlsplit -except ImportError: - from urllib import request as urllib2 - from urllib.parse import urlsplit - -from . import __author__, __copyright__, __license__, __version__ - - -log = logging.getLogger(__name__) - - -def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''): - """Download a document from a URL, save it locally if cache enabled""" - - # check / append a valid schema if not given: - url_scheme, netloc, path, query, fragment = urlsplit(url) - if not url_scheme in ('http', 'https', 'file'): - for scheme in ('http', 'https', 'file'): - try: - if not url.startswith("/") and scheme in ('http', 'https'): - tmp_url = "%s://%s" % (scheme, os.path.join(wsdl_basedir, url)) - else: - tmp_url = "%s:%s" % (scheme, os.path.join(wsdl_basedir, url)) - log.debug('Scheme not found, trying %s' % scheme) - return fetch(tmp_url, http, cache, force_download, wsdl_basedir) - except Exception as e: - log.error(e) - raise RuntimeError('No scheme given for url: %s' % url) - - # make md5 hash of the url for caching... - filename = '%s.xml' % hashlib.md5(url.encode('utf8')).hexdigest() - if isinstance(cache, basestring): - filename = os.path.join(cache, filename) - if cache and os.path.exists(filename) and not force_download: - log.info('Reading file %s' % filename) - f = open(filename, 'r') - xml = f.read() - f.close() - else: - if url_scheme == 'file': - log.info('Fetching url %s using urllib2' % url) - f = urllib2.urlopen(url) - xml = f.read() - else: - log.info('GET %s using %s' % (url, http._wrapper_version)) - response, xml = http.request(url, 'GET', None, {}) - if cache: - log.info('Writing file %s' % filename) - if not os.path.isdir(cache): - os.makedirs(cache) - f = open(filename, 'w') - if isinstance(xml, bytes): - xml = xml.decode('utf-8') - f.write(xml) - f.close() - return xml - - -def sort_dict(od, d): - """Sort parameters (same order as xsd:sequence)""" - if isinstance(od, dict): - ret = OrderedDict() - for k in od.keys(): - v = d.get(k) - # don't append null tags! - if v is not None: - if isinstance(v, dict): - v = sort_dict(od[k], v) - elif isinstance(v, list): - v = [sort_dict(od[k][0], v1) for v1 in v] - ret[k] = v - if hasattr(od, 'namespace'): - ret.namespace = od.namespace - return ret - else: - return d - - -def make_key(element_name, element_type, namespace): - """Return a suitable key for elements""" - # only distinguish 'element' vs other types - if element_type in ('complexType', 'simpleType'): - eltype = 'complexType' - else: - eltype = element_type - if eltype not in ('element', 'complexType', 'simpleType'): - raise RuntimeError("Unknown element type %s = %s" % (element_name, eltype)) - return (element_name, eltype, namespace) - - -def process_element(elements, element_name, node, element_type, xsd_uri, dialect, namespace, - soapenc_uri = 'http://schemas.xmlsoap.org/soap/encoding/'): - """Parse and define simple element types""" - - - - log.debug('Processing element %s %s' % (element_name, element_type)) - for tag in node: - if tag.get_local_name() in ('annotation', 'documentation'): - continue - elif tag.get_local_name() in ('element', 'restriction'): - log.debug('%s has no children! %s' % (element_name, tag)) - children = tag # element "alias"? - alias = True - elif tag.children(): - children = tag.children() - alias = False - else: - log.debug('%s has no children! %s' % (element_name, tag)) - continue # TODO: abstract? - d = OrderedDict() - d.namespace = namespace - for e in children: - t = e['type'] - if not t: - t = e['base'] # complexContent (extension)! - if not t: - t = 'anyType' # no type given! - t = t.split(":") - if len(t) > 1: - ns, type_name = t - else: - ns, type_name = None, t[0] - if element_name == type_name: - pass # warning with infinite recursion - uri = ns and e.get_namespace_uri(ns) or xsd_uri - if uri in (xsd_uri, soapenc_uri) and type_name != 'Array': - # look for the type, None == any - fn = REVERSE_TYPE_MAP.get(type_name, None) - elif uri == soapenc_uri and type_name == 'Array': - # arrays of simple types (look at the attribute tags): - fn = [] - for a in e.children(): - for k, v in a[:]: - if k.endswith(":arrayType"): - type_name = v - if ":" in type_name: - type_name = type_name[type_name.index(":")+1:] - if "[]" in type_name: - type_name = type_name[:type_name.index("[]")] - fn.append(REVERSE_TYPE_MAP.get(type_name, None)) - else: - fn = None - if not fn: - # simple / complex type, postprocess later - if ns: - fn_namespace = uri # use the specified namespace - else: - fn_namespace = namespace # use parent namespace (default) - for k, v in e[:]: - if k.startswith("xmlns:"): - # get the namespace uri from the element - fn_namespace = v - fn = elements.setdefault(make_key(type_name, 'complexType', fn_namespace), OrderedDict()) - - if e['maxOccurs'] == 'unbounded' or (uri == soapenc_uri and type_name == 'Array'): - # it's an array... TODO: compound arrays? and check ns uri! - if isinstance(fn, OrderedDict): - if len(children) > 1 and dialect in ('jetty',): - # Jetty style support - # {'ClassName': [{'attr1': val1, 'attr2': val2}] - fn.array = True - else: - # .NET style support (backward compatibility) - # [{'ClassName': {'attr1': val1, 'attr2': val2}] - d.array = True - else: - if dialect in ('jetty',): - # scalar support [{'attr1': [val1]}] - fn = [fn] - else: - d.array = True - - if e['name'] is not None and not alias: - e_name = e['name'] - d[e_name] = fn - else: - log.debug('complexContent/simpleType/element %s = %s' % (element_name, type_name)) - d[None] = fn - if e is not None and e.get_local_name() == 'extension' and e.children(): - # extend base element: - process_element(elements, element_name, e.children(), element_type, xsd_uri, dialect, namespace) - elements.setdefault(make_key(element_name, element_type, namespace), OrderedDict()).update(d) - - -def postprocess_element(elements): - """Fix unresolved references (elements referenced before its definition, thanks .net)""" - for k, v in elements.items(): - if isinstance(v, OrderedDict): - if v != elements: # TODO: fix recursive elements - postprocess_element(v) - if None in v and v[None]: # extension base? - if isinstance(v[None], dict): - for i, kk in enumerate(v[None]): - # extend base -keep orginal order- - if v[None] is not None: - elements[k].insert(kk, v[None][kk], i) - del v[None] - else: # "alias", just replace - log.debug('Replacing %s = %s' % (k, v[None])) - elements[k] = v[None] - #break - if v.array: - elements[k] = [v] # convert arrays to python lists - if isinstance(v, list): - for n in v: # recurse list - if isinstance(n, (OrderedDict, list)): - postprocess_element(n) - - -def get_message(messages, message_name, part_name): - if part_name: - # get the specific part of the message: - return messages.get((message_name, part_name)) - else: - # get the first part for the specified message: - for (message_name_key, part_name_key), message in messages.items(): - if message_name_key == message_name: - return message - - -get_local_name = lambda s: s and str((':' in s) and s.split(':')[1] or s) -get_namespace_prefix = lambda s: s and str((':' in s) and s.split(':')[0] or None) - - -def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces=None, qualified=False): - """Find schema elements and complex types""" - - from .simplexml import SimpleXMLElement # here to avoid recursive imports - - # analyze the namespaces used in this schema - local_namespaces = {} - for k, v in schema[:]: - if k.startswith("xmlns"): - local_namespaces[get_local_name(k)] = v - if k == 'targetNamespace': - # URI namespace reference for this schema - local_namespaces[None] = v - if k == 'elementFormDefault' and v == "qualified" and qualified is None: - qualified = True - # add schema namespaces to the global namespace dict = {URI: ns prefix} - for ns in local_namespaces.values(): - if ns not in global_namespaces: - global_namespaces[ns] = 'ns%s' % len(global_namespaces) - - for element in schema.children() or []: - if element.get_local_name() in ('import', 'include',): - schema_namespace = element['namespace'] - schema_location = element['schemaLocation'] - if schema_location is None: - log.debug('Schema location not provided for %s!' % schema_namespace) - continue - if schema_location in imported_schemas: - log.debug('Schema %s already imported!' % schema_location) - continue - imported_schemas[schema_location] = schema_namespace - log.debug('Importing schema %s from %s' % (schema_namespace, schema_location)) - # Open uri and read xml: - xml = fetch(schema_location, http, cache, force_download, wsdl_basedir) - - # Parse imported XML schema (recursively): - imported_schema = SimpleXMLElement(xml, namespace=xsd_uri) - preprocess_schema(imported_schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces) - - element_type = element.get_local_name() - if element_type in ('element', 'complexType', "simpleType"): - namespace = local_namespaces[None] # get targetNamespace - element_ns = global_namespaces[ns] # get the prefix - element_name = element['name'] - log.debug("Parsing Element %s: %s" % (element_type, element_name)) - if element.get_local_name() == 'complexType': - children = element.children() - elif element.get_local_name() == 'simpleType': - children = element('restriction', ns=xsd_uri) - elif element.get_local_name() == 'element' and element['type']: - children = element - else: - children = element.children() - if children: - children = children.children() - elif element.get_local_name() == 'element': - children = element - if children: - process_element(elements, element_name, children, element_type, xsd_uri, dialect, namespace) - - -# simplexml utilities: - -try: - _strptime = datetime.datetime.strptime -except AttributeError: # python2.4 - _strptime = lambda s, fmt: datetime.datetime(*(time.strptime(s, fmt)[:6])) - - -# Functions to serialize/deserialize special immutable types: -def datetime_u(s): - fmt = "%Y-%m-%dT%H:%M:%S" - try: - return _strptime(s, fmt) - except ValueError: - try: - # strip utc offset - if s[-3] == ":" and s[-6] in (' ', '-', '+'): - warnings.warn('removing unsupported UTC offset', RuntimeWarning) - s = s[:-6] - # parse microseconds - try: - return _strptime(s, fmt + ".%f") - except: - return _strptime(s, fmt) - except ValueError: - # strip microseconds (not supported in this platform) - if "." in s: - warnings.warn('removing unsuppported microseconds', RuntimeWarning) - s = s[:s.index(".")] - return _strptime(s, fmt) - -datetime_m = lambda dt: dt.isoformat('T') -date_u = lambda s: _strptime(s[0:10], "%Y-%m-%d").date() -date_m = lambda d: d.strftime("%Y-%m-%d") -time_u = lambda s: _strptime(s, "%H:%M:%S").time() -time_m = lambda d: d.strftime("%H%M%S") -bool_u = lambda s: {'0': False, 'false': False, '1': True, 'true': True}[s] -bool_m = lambda s: {False: 'false', True: 'true'}[s] - - -# aliases: -class Alias(object): - def __init__(self, py_type, xml_type): - self.py_type, self.xml_type = py_type, xml_type - - def __call__(self, value): - return self.py_type(value) - - def __repr__(self): - return "" % (self.xml_type, self.py_type) - -if sys.version > '3': - long = Alias(int, 'long') -byte = Alias(str, 'byte') -short = Alias(int, 'short') -double = Alias(float, 'double') -integer = Alias(long, 'integer') -DateTime = datetime.datetime -Date = datetime.date -Time = datetime.time - -# Define convertion function (python type): xml schema type -TYPE_MAP = { - #str: 'string', - unicode: 'string', - bool: 'boolean', - short: 'short', - byte: 'byte', - int: 'int', - long: 'long', - integer: 'integer', - float: 'float', - double: 'double', - Decimal: 'decimal', - datetime.datetime: 'dateTime', - datetime.date: 'date', -} -TYPE_MARSHAL_FN = { - datetime.datetime: datetime_m, - datetime.date: date_m, - bool: bool_m -} -TYPE_UNMARSHAL_FN = { - datetime.datetime: datetime_u, - datetime.date: date_u, - bool: bool_u, - str: unicode, -} - -REVERSE_TYPE_MAP = dict([(v, k) for k, v in TYPE_MAP.items()]) - -REVERSE_TYPE_MAP.update({ - 'base64Binary': str, -}) - -class OrderedDict(dict): - """Minimal ordered dictionary for xsd:sequences""" - def __init__(self): - self.__keys = [] - self.array = False - self.namespace = None - - def __setitem__(self, key, value): - if key not in self.__keys: - self.__keys.append(key) - dict.__setitem__(self, key, value) - - def insert(self, key, value, index=0): - if key not in self.__keys: - self.__keys.insert(index, key) - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - if key in self.__keys: - self.__keys.remove(key) - dict.__delitem__(self, key) - - def __iter__(self): - return iter(self.__keys) - - def keys(self): - return self.__keys - - def items(self): - return [(key, self[key]) for key in self.__keys] - - def update(self, other): - for k, v in other.items(): - self[k] = v - # do not change if we are an array but the other is not: - if isinstance(other, OrderedDict) and not self.array: - self.array = other.array - if isinstance(other, OrderedDict) and not self.namespace: - self.namespace = other.namespace - - def copy(self): - "Make a duplicate" - new = OrderedDict() - new.update(self) - return new - - def __str__(self): - return "%s" % dict.__str__(self) - - def __repr__(self): - s = "{%s}" % ", ".join(['%s: %s' % (repr(k), repr(v)) for k, v in self.items()]) - if self.array and False: - s = "[%s]" % s - return s - diff --git a/pysimplesoap/server.py b/pysimplesoap/server.py deleted file mode 100644 index 82fb33d..0000000 --- a/pysimplesoap/server.py +++ /dev/null @@ -1,560 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation; either version 3, 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 MERCHANTIBILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# for more details. - -"""Pythonic simple SOAP Server implementation""" - - -from __future__ import unicode_literals - -import sys -import logging -import re -import traceback -try: - from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -except ImportError: - from http.server import BaseHTTPRequestHandler, HTTPServer - -from . import __author__, __copyright__, __license__, __version__ -from .simplexml import SimpleXMLElement, TYPE_MAP, Date, Decimal - -log = logging.getLogger(__name__) - -# Deprecated? -NS_RX = re.compile(r'xmlns:(\w+)="(.+?)"') - - -class SoapDispatcher(object): - """Simple Dispatcher for SOAP Server""" - - def __init__(self, name, documentation='', action='', location='', - namespace=None, prefix=False, - soap_uri="http://schemas.xmlsoap.org/soap/envelope/", - soap_ns='soap', - namespaces={}, - pretty=False, - debug=False, - **kwargs): - """ - :param namespace: Target namespace; xmlns=targetNamespace - :param prefix: Prefix for target namespace; xmlns:prefix=targetNamespace - :param namespaces: Specify additional namespaces; example: {'external': 'http://external.mt.moboperator'} - :param pretty: Prettifies generated xmls - :param debug: Use to add tracebacks in generated xmls. - - Multiple namespaces - =================== - - It is possible to support multiple namespaces. - You need to specify additional namespaces by passing `namespace` parameter. - - >>> dispatcher = SoapDispatcher( - ... name = "MTClientWS", - ... location = "http://localhost:8008/ws/MTClientWS", - ... action = 'http://localhost:8008/ws/MTClientWS', # SOAPAction - ... namespace = "http://external.mt.moboperator", prefix="external", - ... documentation = 'moboperator MTClientWS', - ... namespaces = { - ... 'external': 'http://external.mt.moboperator', - ... 'model': 'http://model.common.mt.moboperator' - ... }, - ... ns = True) - - Now the registered method must return node names with namespaces' prefixes. - - >>> def _multi_ns_func(self, serviceMsisdn): - ... ret = { - ... 'external:activateSubscriptionsReturn': [ - ... {'model:code': '0'}, - ... {'model:description': 'desc'}, - ... ]} - ... return ret - - Our prefixes will be changed to those used by the client. - """ - self.methods = {} - self.name = name - self.documentation = documentation - self.action = action # base SoapAction - self.location = location - self.namespace = namespace # targetNamespace - self.prefix = prefix - self.soap_ns = soap_ns - self.soap_uri = soap_uri - self.namespaces = namespaces - self.pretty = pretty - self.debug = debug - - @staticmethod - def _extra_namespaces(xml, ns): - """Extends xml with extra namespaces. - :param ns: dict with namespaceUrl:prefix pairs - :param xml: XML node to modify - """ - if ns: - _tpl = 'xmlns:%s="%s"' - _ns_str = " ".join([_tpl % (prefix, uri) for uri, prefix in ns.items() if uri not in xml]) - xml = xml.replace('/>', ' ' + _ns_str + '/>') - return xml - - def register_function(self, name, fn, returns=None, args=None, doc=None): - self.methods[name] = fn, returns, args, doc or getattr(fn, "__doc__", "") - - def dispatch(self, xml, action=None): - """Receive and process SOAP call""" - # default values: - prefix = self.prefix - ret = fault = None - soap_ns, soap_uri = self.soap_ns, self.soap_uri - soap_fault_code = 'VersionMismatch' - name = None - - # namespaces = [('model', 'http://model.common.mt.moboperator'), ('external', 'http://external.mt.moboperator')] - _ns_reversed = dict(((v, k) for k, v in self.namespaces.items())) # Switch keys-values - # _ns_reversed = {'http://external.mt.moboperator': 'external', 'http://model.common.mt.moboperator': 'model'} - - try: - request = SimpleXMLElement(xml, namespace=self.namespace) - - # detect soap prefix and uri (xmlns attributes of Envelope) - for k, v in request[:]: - if v in ("http://schemas.xmlsoap.org/soap/envelope/", - "http://www.w3.org/2003/05/soap-env",): - soap_ns = request.attributes()[k].localName - soap_uri = request.attributes()[k].value - - # If the value from attributes on Envelope is in additional namespaces - elif v in self.namespaces.values(): - _ns = request.attributes()[k].localName - _uri = request.attributes()[k].value - _ns_reversed[_uri] = _ns # update with received alias - # Now we change 'external' and 'model' to the received forms i.e. 'ext' and 'mod' - # After that we know how the client has prefixed additional namespaces - - ns = NS_RX.findall(xml) - for k, v in ns: - if v in self.namespaces.values(): - _ns_reversed[v] = k - - soap_fault_code = 'Client' - - # parse request message and get local method - method = request('Body', ns=soap_uri).children()(0) - if action: - # method name = action - name = action[len(self.action)+1:-1] - prefix = self.prefix - if not action or not name: - # method name = input message name - name = method.get_local_name() - prefix = method.get_prefix() - - log.debug('dispatch method: %s', name) - function, returns_types, args_types, doc = self.methods[name] - log.debug('returns_types %s', returns_types) - - # de-serialize parameters (if type definitions given) - if args_types: - args = method.children().unmarshall(args_types) - elif args_types is None: - args = {'request': method} # send raw request - else: - args = {} # no parameters - - soap_fault_code = 'Server' - # execute function - ret = function(**args) - log.debug('dispathed method returns: %s', ret) - - except Exception: # This shouldn't be one huge try/except - import sys - etype, evalue, etb = sys.exc_info() - log.error(traceback.format_exc()) - if self.debug: - detail = ''.join(traceback.format_exception(etype, evalue, etb)) - detail += '\n\nXML REQUEST\n\n' + xml - else: - detail = None - fault = {'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), - 'faultstring': evalue, - 'detail': detail} - - # build response message - if not prefix: - xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"/>""" - else: - xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" - xmlns:%(prefix)s="%(namespace)s"/>""" - - xml %= { # a %= {} is a shortcut for a = a % {} - 'namespace': self.namespace, - 'prefix': prefix, - 'soap_ns': soap_ns, - 'soap_uri': soap_uri - } - - # Now we add extra namespaces - xml = SoapDispatcher._extra_namespaces(xml, _ns_reversed) - - # Change our namespace alias to that given by the client. - # We put [('model', 'http://model.common.mt.moboperator'), ('external', 'http://external.mt.moboperator')] - # mix it with {'http://external.mt.moboperator': 'ext', 'http://model.common.mt.moboperator': 'mod'} - mapping = dict(((k, _ns_reversed[v]) for k, v in self.namespaces.items())) # Switch keys-values and change value - # and get {'model': u'mod', 'external': u'ext'} - - response = SimpleXMLElement(xml, - namespace=self.namespace, - namespaces_map=mapping, - prefix=prefix) - - response['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" - response['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema" - - body = response.add_child("%s:Body" % soap_ns, ns=False) - - if fault: - # generate a Soap Fault (with the python exception) - body.marshall("%s:Fault" % soap_ns, fault, ns=False) - else: - # return normal value - res = body.add_child("%sResponse" % name, ns=prefix) - if not prefix: - res['xmlns'] = self.namespace # add target namespace - - # serialize returned values (response) if type definition available - if returns_types: - if not isinstance(ret, dict): - res.marshall(returns_types.keys()[0], ret, ) - else: - for k, v in ret.items(): - res.marshall(k, v) - elif returns_types is None: - # merge xmlelement returned - res.import_node(ret) - elif returns_types == {}: - log.warning('Given returns_types is an empty dict.') - - return response.as_xml(pretty=self.pretty) - - # Introspection functions: - - def list_methods(self): - """Return a list of aregistered operations""" - return [(method, doc) for method, (function, returns, args, doc) in self.methods.items()] - - def help(self, method=None): - """Generate sample request and response messages""" - (function, returns, args, doc) = self.methods[method] - xml = """ - -<%(method)s xmlns="%(namespace)s"/> -""" % {'method': method, 'namespace': self.namespace} - request = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix) - if args: - items = args.items() - elif args is None: - items = [('value', None)] - else: - items = [] - for k, v in items: - request(method).marshall(k, v, add_comments=True, ns=False) - - xml = """ - -<%(method)sResponse xmlns="%(namespace)s"/> -""" % {'method': method, 'namespace': self.namespace} - response = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix) - if returns: - items = returns.items() - elif args is None: - items = [('value', None)] - else: - items = [] - for k, v in items: - response('%sResponse' % method).marshall(k, v, add_comments=True, ns=False) - - return request.as_xml(pretty=True), response.as_xml(pretty=True), doc - - def wsdl(self): - """Generate Web Service Description v1.1""" - xml = """ - - %(documentation)s - - - - - - - -""" % {'namespace': self.namespace, 'name': self.name, 'documentation': self.documentation} - wsdl = SimpleXMLElement(xml) - - for method, (function, returns, args, doc) in self.methods.items(): - # create elements: - - def parse_element(name, values, array=False, complex=False): - if not complex: - element = wsdl('wsdl:types')('xsd:schema').add_child('xsd:element') - complex = element.add_child("xsd:complexType") - else: - complex = wsdl('wsdl:types')('xsd:schema').add_child('xsd:complexType') - element = complex - element['name'] = name - if values: - items = values - elif values is None: - items = [('value', None)] - else: - items = [] - if not array and items: - all = complex.add_child("xsd:all") - elif items: - all = complex.add_child("xsd:sequence") - for k, v in items: - e = all.add_child("xsd:element") - e['name'] = k - if array: - e[:] = {'minOccurs': "0", 'maxOccurs': "unbounded"} - if v in TYPE_MAP.keys(): - t = 'xsd:%s' % TYPE_MAP[v] - elif v is None: - t = 'xsd:anyType' - elif isinstance(v, list): - n = "ArrayOf%s%s" % (name, k) - l = [] - for d in v: - l.extend(d.items()) - parse_element(n, l, array=True, complex=True) - t = "tns:%s" % n - elif isinstance(v, dict): - n = "%s%s" % (name, k) - parse_element(n, v.items(), complex=True) - t = "tns:%s" % n - e.add_attribute('type', t) - - parse_element("%s" % method, args and args.items()) - parse_element("%sResponse" % method, returns and returns.items()) - - # create messages: - for m, e in ('Input', ''), ('Output', 'Response'): - message = wsdl.add_child('wsdl:message') - message['name'] = "%s%s" % (method, m) - part = message.add_child("wsdl:part") - part[:] = {'name': 'parameters', - 'element': 'tns:%s%s' % (method, e)} - - # create ports - portType = wsdl.add_child('wsdl:portType') - portType['name'] = "%sPortType" % self.name - for method, (function, returns, args, doc) in self.methods.items(): - op = portType.add_child('wsdl:operation') - op['name'] = method - if doc: - op.add_child("wsdl:documentation", doc) - input = op.add_child("wsdl:input") - input['message'] = "tns:%sInput" % method - output = op.add_child("wsdl:output") - output['message'] = "tns:%sOutput" % method - - # create bindings - binding = wsdl.add_child('wsdl:binding') - binding['name'] = "%sBinding" % self.name - binding['type'] = "tns:%sPortType" % self.name - soapbinding = binding.add_child('soap:binding') - soapbinding['style'] = "document" - soapbinding['transport'] = "http://schemas.xmlsoap.org/soap/http" - for method in self.methods.keys(): - op = binding.add_child('wsdl:operation') - op['name'] = method - soapop = op.add_child('soap:operation') - soapop['soapAction'] = self.action + method - soapop['style'] = 'document' - input = op.add_child("wsdl:input") - ##input.add_attribute('name', "%sInput" % method) - soapbody = input.add_child("soap:body") - soapbody["use"] = "literal" - output = op.add_child("wsdl:output") - ##output.add_attribute('name', "%sOutput" % method) - soapbody = output.add_child("soap:body") - soapbody["use"] = "literal" - - service = wsdl.add_child('wsdl:service') - service["name"] = "%sService" % self.name - service.add_child('wsdl:documentation', text=self.documentation) - port = service.add_child('wsdl:port') - port["name"] = "%s" % self.name - port["binding"] = "tns:%sBinding" % self.name - soapaddress = port.add_child('soap:address') - soapaddress["location"] = self.location - return wsdl.as_xml(pretty=True) - - -class SOAPHandler(BaseHTTPRequestHandler): - - def do_GET(self): - """User viewable help information and wsdl""" - args = self.path[1:].split("?") - if self.path != "/" and args[0] not in self.server.dispatcher.methods.keys(): - self.send_error(404, "Method not found: %s" % args[0]) - else: - if self.path == "/": - # return wsdl if no method supplied - response = self.server.dispatcher.wsdl() - else: - # return supplied method help (?request or ?response messages) - req, res, doc = self.server.dispatcher.help(args[0]) - if len(args) == 1 or args[1] == "request": - response = req - else: - response = res - self.send_response(200) - self.send_header("Content-type", "text/xml") - self.end_headers() - self.wfile.write(response) - - def do_POST(self): - """SOAP POST gateway""" - self.send_response(200) - self.send_header("Content-type", "text/xml") - self.end_headers() - request = self.rfile.read(int(self.headers.getheader('content-length'))) - response = self.server.dispatcher.dispatch(request) - self.wfile.write(response) - - -class WSGISOAPHandler(object): - - def __init__(self, dispatcher): - self.dispatcher = dispatcher - - def __call__(self, environ, start_response): - return self.handler(environ, start_response) - - def handler(self, environ, start_response): - if environ['REQUEST_METHOD'] == 'GET': - return self.do_get(environ, start_response) - elif environ['REQUEST_METHOD'] == 'POST': - return self.do_post(environ, start_response) - else: - start_response('405 Method not allowed', [('Content-Type', 'text/plain')]) - return ['Method not allowed'] - - def do_get(self, environ, start_response): - path = environ.get('PATH_INFO').lstrip('/') - query = environ.get('QUERY_STRING') - if path != "" and path not in self.dispatcher.methods.keys(): - start_response('404 Not Found', [('Content-Type', 'text/plain')]) - return ["Method not found: %s" % path] - elif path == "": - # return wsdl if no method supplied - response = self.dispatcher.wsdl() - else: - # return supplied method help (?request or ?response messages) - req, res, doc = self.dispatcher.help(path) - if len(query) == 0 or query == "request": - response = req - else: - response = res - start_response('200 OK', [('Content-Type', 'text/xml'), ('Content-Length', str(len(response)))]) - return [response] - - def do_post(self, environ, start_response): - length = int(environ['CONTENT_LENGTH']) - request = environ['wsgi.input'].read(length) - response = self.dispatcher.dispatch(request) - start_response('200 OK', [('Content-Type', 'text/xml'), ('Content-Length', str(len(response)))]) - return [response] - - -if __name__ == "__main__": - - dispatcher = SoapDispatcher( - name="PySimpleSoapSample", - location="http://localhost:8008/", - action='http://localhost:8008/', # SOAPAction - namespace="http://example.com/pysimplesoapsamle/", prefix="ns0", - documentation='Example soap service using PySimpleSoap', - trace=True, - ns=True) - - def adder(p, c, dt=None): - """Add several values""" - import datetime - dt = dt + datetime.timedelta(365) - return {'ab': p['a'] + p['b'], 'dd': c[0]['d'] + c[1]['d'], 'dt': dt} - - def dummy(in0): - """Just return input""" - return in0 - - def echo(request): - """Copy request->response (generic, any type)""" - return request.value - - dispatcher.register_function( - 'Adder', adder, - returns={'AddResult': {'ab': int, 'dd': str}}, - args={'p': {'a': int, 'b': int}, 'dt': Date, 'c': [{'d': Decimal}]} - ) - - dispatcher.register_function( - 'Dummy', dummy, - returns={'out0': str}, - args={'in0': str} - ) - - dispatcher.register_function('Echo', echo) - - if '--local' in sys.argv: - - wsdl = dispatcher.wsdl() - - for method, doc in dispatcher.list_methods(): - request, response, doc = dispatcher.help(method) - - if '--serve' in sys.argv: - log.info("Starting server...") - httpd = HTTPServer(("", 8008), SOAPHandler) - httpd.dispatcher = dispatcher - httpd.serve_forever() - - if '--wsgi-serve' in sys.argv: - log.info("Starting wsgi server...") - from wsgiref.simple_server import make_server - application = WSGISOAPHandler(dispatcher) - wsgid = make_server('', 8008, application) - wsgid.serve_forever() - - if '--consume' in sys.argv: - from .client import SoapClient - client = SoapClient( - location="http://localhost:8008/", - action='http://localhost:8008/', # SOAPAction - namespace="http://example.com/sample.wsdl", - soap_ns='soap', - trace=True, - ns=False - ) - p = {'a': 1, 'b': 2} - c = [{'d': '1.20'}, {'d': '2.01'}] - response = client.Adder(p=p, dt='20100724', c=c) - result = response.AddResult - log.info(int(result.ab)) - log.info(str(result.dd)) diff --git a/pysimplesoap/simplexml.py b/pysimplesoap/simplexml.py deleted file mode 100644 index 61538e2..0000000 --- a/pysimplesoap/simplexml.py +++ /dev/null @@ -1,481 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation; either version 3, 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 MERCHANTIBILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# for more details. - -"""Simple XML manipulation""" - - -from __future__ import unicode_literals -import sys -if sys.version > '3': - basestring = str - unicode = str - -import logging -import re -import time -import xml.dom.minidom - -from . import __author__, __copyright__, __license__, __version__ - -# Utility functions used for marshalling, moved aside for readability -from .helpers import TYPE_MAP, TYPE_MARSHAL_FN, TYPE_UNMARSHAL_FN, \ - REVERSE_TYPE_MAP, OrderedDict, Date, Decimal - -log = logging.getLogger(__name__) - - -class SimpleXMLElement(object): - """Simple XML manipulation (simil PHP)""" - - def __init__(self, text=None, elements=None, document=None, - namespace=None, prefix=None, namespaces_map={}, jetty=False): - """ - :param namespaces_map: How to map our namespace prefix to that given by the client; - {prefix: received_prefix} - """ - self.__namespaces_map = namespaces_map - _rx = "|".join(namespaces_map.keys()) # {'external': 'ext', 'model': 'mod'} -> 'external|model' - self.__ns_rx = re.compile(r"^(%s):.*$" % _rx) # And now we build an expression ^(external|model):.*$ - # to find prefixes in all xml nodes i.e.: 1 - # and later change that to 1 - self.__ns = namespace - self.__prefix = prefix - self.__jetty = jetty # special list support - - if text is not None: - try: - self.__document = xml.dom.minidom.parseString(text) - except: - log.error(text) - raise - self.__elements = [self.__document.documentElement] - else: - self.__elements = elements - self.__document = document - - def add_child(self, name, text=None, ns=True): - """Adding a child tag to a node""" - if not ns or self.__ns is False: - ##log.debug('adding %s without namespace', name) - element = self.__document.createElement(name) - else: - ##log.debug('adding %s ns "%s" %s', name, self.__ns, ns) - if isinstance(ns, basestring): - element = self.__document.createElement(name) - element.setAttribute("xmlns", ns) - elif self.__prefix: - element = self.__document.createElementNS(self.__ns, "%s:%s" % (self.__prefix, name)) - else: - element = self.__document.createElementNS(self.__ns, name) - # don't append null tags! - if text is not None: - element.appendChild(self.__document.createTextNode(text)) - self._element.appendChild(element) - return SimpleXMLElement( - elements=[element], - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - jetty=self.__jetty, - namespaces_map=self.__namespaces_map - ) - - def __setattr__(self, tag, text): - """Add text child tag node (short form)""" - if tag.startswith("_"): - object.__setattr__(self, tag, text) - else: - ##log.debug('__setattr__(%s, %s)', tag, text) - self.add_child(tag, text) - - def __delattr__(self, tag): - """Remove a child tag (non recursive!)""" - elements = [__element for __element in self._element.childNodes - if __element.nodeType == __element.ELEMENT_NODE] - for element in elements: - self._element.removeChild(element) - - def add_comment(self, data): - """Add an xml comment to this child""" - comment = self.__document.createComment(data) - self._element.appendChild(comment) - - def as_xml(self, filename=None, pretty=False): - """Return the XML representation of the document""" - if not pretty: - return self.__document.toxml('UTF-8') - else: - return self.__document.toprettyxml(encoding='UTF-8') - - def __repr__(self): - """Return the XML representation of this tag""" - # NOTE: do not use self.as_xml('UTF-8') as it returns the whole xml doc - return self._element.toxml('UTF-8') - - def get_name(self): - """Return the tag name of this node""" - return self._element.tagName - - def get_local_name(self): - """Return the tag local name (prefix:name) of this node""" - return self._element.localName - - def get_prefix(self): - """Return the namespace prefix of this node""" - return self._element.prefix - - def get_namespace_uri(self, ns): - """Return the namespace uri for a prefix""" - element = self._element - while element is not None and element.attributes is not None: - try: - return element.attributes['xmlns:%s' % ns].value - except KeyError: - element = element.parentNode - - def attributes(self): - """Return a dict of attributes for this tag""" - #TODO: use slice syntax [:]? - return self._element.attributes - - def __getitem__(self, item): - """Return xml tag attribute value or a slice of attributes (iter)""" - ##log.debug('__getitem__(%s)', item) - if isinstance(item, basestring): - if self._element.hasAttribute(item): - return self._element.attributes[item].value - elif isinstance(item, slice): - # return a list with name:values - return list(self._element.attributes.items())[item] - else: - # return element by index (position) - element = self.__elements[item] - return SimpleXMLElement( - elements=[element], - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - jetty=self.__jetty, - namespaces_map=self.__namespaces_map - ) - - def add_attribute(self, name, value): - """Set an attribute value from a string""" - self._element.setAttribute(name, value) - - def __setitem__(self, item, value): - """Set an attribute value""" - if isinstance(item, basestring): - self.add_attribute(item, value) - elif isinstance(item, slice): - # set multiple attributes at once - for k, v in value.items(): - self.add_attribute(k, v) - - def __call__(self, tag=None, ns=None, children=False, root=False, - error=True, ): - """Search (even in child nodes) and return a child tag by name""" - try: - if root: - # return entire document - return SimpleXMLElement( - elements=[self.__document.documentElement], - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - jetty=self.__jetty, - namespaces_map=self.__namespaces_map - ) - if tag is None: - # if no name given, iterate over siblings (same level) - return self.__iter__() - if children: - # future: filter children? by ns? - return self.children() - elements = None - if isinstance(tag, int): - # return tag by index - elements = [self.__elements[tag]] - if ns and not elements: - for ns_uri in isinstance(ns, (tuple, list)) and ns or (ns, ): - ##log.debug('searching %s by ns=%s', tag, ns_uri) - elements = self._element.getElementsByTagNameNS(ns_uri, tag) - if elements: - break - if self.__ns and not elements: - ##log.debug('searching %s by ns=%s', tag, self.__ns) - elements = self._element.getElementsByTagNameNS(self.__ns, tag) - if not elements: - ##log.debug('searching %s', tag) - elements = self._element.getElementsByTagName(tag) - if not elements: - ##log.debug(self._element.toxml()) - if error: - raise AttributeError("No elements found") - else: - return - return SimpleXMLElement( - elements=elements, - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - jetty=self.__jetty, - namespaces_map=self.__namespaces_map) - except AttributeError as e: - raise AttributeError("Tag not found: %s (%s)" % (tag, e)) - - def __getattr__(self, tag): - """Shortcut for __call__""" - return self.__call__(tag) - - def __iter__(self): - """Iterate over xml tags at this level""" - try: - for __element in self.__elements: - yield SimpleXMLElement( - elements=[__element], - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - jetty=self.__jetty, - namespaces_map=self.__namespaces_map) - except: - raise - - def __dir__(self): - """List xml children tags names""" - return [node.tagName for node - in self._element.childNodes - if node.nodeType != node.TEXT_NODE] - - def children(self): - """Return xml children tags element""" - elements = [__element for __element in self._element.childNodes - if __element.nodeType == __element.ELEMENT_NODE] - if not elements: - return None - #raise IndexError("Tag %s has no children" % self._element.tagName) - return SimpleXMLElement( - elements=elements, - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - jetty=self.__jetty, - namespaces_map=self.__namespaces_map - ) - - def __len__(self): - """Return element count""" - return len(self.__elements) - - def __contains__(self, item): - """Search for a tag name in this element or child nodes""" - return self._element.getElementsByTagName(item) - - def __unicode__(self): - """Returns the unicode text nodes of the current element""" - if self._element.childNodes: - rc = "" - for node in self._element.childNodes: - if node.nodeType == node.TEXT_NODE: - rc = rc + node.data - return rc - return '' - - def __str__(self): - """Returns the str text nodes of the current element""" - return self.__unicode__() - - def __int__(self): - """Returns the integer value of the current element""" - return int(self.__str__()) - - def __float__(self): - """Returns the float value of the current element""" - try: - return float(self.__str__()) - except: - raise IndexError(self._element.toxml()) - - _element = property(lambda self: self.__elements[0]) - - def unmarshall(self, types, strict=True): - #import pdb; pdb.set_trace() - - """Convert to python values the current serialized xml element""" - # types is a dict of {tag name: convertion function} - # strict=False to use default type conversion if not specified - # example: types={'p': {'a': int,'b': int}, 'c': [{'d':str}]} - # expected xml:

12

holachau - # returnde value: {'p': {'a':1,'b':2}, `'c':[{'d':'hola'},{'d':'chau'}]} - d = {} - for node in self(): - name = str(node.get_local_name()) - ref_name_type = None - # handle multirefs: href="#id0" - if 'href' in node.attributes().keys(): - href = node['href'][1:] - for ref_node in self(root=True)("multiRef"): - if ref_node['id'] == href: - node = ref_node - ref_name_type = ref_node['xsi:type'].split(":")[1] - break - - try: - if isinstance(types, dict): - fn = types[name] - # custom array only in the response (not defined in the WSDL): - # 1): - # Jetty array style support [{k, v}] - for parent in node: - tmp_dict = {} # unmarshall each value & mix - for child in (node.children() or []): - tmp_dict.update(child.unmarshall(fn[0], strict)) - value.append(tmp_dict) - else: # .Net / Java - for child in (children or []): - value.append(child.unmarshall(fn[0], strict)) - - elif isinstance(fn, tuple): - value = [] - _d = {} - children = node.children() - as_dict = len(fn) == 1 and isinstance(fn[0], dict) - - for child in (children and children() or []): # Readability counts - if as_dict: - _d.update(child.unmarshall(fn[0], strict)) # Merging pairs - else: - value.append(child.unmarshall(fn[0], strict)) - if as_dict: - value.append(_d) - - if name in d: - _tmp = list(d[name]) - _tmp.extend(value) - value = tuple(_tmp) - else: - value = tuple(value) - - elif isinstance(fn, dict): - ##if ref_name_type is not None: - ## fn = fn[ref_name_type] - children = node.children() - value = children and children.unmarshall(fn, strict) - else: - if fn is None: # xsd:anyType not unmarshalled - value = node - elif unicode(node) or (fn == str and unicode(node) != ''): - try: - # get special deserialization function (if any) - fn = TYPE_UNMARSHAL_FN.get(fn, fn) - if fn == str: - # always return an unicode object: - # (avoid encoding errors in py<3!) - value = unicode(node) - else: - value = fn(unicode(node)) - except (ValueError, TypeError) as e: - raise ValueError("Tag: %s: %s" % (name, e)) - else: - value = None - d[name] = value - return d - - def _update_ns(self, name): - """Replace the defined namespace alias with tohse used by the client.""" - pref = self.__ns_rx.search(name) - if pref: - pref = pref.groups()[0] - try: - name = name.replace(pref, self.__namespaces_map[pref]) - except KeyError: - log.warning('Unknown namespace alias %s' % name) - return name - - def marshall(self, name, value, add_child=True, add_comments=False, - ns=False, add_children_ns=True): - """Analyze python value and add the serialized XML element using tag name""" - # Change node name to that used by a client - name = self._update_ns(name) - - if isinstance(value, dict): # serialize dict (value) - # for the first parent node, use the document target namespace - # (ns==True) or use the namespace string uri if passed (elements) - child = add_child and self.add_child(name, ns=ns) or self - for k, v in value.items(): - if not add_children_ns: - ns = False - else: - # for children, use the wsdl element target namespace: - ns = getattr(value, 'namespace', None) - child.marshall(k, v, add_comments=add_comments, ns=ns) - elif isinstance(value, tuple): # serialize tuple (value) - child = add_child and self.add_child(name, ns=ns) or self - if not add_children_ns: - ns = False - for k, v in value: - getattr(self, name).marshall(k, v, add_comments=add_comments, ns=ns) - elif isinstance(value, list): # serialize lists - child = self.add_child(name, ns=ns) - if not add_children_ns: - ns = False - if add_comments: - child.add_comment("Repetitive array of:") - for t in value: - child.marshall(name, t, False, add_comments=add_comments, ns=ns) - elif isinstance(value, basestring): # do not convert strings or unicodes - self.add_child(name, value, ns=ns) - elif value is None: # sent a empty tag? - self.add_child(name, ns=ns) - elif value in TYPE_MAP.keys(): - # add commented placeholders for simple tipes (for examples/help only) - child = self.add_child(name, ns=ns) - child.add_comment(TYPE_MAP[value]) - else: # the rest of object types are converted to string - # get special serialization function (if any) - fn = TYPE_MARSHAL_FN.get(type(value), str) - self.add_child(name, fn(value), ns=ns) - - def import_node(self, other): - x = self.__document.importNode(other._element, True) # deep copy - self._element.appendChild(x) diff --git a/pysimplesoap/transport.py b/pysimplesoap/transport.py deleted file mode 100644 index 9e0e6c4..0000000 --- a/pysimplesoap/transport.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation; either version 3, 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 MERCHANTIBILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# for more details. - -"""Pythonic simple SOAP Client transport""" - - -import logging -import sys -try: - import urllib2 - from cookielib import CookieJar -except ImportError: - from urllib import request as urllib2 - from http.cookiejar import CookieJar - -from . import __author__, __copyright__, __license__, __version__, TIMEOUT -from .simplexml import SimpleXMLElement, TYPE_MAP, OrderedDict - -log = logging.getLogger(__name__) - -# -# Socket wrapper to enable socket.TCP_NODELAY - this greatly speeds up transactions in Linux -# WARNING: this will modify the standard library socket module, use with care! -# TODO: implement this as a transport faciliy -# (to pass options directly to httplib2 or pycurl) -# be aware of metaclasses and socks.py (SocksiPy) used by httplib2 - -if False: - import socket - realsocket = socket.socket - def socketwrap(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0): - sockobj = realsocket(family, type, proto) - if type == socket.SOCK_STREAM: - sockobj.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - return sockobj - socket.socket = socketwrap - -# -# We store metadata about what available transport mechanisms we have available. -# -_http_connectors = {} # libname: classimpl mapping -_http_facilities = {} # functionalitylabel: [sequence of libname] mapping - - -class TransportBase: - @classmethod - def supports_feature(cls, feature_name): - return cls._wrapper_name in _http_facilities[feature_name] - -# -# httplib2 support. -# -try: - import httplib2 - #~ if sys.version > '3' and httplib2.__version__ <= "0.7.7": - if sys.version > '3': - import http.client - # httplib2 workaround: check_hostname needs a SSL context with either - # CERT_OPTIONAL or CERT_REQUIRED - # see https://code.google.com/p/httplib2/issues/detail?id=173 - orig__init__ = http.client.HTTPSConnection.__init__ - def fixer(self, host, port, key_file, cert_file, timeout, context, - check_hostname, *args, **kwargs): - chk = kwargs.get('disable_ssl_certificate_validation', True) ^ True - orig__init__(self, host, port=port, key_file=key_file, - cert_file=cert_file, timeout=timeout, context=context, - check_hostname=chk) - http.client.HTTPSConnection.__init__ = fixer -except ImportError: - TIMEOUT = None # timeout not supported by urllib2 - pass -else: - class Httplib2Transport(httplib2.Http, TransportBase): - _wrapper_version = "httplib2 %s" % httplib2.__version__ - _wrapper_name = 'httplib2' - - def __init__(self, timeout, proxy=None, cacert=None, sessions=False): - ##httplib2.debuglevel=4 - kwargs = {} - if proxy: - import socks - kwargs['proxy_info'] = httplib2.ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, **proxy) - log.info("using proxy %s" % proxy) - - # set optional parameters according supported httplib2 version - if httplib2.__version__ >= '0.3.0': - kwargs['timeout'] = timeout - if httplib2.__version__ >= '0.7.0': - kwargs['disable_ssl_certificate_validation'] = cacert is None - kwargs['ca_certs'] = cacert - httplib2.Http.__init__(self, **kwargs) - - _http_connectors['httplib2'] = Httplib2Transport - _http_facilities.setdefault('proxy', []).append('httplib2') - _http_facilities.setdefault('cacert', []).append('httplib2') - - import inspect - if 'timeout' in inspect.getargspec(httplib2.Http.__init__)[0]: - _http_facilities.setdefault('timeout', []).append('httplib2') - - -# -# urllib2 support. -# -class urllib2Transport(TransportBase): - _wrapper_version = "urllib2 %s" % urllib2.__version__ - _wrapper_name = 'urllib2' - - def __init__(self, timeout=None, proxy=None, cacert=None, sessions=False): - if (timeout is not None) and not self.supports_feature('timeout'): - raise RuntimeError('timeout is not supported with urllib2 transport') - if proxy: - raise RuntimeError('proxy is not supported with urllib2 transport') - if cacert: - raise RuntimeError('cacert is not support with urllib2 transport') - - self.request_opener = urllib2.urlopen - if sessions: - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) - self.request_opener = opener.open - - self._timeout = timeout - - def request(self, url, method="GET", body=None, headers={}): - req = urllib2.Request(url, body, headers) - try: - f = self.request_opener(req, timeout=self._timeout) - return f.info(), f.read() - except urllib2.HTTPError as f: - if f.code != 500: - raise - return f.info(), f.read() - -_http_connectors['urllib2'] = urllib2Transport -_http_facilities.setdefault('sessions', []).append('urllib2') - -import sys -if sys.version_info >= (2, 6): - _http_facilities.setdefault('timeout', []).append('urllib2') -del sys - -# -# pycurl support. -# experimental: pycurl seems faster + better proxy support (NTLM) + ssl features -# -try: - import pycurl -except ImportError: - pass -else: - try: - from cStringIO import StringIO - except ImportError: - try: - from StringIO import StringIO - except ImportError: - from io import StringIO - - class pycurlTransport(TransportBase): - _wrapper_version = pycurl.version - _wrapper_name = 'pycurl' - - def __init__(self, timeout, proxy=None, cacert=None, sessions=False): - self.timeout = timeout - self.proxy = proxy or {} - self.cacert = cacert - - def request(self, url, method, body, headers): - c = pycurl.Curl() - c.setopt(pycurl.URL, url) - if 'proxy_host' in self.proxy: - c.setopt(pycurl.PROXY, self.proxy['proxy_host']) - if 'proxy_port' in self.proxy: - c.setopt(pycurl.PROXYPORT, self.proxy['proxy_port']) - if 'proxy_user' in self.proxy: - c.setopt(pycurl.PROXYUSERPWD, "%(proxy_user)s:%(proxy_pass)s" % self.proxy) - self.buf = StringIO() - c.setopt(pycurl.WRITEFUNCTION, self.buf.write) - #c.setopt(pycurl.READFUNCTION, self.read) - #self.body = StringIO(body) - #c.setopt(pycurl.HEADERFUNCTION, self.header) - if self.cacert: - c.setopt(c.CAINFO, self.cacert) - c.setopt(pycurl.SSL_VERIFYPEER, self.cacert and 1 or 0) - c.setopt(pycurl.SSL_VERIFYHOST, self.cacert and 2 or 0) - c.setopt(pycurl.CONNECTTIMEOUT, self.timeout / 6) - c.setopt(pycurl.TIMEOUT, self.timeout) - if method == 'POST': - c.setopt(pycurl.POST, 1) - c.setopt(pycurl.POSTFIELDS, body) - if headers: - hdrs = ['%s: %s' % (k, v) for k, v in headers.items()] - log.debug(hdrs) - c.setopt(pycurl.HTTPHEADER, hdrs) - c.perform() - c.close() - return {}, self.buf.getvalue() - - _http_connectors['pycurl'] = pycurlTransport - _http_facilities.setdefault('proxy', []).append('pycurl') - _http_facilities.setdefault('cacert', []).append('pycurl') - _http_facilities.setdefault('timeout', []).append('pycurl') - - -class DummyTransport: - """Testing class to load a xml response""" - - def __init__(self, xml_response): - self.xml_response = xml_response - - def request(self, location, method, body, headers): - log.debug("%s %s", method, location) - log.debug(headers) - log.debug(body) - return {}, self.xml_response - - -def get_http_wrapper(library=None, features=[]): - # If we are asked for a specific library, return it. - if library is not None: - try: - return _http_connectors[library] - except KeyError: - raise RuntimeError('%s transport is not available' % (library,)) - - # If we haven't been asked for a specific feature either, then just return our favourite - # implementation. - if not features: - return _http_connectors.get('httplib2', _http_connectors['urllib2']) - - # If we are asked for a connector which supports the given features, then we will - # try that. - current_candidates = _http_connectors.keys() - new_candidates = [] - for feature in features: - for candidate in current_candidates: - if candidate in _http_facilities.get(feature, []): - new_candidates.append(candidate) - current_candidates = new_candidates - new_candidates = [] - - # Return the first candidate in the list. - try: - candidate_name = current_candidates[0] - except IndexError: - raise RuntimeError("no transport available which supports these features: %s" % (features,)) - else: - return _http_connectors[candidate_name] - - -def set_http_wrapper(library=None, features=[]): - """Set a suitable HTTP connection wrapper.""" - global Http - Http = get_http_wrapper(library, features) - return Http - - -def get_Http(): - """Return current transport class""" - global Http - return Http - - -# define the default HTTP connection class (it can be changed at runtime!): -set_http_wrapper() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b787c99 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +pypng +fpdf +pygubu +selenium +pyqrcode +requests diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..aa302b8 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' Setup for Admin CFDI ''' + +from setuptools import setup, find_packages +from pip.req import parse_requirements + + +install_reqs = parse_requirements('requirements.txt', session=False) +reqs = [str(ir.req) for ir in install_reqs] + +setup(name='Admin-CFDI', + version='0.3.0', + description='Herramienta para administracion de CFDIs', + license='GPL 3.0', + author='Ver contributors.txt', + author_email='public@mauriciobaeza.net', + url='https://facturalibre.net/servicios/', + packages=find_packages(), + install_requires=reqs, + package_data={'': ['img/*.png', 'img/*.gif', 'ui/*', 'template/*']}, + scripts=[ + 'admin-cfdi','descarga-cfdi', 'cfdi2pdf', 'admin-cfdi.pyw', + 'descarga-cfdi.cmd', 'cfdi2pdf.cmd'] + ) + +# vim: ts=4 et sw=4 diff --git a/template.py b/template.py deleted file mode 100644 index 7ba50ab..0000000 --- a/template.py +++ /dev/null @@ -1,176 +0,0 @@ -import datetime -import os -from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate -from reportlab.lib import colors -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from reportlab.lib.units import cm -from reportlab.lib.pagesizes import letter -from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT -from reportlab.platypus import Paragraph, Table, TableStyle, Spacer -from reportlab.pdfgen import canvas - - -class NumberedCanvas(canvas.Canvas): - def __init__(self, *args, **kwargs): - #~ kwargs['bottomup'] = 0 - canvas.Canvas.__init__(self, *args, **kwargs) - self._saved_page_states = [] - - def showPage(self): - self._saved_page_states.append(dict(self.__dict__)) - self._startPage() - - def save(self): - """add page info to each page (page x of y)""" - page_count = len(self._saved_page_states) - for state in self._saved_page_states: - self.__dict__.update(state) - self.draw_page_number(page_count) - canvas.Canvas.showPage(self) - canvas.Canvas.save(self) - - def draw_page_number(self, page_count): - self.setFont("Helvetica", 8) - self.drawRightString(20.59 * cm, 1 * cm, - 'Página {} de {}'.format(self._pageNumber, page_count)) - - -class ReportTemplate(BaseDocTemplate): - """Override the BaseDocTemplate class to do custom handle_XXX actions""" - - def __init__(self, *args, **kwargs): - # letter 21.6 x 27.9 - kwargs['pagesize'] = letter - kwargs['rightMargin'] = 1 * cm - kwargs['leftMargin'] = 1 * cm - kwargs['topMargin'] = 4 * cm - kwargs['bottomMargin'] = 2 * cm - BaseDocTemplate.__init__(self, *args, **kwargs) - self.styles = getSampleStyleSheet() - self.header = {} - self.data = [] - - def afterPage(self): - """Called after each page has been processed""" - self.canv.saveState() - date = datetime.datetime.today().strftime('%A, %d de %B del %Y') - self.canv.setStrokeColorRGB(0, 0, 0.5) - self.canv.setFont("Helvetica", 8) - self.canv.drawRightString(20.59 * cm, 26.9 * cm, date) - self.canv.line(1 * cm, 26.4 * cm, 20.6 * cm, 26.4 * cm) - - path_cur = os.path.dirname(os.path.realpath(__file__)) - path_img = os.path.join(path_cur, 'logo.png') - try: - self.canv.drawImage(path_img, 1.5 * cm, 24.2 * cm, 2.5 * cm, 2 * cm) - except: - pass - - self.canv.roundRect( - 5 * cm, 25.4 * cm, 15.5 * cm, 0.6 * cm, 0.15 * cm, - stroke=True, fill=False) - self.canv.setFont('Helvetica-BoldOblique', 10) - self.canv.drawCentredString(12.75 * cm, 25.6 * cm, self.header['emisor']) - - self.canv.roundRect( - 5 * cm, 24.4 * cm, 15.5 * cm, 0.6 * cm, 0.15 * cm, - stroke=True, fill=False) - self.canv.setFont('Helvetica-BoldOblique', 9) - self.canv.drawCentredString(12.75 * cm, 24.6 * cm, self.header['title']) - - self.canv.line(1 * cm, 1.5 * cm, 20.6 * cm, 1.5 * cm) - self.canv.restoreState() - return - - def set_data(self, data): - self.header['emisor'] = data['emisor'] - self.header['title'] = data['title'] - cols = len(data['rows'][0]) - widths = [] - for w in data['widths']: - widths.append(float(w) * cm) - t_styles = [ - ('GRID', (0, 0), (-1, -1), 0.25, colors.darkblue), - ('FONTSIZE', (0, 0), (-1, 0), 9), - ('BOX', (0, 0), (-1, 0), 1, colors.darkblue), - ('TEXTCOLOR', (0, 0), (-1, 0), colors.darkblue), - ('FONTSIZE', (0, 1), (-1, -1), 8), - ('ALIGN', (0, 0), (-1, 0), 'CENTER'), - ('ALIGN', (0, 0), (0, -1), 'RIGHT'), - ] - if cols == 6: - t_styles += [ - ('ALIGN', (1, 1), (1, -1), 'CENTER'), - ('ALIGN', (3, 1), (3, -1), 'CENTER'), - ('ALIGN', (4, 1), (4, -1), 'RIGHT'), - ] - elif cols == 3: - t_styles += [ - ('ALIGN', (-1, 0), (-1, -1), 'RIGHT'), - ('ALIGN', (-2, 0), (-2, -1), 'RIGHT'), - ('ALIGN', (0, 0), (-1, 0), 'CENTER'), - ] - elif cols == 2: - t_styles += [ - ('ALIGN', (-1, 0), (-1, -1), 'RIGHT'), - ('ALIGN', (0, 0), (-1, 0), 'CENTER'), - ] - rows = [] - for i, r in enumerate(data['rows']): - n = i + 1 - rows.append(('{}.-'.format(n),) + r) - if cols == 6: - if r[4] == 'Cancelado': - t_styles += [ - ('GRID', (0, n), (-1, n), 0.25, colors.red), - ('TEXTCOLOR', (0, n), (-1, n), colors.red), - ] - rows.insert(0, data['titles']) - t = Table(rows, colWidths=widths, repeatRows=1) - t.setStyle(TableStyle(t_styles)) - - text = 'Total este reporte = $ {}'.format(data['total']) - ps = ParagraphStyle( - name='Total', - fontSize=12, - fontName='Helvetica-BoldOblique', - textColor=colors.darkblue, - spaceBefore=0.5 * cm, - spaceAfter=0.5 * cm) - p1 = Paragraph(text, ps) - text = 'Nota: esta suma no incluye documentos cancelados' - ps = ParagraphStyle( - name='Note', - fontSize=7, - fontName='Helvetica-BoldOblique') - p2 = Paragraph(text, ps) - self.data = [t, p1, p2] - return - - def make_pdf(self): - frame = Frame( - self.leftMargin, - self.bottomMargin, - self.width, - self.height, - id='normal') - template = PageTemplate(id='report', frames=frame) - self.addPageTemplates([template]) - self.build(self.data, canvasmaker=NumberedCanvas) - return - - -if __name__ == "__main__": - doc = ReportTemplate('filename.pdf') - styles = getSampleStyleSheet() - styleN = styles['Normal'] - styleH = styles['Heading1'] - data = [] - for i in range(100): - data.append(Paragraph("This is line %d." % i, styleN)) - frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id='normal') - template = PageTemplate(id='report', frames=frame) - doc.addPageTemplates([template]) - # Build your doc with your elements and go grab a beer - doc.build(data, canvasmaker=NumberedCanvas) -