easyofd
| Language | Python 3 |
|---|---|
| License | Apache-2.0 (depends on PyMuPDF: AGPL-3.0 or commercial license) |
| Repository | github.com/renoyuan/easyofd |
| Documentation | README and test/demo.py (Chinese) |
| Depends on | ReportLab, xmltodict, fontTools, PyMuPDF, loguru, Pillow |
| Read / write | yes / yes (OFD from PDF or images) |
easyofd is a pure-Python OFD parser. OFD.read() turns a file into nested dicts and lists: for each page, text objects with their positions, images, lines and seals. From that data it draws a PDF with ReportLab, and renders the PDF to images with PyMuPDF. It also converts PDF files and images to OFD.
It handles typical Chinese e-invoices and simple documents well. Complex vector graphics, gradients and unusual fonts may render differently from a dedicated OFD reader. Versions are numbered by date (e.g. 20260427). Because of the PyMuPDF dependency, distributing a program that uses easyofd brings in AGPL obligations unless you have a PyMuPDF commercial license.
Installation#
pip install easyofd
On Linux, install Chinese fonts (for example fonts-noto-cjk or SimSun-compatible fonts); otherwise text drawn into PDF may show as boxes.
Basic usage: convert OFD to PDF and images#
read() accepts a file path, bytes, a BytesIO or a base64 string, selected with fmt:
from easyofd.ofd import OFD
ofd = OFD()
ofd.read("invoice.ofd", fmt="path") # also fmt="binary", "io", "b64" (default)
pdf_bytes = ofd.to_pdf()
with open("invoice.pdf", "wb") as f:
f.write(pdf_bytes)
for i, img in enumerate(ofd.to_jpg()): # list of PIL images, one per page
img.save(f"invoice_{i}.jpg")
ofd.del_data()
How to extract text from an OFD invoice#
After read(), ofd.data is a list with one dict per document. page_info is a dict of page contents; each text item has the text and its box (x, y, width, height in millimeters):
from easyofd.ofd import OFD
ofd = OFD()
ofd.read("invoice.ofd", fmt="path")
for doc in ofd.data:
for page in doc["page_info"].values():
items = sorted(page.get("text_list", []), key=lambda t: (t["pos"][1], t["pos"][0]))
for item in items:
x, y, w, h = item["pos"]
print(f"{x:7.1f} {y:7.1f} {item['text']}")
Sorting by position (top to bottom, then left to right) makes it practical to pick invoice fields by their location.
How to convert PDF to OFD#
from easyofd.ofd import OFD
with open("in.pdf", "rb") as f:
pdf_bytes = f.read()
ofd = OFD()
ofd_bytes = ofd.pdf2ofd(pdf_bytes, optional_text=True) # True keeps selectable text from a text PDF
with open("out.ofd", "wb") as f:
f.write(ofd_bytes)
How to create OFD from images#
from PIL import Image
from easyofd.ofd import OFD
images = [Image.open("scan1.jpg"), Image.open("scan2.jpg")]
ofd_bytes = OFD().jpg2ofd(images) # one page per image
with open("scans.ofd", "wb") as f:
f.write(ofd_bytes)
OFD().jpg2pfd(images) (the method name has a typo) creates a PDF from the same images.
Tips#
ofd.read(path, fmt="path", save_xml=True, xml_name="dump")also writes the unpacked XML files, useful to debug a file that renders wrong.- The repository includes a GUI tool for the same conversions.
- easyofd doesn’t verify signatures; seals are drawn as images.