I would like to generate invoices with a little Python program I’ve written. I want it to output the invoice first as an HTML file, then convert it to a PDF. Currently my code is creating the HTML doc as desired, but it is outputting a blank PDF file:
import pdfkit import time name = input('Enter business name: ') date = input('Enter the date: ') invoice = input('Enter invoice number: ') total = 1000 # Fixed number for now just for testing purposes f = open('invoice.html','w') message = f"""<html> <head> <link rel='stylesheet' href='style.css')> </head> <body> <h1>INVOICE<span id="invoice-number"> {invoice}</h1> <h3>{date} </h3> <h3>{name} </h3> <h3>Total: ${total}</h3> </body> </html>""" f.write(message) time.sleep(2) options = { 'enable-local-file-access': None } pdfkit.from_file('invoice.html', 'out.pdf', options = options)
When I run the code above, the HTML doc it puts out looks fine, but the PDF it creates is blank. I receive no error messages at all. The output in Terminal is:
Loading pages (1/6) Counting pages (2/6) Resolving links (4/6) Loading headers and footers (5/6) Printing pages (6/6) Done
However, I have another file called test.py
in the same directory. This file contains the import statement, exactly the same last four lines as the first code, and nothing else:
import pdfkit options = { 'enable-local-file-access': None } pdfkit.from_file('invoice.html', 'out.pdf', options = options)
If I run this file after the previous file, it correctly outputs the PDF to look just like the HTML version. Why does this code work when run in a separate file, but doesn’t work when included in the original file? How can I get it to run in the same file so I don’t have to execute two commands to get an invoice?
Advertisement
Answer
You never close the file you’re writing to, or explicitly flush it. Since it’s a small text snippet, and buffering is enabled by default, it doesn’t get written to disk until after your process terminates. You don’t see an error because the file does get created immediately as it’s opened though. The issue is not one of timing as you seem to suppose.
There are any number of solutions to this problem. The simplest and most correct is to close the file as soon as you’re finished writing to it. This is how you open a file idiomatically in python:
name = input('Enter business name: ') date = input('Enter the date: ') total = 1000 # Fixed number for now just for testing purposes message = ... options = { 'enable-local-file-access': None } with open('invoice.html','w') as f: f.write(message) pdfkit.from_file('invoice.html', 'out.pdf', options = options)
Notice that you don’t need to wait for anything: exiting the with
block will close the file and flush it in the process even if an error occurs. You should get in the habit of closing files as soon as you finish work on them: most OSes support a finite number of file handles per process. That means that putting the call to pdfkit.from_file
after the with
block guarantees that it will have a fully flushed file to work with.
Here are some other methods, which are not idiomatic, and are not recommended in practice. I’m providing them just to give you a sense of how the different steps work:
Flush the file before attempting to read it. Instead of
sleep
, callf.flush()
This will leave the file object open and not necessarily handle errors correctly, but it will ensure that their HTML content is written out before you attempt to read it.
Turn off buffering. Buffering means that until you write 4kb or so of data (whatever your disk block size is), none of it will be written to disk until you flush it. You can disable this behavior so bytes get written out immediately by calling
open
like thisopen('invoice.html', 'w', buffering=0)