SP/web2py/gluon/contrib/fpdf/fpdf.py
Saturneic 064f602b1a Add.
2018-10-25 23:33:13 +08:00

2060 lines
77 KiB
Python

#!/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.x<margin):
self.x=margin
def set_top_margin(self, margin):
"Set top margin"
self.t_margin=margin
def set_right_margin(self, margin):
"Set right margin"
self.r_margin=margin
def set_auto_page_break(self, auto,margin=0):
"Set auto page break mode and triggering margin"
self.auto_page_break=auto
self.b_margin=margin
self.page_break_trigger=self.h-margin
def set_display_mode(self, zoom,layout='continuous'):
"""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:
self.error('Incorrect zoom display mode: '+zoom)
if(layout=='single' or layout=='continuous' or layout=='two' or layout=='default'):
self.layout_mode=layout
else:
self.error('Incorrect layout display mode: '+layout)
def set_compression(self, compress):
"Set page compression"
self.compress=compress
def set_title(self, title):
"Title of document"
self.title=title
def set_subject(self, subject):
"Subject of document"
self.subject=subject
def set_author(self, author):
"Author of document"
self.author=author
def set_keywords(self, keywords):
"Keywords of document"
self.keywords=keywords
def set_creator(self, creator):
"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
return alias
def error(self, msg):
"Fatal error"
raise RuntimeError('FPDF error: '+msg)
def open(self):
"Begin document"
self.state=1
def close(self):
"Terminate document"
if(self.state==3):
return
if(self.page==0):
self.add_page()
#Page footer
self.in_footer=1
self.footer()
self.in_footer=0
#close page
self._endpage()
#close document
self._enddoc()
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
if self.underline:
style = self.font_style + 'U'
else:
style = self.font_style
size=self.font_size_pt
lw=self.line_width
dc=self.draw_color
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
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)<numt):
adj = -(self.ws * self.k) * 1000 / self.font_size_pt
s += sprintf('%d(%s) ', adj, space)
s += '] TJ'
s += ' ET'
else:
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',(self.x+dx)*k,(self.h-(self.y+.5*h+.3*self.font_size))*k,txt2)
if(self.underline):
s+=' '+self._dounderline(self.x+dx,self.y+.5*h+.3*self.font_size,txt)
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, True),self.font_size,link)
if(s):
self._out(s)
self.lasth=h
if(ln>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(i<nb):
#Get next character
c=s[i]
if(c=="\n"):
#Explicit line break
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))
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(i<nb):
#Get next character
c=s[i]
if(c=="\n"):
#Explicit line break
self.cell(w,h,substr(s,j,i-j),0,2,'',0,link)
i+=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
continue
if(c==' '):
sep=i
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(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('<</Type /Page')
self._out('/Parent 1 0 R')
w_pt = self.pages[n]["w_pt"]
h_pt = self.pages[n]["h_pt"]
if w_pt != dw_pt or h_pt != dh_pt:
self._out(sprintf('/MediaBox [0 0 %.2f %.2f]', w_pt, h_pt))
self._out('/Resources 2 0 R')
if self.page_links and n in self.page_links:
# Links
annots = '/Annots ['
for pl in self.page_links[n]:
rect = sprintf('%.2f %.2f %.2f %.2f', pl[0], pl[1],
pl[0] + pl[2], pl[1] - pl[3])
annots += '<</Type /Annot /Subtype /Link /Rect [' + \
rect + '] /Border [0 0 0] '
if isinstance(pl[4], basestring):
annots += '/A <</S /URI /URI ' + \
self._textstring(pl[4]) + '>>>>'
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 <</Type /Group /S /Transparency"\
"/CS /DeviceRGB>>')
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('<</Type /Pages')
kids = '/Kids ['
for i in range(0, nb):
kids += str(3 + 2 * i) + ' 0 R '
self._out(kids + ']')
self._out('/Count ' + str(nb))
self._out(sprintf('/MediaBox [0 0 %.2f %.2f]', dw_pt, dh_pt))
self._out('>>')
self._out('endobj')
def _putfonts(self):
nf=self.n
for diff in self.diffs:
#Encodings
self._newobj()
self._out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+self.diffs[diff]+']>>')
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('<</Length '+str(len(font)))
if(compressed):
self._out('/Filter /FlateDecode')
self._out('/Length1 '+str(info['length1']))
if('length2' in info):
self._out('/Length2 '+str(info['length2'])+' /Length3 0')
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('<</Type /Font')
self._out('/BaseFont /'+name)
self._out('/Subtype /Type1')
if(name!='Symbol' and name!='ZapfDingbats'):
self._out('/Encoding /WinAnsiEncoding')
self._out('>>')
self._out('endobj')
elif(type=='Type1' or type=='TrueType'):
#Additional Type1 or TrueType font
self._newobj()
self._out('<</Type /Font')
self._out('/BaseFont /'+name)
self._out('/Subtype /'+type)
self._out('/FirstChar 32 /LastChar 255')
self._out('/Widths '+str(self.n+1)+' 0 R')
self._out('/FontDescriptor '+str(self.n+2)+' 0 R')
if(font['enc']):
if('diff' in font):
self._out('/Encoding '+str(nf+font['diff'])+' 0 R')
else:
self._out('/Encoding /WinAnsiEncoding')
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='<</Type /FontDescriptor /FontName /'+name
for k in ('Ascent', 'Descent', 'CapHeight', 'Flags', 'FontBBox', 'ItalicAngle', 'StemV', 'MissingWidth'):
s += ' /%s %s' % (k, font['desc'][k])
filename=font['file']
if(filename):
s+=' /FontFile'
if type!='Type1':
s+='2'
s+=' '+str(self.font_files[filename]['n'])+' 0 R'
self._out(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('<</Type /Font');
self._out('/Subtype /Type0');
self._out('/BaseFont /' + fontname + '');
self._out('/Encoding /Identity-H');
self._out('/DescendantFonts [' + str(self.n + 1) + ' 0 R]')
self._out('/ToUnicode ' + str(self.n + 2) + ' 0 R')
self._out('>>')
self._out('endobj')
# CIDFontType2
# A CIDFont whose glyph descriptions are based on TrueType font technology
self._newobj()
self._out('<</Type /Font')
self._out('/Subtype /CIDFontType2')
self._out('/BaseFont /' + fontname + '')
self._out('/CIDSystemInfo ' + str(self.n + 2) + ' 0 R')
self._out('/FontDescriptor ' + str(self.n + 3) + ' 0 R')
if (font['desc'].get('MissingWidth')):
self._out('/DW %d' % font['desc']['MissingWidth'])
self._putTTfontwidths(font, ttf.maxUni)
self._out('/CIDToGIDMap ' + str(self.n + 4) + ' 0 R')
self._out('>>')
self._out('endobj')
# ToUnicode
self._newobj()
toUni = "/CIDInit /ProcSet findresource begin\n" \
"12 dict begin\n" \
"begincmap\n" \
"/CIDSystemInfo\n" \
"<</Registry (Adobe)\n" \
"/Ordering (UCS)\n" \
"/Supplement 0\n" \
">> def\n" \
"/CMapName /Adobe-Identity-UCS def\n" \
"/CMapType 2 def\n" \
"1 begincodespacerange\n" \
"<0000> <FFFF>\n" \
"endcodespacerange\n" \
"1 beginbfrange\n" \
"<0000> <FFFF> <0000>\n" \
"endbfrange\n" \
"endcmap\n" \
"CMapName currentdict /CMap defineresource pop\n" \
"end\n" \
"end"
self._out('<</Length ' + str(len(toUni)) + '>>')
self._putstream(toUni)
self._out('endobj')
# CIDSystemInfo dictionary
self._newobj()
self._out('<</Registry (Adobe)')
self._out('/Ordering (UCS)')
self._out('/Supplement 0')
self._out('>>')
self._out('endobj')
# Font descriptor
self._newobj()
self._out('<</Type /FontDescriptor')
self._out('/FontName /' + fontname)
for kd in ('Ascent', 'Descent', 'CapHeight', 'Flags', 'FontBBox', 'ItalicAngle', 'StemV', 'MissingWidth'):
v = font['desc'][kd]
if (kd == 'Flags'):
v = v | 4;
v = v & ~32; # SYMBOLIC font flag
self._out(' /%s %s' % (kd, v))
self._out('/FontFile2 ' + str(self.n + 2) + ' 0 R')
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('<</Length ' + str(len(cidtogidmap)) + '')
self._out('/Filter /FlateDecode')
self._out('>>')
self._putstream(cidtogidmap)
self._out('endobj')
#Font file
self._newobj()
self._out('<</Length ' + str(len(fontstream)))
self._out('/Filter /FlateDecode')
self._out('/Length1 ' + str(ttfontsize))
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('<</Type /XObject')
self._out('/Subtype /Image')
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]')
else:
self._out('/ColorSpace /'+info['cs'])
if(info['cs']=='DeviceCMYK'):
self._out('/Decode [1 0 1 0 1 0 1 0]')
self._out('/BitsPerComponent '+str(info['bpc']))
if 'f' in info:
self._out('/Filter /'+info['f'])
if 'dp' in info:
self._out('/DecodeParms <<' + info['dp'] + '>>')
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']