I am doing a report in python reportlab. And I want to align some text from the right side. But each line of text has a different length, and I have to set coordinates manually. How can I do it dynamically?
pdf.setFont('Helvetica', 9)
pdf.drawString(400,600,"Rechnungsdatum: "+today)
pdf.drawString(390,590,"Leistungserbringung: "+master_data.loc[master_data['company']==company]['Leistungserbringung'][0]) #cus_specific
pdf.drawString(343,580,"Leistungszeitraum: "+start+" - "+end)
pdf.drawString(372,570,"Rechnungsnummer: "+master_data.loc[master_data['company']==company]['Rechnungsnummer'][0]) #cus_specific
pdf.drawString(398,560,"Lieferantennummer: "+master_data.loc[master_data['company']==company]['Lieferantennummer'][0]) #cus_specific
pdf.drawString(432,550,"Zahlungsziel: " +str((date.today().replace(day=1) - timedelta(days=1)).day)+ " Tage")
That's how it looks like:
[]
I want all lines to be aligned from the right side for a beautiful view. I am using Canvas and drawing each string via drawString(). Any help will be appreciated)
You can get canvas width using Canvas._pagesize[0], Canvas object has drawRightString method for right-aligning lines.
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.units import inch, mm, cm, pica
from datetime import date, timedelta
if __name__ == "__main__":
pdf = Canvas("output.pdf")
pdf.setFont('Helvetica', 9)
master_data = ...
start = ...
end = ...
company = ...
today = ...
lines = [
"Rechnungsdatum: "+today,
"Leistungserbringung: "+master_data.loc[master_data['company']==company]['Leistungserbringung'][0],
"Leistungszeitraum: "+start+" - "+end,
"Rechnungsnummer: "+master_data.loc[master_data['company']==company]['Rechnungsnummer'][0],
"Lieferantennummer: "+master_data.loc[master_data['company']==company]['Lieferantennummer'][0],
"Zahlungsziel: " +str((date.today().replace(day=1) - timedelta(days=1)).day)+ " Tage",
]
ys = [600,590,580,570,560,550]
width = pdf._pagesize[0]
padding = 10 * mm
for y, line in zip(ys, lines):
pdf.drawRightString(width - padding, y, line)
pdf.save()
Related
I want to convert an xlsx with Python. I used the modules tablib and xtopdf to build a well structured table. Works excellent! Unfortunately the content does not fit on one pdf page. So I wanted to change the pagesize and format to horizontal A3. But I don't know how that could work. My code:
import random
import tablib
from openpyxl import load_workbook
from xtopdf import PDFWriter
from pyPdf import PdfFileWriter, PdfFileReader
workbook = load_workbook('C:/Users/user1/Testexcel.xlsx', guess_types=True, data_only=True)
worksheet = workbook.get_sheet_by_name('Testsheet')
ws_range = worksheet.iter_rows('A4:H6')
# Helper function to output a string to both screen and PDF.
def print_and_write(pw, strng):
print strng
pw.writeLine(strng)
# Create an empty Dataset and set its headers.
data = tablib.Dataset()
data.headers = ['col1', 'col2', 'col3', 'col4']
widths = [30, 20, 10, 20] # Display widths for columns.
for row in ws_range:
col1 = str(row[0].value)
col2 = str(row[1].value)
col3 = str(row[2].value)
col4 = str(row[3].value)
columns = [col1, col2, col3, col4]
row = [ str(col).center(widths[idx]) for idx, col in enumerate(columns) ]
data.append(row)
# Set up the PDFWriter.
pw = PDFWriter('C:/Users/user1/Test.pdf')
pw.setFont('Courier', 10)
pw.setHeader('Test')
pw.setFooter('Test')
# Generate header and data rows as strings; output them to screen and PDF.
separator = '-' * sum(widths)
print_and_write(pw, separator)
# Output headers
header_strs = [ header.center(widths[idx]) for idx, header in enumerate(data.headers) ]
print_and_write(pw, ''.join(header_strs))
print_and_write(pw, separator)
# Output data
for row in data:
print_and_write(pw, ''.join(row))
print_and_write(pw, separator)
pw.close()
Found out that the PDFWriter from xtopdf itself instanciates an canvas object of the reportlab library. In the canvas class an attribute pagesize is declared which is setted by default to 'A4'. But if I change the entry to 'A3' the result pdf still is in 'A4'.
class Canvas(textobject._PDFColorSetter):
from reportlab.pdfgen import canvas
c = canvas.Canvas("hello.pdf")
from reportlab.lib.units import inch
# move the origin up and to the left
c.translate(inch,inch)
# define a large font
c.setFont("Helvetica", 80)
# choose some colors
c.setStrokeColorRGB(0.2,0.5,0.3)
c.setFillColorRGB(1,0,1)
# draw a rectangle
c.rect(inch,inch,6*inch,9*inch, fill=1)
# make text go straight up
c.rotate(90)
# change color
c.setFillColorRGB(0,0,0.77)
# say hello (note after rotate the y coord needs to be negative!)
c.drawString(3*inch, -3*inch, "Hello World")
c.showPage()
c.save()
"""
def __init__(self,filename,
pagesize='A3',
bottomup = 1,
pageCompression=None,
encoding = None,
invariant = None,
verbosity=0):
"""Create a canvas of a given size. etc.
You may pass a file-like object to filename as an alternative to
a string.
Most of the attributes are private - we will use set/get methods
as the preferred interface. Default page size is A4."""
if pagesize is None: pagesize = 'A3'
if encoding is None: encoding = rl_config.defaultEncoding
if invariant is None: invariant = rl_config.invariant
self._filename = filename
self._encodingName = encoding
self._doc = pdfdoc.PDFDocument(encoding,
compression=pageCompression,
invariant=invariant, filename=filename)
#this only controls whether it prints 'saved ...' - 0 disables
self._verbosity = verbosity
#this is called each time a page is output if non-null
self._onPage = None
self._pagesize = pagesize
self._pageRotation = 0
#self._currentPageHasImages = 0
self._pageTransition = None
self._pageDuration = None
self._destinations = {} # dictionary of destinations for cross indexing.
self.setPageCompression(pageCompression)
self._pageNumber = 1 # keep a count
#self3 = [] #where the current page's marking operators accumulate
# when we create a form we need to save operations not in the form
self._codeStack = []
self._restartAccumulators() # restart all accumulation state (generalized, arw)
self._annotationCount = 0
self._outlines = [] # list for a name tree
self._psCommandsBeforePage = [] #for postscript tray/font commands
self._psCommandsAfterPage = [] #for postscript tray/font commands
#PostScript has the origin at bottom left. It is easy to achieve a top-
#down coord system by translating to the top of the page and setting y
#scale to -1, but then text is inverted. So self.bottomup is used
#to also set the text matrix accordingly. You can now choose your
#drawing coordinates.
self.bottomup = bottomup
self.imageCaching = rl_config.defaultImageCaching
self._make_preamble()
self.init_graphics_state()
self.state_stack = []
edit: I think the changes in the reportlab module are not accepted by the system. Tried to remove the dictionary reportlab and tried to import it then in the commandline. Ironically it works ylthough python should not find that module anymore.
try this
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
c = canvas.Canvas("hello.pdf", pagesize = (297 * mm, 420 * mm))
# or (420 * mm, 297 * mm) if you want it in portrait format
# values for inch: 11.69 * inch , 16.53 * inch
#the following would create an empty page
c.showPage()
c.save()
Just forked a project named xtopdf at bitbucket and made the following change:
##------------------------ PDFWriter.__init__ ----------------------------
- def __init__(self, pdf_fn):
+ def __init__(self, pdf_fn, pagesize='A4'):
'''
Constructor.
"pdf_fn" arg is the name of the PDF file to be created.
'''
self.__pdf_fn = pdf_fn # file name of PDF file
- self.__canv = canvas.Canvas(pdf_fn) # canvas to write on
+ self.__canv = canvas.Canvas(pdf_fn, pagesize) # canvas to write on
self.__font_name = None # font name
self.__font_size = None # font size
self.__header_str = None # header string (partial)
Can you try it? use pw = PDFWriter('C:/Users/user1/Test.pdf', 'A3').
I have to create PDF file in which need to add lines at bottom left like footer.
Following code is working:
import StringIO
from reportlab.pdfgen import canvas
import uuid
def test(pdf_file_name="abc.pdf", pdf_size=(432, 648), font_details=("Times-Roman", 9)):
# create a new PDF with Reportla
text_to_add = "I am writing here.."
new_pdf = "test_%s.pdf"%(str(uuid.uuid4()))
packet = StringIO.StringIO()
packet.seek(0)
c = canvas.Canvas(pdf_file_name, pagesize = pdf_size)
#- Get the length of text in a PDF.
text_len = c.stringWidth(text_to_add, font_details[0], font_details[1])
#- take margin 20 and 20 in both axis
#- Adjust starting point on x axis according to text_len
x = pdf_size[0]-20 - text_len
y = 20
#- set font.
c.setFont(font_details[0], font_details[1])
#- write text,
c.drawString(x, y, text_to_add)
c.showPage()
c.save()
return pdf_file_name
Now if text have multiple lines then this is not working because length of text is greater than width of Page size. Understood.
I try with Frame and paragraph but still can not write text in correct position in a PDF
Following is code:
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Paragraph
styles = getSampleStyleSheet()
styleN = styles['Normal']
styleH = styles['Heading1']
def footer(canvas, doc):
canvas.saveState()
P = Paragraph("This is a multi-line footer. It goes on every page. " * 10, styleN)
w, h = P.wrap(doc.width, doc.bottomMargin)
print "w, h:", w, h
print "doc.leftMargin:", doc.leftMargin
P.drawOn(canvas, 10, 30)
canvas.restoreState()
def test():
doc = BaseDocTemplate('test.pdf', pagesize=(432, 648))
print "doc.leftMargin:", doc.leftMargin
print "doc.bottomMargin:", doc.bottomMargin
print "doc.width:", doc.width
print "doc.height:", doc.height
frame = Frame(10, 50, 432, 648, id='normal')
template = PageTemplate(id='test', frames=frame, onPage=footer)
doc.addPageTemplates([template])
text = []
for i in range(1):
text.append(Paragraph("", styleN))
doc.build(text)
Not understand why size of page change, because I set (432, 648) but is show (288.0, 504.0)
doc.leftMargin: 72.0
doc.bottomMargin: 72.0
doc.width: 288.0
doc.height: 504.0
Also Frame size:
w, h: 288.0 96
doc.leftMargin: 72.0
Do not know how to fix this issue.
I refer this link
First the mystery regarding the doc.width, the doc.width isn't the actual width of the document. It is the width of the area between the margins so in this case doc.width + doc.leftMargin + doc.rightMargin equals the width of the actual page.
Now back to why the footer did not span the entire width of the page as you wanted. This is because of the same issue as described above, namely doc.width isn't the actual paper width.
Assuming you want the footer to span the entire page
def footer(canvas, doc):
canvas.saveState()
P = Paragraph("This is a multi-line footer. It goes on every page. " * 10, styleN)
# Notice the letter[0] which is the width of the letter page size
w, h = P.wrap(letter[0] - 20, doc.bottomMargin)
P.drawOn(canvas, 10, 10)
canvas.restoreState()
Assuming you want the footer to span the width of the writable area
Note: the margins on the default setting are pretty big so that is why there is so much empty space on the sides.
def footer(canvas, doc):
canvas.saveState()
P = Paragraph("This is a multi-line footer. It goes on every page. " * 10, styleN)
w, h = P.wrap(doc.width, doc.bottomMargin)
print "w, h:", w, h
print "doc.leftMargin:", doc.leftMargin
P.drawOn(canvas, doc.leftMargin, 10)
canvas.restoreState()
EDIT:
As it might be useful to know where the normal text should start. We need to figure out the height of our footer. Under normal circumstances we cannot use P.height as it depends on the width of the text, calling it will raise a AttributeError.
In our case we actually are able to get the height of the footer either directly from P.wrap (the h) or by calling P.height after we have called P.wrap.
By starting our Frame at the height of the footer we will never have overlapping text. Yet it is important to remember to set the height of the Frame to doc.height - footer.height to ensure the text won't be placed outside the page.
I want to select features and to zoom on them and do all these steps using PyQgis.
And I'm able to do both of them separatly but it doesn't seems to work when I try to mix the two of them.
Both of the codes I use for them are from the internet. Here's what I use to select features of a layer :
from qgis.core import *
import qgis.utils
lyrMap = QgsVectorLayer('C:/someplace', 'MapName', 'ogr')
QgsMapLayerRegistry.instance().addMapLayer(lyrMap)
expr = QgsExpression("'Attribute' IS NOT NULL")
it = lyrMap.getFeatures(QgsFeatureRequest(expr))
ids = [i.id() for i in it] #select only the features for which the expression is true
lyrMap.setSelectedFeatures(ids)
And it seems to do the trick as features appear selected on QGis.
In order to zoom the code is much more simple, it's just :
canvas = qgis.utils.iface.mapCanvas()
canvas.zoomToSelected(lyrMap)
But it seems that canvas doesn't consider that there's a selection on lyrMap and simply do nothing. I've tried to do the selection manually in QGis, and then zoom using zoomToSelected, and it worked.
But my objective is to do it without needing to do the selection manually...
Note : I don't think that's the issue, but the attribute I'm doing the selection on is from a join between lyrMap and another layer (I didn't put the code here because I don't think it's linked).
Thanks in advances for answers, clues or anything really :) !
This is working for my plugin. I am using python 2.7 and QGIS 1.8 and 2.0.1.You can use this code after including using vector file and adding it to the registry.
self.rubberBand = None
#create vertex marker for point..older versons..
self.vMarker = None
#add rubberbands
self.crossRb = QgsRubberBand(iface.mapCanvas(),QGis.Line)
self.crossRb.setColor(Qt.black)
def pan(self):
print "pan button clicked!"
x = self.dlg.ui.mTxtX.text()
y = self.dlg.ui.mTxtY.text()
if not x:
return
if not y:
return
print x + "," + y
canvas = self.canvas
currExt = canvas.extent()
canvasCenter = currExt.center()
dx = float(x) - canvasCenter.x()
dy = float(y) - canvasCenter.y()
xMin = currExt.xMinimum() + dx
xMax = currExt.xMaximum() + dx
yMin = currExt.yMinimum() + dy
yMax = currExt.yMaximum() + dy
newRect = QgsRectangle(xMin,yMin,xMax,yMax)
canvas.setExtent(newRect)
pt = QgsPoint(float(x),float(y))
self.zoom(pt)
canvas.refresh()
def zoom(self,point):
canvas = self.canvas
currExt = canvas.extent()
leftPt = QgsPoint(currExt.xMinimum(),point.y())
rightPt = QgsPoint(currExt.xMaximum(),point.y())
topPt = QgsPoint(point.x(),currExt.yMaximum())
bottomPt = QgsPoint(point.x(),currExt.yMinimum())
horizLine = QgsGeometry.fromPolyline( [ leftPt , rightPt ] )
vertLine = QgsGeometry.fromPolyline( [ topPt , bottomPt ] )
self.crossRb.reset(QGis.Line)
self.crossRb.addGeometry(horizLine,None)
self.crossRb.addGeometry(vertLine,None)
if QGis.QGIS_VERSION_INT >= 10900:
rb = self.rubberBand
rb.reset(QGis.Point)
rb.addPoint(point)
else:
self.vMarker = QgsVertexMarker(self.canvas)
self.vMarker.setIconSize(10)
self.vMarker.setCenter(point)
self.vMarker.show()
# wait .5 seconds to simulate a flashing effect
QTimer.singleShot(500,self.resetRubberbands)
def resetRubberbands(self):
print "resetting rubberbands.."
canvas = self.canvas
if QGis.QGIS_VERSION_INT >= 10900:
self.rubberBand.reset()
else:
self.vMarker.hide()
canvas.scene().removeItem(self.vMarker)
self.crossRb.reset()
print "completed resetting.."
I am new to reportlab lib, I am learning it simultaneously working on a college project.
I have created a desktop application in wxpython, which result in saving data in PDF.
I want to add 2 lines in my pdf. Where line starts with a user input called name, then some words, again at 2nd line some words, user name and then again some words...
I tried use some of Paragraph and canvas methods and classes but I wasn't able to get the desired output.
Desired output:
Alex is working on college project.
reportlab is very good lib, Alex liked it.
My code:
import os
import reportlab
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.pdfmetrics import registerFontFamily
from reportlab.pdfbase.ttfonts import TTFont
# Registered font family
pdfmetrics.registerFont(TTFont('Vera', 'Vera.ttf'))
pdfmetrics.registerFont(TTFont('VeraBd', 'VeraBd.ttf'))
pdfmetrics.registerFont(TTFont('VeraIt', 'VeraIt.ttf'))
pdfmetrics.registerFont(TTFont('VeraBI', 'VeraBI.ttf'))
# Registered fontfamily
registerFontFamily('Vera',normal='Vera',bold='VeraBd',italic='VeraIt',boldItalic='VeraBI')
# Output pdf file name.
can = canvas.Canvas("Bold_Trail.pdf", pagesize=A4)
# Setfont for whole pdf.
can.setFont('Vera', 12)
# student name variable.
student_name ="Alex"
# Content.
line1 = " is working on college project."
line2 = "Reportlab is very good lib, "
line3 = " liked it.<br>"
# Joining whole content together.
content = "<strong>" + student_name + "</strong>" + line1
content2 = line2 + "<strong>"+student_name + "</strong>" + line3
# drawString location calculation.
x = 0; y = 8.5 * 72
# First string.
can.drawString(x,y, content)
y = y - 72
# Second String.
can.drawString(x,y, content2)
# Create PDF.
can.save()
Is there any other way except using XML method <strong> or <b> that do not work in the above program?
The words should remain on one line.
You could use the setFont method of the canvas object, to set the font to Bold when needed, and Normal otherwise.
* UPDATE *
In order to calculate the right value for x, you can use the stringWidth method, that calculates the length of the string given its content, the font name and the font size. You will have to import it from reportlab.pdfbase.pdfmetrics:
[...]
from reportlab.pdfbase.pdfmetrics import stringWidth
[...]
# student name variable.
student_name = 'Alex'
# Content.
line1 = " is working on college project."
line2 = "Reportlab is very good lib, "
line3 = " liked it"
# drawString location calculation.
x = 0
y = 8.5 * 72
# First string.
can.setFont('Helvetica-Bold', 8)
can.drawString(x, y, student_name)
can.setFont('Helvetica', 8)
textWidth = stringWidth(student_name, 'Helvetica-Bold', 8)
x += textWidth + 1
can.drawString(x, y, line1)
y = y - 72
# Second String.
x = 0
can.setFont('Helvetica', 8)
can.drawString(x, y, line2)
textWidth = stringWidth(line2, 'Helvetica', 8)
x += textWidth + 1
can.setFont('Helvetica-Bold', 8)
can.drawString(x, y, student_name)
textWidth = stringWidth(student_name, 'Helvetica-Bold', 8)
x += textWidth + 1
can.setFont('Helvetica', 8)
can.drawString(x, y, line3)
# Create PDF.
can.save()
Or you could have a look at ParagraphStyle and Paragraph (from reportlab.lib.styles import ParagraphStyle, from reportlab.platypus import Paragraph) but I am not sure if you can concatenate two different styles in the same string.
I found a way to format the Paragraph text with XML tags. Firstly you need to register the font family then it should work. Below I downloaded a group of .ttf files as an example and registered them, after that, the XML tags worked properly.
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase.pdfmetrics import registerFontFamily
pdfmetrics.registerFont(TTFont('OpenSansR', 'OpenSans-Regular.ttf'))
pdfmetrics.registerFont(TTFont('OpenSansL', 'OpenSans-Light.ttf'))
pdfmetrics.registerFont(TTFont('OpenSansB', 'OpenSans-Bold.ttf'))
registerFontFamily('OpenSans', normal='OpenSansR', bold='OpenSansB', italic='OpenSansL', boldItalic='OpenSansB')
I'm using pyBarcode to generate PNGs and the number below the barcode is getting cut off on the right. How do I nudge it left a few pixels?
According to the documentation I need to do something like this:
barcode.writer.BaseWriter(paint_text=my_callback)
And define a callback like this:
my_callback(xpos, ypos)
and:
use self.text as text
How exactly do I apply all of that to my Django view (below)?
def barcode(request):
import barcode
from barcode.writer import ImageWriter
from cStringIO import StringIO
def mm2px(mm, dpi=300):
return (mm * dpi) / 25.4
class MyImageWriter(ImageWriter):
def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
width = 2 * self.quiet_zone + modules_per_line * self.module_width
height = 1.0 + self.module_height * number_of_lines
if self.text:
height += (self.font_size + self.text_distance) / 3
return int(mm2px(width, dpi)), int(mm2px(height, dpi))
f = BarcodeForm(request.GET)
if f.is_valid():
try:
i = StringIO()
bc_factory = barcode.get_barcode_class(f.PYBARCODE_TYPE[f.cleaned_data['barcode_type']])
bc_factory.default_writer_options['quiet_zone'] = 1.0
bc_factory.default_writer_options['text_distance'] = 1.0
bc_factory.default_writer_options['module_height'] = 15.0
bc_factory.default_writer_options['module_width'] = 0.3
bc_factory.default_writer_options['font_size'] = 46
bc = bc_factory(f.cleaned_data['text'], writer=MyImageWriter())
bc.write(i)
return HttpResponse(i.getvalue(), mimetype='image/png')
except Exception, e:
return HttpResponseBadRequest(str(e))
else:
return HttpResponseBadRequest('Missing text or unsupported barcode type: %s' % f.errors)
Edit: After answering I noticed you've got a factory that's setting the quiet_zone to 1.0. Change that back to 6.5 and I'd imagine it will look fine.
Edit2: I misunderstood the exact problem you were experiencing.
For whatever reason the author of pyBarcode is putting the text centered in the middle of the bar code. When the render method calls _paint_text() it passes in xpos/2, which sets it in the middle of the barcode. I guess this is okay with the default font he's using, but when you increased the font, it no longer fits.
Instead I was able to place it on the left side, by overriding the _paint_text() method. In the last line below, the variable pos is just a tuple containing an (x,y) coordinate that tells PIL where to draw the text on the bar code. So I've made sure x is lined up with the bar code. If you need it right aligned, you could play around with the xpos variable to get it where you need.
Give this a shot:
class MyImageWriter(ImageWriter):
def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
width = 2 * self.quiet_zone + modules_per_line * self.module_width
height = 1.0 + self.module_height * number_of_lines
if self.text:
height += (self.font_size + self.text_distance) / 3
return int(mm2px(width, dpi)), int(mm2px(height, dpi))
def _paint_text(self, xpos, ypos):
# this should align your font to the left side of the bar code:
xpos = self.quiet_zone
pos = (mm2px(xpos, self.dpi), mm2px(ypos, self.dpi))
font = ImageFont.truetype(FONT, self.font_size)
self._draw.text(pos, self.text, font=font, fill=self.foreground)