Saltar al pie de página
GUíAS DE MIGRACIóN

Migración wkhtmltopdf a IronPDF en España: VeriFactu, ENS y AWS eu-south-2 Madrid

Migración de wkhtmltopdf a IronPDF en España: AWS eu-south-2, Crea y Crece y VeriFactu

La oleada de modernización de infraestructura cloud en España — impulsada por la apertura de la región AWS eu-south-2 (Madrid) en 2022, Azure eu-south-2 y la expansión de Google Cloud en Madrid — ha acelerado la migración de aplicaciones .NET a arquitecturas cloud-native que descartan herramientas de la era pre-contenedor como wkhtmltopdf. Para los equipos que operan portales de administración electrónica, plataformas de medios como los portales RTVE-class o sistemas ERP adyacentes a la AEAT, la presión de migración tiene además una dimensión normativa directa:

  1. Crea y Crece (mandato de facturación electrónica B2B): desde 2027 (empresas con facturación >€8M) y 2028 (resto), todo software de facturación deberá emitir facturas en formato EN 16931 / CIUS-ES. wkhtmltopdf no puede generar el QR de verificación de la AEAT ni la leyenda VERI*FACTU exigidos por el régimen VeriFactu (RDL 15/2025).
  2. ENS Medio/Alto (RD 311/2022): las aplicaciones de la Administración Pública española clasificadas como categoría Medio o Alto bajo el Esquema Nacional de Seguridad no pueden depender de componentes con CVEs críticos activos sin mitigación documentada. wkhtmltopdf lleva el CVE-2022-35583 (CRITICAL 9.8/10) sin parchear desde 2022.
  3. Residencia de datos bajo LOPDGDD: el procesamiento de documentos PDF con datos personales de ciudadanos españoles en infraestructura eu-south-2 (Madrid) es compatible con los requisitos de residencia de datos de la AEPD — pero solo si la biblioteca de renderizado no transmite datos del documento a servicios externos, algo que IronPDF garantiza por diseño.

Por qué wkhtmltopdf bloquea la conformidad con VeriFactu (RDL 15/2025)

VeriFactu es el régimen anti-fraude fiscal español regulado por el RDL 15/2025, obligatorio para software de facturación desde el 29 de julio de 2025. Los tres elementos obligatorios que wkhtmltopdf técnicamente no puede emitir son:

Elemento obligatorio Por qué wkhtmltopdf falla Solución con IronPDF
Leyenda VERI*FACTU en pie de página Qt WebKit 2015: sin soporte CSS moderno para pie de página dinámico con hash en tiempo de renderizado HtmlHeaderFooter con contenido dinámico
QR de verificación AEAT (URL sede electrónica) No puede generar QR ni embeber imágenes dinámicas en tiempo de render QR generado externamente e insertado en HTML
Identificador CSV en facturas B2C Sin capacidad de paginación ni sello temporal en pie de página confiable HtmlHeaderFooter con {page} y datos del sistema

La vulnerabilidad CVE-2022-35583 (SSRF CRÍTICO 9.8/10) introduce además un riesgo específico en contextos VeriFactu: si el PDF generator recibe HTML arbitrario de un usuario malintencionado — escenario frecuente en plataformas de facturación multitenant — wkhtmltopdf puede ser usado para exfiltrar datos de la red interna donde reside la infraestructura de facturación. Para un ISV con exposición de €150.000/año bajo RDL 15/2025, este riesgo combinado hace inviable continuar con wkhtmltopdf.


Alerta de seguridad: CVE-2022-35583

wkhtmltopdf contiene una vulnerabilidad de seguridad crítica permanentemente sin parchear:

Problema Gravedad Estado
CVE-2022-35583 CRÍTICO (9,8/10) SIN PARCHE
Vulnerabilidad SSRF Riesgo de compromiso de infraestructura SIN PARCHE
Última actualización 2016-2017 ABANDONADO
Versión de WebKit 2015 (Qt WebKit) OBSOLETO
Soporte CSS Grid Ninguno Roto
Soporte Flexbox Parcial Roto
ES6+ JavaScript Ninguno Roto

Cómo funciona el ataque SSRF

La vulnerabilidad Server-Side Request Forgery permite a atacantes acceder a servicios internos, robar credenciales, escanear la red interna y exfiltrar datos confidenciales mediante HTML manipulado:


<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
<img src="http://internal-database:5432/admin"/>

<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
<img src="http://internal-database:5432/admin"/>
HTML

Cuando wkhtmltopdf renderiza este HTML, obtiene estas URLs desde el contexto de red del servidor, saltando cortafuegos y controles de seguridad. En una infraestructura eu-south-2 (Madrid) bajo ENS Medio/Alto, este vector es incompatible con los controles de seguridad exigidos (op.exp.4, op.exp.7).

Wrappers .NET afectados

Todos los wrappers .NET para wkhtmltopdf heredan estas vulnerabilidades:

Wrapper Estado Riesgo de seguridad
DinkToPdf Abandonado ⚠ CRÍTICO
Rotativa Abandonado ⚠ CRÍTICO
TuesPechkin Abandonado ⚠ CRÍTICO
wkhtmltopdf-.NET Abandonado ⚠ CRÍTICO
NReco.PdfGenerator Usa wkhtmltopdf ⚠ CRÍTICO

