Most PDF libraries for Python can write a PDF. Far fewer can produce a file that a validator accepts as PDF/A-1b, and fewer still can put a digital signature on it without shelling out to a second tool. Here is the whole job in one script, using the LumasPDF SDK through its Python wheel. The binding is ctypes over a native engine, so there is nothing to compile.
Disclosure: I'm the author of LumasPDF.
Install
pip install lumaspdf
Wheels exist for Windows x64/x86, Linux x64 and macOS. The engine for your platform is inside the wheel. The download is free; unlicensed output carries an evaluation watermark, and a trial key removes it.
You also need two files: an sRGB ICC profile and a PKCS#12 certificate. Both ship as test fixtures in the SDK download (test_files/sample_rgb.icc and test_files/test_cert.pfx, password 123456), or use your own.
The script
"""Create a PDF/A-1b file and sign it, from Python, with the lumaspdf wheel.
pip install lumaspdf
python signed_pdfa.py sRGB.icc cert.pfx cert-password
"""
import os, sys, ctypes
import lumaspdf as L
icc, pfx, pwd = sys.argv[1], sys.argv[2], sys.argv[3]
out = os.path.abspath("signed_pdfa.pdf")
# Errors arrive through a callback; return 0 to let CheckConformance apply fixes.
ERR = ctypes.WINFUNCTYPE if os.name == "nt" else ctypes.CFUNCTYPE
@ERR(ctypes.c_int32, ctypes.c_void_p, ctypes.c_int32, ctypes.c_char_p, ctypes.c_int32)
def on_error(data, code, msg, kind):
print("engine:", msg.decode("latin-1", "replace"))
return 0
pdf = L.pdfNewPDF()
L.pdfSetOnErrorProc(pdf, 0, on_error)
L.pdfCreateNewPDFA(pdf, b"") # output file is chosen at the end
L.pdfSetDocInfoA(pdf, L.diTitle, b"Signed PDF/A-1b from Python")
L.pdfAppend(pdf)
# PDF/A needs every font embedded, so use a real TrueType face, not a base-14 name.
L.pdfSetFontA(pdf, b"Arial", L.fsNone, 11.0, 1, L.cp1252)
L.pdfWriteFTextA(pdf, L.taLeft, b"This file is PDF/A-1b and carries a digital signature.")
# A visible signature field; the engine draws the certificate details into it.
field = L.pdfCreateSigField(pdf, b"Signature", -1, 350.0, 700.0, 200.0, 60.0)
L.pdfEndPage(pdf)
# Check conformance. The return value says which colour space needs an output intent.
rc = L.pdfCheckConformance(pdf, L.ctPDFA_1b_2005, 0, None,
L.TOnFontNotFoundProc(0), L.TOnReplaceICCProfile(0))
if rc in (1, 3): # gray or RGB content
L.pdfAddOutputIntentA(pdf, icc.encode("latin-1"))
elif rc == 2: # CMYK content: use a CMYK profile instead
L.pdfAddOutputIntentA(pdf, icc.encode("latin-1"))
if L.pdfHaveOpenDoc(pdf) and L.pdfOpenOutputFileA(pdf, out.encode("latin-1")):
# Signs with the PKCS#12 certificate and writes the file in one step.
if L.pdfCloseAndSignFile(pdf, pfx.encode("latin-1"), pwd.encode(), b"Approved", b"") != 0:
print("written:", out, os.path.getsize(out), "bytes")
L.pdfDeletePDF(pdf)
Run it:
python signed_pdfa.py sample_rgb.icc test_cert.pfx 123456
written: C:\work\signed_pdfa.pdf 105599 bytes
What each step is doing
The error callback. The engine reports problems through a callback rather than exceptions. Returning 0 means "carry on"; that matters for pdfCheckConformance, which repairs a document (sets missing flags, drops forbidden features) and tells you what it changed through this callback. Return -1 instead if you want any change to abort the run.
pdfCreateNewPDFA(pdf, b""). The empty name defers the output file. Signing needs to write the whole file in one pass at the end, so the output is opened only after the conformance check has finished.
Embedded fonts. PDF/A forbids unembedded fonts, including the fourteen standard ones. pdfSetFontA(..., 1, ...) embeds a real TrueType face. On Linux and macOS, point the engine at a font directory first with pdfAddFontSearchPathA, since there is no system font registry to fall back on.
The signature field. pdfCreateSigField places a visible field; if you close with pdfCloseAndSignFile and no custom appearance was drawn, the engine renders the certificate's subject and date into it. PDF/A requires signature fields to be visible and printable, and the conformance check enforces that.
pdfCheckConformance. The return value tells you which colour spaces the document uses: 1 gray, 2 CMYK, 3 RGB. PDF/A-1b needs an output intent matching that, hence the ICC profile. The TOnFontNotFoundProc(0) and TOnReplaceICCProfile(0) arguments are null callbacks; supply real ones if you want to substitute fonts or profiles interactively.
pdfCloseAndSignFile. Takes the PKCS#12 file, its password, a reason and a location, and produces a PAdES-style signature with a ByteRange covering the whole file. The result stays PDF/A-1b; signing after the fact with a different tool is where conformance usually breaks.
Check it
Open the output in a validator. With veraPDF:
verapdf --flavour 1b signed_pdfa.pdf
You should see isCompliant="true". Adobe Acrobat's signature panel shows the field as signed; it will flag the test certificate as untrusted, which is expected for a self-signed fixture.
Where this goes next
The same calls exist in every LumasPDF binding, under the same names: C, C++, .NET, Delphi, PHP, Node, Flutter. The function reference is at https://www.lumaspdf.com/reference, and the full example set, including this one in each language, is at https://github.com/Lumaspdf/lumaspdf-examples.
Top comments (0)