initial commit

This commit is contained in:
Massimo Di Pierro
2011-11-22 23:30:42 -06:00
commit d421c1321b
580 changed files with 115527 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
Read more about this http://code.google.com/p/pyfpdf
+5
View File
@@ -0,0 +1,5 @@
from fpdf import FPDF
from html import HTMLMixin
from template import Template
+736
View File
@@ -0,0 +1,736 @@
#!/usr/bin/python
# -*- coding: latin-1 -*-
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
"Visual Template designer for PyFPDF (using wxPython OGL library)"
__author__ = "Mariano Reingart <reingart@gmail.com>"
__copyright__ = "Copyright (C) 2011 Mariano Reingart"
__license__ = "GPL 3.0"
__version__ = "1.01a"
# Based on:
# * pySjetch.py wxPython sample application
# * OGL.py and other wxPython demo modules
import os, sys
import wx
import wx.lib.ogl as ogl
from wx.lib.wordwrap import wordwrap
DEBUG = True
class CustomDialog(wx.Dialog):
"A dinamyc dialog to ask user about arbitrary fields"
def __init__(
self, parent, ID, title, size=wx.DefaultSize, pos=wx.DefaultPosition,
style=wx.DEFAULT_DIALOG_STYLE, fields=None, data=None,
):
wx.Dialog.__init__ (self, parent, ID, title, pos, size, style)
sizer = wx.BoxSizer(wx.VERTICAL)
self.textctrls = {}
for field in fields:
box = wx.BoxSizer(wx.HORIZONTAL)
label = wx.StaticText(self, -1, field)
label.SetHelpText("This is the help text for the label")
box.Add(label, 1, wx.ALIGN_CENTRE|wx.ALL, 5)
text = wx.TextCtrl(self, -1, "", size=(80,-1))
text.SetHelpText("Here's some help text for field #1")
if field in data:
text.SetValue(repr(data[field]))
box.Add(text, 1, wx.ALIGN_CENTRE|wx.ALL, 1)
sizer.Add(box, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 1)
self.textctrls[field] = text
line = wx.StaticLine(self, -1, size=(20,-1), style=wx.LI_HORIZONTAL)
sizer.Add(line, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.RIGHT|wx.TOP, 5)
btnsizer = wx.StdDialogButtonSizer()
btn = wx.Button(self, wx.ID_OK)
btn.SetHelpText("The OK button completes the dialog")
btn.SetDefault()
btnsizer.AddButton(btn)
btn = wx.Button(self, wx.ID_CANCEL)
btn.SetHelpText("The Cancel button cancels the dialog. (Cool, huh?)")
btnsizer.AddButton(btn)
btnsizer.Realize()
sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
self.SetSizer(sizer)
sizer.Fit(self)
@classmethod
def do_input(Class, parent, title, fields, data):
dlg = Class(parent, -1, title, size=(350, 200),
style=wx.DEFAULT_DIALOG_STYLE, # & ~wx.CLOSE_BOX,
fields=fields, data=data
)
dlg.CenterOnScreen()
while 1:
val = dlg.ShowModal()
if val == wx.ID_OK:
values = {}
for field in fields:
try:
values[field] = eval(dlg.textctrls[field].GetValue())
except Exception, e:
msg = wx.MessageDialog(parent, unicode(e),
"Error in field %s" % field,
wx.OK | wx.ICON_INFORMATION
)
msg.ShowModal()
msg.Destroy()
break
else:
return dict([(field, values[field]) for field in fields])
else:
return None
class MyEvtHandler(ogl.ShapeEvtHandler):
"Custom Event Handler for Shapes"
def __init__(self, callback):
ogl.ShapeEvtHandler.__init__(self)
self.callback = callback
def OnLeftClick(self, x, y, keys=0, attachment=0):
shape = self.GetShape()
canvas = shape.GetCanvas()
dc = wx.ClientDC(canvas)
canvas.PrepareDC(dc)
if shape.Selected() and keys & ogl.KEY_SHIFT:
shape.Select(False, dc)
#canvas.Redraw(dc)
canvas.Refresh(False)
else:
redraw = False
shapeList = canvas.GetDiagram().GetShapeList()
toUnselect = []
for s in shapeList:
if s.Selected() and not keys & ogl.KEY_SHIFT:
# If we unselect it now then some of the objects in
# shapeList will become invalid (the control points are
# shapes too!) and bad things will happen...
toUnselect.append(s)
shape.Select(True, dc)
if toUnselect:
for s in toUnselect:
s.Select(False, dc)
##canvas.Redraw(dc)
canvas.Refresh(False)
self.callback()
def OnEndDragLeft(self, x, y, keys=0, attachment=0):
shape = self.GetShape()
ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment)
if not shape.Selected():
self.OnLeftClick(x, y, keys, attachment)
self.callback()
def OnSizingEndDragLeft(self, pt, x, y, keys, attch):
ogl.ShapeEvtHandler.OnSizingEndDragLeft(self, pt, x, y, keys, attch)
self.callback()
def OnMovePost(self, dc, x, y, oldX, oldY, display):
shape = self.GetShape()
ogl.ShapeEvtHandler.OnMovePost(self, dc, x, y, oldX, oldY, display)
self.callback()
if "wxMac" in wx.PlatformInfo:
shape.GetCanvas().Refresh(False)
def OnLeftDoubleClick(self, x, y, keys = 0, attachment = 0):
self.callback("LeftDoubleClick")
def OnRightClick(self, *dontcare):
self.callback("RightClick")
class Element(object):
"Visual class that represent a placeholder in the template"
fields = ['name', 'type',
'x1', 'y1', 'x2', 'y2',
'font', 'size',
'bold', 'italic', 'underline',
'foreground', 'background',
'align', 'text', 'priority',]
def __init__(self, canvas=None, frame=None, zoom=5.0, static=False, **kwargs):
self.kwargs = kwargs
self.zoom = zoom
self.frame = frame
self.canvas = canvas
self.static = static
name = kwargs['name']
kwargs['type']
type = kwargs['type']
x, y, w, h = self.set_coordinates(kwargs['x1'], kwargs['y1'], kwargs['x2'], kwargs['y2'])
text = kwargs['text']
shape = self.shape = ogl.RectangleShape(w, h)
if not static:
shape.SetDraggable(True, True)
shape.SetX(x)
shape.SetY(y)
#if pen: shape.SetPen(pen)
#if brush: shape.SetBrush(brush)
shape.SetBrush(wx.TRANSPARENT_BRUSH)
if type not in ('L', 'B', 'BC'):
if not static:
pen = wx.LIGHT_GREY_PEN
else:
pen = wx.RED_PEN
shape.SetPen(pen)
self.text = kwargs['text']
evthandler = MyEvtHandler(self.evt_callback)
evthandler.SetShape(shape)
evthandler.SetPreviousHandler(shape.GetEventHandler())
shape.SetEventHandler(evthandler)
shape.SetCentreResize(False)
shape.SetMaintainAspectRatio(False)
canvas.AddShape( shape )
@classmethod
def new(Class, parent):
data = dict(name='some_name', type='T',
x1=5.0, y1=5.0, x2=100.0, y2=10.0,
font="Arial", size=12,
bold=False, italic=False, underline=False,
foreground= 0x000000, background=0xFFFFFF,
align="L", text="", priority=0)
data = CustomDialog.do_input(parent, 'New element', Class.fields, data)
if data:
return Class(canvas=parent.canvas, frame=parent, **data)
def edit(self):
"Edit current element (show a dialog box with all fields)"
data = self.kwargs.copy()
x1, y1, x2, y2 = self.get_coordinates()
data.update(dict(name=self.name,
text=self.text,
x1=x1, y1=y1, x2=x2, y2=y2,
))
data = CustomDialog.do_input(self.frame, 'Edit element', self.fields, data)
if data:
self.kwargs.update(data)
self.name = data['name']
self.text = data['text']
x,y, w, h = self.set_coordinates(data['x1'], data['y1'], data['x2'], data['y2'])
self.shape.SetX(x)
self.shape.SetY(y)
self.shape.SetWidth(w)
self.shape.SetHeight(h)
self.canvas.Refresh(False)
self.canvas.GetDiagram().ShowAll(1)
def edit_text(self):
"Allow text edition (i.e. for doubleclick)"
dlg = wx.TextEntryDialog(
self.frame, 'Text for %s' % self.name,
'Edit Text', '')
if self.text:
dlg.SetValue(self.text)
if dlg.ShowModal() == wx.ID_OK:
self.text = dlg.GetValue().encode("latin1")
dlg.Destroy()
def copy(self):
"Return an identical duplicate"
kwargs = self.as_dict()
element = Element(canvas=self.canvas, frame=self.frame, zoom=self.zoom, static=self.static, **kwargs)
return element
def remove(self):
"Erases visual shape from OGL canvas (element must be deleted manually)"
self.canvas.RemoveShape(self.shape)
def move(self, dx, dy):
"Change pdf coordinates (converting to wx internal values)"
x1, y1, x2, y2 = self.get_coordinates()
x1 += dx
x2 += dx
y1 += dy
y2 += dy
x, y, w, h = self.set_coordinates(x1, y1, x2, y2)
self.shape.SetX(x)
self.shape.SetY(y)
def evt_callback(self, evt_type=None):
"Event dispatcher"
if evt_type=="LeftDoubleClick":
self.edit_text()
if evt_type=='RightClick':
self.edit()
# update the status bar
x1, y1, x2, y2 = self.get_coordinates()
self.frame.SetStatusText("%s (%0.2f, %0.2f) - (%0.2f, %0.2f)" %
(self.name, x1, y1, x2, y2))
def get_coordinates(self):
"Convert from wx to pdf coordinates"
x, y = self.shape.GetX(), self.shape.GetY()
w, h = self.shape.GetBoundingBoxMax()
w -= 1
h -= 1
x1 = x/self.zoom - w/self.zoom/2.0
x2 = x/self.zoom + w/self.zoom/2.0
y1 = y/self.zoom - h/self.zoom/2.0
y2 = y/self.zoom + h/self.zoom/2.0
return x1, y1, x2, y2
def set_coordinates(self, x1, y1, x2, y2):
"Convert from pdf to wx coordinates"
x1 = x1 * self.zoom
x2 = x2 * self.zoom
y1 = y1 * self.zoom
y2 = y2 * self.zoom
# shapes seems to be centred, pdf coord not
w = max(x1, x2) - min(x1, x2) + 1
h = max(y1, y2) - min(y1, y2) + 1
x = (min(x1, x2) + w/2.0)
y = (min(y1, y2) + h/2.0)
return x, y, w, h
def text(self, txt=None):
if txt is not None:
if not isinstance(txt,str):
txt = str(txt)
self.kwargs['text'] = txt
self.shape.ClearText()
for line in txt.split('\n'):
self.shape.AddText(unicode(line, "latin1"))
self.canvas.Refresh(False)
return self.kwargs['text']
text = property(text, text)
def set_x(self, x):
self.shape.SetX(x)
self.canvas.Refresh(False)
self.evt_callback()
def set_y(self, y):
self.shape.SetY(y)
self.canvas.Refresh(False)
self.evt_callback()
def get_x(self):
return self.shape.GetX()
def get_y(self):
return self.shape.GetY()
x = property(get_x, set_x)
y = property(get_y, set_y)
def selected(self, sel=None):
if sel is not None:
print "Setting Select(%s)" % sel
self.shape.Select(sel)
return self.shape.Selected()
selected = property(selected, selected)
def name(self, name=None):
if name is not None:
self.kwargs['name'] = name
return self.kwargs['name']
name = property(name, name)
def __contains__(self, k):
"Implement in keyword for searchs"
return k in self.name.lower() or self.text and k in self.text.lower()
def as_dict(self):
"Return a dictionary representation, used by pyfpdf"
d = self.kwargs
x1, y1, x2, y2 = self.get_coordinates()
d.update({
'x1': x1, 'y1': y1, 'x2': x2, 'y2': y2,
'text': self.text})
return d
class AppFrame(wx.Frame):
"OGL Designer main window"
title = "PyFPDF Template Designer (wx OGL)"
def __init__(self):
wx.Frame.__init__( self,
None, -1, self.title,
size=(640,480),
style=wx.DEFAULT_FRAME_STYLE )
sys.excepthook = self.except_hook
self.filename = ""
# Create a toolbar:
tsize = (16,16)
self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT)
artBmp = wx.ArtProvider.GetBitmap
self.toolbar.AddSimpleTool(
wx.ID_NEW, artBmp(wx.ART_NEW, wx.ART_TOOLBAR, tsize), "New")
self.toolbar.AddSimpleTool(
wx.ID_OPEN, artBmp(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize), "Open")
self.toolbar.AddSimpleTool(
wx.ID_SAVE, artBmp(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, tsize), "Save")
self.toolbar.AddSimpleTool(
wx.ID_SAVEAS, artBmp(wx.ART_FILE_SAVE_AS, wx.ART_TOOLBAR, tsize),
"Save As...")
#-------
self.toolbar.AddSeparator()
self.toolbar.AddSimpleTool(
wx.ID_UNDO, artBmp(wx.ART_UNDO, wx.ART_TOOLBAR, tsize), "Undo")
self.toolbar.AddSimpleTool(
wx.ID_REDO, artBmp(wx.ART_REDO, wx.ART_TOOLBAR, tsize), "Redo")
self.toolbar.AddSeparator()
#-------
self.toolbar.AddSimpleTool(
wx.ID_CUT, artBmp(wx.ART_CUT, wx.ART_TOOLBAR, tsize), "Remove")
self.toolbar.AddSimpleTool(
wx.ID_COPY, artBmp(wx.ART_COPY, wx.ART_TOOLBAR, tsize), "Duplicate")
self.toolbar.AddSimpleTool(
wx.ID_PASTE, artBmp(wx.ART_PASTE, wx.ART_TOOLBAR, tsize), "Insert")
self.toolbar.AddSeparator()
self.toolbar.AddSimpleTool(
wx.ID_FIND, artBmp(wx.ART_FIND, wx.ART_TOOLBAR, tsize), "Find")
self.toolbar.AddSeparator()
self.toolbar.AddSimpleTool(
wx.ID_PRINT, artBmp(wx.ART_PRINT, wx.ART_TOOLBAR, tsize), "Print")
self.toolbar.AddSimpleTool(
wx.ID_ABOUT, artBmp(wx.ART_HELP, wx.ART_TOOLBAR, tsize), "About")
self.toolbar.Realize()
self.toolbar.EnableTool(wx.ID_SAVEAS, False)
self.toolbar.EnableTool(wx.ID_UNDO, False)
self.toolbar.EnableTool(wx.ID_REDO, False)
menu_handlers = [
(wx.ID_NEW, self.do_new),
(wx.ID_OPEN, self.do_open),
(wx.ID_SAVE, self.do_save),
(wx.ID_PRINT, self.do_print),
(wx.ID_FIND, self.do_find),
(wx.ID_CUT, self.do_cut),
(wx.ID_COPY, self.do_copy),
(wx.ID_PASTE, self.do_paste),
(wx.ID_ABOUT, self.do_about),
]
for menu_id, handler in menu_handlers:
self.Bind(wx.EVT_MENU, handler, id = menu_id)
sizer = wx.BoxSizer(wx.VERTICAL)
# put stuff into sizer
self.CreateStatusBar()
canvas = self.canvas = ogl.ShapeCanvas( self )
maxWidth = 1500
maxHeight = 2000
canvas.SetScrollbars(20, 20, maxWidth/20, maxHeight/20)
sizer.Add( canvas, 1, wx.GROW )
canvas.SetBackgroundColour("WHITE") #
diagram = self.diagram = ogl.Diagram()
canvas.SetDiagram( diagram )
diagram.SetCanvas( canvas )
diagram.SetSnapToGrid( False )
# apply sizer
self.SetSizer(sizer)
self.SetAutoLayout(1)
self.Show(1)
self.Bind(wx.EVT_CHAR_HOOK, self.on_key_event)
self.elements = []
def on_key_event(self, event):
""" Respond to a keypress event.
We make the arrow keys move the selected object(s) by one pixel in
the given direction.
"""
step = 1
if event.ControlDown():
step = 20
if event.GetKeyCode() == wx.WXK_UP:
self.move_elements(0, -step)
elif event.GetKeyCode() == wx.WXK_DOWN:
self.move_elements(0, step)
elif event.GetKeyCode() == wx.WXK_LEFT:
self.move_elements(-step, 0)
elif event.GetKeyCode() == wx.WXK_RIGHT:
self.move_elements(step, 0)
elif event.GetKeyCode() == wx.WXK_DELETE:
self.do_cut()
else:
event.Skip()
def do_new(self, evt=None):
for element in self.elements:
element.remove()
self.elements = []
# draw paper size guides
for k, (w, h) in [('legal', (216, 356)), ('A4', (210, 297)), ('letter', (216, 279))]:
self.create_elements(
k, 'R', 0, 0, w, h,
size=70, foreground=0x808080, priority=-100,
canvas=self.canvas, frame=self, static=True)
self.diagram.ShowAll( 1 )
def do_open(self, evt):
dlg = wx.FileDialog(
self, message="Choose a file",
defaultDir=os.getcwd(),
defaultFile="invoice.csv",
wildcard="CSV Files (*.csv)|*.csv",
style=wx.OPEN
)
if dlg.ShowModal() == wx.ID_OK:
# This returns a Python list of files that were selected.
self.filename = dlg.GetPaths()[0]
dlg.Destroy()
self.SetTitle(self.filename + " - " + self.title)
self.do_new()
tmp = []
f = open(self.filename)
try:
filedata = f.readlines()
finally:
f.close()
for lno, linea in enumerate(filedata):
if DEBUG: print "processing line", lno, linea
args = []
for i,v in enumerate(linea.split(";")):
if not v.startswith("'"):
v = v.replace(",",".")
else:
v = v#.decode('latin1')
if v.strip()=='':
v = None
else:
v = eval(v.strip())
args.append(v)
tmp.append(args)
# sort by z-order (priority)
for args in sorted(tmp, key=lambda t: t[-1]):
if DEBUG: print args
self.create_elements(*args)
self.diagram.ShowAll( 1 ) #
return True
def do_save(self, evt, filename=None):
try:
from time import gmtime, strftime
ts = strftime("%Y%m%d%H%M%S", gmtime())
os.rename(self.filename, self.filename + ts + ".bak")
except Exception, e:
if DEBUG: print e
pass
def csv_repr(v, decimal_sep="."):
if isinstance(v, float):
return ("%0.2f" % v).replace(".", decimal_sep)
else:
return repr(v)
f = open(self.filename, "w")
try:
for element in sorted(self.elements, key=lambda e:e.name):
if element.static:
continue
d = element.as_dict()
l = [d['name'], d['type'],
d['x1'], d['y1'], d['x2'], d['y2'],
d['font'], d['size'],
d['bold'], d['italic'], d['underline'],
d['foreground'], d['background'],
d['align'], d['text'], d['priority'],
]
f.write(";".join([csv_repr(v) for v in l]))
f.write("\n")
finally:
f.close()
def do_print(self, evt):
# genero el renderizador con propiedades del PDF
from template import Template
t = Template(elements=[e.as_dict() for e in self.elements if not e.static])
t.add_page()
if not t['logo'] or not os.path.exists(t['logo']):
# put a default logo so it doesn't trow an exception
logo = os.path.join(os.path.dirname(__file__), 'tutorial','logo.png')
t.set('logo', logo)
try:
t.render(self.filename +".pdf")
except:
if DEBUG and False:
import pdb;
pdb.pm()
else:
raise
if sys.platform=="linux2":
os.system("evince ""%s""" % self.filename +".pdf")
else:
os.startfile(self.filename +".pdf")
def do_find(self, evt):
# busco nombre o texto
dlg = wx.TextEntryDialog(
self, 'Enter text to search for',
'Find Text', '')
if dlg.ShowModal() == wx.ID_OK:
txt = dlg.GetValue().encode("latin1").lower()
for element in self.elements:
if txt in element:
element.selected = True
print "Found:", element.name
self.canvas.Refresh(False)
dlg.Destroy()
def do_cut(self, evt=None):
"Delete selected elements"
new_elements = []
for element in self.elements:
if element.selected:
print "Erasing:", element.name
element.selected = False
self.canvas.Refresh(False)
element.remove()
else:
new_elements.append(element)
self.elements = new_elements
self.canvas.Refresh(False)
self.diagram.ShowAll( 1 )
def do_copy(self, evt):
"Duplicate selected elements"
fields = ['qty', 'dx', 'dy']
data = {'qty': 1, 'dx': 0.0, 'dy': 5.0}
data = CustomDialog.do_input(self, 'Copy elements', fields, data)
if data:
new_elements = []
for i in range(1, data['qty']+1):
for element in self.elements:
if element.selected:
print "Copying:", element.name
new_element = element.copy()
name = new_element.name
if len(name)>2 and name[-2:].isdigit():
new_element.name = name[:-2] + "%02d" % (int(name[-2:])+i)
else:
new_element.name = new_element.name + "_copy"
new_element.selected = False
new_element.move(data['dx']*i, data['dy']*i)
new_elements.append(new_element)
self.elements.extend(new_elements)
self.canvas.Refresh(False)
self.diagram.ShowAll( 1 )
def do_paste(self, evt):
"Insert new elements"
element = Element.new(self)
if element:
self.canvas.Refresh(False)
self.elements.append(element)
self.diagram.ShowAll( 1 )
def create_elements(self, name, type, x1, y1, x2, y2,
font="Arial", size=12,
bold=False, italic=False, underline=False,
foreground= 0x000000, background=0xFFFFFF,
align="L", text="", priority=0, canvas=None, frame=None, static=False,
**kwargs):
element = Element(name=name, type=type, x1=x1, y1=y1, x2=x2, y2=y2,
font=font, size=size,
bold=bold, italic=italic, underline=underline,
foreground= foreground, background=background,
align=align, text=text, priority=priority,
canvas=canvas or self.canvas, frame=frame or self,
static=static)
self.elements.append(element)
def move_elements(self, x, y):
for element in self.elements:
if element.selected:
print "moving", element.name, x, y
element.x = element.x + x
element.y = element.y + y
def do_about(self, evt):
info = wx.AboutDialogInfo()
info.Name = self.title
info.Version = __version__
info.Copyright = __copyright__
info.Description = (
"Visual Template designer for PyFPDF (using wxPython OGL library)\n"
"Input files are CSV format describing the layout, separated by ;\n"
"Use toolbar buttons to open, save, print (preview) your template, "
"and there are buttons to find, add, remove or duplicate elements.\n"
"Over an element, a double left click opens edit text dialog, "
"and a right click opens edit properties dialog. \n"
"Multiple element can be selected with shift left click. \n"
"Use arrow keys or drag-and-drop to move elements.\n"
"For further information see project webpage:"
)
info.WebSite = ("http://code.google.com/p/pyfpdf/wiki/Templates",
"pyfpdf Google Code Project")
info.Developers = [ __author__, ]
info.License = wordwrap(__license__, 500, wx.ClientDC(self))
# Then we call wx.AboutBox giving it that info object
wx.AboutBox(info)
def except_hook(self, type, value, trace):
import traceback
exc = traceback.format_exception(type, value, trace)
for e in exc: wx.LogError(e)
wx.LogError('Unhandled Error: %s: %s'%(str(type), str(value)))
app = wx.PySimpleApp()
ogl.OGLInitialize()
frame = AppFrame()
app.MainLoop()
app.Destroy()
File diff suppressed because it is too large Load Diff
+459
View File
@@ -0,0 +1,459 @@
# -*- coding: latin-1 -*-
"HTML Renderer for FPDF.py"
__author__ = "Mariano Reingart <reingart@gmail.com>"
__copyright__ = "Copyright (C) 2010 Mariano Reingart"
__license__ = "LGPL 3.0"
# Inspired by tuto5.py and several examples from fpdf.org, html2fpdf, etc.
from fpdf import FPDF
from HTMLParser import HTMLParser
DEBUG = False
def px2mm(px):
return int(px)*25.4/72.0
def hex2dec(color = "#000000"):
if color:
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
return r, g, b
class HTML2FPDF(HTMLParser):
"Render basic HTML to FPDF"
def __init__(self, pdf, image_map, **kwargs):
HTMLParser.__init__(self)
self.image_map = image_map
self.style = {}
self.pre = False
self.href = ''
self.align = ''
self.page_links = {}
self.font_list = ("times","courier", "helvetica")
self.pdf = pdf
self.r = self.g = self.b = 0
self.indent = 0
self.bullet = []
self.font_face="times" # initialize font
self.color=0 # initialize font color
self.set_font(kwargs.get("font","times"), kwargs.get("fontsize",12))
self.table = None # table attributes
self.table_col_width = None # column (header) widths
self.table_col_index = None # current column index
self.td = None # cell attributes
self.th = False # header enabled
self.tr = None
self.theader = None # table header cells
self.tfooter = None # table footer cells
self.thead = None
self.tfoot = None
self.theader_out = self.tfooter_out = False
def width2mm(self, length):
if length[-1]=='%':
total = self.pdf.w - self.pdf.r_margin - self.pdf.l_margin
if self.table['width'][-1]=='%':
total *= int(self.table['width'][:-1])/100.0
return int(length[:-1]) * total / 101.0
else:
return int(length) / 6.0
def handle_data(self, txt):
if self.td is not None: # drawing a table?
if 'width' not in self.td and 'colspan' not in self.td:
l = [self.table_col_width[self.table_col_index]]
elif 'colspan' in self.td:
i = self.table_col_index
colspan = int(self.td['colspan'])
l = self.table_col_width[i:i+colspan]
else:
l = [self.td.get('width','240')]
w = sum([self.width2mm(lenght) for lenght in l])
h = int(self.td.get('height', 0)) / 4 or self.h*1.30
self.table_h = h
border = int(self.table.get('border', 0))
if not self.th:
align = self.td.get('align', 'L')[0].upper()
border = border and 'LR'
else:
self.set_style('B',True)
border = border or 'B'
align = 'C'
bgcolor = hex2dec(self.td.get('bgcolor', self.tr.get('bgcolor', '')))
# parsing table header/footer (drawn later):
if self.thead is not None:
self.theader.append(((w,h,txt,border,0,align), bgcolor))
if self.tfoot is not None:
self.tfooter.append(((w,h,txt,border,0,align), bgcolor))
# check if reached end of page, add table footer and header:
height = h + (self.tfooter and self.tfooter[0][0][1] or 0)
if self.pdf.y+height>self.pdf.page_break_trigger and not self.th:
self.output_table_footer()
self.pdf.add_page()
self.theader_out = self.tfooter_out = False
if self.tfoot is None and self.thead is None:
if not self.theader_out:
self.output_table_header()
self.box_shadow(w, h, bgcolor)
if DEBUG: print "td cell", self.pdf.x, w, txt, "*"
self.pdf.cell(w,h,txt,border,0,align)
elif self.table is not None:
# ignore anything else than td inside a table
pass
elif self.align:
if DEBUG: print "cell", txt, "*"
self.pdf.cell(0,self.h,txt,0,1,self.align[0].upper(), self.href)
else:
txt = txt.replace("\n"," ")
if self.href:
self.put_link(self.href,txt)
else:
if DEBUG: print "write", txt, "*"
self.pdf.write(self.h,txt)
def box_shadow(self, w, h, bgcolor):
if DEBUG: print "box_shadow", w, h, bgcolor
if bgcolor:
fill_color = self.pdf.fill_color
self.pdf.set_fill_color(*bgcolor)
self.pdf.rect(self.pdf.x, self.pdf.y, w, h, 'F')
self.pdf.fill_color = fill_color
def output_table_header(self):
if self.theader:
b = self.b
x = self.pdf.x
self.pdf.set_x(self.table_offset)
self.set_style('B',True)
for cell, bgcolor in self.theader:
self.box_shadow(cell[0], cell[1], bgcolor)
self.pdf.cell(*cell)
self.set_style('B',b)
self.pdf.ln(self.theader[0][0][1])
self.pdf.set_x(self.table_offset)
#self.pdf.set_x(x)
self.theader_out = True
def output_table_footer(self):
if self.tfooter:
x = self.pdf.x
self.pdf.set_x(self.table_offset)
#TODO: self.output_table_sep()
for cell, bgcolor in self.tfooter:
self.box_shadow(cell[0], cell[1], bgcolor)
self.pdf.cell(*cell)
self.pdf.ln(self.tfooter[0][0][1])
self.pdf.set_x(x)
if int(self.table.get('border', 0)):
self.output_table_sep()
self.tfooter_out = True
def output_table_sep(self):
self.pdf.set_x(self.table_offset)
x1 = self.pdf.x
y1 = self.pdf.y
w = sum([self.width2mm(lenght) for lenght in self.table_col_width])
self.pdf.line(x1,y1,x1+w,y1)
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if DEBUG: print "STARTTAG", tag, attrs
if tag=='b' or tag=='i' or tag=='u':
self.set_style(tag,1)
if tag=='a':
self.href=attrs['href']
if tag=='br':
self.pdf.ln(5)
if tag=='p':
self.pdf.ln(5)
if attrs:
self.align=attrs['align'].lower()
if tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
k = (2, 1.5, 1.17, 1, 0.83, 0.67)[int(tag[1])]
self.pdf.ln(5*k)
self.pdf.set_text_color(150,0,0)
self.pdf.set_font_size(12 * k)
if attrs: self.align = attrs.get('align')
if tag=='hr':
self.put_line()
if tag=='pre':
self.pdf.set_font('Courier','',11)
self.pdf.set_font_size(11)
self.set_style('B',False)
self.set_style('I',False)
self.pre = True
if tag=='blockquote':
self.set_text_color(100,0,45)
self.pdf.ln(3)
if tag=='ul':
self.indent+=1
self.bullet.append('\x95')
if tag=='ol':
self.indent+=1
self.bullet.append(0)
if tag=='li':
self.pdf.ln(self.h+2)
self.pdf.set_text_color(190,0,0)
bullet = self.bullet[self.indent-1]
if not isinstance(bullet, basestring):
bullet += 1
self.bullet[self.indent-1] = bullet
bullet = "%s. " % bullet
self.pdf.write(self.h,'%s%s ' % (' '*5*self.indent, bullet))
self.set_text_color()
if tag=='font':
if 'color' in attrs:
self.color = hex2dec(attrs['color'])
self.set_text_color(*color)
self.color = color
if 'face' in attrs and attrs['face'].lower() in self.font_list:
face = attrs.get('face').lower()
self.pdf.set_font(face)
self.font_face = face
if 'size' in attrs:
size = int(attrs.get('size'))
self.pdf.set_font(self.font_face, size=int(size))
self.font_size = size
if tag=='table':
self.table = dict([(k.lower(), v) for k,v in attrs.items()])
if not 'width' in self.table:
self.table['width'] = '100%'
if self.table['width'][-1]=='%':
w = self.pdf.w - self.pdf.r_margin - self.pdf.l_margin
w *= int(self.table['width'][:-1])/100.0
self.table_offset = (self.pdf.w-w)/2.0
self.table_col_width = []
self.theader_out = self.tfooter_out = False
self.theader = []
self.tfooter = []
self.thead = None
self.tfoot = None
self.pdf.ln()
if tag=='tr':
self.tr = dict([(k.lower(), v) for k,v in attrs.items()])
self.table_col_index = 0
self.pdf.set_x(self.table_offset)
if tag=='td':
self.td = dict([(k.lower(), v) for k,v in attrs.items()])
if tag=='th':
self.td = dict([(k.lower(), v) for k,v in attrs.items()])
self.th = True
if self.td['width']:
self.table_col_width.append(self.td['width'])
if tag=='thead':
self.thead = {}
if tag=='tfoot':
self.tfoot = {}
if tag=='img':
if 'src' in attrs:
x = self.pdf.get_x()
y = self.pdf.get_y()
w = px2mm(attrs.get('width', 0))
h = px2mm(attrs.get('height',0))
if self.align and self.align[0].upper() == 'C':
x = (self.pdf.w-x)/2.0 - w/2.0
self.pdf.image(self.image_map(attrs['src']),
x, y, w, h, link=self.href)
self.pdf.set_x(x+w)
self.pdf.set_y(y+h)
if tag=='b' or tag=='i' or tag=='u':
self.set_style(tag, True)
if tag=='center':
self.align = 'Center'
def handle_endtag(self, tag):
#Closing tag
if DEBUG: print "ENDTAG", tag
if tag=='h1' or tag=='h2' or tag=='h3' or tag=='h4':
self.pdf.ln(6)
self.set_font()
self.set_style()
self.align = None
if tag=='pre':
self.pdf.set_font(self.font or 'Times','',12)
self.pdf.set_font_size(12)
self.pre=False
if tag=='blockquote':
self.set_text_color(0,0,0)
self.pdf.ln(3)
if tag=='strong':
tag='b'
if tag=='em':
tag='i'
if tag=='b' or tag=='i' or tag=='u':
self.set_style(tag, False)
if tag=='a':
self.href=''
if tag=='p':
self.align=''
if tag in ('ul', 'ol'):
self.indent-=1
self.bullet.pop()
if tag=='table':
if not self.tfooter_out:
self.output_table_footer()
self.table = None
self.th = False
self.theader = None
self.tfooter = None
self.pdf.ln()
if tag=='thead':
self.thead = None
if tag=='tfoot':
self.tfoot = None
if tag=='tbody':
# draw a line separator between table bodies
self.pdf.set_x(self.table_offset)
self.output_table_sep()
if tag=='tr':
h = self.table_h
if self.tfoot is None:
self.pdf.ln(h)
self.tr = None
if tag=='td' or tag=='th':
if self.th:
if DEBUG: print "revert style"
self.set_style('B', False) # revert style
self.table_col_index += int(self.td.get('colspan','1'))
self.td = None
self.th = False
if tag=='font':
if self.color:
self.pdf.set_text_color(0,0,0)
self.color = None
if self.font_face:
self.set_font('Times',12)
if tag=='center':
self.align = None
def set_font(self, face=None, size=None):
if face:
self.font_face = face
if size:
self.font_size = size
self.h = size / 72.0*25.4
if DEBUG: print "H", self.h
self.pdf.set_font(self.font_face or 'times','',12)
self.pdf.set_font_size(self.font_size or 12)
self.set_style('u', False)
self.set_style('b', False)
self.set_style('i', False)
self.set_text_color()
def set_style(self, tag=None, enable=None):
#Modify style and select corresponding font
if tag:
t = self.style.get(tag.lower())
self.style[tag.lower()] = enable
style=''
for s in ('b','i','u'):
if self.style.get(s):
style+=s
if DEBUG: print "SET_FONT_STYLE", style
self.pdf.set_font('',style)
def set_text_color(self, r=None, g=0, b=0):
if r is None:
self.pdf.set_text_color(self.r,self.g,self.b)
else:
self.pdf.set_text_color(r, g, b)
self.r = r
self.g = g
self.b = b
def put_link(self, url, txt):
#Put a hyperlink
self.set_text_color(0,0,255)
self.set_style('u', True)
self.pdf.write(5,txt,url)
self.set_style('u', False)
self.set_text_color(0)
def put_line(self):
self.pdf.ln(2)
self.pdf.line(self.pdf.get_x(),self.pdf.get_y(),self.pdf.get_x()+187,self.pdf.get_y())
self.pdf.ln(3)
class HTMLMixin():
def write_html(self, text, image_map=lambda x:x, **kwargs):
"Parse HTML and convert it to PDF"
h2p = HTML2FPDF(self,image_map=image_map,**kwargs)
h2p.feed(text)
if __name__=='__main__':
html="""
<H1 align="center">html2fpdf</H1>
<h2>Basic usage</h2>
<p>You can now easily print text mixing different
styles : <B>bold</B>, <I>italic</I>, <U>underlined</U>, or
<B><I><U>all at once</U></I></B>!<BR>You can also insert links
on text, such as <A HREF="http://www.fpdf.org">www.fpdf.org</A>,
or on an image: click on the logo.<br>
<center>
<A HREF="http://www.fpdf.org"><img src="tutorial/logo.png" width="104" height="71"></A>
</center>
<h3>Sample List</h3>
<ul><li>option 1</li>
<ol><li>option 2</li></ol>
<li>option 3</li></ul>
<table border="0" align="center" width="50%">
<thead><tr><th width="30%">Header 1</th><th width="70%">header 2</th></tr></thead>
<tbody>
<tr><td>cell 1</td><td>cell 2</td></tr>
<tr><td>cell 2</td><td>cell 3</td></tr>
</tbody>
</table>
<table border="1">
<thead><tr bgcolor="#A0A0A0"><th width="30%">Header 1</th><th width="70%">header 2</th></tr></thead>
<tfoot><tr bgcolor="#E0E0E0"><td>footer 1</td><td>footer 2</td></tr></tfoot>
<tbody>
<tr><td>cell 1</td><td>cell 2</td></tr>
<tr>
<td width="30%">cell 1</td><td width="70%" bgcolor="#D0D0FF" align='right'>cell 2</td>
</tr>
</tbody>
<tbody><tr><td colspan="2">cell spanned</td></tr></tbody>
<tbody>
""" + """<tr bgcolor="#F0F0F0">
<td>cell 3</td><td>cell 4</td>
</tr><tr bgcolor="#FFFFFF">
<td>cell 5</td><td>cell 6</td>
</tr>""" * 200 + """
</tbody>
</table>
"""
class MyFPDF(FPDF, HTMLMixin):
def header(self):
self.image('tutorial/logo_pb.png',10,8,33)
self.set_font('Arial','B',15)
self.cell(80)
self.cell(30,10,'Title',1,0,'C')
self.ln(20)
def footer(self):
self.set_y(-15)
self.set_font('Arial','I',8)
txt = 'Page %s of %s' % (self.page_no(), self.alias_nb_pages())
self.cell(0,10,txt,0,0,'C')
pdf=MyFPDF()
#First page
pdf.add_page()
pdf.write_html(html)
pdf.output('html.pdf','F')
import os
os.system("evince html.pdf")
+278
View File
@@ -0,0 +1,278 @@
# -*- coding: iso-8859-1 -*-
"PDF Template Helper for FPDF.py"
__author__ = "Mariano Reingart <reingart@gmail.com>"
__copyright__ = "Copyright (C) 2010 Mariano Reingart"
__license__ = "LGPL 3.0"
import sys,os,csv
from fpdf import FPDF
def rgb(col):
return (col // 65536), (col // 256 % 256), (col% 256)
class Template:
def __init__(self, infile=None, elements=None, format='A4', orientation='portrait',
title='', author='', subject='', creator='', keywords=''):
if elements:
self.elements = dict([(v['name'].lower(),v) for v in elements])
self.handlers = {'T': self.text, 'L': self.line, 'I': self.image,
'B': self.rect, 'BC': self.barcode, }
self.pg_no = 0
self.texts = {}
pdf = self.pdf = FPDF(format=format,orientation=orientation, unit="mm")
pdf.set_title(title)
pdf.set_author(author)
pdf.set_creator(creator)
pdf.set_subject(subject)
pdf.set_keywords(keywords)
def parse_csv(self, infile, delimiter=",", decimal_sep="."):
"Parse template format csv file and create elements dict"
keys = ('name','type','x1','y1','x2','y2','font','size',
'bold','italic','underline','foreground','background',
'align','text','priority')
self.elements = {}
f = open(infile, 'rb')
try:
for row in csv.reader(f, delimiter=delimiter):
kargs = {}
for i,v in enumerate(row):
if not v.startswith("'") and decimal_sep!=".":
v = v.replace(decimal_sep,".")
else:
v = v
if v=='':
v = None
else:
v = eval(v.strip())
kargs[keys[i]] = v
self.elements[kargs['name'].lower()] = kargs
finally:
f.close()
def add_page(self):
self.pg_no += 1
self.texts[self.pg_no] = {}
def __setitem__(self, name, value):
if name.lower() in self.elements:
if isinstance(value,unicode):
value = value.encode("latin1","ignore")
else:
value = str(value)
self.texts[self.pg_no][name.lower()] = value
# setitem shortcut (may be further extended)
set = __setitem__
def __getitem__(self, name):
if name.lower() in self.elements:
return self.texts[self.pg_no].get(name.lower(), self.elements[name.lower()]['text'])
def split_multicell(self, text, element_name):
"Divide (\n) a string using a given element width"
pdf = self.pdf
element = self.elements[element_name.lower()]
style = ""
if element['bold']: style += "B"
if element['italic']: style += "I"
if element['underline']: style += "U"
pdf.set_font(element['font'],style,element['size'])
align = {'L':'L','R':'R','I':'L','D':'R','C':'C','':''}.get(element['align']) # D/I in spanish
if isinstance(text, unicode):
text = text.encode("latin1","ignore")
else:
text = str(text)
return pdf.multi_cell(w=element['x2']-element['x1'],
h=element['y2']-element['y1'],
txt=text,align=align,split_only=True)
def render(self, outfile, dest="F"):
pdf = self.pdf
for pg in range(1, self.pg_no+1):
pdf.add_page()
pdf.set_font('Arial','B',16)
pdf.set_auto_page_break(False,margin=0)
for element in sorted(self.elements.values(),key=lambda x: x['priority']):
#print "dib",element['type'], element['name'], element['x1'], element['y1'], element['x2'], element['y2']
element = element.copy()
element['text'] = self.texts[pg].get(element['name'].lower(), element['text'])
if 'rotate' in element:
pdf.rotate(element['rotate'], element['x1'], element['y1'])
self.handlers[element['type'].upper()](pdf, **element)
if 'rotate' in element:
pdf.rotate(0)
return pdf.output(outfile, dest)
def text(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', font="arial", size=10,
bold=False, italic=False, underline=False, align="",
foreground=0, backgroud=65535,
*args, **kwargs):
if text:
if pdf.text_color!=rgb(foreground):
pdf.set_text_color(*rgb(foreground))
if pdf.fill_color!=rgb(backgroud):
pdf.set_fill_color(*rgb(backgroud))
font = font.strip().lower()
if font == 'arial black':
font = 'arial'
style = ""
for tag in 'B', 'I', 'U':
if (text.startswith("<%s>" % tag) and text.endswith("</%s>" %tag)):
text = text[3:-4]
style += tag
if bold: style += "B"
if italic: style += "I"
if underline: style += "U"
align = {'L':'L','R':'R','I':'L','D':'R','C':'C','':''}.get(align) # D/I in spanish
pdf.set_font(font,style,size)
##m_k = 72 / 2.54
##h = (size/m_k)
pdf.set_xy(x1,y1)
pdf.cell(w=x2-x1,h=y2-y1,txt=text,border=0,ln=0,align=align)
#pdf.Text(x=x1,y=y1,txt=text)
def line(self, pdf, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, *args, **kwargs):
if pdf.draw_color!=rgb(foreground):
#print "SetDrawColor", hex(foreground)
pdf.set_draw_color(*rgb(foreground))
#print "SetLineWidth", size
pdf.set_line_width(size)
pdf.line(x1, y1, x2, y2)
def rect(self, pdf, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, backgroud=65535, *args, **kwargs):
if pdf.draw_color!=rgb(foreground):
pdf.set_draw_color(*rgb(foreground))
if pdf.fill_color!=rgb(backgroud):
pdf.set_fill_color(*rgb(backgroud))
pdf.set_line_width(size)
pdf.rect(x1, y1, x2-x1, y2-y1)
def image(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', *args,**kwargs):
pdf.image(text,x1,y1,w=x2-x1,h=y2-y1,type='',link='')
def barcode(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', font="arial", size=1,
foreground=0, *args, **kwargs):
if pdf.draw_color!=rgb(foreground):
pdf.set_draw_color(*rgb(foreground))
font = font.lower().strip()
if font == 'interleaved 2of5 nt':
pdf.interleaved2of5(text,x1,y1,w=size,h=y2-y1)
if __name__ == "__main__":
# generate sample invoice (according Argentina's regulations)
import random
from decimal import Decimal
f = Template(format="A4",
title="Sample Invoice", author="Sample Company",
subject="Sample Customer", keywords="Electronic TAX Invoice")
f.parse_csv(infile="invoice.csv", delimiter=";", decimal_sep=",")
detail = "Lorem ipsum dolor sit amet, consectetur. " * 30
items = []
for i in range(1, 30):
ds = "Sample product %s" % i
qty = random.randint(1,10)
price = round(random.random()*100,3)
code = "%s%s%02d" % (chr(random.randint(65,90)), chr(random.randint(65,90)),i)
items.append(dict(code=code, unit='u',
qty=qty, price=price,
amount=qty*price,
ds="%s: %s" % (i,ds)))
# divide and count lines
lines = 0
li_items = []
for it in items:
qty = it['qty']
code = it['code']
unit = it['unit']
for ds in f.split_multicell(it['ds'], 'item_description01'):
# add item description line (without price nor amount)
li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None))
# clean qty and code (show only at first)
unit = qty = code = None
# set last item line price and amount
li_items[-1].update(amount = it['amount'],
price = it['price'])
obs="\n<U>Detail:</U>\n\n" + detail
for ds in f.split_multicell(obs, 'item_description01'):
li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None))
# calculate pages:
lines = len(li_items)
max_lines_per_page = 24
pages = lines / (max_lines_per_page - 1)
if lines % (max_lines_per_page - 1): pages = pages + 1
# completo campos y hojas
for page in range(1, pages+1):
f.add_page()
f['page'] = 'Page %s of %s' % (page, pages)
if pages>1 and page<pages:
s = 'Continues on page %s' % (page+1)
else:
s = ''
f['item_description%02d' % (max_lines_per_page+1)] = s
f["company_name"] = "Sample Company"
f["company_logo"] = "tutorial/logo.png"
f["company_header1"] = "Some Address - somewhere -"
f["company_header2"] = "http://www.example.com"
f["company_footer1"] = "Tax Code ..."
f["company_footer2"] = "Tax/VAT ID ..."
f['number'] = '0001-00001234'
f['issue_date'] = '2010-09-10'
f['due_date'] = '2099-09-10'
f['customer_name'] = "Sample Client"
f['customer_address'] = "Siempreviva 1234"
# print line item...
li = 0
k = 0
total = Decimal("0.00")
for it in li_items:
k = k + 1
if k > page * (max_lines_per_page - 1):
break
if it['amount']:
total += Decimal("%.6f" % it['amount'])
if k > (page - 1) * (max_lines_per_page - 1):
li += 1
if it['qty'] is not None:
f['item_quantity%02d' % li] = it['qty']
if it['code'] is not None:
f['item_code%02d' % li] = it['code']
if it['unit'] is not None:
f['item_unit%02d' % li] = it['unit']
f['item_description%02d' % li] = it['ds']
if it['price'] is not None:
f['item_price%02d' % li] = "%0.3f" % it['price']
if it['amount'] is not None:
f['item_amount%02d' % li] = "%0.2f" % it['amount']
if pages == page:
f['net'] = "%0.2f" % (total/Decimal("1.21"))
f['vat'] = "%0.2f" % (total*(1-1/Decimal("1.21")))
f['total_label'] = 'Total:'
else:
f['total_label'] = 'SubTotal:'
f['total'] = "%0.2f" % total
f.render("./invoice.pdf")
if sys.platform.startswith("linux"):
os.system("evince ./invoice.pdf")
else:
os.system("./invoice.pdf")