Si usa cualquiera de estas bibliotecas, es vulnerable a CVE-2022-35583.


IronPDF vs wkhtmltopdf: comparación técnica para el mercado español

Característica wkhtmltopdf IronPDF
Licencia LGPLv3 (Gratuito) Comercial
Motor de renderizado Qt WebKit (2015) Chromium actual
CVEs activos CVE-2022-35583 (CRÍTICO 9.8) Sin CVEs conocidos
Mantenimiento Abandonado desde 2017 Actualizaciones regulares
*VeriFactu — leyenda `VERIFACTU`** ❌ No soportado HtmlHeaderFooter
VeriFactu — QR AEAT ❌ No fiable ✅ Imagen embebida en HTML
Crea y Crece — PDF/A-3b ❌ No soportado PdfArchiveFormat.PDF_A_3B
ENS — sin CVEs activos ❌ CVE-2022-35583 sin parche ✅ Sin CVEs conocidos
LOPDGDD — sin transmisión de datos ⚠ No documentado ✅ Procesamiento local
CSS Grid / Flexbox ❌ / ⚠ Roto ✅ Soportado
ES6+ JavaScript ❌ No soportado ✅ Soportado
Firmas digitales (PAdES/eIDAS) ❌ No soportado ✅ Soportado
Async/Await ❌ No soportado ✅ Soportado

Migración rápida: de wkhtmltopdf a IronPDF

La migración puede iniciarse inmediatamente con estos pasos fundamentales.

Paso 1: Eliminar paquetes y binarios de wkhtmltopdf

# Eliminar wrappers wkhtmltopdf
dotnet remove package WkHtmlToPdf-DotNet
dotnet remove package DinkToPdf
dotnet remove package TuesPechkin
dotnet remove package Rotativa
dotnet remove package Rotativa.AspNetCore
dotnet remove package NReco.PdfGenerator

# Eliminar el binario wkhtmltopdf del despliegue
# Borrar wkhtmltopdf.exe, wkhtmltox.dll, etc.
# Eliminar wrappers wkhtmltopdf
dotnet remove package WkHtmlToPdf-DotNet
dotnet remove package DinkToPdf
dotnet remove package TuesPechkin
dotnet remove package Rotativa
dotnet remove package Rotativa.AspNetCore
dotnet remove package NReco.PdfGenerator

# Eliminar el binario wkhtmltopdf del despliegue
# Borrar wkhtmltopdf.exe, wkhtmltox.dll, etc.
SHELL

Paso 2: Instalar IronPDF

# Instalar IronPDF (alternativa segura y moderna)
dotnet add package IronPdf
# Instalar IronPDF (alternativa segura y moderna)
dotnet add package IronPdf
SHELL

Paso 3: Actualizar espacios de nombres

// Antes (wkhtmltopdf)
using WkHtmlToPdfDotNet;
using WkHtmlToPdfDotNet.Contracts;

// Después (IronPDF)
using IronPdf;
// Antes (wkhtmltopdf)
using WkHtmlToPdfDotNet;
using WkHtmlToPdfDotNet.Contracts;

// Después (IronPDF)
using IronPdf;
Imports WkHtmlToPdfDotNet
Imports WkHtmlToPdfDotNet.Contracts

Imports IronPdf
$vbLabelText   $csharpLabel

Paso 4: Inicializar licencia

IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY"
$vbLabelText   $csharpLabel

Ejemplos de migración de código

Convertir HTML a PDF

La operación más fundamental muestra la diferencia de complejidad.

Enfoque wkhtmltopdf:

// NuGet: Install-Package WkHtmlToPdf-DotNet
using WkHtmlToPdfDotNet;
using WkHtmlToPdfDotNet.Contracts;
using System.IO;

class Program
{
    static void Main()
    {
        var converter = new SynchronizedConverter(new PdfTools());
        var doc = new HtmlToPdfDocument()
        {
            GlobalSettings = {
                ColorMode = ColorMode.Color,
                Orientation = Orientation.Portrait,
                PaperSize = PaperKind.A4
            },
            Objects = {
                new ObjectSettings()
                {
                    HtmlContent = "<h1>Hello World</h1><p>This is a PDF from HTML.</p>"
                }
            }
        };
        byte[] pdf = converter.Convert(doc);
        File.WriteAllBytes("output.pdf", pdf);
    }
}
// NuGet: Install-Package WkHtmlToPdf-DotNet
using WkHtmlToPdfDotNet;
using WkHtmlToPdfDotNet.Contracts;
using System.IO;

class Program
{
    static void Main()
    {
        var converter = new SynchronizedConverter(new PdfTools());
        var doc = new HtmlToPdfDocument()
        {
            GlobalSettings = {
                ColorMode = ColorMode.Color,
                Orientation = Orientation.Portrait,
                PaperSize = PaperKind.A4
            },
            Objects = {
                new ObjectSettings()
                {
                    HtmlContent = "<h1>Hello World</h1><p>This is a PDF from HTML.</p>"
                }
            }
        };
        byte[] pdf = converter.Convert(doc);
        File.WriteAllBytes("output.pdf", pdf);
    }
}
' NuGet: Install-Package WkHtmlToPdf-DotNet
Imports WkHtmlToPdfDotNet
Imports WkHtmlToPdfDotNet.Contracts
Imports System.IO

