Digital PDF Signing in PowerBuilder 2025 R2 (.Net10 + Python)

Ramón San Félix Ramón
CODE AUTHOR
Posts: 47
 3 weeks 2 days ago -  3 weeks 2 days ago #660 by Ramón San Félix Ramón
Ramón San Félix Ramón created the code: Digital PDF Signing in PowerBuilder 2025 R2 (.Net10 + Python)

This is a complete, ready-to-run PowerBuilder 2025 example that digitally signs PDF files (visible and invisible) with a .pfx certificate. The same "Sign PDF" button can sign through three different engines, selected with a couple of radio buttons:

·         Console (.EXE): launches TestNetPdfService.exe and captures its output.

·         .NET library: loads NetPdfService.dll in-process as a dotnetobject (this is the interesting one).

·         Python (pyHanko): calls a Python script through PyPb, with a portable Python runtime (one-time setup, nothing installed system-wide).

The two iText engines are self-contained: you clone it, open it, compile it and it runs with no setup. The Python engine just needs a one-time setup script (explained below) that fetches its portable runtime.

A note for the international community

This example was originally written in Spanish. We have translated almost everything into English for the international Appeon community: comments, UI labels, messages and the documentation. However, a few PowerBuilder function names (for example of_firmar, of_controles_previos) and some object and control names remain in Spanish, as do the .NET Firmar(...) method and its nombre/dni parameters. We left those identifiers untouched on purpose, because renaming them would break the wiring that connects PowerBuilder, the .NET library and the Python script. Apologies for the parts that are still in Spanish, and we hope the example is clear enough despite that.

The big one: using iText7 as an in-process library, no more .exe

For years this example shipped with a workaround. The signing was done by iText7, but I could only make it work by compiling a small console application (TestNetPdfService.exe) and launching it as an external process from PowerBuilder, capturing its output. Loading the very same DLL in-process as a dotnetobject always blew up with this cryptic error:

The type initializer for 'iText.IO.Util.ResourceUtil' threw an exception.

The same NetPdfService.dll worked perfectly in console mode but crashed inside PowerBuilder. Neither Appeon support nor iText support could tell me why, and I had it marked as "impossible" for a long time. It is finally solved.

The real cause

The problem is NOT iText, and it is NOT a missing dependency. It is how PowerBuilder hosts the .NET CLR. When you consume a .NET class library in-process from PowerBuilder, AppDomain.CurrentDomain.BaseDirectory is left empty ("") instead of pointing to the DLL's folder. On the very first iText call, the static constructor of iText.IO.Util.ResourceUtil runs:

if (FileUtil.GetBaseDirectory() == null) return;   // only checks null
string[] files = Directory.GetFiles(FileUtil.GetBaseDirectory(), "*.dll");

It only checks whether BaseDirectory is null, not whether it is an empty string. Under PowerBuilder it is "" (not null), so it reaches Directory.GetFiles("", "*.dll") and throws:

System.ArgumentException: The path is empty. (Parameter 'path')

That inner exception bubbles up wrapped as the generic "type initializer for ResourceUtil" message, which is what sent everyone (me included) chasing the wrong thing: the transitive dependencies Microsoft.DotNet.PlatformAbstractions and Microsoft.Extensions.DependencyModel. Those load fine; they were a red herring. In a console app it works because there BaseDirectory is the .exe folder. Same DLL, different hosting environment.

The fix

