#!/usr/bin/env python # -*- coding: latin-1 -*- # **************************************************************************** # * Software: FPDF for python * # * Version: 1.7.1 * # * Date: 2010-09-10 * # * Last update: 2012-08-16 * # * License: LGPL v3.0 * # * * # * Original Author (PHP): Olivier PLATHEY 2004-12-31 * # * Ported to Python 2.4 by Max (maxpat78@yahoo.it) on 2006-05 * # * Maintainer: Mariano Reingart (reingart@gmail.com) et al since 2008 est. * # * 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 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.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 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'): # 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 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_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. else: self.error("Incorrect unit: " + unit) # Page format 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 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 # Page margins (1 cm) margin = 28.35 / self.k self.set_margins(margin, margin) # Interior cell margin (1 mm) self.c_margin = margin / 10.0 # line width (0.2 mm) self.line_width = .567 / self.k # Automatic page break 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' @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" self.l_margin=left self.t_margin=top if(right==-1): right=left self.r_margin=right def set_left_margin(self, margin): "Set left margin" self.l_margin=margin if(self.page>0 and self.x0): #Page footer self.in_footer=1 self.footer() self.in_footer=0 #close page self._endpage() #Start new page self._beginpage(orientation, format, same) #Set line cap style to square self._out('2 J') #Set line width self.line_width=lw self._out(sprintf('%.2f w',lw*self.k)) #Set font if(family): self.set_font(family,style,size) #Set colors self.draw_color=dc if(dc!='0 G'): self._out(dc) self.fill_color=fc if(fc!='0 g'): self._out(fc) self.text_color=tc self.color_flag=cf #Page header self.header() #Restore line width if(self.line_width!=lw): self.line_width=lw self._out(sprintf('%.2f w',lw*self.k)) #Restore font if(family): self.set_font(family,style,size) #Restore colors if(self.draw_color!=dc): self.draw_color=dc self._out(dc) if(self.fill_color!=fc): self.fill_color=fc 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" pass def footer(self): "Footer to be implemented in your own inherited class" pass def page_no(self): "Get current page number" return self.page def set_draw_color(self, r,g=-1,b=-1): "Set color for all stroking operations" if((r==0 and g==0 and b==0) or g==-1): self.draw_color=sprintf('%.3f G',r/255.0) else: self.draw_color=sprintf('%.3f %.3f %.3f RG',r/255.0,g/255.0,b/255.0) if(self.page>0): self._out(self.draw_color) def set_fill_color(self,r,g=-1,b=-1): "Set color for all filling operations" if((r==0 and g==0 and b==0) or g==-1): self.fill_color=sprintf('%.3f g',r/255.0) else: self.fill_color=sprintf('%.3f %.3f %.3f rg',r/255.0,g/255.0,b/255.0) self.color_flag=(self.fill_color!=self.text_color) if(self.page>0): self._out(self.fill_color) def set_text_color(self, r,g=-1,b=-1): "Set color for text" if((r==0 and g==0 and b==0) or g==-1): self.text_color=sprintf('%.3f g',r/255.0) else: 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, 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) if self.unifontsubset: for char in s: char = ord(char) if len(cw) > char: w += cw[char] # ord(cw[2*char])<<8 + ord(cw[2*char+1]) #elif (char>0 and char<128 and isset($cw[chr($char)])) { $w += $cw[chr($char)]; } elif (self.current_font['desc']['MissingWidth']) : w += self.current_font['desc']['MissingWidth'] #elif (isset($this->CurrentFont['MissingWidth'])) { $w += $this->CurrentFont['MissingWidth']; } else: w += 500 else: for i in range(0, l): w += cw.get(s[i],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" self.line_width=width 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)) def _set_dash(self, dash_length=False, space_length=False): if(dash_length and space_length): s = sprintf('[%.3f %.3f] 0 d', dash_length*self.k, space_length*self.k) else: 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 - space_length: Length of the space between dashes""" self._set_dash(dash_length, space_length) 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'): op='f' elif(style=='FD' or style=='DF'): op='B' else: 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() if (fname == ''): fname = family.replace(' ','') + style.lower() + '.pkl' if (family == 'arial'): family = 'helvetica' style = style.upper() if (style == 'IB'): style = 'BI' fontkey = family+style if fontkey in self.fonts: # Font already added! return if (uni): global SYSTEM_TTFONTS, FPDF_CACHE_MODE, FPDF_CACHE_DIR if os.path.exists(fname): ttffilename = fname elif (FPDF_FONT_DIR and os.path.exists(os.path.join(FPDF_FONT_DIR, fname))): ttffilename = os.path.join(FPDF_FONT_DIR, fname) elif (SYSTEM_TTFONTS and os.path.exists(os.path.join(SYSTEM_TTFONTS, fname))): ttffilename = os.path.join(SYSTEM_TTFONTS, fname) else: raise RuntimeError("TTF Font file not found: %s" % fname) name = '' 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 = { 'Ascent': int(round(ttf.ascent, 0)), 'Descent': int(round(ttf.descent, 0)), 'CapHeight': int(round(ttf.capHeight, 0)), 'Flags': ttf.flags, 'FontBBox': "[%s %s %s %s]" % ( int(round(ttf.bbox[0], 0)), int(round(ttf.bbox[1], 0)), int(round(ttf.bbox[2], 0)), int(round(ttf.bbox[3], 0))), 'ItalicAngle': int(ttf.italicAngle), 'StemV': int(round(ttf.stemV, 0)), 'MissingWidth': int(round(ttf.defaultWidth, 0)), } # Generate metrics .pkl file font_dict = { 'name': re.sub('[ ()]', '', ttf.fullName), 'type': 'TTF', 'desc': desc, 'up': round(ttf.underlinePosition), 'ut': round(ttf.underlineThickness), 'ttffile': ttffilename, 'fontkey': fontkey, 'originalsize': os.stat(ttffilename).st_size, 'cw': ttf.charWidths, } 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 = list(range(0,57)) # include numbers in the subset! else: 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'], 'up': font_dict['up'], 'ut': font_dict['ut'], 'cw': font_dict['cw'], 'ttffile': font_dict['ttffile'], 'fontkey': fontkey, 'subset': sbarr, 'unifilename': unifilename, } self.font_files[fontkey] = {'length1': font_dict['originalsize'], 'type': "TTF", 'ttffile': ttffilename} self.font_files[fname] = {'type': "TTF"} else: with open(fname, 'rb') as fontfile: font_dict = pickle.load(fontfile) 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 range(1, nb+1): if(self.diffs[i] == diff): d = i break if (d == 0): d = nb + 1 self.diffs[d] = diff self.fonts[fontkey]['diff'] = d filename = font_dict.get('filename') if (filename): if (font_dict['type'] == 'TrueType'): originalsize = font_dict['originalsize'] self.font_files[filename]={'length1': originalsize} else: 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" family=family.lower() if(family==''): family=self.font_family if(family=='arial'): family='helvetica' elif(family=='symbol' or family=='zapfdingbats'): style='' style=style.upper() if('U' in style): self.underline=1 style=style.replace('U','') else: self.underline=0 if(style=='IB'): style='BI' if(size==0): size=self.font_size_pt #Test if font is already selected if(self.font_family==family and self.font_style==style and self.font_size_pt==size): return #Test if used for the first time fontkey=family+style if fontkey not in self.fonts: #Check if one of the standard fonts if fontkey in self.core_fonts: if fontkey not in fpdf_charwidths: #Load metric file name=os.path.join(FPDF_FONT_DIR,family) if(family=='times' or family=='helvetica'): name+=style.lower() 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 self.fonts[fontkey]={'i':i,'type':'core','name':self.core_fonts[fontkey],'up':-100,'ut':50,'cw':fpdf_charwidths[fontkey]} else: self.error('Undefined font: '+family+' '+style) #Select it self.font_family=family self.font_style=style self.font_size_pt=size self.font_size=size/self.k self.current_font=self.fonts[fontkey] self.unifontsubset = (self.fonts[fontkey]['type'] == 'TTF') if(self.page>0): self._out(sprintf('BT /F%d %.2f Tf ET',self.current_font['i'],self.font_size_pt)) def set_font_size(self, size): "Set font size in points" if(self.font_size_pt==size): return self.font_size_pt=size self.font_size=size/self.k 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 self.links[n]=(0,0) return n def set_link(self, link,y=0,page=-1): "Set destination of internal link" if(y==-1): y=self.y if(page==-1): page=self.page self.links[link]=[page,y] def link(self, x,y,w,h,link): "Put a link on the page" if not self.page in self.page_links: 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) if (self.unifontsubset): txt2 = self._escape(UTF8ToUTF16BE(txt, False)) for uni in UTF8StringToArray(txt): self.current_font['subset'].append(uni) else: txt2 = self._escape(txt) s=sprintf('BT %.2f %.2f Td (%s) Tj ET',x*self.k,(self.h-y)*self.k, txt2) if(self.underline and txt!=''): s+=' '+self._dounderline(x,y,txt) if(self.color_flag): 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 if y is None: y = self.y; if self.angle!=0: self._out('Q') self.angle = angle if angle!=0: angle *= math.pi/180; c = math.cos(angle); s = math.sin(angle); cx = x*self.k; cy = (self.h-y)*self.k s = sprintf('q %.5F %.5F %.5F %.5F %.2F %.2F cm 1 0 0 1 %.2F %.2F cm',c,s,-s,c,cx,cy,-cx,-cy) self._out(s) def accept_page_break(self): "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) k=self.k if(self.y+h>self.page_break_trigger and not self.in_footer and self.accept_page_break()): #Automatic page break x=self.x ws=self.ws if(ws>0): self.ws=0 self._out('0 Tw') self.add_page(same = True) self.x=x if(ws>0): self.ws=ws self._out(sprintf('%.3f Tw',ws*k)) if(w==0): w=self.w-self.r_margin-self.x s='' if(fill==1 or border==1): if(fill==1): if border==1: op='B' else: op='f' else: op='S' s=sprintf('%.2f %.2f %.2f %.2f re %s ',self.x*k,(self.h-self.y)*k,w*k,-h*k,op) if(isinstance(border,basestring)): x=self.x y=self.y if('L' in border): s+=sprintf('%.2f %.2f m %.2f %.2f l S ',x*k,(self.h-y)*k,x*k,(self.h-(y+h))*k) if('T' in border): s+=sprintf('%.2f %.2f m %.2f %.2f l S ',x*k,(self.h-y)*k,(x+w)*k,(self.h-y)*k) if('R' in border): s+=sprintf('%.2f %.2f m %.2f %.2f l S ',(x+w)*k,(self.h-y)*k,(x+w)*k,(self.h-(y+h))*k) if('B' in border): 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, True) elif(align=='C'): dx=(w-self.get_string_width(txt, True))/2.0 else: dx=self.c_margin if(self.color_flag): s+='q '+self.text_color+' ' # If multibyte, Tw has no effect - do word spacing using an adjustment before each space if (self.ws and self.unifontsubset): for uni in UTF8StringToArray(txt): self.current_font['subset'].append(uni) space = self._escape(UTF8ToUTF16BE(' ', False)) s += sprintf('BT 0 Tw %.2F %.2F Td [',(self.x + dx) * k,(self.h - (self.y + 0.5*h+ 0.3 * self.font_size)) * k) t = txt.split(' ') numt = len(t) for i in range(numt): tx = t[i] tx = '(' + self._escape(UTF8ToUTF16BE(tx, False)) + ')' s += sprintf('%s ', tx); if ((i+1)0): #Go to next line self.y+=h if(ln==1): self.x=self.l_margin 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) ret = [] # if split_only = True, returns splited text cells cw=self.current_font['cw'] if(w==0): w=self.w-self.r_margin-self.x wmax=(w-2*self.c_margin)*1000.0/self.font_size s=txt.replace("\r",'') nb=len(s) if(nb>0 and s[nb-1]=="\n"): nb-=1 b=0 if(border): if(border==1): border='LTRB' b='LRT' b2='LR' else: b2='' if('L' in border): b2+='L' if('R' in border): b2+='R' if ('T' in border): b=b2+'T' else: b=b2 sep=-1 i=0 j=0 l=0 ns=0 nl=1 while(i0): self.ws=0 if not split_only: self._out('0 Tw') if not split_only: self.cell(w,h,substr(s,j,i-j),b,2,align,fill) else: ret.append(substr(s,j,i-j)) i+=1 sep=-1 j=i l=0 ns=0 nl+=1 if(border and nl==2): b=b2 continue if(c==' '): sep=i ls=l ns+=1 if self.unifontsubset: l += self.get_string_width(c, True) / self.font_size*1000.0 else: l += cw.get(c,0) if(l>wmax): #Automatic line break if(sep==-1): if(i==j): i+=1 if(self.ws>0): self.ws=0 if not split_only: self._out('0 Tw') if not split_only: self.cell(w,h,substr(s,j,i-j),b,2,align,fill) else: ret.append(substr(s,j,i-j)) else: if(align=='J'): if ns>1: self.ws=(wmax-ls)/1000.0*self.font_size/(ns-1) else: self.ws=0 if not split_only: self._out(sprintf('%.3f Tw',self.ws*self.k)) if not split_only: self.cell(w,h,substr(s,j,sep-j),b,2,align,fill) else: ret.append(substr(s,j,sep-j)) i=sep+1 sep=-1 j=i l=0 ns=0 nl+=1 if(border and nl==2): b=b2 else: i+=1 #Last chunk if(self.ws>0): self.ws=0 if not split_only: self._out('0 Tw') if(border and 'B' in border): b+='B' if not split_only: self.cell(w,h,substr(s,j,i-j),b,2,align,fill) self.x=self.l_margin else: 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) cw=self.current_font['cw'] w=self.w-self.r_margin-self.x wmax=(w-2*self.c_margin)*1000.0/self.font_size s=txt.replace("\r",'') nb=len(s) sep=-1 i=0 j=0 l=0 nl=1 while(iwmax): #Automatic line break if(sep==-1): if(self.x>self.l_margin): #Move to next line self.x=self.l_margin self.y+=h w=self.w-self.r_margin-self.x wmax=(w-2*self.c_margin)*1000.0/self.font_size i+=1 nl+=1 continue if(i==j): i+=1 self.cell(w,h,substr(s,j,i-j),0,2,'',0,link) else: self.cell(w,h,substr(s,j,sep-j),0,2,'',0,link) i=sep+1 sep=-1 j=i l=0 if(nl==1): self.x=self.l_margin w=self.w-self.r_margin-self.x wmax=(w-2*self.c_margin)*1000.0/self.font_size nl+=1 else: i+=1 #Last chunk 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: #First use of image, get info if(type==''): pos=name.rfind('.') if(not pos): self.error('image file has no extension and no type was specified: '+name) type=substr(name,pos+1) type=type.lower() if(type=='jpg' or type=='jpeg'): info=self._parsejpg(name) elif(type=='png'): info=self._parsepng(name) else: #Allow for additional formats #maybe the image is not showing the correct extension, #but the header is OK, succeed_parsing = False #try all the parsing functions parsing_functions = [self._parsejpg,self._parsepng,self._parsegif] for pf in parsing_functions: try: info = pf(name) succeed_parsing = True break; except: pass #last resource if not succeed_parsing: mtd='_parse'+type if not hasattr(self,mtd): self.error('Unsupported image type: '+type) info=getattr(self, mtd)(name) mtd='_parse'+type if not hasattr(self,mtd): self.error('Unsupported image type: '+type) info=getattr(self, mtd)(name) info['i']=len(self.images)+1 self.images[name]=info else: info=self.images[name] #Automatic width and height calculation if needed if(w==0 and h==0): #Put image at 72 dpi w=info['w']/self.k h=info['h']/self.k elif(w==0): w=h*info['w']/info['h'] elif(h==0): h=w*info['h']/info['w'] # Flowing mode if y is None: 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(same = True) self.x = x y = self.y self.y += h if x is None: x = self.x self._out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q',w*self.k,h*self.k,x*self.k,(self.h-(y+h))*self.k,info['i'])) 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 if(isinstance(h, basestring)): self.y+=self.lasth else: self.y+=h def get_x(self): "Get x position" return self.x def set_x(self, x): "Set x position" if(x>=0): self.x=x else: self.x=self.w+x def get_y(self): "Get y position" return self.y def set_y(self, y): "Set y position and reset x" self.x=self.l_margin if(y>=0): self.y=y else: self.y=self.h+y def set_xy(self, x,y): "Set x and y positions" self.set_y(y) self.set_x(x) def output(self, name='',dest=''): """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==''): dest='I' else: dest='F' 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 with open(name,'wb') as f: f.write(buffer) elif dest=='S': #Return as a byte string return buffer else: self.error('Incorrect output destination: '+dest) 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 (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): # self.error("Don\'t alter the locale before including class file"); #Check for decimal separator if(sprintf('%.1f',1.0)!='1.0'): import locale locale.setlocale(locale.LC_NUMERIC,'C') def _getfontpath(self): return FPDF_FONT_DIR+'/' def _putpages(self): 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) r = UTF8ToUTF16BE(str(nb), False) 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 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: dw_pt = self.dh_pt dh_pt = self.dw_pt if self.compress: filter = '/Filter /FlateDecode ' else: filter = '' for n in range(1, nb + 1): # Page self._newobj() self._out('<>>>' else: l = self.links[pl[4]] if l[0] in self.orientation_changes: 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>>') self._out('endobj') # Page content content = self.pages[n]["content"] if self.compress: # 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 = content self._newobj() self._out('<<' + filter + '/Length ' + str(len(p)) + '>>') self._putstream(p) self._out('endobj') # Pages root self.offsets[1] = len(self.buffer) self._out('1 0 obj') self._out('<>') self._out('endobj') def _putfonts(self): nf=self.n for diff in self.diffs: #Encodings self._newobj() self._out('<>') self._out('endobj') 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 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) if(header): #Strip first binary header font=substr(font,6) if(header and ord(font[info['length1']])==128): #Strip second binary header font=substr(font,0,info['length1'])+substr(font,info['length1']+6) self._out('<>') self._putstream(font) self._out('endobj') 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'] name=font['name'] if(type=='core'): #Standard font self._newobj() self._out('<>') self._out('endobj') elif(type=='Type1' or type=='TrueType'): #Additional Type1 or TrueType font self._newobj() self._out('<>') self._out('endobj') #Widths self._newobj() cw=font['cw'] s='[' 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='<>') self._out('endobj') elif (type == 'TTF'): self.fonts[k]['n'] = self.n + 1 ttf = TTFontFile() fontname = 'MPDFAA' + '+' + font['name'] subset = font['subset'] del subset[0] ttfontstream = ttf.makeSubset(font['ttffile'], subset) ttfontsize = len(ttfontstream) fontstream = zlib.compress(ttfontstream) codeToGlyph = ttf.codeToGlyph ##del codeToGlyph[0] # Type0 Font # A composite font - a font composed of other fonts, organized hierarchically self._newobj() self._out('<>') self._out('endobj') # CIDFontType2 # A CIDFont whose glyph descriptions are based on TrueType font technology self._newobj() self._out('<>') self._out('endobj') # ToUnicode self._newobj() toUni = "/CIDInit /ProcSet findresource begin\n" \ "12 dict begin\n" \ "begincmap\n" \ "/CIDSystemInfo\n" \ "<> def\n" \ "/CMapName /Adobe-Identity-UCS def\n" \ "/CMapType 2 def\n" \ "1 begincodespacerange\n" \ "<0000> \n" \ "endcodespacerange\n" \ "1 beginbfrange\n" \ "<0000> <0000>\n" \ "endbfrange\n" \ "endcmap\n" \ "CMapName currentdict /CMap defineresource pop\n" \ "end\n" \ "end" self._out('<>') self._putstream(toUni) self._out('endobj') # CIDSystemInfo dictionary self._newobj() self._out('<>') self._out('endobj') # Font descriptor self._newobj() self._out('<>') self._out('endobj') # Embed CIDToGIDMap # A specification of the mapping from CIDs to glyph indices cidtogidmap = ''; cidtogidmap = ["\x00"] * 256*256*2 for cc, glyph in codeToGlyph.items(): cidtogidmap[cc*2] = chr(glyph >> 8) cidtogidmap[cc*2 + 1] = chr(glyph & 0xFF) 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('<>') self._putstream(cidtogidmap) self._out('endobj') #Font file self._newobj() self._out('<>') self._putstream(fontstream) self._out('endobj') del ttf else: #Allow for additional types mtd='_put'+type.lower() if(not method_exists(self,mtd)): self.error('Unsupported font type: '+type) self.mtd(font) def _putTTfontwidths(self, font, maxUni): if font['unifilename']: cw127fname = os.path.splitext(font['unifilename'])[0] + '.cw127.pkl' else: cw127fname = None font_dict = load_cache(cw127fname) if font_dict is None: rangeid = 0 range_ = {} range_interval = {} prevcid = -2 prevwidth = -1 interval = False startcid = 1 else: rangeid = font_dict['rangeid'] range_ = font_dict['range'] prevcid = font_dict['prevcid'] prevwidth = font_dict['prevwidth'] interval = font_dict['interval'] range_interval = font_dict['range_interval'] startcid = 128 cwlen = maxUni + 1 # for each character subset = set(font['subset']) for cid in range(startcid, cwlen): if cid == 128 and cw127fname and not os.path.exists(cw127fname): try: with open(cw127fname, "wb") as fh: font_dict = {} font_dict['rangeid'] = rangeid font_dict['prevcid'] = prevcid font_dict['prevwidth'] = prevwidth font_dict['interval'] = interval font_dict['range_interval'] = range_interval font_dict['range'] = range_ pickle.dump(font_dict, fh) except IOError: if not exception().errno == errno.EACCES: raise # Not a permission error. if cid > 255 and (cid not in subset): # continue width = font['cw'][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): if (width == range_[rangeid][0]): range_.setdefault(rangeid, []).append(width) else: range_[rangeid].pop() # new range rangeid = prevcid range_[rangeid] = [prevwidth, width] interval = True range_interval[rangeid] = True else: if (interval): # new range rangeid = cid range_[rangeid] = [width] else: range_[rangeid].append(width) interval = False else: rangeid = cid range_[rangeid] = [width] interval = False prevcid = cid prevwidth = width prevk = -1 nextk = -1 prevint = False for k, ws in sorted(range_.items()): cws = len(ws) if (k == nextk and not prevint and (not k in range_interval or cws < 3)): if (k in range_interval): del range_interval[k] range_[prevk] = range_[prevk] + range_[k] del range_[k] else: prevk = k nextk = k + cws if (k in range_interval): prevint = (cws > 3) del range_interval[k] nextk -= 1 else: prevint = False w = [] for k, ws in sorted(range_.items()): if (len(set(ws)) == 1): w.append(' %s %s %s' % (k, k + len(ws) - 1, ws[0])) else: w.append(' %s [ %s ]\n' % (k, ' '.join([str(int(h)) for h in ws]))) ## self._out('/W [%s]' % ''.join(w)) def _putimages(self): filter='' if self.compress: 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: del info['smask'] def _putimage(self, info): if 'data' in info: self._newobj() info['n']=self.n self._out('<>') if('trns' in info and isinstance(info['trns'], list)): 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): self._out('/SMask ' + str(self.n+1) + ' 0 R'); self._out('/Length '+str(len(info['data']))+'>>') self._putstream(info['data']) self._out('endobj') # Soft mask if('smask' in info): dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' + str(info['w']) smask = {'w': info['w'], 'h': info['h'], 'cs': 'DeviceGray', 'bpc': 8, 'f': info['f'], 'dp': dp, 'data': info['smask']} self._putimage(smask) #Palette if(info['cs']=='Indexed'): self._newobj() filter = self.compress and '/Filter /FlateDecode ' or '' if self.compress: pal=zlib.compress(info['pal']) else: pal=info['pal'] self._out('<<'+filter+'/Length '+str(len(pal))+'>>') self._putstream(pal) self._out('endobj') def _putxobjectdict(self): 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 <<') 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() self._out('>>') def _putresources(self): self._putfonts() self._putimages() #Resource dictionary self.offsets[2]=len(self.buffer) self._out('2 0 obj') self._out('<<') self._putresourcedict() self._out('>>') self._out('endobj') def _putinfo(self): self._out('/Producer '+self._textstring('PyFPDF '+FPDF_VERSION+' http://pyfpdf.googlecode.com/')) if hasattr(self,'title'): self._out('/Title '+self._textstring(self.title)) if hasattr(self,'subject'): self._out('/Subject '+self._textstring(self.subject)) if hasattr(self,'author'): self._out('/Author '+self._textstring(self.author)) if hasattr (self,'keywords'): self._out('/Keywords '+self._textstring(self.keywords)) if hasattr(self,'creator'): self._out('/Creator '+self._textstring(self.creator)) self._out('/CreationDate '+self._textstring('D:'+datetime.now().strftime('%Y%m%d%H%M%S'))) def _putcatalog(self): self._out('/Type /Catalog') self._out('/Pages 1 0 R') if(self.zoom_mode=='fullpage'): self._out('/OpenAction [3 0 R /Fit]') elif(self.zoom_mode=='fullwidth'): self._out('/OpenAction [3 0 R /FitH null]') elif(self.zoom_mode=='real'): self._out('/OpenAction [3 0 R /XYZ null null 1]') elif(not isinstance(self.zoom_mode,basestring)): 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'): self._out('/PageLayout /OneColumn') elif(self.layout_mode=='two'): self._out('/PageLayout /TwoColumnLeft') def _putheader(self): self._out('%PDF-'+self.pdf_version) def _puttrailer(self): self._out('/Size '+str(self.n+1)) self._out('/Root '+str(self.n)+' 0 R') self._out('/Info '+str(self.n-1)+' 0 R') def _enddoc(self): self._putheader() self._putpages() self._putresources() #Info self._newobj() self._out('<<') self._putinfo() self._out('>>') self._out('endobj') #Catalog self._newobj() self._out('<<') self._putcatalog() self._out('>>') self._out('endobj') #Cross-ref o=len(self.buffer) self._out('xref') self._out('0 '+(str(self.n+1))) self._out('0000000000 65535 f ') for i in range(1,self.n+1): self._out(sprintf('%010d 00000 n ',self.offsets[i])) #Trailer self._out('trailer') self._out('<<') self._puttrailer() self._out('>>') self._out('startxref') self._out(o) self._out('%%EOF') self.state=3 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: # 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 self.state=1 def _newobj(self): #Begin a new object self.n+=1 self.offsets[self.n]=len(self.buffer) self._out(str(self.n)+' 0 obj') def _dounderline(self, x, y, txt): #Underline text up=self.current_font['up'] ut=self.current_font['ut'] 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 f = None try: 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()))) 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) if Image is None: self.error('PIL is required for GIF support') try: im = Image.open(filename) except Exception: self.error('Missing or incorrect image file: %s. error: %s' % (filename, str(exception()))) else: # Use temporary file 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: im.save(tmp) info = self._parsepng(tmp) os.unlink(tmp) return info def _parsepng(self, filename): #Extract info from a PNG file f = self.load_resource("image", filename) #Check signature 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) 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: ' + filename) ct=ord(f.read(1)) if(ct==0 or ct==4): colspace='DeviceGray' elif(ct==2 or ct==6): colspace='DeviceRGB' elif(ct==3): colspace='Indexed' else: self.error('Unknown color type: ' + filename) if(ord(f.read(1))!=0): self.error('Unknown compression method: ' + filename) if(ord(f.read(1))!=0): self.error('Unknown filter method: ' + filename) if(ord(f.read(1))!=0): self.error('Interlacing not supported: ' + filename) f.read(4) dp='/Predictor 15 /Colors ' if colspace == 'DeviceRGB': dp+='3' else: dp+='1' dp+=' /BitsPerComponent '+str(bpc)+' /Columns '+str(w)+'' #Scan chunks looking for palette, transparency and image data pal='' trns='' data=bytes() if PY3K else str() n=1 while n != None: n=self._freadint(f) type=f.read(4).decode("latin1") if(type=='PLTE'): #Read palette pal=f.read(n) f.read(4) elif(type=='tRNS'): #Read transparency info t=f.read(n) if(ct==0): trns=[ord(substr(t,1,1)),] elif(ct==2): trns=[ord(substr(t,1,1)),ord(substr(t,3,1)),ord(substr(t,5,1))] else: pos=t.find('\x00'.encode("latin1")) if(pos!=-1): trns=[pos,] f.read(4) elif(type=='IDAT'): #Read image data block data+=f.read(n) f.read(4) elif(type=='IEND'): break else: f.read(n+4) if(colspace=='Indexed' and not pal): 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 = b('') alpha = b('') if(ct==4): # Gray image length = 2*w for i in range(h): pos = (1+length)*i color += b(data[pos]) alpha += b(data[pos]) line = substr(data, pos+1, length) 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 += b(data[pos]) alpha += b(data[pos]) line = substr(data, pos+1, length) 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) if (self.pdf_version < '1.4'): self.pdf_version = '1.4' info['data'] = data return info def _freadint(self, f): #Read a 4-byte integer from file try: return struct.unpack('>I', f.read(4))[0] except: return None def _textstring(self, s): #Format a text string return '('+self._escape(s)+')' def _escape(self, s): #Add \ before \, ( and ) return s.replace('\\','\\\\').replace(')','\\)').replace('(','\\(').replace('\r','\\r') def _putstream(self, s): self._out('stream') self._out(s) self._out('endstream') def _out(self, s): #Add a line to the document 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 += (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 wide = w # wide/narrow codes for the digits bar_char={'0': 'nnwwn', '1': 'wnnnw', '2': 'nwnnw', '3': 'wwnnn', '4': 'nnwnw', '5': 'wnwnn', '6': 'nwwnn', '7': 'nnnww', '8': 'wnnwn', '9': 'nwnwn', 'A': 'nn', 'Z': 'wn'} self.set_fill_color(0) code = txt # add leading zero if code-length is odd if len(code) % 2 != 0: code = '0' + code # add start and stop codes code = 'AA' + code.lower() + 'ZA' for i in range(0, len(code), 2): # choose next pair of digits char_bar = code[i] char_space = code[i+1] # check whether it is a valid digit if not char_bar in bar_char.keys(): raise RuntimeError ('Char "%s" invalid for I25: ' % char_bar) if not char_space in bar_char.keys(): raise RuntimeError ('Char "%s" invalid for I25: ' % char_space) # create a wide/narrow-seq (first digit=bars, second digit=spaces) seq = '' for s in range(0, len(bar_char[char_bar])): seq += bar_char[char_bar][s] + bar_char[char_space][s] for bar in range(0, len(seq)): # set line_width depending on value if seq[bar] == 'n': line_width = narrow else: line_width = wide # draw every second value, the other is represented by space if bar % 2 == 0: self.rect(x, y, line_width, h, 'F') x += line_width @check_page def code39(self, txt, x, y, w=1.5, h=5.0): """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) 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']