Class Program
    Shared Sub Main()
        Dim converter = New SynchronizedConverter(New PdfTools())
        Dim doc = New HtmlToPdfDocument() With {
            .GlobalSettings = New GlobalSettings() With {
                .ColorMode = ColorMode.Color,
                .Orientation = Orientation.Portrait,
                .PaperSize = PaperKind.A4
            },
            .Objects = {
                New ObjectSettings() With {
                    .HtmlContent = "<h1>Hello World</h1><p>This is a PDF from HTML.</p>"
                }
            }
        }
        Dim pdf As Byte() = converter.Convert(doc)
        File.WriteAllBytes("output.pdf", pdf)
    End Sub
End Class
$vbLabelText   $csharpLabel

Enfoque IronPDF:

// NuGet: Install-Package IronPdf
using IronPdf;
using System;

class Program
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();
        var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><p>This is a PDF from HTML.</p>");
        pdf.SaveAs("output.pdf");
    }
}
// NuGet: Install-Package IronPdf
using IronPdf;
using System;

class Program
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();
        var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><p>This is a PDF from HTML.</p>");
        pdf.SaveAs("output.pdf");
    }
}
Imports IronPdf
Imports System

Class Program
    Shared Sub Main()
        Dim renderer = New ChromePdfRenderer()
        Dim pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><p>This is a PDF from HTML.</p>")
        pdf.SaveAs("output.pdf")
    End Sub
End Class
$vbLabelText   $csharpLabel

wkhtmltopdf requiere crear un SynchronizedConverter con PdfTools, construir un HtmlToPdfDocument con GlobalSettings y Objects, y llamar a converter.Convert() para obtener bytes sin procesar. IronPDF elimina esta ceremonia por completo: ChromePdfRenderer, RenderHtmlAsPdf(), SaveAs().

Convertir URL a PDF

// Antes (wkhtmltopdf) — requiere construcción completa del documento
using WkHtmlToPdfDotNet;
using WkHtmlToPdfDotNet.Contracts;
using System.IO;

class Program
{
    static void Main()
    {
        var converter = new SynchronizedConverter(new PdfTools());
        var doc = new HtmlToPdfDocument()
        {
            GlobalSettings = {
                ColorMode = ColorMode.Color,
                Orientation = Orientation.Portrait,
                PaperSize = PaperKind.A4
            },
            Objects = {
                new ObjectSettings()
                {
                    Page = "https://www.example.com"
                }
            }
        };
        byte[] pdf = converter.Convert(doc);
        File.WriteAllBytes("webpage.pdf", pdf);
    }
}
// Antes (wkhtmltopdf) — requiere construcción completa del documento
using WkHtmlToPdfDotNet;
using WkHtmlToPdfDotNet.Contracts;
using System.IO;

class Program
{
    static void Main()
    {
        var converter = new SynchronizedConverter(new PdfTools());
        var doc = new HtmlToPdfDocument()
        {
            GlobalSettings = {
                ColorMode = ColorMode.Color,
                Orientation = Orientation.Portrait,
                PaperSize = PaperKind.A4
            },
            Objects = {
                new ObjectSettings()
                {
                    Page = "https://www.example.com"
                }
            }
        };
        byte[] pdf = converter.Convert(doc);
        File.WriteAllBytes("webpage.pdf", pdf);
    }
}
Imports WkHtmlToPdfDotNet
Imports WkHtmlToPdfDotNet.Contracts
Imports System.IO

Class Program
    Shared Sub Main()
        Dim converter = New SynchronizedConverter(New PdfTools())
        Dim doc = New HtmlToPdfDocument() With {
            .GlobalSettings = New GlobalSettings() With {
                .ColorMode = ColorMode.Color,
                .Orientation = Orientation.Portrait,
                .PaperSize = PaperKind.A4
            },
            .Objects = New List(Of ObjectSettings) From {
                New ObjectSettings() With {
                    .Page = "https://www.example.com"
                }
            }
        }
        Dim pdf As Byte() = converter.Convert(doc)
        File.WriteAllBytes("webpage.pdf", pdf)
    End Sub
End Class
$vbLabelText   $csharpLabel
// Después (IronPDF)
using IronPdf;
using System;

class Program
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();
        var pdf = renderer.RenderUrlAsPdf("https://www.example.com");
        pdf.SaveAs("webpage.pdf");
    }
}
// Después (IronPDF)
using IronPdf;
using System;

class Program
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();
        var pdf = renderer.RenderUrlAsPdf("https://www.example.com");
        pdf.SaveAs("webpage.pdf");
    }
}
Imports IronPdf
Imports System

Class Program
    Shared Sub Main()
        Dim renderer As New ChromePdfRenderer()
        Dim pdf = renderer.RenderUrlAsPdf("https://www.example.com")
        pdf.SaveAs("webpage.pdf")
    End Sub
End Class
$vbLabelText   $csharpLabel

Configuración de página: orientación, márgenes y tamaño

// Antes (wkhtmltopdf)
using WkHtmlToPdfDotNet;
using WkHtmlToPdfDotNet.Contracts;
using System.IO;

