From dcd24cf88cffa7eb59995548dd4d149db742b73f Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 4 Jun 2016 09:11:29 +0200 Subject: [PATCH] Updated fpdf to the last version (py3 compatible), fix contrib/appconfig --- gluon/contrib/appconfig.py | 7 +- gluon/contrib/fpdf/__init__.py | 8 +- gluon/contrib/fpdf/fonts.py | 2 +- gluon/contrib/fpdf/fpdf.py | 935 +++++++++++++++++++-------------- gluon/contrib/fpdf/html.py | 38 +- gluon/contrib/fpdf/php.py | 13 +- gluon/contrib/fpdf/py3k.py | 83 +++ gluon/contrib/fpdf/template.py | 204 +++---- gluon/contrib/fpdf/ttfonts.py | 724 +++++++++++++------------ gluon/contrib/pyfpdf.py | 2 +- gluon/tests/__init__.py | 2 +- gluon/tests/test_contribs.py | 6 +- 12 files changed, 1087 insertions(+), 937 deletions(-) create mode 100644 gluon/contrib/fpdf/py3k.py diff --git a/gluon/contrib/appconfig.py b/gluon/contrib/appconfig.py index 148ad900..1160e08a 100644 --- a/gluon/contrib/appconfig.py +++ b/gluon/contrib/appconfig.py @@ -27,11 +27,10 @@ Once the value has been fetched (and casted) it won't change until the process is restarted (or reload=True is passed). """ -import thread import os -from ConfigParser import SafeConfigParser -from gluon import current import json +from gluon._compat import thread, configparser +from gluon.globals import current locker = thread.allocate_lock() @@ -121,7 +120,7 @@ class AppConfigLoader(object): self.read_config() def read_config_ini(self): - config = SafeConfigParser() + config = configparser.SafeConfigParser() config.read(self.file) settings = {} for section in config.sections(): diff --git a/gluon/contrib/fpdf/__init__.py b/gluon/contrib/fpdf/__init__.py index 3ba17860..6c8a431c 100644 --- a/gluon/contrib/fpdf/__init__.py +++ b/gluon/contrib/fpdf/__init__.py @@ -4,13 +4,13 @@ "FPDF for python" __license__ = "LGPL 3.0" -__version__ = "1.7" +__version__ = "1.7.2" -from fpdf import * +from .fpdf import FPDF, FPDF_FONT_DIR, FPDF_VERSION, SYSTEM_TTFONTS, set_global, FPDF_CACHE_MODE, FPDF_CACHE_DIR try: - from html import HTMLMixin + from .html import HTMLMixin except ImportError: import warnings warnings.warn("web2py gluon package not installed, required for html2pdf") -from template import Template +from .template import Template diff --git a/gluon/contrib/fpdf/fonts.py b/gluon/contrib/fpdf/fonts.py index b30ca3d3..aeb8af6e 100644 --- a/gluon/contrib/fpdf/fonts.py +++ b/gluon/contrib/fpdf/fonts.py @@ -7,7 +7,7 @@ fpdf_charwidths = {} fpdf_charwidths['courier']={} -for i in xrange(0,256): +for i in range(0,256): fpdf_charwidths['courier'][chr(i)]=600 fpdf_charwidths['courierB']=fpdf_charwidths['courier'] fpdf_charwidths['courierI']=fpdf_charwidths['courier'] diff --git a/gluon/contrib/fpdf/fpdf.py b/gluon/contrib/fpdf/fpdf.py index 819413c2..42747011 100644 --- a/gluon/contrib/fpdf/fpdf.py +++ b/gluon/contrib/fpdf/fpdf.py @@ -13,144 +13,159 @@ # * NOTE: 'I' and 'D' destinations are disabled, and simply print to STDOUT * # **************************************************************************** +from __future__ import division, with_statement + from datetime import datetime +from functools import wraps import math import errno import os, sys, zlib, struct, re, tempfile, struct -try: - import cPickle as pickle -except ImportError: - import pickle - -# Check if PIL is available (tries importing both pypi version and corrected or manually installed versions). -# Necessary for JPEG and GIF support. -try: - try: - import Image - except: - from PIL import Image -except ImportError: - Image = None - - -from ttfonts import TTFontFile -from fonts import fpdf_charwidths -from php import substr, sprintf, print_r, UTF8ToUTF16BE, UTF8StringToArray - +from .ttfonts import TTFontFile +from .fonts import fpdf_charwidths +from .php import substr, sprintf, print_r, UTF8ToUTF16BE, UTF8StringToArray +from .py3k import PY3K, pickle, urlopen, BytesIO, Image, basestring, unicode, exception, b, hashpath # Global variables -FPDF_VERSION = '1.7.1' +FPDF_VERSION = '1.7.2' FPDF_FONT_DIR = os.path.join(os.path.dirname(__file__),'font') +FPDF_CACHE_MODE = 0 # 0 - in same folder, 1 - none, 2 - hash +FPDF_CACHE_DIR = None SYSTEM_TTFONTS = None -PY3K = sys.version_info >= (3, 0) +PAGE_FORMATS = { + "a3": (841.89, 1190.55), + "a4": (595.28, 841.89), + "a5": (420.94, 595.28), + "letter": (612, 792), + "legal": (612, 1008), +} def set_global(var, val): globals()[var] = val +def load_cache(filename): + """Return unpickled object, or None if cache unavailable""" + if not filename: + return None + try: + with open(filename, "rb") as fh: + return pickle.load(fh) + except (IOError, ValueError): # File missing, unsupported pickle, etc + return None class FPDF(object): "PDF Generation class" - def __init__(self, orientation='P',unit='mm',format='A4'): + def __init__(self, orientation = 'P', unit = 'mm', format = 'A4'): # Some checks self._dochecks() # Initialization of properties - self.offsets={} # array of object offsets - self.page=0 # current page number - self.n=2 # current object number - self.buffer='' # buffer holding in-memory PDF - self.pages={} # array containing pages - self.orientation_changes={} # array indicating orientation changes - self.state=0 # current document state - self.fonts={} # array of used fonts - self.font_files={} # array of font files - self.diffs={} # array of encoding differences - self.images={} # array of used images - self.page_links={} # array of links in pages - self.links={} # array of internal links - self.in_footer=0 # flag set when processing footer - self.lastw=0 - self.lasth=0 # height of last cell printed - self.font_family='' # current font family - self.font_style='' # current font style - self.font_size_pt=12 # current font size in points - self.underline=0 # underlining flag - self.draw_color='0 G' - self.fill_color='0 g' - self.text_color='0 g' - self.color_flag=0 # indicates whether fill and text colors are different - self.ws=0 # word spacing - self.angle=0 + self.offsets = {} # array of object offsets + self.page = 0 # current page number + self.n = 2 # current object number + self.buffer = '' # buffer holding in-memory PDF + self.pages = {} # array containing pages and metadata + self.state = 0 # current document state + self.fonts = {} # array of used fonts + self.font_files = {} # array of font files + self.diffs = {} # array of encoding differences + self.images = {} # array of used images + self.page_links = {} # array of links in pages + self.links = {} # array of internal links + self.in_footer = 0 # flag set when processing footer + self.lastw = 0 + self.lasth = 0 # height of last cell printed + self.font_family = '' # current font family + self.font_style = '' # current font style + self.font_size_pt = 12 # current font size in points + self.font_stretching = 100 # current font stretching + self.underline = 0 # underlining flag + self.draw_color = '0 G' + self.fill_color = '0 g' + self.text_color = '0 g' + self.color_flag = 0 # indicates whether fill and text colors are different + self.ws = 0 # word spacing + self.angle = 0 # Standard fonts - self.core_fonts={'courier':'Courier','courierB':'Courier-Bold','courierI':'Courier-Oblique','courierBI':'Courier-BoldOblique', - 'helvetica':'Helvetica','helveticaB':'Helvetica-Bold','helveticaI':'Helvetica-Oblique','helveticaBI':'Helvetica-BoldOblique', - 'times':'Times-Roman','timesB':'Times-Bold','timesI':'Times-Italic','timesBI':'Times-BoldItalic', - 'symbol':'Symbol','zapfdingbats':'ZapfDingbats'} + self.core_fonts={'courier': 'Courier', 'courierB': 'Courier-Bold', + 'courierI': 'Courier-Oblique', 'courierBI': 'Courier-BoldOblique', + 'helvetica': 'Helvetica', 'helveticaB': 'Helvetica-Bold', + 'helveticaI': 'Helvetica-Oblique', + 'helveticaBI': 'Helvetica-BoldOblique', + 'times': 'Times-Roman', 'timesB': 'Times-Bold', + 'timesI': 'Times-Italic', 'timesBI': 'Times-BoldItalic', + 'symbol': 'Symbol', 'zapfdingbats': 'ZapfDingbats'} + self.core_fonts_encoding = "latin-1" # Scale factor - if(unit=='pt'): - self.k=1 - elif(unit=='mm'): - self.k=72/25.4 - elif(unit=='cm'): - self.k=72/2.54 - elif(unit=='in'): - self.k=72 + if unit == "pt": + self.k = 1 + elif unit == "mm": + self.k = 72 / 25.4 + elif unit == "cm": + self.k = 72 / 2.54 + elif unit == 'in': + self.k = 72. else: - self.error('Incorrect unit: '+unit) + self.error("Incorrect unit: " + unit) # Page format - if(isinstance(format,basestring)): - format=format.lower() - if(format=='a3'): - format=(841.89,1190.55) - elif(format=='a4'): - format=(595.28,841.89) - elif(format=='a5'): - format=(420.94,595.28) - elif(format=='letter'): - format=(612,792) - elif(format=='legal'): - format=(612,1008) - else: - self.error('Unknown page format: '+format) - self.fw_pt=format[0] - self.fh_pt=format[1] - else: - self.fw_pt=format[0]*self.k - self.fh_pt=format[1]*self.k - self.fw=self.fw_pt/self.k - self.fh=self.fh_pt/self.k + self.fw_pt, self.fh_pt = self.get_page_format(format, self.k) + self.dw_pt = self.fw_pt + self.dh_pt = self.fh_pt + self.fw = self.fw_pt / self.k + self.fh = self.fh_pt / self.k # Page orientation - orientation=orientation.lower() - if(orientation=='p' or orientation=='portrait'): - self.def_orientation='P' - self.w_pt=self.fw_pt - self.h_pt=self.fh_pt - elif(orientation=='l' or orientation=='landscape'): - self.def_orientation='L' - self.w_pt=self.fh_pt - self.h_pt=self.fw_pt + orientation = orientation.lower() + if orientation in ('p', 'portrait'): + self.def_orientation = 'P' + self.w_pt = self.fw_pt + self.h_pt = self.fh_pt + elif orientation in ('l', 'landscape'): + self.def_orientation = 'L' + self.w_pt = self.fh_pt + self.h_pt = self.fw_pt else: - self.error('Incorrect orientation: '+orientation) - self.cur_orientation=self.def_orientation - self.w=self.w_pt/self.k - self.h=self.h_pt/self.k + self.error('Incorrect orientation: ' + orientation) + self.cur_orientation = self.def_orientation + self.w = self.w_pt / self.k + self.h = self.h_pt / self.k # Page margins (1 cm) - margin=28.35/self.k - self.set_margins(margin,margin) + margin = 28.35 / self.k + self.set_margins(margin, margin) # Interior cell margin (1 mm) - self.c_margin=margin/10.0 + self.c_margin = margin / 10.0 # line width (0.2 mm) - self.line_width=.567/self.k + self.line_width = .567 / self.k # Automatic page break - self.set_auto_page_break(1,2*margin) + self.set_auto_page_break(1, 2 * margin) # Full width display mode self.set_display_mode('fullwidth') # Enable compression self.set_compression(1) # Set default PDF version number - self.pdf_version='1.3' + self.pdf_version = '1.3' + + @staticmethod + def get_page_format(format, k): + "Return scale factor, page w and h size in points" + if isinstance(format, basestring): + format = format.lower() + if format in PAGE_FORMATS: + return PAGE_FORMATS[format] + else: + raise RuntimeError("Unknown page format: " + format) + else: + return (format[0] * k, format[1] * k) + + def check_page(fn): + "Decorator to protect drawing methods" + @wraps(fn) + def wrapper(self, *args, **kwargs): + if not self.page and not kwargs.get('split_only'): + self.error("No page open, you need to call add_page() first") + else: + return fn(self, *args, **kwargs) + return wrapper def set_margins(self, left,top,right=-1): "Set left, top and right margins" @@ -181,7 +196,11 @@ class FPDF(object): self.page_break_trigger=self.h-margin def set_display_mode(self, zoom,layout='continuous'): - "Set display mode in viewer" + """Set display mode in viewer + + The "zoom" argument may be 'fullpage', 'fullwidth', 'real', + 'default', or a number, interpreted as a percentage.""" + if(zoom=='fullpage' or zoom=='fullwidth' or zoom=='real' or zoom=='default' or not isinstance(zoom,basestring)): self.zoom_mode=zoom else: @@ -215,6 +234,13 @@ class FPDF(object): "Creator of document" self.creator=creator + def set_doc_option(self, opt, value): + "Set document option" + if opt == "core_fonts_encoding": + self.core_fonts_encoding = value + else: + self.error("Unknown document option \"%s\"" % str(opt)) + def alias_nb_pages(self, alias='{nb}'): "Define an alias for total number of pages" self.str_alias_nb_pages=alias @@ -243,8 +269,8 @@ class FPDF(object): #close document self._enddoc() - def add_page(self, orientation=''): - "Start a new page" + def add_page(self, orientation = '', format = '', same = False): + "Start a new page, if same page format will be same as previous" if(self.state==0): self.open() family=self.font_family @@ -258,6 +284,7 @@ class FPDF(object): fc=self.fill_color tc=self.text_color cf=self.color_flag + stretching=self.font_stretching if(self.page>0): #Page footer self.in_footer=1 @@ -266,7 +293,7 @@ class FPDF(object): #close page self._endpage() #Start new page - self._beginpage(orientation) + self._beginpage(orientation, format, same) #Set line cap style to square self._out('2 J') #Set line width @@ -302,6 +329,9 @@ class FPDF(object): self._out(fc) self.text_color=tc self.color_flag=cf + #Restore stretching + if(stretching != 100): + self.set_stretching(stretching) def header(self): "Header to be implemented in your own inherited class" @@ -342,8 +372,10 @@ class FPDF(object): self.text_color=sprintf('%.3f %.3f %.3f rg',r/255.0,g/255.0,b/255.0) self.color_flag=(self.fill_color!=self.text_color) - def get_string_width(self, s): + def get_string_width(self, s, normalized = False): "Get width of a string in the current font" + # normalized is parameter for internal use + s = s if normalized else self.normalize_text(s) cw=self.current_font['cw'] w=0 l=len(s) @@ -359,9 +391,11 @@ class FPDF(object): else: w += 500 else: - for i in xrange(0, l): + for i in range(0, l): w += cw.get(s[i],0) - return w*self.font_size/1000.0 + if self.font_stretching != 100: + w = w * self.font_stretching / 100.0 + return w * self.font_size / 1000.0 def set_line_width(self, width): "Set line width" @@ -369,6 +403,7 @@ class FPDF(object): if(self.page>0): self._out(sprintf('%.2f w',width*self.k)) + @check_page def line(self, x1,y1,x2,y2): "Draw a line" self._out(sprintf('%.2f %.2f m %.2f %.2f l S',x1*self.k,(self.h-y1)*self.k,x2*self.k,(self.h-y2)*self.k)) @@ -380,6 +415,7 @@ class FPDF(object): s = '[] 0 d' self._out(s) + @check_page def dashed_line(self, x1,y1,x2,y2, dash_length=1, space_length=1): """Draw a dashed line. Same interface as line() except: - dash_length: Length of the dash @@ -388,6 +424,7 @@ class FPDF(object): self.line(x1, y1, x2, y2) self._set_dash() + @check_page def rect(self, x,y,w,h,style=''): "Draw a rectangle" if(style=='F'): @@ -398,6 +435,43 @@ class FPDF(object): op='S' self._out(sprintf('%.2f %.2f %.2f %.2f re %s',x*self.k,(self.h-y)*self.k,w*self.k,-h*self.k,op)) + @check_page + def ellipse(self, x,y,w,h,style=''): + "Draw a ellipse" + if(style=='F'): + op='f' + elif(style=='FD' or style=='DF'): + op='B' + else: + op='S' + + cx = x + w/2.0 + cy = y + h/2.0 + rx = w/2.0 + ry = h/2.0 + + lx = 4.0/3.0*(math.sqrt(2)-1)*rx + ly = 4.0/3.0*(math.sqrt(2)-1)*ry + + self._out(sprintf('%.2f %.2f m %.2f %.2f %.2f %.2f %.2f %.2f c', + (cx+rx)*self.k, (self.h-cy)*self.k, + (cx+rx)*self.k, (self.h-(cy-ly))*self.k, + (cx+lx)*self.k, (self.h-(cy-ry))*self.k, + cx*self.k, (self.h-(cy-ry))*self.k)) + self._out(sprintf('%.2f %.2f %.2f %.2f %.2f %.2f c', + (cx-lx)*self.k, (self.h-(cy-ry))*self.k, + (cx-rx)*self.k, (self.h-(cy-ly))*self.k, + (cx-rx)*self.k, (self.h-cy)*self.k)) + self._out(sprintf('%.2f %.2f %.2f %.2f %.2f %.2f c', + (cx-rx)*self.k, (self.h-(cy+ly))*self.k, + (cx-lx)*self.k, (self.h-(cy+ry))*self.k, + cx*self.k, (self.h-(cy+ry))*self.k)) + self._out(sprintf('%.2f %.2f %.2f %.2f %.2f %.2f c %s', + (cx+lx)*self.k, (self.h-(cy+ry))*self.k, + (cx+rx)*self.k, (self.h-(cy+ly))*self.k, + (cx+rx)*self.k, (self.h-cy)*self.k, + op)) + def add_font(self, family, style='', fname='', uni=False): "Add a TrueType or Type1 font" family = family.lower() @@ -413,7 +487,7 @@ class FPDF(object): # Font already added! return if (uni): - global SYSTEM_TTFONTS + global SYSTEM_TTFONTS, FPDF_CACHE_MODE, FPDF_CACHE_DIR if os.path.exists(fname): ttffilename = fname elif (FPDF_FONT_DIR and @@ -424,15 +498,16 @@ class FPDF(object): ttffilename = os.path.join(SYSTEM_TTFONTS, fname) else: raise RuntimeError("TTF Font file not found: %s" % fname) - unifilename = os.path.splitext(ttffilename)[0] + '.pkl' name = '' - if os.path.exists(unifilename): - fh = open(unifilename) - try: - font_dict = pickle.load(fh) - finally: - fh.close() + if FPDF_CACHE_MODE == 0: + unifilename = os.path.splitext(ttffilename)[0] + '.pkl' + elif FPDF_CACHE_MODE == 2: + unifilename = os.path.join(FPDF_CACHE_DIR, \ + hashpath(ttffilename) + ".pkl") else: + unifilename = None + font_dict = load_cache(unifilename) + if font_dict is None: ttf = TTFontFile() ttf.getMetrics(ttffilename) desc = { @@ -461,18 +536,18 @@ class FPDF(object): 'originalsize': os.stat(ttffilename).st_size, 'cw': ttf.charWidths, } - try: - fh = open(unifilename, "w") - pickle.dump(font_dict, fh) - fh.close() - except IOError, e: - if not e.errno == errno.EACCES: - raise # Not a permission error. + if unifilename: + try: + with open(unifilename, "wb") as fh: + pickle.dump(font_dict, fh) + except IOError: + if not exception().errno == errno.EACCES: + raise # Not a permission error. del ttf if hasattr(self,'str_alias_nb_pages'): - sbarr = range(0,57) # include numbers in the subset! + sbarr = list(range(0,57)) # include numbers in the subset! else: - sbarr = range(0,32) + sbarr = list(range(0,32)) self.fonts[fontkey] = { 'i': len(self.fonts)+1, 'type': font_dict['type'], 'name': font_dict['name'], 'desc': font_dict['desc'], @@ -485,18 +560,16 @@ class FPDF(object): 'type': "TTF", 'ttffile': ttffilename} self.font_files[fname] = {'type': "TTF"} else: - fontfile = open(fname) - try: + with open(fname, 'rb') as fontfile: font_dict = pickle.load(fontfile) - finally: - fontfile.close() self.fonts[fontkey] = {'i': len(self.fonts)+1} self.fonts[fontkey].update(font_dict) + diff = font_dict.get('diff') if (diff): #Search existing encodings d = 0 nb = len(self.diffs) - for i in xrange(1, nb+1): + for i in range(1, nb+1): if(self.diffs[i] == diff): d = i break @@ -506,11 +579,12 @@ class FPDF(object): self.fonts[fontkey]['diff'] = d filename = font_dict.get('filename') if (filename): - if (type == 'TrueType'): + if (font_dict['type'] == 'TrueType'): + originalsize = font_dict['originalsize'] self.font_files[filename]={'length1': originalsize} else: - self.font_files[filename]={'length1': size1, - 'length2': size2} + self.font_files[filename]={'length1': font_dict['size1'], + 'length2': font_dict['size2']} def set_font(self, family,style='',size=0): "Select a font; size given in points" @@ -544,7 +618,8 @@ class FPDF(object): name=os.path.join(FPDF_FONT_DIR,family) if(family=='times' or family=='helvetica'): name+=style.lower() - execfile(name+'.font') + with open(name+'.font') as file: + exec(compile(file.read(), name+'.font', 'exec')) if fontkey not in fpdf_charwidths: self.error('Could not include font metric file for'+fontkey) i=len(self.fonts)+1 @@ -570,6 +645,14 @@ class FPDF(object): if(self.page>0): self._out(sprintf('BT /F%d %.2f Tf ET',self.current_font['i'],self.font_size_pt)) + def set_stretching(self, factor): + "Set from stretch factor percents (default: 100.0)" + if(self.font_stretching == factor): + return + self.font_stretching = factor + if (self.page > 0): + self._out(sprintf('BT %.2f Tz ET', self.font_stretching)) + def add_link(self): "Create a new internal link" n=len(self.links)+1 @@ -590,6 +673,7 @@ class FPDF(object): self.page_links[self.page] = [] self.page_links[self.page] += [(x*self.k,self.h_pt-y*self.k,w*self.k,h*self.k,link),] + @check_page def text(self, x, y, txt=''): "Output a string" txt = self.normalize_text(txt) @@ -606,6 +690,7 @@ class FPDF(object): s='q '+self.text_color+' '+s+' Q' self._out(s) + @check_page def rotate(self, angle, x=None, y=None): if x is None: x = self.x @@ -627,6 +712,7 @@ class FPDF(object): "Accept automatic page break or not" return self.auto_page_break + @check_page def cell(self, w,h=0,txt='',border=0,ln=0,align='',fill=0,link=''): "Output a cell" txt = self.normalize_text(txt) @@ -638,7 +724,7 @@ class FPDF(object): if(ws>0): self.ws=0 self._out('0 Tw') - self.add_page(self.cur_orientation) + self.add_page(same = True) self.x=x if(ws>0): self.ws=ws @@ -668,9 +754,9 @@ class FPDF(object): s+=sprintf('%.2f %.2f m %.2f %.2f l S ',x*k,(self.h-(y+h))*k,(x+w)*k,(self.h-(y+h))*k) if(txt!=''): if(align=='R'): - dx=w-self.c_margin-self.get_string_width(txt) + dx=w-self.c_margin-self.get_string_width(txt, True) elif(align=='C'): - dx=(w-self.get_string_width(txt))/2.0 + dx=(w-self.get_string_width(txt, True))/2.0 else: dx=self.c_margin if(self.color_flag): @@ -707,7 +793,7 @@ class FPDF(object): if(self.color_flag): s+=' Q' if(link): - self.link(self.x+dx,self.y+.5*h-.5*self.font_size,self.get_string_width(txt),self.font_size,link) + self.link(self.x+dx,self.y+.5*h-.5*self.font_size,self.get_string_width(txt, True),self.font_size,link) if(s): self._out(s) self.lasth=h @@ -719,6 +805,7 @@ class FPDF(object): else: self.x+=w + @check_page def multi_cell(self, w, h, txt='', border=0, align='J', fill=0, split_only=False): "Output text with automatic or explicit line breaks" txt = self.normalize_text(txt) @@ -780,7 +867,7 @@ class FPDF(object): ls=l ns+=1 if self.unifontsubset: - l += self.get_string_width(c) / self.font_size*1000.0 + l += self.get_string_width(c, True) / self.font_size*1000.0 else: l += cw.get(c,0) if(l>wmax): @@ -832,6 +919,7 @@ class FPDF(object): ret.append(substr(s,j,i-j)) return ret + @check_page def write(self, h, txt='', link=''): "Output text in flowing mode" txt = self.normalize_text(txt) @@ -864,7 +952,7 @@ class FPDF(object): if(c==' '): sep=i if self.unifontsubset: - l += self.get_string_width(c) / self.font_size*1000.0 + l += self.get_string_width(c, True) / self.font_size*1000.0 else: l += cw.get(c,0) if(l>wmax): @@ -899,6 +987,7 @@ class FPDF(object): if(i!=j): self.cell(l/1000.0*self.font_size,h,substr(s,j),0,0,'',0,link) + @check_page def image(self, name, x=None, y=None, w=0,h=0,type='',link=''): "Put an image on the page" if not name in self.images: @@ -955,7 +1044,7 @@ class FPDF(object): if (self.y + h > self.page_break_trigger and not self.in_footer and self.accept_page_break()): #Automatic page break x = self.x - self.add_page(self.cur_orientation) + self.add_page(same = True) self.x = x y = self.y self.y += h @@ -965,6 +1054,7 @@ class FPDF(object): if(link): self.link(x,y,w,h,link) + @check_page def ln(self, h=''): "Line Feed; default value is last cell height" self.x=self.l_margin @@ -1002,50 +1092,54 @@ class FPDF(object): self.set_x(x) def output(self, name='',dest=''): - "Output PDF to some destination" + """Output PDF to some destination + + By default the PDF is written to sys.stdout. If a name is given, the + PDF is written to a new file. If dest='S' is given, the PDF data is + returned as a byte string.""" + #Finish document if necessary if(self.state<3): self.close() dest=dest.upper() if(dest==''): if(name==''): - name='doc.pdf' dest='I' else: dest='F' - if dest=='I': - print self.buffer - elif dest=='D': - print self.buffer + if PY3K: + # manage binary data as latin1 until PEP461 or similar is implemented + buffer = self.buffer.encode("latin1") + else: + buffer = self.buffer + if dest in ('I', 'D'): + # Python < 3 writes byte data transparently without "buffer" + stdout = getattr(sys.stdout, 'buffer', sys.stdout) + stdout.write(buffer) elif dest=='F': #Save to local file - f=open(name,'wb') - if(not f): - self.error('Unable to create output file: '+name) - if PY3K: - # TODO: proper unicode support - f.write(self.buffer.encode("latin1")) - else: - f.write(self.buffer) - f.close() + with open(name,'wb') as f: + f.write(buffer) elif dest=='S': - #Return as a string - return self.buffer + #Return as a byte string + return buffer else: self.error('Incorrect output destination: '+dest) - return '' def normalize_text(self, txt): "Check that text input is in the correct format/encoding" # - for TTF unicode fonts: unicode object (utf8 encoding) - # - for built-in fonts: string instances (latin 1 encoding) - if self.unifontsubset and isinstance(txt, str): - txt = txt.decode('utf8') - elif not self.unifontsubset and isinstance(txt, unicode) and not PY3K: - txt = txt.encode('latin1') + # - for built-in fonts: string instances (encoding: latin-1, cp1252) + if not PY3K: + if self.unifontsubset and isinstance(txt, str): + return txt.decode("utf-8") + elif not self.unifontsubset and isinstance(txt, unicode): + return txt.encode(self.core_fonts_encoding) + else: + if not self.unifontsubset and self.core_fonts_encoding: + return txt.encode(self.core_fonts_encoding).decode("latin-1") return txt - def _dochecks(self): #Check for locale-related bug # if(1.1==1): @@ -1059,73 +1153,86 @@ class FPDF(object): return FPDF_FONT_DIR+'/' def _putpages(self): - nb=self.page - if hasattr(self,'str_alias_nb_pages'): + nb = self.page + if hasattr(self, 'str_alias_nb_pages'): # Replace number of pages in fonts using subsets (unicode) - alias = UTF8ToUTF16BE(self.str_alias_nb_pages, False); + alias = UTF8ToUTF16BE(self.str_alias_nb_pages, False) r = UTF8ToUTF16BE(str(nb), False) - for n in xrange(1, nb+1): - self.pages[n] = self.pages[n].replace(alias, r) + for n in range(1, nb + 1): + self.pages[n]["content"] = \ + self.pages[n]["content"].replace(alias, r) # Now repeat for no pages in non-subset fonts - for n in xrange(1,nb+1): - self.pages[n]=self.pages[n].replace(self.str_alias_nb_pages,str(nb)) - if(self.def_orientation=='P'): - w_pt=self.fw_pt - h_pt=self.fh_pt + for n in range(1,nb + 1): + self.pages[n]["content"] = \ + self.pages[n]["content"].replace(self.str_alias_nb_pages, + str(nb)) + if self.def_orientation == 'P': + dw_pt = self.dw_pt + dh_pt = self.dh_pt else: - w_pt=self.fh_pt - h_pt=self.fw_pt + dw_pt = self.dh_pt + dh_pt = self.dw_pt if self.compress: - filter='/Filter /FlateDecode ' + filter = '/Filter /FlateDecode ' else: - filter='' - for n in xrange(1,nb+1): - #Page + filter = '' + for n in range(1, nb + 1): + # Page self._newobj() self._out('<>>>' + rect = sprintf('%.2f %.2f %.2f %.2f', pl[0], pl[1], + pl[0] + pl[2], pl[1] - pl[3]) + annots += '<>>>' else: - l=self.links[pl[4]] + l = self.links[pl[4]] if l[0] in self.orientation_changes: - h=w_pt + h = w_pt else: - h=h_pt - annots+=sprintf('/Dest [%d 0 R /XYZ 0 %.2f null]>>',1+2*l[0],h-l[1]*self.k) - self._out(annots+']') - if(self.pdf_version>'1.3'): - self._out('/Group <>') - self._out('/Contents '+str(self.n+1)+' 0 R>>') + h = h_pt + annots += sprintf('/Dest [%d 0 R /XYZ 0 %.2f null]>>', + 1 + 2 * l[0], h - l[1] * self.k) + self._out(annots + ']') + if self.pdf_version > '1.3': + self._out('/Group <>') + self._out('/Contents ' + str(self.n + 1) + ' 0 R>>') self._out('endobj') - #Page content + # Page content + content = self.pages[n]["content"] if self.compress: - p = zlib.compress(self.pages[n]) + # manage binary data as latin1 until PEP461 or similar is implemented + p = content.encode("latin1") if PY3K else content + p = zlib.compress(p) else: - p = self.pages[n] + p = content self._newobj() - self._out('<<'+filter+'/Length '+str(len(p))+'>>') + self._out('<<' + filter + '/Length ' + str(len(p)) + '>>') self._putstream(p) self._out('endobj') - #Pages root - self.offsets[1]=len(self.buffer) + # Pages root + self.offsets[1] = len(self.buffer) self._out('1 0 obj') self._out('<>') self._out('endobj') @@ -1136,17 +1243,13 @@ class FPDF(object): self._newobj() self._out('<>') self._out('endobj') - for name,info in self.font_files.iteritems(): + for name,info in self.font_files.items(): if 'type' in info and info['type'] != 'TTF': #Font file embedding self._newobj() self.font_files[name]['n']=self.n - font='' - f=open(self._getfontpath()+name,'rb',1) - if(not f): - self.error('Font file not found') - font=f.read() - f.close() + with open(self._getfontpath()+name,'rb',1) as f: + font=f.read() compressed=(substr(name,-2)=='.z') if(not compressed and 'length2' in info): header=(ord(font[0])==128) @@ -1165,7 +1268,9 @@ class FPDF(object): self._out('>>') self._putstream(font) self._out('endobj') - for k,font in self.fonts.iteritems(): + flist = [(x[1]["i"],x[0],x[1]) for x in self.fonts.items()] + flist.sort() + for idx,k,font in flist: #Font objects self.fonts[k]['n']=self.n+1 type=font['type'] @@ -1200,15 +1305,15 @@ class FPDF(object): self._newobj() cw=font['cw'] s='[' - for i in xrange(32,256): - # Get doesn't rise exception; returns 0 instead of None if not set + for i in range(32,256): + # Get doesn't raise exception; returns 0 instead of None if not set s+=str(cw.get(chr(i)) or 0)+' ' self._out(s+']') self._out('endobj') #Descriptor self._newobj() s='<> 8) cidtogidmap[cc*2 + 1] = chr(glyph & 0xFF) - cidtogidmap = zlib.compress(''.join(cidtogidmap)); + cidtogidmap = ''.join(cidtogidmap) + if PY3K: + # manage binary data as latin1 until PEP461-like function is implemented + cidtogidmap = cidtogidmap.encode("latin1") + cidtogidmap = zlib.compress(cidtogidmap); self._newobj() self._out('< 255 and (cid not in subset): # continue width = font['cw'][cid] - if (width == 65535): width = 0 - if (cid > 255 and (cid not in font['subset']) or not cid): # + if (width == 0): continue + if (width == 65535): width = 0 if ('dw' not in font or (font['dw'] and width != font['dw'])): if (cid == (prevcid + 1)): if (width == prevwidth): @@ -1439,8 +1547,10 @@ class FPDF(object): def _putimages(self): filter='' if self.compress: - filter='/Filter /FlateDecode ' - for filename,info in self.images.iteritems(): + filter='/Filter /FlateDecode ' + i = [(x[1]["i"],x[1]) for x in self.images.items()] + i.sort() + for idx,info in i: self._putimage(info) del info['data'] if 'smask' in info: @@ -1455,7 +1565,7 @@ class FPDF(object): self._out('/Width '+str(info['w'])) self._out('/Height '+str(info['h'])) if(info['cs']=='Indexed'): - self._out('/ColorSpace [/Indexed /DeviceRGB '+str(len(info['pal'])/3-1)+' '+str(self.n+1)+' 0 R]') + self._out('/ColorSpace [/Indexed /DeviceRGB '+str(len(info['pal'])//3-1)+' '+str(self.n+1)+' 0 R]') else: self._out('/ColorSpace /'+info['cs']) if(info['cs']=='DeviceCMYK'): @@ -1467,7 +1577,7 @@ class FPDF(object): self._out('/DecodeParms <<' + info['dp'] + '>>') if('trns' in info and isinstance(info['trns'], list)): trns='' - for i in xrange(0,len(info['trns'])): + for i in range(0,len(info['trns'])): trns+=str(info['trns'][i])+' '+str(info['trns'][i])+' ' self._out('/Mask ['+trns+']') if('smask' in info): @@ -1493,14 +1603,18 @@ class FPDF(object): self._out('endobj') def _putxobjectdict(self): - for image in self.images.values(): - self._out('/I'+str(image['i'])+' '+str(image['n'])+' 0 R') + i = [(x["i"],x["n"]) for x in self.images.values()] + i.sort() + for idx,n in i: + self._out('/I'+str(idx)+' '+str(n)+' 0 R') def _putresourcedict(self): self._out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]') self._out('/Font <<') - for font in self.fonts.values(): - self._out('/F'+str(font['i'])+' '+str(font['n'])+' 0 R') + f = [(x["i"],x["n"]) for x in self.fonts.values()] + f.sort() + for idx,n in f: + self._out('/F'+str(idx)+' '+str(n)+' 0 R') self._out('>>') self._out('/XObject <<') self._putxobjectdict() @@ -1541,7 +1655,7 @@ class FPDF(object): elif(self.zoom_mode=='real'): self._out('/OpenAction [3 0 R /XYZ null null 1]') elif(not isinstance(self.zoom_mode,basestring)): - self._out('/OpenAction [3 0 R /XYZ null null '+(self.zoom_mode/100)+']') + self._out(sprintf('/OpenAction [3 0 R /XYZ null null %s]',self.zoom_mode/100)) if(self.layout_mode=='single'): self._out('/PageLayout /SinglePage') elif(self.layout_mode=='continuous'): @@ -1578,7 +1692,7 @@ class FPDF(object): self._out('xref') self._out('0 '+(str(self.n+1))) self._out('0000000000 65535 f ') - for i in xrange(1,self.n+1): + for i in range(1,self.n+1): self._out(sprintf('%010d 00000 n ',self.offsets[i])) #Trailer self._out('trailer') @@ -1590,34 +1704,43 @@ class FPDF(object): self._out('%%EOF') self.state=3 - def _beginpage(self, orientation): - self.page+=1 - self.pages[self.page]='' - self.state=2 - self.x=self.l_margin - self.y=self.t_margin - self.font_family='' - #Page orientation - if(not orientation): - orientation=self.def_orientation - else: - orientation=orientation[0].upper() - if(orientation!=self.def_orientation): - self.orientation_changes[self.page]=1 - if(orientation!=self.cur_orientation): - #Change orientation - if(orientation=='P'): - self.w_pt=self.fw_pt - self.h_pt=self.fh_pt - self.w=self.fw - self.h=self.fh + def _beginpage(self, orientation, format, same): + self.page += 1 + self.pages[self.page] = {"content": ""} + self.state = 2 + self.x = self.l_margin + self.y = self.t_margin + self.font_family = '' + self.font_stretching = 100 + if not same: + # Page format + if format: + # Change page format + self.fw_pt, self.fh_pt = self.get_page_format(format, self.k) else: - self.w_pt=self.fh_pt - self.h_pt=self.fw_pt - self.w=self.fh - self.h=self.fw - self.page_break_trigger=self.h-self.b_margin - self.cur_orientation=orientation + # Set to default format + self.fw_pt = self.dw_pt + self.fh_pt = self.dh_pt + self.fw = self.fw_pt / self.k + self.fh = self.fh_pt / self.k + # Page orientation + if not orientation: + orientation = self.def_orientation + else: + orientation = orientation[0].upper() + if orientation == 'P': + self.w_pt = self.fw_pt + self.h_pt = self.fh_pt + else: + self.w_pt = self.fh_pt + self.h_pt = self.fw_pt + self.w = self.w_pt / self.k + self.h = self.h_pt / self.k + self.cur_orientation = orientation + self.page_break_trigger = self.h - self.b_margin + self.cur_orientation = orientation + self.pages[self.page]["w_pt"] = self.w_pt + self.pages[self.page]["h_pt"] = self.h_pt def _endpage(self): #End of page contents @@ -1629,38 +1752,60 @@ class FPDF(object): self.offsets[self.n]=len(self.buffer) self._out(str(self.n)+' 0 obj') - def _dounderline(self, x,y,txt): + def _dounderline(self, x, y, txt): #Underline text up=self.current_font['up'] ut=self.current_font['ut'] - w=self.get_string_width(txt)+self.ws*txt.count(' ') + w=self.get_string_width(txt, True)+self.ws*txt.count(' ') return sprintf('%.2f %.2f %.2f %.2f re f',x*self.k,(self.h-(y-up/1000.0*self.font_size))*self.k,w*self.k,-ut/1000.0*self.font_size_pt) + def load_resource(self, reason, filename): + "Load external file" + # by default loading from network is allowed for all images + if reason == "image": + if filename.startswith("http://") or filename.startswith("https://"): + f = BytesIO(urlopen(filename).read()) + else: + f = open(filename, "rb") + return f + else: + self.error("Unknown resource loading reason \"%s\"" % reason) + def _parsejpg(self, filename): # Extract info from a JPEG file - if Image is None: - self.error('PIL not installed') + f = None try: - f = open(filename, 'rb') - im = Image.open(f) - except Exception, e: - self.error('Missing or incorrect image file: %s. error: %s' % (filename, str(e))) - else: - a = im.size - # We shouldn't get into here, as Jpeg is RGB=8bpp right(?), but, just in case... - bpc=8 - if im.mode == 'RGB': - colspace='DeviceRGB' - elif im.mode == 'CMYK': - colspace='DeviceCMYK' - else: - colspace='DeviceGray' + f = self.load_resource("image", filename) + while True: + markerHigh, markerLow = struct.unpack('BB', f.read(2)) + if markerHigh != 0xFF or markerLow < 0xC0: + raise SyntaxError('No JPEG marker found') + elif markerLow == 0xDA: # SOS + raise SyntaxError('No JPEG SOF marker found') + elif (markerLow == 0xC8 or # JPG + (markerLow >= 0xD0 and markerLow <= 0xD9) or # RSTx + (markerLow >= 0xF0 and markerLow <= 0xFD)): # JPGx + pass + else: + dataSize, = struct.unpack('>H', f.read(2)) + data = f.read(dataSize - 2) if dataSize > 2 else '' + if ((markerLow >= 0xC0 and markerLow <= 0xC3) or # SOF0 - SOF3 + (markerLow >= 0xC5 and markerLow <= 0xC7) or # SOF4 - SOF7 + (markerLow >= 0xC9 and markerLow <= 0xCB) or # SOF9 - SOF11 + (markerLow >= 0xCD and markerLow <= 0xCF)): # SOF13 - SOF15 + bpc, height, width, layers = struct.unpack_from('>BHHB', data) + colspace = 'DeviceRGB' if layers == 3 else ('DeviceCMYK' if layers == 4 else 'DeviceGray') + break + except Exception: + if f: + f.close() + self.error('Missing or incorrect image file: %s. error: %s' % (filename, str(exception()))) - # Read whole file from the start - f.seek(0) - data = f.read() - f.close() - return {'w':a[0],'h':a[1],'cs':colspace,'bpc':bpc,'f':'DCTDecode','data':data} + with f: + # Read whole file from the start + f.seek(0) + data = f.read() + return {'w':width,'h':height,'cs':colspace,'bpc':bpc,'f':'DCTDecode','data':data} def _parsegif(self, filename): # Extract info from a GIF file (via PNG conversion) @@ -1668,13 +1813,13 @@ class FPDF(object): self.error('PIL is required for GIF support') try: im = Image.open(filename) - except Exception, e: - self.error('Missing or incorrect image file: %s. error: %s' % (filename, str(e))) + except Exception: + self.error('Missing or incorrect image file: %s. error: %s' % (filename, str(exception()))) else: # Use temporary file - f = tempfile.NamedTemporaryFile(delete=False, suffix=".png") - tmp = f.name - f.close() + with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as \ + f: + tmp = f.name if "transparency" in im.info: im.save(tmp, transparency = im.info['transparency']) else: @@ -1683,27 +1828,25 @@ class FPDF(object): os.unlink(tmp) return info - def _parsepng(self, name): + def _parsepng(self, filename): #Extract info from a PNG file - if name.startswith("http://") or name.startswith("https://"): - import urllib - f = urllib.urlopen(name) - else: - f=open(name,'rb') - if(not f): - self.error("Can't open image file: "+name) + f = self.load_resource("image", filename) #Check signature - if(f.read(8)!='\x89'+'PNG'+'\r'+'\n'+'\x1a'+'\n'): - self.error('Not a PNG file: '+name) + magic = f.read(8).decode("latin1") + signature = '\x89'+'PNG'+'\r'+'\n'+'\x1a'+'\n' + if not PY3K: signature = signature.decode("latin1") + if(magic!=signature): + self.error('Not a PNG file: ' + filename) #Read header chunk f.read(4) - if(f.read(4)!='IHDR'): - self.error('Incorrect PNG file: '+name) + chunk = f.read(4).decode("latin1") + if(chunk!='IHDR'): + self.error('Incorrect PNG file: ' + filename) w=self._freadint(f) h=self._freadint(f) bpc=ord(f.read(1)) if(bpc>8): - self.error('16-bit depth not supported: '+name) + self.error('16-bit depth not supported: ' + filename) ct=ord(f.read(1)) if(ct==0 or ct==4): colspace='DeviceGray' @@ -1712,13 +1855,13 @@ class FPDF(object): elif(ct==3): colspace='Indexed' else: - self.error('Unknown color type: '+name) + self.error('Unknown color type: ' + filename) if(ord(f.read(1))!=0): - self.error('Unknown compression method: '+name) + self.error('Unknown compression method: ' + filename) if(ord(f.read(1))!=0): - self.error('Unknown filter method: '+name) + self.error('Unknown filter method: ' + filename) if(ord(f.read(1))!=0): - self.error('Interlacing not supported: '+name) + self.error('Interlacing not supported: ' + filename) f.read(4) dp='/Predictor 15 /Colors ' if colspace == 'DeviceRGB': @@ -1729,11 +1872,11 @@ class FPDF(object): #Scan chunks looking for palette, transparency and image data pal='' trns='' - data='' + data=bytes() if PY3K else str() n=1 while n != None: n=self._freadint(f) - type=f.read(4) + type=f.read(4).decode("latin1") if(type=='PLTE'): #Read palette pal=f.read(n) @@ -1746,7 +1889,7 @@ class FPDF(object): elif(ct==2): trns=[ord(substr(t,1,1)),ord(substr(t,3,1)),ord(substr(t,5,1))] else: - pos=t.find('\x00') + pos=t.find('\x00'.encode("latin1")) if(pos!=-1): trns=[pos,] f.read(4) @@ -1759,34 +1902,38 @@ class FPDF(object): else: f.read(n+4) if(colspace=='Indexed' and not pal): - self.error('Missing palette in '+name) + self.error('Missing palette in ' + filename) f.close() info = {'w':w,'h':h,'cs':colspace,'bpc':bpc,'f':'FlateDecode','dp':dp,'pal':pal,'trns':trns,} if(ct>=4): # Extract alpha channel data = zlib.decompress(data) - color = ''; - alpha = ''; + color = b('') + alpha = b('') if(ct==4): # Gray image length = 2*w for i in range(h): pos = (1+length)*i - color += data[pos] - alpha += data[pos] + color += b(data[pos]) + alpha += b(data[pos]) line = substr(data, pos+1, length) - color += re.sub('(.).',lambda m: m.group(1),line, flags=re.DOTALL) - alpha += re.sub('.(.)',lambda m: m.group(1),line, flags=re.DOTALL) + re_c = re.compile('(.).'.encode("ascii"), flags=re.DOTALL) + re_a = re.compile('.(.)'.encode("ascii"), flags=re.DOTALL) + color += re_c.sub(lambda m: m.group(1), line) + alpha += re_a.sub(lambda m: m.group(1), line) else: # RGB image length = 4*w for i in range(h): pos = (1+length)*i - color += data[pos] - alpha += data[pos] + color += b(data[pos]) + alpha += b(data[pos]) line = substr(data, pos+1, length) - color += re.sub('(.{3}).',lambda m: m.group(1),line, flags=re.DOTALL) - alpha += re.sub('.{3}(.)',lambda m: m.group(1),line, flags=re.DOTALL) + re_c = re.compile('(...).'.encode("ascii"), flags=re.DOTALL) + re_a = re.compile('...(.)'.encode("ascii"), flags=re.DOTALL) + color += re_c.sub(lambda m: m.group(1), line) + alpha += re_a.sub(lambda m: m.group(1), line) del data data = zlib.compress(color) info['smask'] = zlib.compress(alpha) @@ -1817,11 +1964,19 @@ class FPDF(object): def _out(self, s): #Add a line to the document - if(self.state==2): - self.pages[self.page]+=s+"\n" + if PY3K and isinstance(s, bytes): + # manage binary data as latin1 until PEP461-like function is implemented + s = s.decode("latin1") + elif not PY3K and isinstance(s, unicode): + s = s.encode("latin1") # default encoding (font name and similar) + elif not isinstance(s, basestring): + s = str(s) + if(self.state == 2): + self.pages[self.page]["content"] += (s + "\n") else: - self.buffer+=str(s)+"\n" + self.buffer += (s + "\n") + @check_page def interleaved2of5(self, txt, x, y, w=1.0, h=10.0): "Barcode I2of5 (numeric), adds a 0 if odd lenght" narrow = w / 3.0 @@ -1841,7 +1996,7 @@ class FPDF(object): # add start and stop codes code = 'AA' + code.lower() + 'ZA' - for i in xrange(0, len(code), 2): + for i in range(0, len(code), 2): # choose next pair of digits char_bar = code[i] char_space = code[i+1] @@ -1853,10 +2008,10 @@ class FPDF(object): # create a wide/narrow-seq (first digit=bars, second digit=spaces) seq = '' - for s in xrange(0, len(bar_char[char_bar])): + for s in range(0, len(bar_char[char_bar])): seq += bar_char[char_bar][s] + bar_char[char_space][s] - for bar in xrange(0, len(seq)): + for bar in range(0, len(seq)): # set line_width depending on value if seq[bar] == 'n': line_width = narrow @@ -1870,51 +2025,35 @@ class FPDF(object): x += line_width + @check_page def code39(self, txt, x, y, w=1.5, h=5.0): - "Barcode 3of9" - wide = w - narrow = w / 3.0 - gap = narrow - - bar_char={'0': 'nnnwwnwnn', '1': 'wnnwnnnnw', '2': 'nnwwnnnnw', - '3': 'wnwwnnnnn', '4': 'nnnwwnnnw', '5': 'wnnwwnnnn', - '6': 'nnwwwnnnn', '7': 'nnnwnnwnw', '8': 'wnnwnnwnn', - '9': 'nnwwnnwnn', 'A': 'wnnnnwnnw', 'B': 'nnwnnwnnw', - 'C': 'wnwnnwnnn', 'D': 'nnnnwwnnw', 'E': 'wnnnwwnnn', - 'F': 'nnwnwwnnn', 'G': 'nnnnnwwnw', 'H': 'wnnnnwwnn', - 'I': 'nnwnnwwnn', 'J': 'nnnnwwwnn', 'K': 'wnnnnnnww', - 'L': 'nnwnnnnww', 'M': 'wnwnnnnwn', 'N': 'nnnnwnnww', - 'O': 'wnnnwnnwn', 'P': 'nnwnwnnwn', 'Q': 'nnnnnnwww', - 'R': 'wnnnnnwwn', 'S': 'nnwnnnwwn', 'T': 'nnnnwnwwn', - 'U': 'wwnnnnnnw', 'V': 'nwwnnnnnw', 'W': 'wwwnnnnnn', - 'X': 'nwnnwnnnw', 'Y': 'wwnnwnnnn', 'Z': 'nwwnwnnnn', - '-': 'nwnnnnwnw', '.': 'wwnnnnwnn', ' ': 'nwwnnnwnn', - '*': 'nwnnwnwnn', '$': 'nwnwnwnnn', '/': 'nwnwnnnwn', - '+': 'nwnnnwnwn', '%': 'nnnwnwnwn'} - + """Barcode 3of9""" + dim = {'w': w, 'n': w/3.} + chars = { + '0': 'nnnwwnwnn', '1': 'wnnwnnnnw', '2': 'nnwwnnnnw', + '3': 'wnwwnnnnn', '4': 'nnnwwnnnw', '5': 'wnnwwnnnn', + '6': 'nnwwwnnnn', '7': 'nnnwnnwnw', '8': 'wnnwnnwnn', + '9': 'nnwwnnwnn', 'A': 'wnnnnwnnw', 'B': 'nnwnnwnnw', + 'C': 'wnwnnwnnn', 'D': 'nnnnwwnnw', 'E': 'wnnnwwnnn', + 'F': 'nnwnwwnnn', 'G': 'nnnnnwwnw', 'H': 'wnnnnwwnn', + 'I': 'nnwnnwwnn', 'J': 'nnnnwwwnn', 'K': 'wnnnnnnww', + 'L': 'nnwnnnnww', 'M': 'wnwnnnnwn', 'N': 'nnnnwnnww', + 'O': 'wnnnwnnwn', 'P': 'nnwnwnnwn', 'Q': 'nnnnnnwww', + 'R': 'wnnnnnwwn', 'S': 'nnwnnnwwn', 'T': 'nnnnwnwwn', + 'U': 'wwnnnnnnw', 'V': 'nwwnnnnnw', 'W': 'wwwnnnnnn', + 'X': 'nwnnwnnnw', 'Y': 'wwnnwnnnn', 'Z': 'nwwnwnnnn', + '-': 'nwnnnnwnw', '.': 'wwnnnnwnn', ' ': 'nwwnnnwnn', + '*': 'nwnnwnwnn', '$': 'nwnwnwnnn', '/': 'nwnwnnnwn', + '+': 'nwnnnwnwn', '%': 'nnnwnwnwn', + } self.set_fill_color(0) - code = txt - - code = code.upper() - for i in xrange (0, len(code), 2): - char_bar = code[i] - - if not char_bar in bar_char.keys(): - raise RuntimeError ('Char "%s" invalid for Code39' % char_bar) - - seq= '' - for s in xrange(0, len(bar_char[char_bar])): - seq += bar_char[char_bar][s] - - for bar in xrange(0, len(seq)): - if seq[bar] == 'n': - line_width = narrow - else: - line_width = wide - - if bar % 2 == 0: - self.rect(x, y, line_width, h, 'F') - x += line_width - x += gap + for c in txt.upper(): + if c not in chars: + raise RuntimeError('Invalid char "%s" for Code39' % c) + for i, d in enumerate(chars[c]): + if i % 2 == 0: + self.rect(x, y, dim[d], h, 'F') + x += dim[d] + x += dim['n'] diff --git a/gluon/contrib/fpdf/html.py b/gluon/contrib/fpdf/html.py index 0863d5ed..60eb6b2c 100644 --- a/gluon/contrib/fpdf/html.py +++ b/gluon/contrib/fpdf/html.py @@ -8,8 +8,8 @@ __license__ = "LGPL 3.0" # Inspired by tuto5.py and several examples from fpdf.org, html2fpdf, etc. -from fpdf import FPDF -from HTMLParser import HTMLParser +from .fpdf import FPDF +from .py3k import PY3K, basestring, unicode, HTMLParser DEBUG = False @@ -78,8 +78,8 @@ class HTML2FPDF(HTMLParser): l = self.table_col_width[i:i+colspan] else: l = [self.td.get('width','240')] - w = sum([self.width2mm(lenght) for lenght in l]) - h = int(self.td.get('height', 0)) / 4 or self.h*1.30 + w = sum([self.width2mm(length) for length in l]) + h = int(self.td.get('height', 0)) // 4 or self.h*1.30 self.table_h = h border = int(self.table.get('border', 0)) if not self.th: @@ -99,30 +99,30 @@ class HTML2FPDF(HTMLParser): height = h + (self.tfooter and self.tfooter[0][0][1] or 0) if self.pdf.y+height>self.pdf.page_break_trigger and not self.th: self.output_table_footer() - self.pdf.add_page() + self.pdf.add_page(same = True) self.theader_out = self.tfooter_out = False if self.tfoot is None and self.thead is None: if not self.theader_out: self.output_table_header() self.box_shadow(w, h, bgcolor) - if DEBUG: print "td cell", self.pdf.x, w, txt, "*" + if DEBUG: print("td cell", self.pdf.x, w, txt, "*") self.pdf.cell(w,h,txt,border,0,align) elif self.table is not None: # ignore anything else than td inside a table pass elif self.align: - if DEBUG: print "cell", txt, "*" + if DEBUG: print("cell", txt, "*") self.pdf.cell(0,self.h,txt,0,1,self.align[0].upper(), self.href) else: txt = txt.replace("\n"," ") if self.href: self.put_link(self.href,txt) else: - if DEBUG: print "write", txt, "*" + if DEBUG: print("write", txt, "*") self.pdf.write(self.h,txt) def box_shadow(self, w, h, bgcolor): - if DEBUG: print "box_shadow", w, h, bgcolor + if DEBUG: print("box_shadow", w, h, bgcolor) if bgcolor: fill_color = self.pdf.fill_color self.pdf.set_fill_color(*bgcolor) @@ -168,7 +168,7 @@ class HTML2FPDF(HTMLParser): def handle_starttag(self, tag, attrs): attrs = dict(attrs) - if DEBUG: print "STARTTAG", tag, attrs + if DEBUG: print("STARTTAG", tag, attrs) if tag=='b' or tag=='i' or tag=='u': self.set_style(tag,1) if tag=='a': @@ -216,13 +216,16 @@ class HTML2FPDF(HTMLParser): # save previous font state: self.font_stack.append((self.font_face, self.font_size, self.color)) if 'color' in attrs: - self.color = hex2dec(attrs['color']) + color = hex2dec(attrs['color']) self.set_text_color(*color) self.color = color if 'face' in attrs: face = attrs.get('face').lower() - self.pdf.set_font(face) - self.font_face = face + try: + self.pdf.set_font(face) + self.font_face = face + except RuntimeError: + pass # font not found, ignore if 'size' in attrs: size = int(attrs.get('size')) self.pdf.set_font(self.font_face, size=int(size)) @@ -277,7 +280,7 @@ class HTML2FPDF(HTMLParser): def handle_endtag(self, tag): #Closing tag - if DEBUG: print "ENDTAG", tag + if DEBUG: print("ENDTAG", tag) if tag=='h1' or tag=='h2' or tag=='h3' or tag=='h4': self.pdf.ln(6) self.set_font() @@ -326,7 +329,7 @@ class HTML2FPDF(HTMLParser): self.tr = None if tag=='td' or tag=='th': if self.th: - if DEBUG: print "revert style" + if DEBUG: print("revert style") self.set_style('B', False) # revert style self.table_col_index += int(self.td.get('colspan','1')) self.td = None @@ -348,7 +351,7 @@ class HTML2FPDF(HTMLParser): if size: self.font_size = size self.h = size / 72.0*25.4 - if DEBUG: print "H", self.h + if DEBUG: print("H", self.h) self.pdf.set_font(self.font_face or 'times','',12) self.pdf.set_font_size(self.font_size or 12) self.set_style('u', False) @@ -365,7 +368,7 @@ class HTML2FPDF(HTMLParser): for s in ('b','i','u'): if self.style.get(s): style+=s - if DEBUG: print "SET_FONT_STYLE", style + if DEBUG: print("SET_FONT_STYLE", style) self.pdf.set_font('',style) def set_text_color(self, r=None, g=0, b=0): @@ -394,5 +397,6 @@ class HTMLMixin(object): def write_html(self, text, image_map=None): "Parse HTML and convert it to PDF" h2p = HTML2FPDF(self, image_map) + text = h2p.unescape(text) # To deal with HTML entities h2p.feed(text) diff --git a/gluon/contrib/fpdf/php.py b/gluon/contrib/fpdf/php.py index 00cbe4f0..33717892 100644 --- a/gluon/contrib/fpdf/php.py +++ b/gluon/contrib/fpdf/php.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: latin-1 -*- +from .py3k import PY3K, basestring, unicode + # fpdf php helpers: def substr(s, start, length=-1): @@ -14,16 +16,19 @@ def print_r(array): if not isinstance(array, dict): array = dict([(k, k) for k in array]) for k, v in array.items(): - print "[%s] => %s" % (k, v), + print("[%s] => %s " % (k, v)) def UTF8ToUTF16BE(instr, setbom=True): "Converts UTF-8 strings to UTF16-BE." - outstr = "" + outstr = "".encode() if (setbom): - outstr += "\xFE\xFF"; + outstr += "\xFE\xFF".encode("latin1") if not isinstance(instr, unicode): instr = instr.decode('UTF-8') outstr += instr.encode('UTF-16BE') + # convert bytes back to fake unicode string until PEP461-like is implemented + if PY3K: + outstr = outstr.decode("latin1") return outstr def UTF8StringToArray(instr): @@ -46,4 +51,4 @@ def str_pad(s, pad_length=0, pad_char= " ", pad_type= +1 ): else: # pad both return s.center(pad_length, pad_char) -strlen = count = lambda s: len(s) \ No newline at end of file +strlen = count = lambda s: len(s) diff --git a/gluon/contrib/fpdf/py3k.py b/gluon/contrib/fpdf/py3k.py new file mode 100644 index 00000000..86b16e3a --- /dev/null +++ b/gluon/contrib/fpdf/py3k.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"Special module to handle differences between Python 2 and 3 versions" + +import sys + +PY3K = sys.version_info >= (3, 0) + +try: + import cPickle as pickle +except ImportError: + import pickle + +try: + from urllib import urlopen +except ImportError: + from urllib.request import urlopen + +try: + from io import BytesIO +except ImportError: + try: + from cStringIO import StringIO as BytesIO + except ImportError: + from StringIO import StringIO as BytesIO + +try: + from hashlib import md5 +except ImportError: + try: + from md5 import md5 + except ImportError: + md5 = None +def hashpath(fn): + h = md5() + if PY3K: + h.update(fn.encode("UTF-8")) + else: + h.update(fn) + return h.hexdigest() + +# Check if PIL is available (tries importing both pypi version and corrected or manually installed versions). +# Necessary for JPEG and GIF support. +# TODO: Pillow support +try: + from PIL import Image +except ImportError: + try: + import Image + except ImportError: + Image = None + +try: + from HTMLParser import HTMLParser +except ImportError: + from html.parser import HTMLParser + +if PY3K: + basestring = str + unicode = str + ord = lambda x: x +else: + basestring = basestring + unicode = unicode + ord = ord + +# shortcut to bytes conversion (b prefix) +def b(s): + if isinstance(s, basestring): + return s.encode("latin1") + elif isinstance(s, int): + if PY3K: + return bytes([s]) # http://bugs.python.org/issue4588 + else: + return chr(s) + +def exception(): + "Return the current the exception instance currently being handled" + # this is needed to support Python 2.5 that lacks "as" syntax + return sys.exc_info()[1] + + diff --git a/gluon/contrib/fpdf/template.py b/gluon/contrib/fpdf/template.py index 916e8355..a004538e 100644 --- a/gluon/contrib/fpdf/template.py +++ b/gluon/contrib/fpdf/template.py @@ -2,12 +2,15 @@ "PDF Template Helper for FPDF.py" +from __future__ import with_statement + __author__ = "Mariano Reingart " __copyright__ = "Copyright (C) 2010 Mariano Reingart" __license__ = "LGPL 3.0" import sys,os,csv -from fpdf import FPDF +from .fpdf import FPDF +from .py3k import PY3K, basestring, unicode def rgb(col): return (col // 65536), (col // 256 % 256), (col% 256) @@ -16,11 +19,9 @@ class Template: def __init__(self, infile=None, elements=None, format='A4', orientation='portrait', title='', author='', subject='', creator='', keywords=''): if elements: - self.elements = elements - self.keys = [v['name'].lower() for v in self.elements] + self.load_elements(elements) self.handlers = {'T': self.text, 'L': self.line, 'I': self.image, - 'B': self.rect, 'BC': self.barcode, } - self.pg_no = 0 + 'B': self.rect, 'BC': self.barcode, 'W': self.write, } self.texts = {} pdf = self.pdf = FPDF(format=format,orientation=orientation, unit="mm") pdf.set_title(title) @@ -29,25 +30,37 @@ class Template: pdf.set_subject(subject) pdf.set_keywords(keywords) + def load_elements(self, elements): + "Initialize the internal element structures" + self.pg_no = 0 + self.elements = elements + self.keys = [v['name'].lower() for v in self.elements] + def parse_csv(self, infile, delimiter=",", decimal_sep="."): "Parse template format csv file and create elements dict" keys = ('name','type','x1','y1','x2','y2','font','size', 'bold','italic','underline','foreground','background', 'align','text','priority', 'multiline') self.elements = [] - for row in csv.reader(open(infile, 'rb'), delimiter=delimiter): - kargs = {} - for i,v in enumerate(row): - if not v.startswith("'") and decimal_sep!=".": - v = v.replace(decimal_sep,".") - else: - v = v - if v=='': - v = None - else: - v = eval(v.strip()) - kargs[keys[i]] = v - self.elements.append(kargs) + self.pg_no = 0 + if not PY3K: + f = open(infile, 'rb') + else: + f = open(infile) + with f: + for row in csv.reader(f, delimiter=delimiter): + kargs = {} + for i,v in enumerate(row): + if not v.startswith("'") and decimal_sep!=".": + v = v.replace(decimal_sep,".") + else: + v = v + if v=='': + v = None + else: + v = eval(v.strip()) + kargs[keys[i]] = v + self.elements.append(kargs) self.keys = [v['name'].lower() for v in self.elements] def add_page(self): @@ -55,8 +68,8 @@ class Template: self.texts[self.pg_no] = {} def __setitem__(self, name, value): - if self.has_key(name): - if isinstance(value,unicode): + if name.lower() in self.keys: + if not PY3K and isinstance(value, unicode): value = value.encode("latin1","ignore") elif value is None: value = "" @@ -71,7 +84,7 @@ class Template: return name.lower() in self.keys def __getitem__(self, name): - if self.has_key(name): + if name in self.keys: key = name.lower() if key in self.texts: # text for this page: @@ -94,7 +107,7 @@ class Template: if element['underline']: style += "U" pdf.set_font(element['font'],style,element['size']) align = {'L':'L','R':'R','I':'L','D':'R','C':'C','':''}.get(element['align']) # D/I in spanish - if isinstance(text, unicode): + if isinstance(text, unicode) and not PY3K: text = text.encode("latin1","ignore") else: text = str(text) @@ -110,16 +123,17 @@ class Template: pdf.set_auto_page_break(False,margin=0) for element in sorted(self.elements,key=lambda x: x['priority']): - # make a copy of the element: - element = dict(element) + #print "dib",element['type'], element['name'], element['x1'], element['y1'], element['x2'], element['y2'] + element = element.copy() element['text'] = self.texts[pg].get(element['name'].lower(), element['text']) if 'rotate' in element: pdf.rotate(element['rotate'], element['x1'], element['y1']) self.handlers[element['type'].upper()](pdf, **element) if 'rotate' in element: pdf.rotate(0) - - return pdf.output(outfile, dest) + + if dest: + return pdf.output(outfile, dest) def text(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', font="arial", size=10, bold=False, italic=False, underline=False, align="", @@ -157,7 +171,7 @@ class Template: # multiline==False: trim to fit exactly the space defined text = pdf.multi_cell(w=x2-x1, h=y2-y1, txt=text, align=align, split_only=True)[0] - print "trimming: *%s*" % text + print("trimming: *%s*" % text) pdf.cell(w=x2-x1,h=y2-y1,txt=text,border=0,ln=0,align=align) #pdf.Text(x=x1,y=y1,txt=text) @@ -179,7 +193,8 @@ class Template: pdf.rect(x1, y1, x2-x1, y2-y1) def image(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', *args,**kwargs): - pdf.image(text,x1,y1,w=x2-x1,h=y2-y1,type='',link='') + if text: + pdf.image(text,x1,y1,w=x2-x1,h=y2-y1,type='',link='') def barcode(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', font="arial", size=1, foreground=0, *args, **kwargs): @@ -189,113 +204,26 @@ class Template: if font == 'interleaved 2of5 nt': pdf.interleaved2of5(text,x1,y1,w=size,h=y2-y1) - -if __name__ == "__main__": - - # generate sample invoice (according Argentina's regulations) - - import random - from decimal import Decimal - - f = Template(format="A4", - title="Sample Invoice", author="Sample Company", - subject="Sample Customer", keywords="Electronic TAX Invoice") - f.parse_csv(infile="invoice.csv", delimiter=";", decimal_sep=",") - - detail = "Lorem ipsum dolor sit amet, consectetur. " * 30 - items = [] - for i in range(1, 30): - ds = "Sample product %s" % i - qty = random.randint(1,10) - price = round(random.random()*100,3) - code = "%s%s%02d" % (chr(random.randint(65,90)), chr(random.randint(65,90)),i) - items.append(dict(code=code, unit='u', - qty=qty, price=price, - amount=qty*price, - ds="%s: %s" % (i,ds))) - - # divide and count lines - lines = 0 - li_items = [] - for it in items: - qty = it['qty'] - code = it['code'] - unit = it['unit'] - for ds in f.split_multicell(it['ds'], 'item_description01'): - # add item description line (without price nor amount) - li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None)) - # clean qty and code (show only at first) - unit = qty = code = None - # set last item line price and amount - li_items[-1].update(amount = it['amount'], - price = it['price']) - - obs="\nDetail:\n\n" + detail - for ds in f.split_multicell(obs, 'item_description01'): - li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None)) - - # calculate pages: - lines = len(li_items) - max_lines_per_page = 24 - pages = lines / (max_lines_per_page - 1) - if lines % (max_lines_per_page - 1): pages = pages + 1 - - # completo campos y hojas - for page in range(1, pages+1): - f.add_page() - f['page'] = 'Page %s of %s' % (page, pages) - if pages>1 and page page * (max_lines_per_page - 1): - break - if it['amount']: - total += Decimal("%.6f" % it['amount']) - if k > (page - 1) * (max_lines_per_page - 1): - li += 1 - if it['qty'] is not None: - f['item_quantity%02d' % li] = it['qty'] - if it['code'] is not None: - f['item_code%02d' % li] = it['code'] - if it['unit'] is not None: - f['item_unit%02d' % li] = it['unit'] - f['item_description%02d' % li] = it['ds'] - if it['price'] is not None: - f['item_price%02d' % li] = "%0.3f" % it['price'] - if it['amount'] is not None: - f['item_amount%02d' % li] = "%0.2f" % it['amount'] - - if pages == page: - f['net'] = "%0.2f" % (total/Decimal("1.21")) - f['vat'] = "%0.2f" % (total*(1-1/Decimal("1.21"))) - f['total_label'] = 'Total:' - else: - f['total_label'] = 'SubTotal:' - f['total'] = "%0.2f" % total - - f.render("./invoice.pdf") - if sys.platform.startswith("linux"): - os.system("evince ./invoice.pdf") - else: - os.system("./invoice.pdf") + # Added by Derek Schwalenberg Schwalenberg1013@gmail.com to allow (url) links in templates (using write method) 2014-02-22 + def write(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', font="arial", size=1, + bold=False, italic=False, underline=False, align="", link='http://example.com', + foreground=0, *args, **kwargs): + if pdf.text_color!=rgb(foreground): + pdf.set_text_color(*rgb(foreground)) + font = font.strip().lower() + if font == 'arial black': + font = 'arial' + style = "" + for tag in 'B', 'I', 'U': + if (text.startswith("<%s>" % tag) and text.endswith("" %tag)): + text = text[3:-4] + style += tag + if bold: style += "B" + if italic: style += "I" + if underline: style += "U" + align = {'L':'L','R':'R','I':'L','D':'R','C':'C','':''}.get(align) # D/I in spanish + pdf.set_font(font,style,size) + ##m_k = 72 / 2.54 + ##h = (size/m_k) + pdf.set_xy(x1,y1) + pdf.write(5,text,link) diff --git a/gluon/contrib/fpdf/ttfonts.py b/gluon/contrib/fpdf/ttfonts.py index d9bc5784..3550727d 100644 --- a/gluon/contrib/fpdf/ttfonts.py +++ b/gluon/contrib/fpdf/ttfonts.py @@ -16,10 +16,13 @@ # #****************************************************************************** +from __future__ import with_statement + from struct import pack, unpack, unpack_from import re import warnings -from php import die, substr, str_repeat, str_pad, strlen, count +from .php import die, substr, str_repeat, str_pad, strlen, count +from .py3k import b, ord # Define the value used in the "head" table of a created TTF file @@ -55,7 +58,7 @@ def sub32(x, y): def calcChecksum(data): if (strlen(data) % 4): - data += str_repeat("\0", (4-(len(data) % 4))) + data += str_repeat(b("\0"), (4-(len(data) % 4))) hi=0x0000 lo=0x0000 for i in range(0, len(data), 4): @@ -74,26 +77,25 @@ class TTFontFile: def getMetrics(self, file): self.filename = file - self.fh = open(file,'rb') - self._pos = 0 - self.charWidths = [] - self.glyphPos = {} - self.charToGlyph = {} - self.tables = {} - self.otables = {} - self.ascent = 0 - self.descent = 0 - self.TTCFonts = {} - self.version = version = self.read_ulong() - if (version==0x4F54544F): - die("Postscript outlines are not supported") - if (version==0x74746366): - die("ERROR - TrueType Fonts Collections not supported") - if (version not in (0x00010000,0x74727565)): - die("Not a TrueType font: version=" + version) - self.readTableDirectory() - self.extractInfo() - self.fh.close() + with open(file,'rb') as self.fh: + self._pos = 0 + self.charWidths = [] + self.glyphPos = {} + self.charToGlyph = {} + self.tables = {} + self.otables = {} + self.ascent = 0 + self.descent = 0 + self.TTCFonts = {} + self.version = version = self.read_ulong() + if (version==0x4F54544F): + die("Postscript outlines are not supported") + if (version==0x74746366): + die("ERROR - TrueType Fonts Collections not supported") + if (version not in (0x00010000,0x74727565)): + die("Not a TrueType font: version=" + str(version)) + self.readTableDirectory() + self.extractInfo() def readTableDirectory(self, ): self.numTables = self.read_ushort() @@ -130,7 +132,7 @@ class TTFontFile: def read_tag(self): self._pos += 4 - return self.fh.read(4) + return self.fh.read(4).decode("latin1") def read_short(self): self._pos += 2 @@ -204,7 +206,7 @@ class TTFontFile: def add(self, tag, data): if (tag == 'head') : - data = self.splice(data, 8, "\0\0\0\0") + data = self.splice(data, 8, b("\0\0\0\0")) self.otables[tag] = data ############################################/ @@ -226,7 +228,7 @@ class TTFontFile: numRecords = self.read_ushort() string_data_offset = name_offset + self.read_ushort() names = {1:'',2:'',3:'',4:'',6:''} - K = names.keys() + K = list(names.keys()) nameCount = len(names) for i in range(numRecords): platformId = self.read_ushort() @@ -242,7 +244,7 @@ class TTFontFile: self.seek(string_data_offset + offset) if (length % 2 != 0): die("PostScript name is UTF-16BE string of odd length") - length /= 2 + length //= 2 N = '' while (length > 0): char = self.read_ushort() @@ -253,7 +255,7 @@ class TTFontFile: elif (platformId == 1 and encodingId == 0 and languageId == 0): # Macintosh, Roman, English, PS Name opos = self._pos - N = self.get_chunk(string_data_offset + offset, length) + N = self.get_chunk(string_data_offset + offset, length).decode("latin1") self._pos = opos self.seek(opos) @@ -455,346 +457,344 @@ class TTFontFile: def makeSubset(self, file, subset): self.filename = file - self.fh = open(file ,'rb') - self._pos = 0 - self.charWidths = [] - self.glyphPos = {} - self.charToGlyph = {} - self.tables = {} - self.otables = {} - self.ascent = 0 - self.descent = 0 - self.skip(4) - self.maxUni = 0 - self.readTableDirectory() + with open(file ,'rb') as self.fh: + self._pos = 0 + self.charWidths = [] + self.glyphPos = {} + self.charToGlyph = {} + self.tables = {} + self.otables = {} + self.ascent = 0 + self.descent = 0 + self.skip(4) + self.maxUni = 0 + self.readTableDirectory() - #################/ - # head - Font header table - #################/ - self.seek_table("head") - self.skip(50) - indexToLocFormat = self.read_ushort() - glyphDataFormat = self.read_ushort() + #################/ + # head - Font header table + #################/ + self.seek_table("head") + self.skip(50) + indexToLocFormat = self.read_ushort() + glyphDataFormat = self.read_ushort() - #################/ - # hhea - Horizontal header table - #################/ - self.seek_table("hhea") - self.skip(32) - metricDataFormat = self.read_ushort() - orignHmetrics = numberOfHMetrics = self.read_ushort() + #################/ + # hhea - Horizontal header table + #################/ + self.seek_table("hhea") + self.skip(32) + metricDataFormat = self.read_ushort() + orignHmetrics = numberOfHMetrics = self.read_ushort() - #################/ - # maxp - Maximum profile table - #################/ - self.seek_table("maxp") - self.skip(4) - numGlyphs = self.read_ushort() + #################/ + # maxp - Maximum profile table + #################/ + self.seek_table("maxp") + self.skip(4) + numGlyphs = self.read_ushort() - #################/ - # cmap - Character to glyph index mapping table - #################/ - cmap_offset = self.seek_table("cmap") - self.skip(2) - cmapTableCount = self.read_ushort() - unicode_cmap_offset = 0 - unicode_cmap_offset12 = 0 - for i in range(cmapTableCount): - platformID = self.read_ushort() - encodingID = self.read_ushort() - offset = self.read_ulong() - save_pos = self._pos - if platformID == 3 and encodingID == 10: # Microsoft, UCS-4 - format = self.get_ushort(cmap_offset + offset) - if (format == 12): - if not unicode_cmap_offset12: - unicode_cmap_offset12 = cmap_offset + offset - break - if ((platformID == 3 and encodingID == 1) or platformID == 0): # Microsoft, Unicode - format = self.get_ushort(cmap_offset + offset) - if (format == 4): - unicode_cmap_offset = cmap_offset + offset - break + #################/ + # cmap - Character to glyph index mapping table + #################/ + cmap_offset = self.seek_table("cmap") + self.skip(2) + cmapTableCount = self.read_ushort() + unicode_cmap_offset = 0 + unicode_cmap_offset12 = 0 + for i in range(cmapTableCount): + platformID = self.read_ushort() + encodingID = self.read_ushort() + offset = self.read_ulong() + save_pos = self._pos + if platformID == 3 and encodingID == 10: # Microsoft, UCS-4 + format = self.get_ushort(cmap_offset + offset) + if (format == 12): + if not unicode_cmap_offset12: + unicode_cmap_offset12 = cmap_offset + offset + break + if ((platformID == 3 and encodingID == 1) or platformID == 0): # Microsoft, Unicode + format = self.get_ushort(cmap_offset + offset) + if (format == 4): + unicode_cmap_offset = cmap_offset + offset + break - self.seek(save_pos ) - - if not unicode_cmap_offset and not unicode_cmap_offset12: - die('Font (' + self.filename + ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 3, encoding 10, format 12, or platform 0, any encoding, format 4)') + self.seek(save_pos ) + + if not unicode_cmap_offset and not unicode_cmap_offset12: + die('Font (' + self.filename + ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 3, encoding 10, format 12, or platform 0, any encoding, format 4)') - glyphToChar = {} - charToGlyph = {} - if unicode_cmap_offset12: - self.getCMAP12(unicode_cmap_offset12, glyphToChar, charToGlyph) - else: - self.getCMAP4(unicode_cmap_offset, glyphToChar, charToGlyph) + glyphToChar = {} + charToGlyph = {} + if unicode_cmap_offset12: + self.getCMAP12(unicode_cmap_offset12, glyphToChar, charToGlyph) + else: + self.getCMAP4(unicode_cmap_offset, glyphToChar, charToGlyph) - self.charToGlyph = charToGlyph + self.charToGlyph = charToGlyph - #################/ - # hmtx - Horizontal metrics table - #################/ - scale = 1 # not used - self.getHMTX(numberOfHMetrics, numGlyphs, glyphToChar, scale) + #################/ + # hmtx - Horizontal metrics table + #################/ + scale = 1 # not used + self.getHMTX(numberOfHMetrics, numGlyphs, glyphToChar, scale) - #################/ - # loca - Index to location - #################/ - self.getLOCA(indexToLocFormat, numGlyphs) + #################/ + # loca - Index to location + #################/ + self.getLOCA(indexToLocFormat, numGlyphs) - subsetglyphs = [(0, 0)] # special "sorted dict"! - subsetCharToGlyph = {} - for code in subset: - if (code in self.charToGlyph): - if (self.charToGlyph[code], code) not in subsetglyphs: - subsetglyphs.append((self.charToGlyph[code], code)) # Old Glyph ID => Unicode - subsetCharToGlyph[code] = self.charToGlyph[code] # Unicode to old GlyphID - self.maxUni = max(self.maxUni, code) - (start,dummy) = self.get_table_pos('glyf') + subsetglyphs = [(0, 0)] # special "sorted dict"! + subsetCharToGlyph = {} + for code in subset: + if (code in self.charToGlyph): + if (self.charToGlyph[code], code) not in subsetglyphs: + subsetglyphs.append((self.charToGlyph[code], code)) # Old Glyph ID => Unicode + subsetCharToGlyph[code] = self.charToGlyph[code] # Unicode to old GlyphID + self.maxUni = max(self.maxUni, code) + (start,dummy) = self.get_table_pos('glyf') - subsetglyphs.sort() - glyphSet = {} - n = 0 - fsLastCharIndex = 0 # maximum Unicode index (character code) in this font, according to the cmap subtable for platform ID 3 and platform- specific encoding ID 0 or 1. - for originalGlyphIdx, uni in subsetglyphs: - fsLastCharIndex = max(fsLastCharIndex , uni) - glyphSet[originalGlyphIdx] = n # old glyphID to new glyphID - n += 1 + subsetglyphs.sort() + glyphSet = {} + n = 0 + fsLastCharIndex = 0 # maximum Unicode index (character code) in this font, according to the cmap subtable for platform ID 3 and platform- specific encoding ID 0 or 1. + for originalGlyphIdx, uni in subsetglyphs: + fsLastCharIndex = max(fsLastCharIndex , uni) + glyphSet[originalGlyphIdx] = n # old glyphID to new glyphID + n += 1 - codeToGlyph = {} - for uni, originalGlyphIdx in sorted(subsetCharToGlyph.items()): - codeToGlyph[uni] = glyphSet[originalGlyphIdx] - - self.codeToGlyph = codeToGlyph - - for originalGlyphIdx, uni in subsetglyphs: - nonlocals = {'start': start, 'glyphSet': glyphSet, - 'subsetglyphs': subsetglyphs} - self.getGlyphs(originalGlyphIdx, nonlocals) + codeToGlyph = {} + for uni, originalGlyphIdx in sorted(subsetCharToGlyph.items()): + codeToGlyph[uni] = glyphSet[originalGlyphIdx] + + self.codeToGlyph = codeToGlyph + + for originalGlyphIdx, uni in subsetglyphs: + nonlocals = {'start': start, 'glyphSet': glyphSet, + 'subsetglyphs': subsetglyphs} + self.getGlyphs(originalGlyphIdx, nonlocals) - numGlyphs = numberOfHMetrics = len(subsetglyphs) + numGlyphs = numberOfHMetrics = len(subsetglyphs) - #tables copied from the original - tags = ['name'] - for tag in tags: - self.add(tag, self.get_table(tag)) - tags = ['cvt ', 'fpgm', 'prep', 'gasp'] - for tag in tags: - if (tag in self.tables): - self.add(tag, self.get_table(tag)) + #tables copied from the original + tags = ['name'] + for tag in tags: + self.add(tag, self.get_table(tag)) + tags = ['cvt ', 'fpgm', 'prep', 'gasp'] + for tag in tags: + if (tag in self.tables): + self.add(tag, self.get_table(tag)) - # post - PostScript - opost = self.get_table('post') - post = "\x00\x03\x00\x00" + substr(opost,4,12) + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - self.add('post', post) + # post - PostScript + opost = self.get_table('post') + post = b("\x00\x03\x00\x00") + substr(opost,4,12) + b("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + self.add('post', post) - # Sort CID2GID map into segments of contiguous codes - if 0 in codeToGlyph: - del codeToGlyph[0] - #unset(codeToGlyph[65535]) - rangeid = 0 - range_ = {} - prevcid = -2 - prevglidx = -1 - # for each character - for cid, glidx in sorted(codeToGlyph.items()): - if (cid == (prevcid + 1) and glidx == (prevglidx + 1)): - range_[rangeid].append(glidx) - else: - # new range - rangeid = cid - range_[rangeid] = [] - range_[rangeid].append(glidx) - prevcid = cid - prevglidx = glidx + # Sort CID2GID map into segments of contiguous codes + if 0 in codeToGlyph: + del codeToGlyph[0] + #unset(codeToGlyph[65535]) + rangeid = 0 + range_ = {} + prevcid = -2 + prevglidx = -1 + # for each character + for cid, glidx in sorted(codeToGlyph.items()): + if (cid == (prevcid + 1) and glidx == (prevglidx + 1)): + range_[rangeid].append(glidx) + else: + # new range + rangeid = cid + range_[rangeid] = [] + range_[rangeid].append(glidx) + prevcid = cid + prevglidx = glidx - # cmap - Character to glyph mapping - Format 4 (MS / ) - segCount = len(range_) + 1 # + 1 Last segment has missing character 0xFFFF - searchRange = 1 - entrySelector = 0 - while (searchRange * 2 <= segCount ): + # cmap - Character to glyph mapping - Format 4 (MS / ) + segCount = len(range_) + 1 # + 1 Last segment has missing character 0xFFFF + searchRange = 1 + entrySelector = 0 + while (searchRange * 2 <= segCount ): + searchRange = searchRange * 2 + entrySelector = entrySelector + 1 + searchRange = searchRange * 2 - entrySelector = entrySelector + 1 - - searchRange = searchRange * 2 - rangeShift = segCount * 2 - searchRange - length = 16 + (8*segCount ) + (numGlyphs+1) - cmap = [0, 1, # Index : version, number of encoding subtables - 3, 1, # Encoding Subtable : platform (MS=3), encoding (Unicode) - 0, 12, # Encoding Subtable : offset (hi,lo) - 4, length, 0, # Format 4 Mapping subtable: format, length, language - segCount*2, - searchRange, - entrySelector, - rangeShift] + rangeShift = segCount * 2 - searchRange + length = 16 + (8*segCount ) + (numGlyphs+1) + cmap = [0, 1, # Index : version, number of encoding subtables + 3, 1, # Encoding Subtable : platform (MS=3), encoding (Unicode) + 0, 12, # Encoding Subtable : offset (hi,lo) + 4, length, 0, # Format 4 Mapping subtable: format, length, language + segCount*2, + searchRange, + entrySelector, + rangeShift] - range_ = sorted(range_.items()) - - # endCode(s) - for start, subrange in range_: - endCode = start + (len(subrange)-1) - cmap.append(endCode) # endCode(s) - - cmap.append(0xFFFF) # endCode of last Segment - cmap.append(0) # reservedPad + range_ = sorted(range_.items()) + + # endCode(s) + for start, subrange in range_: + endCode = start + (len(subrange)-1) + cmap.append(endCode) # endCode(s) + + cmap.append(0xFFFF) # endCode of last Segment + cmap.append(0) # reservedPad - # startCode(s) - for start, subrange in range_: - cmap.append(start) # startCode(s) - - cmap.append(0xFFFF) # startCode of last Segment - # idDelta(s) - for start, subrange in range_: - idDelta = -(start-subrange[0]) - n += count(subrange) - cmap.append(idDelta) # idDelta(s) - - cmap.append(1) # idDelta of last Segment - # idRangeOffset(s) - for subrange in range_: - cmap.append(0) # idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 - - cmap.append(0) # idRangeOffset of last Segment - for subrange, glidx in range_: - cmap.extend(glidx) - - cmap.append(0) # Mapping for last character - cmapstr = '' - for cm in cmap: - if cm >= 0: - cmapstr += pack(">H", cm) - else: + # startCode(s) + for start, subrange in range_: + cmap.append(start) # startCode(s) + + cmap.append(0xFFFF) # startCode of last Segment + # idDelta(s) + for start, subrange in range_: + idDelta = -(start-subrange[0]) + n += count(subrange) + cmap.append(idDelta) # idDelta(s) + + cmap.append(1) # idDelta of last Segment + # idRangeOffset(s) + for subrange in range_: + cmap.append(0) # idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 + + cmap.append(0) # idRangeOffset of last Segment + for subrange, glidx in range_: + cmap.extend(glidx) + + cmap.append(0) # Mapping for last character + cmapstr = b('') + for cm in cmap: + if cm >= 0: + cmapstr += pack(">H", cm) + else: + try: + cmapstr += pack(">h", cm) + except: + warnings.warn("cmap value too big/small: %s" % cm) + cmapstr += pack(">H", -cm) + self.add('cmap', cmapstr) + + # glyf - Glyph data + (glyfOffset,glyfLength) = self.get_table_pos('glyf') + if (glyfLength < self.maxStrLenRead): + glyphData = self.get_table('glyf') + + offsets = [] + glyf = b('') + pos = 0 + + hmtxstr = b('') + xMinT = 0 + yMinT = 0 + xMaxT = 0 + yMaxT = 0 + advanceWidthMax = 0 + minLeftSideBearing = 0 + minRightSideBearing = 0 + xMaxExtent = 0 + maxPoints = 0 # points in non-compound glyph + maxContours = 0 # contours in non-compound glyph + maxComponentPoints = 0 # points in compound glyph + maxComponentContours = 0 # contours in compound glyph + maxComponentElements = 0 # number of glyphs referenced at top level + maxComponentDepth = 0 # levels of recursion, set to 0 if font has only simple glyphs + self.glyphdata = {} + + for originalGlyphIdx, uni in subsetglyphs: + # hmtx - Horizontal Metrics + hm = self.getHMetric(orignHmetrics, originalGlyphIdx) + hmtxstr += hm + + offsets.append(pos) try: - cmapstr += pack(">h", cm) - except: - warnings.warn("cmap value too big/small: %s" % cm) - cmapstr += pack(">H", -cm) - self.add('cmap', cmapstr) + glyphPos = self.glyphPos[originalGlyphIdx] + glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos + except IndexError: + warnings.warn("missing glyph %s" % (originalGlyphIdx)) + glyphLen = 0 - # glyf - Glyph data - (glyfOffset,glyfLength) = self.get_table_pos('glyf') - if (glyfLength < self.maxStrLenRead): - glyphData = self.get_table('glyf') - - offsets = [] - glyf = '' - pos = 0 - - hmtxstr = '' - xMinT = 0 - yMinT = 0 - xMaxT = 0 - yMaxT = 0 - advanceWidthMax = 0 - minLeftSideBearing = 0 - minRightSideBearing = 0 - xMaxExtent = 0 - maxPoints = 0 # points in non-compound glyph - maxContours = 0 # contours in non-compound glyph - maxComponentPoints = 0 # points in compound glyph - maxComponentContours = 0 # contours in compound glyph - maxComponentElements = 0 # number of glyphs referenced at top level - maxComponentDepth = 0 # levels of recursion, set to 0 if font has only simple glyphs - self.glyphdata = {} - - for originalGlyphIdx, uni in subsetglyphs: - # hmtx - Horizontal Metrics - hm = self.getHMetric(orignHmetrics, originalGlyphIdx) - hmtxstr += hm + if (glyfLength < self.maxStrLenRead): + data = substr(glyphData,glyphPos,glyphLen) + else: + if (glyphLen > 0): + data = self.get_chunk(glyfOffset+glyphPos,glyphLen) + else: + data = b('') + + if (glyphLen > 0): + up = unpack(">H", substr(data,0,2))[0] + if (glyphLen > 2 and (up & (1 << 15)) ): # If number of contours <= -1 i.e. composite glyph + pos_in_glyph = 10 + flags = GF_MORE + nComponentElements = 0 + while (flags & GF_MORE): + nComponentElements += 1 # number of glyphs referenced at top level + up = unpack(">H", substr(data,pos_in_glyph,2)) + flags = up[0] + up = unpack(">H", substr(data,pos_in_glyph+2,2)) + glyphIdx = up[0] + self.glyphdata.setdefault(originalGlyphIdx, {}).setdefault('compGlyphs', []).append(glyphIdx) + try: + data = self._set_ushort(data, pos_in_glyph + 2, glyphSet[glyphIdx]) + except KeyError: + data = 0 + warnings.warn("missing glyph data %s" % glyphIdx) + pos_in_glyph += 4 + if (flags & GF_WORDS): + pos_in_glyph += 4 + else: + pos_in_glyph += 2 + if (flags & GF_SCALE): + pos_in_glyph += 2 + elif (flags & GF_XYSCALE): + pos_in_glyph += 4 + elif (flags & GF_TWOBYTWO): + pos_in_glyph += 8 + + maxComponentElements = max(maxComponentElements, nComponentElements) + + glyf += data + pos += glyphLen + if (pos % 4 != 0): + padding = 4 - (pos % 4) + glyf += str_repeat(b("\0"),padding) + pos += padding offsets.append(pos) - try: - glyphPos = self.glyphPos[originalGlyphIdx] - glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos - except IndexError: - warnings.warn("missing glyph %s" % (originalGlyphIdx)) - glyphLen = 0 + self.add('glyf', glyf) - if (glyfLength < self.maxStrLenRead): - data = substr(glyphData,glyphPos,glyphLen) + # hmtx - Horizontal Metrics + self.add('hmtx', hmtxstr) + + # loca - Index to location + locastr = b('') + if (((pos + 1) >> 1) > 0xFFFF): + indexToLocFormat = 1 # long format + for offset in offsets: + locastr += pack(">L",offset) else: - if (glyphLen > 0): - data = self.get_chunk(glyfOffset+glyphPos,glyphLen) - else: - data = '' + indexToLocFormat = 0 # short format + for offset in offsets: + locastr += pack(">H",offset//2) - if (glyphLen > 0): - up = unpack(">H", substr(data,0,2))[0] - if (glyphLen > 2 and (up & (1 << 15)) ): # If number of contours <= -1 i.e. composiste glyph - pos_in_glyph = 10 - flags = GF_MORE - nComponentElements = 0 - while (flags & GF_MORE): - nComponentElements += 1 # number of glyphs referenced at top level - up = unpack(">H", substr(data,pos_in_glyph,2)) - flags = up[0] - up = unpack(">H", substr(data,pos_in_glyph+2,2)) - glyphIdx = up[0] - self.glyphdata.setdefault(originalGlyphIdx, {}).setdefault('compGlyphs', []).append(glyphIdx) - try: - data = self._set_ushort(data, pos_in_glyph + 2, glyphSet[glyphIdx]) - except KeyError: - data = 0 - warnings.warn("missing glyph data %s" % glyphIdx) - pos_in_glyph += 4 - if (flags & GF_WORDS): - pos_in_glyph += 4 - else: - pos_in_glyph += 2 - if (flags & GF_SCALE): - pos_in_glyph += 2 - elif (flags & GF_XYSCALE): - pos_in_glyph += 4 - elif (flags & GF_TWOBYTWO): - pos_in_glyph += 8 - - maxComponentElements = max(maxComponentElements, nComponentElements) - - glyf += data - pos += glyphLen - if (pos % 4 != 0): - padding = 4 - (pos % 4) - glyf += str_repeat("\0",padding) - pos += padding + self.add('loca', locastr) - offsets.append(pos) - self.add('glyf', glyf) + # head - Font header + head = self.get_table('head') + head = self._set_ushort(head, 50, indexToLocFormat) + self.add('head', head) - # hmtx - Horizontal Metrics - self.add('hmtx', hmtxstr) + # hhea - Horizontal Header + hhea = self.get_table('hhea') + hhea = self._set_ushort(hhea, 34, numberOfHMetrics) + self.add('hhea', hhea) - # loca - Index to location - locastr = '' - if (((pos + 1) >> 1) > 0xFFFF): - indexToLocFormat = 1 # long format - for offset in offsets: - locastr += pack(">L",offset) - else: - indexToLocFormat = 0 # short format - for offset in offsets: - locastr += pack(">H",(offset/2)) - - self.add('loca', locastr) + # maxp - Maximum Profile + maxp = self.get_table('maxp') + maxp = self._set_ushort(maxp, 4, numGlyphs) + self.add('maxp', maxp) - # head - Font header - head = self.get_table('head') - head = self._set_ushort(head, 50, indexToLocFormat) - self.add('head', head) - - # hhea - Horizontal Header - hhea = self.get_table('hhea') - hhea = self._set_ushort(hhea, 34, numberOfHMetrics) - self.add('hhea', hhea) - - # maxp - Maximum Profile - maxp = self.get_table('maxp') - maxp = self._set_ushort(maxp, 4, numGlyphs) - self.add('maxp', maxp) - - # OS/2 - OS/2 - os2 = self.get_table('OS/2') - self.add('OS/2', os2 ) - - self.fh.close() + # OS/2 - OS/2 + os2 = self.get_table('OS/2') + self.add('OS/2', os2 ) # Put the TTF file together stm = self.endTTFile('') @@ -864,11 +864,16 @@ class TTFontFile: def getHMTX(self, numberOfHMetrics, numGlyphs, glyphToChar, scale): start = self.seek_table("hmtx") aw = 0 - self.charWidths = [0] * 256*256*2 + self.charWidths = [] + def resize_cw(size, default): + size = (((size + 1) // 1024) + 1) * 1024 + delta = size - len(self.charWidths) + if delta > 0: + self.charWidths += [default] * delta nCharWidths = 0 if ((numberOfHMetrics*4) < self.maxStrLenRead): data = self.get_chunk(start,(numberOfHMetrics*4)) - arr = unpack(">" + "H" * (len(data)/2), data) + arr = unpack(">%dH" % (len(data)//2), data) else: self.seek(start) for glyph in range(numberOfHMetrics): @@ -886,26 +891,30 @@ class TTFontFile: self.defaultWidth = scale*aw continue - for char in glyphToChar[glyph]: + for char in glyphToChar[glyph]: if (char != 0 and char != 65535): - w = int(round(scale*aw)) + w = int(round(scale*aw+0.001)) # ROUND_HALF_UP in PY3K (like php) if (w == 0): w = 65535 - if (char < 196608): + if (char < 196608): + if char >= len(self.charWidths): + resize_cw(char, self.defaultWidth) self.charWidths[char] = w nCharWidths += 1 data = self.get_chunk((start+numberOfHMetrics*4),(numGlyphs*2)) - arr = unpack(">" + "H" * (len(data)/2), data) + arr = unpack(">%dH" % (len(data)//2), data) diff = numGlyphs-numberOfHMetrics for pos in range(diff): glyph = pos + numberOfHMetrics if (glyph in glyphToChar): for char in glyphToChar[glyph]: if (char != 0 and char != 65535): - w = int(round(scale*aw)) + w = int(round(scale*aw+0.001)) # ROUND_HALF_UP in PY3K (like php) if (w == 0): w = 65535 if (char < 196608): + if char >= len(self.charWidths): + resize_cw(char, self.defaultWidth) self.charWidths[char] = w nCharWidths += 1 @@ -933,12 +942,12 @@ class TTFontFile: self.glyphPos = [] if (indexToLocFormat == 0): data = self.get_chunk(start,(numGlyphs*2)+2) - arr = unpack(">" + "H" * (len(data)/2), data) + arr = unpack(">%dH" % (len(data)//2), data) for n in range(numGlyphs): self.glyphPos.append((arr[n] * 2)) # n+1 !? elif (indexToLocFormat == 1): data = self.get_chunk(start,(numGlyphs*4)+4) - arr = unpack(">" + "L" * (len(data)/4), data) + arr = unpack(">%dL" % (len(data)//4), data) for n in range(numGlyphs): self.glyphPos.append((arr[n])) # n+1 !? else: @@ -952,7 +961,7 @@ class TTFontFile: limit = unicode_cmap_offset + length self.skip(2) - segCount = self.read_ushort() / 2 + segCount = self.read_ushort() // 2 self.skip(6) endCount = [] for i in range(segCount): @@ -1020,7 +1029,7 @@ class TTFontFile: # Put the TTF file together def endTTFile(self, stm): - stm = '' + stm = b('') numTables = count(self.otables) searchRange = 1 entrySelector = 0 @@ -1046,7 +1055,7 @@ class TTFontFile: for tag, data in sorted_tables: if (tag == 'head'): head_start = offset - stm += tag + stm += tag.encode("latin1") checksum = calcChecksum(data) stm += pack(">HH", checksum[0],checksum[1]) stm += pack(">LL", offset, strlen(data)) @@ -1055,7 +1064,7 @@ class TTFontFile: # Table data for tag, data in sorted_tables: - data += "\0\0\0" + data += b("\0\0\0") stm += substr(data,0,(strlen(data)&~3)) checksum = calcChecksum(stm) @@ -1064,20 +1073,3 @@ class TTFontFile: stm = self.splice(stm,(head_start + 8),chk) return stm -if __name__ == '__main__': - ttf = TTFontFile() - ttffile = 'DejaVuSansCondensed.ttf'; - ttf.getMetrics(ttffile) - # test basic metrics: - assert round(ttf.descent, 0) == -236 - assert round(ttf.capHeight, 0) == 928 - assert ttf.flags == 4 - assert [round(i, 0) for i in ttf.bbox] == [-918, -415, 1513, 1167] - assert ttf.italicAngle == 0 - assert ttf.stemV == 87 - assert round(ttf.defaultWidth, 0) == 540 - assert round(ttf.underlinePosition, 0) == -63 - assert round(ttf.underlineThickness, 0) == 44 - # test char widths 8(against binary file generated by tfpdf.php): - assert ''.join(ttf.charWidths) == open("dejavusanscondensed.cw.dat").read() - diff --git a/gluon/contrib/pyfpdf.py b/gluon/contrib/pyfpdf.py index 26fc2940..4c47e7c8 100644 --- a/gluon/contrib/pyfpdf.py +++ b/gluon/contrib/pyfpdf.py @@ -5,7 +5,7 @@ # Read more about this http://code.google.com/p/pyfpdf # Please note that new package name is fpdf (to avoid some naming conflicts) # import fpdf into pyfpdf for backward compatibility (prior web2py 2.0): -from fpdf import * +from gluon.contrib.fpdf import * # import warnings # warnings.warn("pyfpdf package name is deprecated, please use fpdf instead") diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 5f242434..15ee3631 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -10,6 +10,7 @@ from .test_dal import * from .test_cache import * from .test_template import * from .test_html import * +from .test_contribs import * if sys.version[:3] == '2.7': from .test_compileapp import * @@ -24,5 +25,4 @@ if sys.version[:3] == '2.7': from .test_appadmin import * from .test_scheduler import * from .test_web import * - from .test_contribs import * from .test_old_doctests import * diff --git a/gluon/tests/test_contribs.py b/gluon/tests/test_contribs.py index bdc2b4e9..577056e4 100644 --- a/gluon/tests/test_contribs.py +++ b/gluon/tests/test_contribs.py @@ -8,7 +8,7 @@ import os from .fix_path import fix_sys_path fix_sys_path(__file__) - +from gluon._compat import to_bytes from gluon.storage import Storage from gluon.contrib import fpdf as fpdf from gluon.contrib import pyfpdf as pyfpdf @@ -42,8 +42,8 @@ class TestContribs(unittest.TestCase): pdf.write(5, 'hello world') pdf_out = pdf.output('', 'S') - self.assertTrue(fpdf.FPDF_VERSION in pdf_out, 'version string') - self.assertTrue('hello world' in pdf_out, 'sample message') + self.assertTrue(to_bytes(fpdf.FPDF_VERSION) in pdf_out, 'version string') + self.assertTrue(to_bytes('hello world') in pdf_out, 'sample message') def test_appconfig(self): """