The fix is tiny: a static constructor in the signing class that gives .NET a valid base directory (the DLL's own folder) before iText initializes. The static constructor runs once, automatically, when PowerBuilder does CREATE nvo_pdfservice, which is before any iText call:

static PdfService()
{
    string dir = System.IO.Path.GetDirectoryName(typeof(PdfService).Assembly.Location) ?? "";
    if (string.IsNullOrEmpty(AppContext.BaseDirectory) && dir != "")
        AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", dir.EndsWith("\\") ? dir : dir + "\\");
}

That's it. Recompile, and it signs from PowerBuilder in library mode exactly like the console app did. AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", dir) really does change AppDomain.CurrentDomain.BaseDirectory (verified on .NET 8 and .NET 10). The fix is valid for both iText 8 and iText 9 (9.6.0 here), because the cause is the CLR hosting under PowerBuilder, not the iText version. The example also adds a small assembly resolver as a belt-and-suspenders measure, to load the transitive dependencies from the DLL's folder in case the host does not resolve them.

Bottom line: we no longer need the external .exe. iText7 runs inside PowerBuilder as a normal dotnetobject.

Signing with Python (pyHanko) - a portable runtime, set up in one step

The third engine signs the PDF with Python, using the excellent pyHanko library, called from PowerBuilder through PyPb (Appeon's bridge over Python.NET). For distribution, a portable Python 3.13 runtime runs next to the application, inside the python.runtime folder, together with pyHanko and Pillow - nothing is installed system-wide on the client machine. Here is the one practical difference between this CodeXchange edition and my GitHub repo: CodeXchange caps each upload at 20 MB, and that runtime weighs about 74 MB, so it is NOT bundled in this download. A single script, setup_python.ps1, rebuilds it in one step (it downloads the official embeddable Python and pip-installs pyHanko + Pillow into python.runtime). On GitHub there is no such size cap, so the repo ships the full python.runtime and is clone-and-run, with no setup. Either way you only need Python for this third engine; the two iText engines work with no setup at all.

The PowerBuilder side stays clean thanks to two simple objects:

·         n_cst_pyton: a thin facade over PyPb (of_init / of_import / of_invoke), with no exceptions leaking out, just a 0/-1 return code plus of_lasterror().

·         n_cst_pyton_pdfsign: the business object that imports sign_pdf.py and invokes its sign(...) function with named arguments.

The Python script (sign_pdf.py) signs in PAdES with timestamp and LTV, and builds the visible appearance with Pillow (the handwritten signature centered, plus the name and ID at the bottom-left), mimicking the iText stamp. The signature coordinates use the same semantics as iText, so the same X1/Y1/X2/Y2 fields produce the same box across all three engines.

One gotcha worth knowing: PowerBuilder loads the .NET runtime only once per process. Because PyPb also initializes a .NET runtime, it must be pre-loaded at startup (in the application object's open event), before the iText dotnetobject is used. Otherwise the two .NET initializations can collide.

PAdES, timestamp and LTV, with offline-safe degradation

All three engines sign in PAdES (ETSI.CAdES.detached), add a timestamp from a free TSA (DigiCert) and, when possible, LTV (long-term validation, PAdES-B-LTA). If LTV or the timestamp cannot be obtained (no network, TSA down), the signature degrades gracefully and is always produced:

B-LTA  (PAdES + timestamp + LTV)  ->  B-T  (PAdES + timestamp)  ->  B-B  (PAdES)

So it never fails just for being offline; in the worst case you still get a valid PAdES signature. A legal demo certificate (firma_legal.pfx, password PDFSIGN) is included so you can reach PAdES-B-LTA out of the box. Note it is a DEMO certificate, not a qualified one: for real legal validity, use a qualified certificate from a trust service provider.

How to try it

Open the workspace in the PowerBuilder 2025 IDE, make sure pdfsign.pbl, dotnet.pbl and pypblib.pbl are in the library list, then build and run. Choose a PDF, a certificate and its password, check or uncheck Visible, pick the engine and press Sign PDF.

Requirements: PowerBuilder 2025 R2 (with PyPb for the Python method) and the .NET 10 SDK for the iText engines. For the Python engine, run setup_python.ps1 once (it needs an internet connection) to build the portable runtime inside python.runtime; nothing is installed system-wide. The iText engines need no setup.

Links

The copy attached here is the English-translated version, trimmed to fit the 20 MB CodeXchange limit (so it does not carry the embedded Python runtime - run setup_python.ps1 to rebuild it). I always recommend downloading the latest sources from my GitHub in case there are updates: there the example is the full, clone-and-run version (with the portable runtime included), although the code will be in Spanish.

·         PowerBuilder example (solution mode): github.com/rasanfe/pdfsign

·         .NET signing engine (NetPdfService): github.com/rasanfe/NetPdfService

·         Blog: rsrsystem.blogspot.com

 

This message has an attachment file.
Please log in or register to see it.

Last Edit: 3 weeks 2 days ago by  Ramón San Félix RamónReason: Attachment got lost; I'm adding it again.

Please Log in or Create an account to join the conversation.