class Program
{
    static void Main()
    {
        var converter = new SynchronizedConverter(new PdfTools());
        var doc = new HtmlToPdfDocument()
        {
            GlobalSettings = {
                ColorMode = ColorMode.Color,
                Orientation = Orientation.Landscape,
                PaperSize = PaperKind.A4,
                Margins = new MarginSettings() { Top = 10, Bottom = 10, Left = 10, Right = 10 }
            },
            Objects = {
                new ObjectSettings()
                {
                    Page = "input.html",
                    WebSettings = { DefaultEncoding = "utf-8" }
                }
            }
        };
        byte[] pdf = converter.Convert(doc);
        File.WriteAllBytes("custom-output.pdf", pdf);
    }
}
// Antes (wkhtmltopdf)
using WkHtmlToPdfDotNet;
using WkHtmlToPdfDotNet.Contracts;
using System.IO;

class Program
{
    static void Main()
    {
        var converter = new SynchronizedConverter(new PdfTools());
        var doc = new HtmlToPdfDocument()
        {
            GlobalSettings = {
                ColorMode = ColorMode.Color,
                Orientation = Orientation.Landscape,
                PaperSize = PaperKind.A4,
                Margins = new MarginSettings() { Top = 10, Bottom = 10, Left = 10, Right = 10 }
            },
            Objects = {
                new ObjectSettings()
                {
                    Page = "input.html",
                    WebSettings = { DefaultEncoding = "utf-8" }
                }
            }
        };
        byte[] pdf = converter.Convert(doc);
        File.WriteAllBytes("custom-output.pdf", pdf);
    }
}
Imports WkHtmlToPdfDotNet
Imports WkHtmlToPdfDotNet.Contracts
Imports System.IO

Class Program
    Shared Sub Main()
        Dim converter = New SynchronizedConverter(New PdfTools())
        Dim doc = New HtmlToPdfDocument() With {
            .GlobalSettings = New GlobalSettings() With {
                .ColorMode = ColorMode.Color,
                .Orientation = Orientation.Landscape,
                .PaperSize = PaperKind.A4,
                .Margins = New MarginSettings() With {.Top = 10, .Bottom = 10, .Left = 10, .Right = 10}
            },
            .Objects = {
                New ObjectSettings() With {
                    .Page = "input.html",
                    .WebSettings = New WebSettings() With {.DefaultEncoding = "utf-8"}
                }
            }
        }
        Dim pdf As Byte() = converter.Convert(doc)
        File.WriteAllBytes("custom-output.pdf", pdf)
    End Sub
End Class
$vbLabelText   $csharpLabel
// Después (IronPDF)
using IronPdf;
using IronPdf.Rendering;
using System;

class Program
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();
        renderer.RenderingOptions.PaperOrientation = PdfPaperOrientation.Landscape;
        renderer.RenderingOptions.MarginTop = 10;
        renderer.RenderingOptions.MarginBottom = 10;
        renderer.RenderingOptions.MarginLeft = 10;
        renderer.RenderingOptions.MarginRight = 10;
        renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;

        var pdf = renderer.RenderHtmlFileAsPdf("input.html");
        pdf.SaveAs("custom-output.pdf");
    }
}
// Después (IronPDF)
using IronPdf;
using IronPdf.Rendering;
using System;

class Program
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();
        renderer.RenderingOptions.PaperOrientation = PdfPaperOrientation.Landscape;
        renderer.RenderingOptions.MarginTop = 10;
        renderer.RenderingOptions.MarginBottom = 10;
        renderer.RenderingOptions.MarginLeft = 10;
        renderer.RenderingOptions.MarginRight = 10;
        renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;

        var pdf = renderer.RenderHtmlFileAsPdf("input.html");
        pdf.SaveAs("custom-output.pdf");
    }
}
Imports IronPdf
Imports IronPdf.Rendering
Imports System

Class Program
    Shared Sub Main()
        Dim renderer As New ChromePdfRenderer()
        renderer.RenderingOptions.PaperOrientation = PdfPaperOrientation.Landscape
        renderer.RenderingOptions.MarginTop = 10
        renderer.RenderingOptions.MarginBottom = 10
        renderer.RenderingOptions.MarginLeft = 10
        renderer.RenderingOptions.MarginRight = 10
        renderer.RenderingOptions.PaperSize = PdfPaperSize.A4

        Dim pdf = renderer.RenderHtmlFileAsPdf("input.html")
        pdf.SaveAs("custom-output.pdf")
    End Sub
End Class
$vbLabelText   $csharpLabel

Generación de recibos VeriFactu con IronPDF (lo que wkhtmltopdf no puede hacer)

Para plataformas de facturación que operan bajo VeriFactu (RDL 15/2025), la migración a IronPDF desbloquea la capacidad de emitir recibos conformes con los tres elementos obligatorios. El siguiente ejemplo muestra cómo incluir la leyenda VERI\*FACTU, el QR de la AEAT y el hash SHA-256 en el pie de página, con datos en formato peninsular (NIF, EUR 1.234,56 €, IVA 21%):

using IronPdf;
using System.Security.Cryptography;

IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.MarginTop = 15;
renderer.RenderingOptions.MarginBottom = 20;

// Factura con datos de empresa española
string facturaHtml = @"
<html>
<body style='font-family: Arial; margin: 40px;'>
    <h1>FACTURA Nº WK-2026-0055</h1>
    <p>Emisor: Portal Digital S.L. | NIF: B-44.556.677</p>
    <p>Receptor: Administración Local | CIF: P-2800100-I</p>
    <table border='1' cellpadding='6' style='width:100%;'>
        <tr><th>Descripción</th><th>Base (€)</th><th>IVA 21%</th><th>Total (€)</th></tr>
        <tr><td>Servicio plataforma SaaS</td>
            <td>1.234,56 €</td><td>259,26 €</td><td>1.493,82 €</td></tr>
    </table>
