I am trying to produce a PDF document with different odd/even page layouts (to allow asymmetric borders for binding) using Python 2.7 and ReportLab. To further complicate matters I am trying to produce two-columns per page.
def WritePDF(files):
story = []
doc = BaseDocTemplate("Polar.pdf", pagesize=A4, title = "Polar Document 5th Edition")
oddf1 = Frame(doc.leftMargin, doc.bottomMargin, doc.width/2-6, doc.height, id='oddcol1')
oddf2 = Frame(doc.leftMargin+doc.width/2+6, doc.bottomMargin, doc.width/2-6, doc.height, id='oddcol2')
evenf1 = Frame(doc.leftMargin, doc.bottomMargin, doc.width/2-6, doc.height, id='evencol1')
evenf2 = Frame(doc.leftMargin+doc.width/2+6, doc.bottomMargin, doc.width/2-6, doc.height, id='evencol2')
doc.addPageTemplates([PageTemplate(id='EvenTwoC',frames=[evenf1,evenf2],onPage=evenFooter),
PageTemplate(id='OddTwoC', frames=[oddf1, oddf2], onPage=oddFooter)])
...
story.append(Paragraph(whatever, style))
What I can't figure out is how to make ReportLab alternate between right and left (or odd and even) pages. Any suggestions?
I found the solution I guess ! :)
I had to dig into the source code. I found the solution in the file reportlab/platypus/doctemplate.py on line 636. It's not the first time I had to do this, as the documentation is pretty limited...
Now, what I found:
def handle_nextPageTemplate(self,pt):
'''On endPage change to the page template with name or index pt'''
if type(pt) is StringType:
# ... in short, set self._nextPageTemplate
elif type(pt) is IntType:
# ... in short, set self._nextPageTemplate
elif type(pt) in (ListType, TupleType):
#used for alternating left/right pages
#collect the refs to the template objects, complain if any are bad
c = PTCycle()
for ptn in pt:
found = 0
if ptn=='*': #special case name used to short circuit the iteration
c._restart = len(c)
continue
for t in self.pageTemplates:
if t.id == ptn:
c.append(t)
found = 1
if not found:
raise ValueError("Cannot find page template called %s" % ptn)
if not c:
raise ValueError("No valid page templates in cycle")
elif c._restart>len(c):
raise ValueError("Invalid cycle restart position")
#ensure we start on the first one
self._nextPageTemplateCycle = c.cyclicIterator()
else:
raise TypeError("argument pt should be string or integer or list")
And I checked where this self._nextPageTemplateCycle is used, so this is what I think should work (not tested though):
story = []
# ...
# doc.addPageTemplates([...])
story.append(NextPageTemplate(['pageLeft', 'pageRight'])) # this will cycle through left/right/left/right/...
story.append(NextPageTemplate(['firstPage', 'secondPage', '*', 'pageLeft', 'pageRight'])) # this will cycle through first/second/left/right/left/right/...
Add this to story once, when you want to start alternating pages. Use another normal NextPageTemplate to stop this cycle (because in the source, there's a del self._nextPageTemplateCycle if you do that).
Hope it helps, and do say if it works, I can't make sure right now, but I will!
Related
I'm writing a tool that will help make me make custom uvSets on a model that will be linked to ramps(no interpolation) that will be sent to vray_extraTex render elements. these will become mattes and used in the same way as vRay multi mattes. however, I am not able to link the ramp that is my texture for the vray_extraTex to the custom uvSet using pymel.
I can do all this manually in Maya but for some reason, I am missing something for pymel to link the UVs to the ramp. I am testing in a Maya scene with a pSphere that has two uvSets and the second set is active. This code has been stripped down a bit:
def main():
inclusiveSet = None
renderElements =[]
ramps = []
newChannels = ['TestA','TestB','TestC','TestD']
for i, channel in enumerate(newChannels):
modIndex = i % 3 # 0:Red, 1:Green, 2:Blue
shapeNode=pm.PyNode('pSphereShape1')
transformNode=shapeNode.getTransform()
if modIndex == 0: # the first channel in the new Render Element
# make an etex render element
eTexElement = pm.PyNode(pm.mel.eval('vrayAddRenderElement("ExtraTexElement")'))
eTexElement.vray_name_extratex.set('')
eTexElement.vray_explicit_name_extratex.set('empty_empty_empy')
renderElements.append(eTexElement)
# make a ramp
ramp = pm.shadingNode('ramp', asTexture=True, name='eTex_{}_ramp'.format(transformNode.name()))
ramps.append(ramp)
ramp.outColor.connect(eTexElement.vray_texture_extratex)
# make a place2dtexture
place2d = pm.shadingNode('place2dTexture', asUtility=True)
place2d.outUV.connect(ramp.uv)
place2d.translateFrameU.set(len(renderElements) - 1)
# link UVs to ramp
# NOT WORKING
indices = pm.polyUVSet(shapeNode.name(), query=True, allUVSetsIndices=True)
currentUVSet = pm.polyUVSet(shapeNode, query=True, currentUVSet=True )
for i in indices:
if currentUVSet == pm.getAttr("{}.uvSet[{}].uvSetName".format(shapeNode.name(), i)):
pm.uvLink(uvSet='{}.uvSet[{}].uvSetName'.format(shapeNode.name(), i) , texture=ramp)
explicit_name = eTexElement.vray_explicit_name_extratex.get()
nameTokens = explicit_name.split('_')
nameTokens[modIndex] = channel
explicit_name = '_'.join(nameTokens)
eTexElement.vray_explicit_name_extratex.set(explicit_name)
main()
I get no errors but when I check the UV Linking the ramps are still set to map1 uvSet and not the second set that was active.
I expected to see the ramps connected to the uvChooser node and linked to the second uvSet.
I realized while writing this post that maybe I need to attach the ramps to the shader that is assigned to the geo before I can uvlink them with python. I'll try and test that next
so after some more testing, I was able to get it to work after assigning the ramps to a new attr on the objects' shader and with a couple of minor fixes to the code I posted before. fixed:
def connectTexture(layeredTex, shapeNode):
shadingGrps = pm.listConnections(shapeNode,type='shadingEngine')
shaders = pm.ls(pm.listConnections(shadingGrps),materials=1)
shader = shaders[0]
pm.addAttr(shader, longName='eTexLayeredTex', usedAsColor=True, attributeType='float3' )
pm.addAttr(shader, longName='eTexLayeredTexR', attributeType='float', parent='eTexLayeredTex' )
pm.addAttr(shader, longName='eTexLayeredTexG', attributeType='float', parent='eTexLayeredTex' )
pm.addAttr(shader, longName='eTexLayeredTexB', attributeType='float', parent='eTexLayeredTex' )
layeredTex.outColor.connect(shader.eTexLayeredTex)
def main():
inclusiveSet=None
renderElements=[]
ramps=[]
newChannels = ['TestA','TestB','TestC','TestD']
shapeNode=pm.PyNode('pSphereShape1')
transformNode=shapeNode.getTransform()
# make a layeredTexture
layeredTexture = pm.shadingNode('layeredTexture', asTexture=True, name='eTex_{}_layeredTexture'.format(transformNode.name()))
layeredTexture.attr('inputs')[0].color.set(0,0,0)
layeredTexture.attr('inputs')[0].alpha.set(1)
layeredTexture.attr('inputs')[0].blendMode.set(1)
connectTexture(layeredTexture, shapeNode)
for i, channel in enumerate(newChannels):
modIndex = i % 3 # 0:Red, 1:Green, 2:Blue
if modIndex == 0: # the first channel in the new Render Element
# make an etex render element
eTexElement = pm.PyNode(pm.mel.eval('vrayAddRenderElement("ExtraTexElement")'))
eTexElement.vray_name_extratex.set('')
eTexElement.vray_explicit_name_extratex.set('empty_empty_empy')
renderElements.append(eTexElement)
# make a ramp
ramp = pm.shadingNode('ramp', asTexture=True, name='eTex_{}_ramp'.format(transformNode.name()))
ramps.append(ramp)
ramp.interpolation.set(0)
ramp.colorEntryList[0].position.set(0.0)
ramp.colorEntryList[1].position.set((1.0 / 3.0))
ramp.colorEntryList[2].position.set((2.0 / 3.0))
ramp.colorEntryList[0].color.set(1,0,0)
ramp.colorEntryList[1].color.set(0,1,0)
ramp.colorEntryList[2].color.set(0,0,1)
ramp.defaultColor.set(0,0,0)
ramp.outColor.connect(eTexElement.vray_texture_extratex)
ramp.outColor.connect( layeredTexture.attr( 'inputs[{}].color'.format( 1 + i // 3)))
# make a place2dtexture
place2d = pm.shadingNode('place2dTexture', asUtility=True)
place2d.outUV.connect(ramp.uv)
place2d.outUvFilterSize.connect(ramp.uvFilterSize)
place2d.wrapU.set(0)
place2d.wrapV.set(0)
place2d.translateFrameU.set(len(renderElements) - 1)
# link UVs to ramp
indices = pm.polyUVSet(shapeNode.name(), query=True, allUVSetsIndices=True)
currentUVSet = pm.polyUVSet(shapeNode, query=True, currentUVSet=True )[0]
for index in indices:
if currentUVSet == pm.getAttr('{}.uvSet[{}].uvSetName'.format(shapeNode.name(), index)):
pm.uvLink(uvSet='{}.uvSet[{}].uvSetName'.format(shapeNode.name(), index) , texture=ramp)
explicit_name = eTexElement.vray_explicit_name_extratex.get()
nameTokens = explicit_name.split('_')
nameTokens[modIndex] = channel
explicit_name = '_'.join(nameTokens)
eTexElement.vray_explicit_name_extratex.set(explicit_name)
main()
they key was connecting the ramps to a material assigned to the object. Strangely after the uvLinks are made you can delete the connection between the ramps and the material and the uvLinks still work correctly. I posted this in case someone comes across a similar problem
Good Afternoon all,
I've been working on a contact-book program for a school project. I've got all of the underlying code complete. However I've decided to take it one step further and implement a basic interface. I am trying to display all of the contacts using the code snippet below:
elif x==2:
phonebook_data= open(data_path,mode='r',encoding = 'utf8')
if os.stat(data_path)[6]==0:
print("Your contact book is empty.")
else:
for line in phonebook_data:
data= eval(line)
for k,v in sorted(data.items()):
x= (k + ": " + v)
from tkinter import *
root = Tk()
root.title("Contacts")
text = Text(root)
text.insert('1.0', x)
text.pack()
text.update()
root.mainloop()
phonebook_data.close()
The program works, however every contact opens in a new window. I would like to display all of the same information in a single loop. I'm fairly new to tkinter and I apologize if the code is confusing at all. Any help would be greatly appreciated!!
First of all, the top of the snippet could be much more efficient:
phonebook_data= open(data_path,mode='r',encoding = 'utf8') should be changed to
phonebook_data = open(data_path).
Afterwards, just use:
contents = phonebook_data.read()
if contents == "": # Can be shortened to `if not contents:`
print("Your contact book is empty.")
And by the way, it's good practice to close the file as soon as you're done using it.
phonebook_data = open(data_path)
contents = phonebook_data.read()
phonebook_data.close()
if contents == "":
print("Your contact book is empty.")
Now for your graphics issue. Firstly, you should consider whether or not you really need a graphical interface for this application. If so:
# Assuming that the contact book is formatted `Name` `Number` (split by a space)
name_number = []
for line in contents.split("\n"): # Get each line
name, number = line.split()
name_number.append(name + ": " + number) # Append a string of `Name`: `Number` to the list
name_number.sort() # Sort by name
root = Tk()
root.title("Contact Book")
text = Text(root)
text.pack(fill=BOTH)
text.insert("\n".join(name_number))
root.mainloop()
Considering how much I have shown you, it would probably be considered cheating for you to use it. Do some more research into the code though, it didn't seem like the algorithm would work in the first place.
Since yesterday I'm trying to extract the text from some highlighted annotations in one pdf, using python-poppler-qt4.
According to this documentation, looks like I have to get the text using the Page.text() method, passing a Rectangle argument from the higlighted annotation, which I get using Annotation.boundary(). But I get only blank text. Can someone help me? I copied my code below and added a link for the PDF I am using. Thanks for any help!
import popplerqt4
import sys
import PyQt4
def main():
doc = popplerqt4.Poppler.Document.load(sys.argv[1])
total_annotations = 0
for i in range(doc.numPages()):
page = doc.page(i)
annotations = page.annotations()
if len(annotations) > 0:
for annotation in annotations:
if isinstance(annotation, popplerqt4.Poppler.Annotation):
total_annotations += 1
if(isinstance(annotation, popplerqt4.Poppler.HighlightAnnotation)):
print str(page.text(annotation.boundary()))
if total_annotations > 0:
print str(total_annotations) + " annotation(s) found"
else:
print "no annotations found"
if __name__ == "__main__":
main()
Test pdf:
https://www.dropbox.com/s/10plnj67k9xd1ot/test.pdf
Looking at the documentation for Annotations it seems that the boundary property Returns this annotation's boundary rectangle in normalized coordinates. Although this seems a strange decision we can simply scale the coordinates by the page.pageSize().width() and .height() values.
import popplerqt4
import sys
import PyQt4
def main():
doc = popplerqt4.Poppler.Document.load(sys.argv[1])
total_annotations = 0
for i in range(doc.numPages()):
#print("========= PAGE {} =========".format(i+1))
page = doc.page(i)
annotations = page.annotations()
(pwidth, pheight) = (page.pageSize().width(), page.pageSize().height())
if len(annotations) > 0:
for annotation in annotations:
if isinstance(annotation, popplerqt4.Poppler.Annotation):
total_annotations += 1
if(isinstance(annotation, popplerqt4.Poppler.HighlightAnnotation)):
quads = annotation.highlightQuads()
txt = ""
for quad in quads:
rect = (quad.points[0].x() * pwidth,
quad.points[0].y() * pheight,
quad.points[2].x() * pwidth,
quad.points[2].y() * pheight)
bdy = PyQt4.QtCore.QRectF()
bdy.setCoords(*rect)
txt = txt + unicode(page.text(bdy)) + ' '
#print("========= ANNOTATION =========")
print(unicode(txt))
if total_annotations > 0:
print str(total_annotations) + " annotation(s) found"
else:
print "no annotations found"
if __name__ == "__main__":
main()
Additionally, I decided to concatenate the .highlightQuads() to get a better representation of what was actually highlighted.
Please be aware of the explicit <space> I have appended to each quad region of text.
In the example document the returned QString could not be passed directly to print() or str(), the solution to this was to use unicode() instead.
I hope this helps someone as it helped me.
Note: Page rotation may affect the scaling values, I have not been able to test this.
Well, while I don't consider myself an experienced programmer, especially when it comes to multimedia applications, I had to do this image viewer sort of a program that displays images fullscreen, and the images change when the users press <- or -> arrows.
The general idea was to make a listbox where all the images contained in a certain folder (imgs) are shown, and when someone does double click or hits RETURN a new, fullscreen frame is generated containing, at first, the image selected in the listbox but after the user hits the arrows the images change in conecutive order, just like in a regular image viewer.
At the beginning, when the first 15 - 20 images are generated, everything goes well, after that, although the program still works an intermediary, very unpleasant and highly unwanted, effect appears between image generations, where basically some images that got generated previously are displayed quickly on the screen and after this the right, consecutive image appears. On the first apparitions the effect is barelly noticeble, but after a while it takes longer and longer between generations.
Here's the code that runs when someone does double click on a listbox entry:
def lbclick(self, eve):
frm = wx.Frame(None, -1, '')
frm.ShowFullScreen(True)
self.sel = self.lstb.GetSelection() # getting the selection from the listbox
def pressk(eve):
keys = eve.GetKeyCode()
if keys == wx.WXK_LEFT:
self.sel = self.sel - 1
if self.sel < 0:
self.sel = len(self.chk) - 1
imgs() # invocking the function made for displaying fullscreen images when left arrow key is pressed
elif keys == wx.WXK_RIGHT:
self.sel = self.sel + 1
if self.sel > len(self.chk) - 1:
self.sel = 0
imgs() # doing the same for the right arrow
elif keys == wx.WXK_ESCAPE:
frm.Destroy()
eve.Skip()
frm.Bind(wx.EVT_CHAR_HOOK, pressk)
def imgs(): # building the function
imgsl = self.chk[self.sel]
itm = wx.Image(str('imgs/{0}'.format(imgsl)), wx.BITMAP_TYPE_JPEG).ConvertToBitmap() # obtaining the name of the image stored in the list self.chk
mar = itm.Size # Because not all images are landscaped I had to figure a method to rescale them after height dimension, which is common to all images
frsz = frm.GetSize()
marx = float(mar[0])
mary = float(mar[1])
val = frsz[1] / mary
vsize = int(mary * val)
hsize = int(marx * val)
panl = wx.Panel(frm, -1, size = (hsize, vsize), pos = (frsz[0] / 2 - hsize / 2, 0)) # making a panel container
panl.SetBackgroundColour('#000000')
imag = wx.Image(str('imgs/{0}'.format(imgsl)), wx.BITMAP_TYPE_JPEG).Scale(hsize, vsize, quality = wx.IMAGE_QUALITY_NORMAL).ConvertToBitmap()
def destr(eve): # unprofessionaly trying to destroy the panel container when a new image is generated hopeing the unvanted effect, with previous generated images will disappear. But it doesn't.
keycd = eve.GetKeyCode()
if keycd == wx.WXK_LEFT or keycd == wx.WXK_RIGHT:
try:
panl.Destroy()
except:
pass
eve.Skip()
panl.Bind(wx.EVT_CHAR_HOOK, destr) # the end of my futile tries
if vsize > hsize: # if the image is portrait instead of landscaped I have to put a black image as a container, otherwise in the background the previous image will remain, even if I use Refresh() on the container (the black bitmap named fundal)
intermed = wx.Image('./res/null.jpg', wx.BITMAP_TYPE_JPEG).Scale(frsz[0], frsz[1]).ConvertToBitmap()
fundal = wx.StaticBitmap(frm, 101, intermed)
stimag = wx.StaticBitmap(fundal, -1, imag, size = (hsize, vsize), pos = (frsz[0] / 2 - hsize / 2, 0))
fundal.Refresh()
stimag.SetToolTip(wx.ToolTip('Esc = exits fullscreen\n<- -> arrows = quick navigation'))
def destr(eve): # the same lame attempt to destroy the container in the portarit images situation
keycd = eve.GetKeyCode()
if keycd == wx.WXK_LEFT or keycd == wx.WXK_RIGHT:
try:
fundal.Destroy()
except:
pass
eve.Skip()
frm.Bind(wx.EVT_CHAR_HOOK, destr)
else: # the case when the images are landscape
stimag = wx.StaticBitmap(panl, -1, imag)
stimag.Refresh()
stimag.SetToolTip(wx.ToolTip('Esc = exits fullscreen\n<- -> arrows = quick navigation'))
imgs() # invocking the function imgs for the situation when someone does double click
frm.Center()
frm.Show(True)
Thanks for any advice in advance.
Added later:
The catch is I'm trying to do an autorun presentation for a DVD with lots of images on it. Anyway it's not necessarely to make the above piece of code work properly if there are any other options. I've already tried to use windows image viewer, but strangely enough it doesn't recognizes relative paths and when I do this
path = os.getcwd() # getting the path of the current working directory
sel = listbox.GetSelection() # geting the value of the current selection from the list box
imgname = memlist[sel] # retrieving the name of the images stored in a list, using the listbox selection, that uses as choices the same list
os.popen(str('rundll32.exe C:\WINDOWS\System32\shimgvw.dll,ImageView_Fullscreen {0}/imgsdir/{1}'.format(path, imgname))) # oepning the images up with bloody windows image viewer
it opens the images only when my program is on the hard disk, if it's on a CD / image drive it doesn't do anything.
There's an image viewer included with the wxPython demo package. I also wrote a really simple one here. Either one should help you in your journey. I suspect that you're not reusing your panels/frames and instead you're seeing old ones that were never properly destroyed.
I'm trying to add a simple "page x of y" to a report made with ReportLab.. I found this old post about it, but maybe six years later something more straightforward has emerged? ^^;
I found this recipe too, but when I use it, the resulting PDF is missing the images..
I was able to implement the NumberedCanvas approach from ActiveState. It was very easy to do and did not change much of my existing code. All I had to do was add that NumberedCanvas class and add the canvasmaker attribute when building my doc. I also changed the measurements of where the "x of y" was displayed:
self.doc.build(pdf)
became
self.doc.build(pdf, canvasmaker=NumberedCanvas)
doc is a BaseDocTemplate and pdf is my list of flowable elements.
use doc.multiBuild
and in the page header method (defined by "onLaterPages="):
global TOTALPAGES
if doc.page > TOTALPAGES:
TOTALPAGES = doc.page
else:
canvas.drawString(270 * mm, 5 * mm, "Seite %d/%d" % (doc.page,TOTALPAGES))
Just digging up some code for you, we use this:
SimpleDocTemplate(...).build(self.story,
onFirstPage=self._on_page,
onLaterPages=self._on_page)
Now self._on_page is a method that gets called for each page like:
def _on_page(self, canvas, doc):
# ... do any additional page formatting here for each page
print doc.page
I came up with a solution for platypus, that is easier to understand (at least I think it is). You can manually do two builds. In the first build, you can store the total number of pages. In the second build, you already know it in advance. I think it is easier to use and understand, because it works with platypus level event handlers, instead of canvas level events.
import copy
import io
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
styles = getSampleStyleSheet()
Title = "Hello world"
pageinfo = "platypus example"
total_pages = 0
def on_page(canvas, doc: SimpleDocTemplate):
global total_pages
total_pages = max(total_pages, doc.page)
canvas.saveState()
canvas.setFont('Times-Roman', 9)
canvas.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, total_pages))
canvas.restoreState()
Story = [Spacer(1, 2 * inch)]
style = styles["Normal"]
for i in range(100):
bogustext = ("This is Paragraph number %s. " % i) * 20
p = Paragraph(bogustext, style)
Story.append(p)
Story.append(Spacer(1, 0.2 * inch))
# You MUST use a deep copy of the story!
# https://mail.python.org/pipermail/python-list/2022-March/905728.html
# First pass
with io.BytesIO() as out:
doc = SimpleDocTemplate(out)
doc.build(copy.deepcopy(Story), onFirstPage=on_page, onLaterPages=on_page)
# Second pass
with open("test.pdf", "wb+") as out:
doc = SimpleDocTemplate(out)
doc.build(copy.deepcopy(Story), onFirstPage=on_page, onLaterPages=on_page)
You just need to make sure that you always render a deep copy of your original story. Otherwise it won't work. (You will either get an empty page as the output, or a render error telling that a Flowable doesn't fit in the frame.)