瀏覽代碼

New version

Marcin Ziąbek 1 月之前
父節點
當前提交
e6bcd78736

+ 0 - 0
Source/CLAUDE.md


+ 9 - 6
Source/QuestPDF.InteropGenerators.Tests/QuestPDF.InteropGenerators.Tests.csproj

@@ -2,11 +2,12 @@
 
     <PropertyGroup>
         <TargetFramework>net10.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
+        <ImplicitUsings>disable</ImplicitUsings>
         <Nullable>enable</Nullable>
-
+        <OutputType>Exe</OutputType>
         <IsPackable>false</IsPackable>
-        <IsTestProject>true</IsTestProject>
+        <IsTestProject>false</IsTestProject>
+        <GenerateProgramFile>false</GenerateProgramFile>
     </PropertyGroup>
 
     <ItemGroup>
@@ -21,13 +22,15 @@
         <PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
     </ItemGroup>
 
-    <ItemGroup>
-        <Using Include="NUnit.Framework"/>
-    </ItemGroup>
 
     <ItemGroup>
       <ProjectReference Include="..\QuestPDF.InteropGenerators\QuestPDF.InteropGenerators.csproj" />
       <ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
     </ItemGroup>
 
+
+    <ItemGroup>
+      <Folder Include="_generated\" />
+    </ItemGroup>
+
 </Project>

+ 3 - 2
Source/QuestPDF.InteropGenerators.Tests/Run.cs

@@ -10,7 +10,8 @@ using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.MSBuild;
 
 // TODO: point to your generator type:
