How to append a Tkinter widget to an existing pdf without creating a temporary intermediate file?

Question:

I want to append the contents of a Tkinter widget to the end of an existing pdf.

First, I capture the widget to an PIL image. Then, it seems it is required when using PyPDF2 to create an intermediate temporary file from the image, which feels unnecessary. Instead I would like to append the image directly, or at least convert the image to something that can be appended without the need to be written to a file.

In the code snippet below I save the image to a temporary pdf, then open the pdf and append. This works, but is not a very elegant solution.

import tkinter as tk
from PIL import ImageGrab
import os

import PyPDF2

def process_pdf(widget, pdf_filepath):
    """"Append Tkinter widget to pdf"""

    # capture the widget
    img = capture_widget(widget)

    # create pdf merger object
    merger = PyPDF2.PdfMerger()

    # creating a pdf file object of original pdf and add to output
    pdf_org = open(pdf_filepath, 'rb')
    merger.append(pdf_org)
    
    # NOTE I want to get rid of this step
    # create temporary file, read it, append it to output, delete it.
    temp_filename = pdf_filepath[:-4] + "_temp.pdf"
    img.save(temp_filename)
    pdf_temp = open(temp_filename, 'rb')
    merger.append(pdf_temp)
    pdf_temp.close()

    # write
    outputfile = open(pdf_filepath, "wb")
    merger.write(outputfile)

    # clean up
    pdf_org.close()
    outputfile.close()
    os.remove(temp_filename)


def capture_widget(widget):
    """Take screenshot of the passed widget"""

    x0 = widget.winfo_rootx()
    y0 = widget.winfo_rooty()
    x1 = x0 + widget.winfo_width()
    y1 = y0 + widget.winfo_height()

    img = ImageGrab.grab((x0, y0, x1, y1))
    return img

Does someone have a more elegant solution not requiring an intermediate file while retaining the flexibility PyPDF2 provides?

Thanks.

Asked By: RCTW

||

Answers:

So I was able to find a solution myself. The trick is writing the PIL image to a byte array (see this question), then converting that to a pdf using img2pdf. For img2pdf, it is required to use the format = "jpeg" argument during saving to byte array.

Subsequently, the result of img2pdf can be written to another io.BytesIO() stream. Since it implements a .read() method, PyPDF2.PdfMerger() can read it.

Below is the code, hope this helps someone.

import io
import PyPDF2
import img2pdf

def process_pdf(widget, pdf_filepath):
    """"Append Tkinter widget to pdf"""

    # create pdf merger object
    merger = PyPDF2.PdfMerger()

    # creating a pdf file object of original pdf and add to output
    pdf_org = open(pdf_filepath, 'rb')
    merger.append(pdf_org)
    pdf_org.close()
    
    # capture the widget and rotate
    img = capture_widget(widget)

    # create img bytearray
    img_byte_arr = io.BytesIO()
    img.save(img_byte_arr, format = "jpeg", quality=100)
    img_byte_arr = img_byte_arr.getvalue()

    # create a pdf bytearray and do formatting using img2pdf
    pdf_byte_arr = io.BytesIO()
    layout_fun = img2pdf.get_layout_fun((img2pdf.mm_to_pt(210),img2pdf.mm_to_pt(297)))
    pdf_byte_arr.write(img2pdf.convert(img_byte_arr, layout_fun=layout_fun))
    merger.append(pdf_byte_arr)

    # write
    outputfile = open(pdf_filepath[:-4] + "_appended.pdf", "wb")
    merger.write(outputfile)
    outputfile.close()
Answered By: RCTW