Problems Creating Multiple Anvil PDF's

Hi There!

First of all i love the simplicity which the Anvil PDF creation offers, thus I hope to find a soltion which allows me to stay with anvil pdf’s. Any suggestions appreciated.

Usecase
I have a list of offers which can be printed. Currently there is a button in each row which lets you print the offer. This is already used in Production Version and works pretty good.
Now we want to be able to select multiple orders and print all selected Orders (lets say up to 100 Orders). Thus no loop over each because this would result in 100 printing dialouges.

I have thought about generating the pdf when the order is saved, currently this would result in too much usless PDF creation since most orders are not even printed and changed quite often before finally printed.


(Example of the UI, orders are selected and all selected orders then printed, i.e. one document)

Problems with my Attempts
I’m not able to share the application, but my current implementation looks like this:
https://anvil.works/build#clone:6FBGNTPHWXFPXMVM=VINOWOPWVQKKHTFAV5Z6UUKR

General Performance
Creating a fairly simple Order 1-3 Pages takes me 5 seconds. Including Roundtrip time this can be too long. Even in the VERY Simple example above the creation itself takes 2 seconds. (Compared with other Libraries this ist about 50 times slower)

  1. Do you see a way to improve my Performance in general?

1st Attempt
Creating each pdf in a loop and merging them back together with pypdf2.
This would simply take too long considering the amount it takes to create a single pdf.

2nd Attempt
Putting the original Form in a Repeating panel and populate all x orders at once and create the pdf just once. This works in principle, however I could not get a page break to work.
2. Is this the way to go? If yes, how do I handle the Page break for different Page formats (A4, A5 etc.)

Additional Question:
Could i save the last roundtrip by sending the pdf itself at 2. instead of the url?
If so, any idea on how to implement it?

  1. C requests a pdf
  2. S creates pdf and sends back url
  3. url is used to print/show a pdf