-using QuestPDF.InteropGenerators; // e.g., using QuestPDF.InteropGenerators;
+using QuestPDF.InteropGenerators;
+using ISourceGenerator = Microsoft.CodeAnalysis.ISourceGenerator; // e.g., using QuestPDF.InteropGenerators;
 
 internal static class Program
 {
@@ -20,7 +21,7 @@ internal static class Program
     private const string Configuration = "Debug"; // or "Release"
     // Optional: if you need a specific TFM, set: ["TargetFramework"] = "net10.0"
 
-    public static async Task Main()
+    public static async Task Main(string[] args)
     {
         if (!MSBuildLocator.IsRegistered)
             MSBuildLocator.RegisterDefaults();

+ 0 - 15
Source/QuestPDF.InteropGenerators.Tests/UnitTest1.cs

@@ -1,15 +0,0 @@
-namespace QuestPDF.InteropGenerators.Tests;
-
-public class Tests
-{
-    [SetUp]
-    public void Setup()
-    {
-    }
-
-    [Test]
-    public async Task Test1()
-    {
-        await Program.Main();
-    }
-}

+ 464 - 0
Source/QuestPDF.InteropGenerators/ContainerSourceGenerator.cs

@@ -0,0 +1,464 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+
+namespace QuestPDF.InteropGenerators;
+
+/// <summary>
+/// Generates native AOT/C ABI FFI code and Python bindings for QuestPDF IContainer extension methods.
+/// This enables fluent API usage in Python through C# interop.
+/// </summary>
+public class ContainerSourceGenerator : ISourceGenerator
+{
+    /// <summary>
+    /// Generates C# UnmanagedCallersOnly methods for native AOT compilation with C ABI compatibility.
+    /// Each IContainer extension method gets a corresponding FFI wrapper.
+    /// </summary>
+    public string GenerateCSharpCode(INamespaceSymbol namespaceSymbol)
+    {
+        var containerMethods = FindContainerExtensionMethods(namespaceSymbol);
+
+        if (!containerMethods.Any())
+            return "// No IContainer extension methods found";
+
+        var code = new StringBuilder();
+
+        // Generate header
+        code.AppendLine("using System;");
+        code.AppendLine("using System.Runtime.InteropServices;");
+        code.AppendLine("using System.Runtime.CompilerServices;");
+        code.AppendLine("using QuestPDF.Infrastructure;");
+        code.AppendLine("using QuestPDF.Fluent;");
+        code.AppendLine();
+        code.AppendLine("namespace QuestPDF.InteropBindings");
+        code.AppendLine("{");
+        code.AppendLine("    /// <summary>");
+        code.AppendLine("    /// Native AOT FFI bindings for IContainer extension methods");
+        code.AppendLine("    /// </summary>");
+        code.AppendLine("    public static class ContainerInterop");
+        code.AppendLine("    {");
+
+        // Generate handle management
+        code.AppendLine("        private static readonly Dictionary<IntPtr, IContainer> ContainerHandles = new();");
+        code.AppendLine("        private static IntPtr _nextHandle = (IntPtr)1;");
+        code.AppendLine();
+        code.AppendLine("        private static IntPtr AllocateHandle(IContainer container)");
+        code.AppendLine("        {");
+        code.AppendLine("            var handle = _nextHandle;");
+        code.AppendLine("            _nextHandle = (IntPtr)((long)_nextHandle + 1);");
+        code.AppendLine("            ContainerHandles[handle] = container;");
+        code.AppendLine("            return handle;");
+        code.AppendLine("        }");
+        code.AppendLine();
+        code.AppendLine("        private static IContainer GetContainer(IntPtr handle)");
+        code.AppendLine("        {");
+        code.AppendLine("            if (!ContainerHandles.TryGetValue(handle, out var container))");
+        code.AppendLine("                throw new InvalidOperationException($\"Invalid container handle: {handle}\");");
+        code.AppendLine("            return container;");
+        code.AppendLine("        }");
+        code.AppendLine();
+        code.AppendLine("        [UnmanagedCallersOnly(EntryPoint = \"container_release\")]");
+        code.AppendLine("        public static void ReleaseContainer(IntPtr handle)");
+        code.AppendLine("        {");
+        code.AppendLine("            ContainerHandles.Remove(handle);");
+        code.AppendLine("        }");
+        code.AppendLine();
+
+        // Generate FFI methods for each extension method
+        foreach (var method in containerMethods)
+        {
+            GenerateCSharpMethod(code, method);
+        }
+
+        code.AppendLine("    }");
+        code.AppendLine("}");
+
+        return code.ToString();
+    }
+
+    /// <summary>
+    /// Generates Python bindings using CFFI for FFI calls to the C# native AOT library.
+    /// Creates a Python Container class with fluent API methods.
+    /// </summary>
+    public string GeneratePythonCode(INamespaceSymbol namespaceSymbol)
+    {
+        var containerMethods = FindContainerExtensionMethods(namespaceSymbol);
+
+        if (!containerMethods.Any())
+            return "# No IContainer extension methods found";
+
+        var code = new StringBuilder();
+
+        // Generate imports and setup
+        code.AppendLine("from cffi import FFI");
+        code.AppendLine("from typing import Optional, Callable, Any");
+        code.AppendLine("from enum import IntEnum");
+        code.AppendLine();
+        code.AppendLine("# Initialize CFFI");
+        code.AppendLine("ffi = FFI()");
+        code.AppendLine();
+        code.AppendLine("# Define C function signatures");
+        code.AppendLine("ffi.cdef(\"\"\"");
+
+        // Generate C function declarations for CFFI
+        code.AppendLine("    void container_release(void* handle);");
+        foreach (var method in containerMethods)
+        {
+            GenerateCFFISignature(code, method);
+        }
+
+        code.AppendLine("\"\"\")");
+        code.AppendLine();
+        code.AppendLine("# Load the native library");
+        code.AppendLine("_lib = ffi.dlopen('./QuestPDF.Native.dll')  # Adjust path as needed");
+        code.AppendLine();
+        code.AppendLine("class Container:");
+        code.AppendLine("    \"\"\"");
+        code.AppendLine("    Represents a layout structure with exactly one child element.");
+        code.AppendLine("    Provides fluent API for building QuestPDF documents.");
+        code.AppendLine("    \"\"\"");
+        code.AppendLine();
+        code.AppendLine("    def __init__(self, handle):");
+        code.AppendLine("        \"\"\"Initialize container with native handle\"\"\"");
+        code.AppendLine("        self._handle = handle");
+        code.AppendLine();
+        code.AppendLine("    def __del__(self):");
+        code.AppendLine("        \"\"\"Release native resources\"\"\"");
+        code.AppendLine("        if hasattr(self, '_handle') and self._handle:");
+        code.AppendLine("            _lib.container_release(self._handle)");
+        code.AppendLine();
+        code.AppendLine("    @property");
+        code.AppendLine("    def handle(self):");
+        code.AppendLine("        \"\"\"Get the native handle\"\"\"");
+        code.AppendLine("        return self._handle");
+        code.AppendLine();
+
+        // Generate Python methods for each extension method
+        foreach (var method in containerMethods)
+        {
+            GeneratePythonMethod(code, method);
+        }
+
+        return code.ToString();
+    }
+
+    private List<IMethodSymbol> FindContainerExtensionMethods(INamespaceSymbol namespaceSymbol)
+    {
+        var methods = new List<IMethodSymbol>();
+        FindExtensionMethodsRecursive(namespaceSymbol, methods);
+        return methods.Where(m => IsContainerExtensionMethod(m)).ToList();
+    }
+
+    private void FindExtensionMethodsRecursive(INamespaceSymbol namespaceSymbol, List<IMethodSymbol> methods)
+    {
+        // Search in current namespace types
+        foreach (var type in namespaceSymbol.GetTypeMembers())
+        {
+            if (type.IsStatic)
+            {
+                foreach (var member in type.GetMembers().OfType<IMethodSymbol>())
+                {
+                    if (member.IsExtensionMethod)
+                    {
+                        methods.Add(member);
+                    }
+                }
+            }
+        }
+
+        // Recursively search child namespaces
+        foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers())
+        {
+            FindExtensionMethodsRecursive(childNamespace, methods);
+        }
+    }
+
+    private bool IsContainerExtensionMethod(IMethodSymbol method)
+    {
+        if (!method.IsExtensionMethod)
+            return false;
+
+        var firstParam = method.Parameters.FirstOrDefault();
+        if (firstParam == null)
+            return false;
+
+        // Check if the first parameter is IContainer
+        var paramType = firstParam.Type;
+        return paramType.Name == "IContainer" &&
+               paramType.ContainingNamespace?.ToDisplayString() == "QuestPDF.Infrastructure";
+    }
+
+    private void GenerateCSharpMethod(StringBuilder code, IMethodSymbol method)
+    {
+        var methodName = ToSnakeCaseLower(method.Name);
+        var entryPoint = $"container_{methodName}";
+
+        code.AppendLine($"        [UnmanagedCallersOnly(EntryPoint = \"{entryPoint}\")]");
+
+        // Generate method signature
+        var returnType = method.ReturnsVoid ? "void" : "IntPtr";
+        code.Append($"        public static {returnType} {ToPascalCase(method.Name)}(IntPtr containerHandle");
+
+        // Add parameters (skip the first one as it's the extension method's 'this' parameter)
+        foreach (var param in method.Parameters.Skip(1))
+        {
+            code.Append($", {GetCSharpFFIType(param.Type)} {param.Name}");
+        }
+        code.AppendLine(")");
+        code.AppendLine("        {");
+
+        // Generate method body
+        code.AppendLine("            try");
+        code.AppendLine("            {");
+        code.AppendLine("                var container = GetContainer(containerHandle);");
+
+        // Generate the actual method call
+        var callParams = string.Join(", ", method.Parameters.Skip(1).Select(p => ConvertFromFFI(p)));
+
+        if (method.ReturnsVoid)
+        {
+            code.AppendLine($"                container.{method.Name}({callParams});");
+        }
+        else if (IsContainerReturnType(method.ReturnType))
+        {
+            code.AppendLine($"                var result = container.{method.Name}({callParams});");
+            code.AppendLine("                return AllocateHandle(result);");
+        }
+        else
+        {
+            code.AppendLine($"                return container.{method.Name}({callParams});");
+        }
+
+        code.AppendLine("            }");
+        code.AppendLine("            catch");
+        code.AppendLine("            {");
+        code.AppendLine(method.ReturnsVoid ? "                return;" : "                return IntPtr.Zero;");
+        code.AppendLine("            }");
+        code.AppendLine("        }");
+        code.AppendLine();
+    }
+
+    private void GenerateCFFISignature(StringBuilder code, IMethodSymbol method)
+    {
+        var cFunctionName = $"container_{ToSnakeCaseLower(method.Name)}";
+
+        // Generate return type
+        var returnType = method.ReturnsVoid ? "void" : GetCFFIType(method.ReturnType);
+        code.Append($"    {returnType} {cFunctionName}(void* handle");
+
+        // Add parameters (skip the first one as it's the extension method's 'this' parameter)
+        foreach (var param in method.Parameters.Skip(1))
+        {
+            code.Append($", {GetCFFIType(param.Type)} {ToSnakeCaseLower(param.Name)}");
+        }
+
+        code.AppendLine(");");
+    }
+
+    private void GeneratePythonMethod(StringBuilder code, IMethodSymbol method)
+    {
+        var pythonMethodName = ToSnakeCaseLower(method.Name);
+        var doc = DocumentationHelper.ExtractDocumentation(method.GetDocumentationCommentXml());
+
+        // Generate method signature
+        code.Append($"    def {pythonMethodName}(self");
+
+        // Add parameters
+        foreach (var param in method.Parameters.Skip(1))
+        {
+            var paramName = ToSnakeCaseLower(param.Name);
+            var pythonType = GetPythonType(param.Type);
+            var defaultValue = GetPythonDefaultValue(param);
+            code.Append($", {paramName}: {pythonType}{defaultValue}");
+        }
+
+        code.AppendLine("):");
+
+        // Add docstring
+        if (!string.IsNullOrEmpty(doc))
+        {
+            code.AppendLine("        \"\"\"");
+            code.AppendLine($"        {doc}");
+
+            // Add parameter documentation
+            if (method.Parameters.Length > 1)
+            {
+                code.AppendLine();
+                code.AppendLine("        Args:");
+                foreach (var param in method.Parameters.Skip(1))
+                {
+                    var paramName = ToSnakeCaseLower(param.Name);
+                    code.AppendLine($"            {paramName}: {GetPythonType(param.Type)}");
+                }
+            }
+
+            // Add return documentation
+            if (!method.ReturnsVoid && IsContainerReturnType(method.ReturnType))
+            {
+                code.AppendLine();
+                code.AppendLine("        Returns:");
+                code.AppendLine("            Container: Self for method chaining");
+            }
+
+            code.AppendLine("        \"\"\"");
+        }
+
+        // Generate method body
+        var cFunctionName = $"container_{ToSnakeCaseLower(method.Name)}";
+        var callParams = "self._handle";
+
+        foreach (var param in method.Parameters.Skip(1))
+        {
+            var paramName = ToSnakeCaseLower(param.Name);
+            callParams += $", {ConvertToCFFI(param, paramName)}";
+        }
+
+        if (method.ReturnsVoid)
+        {
+            code.AppendLine($"        _lib.{cFunctionName}({callParams})");
+            code.AppendLine("        return self");
+        }
+        else if (IsContainerReturnType(method.ReturnType))
+        {
+            code.AppendLine($"        new_handle = _lib.{cFunctionName}({callParams})");
+            code.AppendLine("        if new_handle != ffi.NULL:");
+            code.AppendLine("            return Container(new_handle)");
+            code.AppendLine("        return self");
+        }
+        else
+        {
+            code.AppendLine($"        return _lib.{cFunctionName}({callParams})");
+        }
+
+        code.AppendLine();
+    }
+
+    private string GetCFFIType(ITypeSymbol type)
+    {
+        if (IsContainerReturnType(type))
+            return "void*";
+
+        return type.SpecialType switch
+        {
+            SpecialType.System_Boolean => "bool",
+            SpecialType.System_Int32 => "int",
+            SpecialType.System_Single => "float",
+            SpecialType.System_Double => "double",
+            SpecialType.System_String => "char*",
+            _ when type.TypeKind == TypeKind.Enum => "int",
+            _ => "void*"
+        };
+    }
+
+    private string GetCSharpFFIType(ITypeSymbol type)
+    {
+        return type.SpecialType switch
+        {
+            SpecialType.System_Boolean => "bool",
+            SpecialType.System_Int32 => "int",
+            SpecialType.System_Single => "float",
+            SpecialType.System_Double => "double",
+            SpecialType.System_String => "IntPtr", // Marshalled as char*
+            _ when type.TypeKind == TypeKind.Enum => "int",
+            _ => "IntPtr"
+        };
+    }
+
+    private string GetPythonType(ITypeSymbol type)
+    {
+        return type.SpecialType switch
+        {
+            SpecialType.System_Boolean => "bool",
+            SpecialType.System_Int32 => "int",
+            SpecialType.System_Single => "float",
+            SpecialType.System_Double => "float",
+            SpecialType.System_String => "str",
+            _ when type.TypeKind == TypeKind.Enum => "int",
+            _ when type.Name == "Action" || type.Name == "Func" => "Callable",
+            _ => "Any"
+        };
+    }
+
+    private string ConvertToCFFI(IParameterSymbol param, string paramName)
+    {
+        if (param.Type.SpecialType == SpecialType.System_String)
+        {
+            return $"{paramName}.encode('utf-8') if isinstance({paramName}, str) else {paramName}";
+        }
+
+        return paramName;
+    }
+
+    private string GetPythonDefaultValue(IParameterSymbol param)
+    {
+        if (!param.HasExplicitDefaultValue)
+            return "";
+
+        if (param.ExplicitDefaultValue == null)
+            return " = None";
+
+        return param.Type.SpecialType switch
+        {
+            SpecialType.System_Boolean => $" = {param.ExplicitDefaultValue.ToString().ToLower()}",
+            SpecialType.System_Int32 or SpecialType.System_Single or SpecialType.System_Double => $" = {param.ExplicitDefaultValue}",
+            SpecialType.System_String => $" = \"{param.ExplicitDefaultValue}\"",
+            _ => ""
+        };
+    }
+
+    private string ConvertFromFFI(IParameterSymbol param)
+    {
+        if (param.Type.SpecialType == SpecialType.System_String)
+        {
+            return $"Marshal.PtrToStringUTF8({param.Name})";
+        }
+
+        if (param.Type.TypeKind == TypeKind.Enum)
+        {
+            return $"({param.Type.Name}){param.Name}";
+        }
+
+        return param.Name;
+    }
+
+
+    private bool IsContainerReturnType(ITypeSymbol type)
+    {
+        return type.Name == "IContainer" &&
+               type.ContainingNamespace?.ToDisplayString() == "QuestPDF.Infrastructure";
+    }
+
+    private string ToSnakeCaseLower(string pascalCase)
+    {
+        if (string.IsNullOrEmpty(pascalCase))
+            return pascalCase;
+
+        var result = new StringBuilder();
+        result.Append(char.ToLower(pascalCase[0]));
+
+        for (int i = 1; i < pascalCase.Length; i++)
+        {
+            if (char.IsUpper(pascalCase[i]))
+            {
+                result.Append('_');
+                result.Append(char.ToLower(pascalCase[i]));
+            }
+            else
+            {
+                result.Append(pascalCase[i]);
+            }
+        }
+
+        return result.ToString();
+    }
+
+    private string ToPascalCase(string input)
+    {
+        if (string.IsNullOrEmpty(input))
+            return input;
+
+        return char.ToUpper(input[0]) + input.Substring(1);
+    }
+}

+ 125 - 0
Source/QuestPDF.InteropGenerators/DocumentationHelper.cs

@@ -0,0 +1,125 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Xml.Linq;
+
+namespace QuestPDF.InteropGenerators;
+
+/// <summary>
+/// Helper class for extracting and processing XML documentation comments
+/// </summary>
+public static class DocumentationHelper
+{
+    /// <summary>
+    /// Extracts documentation from XML documentation comments.
+    /// Processes summary and remarks sections, stripping XML tags and formatting.
+    /// </summary>
+    /// <param name="xmlDocumentation">Raw XML documentation string from Roslyn symbol</param>
+    /// <returns>Cleaned documentation text, or empty string if no documentation exists</returns>
+    public static string ExtractDocumentation(string? xmlDocumentation)
+    {
+        if (string.IsNullOrWhiteSpace(xmlDocumentation))
+            return string.Empty;
+
+        var doc = new StringBuilder();
+
+        try
+        {
+            var xml = XDocument.Parse(xmlDocumentation);
+
+            // Extract summary
+            var summary = xml.Descendants("summary").FirstOrDefault();
+            if (summary != null)
+            {
+                var summaryText = CleanXmlContent(summary);
+                if (!string.IsNullOrWhiteSpace(summaryText))
+                    doc.AppendLine(summaryText);
+            }
+
+            // Extract remarks
+            var remarks = xml.Descendants("remarks").FirstOrDefault();
+            if (remarks != null)
+            {
+                var remarksText = CleanXmlContent(remarks);
+                if (!string.IsNullOrWhiteSpace(remarksText))
+                {
+                    if (doc.Length > 0)
+                        doc.AppendLine();
+                    doc.Append(remarksText);
+                }
+            }
+        }
+        catch
+        {
+            // If XML parsing fails, return empty string
+            return string.Empty;
+        }
+
+        return doc.ToString().Trim();
+    }
+
+    /// <summary>
+    /// Cleans XML content by removing tags like &lt;see cref="..." /&gt; and normalizing whitespace
+    /// </summary>
+    /// <param name="element">XML element to clean</param>
+    /// <returns>Cleaned text content</returns>
+    private static string CleanXmlContent(XElement element)
+    {
+        var text = new StringBuilder();
+
+        foreach (var node in element.Nodes())
+        {
+            if (node is XText textNode)
+            {
+                text.Append(textNode.Value);
+            }
+            else if (node is XElement childElement)
+            {
+                // Handle specific XML elements
+                switch (childElement.Name.LocalName)
+                {
+                    case "see":
+                    case "seealso":
+                        // Extract the referenced name from cref attribute
+                        var cref = childElement.Attribute("cref")?.Value;
+                        if (!string.IsNullOrEmpty(cref))
+                        {
+                            // Extract just the member name (e.g., "Height" from "ConstrainedExtensions.Height")
+                            var parts = cref.Split('.');
+                            var memberName = parts.Length > 0 ? parts[parts.Length - 1] : cref;
+                            text.Append(memberName);
+                        }
+                        else
+                        {
+                            // Fallback to inner text if no cref
+                            text.Append(childElement.Value);
+                        }
+                        break;
+
+                    case "paramref":
+                    case "typeparamref":
+                        // Extract parameter name
+                        var name = childElement.Attribute("name")?.Value ?? childElement.Value;
+                        text.Append(name);
+                        break;
+
+                    case "c":
+                    case "code":
+                        // Inline code or code blocks
+                        text.Append(childElement.Value);
+                        break;
+
+                    default:
+                        // For other elements, just get the text content
+                        text.Append(childElement.Value);
+                        break;
+                }
+            }
+        }
+
+        // Normalize whitespace: collapse multiple spaces/newlines into single spaces
+        var result = text.ToString();
+        result = System.Text.RegularExpressions.Regex.Replace(result, @"\s+", " ");
+        return result.Trim();
+    }
+}

+ 106 - 0
Source/QuestPDF.InteropGenerators/EnumSourceGenerator.cs

@@ -0,0 +1,106 @@
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+namespace QuestPDF.InteropGenerators;
+
+internal class EnumSourceGenerator(string targetNamespace) : ISourceGenerator
+{
+    private string TargetNamespace { get; } = targetNamespace;
+    
+    public string GenerateCSharpCode(INamespaceSymbol namespaceSymbol)
+    {
+        // Enums don't require C ABI interop code generation
+        // They are passed as integers across the FFI boundary
+        return string.Empty;
+    }
+    
+    public string GeneratePythonCode(INamespaceSymbol namespaceSymbol)
+    {
+        var enumSymbol = FindEnumSymbol(namespaceSymbol, TargetNamespace);
+
+        if (enumSymbol == null)
+            return $"# Enum not found: {TargetNamespace}";
+
+        var code = new System.Text.StringBuilder();
+
+        // Generate Python enum class
+        code.AppendLine($"class {enumSymbol.Name}(IntEnum):");
+
+        // Add class docstring from enum documentation
+        var enumDocumentation = DocumentationHelper.ExtractDocumentation(enumSymbol.GetDocumentationCommentXml());
+        if (!string.IsNullOrEmpty(enumDocumentation))
+        {
+            code.AppendLine("    \"\"\"");
+            code.AppendLine($"    {enumDocumentation}");
+            code.AppendLine("    \"\"\"");
+            code.AppendLine();
+        }
+
+        var members = enumSymbol.GetMembers().OfType<IFieldSymbol>().ToList();
+
+        if (members.Count == 0)
+        {
+            code.AppendLine("    pass");
+        }
+        else
+        {
+            for (int i = 0; i < members.Count; i++)
+            {
+                var member = members[i];
+                var value = member.HasConstantValue ? member.ConstantValue : 0;
+
+                // Add blank line between members for readability
+                if (i > 0)
+                    code.AppendLine();
+
+                // Add member with value
+                code.AppendLine($"    {NamingHelper.ToSnakeCase(member.Name)} = {value}");
+
+                // Add member documentation as docstring (visible in IDE IntelliSense)
+                var memberDoc = DocumentationHelper.ExtractDocumentation(member.GetDocumentationCommentXml());
+                if (!string.IsNullOrEmpty(memberDoc))
+                {
+                    // Use triple-quoted docstring with consistent multi-line format
+                    // This makes it visible in PyCharm and other IDE tooltips
+                    code.AppendLine("    \"\"\"");
+
+                    // Handle multi-line documentation
+                    var docLines = memberDoc.Split(new[] { '\n' }, System.StringSplitOptions.RemoveEmptyEntries);
+                    foreach (var line in docLines)
+                    {
+                        code.AppendLine($"    {line.Trim()}");
+                    }
+
+                    code.AppendLine("    \"\"\"");
+                }
+            }
+        }
+
+        code.AppendLine();
+        return code.ToString();
+    }
+
+    private static INamedTypeSymbol? FindEnumSymbol(INamespaceSymbol rootNamespace, string fullyQualifiedName)
+    {
+        // Split the fully qualified name into parts
+        var parts = fullyQualifiedName.Split('.');
+
+        // Navigate to the target namespace
+        var currentNamespace = rootNamespace;
+        for (int i = 0; i < parts.Length - 1; i++)
+        {
+            var nextNamespace = currentNamespace.GetNamespaceMembers()
+                .FirstOrDefault(ns => ns.Name == parts[i]);
+
+            if (nextNamespace == null)
+                return null;
+
+            currentNamespace = nextNamespace;
+        }
+
+        // Find the enum type
+        var enumName = parts[parts.Length - 1];
+        return currentNamespace.GetTypeMembers()
+            .FirstOrDefault(t => t.Name == enumName && t.TypeKind == TypeKind.Enum);
+    }
+}

+ 27 - 0
Source/QuestPDF.InteropGenerators/ISourceGenerator.cs

@@ -0,0 +1,27 @@
+using Microsoft.CodeAnalysis;
+
+namespace QuestPDF.InteropGenerators;
+
+/// <summary>
+/// Interface for generating interop bindings for QuestPDF public API.
+/// Generates both C# native AOT code for C ABI/FFI and Python bindings.
+/// </summary>
+public interface ISourceGenerator
+{
+    /// <summary>
+    /// Generates C# code for native AOT compilation with C ABI compatibility.
+    /// This should produce UnmanagedCallersOnly methods for FFI interop.
+    /// Return empty string if the type doesn't require C interop code (e.g., enums).
+    /// </summary>
+    /// <param name="namespaceSymbol">Root namespace symbol to analyze</param>
+    /// <returns>Generated C# interop code or empty string if not applicable</returns>
+    string GenerateCSharpCode(INamespaceSymbol namespaceSymbol);
+
+    /// <summary>
+    /// Generates Python bindings code using ctypes or similar FFI mechanisms.
+    /// This creates Python classes and functions that wrap the C ABI interface.
+    /// </summary>
+    /// <param name="namespaceSymbol">Root namespace symbol to analyze</param>
+    /// <returns>Generated Python binding code</returns>
+    string GeneratePythonCode(INamespaceSymbol namespaceSymbol);
+}

+ 36 - 0
Source/QuestPDF.InteropGenerators/NamingHelper.cs

@@ -0,0 +1,36 @@
+namespace QuestPDF.InteropGenerators;
+
+/// <summary>
+/// Helper class for naming convention conversions
+/// </summary>
+public static class NamingHelper
+{
+    /// <summary>
+    /// Converts PascalCase to SCREAMING_SNAKE_CASE
+    /// </summary>
+    /// <param name="pascalCase">String in PascalCase format</param>
+    /// <returns>String in SCREAMING_SNAKE_CASE format</returns>
+    public static string ToSnakeCase(string pascalCase)
+    {
+        if (string.IsNullOrEmpty(pascalCase))
+            return pascalCase;
+
+        var result = new System.Text.StringBuilder();
+        result.Append(char.ToUpperInvariant(pascalCase[0]));
+
+        for (int i = 1; i < pascalCase.Length; i++)
+        {
+            if (char.IsUpper(pascalCase[i]))
+            {
+                result.Append('_');
+                result.Append(pascalCase[i]);
+            }
+            else
+            {
+                result.Append(char.ToUpperInvariant(pascalCase[i]));
+            }
+        }
+
+        return result.ToString();
+    }
+}

+ 0 - 0
Source/QuestPDF.InteropGenerators/CSharpInteropGenerator.cs → Source/QuestPDF.InteropGenerators/Old/CSharpInteropGenerator.cs


+ 0 - 0
Source/QuestPDF.InteropGenerators/NewGenerator.cs → Source/QuestPDF.InteropGenerators/Old/NewGenerator.cs


+ 0 - 0
Source/QuestPDF.InteropGenerators/PublicApiAnalyzer.cs → Source/QuestPDF.InteropGenerators/Old/PublicApiAnalyzer.cs


+ 0 - 0
Source/QuestPDF.InteropGenerators/PythonBindingsGenerator.cs → Source/QuestPDF.InteropGenerators/Old/PythonBindingsGenerator.cs


+ 0 - 0
Source/QuestPDF.InteropGenerators/README.md → Source/QuestPDF.InteropGenerators/Old/README.md


+ 35 - 24
Source/QuestPDF.InteropGenerators/PublicApiGenerator.cs

@@ -1,4 +1,6 @@
+using System.Collections.Generic;
 using System.Runtime.Serialization;
+using System.Text;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.Text;
@@ -17,32 +19,41 @@ public sealed class PublicApiGenerator : IIncrementalGenerator
         context.RegisterSourceOutput(context.CompilationProvider, static (spc, compilation) =>
         {
             var content = NewGenerator.AnalyzeAndGenerate(compilation.Assembly.GlobalNamespace);
-            
-            var syntaxTree = CSharpSyntaxTree.ParseText(content);
-            var root = syntaxTree
-                .GetRoot()
-                .NormalizeWhitespace(elasticTrivia: true); // auto-indents and cleans up
 
-            var newCode = root.ToFullString();
-            
-            spc.AddSource("Interop.g.cs", newCode);
-            
+            var csharpBuilder = new StringBuilder();
+            var pythonBuilder = new StringBuilder();
+
+            var generators = new List<ISourceGenerator>
+            {
+                new EnumSourceGenerator("QuestPDF.Infrastructure.AspectRatioOption"),
+                new EnumSourceGenerator("QuestPDF.Infrastructure.ImageCompressionQuality"),
+                new EnumSourceGenerator("QuestPDF.Infrastructure.ImageFormat"),
+                new ContainerSourceGenerator() // Generate interop for IContainer extension methods
+            };
+
+            foreach (var generator in generators)
+            {
+                var csharpCodeFragment = generator.GenerateCSharpCode(compilation.Assembly.GlobalNamespace);
+                csharpBuilder.AppendLine(csharpCodeFragment);
+                
+                var pythonCodeFragment = generator.GeneratePythonCode(compilation.Assembly.GlobalNamespace);
+                pythonBuilder.AppendLine(pythonCodeFragment);
+            }
             
-            // // Step 1: Analyze the public API and collect all interop methods
-            // // This includes both extension methods AND public methods from Fluent API classes
-            // // (descriptors, configurations, handlers, etc.)
-            // var allMethods = PublicApiAnalyzer.CollectAllInteropMethods(compilation.Assembly.GlobalNamespace);
-            //
-            // // Step 2: Generate C# UnmanagedCallersOnly interop code
-            // var csharpInteropCode = CSharpInteropGenerator.GenerateInteropCode(allMethods);
-            // spc.AddSource("GeneratedInterop.g.cs", csharpInteropCode);
-            //
-            // // Step 3: Generate Python ctypes bindings
-            // // Python bindings strictly follow all C# interop functionalities
-            // var pythonBindingsCode = PythonBindingsGenerator.GeneratePythonBindings(allMethods);
-            // // Note: Python file is added as .txt so it appears in generated files
-            // // Extract and rename to .py for actual use
-            // spc.AddSource("GeneratedInterop.g.py.txt", pythonBindingsCode);
+            var csharpCode = csharpBuilder.ToString();
+            var pythonCode = pythonBuilder.ToString();
+
+            // Output C# interop code1
+            if (!string.IsNullOrWhiteSpace(csharpCode))
+            {
+                spc.AddSource("QuestPDF.Interop.g.cs", SourceText.From(csharpCode, System.Text.Encoding.UTF8));
+            }
+
+            // Output Python bindings code
+            if (!string.IsNullOrWhiteSpace(pythonCode))
+            {
+                //spc.AddSource("QuestPDF.Python.py", SourceText.From(pythonCode, System.Text.Encoding.UTF8));
+            }
         });
     }
 }

