anvil.pdf.PDFRenderer got slower

We use the anvil.pdf.PDFRenderer for a report. To get it done (30sec limit) we sliced the report in 7 parts. The Renderer call the forms (although in background task) in we have the 30 sec limit.
Since a few days the Renderer got so slow, that the limit is surpassed.
Question @ anvil: was there a change - any idea how to get it faster (or best would be to perform the Renderer including the called forms in background task.
Question to all: is there any new tool we could use?

1 Like

You might take some inspiration from reading the code in the clone here:

1 Like

I ended up with using fpdf2 in some apps.

It takes longer to write the code to generate every single item in the pdf than using a form that I already had, but on the other hand it’s very fast and it gives you more flexibility. For example I can better manage page headers and footers.

I even reported 3 bugs and they were fixed after a few hours!
(I love working with small open source packages)

1 Like

Thanks Stefano,

Does that mean, you had to define every component (can imagine that), but how do you do for example repeating-panels? Would love to see a report you did.

Cheers Aaron

I don’t think in components, I think of the final PDF.

A repeating panel becomes a for cycle that adds to the page.

I have a class with members that add panels with header, footer, address, tables, etc.

The code creates an instance of the class, then calls its members to add chunks to the PDF.

Here is some code. I have removed the data collection and other housekeeping and left other parts to show how it works. It’s tons of code, but it’s easy to debug locally with PyCharm. When it works, I push and the app just works.

@anvil.server.background_task
def create_pdf_bg_task(project_number, truck_number, user):
    # skipping lines for data collection and organization
    row = app_tables.pdffiles.add_row(
        key=filename,
        expiration=datetime.now() + timedelta(minutes=60),
    )
    pdf = PdfPackingList()
    pdf.add_page2(f'T{truck.truck_number}')
    pdf.add_panel_with_headers_and_values(
        ['Project', 'Truck', 'Customer Order', 'Order Complete'],
        [
            project.project_number,
            f'{truck.truck_number} of {len(truck.project.trucks)}',
            truck.order_no,
            'Yes' if truck.order_complete else 'No',
        ]
    )
    pdf.add_panel_with_headers_and_values(
        ['Weight', 'Qty Crates', 'Qty Panels', 'Closed by', 'Shipping Date'],
        [
            f'{truck.weight_or_estimated} lb',
            len(truck.crates),
            truck.n_panels,
            truck.closed_by,
            truck.shipping_date.strftime('%m-%d-%Y') if truck.shipping_date else '-',
        ]
    )
    pdf.add_panel_with_addresses(
        ship_to=truck.ship_to,
        c_o=truck.ship_c_o,
        attn=truck.ship_attention,
        phone=truck.ship_phone,
        ship_via=truck.shipped_via,
        sold_to=truck.sold_to,
        notes=truck.notes,
    )
    pdf.add_table(
        ['Crate #', 'Size', 'Weight', 'PckGrp', 'Panels'],
        [20, 30, 15, 20, -1],
        [
            [
                crate.name,
                crate.size,
                crate.weight_or_estimated,
                crate.packing_groups,
                crate.panel_list,
            ]
            for crate in truck.crates
        ]
    )

    for n, crate in enumerate(truck.crates):
        update_pdf_generation_status(f'Generating crate {crate.name} page ({n + 1}/{len(truck.crates)})...', row=row)
        pdf.add_page2(f'{crate.release.release_number_short}-C{crate.crate_number}')
        pdf.add_panel_with_headers_and_values(
            ['Project', 'Release', 'Crate', 'Truck', 'Notes'],
            [
                crate.release.project.project_number,
                crate.release.release_number,
                f'{crate.crate_number} of {len(crate.release.crates)}',
                crate.truck.truck_number,
                crate.notes,
            ]
        )
        pdf.add_panel_with_headers_and_values(
            ['Size', 'Weight', 'Qty Panels', 'Closed by', 'Shipping Date', 'Packing groups'],
            [
                crate.size,
                f'{crate.weight_or_estimated} lb',
                crate.n_panels_closed_or_counted,
                crate.closed_by,
                crate.truck.shipping_date and crate.truck.shipping_date.strftime('%m-%d-%Y'),
                crate.packing_groups,
            ]
        )
        pdf.add_table(['Qty', 'Part #'], [25, -1], crate.pack_list())

    tmp_file = r'C:\workspace\Anvil\Crating\App\Tests\test.pdf' if anvil.server.context.type == 'uplink' else f'/tmp/{filename}'
    pdf.output(tmp_file)
    media_object = anvil.media.from_file(tmp_file, 'application/pdf')
    row.update(pdf=media_object)
    os.unlink(tmp_file)


@anvil.server.background_task
def delete_expired_pdf():
    for row in app_tables.pdffiles.search(expiration=q.less_than(datetime.now())):
        print(f"Deleting expired PDF {row['key']}")
        row.delete()


@anvil.server.callable
def get_pdf_url(project_number, truck_number):
    row = app_tables.pdffiles.get(key=pdf_file_name(project_number, truck_number))
    return row['pdf'].get_url(False)


class PdfPackingList(FPDF):
    def __init__(self):
        super().__init__()

        self.page_title = ''

        self.default_text_color = 100, 100, 100
        self.default_draw_color = 180, 180, 180
        self.default_font_size = 10
        self.header_font_size = self.default_font_size * 0.85

        self.set_text_color(*self.default_text_color)
        self.set_draw_color(*self.default_draw_color)
        self.set_font(family='Helvetica', size=self.default_font_size)

    def _add_address_pair(self, x, header, value):
        col_1_width = 20  # width of columns 1 and 3, the ones with headers
        col2_width = (self.w - self.l_margin - self.r_margin) / 2 - col_1_width  # width of columns 2 and 4, the ones with values

        self.ln(2)
        self.set_font(style='b', size=self.header_font_size)
        self.set_x(x)
        self.cell(w=col_1_width, text=header + ':', align='R')
        self.set_font(style='', size=self.default_font_size)
        self.multi_cell(w=col2_width, text=value, new_x=XPos.LMARGIN)

    def add_panel_with_addresses(self, ship_to, c_o, attn, phone, ship_via, sold_to, notes):
        y = self.get_y()

        self._add_address_pair(self.l_margin, 'Ship to', ship_to)
        self._add_address_pair(self.l_margin, 'c/o', c_o)
        self._add_address_pair(self.l_margin, 'Attn', attn)
        self._add_address_pair(self.l_margin, 'Phone', phone)
        y1 = self.get_y()

        self.set_y(y)
        self._add_address_pair(self.w / 2, 'Ship via', ship_via)
        self._add_address_pair(self.w / 2, 'Sold to', sold_to)
        self._add_address_pair(self.w / 2, 'Notes', notes)
        panel_height = max(y1, self.get_y()) - y + 2

        self.set_y(y)
        self.cell(w=0, h=panel_height, border=1)

        self.ln(panel_height + 1)

    def add_panel_with_headers_and_values(self, headers, values):
        """headers and values are two lists with the same number of items"""
        y = self.get_y()
        n_columns = len(headers)
        column_width = (self.w - self.l_margin - self.r_margin) / n_columns

        headers_bottom = 0
        self.set_font(style='b', size=self.header_font_size)
        for c in range(n_columns):
            self.set_xy(self.l_margin + c * column_width, y + 2)
            self.multi_cell(w=column_width, text=str(headers[c]), align='C')
            headers_bottom = max(headers_bottom, self.get_y())

        values_bottom = 0
        self.set_font(style='', size=self.default_font_size)
        for c in range(n_columns):
            self.set_xy(self.l_margin + c * column_width, headers_bottom + 2)
            self.multi_cell(w=column_width, text=str(values[c]), align='C')
            values_bottom = max(values_bottom, self.get_y())

        panel_height = values_bottom - y + 2
        self.set_y(y)
        self.cell(w=0, h=panel_height, border=1)

        self.ln(panel_height + 1)

    def add_horizontal_line(self, gap_above=1.5, gap_below=1.5, color=None):
        self.set_xy(self.l_margin, self.y + gap_above)
        if color:
            self.set_draw_color(color)

        self.cell(w=0, h=0, border=1)

        self.set_xy(self.l_margin, self.y - 2.5 + gap_below)
        if color:
            self.set_draw_color(self.default_draw_color)

    def _add_table_row(self, column_widths, row):
        # replace -1 with the remaining column width, without modifying the original list
        column_widths = [width if width > 0 else self.w - self.l_margin - self.r_margin - sum(column_widths)
                         for width in column_widths]

        y_top = self.y
        y_bottom = 0
        for c in range(len(column_widths)):
            self._add_table_cell_row(self.x, self.x + column_widths[c], y_top + 2, str(row[c]))
            y_bottom = max(y_bottom, self.y)

        self.set_y(y_bottom + 4.5)

    def _add_table_cell_row(self, x1, x2, y, text):
        """The text enclosed in _<...>_ will be printed in smaller size """
        lm, tm, rm = self.l_margin, self.t_margin, self.r_margin
        self.set_margins(x1, self.t_margin, self.w - x2)
        self.set_xy(x1, y)

        chunks = re.split(r'_<|>_', text)
        for n in range(0, len(chunks), 2):
            self.write(h=5.5, text=chunks[n])
            if n == len(chunks) - 1:
                break

            self.set_text_color(130, 130, 130)
            self.set_font(size=self.header_font_size)
            self.write(h=5.5, text=chunks[n + 1])
            self.set_text_color(*self.default_text_color)
            self.set_font(size=self.default_font_size)

        self.set_margins(lm, tm, rm)
        self.set_x(x2)

    def _add_table_title_row(self, headers, column_widths):
        self.set_font(style='b', size=self.header_font_size)
        self._add_table_row(column_widths, headers)

        self.add_horizontal_line(color=self.default_text_color)

        self.set_font(style='', size=self.default_font_size)

    def add_table(self, headers, column_widths, rows: List[Union[List[str], str]]):
        """Rows is a list of lists or strings.
        If a row is a lists of strings, then each list contains the values for the row.
        If a row is a string, it must be '---' and a horizontal line is added."""
        self._add_table_title_row(headers, column_widths)

        for row in rows:
            if type(row) is str:
                if row == '---':
                    self.add_horizontal_line(color=(200, 200, 200))
                else:
                    raise ValueError('String arguments can only contain the value "---" to add an horizontal line.')
                continue

            # dummy rendering to check if the row fits in this page
            with self.offset_rendering() as dummy:
                self._add_table_row(column_widths, row)

            # if it doesn't, add a page and the table headers
            if dummy.page_break_triggered:
                self.add_page2()
                self._add_table_title_row(headers, column_widths)

            # perform the real row rendering
            self._add_table_row(column_widths, row)

    def add_page2(self, title=None):
        if title:
            self.page_title = title
        self.add_page(format='letter')

    def header(self):
        self.image(r'https://some.url/logo.png',
                   x=10, y=8, w=60)

        self.set_font(style='b', size=25)
        self.cell(w=0, text=self.page_title, align='C')

        self.set_font(size=18)
        self.cell(w=0, text='Packing List', align='R')

        self.ln(12)

    def footer(self):
        self.set_y(-17)
        self.set_font(size=7.7)
        self.multi_cell(
            w=0, align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT,
            text='some text'
        )
1 Like

Thank you so much - I will deep dive into it - look really like tons of code, but if it is fast it is worth it.

Just make sure you work on it off line, not on the ide. It’s much faster to go through all the required trial and error on your PC than with the anvil ide.

3 Likes

I just wanted to contribute that i had the exact same problem, and didnt have enough time to learn all the docs of the fpdf2, so I uploaded an example PDF to openAI chatGPT, i just had to give a very detailed explanation of what had to be edited in each pdf and it wrote all the needed code to replicate the pdf each time with different data.

Worked as a charm to me. Maybe you can try it.

that sound great - would be fantastic if you could show some of the process how you did it. Did chatGPT write the code for fpdf2?

Yes, chatgpt wrote all the code, it resulted in a simplified version of the pdf I uploaded, but simple corrections did the job. I can share the prompt I used (it is translated via google as I use chatGPT in spanish):

For the following requirement, act as a professional programmer experienced in python and specifically in creating applications in anvil.works. I need to recreate the previous PDF using a python def function using the fpdf2 library, on the first page, the entire header must remain the same, the only field that changes between each pdf is the number “10130” which corresponds to the proforma code and must be one of the variables to enter the function. The content of the table must maintain the titles of each column of PRODUCT, QUANTITY, UNIT VALUE, TOTAL VALUE, but must change the content of the table that enters as a variable of the function in the form of a list of dictionaries where each item of the list represents a row in the table. The format of the first sheet must have a maximum of 10 rows of the table. If the dictionary list has more than 10 items, the first 10 items go on the first sheet and the second 10 on a second sheet with the same format. . The small table that in its first column has GROSS TOTAL, VAT, TOTAL PAYABLE, is added to the last sheet that has the table. The sheet containing the 8 numerals (the second sheet in the submitted pdf) should go at the end and shares the same header as the other format.

And this is a sample of the PDF i had to make:

2 Likes

thank you very much - that is a great help.