</body>
</html>";

// Renderizar el recibo con los datos de la factura
var pdf = renderer.RenderHtmlAsPdf(facturaHtml);

// Hash SHA-256 para trazabilidad VeriFactu
string hash = Convert.ToHexString(SHA256.HashData(pdf.BinaryData));
string shortHash = hash[..12];

// Pie de página con leyenda VERI*FACTU obligatoria
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
    HtmlFragment = $@"
        <div style='font-size:9px; color:#555; text-align:center; border-top:1px solid #ccc; padding-top:4px;'>
            VERI*FACTU &nbsp;|&nbsp;
            Factura verificable en la sede electrónica de la AEAT &nbsp;|&nbsp;
            SHA-256: {shortHash}... &nbsp;|&nbsp;
            Página {{page}} de {{total-pages}}
        </div>"
};

// Archivar en PDF/A-3b para cumplimiento Crea y Crece
renderer.RenderingOptions.PdfArchiveFormat = IronPdf.Rendering.PdfArchiveFormat.PDF_A_3B;
using IronPdf;
using System.Security.Cryptography;

IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.MarginTop = 15;
renderer.RenderingOptions.MarginBottom = 20;

// Factura con datos de empresa española
string facturaHtml = @"
<html>
<body style='font-family: Arial; margin: 40px;'>
    <h1>FACTURA Nº WK-2026-0055</h1>
    <p>Emisor: Portal Digital S.L. | NIF: B-44.556.677</p>
    <p>Receptor: Administración Local | CIF: P-2800100-I</p>
    <table border='1' cellpadding='6' style='width:100%;'>
        <tr><th>Descripción</th><th>Base (€)</th><th>IVA 21%</th><th>Total (€)</th></tr>
        <tr><td>Servicio plataforma SaaS</td>
            <td>1.234,56 €</td><td>259,26 €</td><td>1.493,82 €</td></tr>
    </table>
</body>
</html>";

// Renderizar el recibo con los datos de la factura
var pdf = renderer.RenderHtmlAsPdf(facturaHtml);

// Hash SHA-256 para trazabilidad VeriFactu
string hash = Convert.ToHexString(SHA256.HashData(pdf.BinaryData));
string shortHash = hash[..12];

// Pie de página con leyenda VERI*FACTU obligatoria
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
    HtmlFragment = $@"
        <div style='font-size:9px; color:#555; text-align:center; border-top:1px solid #ccc; padding-top:4px;'>
            VERI*FACTU &nbsp;|&nbsp;
            Factura verificable en la sede electrónica de la AEAT &nbsp;|&nbsp;
            SHA-256: {shortHash}... &nbsp;|&nbsp;
            Página {{page}} de {{total-pages}}
        </div>"
};

// Archivar en PDF/A-3b para cumplimiento Crea y Crece
renderer.RenderingOptions.PdfArchiveFormat = IronPdf.Rendering.PdfArchiveFormat.PDF_A_3B;
Imports IronPdf
Imports System.Security.Cryptography

IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY"
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.MarginTop = 15
renderer.RenderingOptions.MarginBottom = 20

' Factura con datos de empresa española
Dim facturaHtml As String = "
<html>
<body style='font-family: Arial; margin: 40px;'>
    <h1>FACTURA Nº WK-2026-0055</h1>
    <p>Emisor: Portal Digital S.L. | NIF: B-44.556.677</p>
    <p>Receptor: Administración Local | CIF: P-2800100-I</p>
    <table border='1' cellpadding='6' style='width:100%;'>
        <tr><th>Descripción</th><th>Base (€)</th><th>IVA 21%</th><th>Total (€)</th></tr>
        <tr><td>Servicio plataforma SaaS</td>
            <td>1.234,56 €</td><td>259,26 €</td><td>1.493,82 €</td></tr>
    </table>
</body>
</html>"

' Renderizar el recibo con los datos de la factura
Dim pdf = renderer.RenderHtmlAsPdf(facturaHtml)

' Hash SHA-256 para trazabilidad VeriFactu
Dim hash As String = Convert.ToHexString(SHA256.HashData(pdf.BinaryData))
Dim shortHash As String = hash.Substring(0, 12)

' Pie de página con leyenda VERI*FACTU obligatoria
renderer.RenderingOptions.HtmlFooter = New HtmlHeaderFooter With {
    .HtmlFragment = $"
        <div style='font-size:9px; color:#555; text-align:center; border-top:1px solid #ccc; padding-top:4px;'>
            VERI*FACTU &nbsp;|&nbsp;
            Factura verificable en la sede electrónica de la AEAT &nbsp;|&nbsp;
            SHA-256: {shortHash}... &nbsp;|&nbsp;
            Página {{page}} de {{total-pages}}
        </div>"
}

' Archivar en PDF/A-3b para cumplimiento Crea y Crece
renderer.RenderingOptions.PdfArchiveFormat = IronPdf.Rendering.PdfArchiveFormat.PDF_A_3B
$vbLabelText   $csharpLabel