+ 1 - 1
Source/QuestPDF/Infrastructure/VerticalAlignment.cs

@@ -1,6 +1,6 @@
 namespace QuestPDF.Infrastructure
 {
-    public enum VerticalAlignment
+    internal enum VerticalAlignment
     {
         Top,
         Middle,

+ 1 - 15
Source/QuestPDF/QuestPDF.csproj

@@ -3,7 +3,7 @@
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF</PackageId>
-        <Version>2025.7.3</Version>
+        <Version>2025.7.4</Version>
         <PackageDescription>Generate and edit PDF documents in your .NET applications using the open-source QuestPDF library and its C# Fluent API. Build invoices, reports and data exports with ease.</PackageDescription>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <LangVersion>12</LangVersion>
@@ -25,14 +25,6 @@
         <SymbolPackageFormat>snupkg</SymbolPackageFormat>
         <GenerateDocumentationFile>True</GenerateDocumentationFile>
         <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-
-        <PublishAot>true</PublishAot>
-        <NativeLib>Shared</NativeLib>
-        <SelfContained>true</SelfContained>
-        <InvariantGlobalization>true</InvariantGlobalization>
-        <StripSymbols>true</StripSymbols>
-        <EnableRuntimeMarshalling>true</EnableRuntimeMarshalling>
-        <IlcOptimizationPreference>Speed</IlcOptimizationPreference>
     </PropertyGroup>
 
     <PropertyGroup Condition="'$(BUILD_PACKAGE)' == 'true'">
@@ -53,12 +45,6 @@
         <PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
     </ItemGroup>
     
-    <ItemGroup>
-        <ProjectReference Include="..\QuestPDF.InteropGenerators\QuestPDF.InteropGenerators.csproj"
-                          OutputItemType="Analyzer"
-                          ReferenceOutputAssembly="false" />
-    </ItemGroup>
-    
     <ItemGroup>
         <None Include="Resources\**\*.*">
             <Pack>true</Pack>