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:
- 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*FACTUexigidos por el régimen VeriFactu (RDL 15/2025). - 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.
- 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"/>
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.
Paso 2: Instalar IronPDF
# Instalar IronPDF (alternativa segura y moderna)
dotnet add package IronPdf
# Instalar IronPDF (alternativa segura y moderna)
dotnet add package IronPdf
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
Paso 4: Inicializar licencia
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY"
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
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
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
// 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
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
// 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
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 |
Factura verificable en la sede electrónica de la AEAT |
SHA-256: {shortHash}... |
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 |
Factura verificable en la sede electrónica de la AEAT |
SHA-256: {shortHash}... |
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 |
Factura verificable en la sede electrónica de la AEAT |
SHA-256: {shortHash}... |
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
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
}
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")
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)
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
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" .
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
- Eliminar todos los paquetes NuGet wkhtmltopdf wrapper
- Eliminar binarios wkhtmltopdf (wkhtmltopdf.exe, wkhtmltox.dll)
- Instalar el paquete NuGet IronPdf
- Actualizar importaciones de espacios de nombres de
WkHtmlToPdfDotNetaIronPdf - Reemplazar
SynchronizedConverterconChromePdfRenderer - Convertir patrones
HtmlToPdfDocumenta métodos de renderizado directo - Actualizar configuraciones de
GlobalSettingsaRenderingOptions - Convertir configuraciones de margen de
MarginSettingsa propiedades individuales - Actualizar sintaxis de marcadores (
[page]→{page},[toPage]→{total-pages}) - Añadir inicialización de licencia IronPDF al inicio
- Para VeriFactu: añadir
HtmlHeaderFootercon leyendaVERI*FACTU, QR AEAT y hash SHA-256 - Para Crea y Crece: habilitar
PdfArchiveFormat.PDF_A_3Ben facturas B2B - 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
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.
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.