Este patrón — completamente imposible con wkhtmltopdf — permite a los ISV españoles distribuir software de facturación conforme con RDL 15/2025 sin exposición a la penalización de €150.000/año.


Referencia de mapeo de API

CLI a IronPDF

Opción CLI wkhtmltopdf Equivalente IronPDF
wkhtmltopdf input.html output.pdf renderer.RenderHtmlFileAsPdf()
wkhtmltopdf URL output.pdf renderer.RenderUrlAsPdf()
--page-size A4 RenderingOptions.PaperSize = PdfPaperSize.A4
--page-size Letter RenderingOptions.PaperSize = PdfPaperSize.Letter
--orientation Landscape RenderingOptions.PaperOrientation = Landscape
--margin-top 10mm RenderingOptions.MarginTop = 10
--margin-bottom 10mm RenderingOptions.MarginBottom = 10
--margin-left 10mm RenderingOptions.MarginLeft = 10
--margin-right 10mm RenderingOptions.MarginRight = 10
--header-html header.html RenderingOptions.HtmlHeader
--footer-center "[page]" {page} marcador de posición
--footer-center "[toPage]" {total-pages} marcador de posición
--enable-javascript Activado por defecto
--javascript-delay 500 RenderingOptions.WaitFor.RenderDelay = 500
--dpi 300 RenderingOptions.Dpi = 300
--grayscale RenderingOptions.GrayScale = true

Wrapper C# a IronPDF

Wrapper wkhtmltopdf IronPDF
SynchronizedConverter ChromePdfRenderer
HtmlToPdfDocument RenderingOptions
GlobalSettings.Out pdf.SaveAs()
GlobalSettings.PaperSize RenderingOptions.PaperSize
GlobalSettings.Orientation RenderingOptions.PaperOrientation
GlobalSettings.Margins RenderingOptions.Margin*
ObjectSettings.Page RenderHtmlFileAsPdf()
ObjectSettings.HtmlContent RenderHtmlAsPdf()
converter.Convert(doc) renderer.RenderHtmlAsPdf()

Migración de marcadores de posición

wkhtmltopdf IronPDF
[page] {page}
[toPage] {total-pages}
[date] {date}
[time] {time}
[title] {html-title}
[url] {url}

Problemas comunes de migración y soluciones

Problema 1: Sintaxis de marcadores de pie de página

wkhtmltopdf: usa sintaxis de corchetes como [page] y [toPage].

Solución: Actualizar a marcadores de llaves de IronPDF:

// Antes (wkhtmltopdf)
FooterSettings = { Left = "Page [page] of [toPage]" }

// Después (IronPDF) — con leyenda VERI*FACTU para conformidad española
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
    HtmlFragment = "<div style='text-align:left;'>Página {page} de {total-pages} | VERI*FACTU</div>",
    MaxHeight = 25
};
// Antes (wkhtmltopdf)
FooterSettings = { Left = "Page [page] of [toPage]" }

// Después (IronPDF) — con leyenda VERI*FACTU para conformidad española
renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
{
    HtmlFragment = "<div style='text-align:left;'>Página {page} de {total-pages} | VERI*FACTU</div>",
    MaxHeight = 25
};
' Antes (wkhtmltopdf)
FooterSettings = New With {.Left = "Page [page] of [toPage]"}

' Después (IronPDF) — con leyenda VERI*FACTU para conformidad española
renderer.RenderingOptions.HtmlFooter = New HtmlHeaderFooter With {
    .HtmlFragment = "<div style='text-align:left;'>Página {page} de {total-pages} | VERI*FACTU</div>",
    .MaxHeight = 25
}
$vbLabelText   $csharpLabel

Problema 2: Configuración del retardo JavaScript

wkhtmltopdf: usa la propiedad JavascriptDelay con fiabilidad limitada.

Solución: IronPDF ofrece múltiples opciones:

renderer.RenderingOptions.EnableJavaScript = true;

// Opción 1: Retardo fijo
renderer.RenderingOptions.WaitFor.RenderDelay(500);

// Opción 2: Esperar un elemento específico (más fiable)
renderer.RenderingOptions.WaitFor.HtmlElementById("content-loaded");

// Opción 3: Esperar condición JavaScript
renderer.RenderingOptions.WaitFor.JavaScript("window.renderComplete === true");
renderer.RenderingOptions.EnableJavaScript = true;

// Opción 1: Retardo fijo
renderer.RenderingOptions.WaitFor.RenderDelay(500);

// Opción 2: Esperar un elemento específico (más fiable)
renderer.RenderingOptions.WaitFor.HtmlElementById("content-loaded");

// Opción 3: Esperar condición JavaScript
renderer.RenderingOptions.WaitFor.JavaScript("window.renderComplete === true");
Imports System

renderer.RenderingOptions.EnableJavaScript = True

' Opción 1: Retardo fijo
renderer.RenderingOptions.WaitFor.RenderDelay(500)

' Opción 2: Esperar un elemento específico (más fiable)
renderer.RenderingOptions.WaitFor.HtmlElementById("content-loaded")

' Opción 3: Esperar condición JavaScript
renderer.RenderingOptions.WaitFor.JavaScript("window.renderComplete = True")
$vbLabelText   $csharpLabel

Problema 3: CSS moderno no renderiza correctamente

Síntoma: Los diseños CSS Grid y Flexbox se muestran incorrectamente en wkhtmltopdf.