3rd Attempt
Would be to try other libraries like PyFPDF, ReportLab
Yet this would result in massive costs for us since the application includes dozens of different PDF Templates currently created in Anvil.
So I sincerly hope I can avoid this path(;

Thanks in advance for your help!

1 Like

Hi @mark.breuss, if waiting 5 seconds is not suitable for your case, the open source app server with external databse will certainly help. I see massive perfomance improvement with this solution.

However, there are a lot things to consider when moving to the server.

The alternative is to load all data to client and generate pdf there, which may not feasible for sensitive data.

Hi @Tony.Nguyen,

thanks for the fast reply!

I guess I was not specific enough, getting the data from the database and the round trip time itself are fast enough.
The 5 seconds i mentioned refer just to this line of code (rendering the pdf):

pdf = PDFRenderer(page_size='A4',filename=str(row['id']) + '.pdf',margins=0).render_form('MarkOne_Bestellungen.mulitple_a5_orders',data)

I’d like to avoid the external Server way in order to keep the anvil conveniences. Also If I can’t make the mulitple Anvil PDF’s work I can use the pyfpdf etc. and keep the Anvil Server.

Shipping the data to the client would not work since, as far as I know, the anvil pdf is only available Server Side.

Do you have an Idea on how to make the 2nd Attempt work?
(Cause I can live with the 2-5 seconds rendering time. But I need to be able to print multiple pdfs at once)

best,
Mark

Perhaps make a separate form made up of a repeating panel which is just your PDF form repeated.

Then populate the repeating panel with all selected orders. If there is only one then one shows. The rendering should be about the same speed (I hope but haven’t tested).

even with a blank form and no data it will take around 2 seconds to render a form in anvil using PDFRenderer.

You can check by timing

pdf = PDFRenderer(page_size='A4',
                  filename='testpdf.pdf',
                  quality='original',
                  margins=0).render_form('BlankForm')

So… if lightning speed is your goal then i would say move to a server side pdf renderer.
If convenience is the goal - stick with the anvil PDFRender

(note - data bindings with repeating panels can be the cause of additional slowness when you do the multipage version… and so @robert suggestion will go someway to reducing the 5 seconds to something more reasonable…)

Hi @robert, Hi @stucork,

Thanks for your input!

Yes the repeating panel workaround works in terms of speed. Which is great!
BUT I cant get a pagebreak to work.

https://anvil.works/build#clone:6FBGNTPHWXFPXMVM=VINOWOPWVQKKHTFAV5Z6UUKR
(Click on Print Multiple Orders)
Each Order should have its own page. (which is tricky since it could be 0.7 pages long or 5.1 pages long)

@stucork Alright! Although its not the fastetst the convienience aspect outweighs that by quite a margin(;

Thanks for your help!

The page breaks between the form and the rendered PDF are really beyond me.

Could it be possible to get the size of the rendered order in pixels and convert that to the size of paper? Then using some rules insert a spacer of a given height necessary to give the page break. That seems convoluted though…

1 Like

you can adjust page breaks using css

This works in the clone link… (added to theme.css) but may need adjusting depending on the form you render…

@media print {
  .column-panel .has-components {page-break-inside: avoid;}
}

https://www.w3schools.com/cssref/pr_print_pagebi.asp

related topic

How to prevent rows from splitting in two when printing

3 Likes

I currently generate variable page documents in some of my anvil apps using the fpdf python library. With this library I have total control of the layout and the number pages of the PDF report generated.

I hope this snippet of code below may be of some help:

import anvil.secrets
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.server
import anvil.media

from fpdf import FPDF
import random
from io import BytesIO


class REPORT(FPDF):

    date = ""
    data = {}

    def __init__(self,  date, data):
        self.date = date
        self.data = data
        FPDF.__init__(self)
    

    def footer(self):
        # Position at 1.5 cm from bottom
        self.set_y(-15)
        # Arial italic 8
        self.set_font('Arial', 'I', 8)
        # Text color in gray
        self.set_text_color(128)

        #Date
        self.cell(50, 10, self.date , 0, 0, 'L')

        #Report
        self.cell(65, 10, "Sample Invoice", 0, 0, "C")

        # Page number
        self.cell(65, 10, 'Page ' + str(self.page_no()), 0, 0, 'R')

    def generate(self):
      
        try:
          
          if self.data is not None:
            
            #This line adds a new page in the pdf report
            self.add_page()
            #self.ln(10)
    
            header_column_one = 50
            header_fontsize = 14
            header_height = 6
      
            self.set_fill_color(221, 255, 221)            
            # Times 12
            self.set_font('Times', 'B', 20)
            self.cell(w=0 , h=15, txt="Sample Report Header",  border=0, ln=1, align="C", fill = True)
            self.ln(5)
    
            self.set_font('Times', 'B', header_fontsize)
            self.cell(w=header_column_one , h=header_height, txt="Reported By",  border=1, ln=0, align="L", fill = False)
            self.set_font('Times', '', header_fontsize)
            self.cell(w=0 , h=header_height, txt=self.data['username'],  border=1, ln=1, align="L", fill = False)
            
            self.set_font('Times', 'B', header_fontsize)
            self.cell(w=header_column_one , h=header_height, txt="Phone Number",  border=1, ln=0, align="L", fill = False)
            self.set_font('Times', '', header_fontsize)
            self.cell(w=0 , h=header_height, txt=self.data['phone_number'],  border=1, ln=1, align="L", fill = False)
            
            self.set_font('Times', 'B', header_fontsize)
            self.cell(w=header_column_one , h=header_height, txt="Reported On",  border=1, ln=0, align="L", fill = False)
            self.set_font('Times', '', header_fontsize)
            self.cell(w=0 , h=header_height, txt=self.data['date_time'],  border=1, ln=1, align="L", fill = False)
            
            self.set_font('Times', 'B', header_fontsize)
            self.cell(w=header_column_one , h=header_height, txt="Printed On",  border=1, ln=0, align="L", fill = False)
            self.set_font('Times', '', header_fontsize)
            self.cell(w=0 , h=header_height, txt=self.date,  border=1, ln=1, align="L", fill = False)
            self.ln(5)
    
                
            font_size = 12 #12
            height = 6 #5
            count = 1
            
            self.set_font('Times', '', font_size)
            
            #Perform additional operations here
            for item in self.data['items']:
              pass

              
        except Exception as err:
          print("Error in report class:", err)
          

### End of REPORT class



@anvil.server.callable
def generate_report(date, data):
  
  
  report = REPORT(date, data)
  report.generate()
  
  try:
    
    report_name = "Report.pdf"
    
    if data is not None:
      report_name = (data['username'] + "_" + data['date_time'] + ".pdf").replace(" ", "_").replace(",","")
      
    pdf_bytes = report.output(dest='S').encode('latin-1')      
    pdf_report = anvil.BlobMedia("application/pdf", pdf_bytes, name=report_name)
    
    return pdf_report
  
  except Exception as err:
    print("Error in generate report fxn:", err)
  
  return None

For more information about the fpdf library, take a look at the documentation and some tutorials in the links below:


Should you have any particular question on how to implement this in Anvil, let me know, I’ll be happy to help.

1 Like

Hi @stucork,

this looks like it wants to work - however I cant get it to work in the colone link.
I’ve just copied it into theme css?

Just to avoid confusion:
each James (which is an item in a repeating panel) should have its own page.

best, Mark

Hi @robert,

this seems like the kind of idea I usually have (;
If I cant get the CSS pagebreak to work I’ll definitely give it a try.

Thanks!

Hi @Agobin,

Although for the most PDF’s I need to create a few seconds waiting time is fine there are some which need to be faster and require more control over the PDF.

So I’ll definetly give it a try!
Thanks for the Snippet should I have any questions I’ll happily take you up on the offer :wink:

@stucork @robert,

my current approach is sort of a hybrid of your answers sadly it does not work.
https://anvil.works/build#clone:6FBGNTPHWXFPXMVM=VINOWOPWVQKKHTFAV5Z6UUKR

I’ve created a role page-break and assigned the last element of the template with it.

CSS Role:
image

In my very basic understanding of css, this should solve all problems, except the fact that it doesnt.

Any tipps appreciated!

Finally got it to work :partying_face:

check this beauty out:
https://anvil.works/build#clone:6FBGNTPHWXFPXMVM=VINOWOPWVQKKHTFAV5Z6UUKR

Needless to say that this is the effort of the very supportive Anvil community.

It works actually pretty simple.

The pdf template is wrapped into a repeating panel.
The item of the repeating panel is a custom form which has the pdf tempate in a column panel and a page wrapper below it.

Thanks again for your help @robert, @robert, @Agobin, @Tony.Nguyen!

3 Likes

Glad you got it working! I think I will probably be doing something similar in the near future, so this is quite helpful for me too!

1 Like