This project contains the source generators that automatically create interop bindings for the QuestPDF public API.
The code is organized into 4 separate, well-defined components:
Purpose: Analyzes the QuestPDF public API and extracts methods suitable for interop.
Responsibilities:
Key Methods:
CollectExtensionMethods() - Recursively scans namespaces for public extension methodsCollectFluentApiMethods() - Collects public methods from Fluent API classesCollectAllInteropMethods() - Collects both extension methods AND Fluent API class methodsIsFluentApiClass() - Determines if a class is part of the Fluent API (checks for Descriptor/Configuration/Handler/Builder suffixes, QuestPDF.Fluent namespace, IContainer interfaces)IsSupported() - Validates if a method can be exposed via interopIsSupportedType() - Checks if a type is compatible with UnmanagedCallersOnlyIsReferenceType() - Identifies classes/interfaces that need handle boxingIsTaskRelatedType() - Detects Task, CancellationToken, and async-related typesFluent API Class Detection: The analyzer now automatically detects Fluent API classes using multiple strategies:
QuestPDF.Fluent namespaceDescriptor, Configuration, Handler, Builder, or SettingsIContainer or IDocumentContainerExamples of detected Fluent API classes:
TextSpanDescriptor, TextPageNumberDescriptor, TextBlockDescriptor, TextDescriptorTableColumnsDefinitionDescriptor, TableCellDescriptor, TableDescriptorPageDescriptor, DecorationDescriptor, LayersDescriptor, GridDescriptorColumnDescriptor, RowDescriptor, InlinedDescriptor, MultiColumnDescriptorDocumentOperation.LayerConfiguration, DocumentOperation.DocumentAttachmentPurpose: Generates C# UnmanagedCallersOnly bindings for native interop.
Responsibilities:
GeneratedInterop.g.cs with all interop methodsquestpdf_fluent_alignmentextensions_alignright)Key Methods:
GenerateInteropCode() - Creates the complete C# interop fileGenerateEntryPointName() - Creates consistent entry point namingGenerateInteropMethod() - UPDATED: Generates wrappers for both extension and instance methodsGenerated Features:
Example for Extension Method:
// Original: public static IContainer AlignCenter(this IContainer container)
[UnmanagedCallersOnly(EntryPoint = "questpdf_fluent_alignmentextensions_aligncenter")]
public static nint AlignmentExtensions_AlignCenter(nint container)
{
var container_obj = UnboxHandle<IContainer>(container);
var result = QuestPDF.Fluent.AlignmentExtensions.AlignCenter(container_obj);
return BoxHandle(result);
}
Example for Instance Method:
// Original: public TextBlockDescriptor AlignCenter() (instance method)
[UnmanagedCallersOnly(EntryPoint = "questpdf_fluent_textblockdescriptor_aligncenter")]
public static nint TextBlockDescriptor_AlignCenter(nint @this)
{
var this_obj = UnboxHandle<TextBlockDescriptor>(@this);
var result = this_obj.AlignCenter();
return BoxHandle(result);
}
Purpose: Generates Python ctypes bindings for the interop layer with proper class organization.
Responsibilities:
GeneratedInterop.g.py.txt with Python wrapper classesKey Features:
Handle or QuestPDFLibrary class, methods are organized by typeIContainer, PageDescriptor, etc. map to Python classes IContainer, PageDescriptorself for the extended type parameterKey Methods:
GeneratePythonBindings() - Orchestrates the complete Python generation processGroupMethodsByType() - NEW: Groups methods by their C# type (extension target or containing type)GenerateLibraryClass() - Generates the QuestPDFLibrary class that manages the native libraryGeneratePythonWrapperClasses() - NEW: Generates separate Python classes for each C# typeGeneratePythonClassMethod() - NEW: Generates methods within their appropriate Python classToPythonClassName() - NEW: Converts C# type names to Python class namesArchitecture Example:
# Before (all methods on QuestPDFLibrary or Handle):
lib = QuestPDFLibrary()
container_handle = lib.page_content(document_handle)
aligned_handle = lib.align_center(container_handle)
# After (organized by type):
lib = QuestPDFLibrary()
page = PageDescriptor(lib, page_handle)
container = page.content() # Returns IContainer
aligned = container.align_center() # Returns IContainer
Generated Python Classes:
For each C# type (e.g., PageDescriptor, IContainer, ColumnDescriptor), a Python class is generated with:
__init__(self, lib: QuestPDFLibrary, handle: int) - Constructor accepting library and native handlehandle property - Access to underlying native handle__enter__, __exit__) - For automatic cleanupComplete Usage Example:
from questpdf import QuestPDFLibrary
# Initialize the library
lib = QuestPDFLibrary()
# Create a document
document = lib.create_document()
# Configure pages using PageDescriptor methods
page = document.page()
page.size(PageSize.A4)
page.margin_left(50)
page.margin_right(50)
# Get the content container (returns IContainer)
container = page.content()
# Use IContainer extension methods with fluent chaining
container = container.padding(20)
container = container.align_center()
container = container.background("#FFFFFF")
# Add a column layout (returns ColumnDescriptor)
column = container.column()
column.spacing(10)
# Add items to the column
item1 = column.item()
item1.text("Hello, World!")
item2 = column.item()
item2.text("This is generated from Python!")
# Generate the PDF
document.generate_pdf("output.pdf")
Key Advantages:
container.padding(20).align_center().background("#FFF")Key Methods:
GeneratePythonBindings() - Creates the complete Python bindings fileGetPythonCType() - Maps C# types to ctypes typesGetPythonTypeHint() - Creates Python type hints for IDE supportToPythonMethodName() - Converts PascalCase to snake_caseGeneratePythonFunctionSetup() - Now handles instance methods by adding c_void_p for 'this' parameterGeneratePythonWrapperMethod() - Now generates proper wrappers for instance methodsGenerated Features:
QuestPDFLibrary class - Main wrapper with automatic library discoveryHandle class - Automatic memory management for managed objectsExample for Extension Method (Python):
def align_center(self, container: Handle) -> Handle:
"""
AlignmentExtensions.AlignCenter
"""
result = self._lib.questpdf_fluent_alignmentextensions_aligncenter(
container.value if isinstance(container, Handle) else container
)
return Handle(self, result)
Example for Instance Method (Python):
def align_center(self, this_handle: Handle) -> Handle:
"""
TextBlockDescriptor.AlignCenter
Instance method - requires handle to the object.
"""
result = self._lib.questpdf_fluent_textblockdescriptor_aligncenter(
this_handle.value if isinstance(this_handle, Handle) else this_handle
)
return Handle(self, result)
Purpose: Main orchestrator that combines all components.
Responsibilities:
Pipeline Flow:
Compilation
↓
PublicApiAnalyzer.CollectAllInteropMethods()
├─→ CollectExtensionMethods() (public static extension methods)
└─→ CollectFluentApiMethods() (public instance methods from descriptors, etc.)
↓
[List of IMethodSymbol - BOTH extension methods AND instance methods]
↓
├─→ CSharpInteropGenerator.GenerateInteropCode()
│ ↓
│ GeneratedInterop.g.cs (added to compilation)
│ - Extension methods: static wrappers
│ - Instance methods: wrappers with 'this' parameter
│
└─→ PythonBindingsGenerator.GeneratePythonBindings()
↓
GeneratedInterop.g.py.txt (available in obj folder)
- Extension methods: accept container/object handle
- Instance methods: accept 'this_handle' as first parameter
- Python bindings strictly mirror C# interop functionality
The interop generators now provide complete coverage of the QuestPDF Fluent API, including:
All public static extension methods from classes like:
AlignmentExtensions, PaddingExtensions, RowExtensions, ColumnExtensionsTextExtensions, ImageExtensions, TableExtensions, PageExtensionsDecorationExtensions, LayerExtensions, RotateExtensions, ScaleExtensionsAll public instance methods from descriptor classes including:
Text Descriptors:
TextSpanDescriptor - Style(), FontSize(), FontColor(), etc.TextPageNumberDescriptor - Format(), and all inherited TextSpanDescriptor methodsTextBlockDescriptor - AlignLeft(), AlignCenter(), Justify(), ClampLines(), etc.TextDescriptor - Span(), Line(), EmptyLine(), CurrentPageNumber(), TotalPages(), etc.Table Descriptors:
TableColumnsDefinitionDescriptor - ConstantColumn(), RelativeColumn()TableCellDescriptor - Cell()TableDescriptor - ColumnsDefinition(), Header(), Footer(), Cell(), etc.Layout Descriptors:
PageDescriptor - Size(), Margin(), Content(), Header(), Footer(), etc.ColumnDescriptor - Item(), Spacing()RowDescriptor - AutoItem(), RelativeItem(), ConstantItem()DecorationDescriptor - Before(), Content(), After()LayersDescriptor - Layer(), PrimaryLayer()GridDescriptor - Item(), Columns(), Spacing()Image/SVG Descriptors:
ImageDescriptor - FitArea(), FitWidth(), FitHeight(), etc.DynamicImageDescriptor - Image configuration methodsSvgImageDescriptor - SVG-specific configurationOther Descriptors:
InlinedDescriptor - Item(), Spacing()MultiColumnDescriptor - Columns(), Spacing()Public instance methods from configuration classes:
DocumentOperation.LayerConfiguration - FilePath, TargetPages, SourcePages, etc.DocumentOperation.DocumentAttachment - Key, FilePath, AttachmentName, etc.Any public classes with Builder, Handler, or Settings suffix that expose public instance methods.
The generators run automatically during compilation:
dotnet build QuestPDF/QuestPDF.csproj
QuestPDF/obj/Debug/netX.0/generated/.../GeneratedInterop.g.csQuestPDF/obj/Debug/netX.0/generated/.../GeneratedInterop.g.py.txtfrom questpdf import QuestPDFLibrary, Handle
# Initialize library
pdf = QuestPDFLibrary()
# Use the API (methods converted to snake_case)
container = pdf.create_container()
with container as c:
aligned = pdf.align_right(c) # Returns Handle
padded = pdf.padding(aligned, 10)
# Handles automatically freed when out of scope
questpdf_free_handle exported for cleanupHandle class wraps nint pointers__del__ and context managersTo add support for new types:
PublicApiAnalyzer.IsSupportedType()CSharpInteropGenerator for C# marshallingPythonBindingsGenerator.GetPythonCType() for Python mappingTo exclude specific methods:
PublicApiAnalyzer.IsSupported()