Solución: El motor Chromium de IronPDF gestiona correctamente el CSS moderno:

// Este CSS ahora funciona correctamente con IronPDF
var html = @"
<style>
    .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
    .flex { display: flex; justify-content: space-between; align-items: center; }
</style>
<div class='grid'>
    <div>Columna 1</div>
    <div>Columna 2</div>
    <div>Columna 3</div>
</div>";

var pdf = renderer.RenderHtmlAsPdf(html);
// Este CSS ahora funciona correctamente con IronPDF
var html = @"
<style>
    .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
    .flex { display: flex; justify-content: space-between; align-items: center; }
</style>
<div class='grid'>
    <div>Columna 1</div>
    <div>Columna 2</div>
    <div>Columna 3</div>
</div>";

var pdf = renderer.RenderHtmlAsPdf(html);
' Este CSS ahora funciona correctamente con IronPDF
Dim html As String = "
<style>
    .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
    .flex { display: flex; justify-content: space-between; align-items: center; }
</style>
<div class='grid'>
    <div>Columna 1</div>
    <div>Columna 2</div>
    <div>Columna 3</div>
</div>"

Dim pdf = renderer.RenderHtmlAsPdf(html)
$vbLabelText   $csharpLabel

Problema 4: Renderizado síncrono frente a asíncrono

wkhtmltopdf: los wrappers son síncronos y bloquean hilos.

Solución: IronPDF soporta renderizado asíncrono:

public async Task<byte[]> GeneratePdfAsync(string html)
{
    var renderer = new ChromePdfRenderer();
    var pdf = await renderer.RenderHtmlAsPdfAsync(html);
    return pdf.BinaryData;
}
public async Task<byte[]> GeneratePdfAsync(string html)
{
    var renderer = new ChromePdfRenderer();
    var pdf = await renderer.RenderHtmlAsPdfAsync(html);
    return pdf.BinaryData;
}
Imports System.Threading.Tasks

Public Async Function GeneratePdfAsync(html As String) As Task(Of Byte())
    Dim renderer As New ChromePdfRenderer()
    Dim pdf = Await renderer.RenderHtmlAsPdfAsync(html)
    Return pdf.BinaryData
End Function
$vbLabelText   $csharpLabel

Lista de verificación para la migración

Tareas previas a la migración

Auditar el código base para identificar todos los usos de wkhtmltopdf:

# Buscar todas las referencias a wkhtmltopdf
grep -r "WkHtmlToPdfDotNet\|DinkToPdf\|TuesPechkin\|Rotativa" --include="*.cs" .
grep -r "wkhtmltopdf" --include="*.yml" --include="*.yaml" --include="Dockerfile" .
# Buscar todas las referencias a wkhtmltopdf
grep -r "WkHtmlToPdfDotNet\|DinkToPdf\|TuesPechkin\|Rotativa" --include="*.cs" .
grep -r "wkhtmltopdf" --include="*.yml" --include="*.yaml" --include="Dockerfile" .
SHELL

Localizar y documentar binarios wkhtmltopdf para eliminación. Documentar la configuración actual (tamaño de papel, márgenes, encabezados/pies de página). Para entornos VeriFactu y ENS Medio/Alto: documentar todos los puntos del código donde se genera PDF con datos fiscales o personales.

Tareas de actualización de código

  1. Eliminar todos los paquetes NuGet wkhtmltopdf wrapper
  2. Eliminar binarios wkhtmltopdf (wkhtmltopdf.exe, wkhtmltox.dll)
  3. Instalar el paquete NuGet IronPdf
  4. Actualizar importaciones de espacios de nombres de WkHtmlToPdfDotNet a IronPdf
  5. Reemplazar SynchronizedConverter con ChromePdfRenderer
  6. Convertir patrones HtmlToPdfDocument a métodos de renderizado directo
  7. Actualizar configuraciones de GlobalSettings a RenderingOptions
  8. Convertir configuraciones de margen de MarginSettings a propiedades individuales
  9. Actualizar sintaxis de marcadores ([page]{page}, [toPage]{total-pages})
  10. Añadir inicialización de licencia IronPDF al inicio
  11. Para VeriFactu: añadir HtmlHeaderFooter con leyenda VERI*FACTU, QR AEAT y hash SHA-256
  12. Para Crea y Crece: habilitar PdfArchiveFormat.PDF_A_3B en facturas B2B
  13. Para TicketBAI (Bizkaia, Gipuzkoa, Araba): configurar QR foral y leyenda de diputación foral en pie de página

Verificación de seguridad post-migración

# Buscar artefactos wkhtmltopdf restantes
find /var/www/ -name "*wkhtmlto*" 2>/dev/null
find /usr/local/bin/ -name "*wkhtmlto*" 2>/dev/null
docker images | grep wkhtmltopdf

# Verificar que ningún proceso aún lo usa
ps aux | grep wkhtmltopdf
# Buscar artefactos wkhtmltopdf restantes
find /var/www/ -name "*wkhtmlto*" 2>/dev/null
find /usr/local/bin/ -name "*wkhtmlto*" 2>/dev/null
docker images | grep wkhtmltopdf

# Verificar que ningún proceso aún lo usa
ps aux | grep wkhtmltopdf
SHELL

Ventajas clave para equipos en España

Conformidad VeriFactu y ENS: La eliminación de CVE-2022-35583 resuelve una brecha de cumplimiento bajo ENS Medio/Alto (RD 311/2022). IronPDF no tiene CVEs conocidos y recibe actualizaciones de seguridad regulares.

Motor de renderizado moderno: IronPDF usa el motor Chromium actual, garantizando soporte completo de CSS3, CSS Grid, Flexbox y JavaScript ES6+. Los portales de administración electrónica con diseños modernos renderizan correctamente.

VeriFactu y Crea y Crece nativos: HtmlHeaderFooter permite incluir la leyenda VERI*FACTU, el QR de la AEAT y el identificador CSV. PdfArchiveFormat.PDF_A_3B habilita archivado bajo Crea y Crece y la cadena EN 16931 / CIUS-ES. Para plataformas con clientes en el País Vasco, IronPDF gestiona el QR y la leyenda TicketBAI para las tres diputaciones forales (Bizkaia con BATUZ, Gipuzkoa, Araba).

LOPDGDD y residencia de datos: El procesamiento de IronPDF es completamente local — no transmite datos del documento a través de Internet — lo que facilita el cumplimiento de los requisitos de residencia de datos de la AEPD para infraestructura eu-south-2 (Madrid).

Firmas electrónicas eIDAS y FNMT-RCM: IronPDF soporta firmas digitales (PAdES) compatibles con eIDAS para facturas Facturae y documentos con certificados FNMT-RCM y TSPs acreditadas españolas. Las firmas XAdES requeridas por TicketBAI se gestionan en la capa del ISV conforme a eIDAS.

SII (Suministro Inmediato de Información): Para grandes empresas sujetas al SII de la AEAT, IronPDF soporta generación asíncrona nativa con RenderHtmlAsPdfAsync, permitiendo la transmisión XML al sistema tributario en paralelo con la generación del PDF, sin cuellos de botella.

API simplificada: Los métodos de renderizado directo reemplazan patrones de construcción de documentos. El método SaveAs() integrado elimina el manejo manual de bytes.

Compatibilidad asíncrona: Evita el bloqueo de hilos en aplicaciones web de alta carga con soporte nativo async/await — crítico para portales de administración electrónica y plataformas de facturación bajo carga.

Por favor notaDinkToPdf, NReco, Rotativa, TuesPechkin y wkhtmltopdf son marcas registradas de sus respectivos propietarios. Este sitio no está afiliado, respaldado ni patrocinado por DinkToPdf, NReco, Rotativa, TuesPechkin ni wkhtmltopdf. Todos los nombres de productos, logotipos y marcas son propiedad de sus respectivos propietarios. Las comparaciones tienen finalidad exclusivamente informativa y reflejan información públicamente disponible en el momento de su publicación.

Preguntas Frecuentes

¿Por qué wkhtmltopdf es incompatible con VeriFactu (RDL 15/2025)?

wkhtmltopdf no puede emitir la leyenda obligatoria VERI*FACTU (con asterisco en posición central), el código QR de verificación de la sede electrónica de la AEAT ni el identificador CSV requeridos por el RDL 15/2025. Usa Qt WebKit 2015, sin soporte para las capacidades de pie de página dinámico necesarias para incluir el hash SHA-256 en tiempo de renderizado. IronPDF lo resuelve con HtmlHeaderFooter dinámico y procesamiento local.

¿Por qué CVE-2022-35583 bloquea el cumplimiento ENS Medio/Alto?

El Esquema Nacional de Seguridad (RD 311/2022) para categorías Medio y Alto exige que los componentes de software no tengan CVEs críticos activos sin mitigación documentada. CVE-2022-35583 tiene gravedad CRÍTICA 9.8/10 y está permanentemente sin parchear — wkhtmltopdf fue abandonado en 2017. Para aplicaciones de la Administración Pública española o software que procesa datos de la AEAT, esta brecha es directamente incompatible con ENS.

¿Cómo cumple IronPDF con los requisitos LOPDGDD/AEPD en eu-south-2 (Madrid)?

IronPDF procesa todos los documentos PDF localmente, sin transmitir datos del documento a través de Internet. Esto elimina la transferencia de datos personales a servicios externos y facilita el cumplimiento de los requisitos de residencia de datos de la AEPD para infraestructura eu-south-2 (Madrid). La LOPDGDD exige que los datos personales de ciudadanos españoles se procesen en infraestructura dentro de la UE con las garantías adecuadas.

¿Qué penalización conlleva no migrar desde wkhtmltopdf si se usa para software de facturación VeriFactu?

Hasta €150.000/año para el ISV que distribuye el software de facturación no conforme, independientemente del número de usuarios. Si el sistema de facturación usa wkhtmltopdf para generar facturas que deberían incluir la leyenda VERI*FACTU y el QR de la AEAT, el ISV distribuidor asume esta exposición directamente bajo RDL 15/2025.

Curtis Chau
Escritor Técnico

Curtis Chau tiene una licenciatura en Ciencias de la Computación (Carleton University) y se especializa en el desarrollo front-end con experiencia en Node.js, TypeScript, JavaScript y React. Apasionado por crear interfaces de usuario intuitivas y estéticamente agradables, disfruta trabajando con frameworks modernos y creando manuales bien ...

Leer más

Equipo de soporte de Iron

Estamos disponibles online las 24 horas, 5 días a la semana.
Chat
Email
Llámame