Cómo depurar HTML en Chrome para crear PDFs perfectamente píxel
Migración de iText a IronPDF bajo VeriFactu: eliminar la exposición AGPL en software de facturación español
Para el ISV español que usa iText 7 en su software de facturación, 2026 marca un punto de inflexión. La licencia AGPL de iText 7 — que exige publicar el código fuente de toda la aplicación si se distribuye o se presta como servicio — se intersecta con el régimen VeriFactu: la AEAT puede sancionar con hasta €150.000/año a los proveedores de software de facturación no conforme. El componente que genera los PDFs de factura (iText o cualquier alternativa) está en el centro de esa exposición dual.
Esta guía proporciona la ruta de migración completa de iText 7 a IronPDF con foco en las obligaciones del mercado español: generación de facturas con VERI*FACTU y QR de verificación AEAT, firmado PAdES bajo eIDAS con certificados FNMT para TicketBAI (Bizkaia/BATUZ, Gipuzkoa, Araba), archivado PDF/A-3 para Facturae/FACe bajo Crea y Crece, y procesamiento de volumen compatible con el SII.
La exposición AGPL × VeriFactu que hace urgente la migración
La licencia AGPL impone la divulgación del código fuente a cualquier receptor del software o del servicio. Para un ISV de facturación, esto significa:
Si distribuye software de facturación instalable: Cualquier cliente que recibe el producto puede exigir el código fuente completo bajo AGPL — incluyendo los algoritmos de generación de huella encadenada para VeriFactu, la lógica de integración con la AEAT y las rutinas de cálculo fiscal propietarias.
Si presta el servicio de facturación en la nube (SaaS): La AGPL cierra la laguna "ASP loophole" — ofrecer el servicio en red sin distribuir binarios también activa la obligación copyleft. Un SaaS de facturación VeriFactu construido sobre iText 7 AGPL es incompatible con un modelo de negocio propietario.
La intersección con VeriFactu: El régimen VeriFactu sanciona al proveedor del software de facturación — no al usuario final. Si el ISV distribuye software de facturación con iText 7 AGPL sin licencia comercial y sin publicar el código fuente, acumula simultáneamente riesgo de incumplimiento copyleft y riesgo de sanción regulatoria sobre el mismo componente.
La migración a IronPDF elimina el riesgo copyleft: la licencia comercial de IronPDF no impone obligaciones de código abierto sobre el software que lo incorpora.
Cambio de paradigma: de construcción programática a HTML-first
iText 7 requiere construir los documentos PDF con objetos Paragraph, Table, Cell y un sistema de coordenadas propio. IronPDF adopta un paradigma HTML-first: el documento se define en HTML/CSS y el motor Chromium lo renderiza en PDF.
Para el ISV de facturación, este cambio tiene una ventaja concreta: las plantillas de factura con los literales obligatorios VeriFactu (VERI*FACTU, Factura verificable en la sede electrónica de la AEAT) y el código QR de la AEAT se pueden mantener en HTML por el equipo de producto o el departamento de diseño, sin tocar el código C# cada vez que cambia el layout.
Auditoría previa a la migración
Antes de migrar, identificar el alcance del uso de iText en el proyecto:
# En PowerShell — identifica referencias iText en el proyecto
Select-String -Path "**/*.cs" -Pattern "using iText" -Recurse
Select-String -Path "**/*.cs" -Pattern "PdfWriter|PdfDocument|HtmlConverter|PdfMerger" -Recurse
Select-String -Path "**/*.cs" -Pattern "iText\.Signatures|PdfSigner|PrivateKeySignature" -Recurse
Cambio de paquetes NuGet
# Package Manager Console de Visual Studio
Uninstall-Package itext7
Uninstall-Package itext7.pdfhtml
Uninstall-Package iTextSharp
Install-Package IronPdf
Equivalencias API completas: iText 7 → IronPDF
Generación de factura VeriFactu: HTML-first con literales AEAT
// iText 7 AGPL: construcción programática — requiere pdfHTML para HTML-to-PDF
using iText.Html2pdf;
// pdfHTML NO incluido en itext7 base — licencia y paquete separado
using (FileStream htmlSrc = File.Open("factura.html", FileMode.Open))
using (FileStream pdfDest = File.Open("factura.pdf", FileMode.Create))
{
ConverterProperties props = new ConverterProperties();
// CSS limitado: no renderiza flexbox ni grid correctamente
// Los literales VERI*FACTU en HTML pueden no preservarse con fidelidad
HtmlConverter.ConvertToPdf(htmlSrc, pdfDest, props);
}
// IronPDF: motor Chromium integrado — sin complemento adicional
using IronPdf;
string facturaHtml = @"
<!DOCTYPE html>
<html lang='es'>
<head>
<style>
body { font-family: Arial; font-size: 11px; margin: 30px; }
.encabezado { background: #1a3a5c; color: #fff; padding: 15px; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th { background: #2c3e50; color: #fff; padding: 6px; text-align: left; }
td { padding: 6px; border: 1px solid #ddd; }
.total-row td { font-weight: bold; background: #f0f0f0; }
.leyenda-verifactu {
border: 2px solid #1a3a5c; padding: 10px;
text-align: center; font-weight: bold;
margin-top: 20px; font-size: 10px;
}
.qr-aeat { float: right; margin-left: 15px; }
</style>
</head>
<body>
<div class='encabezado'>
<h2>FACTURA NÚM. {{NumeroFactura}}</h2>
<p>{{NombreEmisor}} — NIF: {{NifEmisor}}</p>
<p>Fecha: {{FechaFactura}} | Hora: {{HoraFactura}}</p>
</div>
<table>
<thead>
<tr><th>Descripción</th><th>Cant.</th><th>Precio unit.</th><th>IVA %</th><th>Total</th></tr>
</thead>
<tbody>{{FilasFactura}}</tbody>
<tfoot>
<tr class='total-row'><td colspan='4'>Base imponible</td><td>{{BaseImponible}} €</td></tr>
<tr class='total-row'><td colspan='4'>IVA ({{TipoIva}} %)</td><td>{{CuotaIva}} €</td></tr>
<tr class='total-row'><td colspan='4'><strong>TOTAL</strong></td><td><strong>{{TotalFactura}} €</strong></td></tr>
</tfoot>
</table>
<img class='qr-aeat' src='data:image/png;base64,{{QrAeatBase64}}'
width='90' height='90' alt='QR verificación AEAT' />
<div class='leyenda-verifactu'>
VERI*FACTU<br/>
Factura verificable en la sede electrónica de la AEAT
</div>
<p style='font-size:8px;margin-top:15px;'>
Huella: {{HuellaEncadenada}}
</p>
</body>
</html>";
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
string html = facturaHtml
.Replace("{{NumeroFactura}}", registro.NumeroFactura)
.Replace("{{NifEmisor}}", registro.NifEmisor)
.Replace("{{NombreEmisor}}", registro.NombreEmisor)
.Replace("{{FechaFactura}}", registro.FechaFactura.ToString("dd/MM/yyyy"))
.Replace("{{HoraFactura}}", registro.FechaFactura.ToString("HH:mm:ss"))
.Replace("{{QrAeatBase64}}", qrAeatBase64)
.Replace("{{HuellaEncadenada}}", registro.HuellaEncadenada);
// ... resto de sustituciones de líneas y totales
using var pdfFactura = renderer.RenderHtmlAsPdf(html);
pdfFactura.SaveAs($"factura_{registro.NumeroFactura}.pdf");
// iText 7 AGPL: construcción programática — requiere pdfHTML para HTML-to-PDF
using iText.Html2pdf;
// pdfHTML NO incluido en itext7 base — licencia y paquete separado
using (FileStream htmlSrc = File.Open("factura.html", FileMode.Open))
using (FileStream pdfDest = File.Open("factura.pdf", FileMode.Create))
{
ConverterProperties props = new ConverterProperties();
// CSS limitado: no renderiza flexbox ni grid correctamente
// Los literales VERI*FACTU en HTML pueden no preservarse con fidelidad
HtmlConverter.ConvertToPdf(htmlSrc, pdfDest, props);
}
// IronPDF: motor Chromium integrado — sin complemento adicional
using IronPdf;
string facturaHtml = @"
<!DOCTYPE html>
<html lang='es'>
<head>
<style>
body { font-family: Arial; font-size: 11px; margin: 30px; }
.encabezado { background: #1a3a5c; color: #fff; padding: 15px; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th { background: #2c3e50; color: #fff; padding: 6px; text-align: left; }
td { padding: 6px; border: 1px solid #ddd; }
.total-row td { font-weight: bold; background: #f0f0f0; }
.leyenda-verifactu {
border: 2px solid #1a3a5c; padding: 10px;
text-align: center; font-weight: bold;
margin-top: 20px; font-size: 10px;
}
.qr-aeat { float: right; margin-left: 15px; }
</style>
</head>
<body>
<div class='encabezado'>
<h2>FACTURA NÚM. {{NumeroFactura}}</h2>
<p>{{NombreEmisor}} — NIF: {{NifEmisor}}</p>
<p>Fecha: {{FechaFactura}} | Hora: {{HoraFactura}}</p>
</div>
<table>
<thead>
<tr><th>Descripción</th><th>Cant.</th><th>Precio unit.</th><th>IVA %</th><th>Total</th></tr>
</thead>
<tbody>{{FilasFactura}}</tbody>
<tfoot>
<tr class='total-row'><td colspan='4'>Base imponible</td><td>{{BaseImponible}} €</td></tr>
<tr class='total-row'><td colspan='4'>IVA ({{TipoIva}} %)</td><td>{{CuotaIva}} €</td></tr>
<tr class='total-row'><td colspan='4'><strong>TOTAL</strong></td><td><strong>{{TotalFactura}} €</strong></td></tr>
</tfoot>
</table>
<img class='qr-aeat' src='data:image/png;base64,{{QrAeatBase64}}'
width='90' height='90' alt='QR verificación AEAT' />
<div class='leyenda-verifactu'>
VERI*FACTU<br/>
Factura verificable en la sede electrónica de la AEAT
</div>
<p style='font-size:8px;margin-top:15px;'>
Huella: {{HuellaEncadenada}}
</p>
</body>
</html>";
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
string html = facturaHtml
.Replace("{{NumeroFactura}}", registro.NumeroFactura)
.Replace("{{NifEmisor}}", registro.NifEmisor)
.Replace("{{NombreEmisor}}", registro.NombreEmisor)
.Replace("{{FechaFactura}}", registro.FechaFactura.ToString("dd/MM/yyyy"))
.Replace("{{HoraFactura}}", registro.FechaFactura.ToString("HH:mm:ss"))
.Replace("{{QrAeatBase64}}", qrAeatBase64)
.Replace("{{HuellaEncadenada}}", registro.HuellaEncadenada);
// ... resto de sustituciones de líneas y totales
using var pdfFactura = renderer.RenderHtmlAsPdf(html);
pdfFactura.SaveAs($"factura_{registro.NumeroFactura}.pdf");
Imports iText.Html2pdf
Imports IronPdf
Imports System.IO
' iText 7 AGPL: construcción programática — requiere pdfHTML para HTML-to-PDF
' pdfHTML NO incluido en itext7 base — licencia y paquete separado
Using htmlSrc As FileStream = File.Open("factura.html", FileMode.Open)
Using pdfDest As FileStream = File.Open("factura.pdf", FileMode.Create)
Dim props As New ConverterProperties()
' CSS limitado: no renderiza flexbox ni grid correctamente
' Los literales VERI*FACTU en HTML pueden no preservarse con fidelidad
HtmlConverter.ConvertToPdf(htmlSrc, pdfDest, props)
End Using
End Using
' IronPDF: motor Chromium integrado — sin complemento adicional
Dim facturaHtml As String = "
<!DOCTYPE html>
<html lang='es'>
<head>
<style>
body { font-family: Arial; font-size: 11px; margin: 30px; }
.encabezado { background: #1a3a5c; color: #fff; padding: 15px; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th { background: #2c3e50; color: #fff; padding: 6px; text-align: left; }
td { padding: 6px; border: 1px solid #ddd; }
.total-row td { font-weight: bold; background: #f0f0f0; }
.leyenda-verifactu {
border: 2px solid #1a3a5c; padding: 10px;
text-align: center; font-weight: bold;
margin-top: 20px; font-size: 10px;
}
.qr-aeat { float: right; margin-left: 15px; }
</style>
</head>
<body>
<div class='encabezado'>
<h2>FACTURA NÚM. {{NumeroFactura}}</h2>
<p>{{NombreEmisor}} — NIF: {{NifEmisor}}</p>
<p>Fecha: {{FechaFactura}} | Hora: {{HoraFactura}}</p>
</div>
<table>
<thead>
<tr><th>Descripción</th><th>Cant.</th><th>Precio unit.</th><th>IVA %</th><th>Total</th></tr>
</thead>
<tbody>{{FilasFactura}}</tbody>
<tfoot>
<tr class='total-row'><td colspan='4'>Base imponible</td><td>{{BaseImponible}} €</td></tr>
<tr class='total-row'><td colspan='4'>IVA ({{TipoIva}} %)</td><td>{{CuotaIva}} €</td></tr>
<tr class='total-row'><td colspan='4'><strong>TOTAL</strong></td><td><strong>{{TotalFactura}} €</strong></td></tr>
</tfoot>
</table>
<img class='qr-aeat' src='data:image/png;base64,{{QrAeatBase64}}'
width='90' height='90' alt='QR verificación AEAT' />
<div class='leyenda-verifactu'>
VERI*FACTU<br/>
Factura verificable en la sede electrónica de la AEAT
</div>
<p style='font-size:8px;margin-top:15px;'>
Huella: {{HuellaEncadenada}}
</p>
</body>
</html>"
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4
Dim html As String = facturaHtml _
.Replace("{{NumeroFactura}}", registro.NumeroFactura) _
.Replace("{{NifEmisor}}", registro.NifEmisor) _
.Replace("{{NombreEmisor}}", registro.NombreEmisor) _
.Replace("{{FechaFactura}}", registro.FechaFactura.ToString("dd/MM/yyyy")) _
.Replace("{{HoraFactura}}", registro.FechaFactura.ToString("HH:mm:ss")) _
.Replace("{{QrAeatBase64}}", qrAeatBase64) _
.Replace("{{HuellaEncadenada}}", registro.HuellaEncadenada)
' ... resto de sustituciones de líneas y totales
Using pdfFactura = renderer.RenderHtmlAsPdf(html)
pdfFactura.SaveAs($"factura_{registro.NumeroFactura}.pdf")
End Using
Firma PAdES con certificado FNMT para TicketBAI en los tres territorios vascos
TicketBAI es obligatorio en Bizkaia (con BATUZ), Gipuzkoa y Araba. Cada registro debe ir firmado con XAdES; el PDF de acompañamiento usa PAdES bajo eIDAS. Los certificados válidos son de CA acreditadas como FNMT (Fábrica Nacional de Moneda y Timbre):
// iText 7: firma digital (requiere Bouncy Castle y configuración compleja)
using iText.Kernel.Pdf;
using iText.Signatures;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
Pkcs12Store ks = new Pkcs12Store(new FileStream("cert_fnmt.pfx", FileMode.Open),
"password".ToCharArray());
string alias = ks.Aliases.Cast<string>().First(ks.IsKeyEntry);
ICipherParameters pk = ks.GetKey(alias).Key;
X509CertificateEntry[] chain = ks.GetCertificateChain(alias);
using (PdfReader reader = new PdfReader("factura.pdf"))
using (PdfWriter writer = new PdfWriter("factura_firmada.pdf"))
using (PdfDocument pdfDoc = new PdfDocument(reader, writer))
{
PdfSigner signer = new PdfSigner(pdfDoc, writer, new StampingProperties().UseAppendMode());
signer.GetSignatureAppearance()
.SetReason("TicketBAI — Bizkaia BATUZ")
.SetLocation("Bilbao, Bizkaia");
IExternalSignature pks = new PrivateKeySignature(pk, "SHA-256");
signer.SignDetached(pks, chain, null, null, null, 0, PdfSigner.CryptoStandard.CMS);
}
// IronPDF: PdfSignature — API directa, sin Bouncy Castle
using IronPdf;
using IronPdf.Signing;
using System.Security.Cryptography.X509Certificates;
// Certificado FNMT — gestión segura en producción (Azure Key Vault, HSM)
var certFNMT = new X509Certificate2(
"cert_fnmt.pfx", "password", X509KeyStorageFlags.Exportable);
var firmaPAdES = new PdfSignature(certFNMT)
{
SigningContact = "facturacion@empresa.es",
SigningLocation = "Bilbao, Bizkaia — TicketBAI/BATUZ",
SigningReason = "Factura electrónica TicketBAI — VeriFactu"
};
var pdf = PdfDocument.FromFile("factura.pdf");
pdf.Sign(firmaPAdES);
pdf.SaveAs("factura_firmada_ticketbai.pdf");
// iText 7: firma digital (requiere Bouncy Castle y configuración compleja)
using iText.Kernel.Pdf;
using iText.Signatures;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
Pkcs12Store ks = new Pkcs12Store(new FileStream("cert_fnmt.pfx", FileMode.Open),
"password".ToCharArray());
string alias = ks.Aliases.Cast<string>().First(ks.IsKeyEntry);
ICipherParameters pk = ks.GetKey(alias).Key;
X509CertificateEntry[] chain = ks.GetCertificateChain(alias);
using (PdfReader reader = new PdfReader("factura.pdf"))
using (PdfWriter writer = new PdfWriter("factura_firmada.pdf"))
using (PdfDocument pdfDoc = new PdfDocument(reader, writer))
{
PdfSigner signer = new PdfSigner(pdfDoc, writer, new StampingProperties().UseAppendMode());
signer.GetSignatureAppearance()
.SetReason("TicketBAI — Bizkaia BATUZ")
.SetLocation("Bilbao, Bizkaia");
IExternalSignature pks = new PrivateKeySignature(pk, "SHA-256");
signer.SignDetached(pks, chain, null, null, null, 0, PdfSigner.CryptoStandard.CMS);
}
// IronPDF: PdfSignature — API directa, sin Bouncy Castle
using IronPdf;
using IronPdf.Signing;
using System.Security.Cryptography.X509Certificates;
// Certificado FNMT — gestión segura en producción (Azure Key Vault, HSM)
var certFNMT = new X509Certificate2(
"cert_fnmt.pfx", "password", X509KeyStorageFlags.Exportable);
var firmaPAdES = new PdfSignature(certFNMT)
{
SigningContact = "facturacion@empresa.es",
SigningLocation = "Bilbao, Bizkaia — TicketBAI/BATUZ",
SigningReason = "Factura electrónica TicketBAI — VeriFactu"
};
var pdf = PdfDocument.FromFile("factura.pdf");
pdf.Sign(firmaPAdES);
pdf.SaveAs("factura_firmada_ticketbai.pdf");
Imports iText.Kernel.Pdf
Imports iText.Signatures
Imports Org.BouncyCastle.Pkcs
Imports Org.BouncyCastle.X509
Imports IronPdf
Imports IronPdf.Signing
Imports System.Security.Cryptography.X509Certificates
Imports System.IO
Dim ks As New Pkcs12Store(New FileStream("cert_fnmt.pfx", FileMode.Open), "password".ToCharArray())
Dim aliasName As String = ks.Aliases.Cast(Of String)().First(Function(a) ks.IsKeyEntry(a))
Dim pk As ICipherParameters = ks.GetKey(aliasName).Key
Dim chain As X509CertificateEntry() = ks.GetCertificateChain(aliasName)
Using reader As New PdfReader("factura.pdf"),
writer As New PdfWriter("factura_firmada.pdf"),
pdfDoc As New PdfDocument(reader, writer)
Dim signer As New PdfSigner(pdfDoc, writer, New StampingProperties().UseAppendMode())
signer.GetSignatureAppearance().
SetReason("TicketBAI — Bizkaia BATUZ").
SetLocation("Bilbao, Bizkaia")
Dim pks As IExternalSignature = New PrivateKeySignature(pk, "SHA-256")
signer.SignDetached(pks, chain, Nothing, Nothing, Nothing, 0, PdfSigner.CryptoStandard.CMS)
End Using
' IronPDF: PdfSignature — API directa, sin Bouncy Castle
Dim certFNMT As New X509Certificate2("cert_fnmt.pfx", "password", X509KeyStorageFlags.Exportable)
Dim firmaPAdES As New PdfSignature(certFNMT) With {
.SigningContact = "facturacion@empresa.es",
.SigningLocation = "Bilbao, Bizkaia — TicketBAI/BATUZ",
.SigningReason = "Factura electrónica TicketBAI — VeriFactu"
}
Dim pdf As PdfDocument = PdfDocument.FromFile("factura.pdf")
pdf.Sign(firmaPAdES)
pdf.SaveAs("factura_firmada_ticketbai.pdf")
La diferencia de complejidad es significativa: iText 7 requiere Bouncy Castle para gestionar el keystore PKCS12 y construir manualmente la cadena de certificados. IronPDF usa directamente X509Certificate2 del runtime .NET.
Fusión de PDFs para facturas con anexos
// iText 7: PdfMerger
using iText.Kernel.Pdf;
using iText.Kernel.Utils;
PdfDocument resultado = new PdfDocument(new PdfWriter("factura_completa.pdf"));
PdfMerger merger = new PdfMerger(resultado);
string[] archivos = { "factura_principal.pdf", "anexo_desglose.pdf" };
foreach (string archivo in archivos)
{
PdfDocument src = new PdfDocument(new PdfReader(archivo));
merger.Merge(src, 1, src.GetNumberOfPages());
src.Close();
}
resultado.Close();
// IronPDF: Merge estático
using IronPdf;
var pdfs = new[] { "factura_principal.pdf", "anexo_desglose.pdf" }
.Select(PdfDocument.FromFile).ToList();
var fusionado = PdfDocument.Merge(pdfs);
fusionado.SaveAs("factura_completa.pdf");
pdfs.ForEach(p => p.Dispose());
// iText 7: PdfMerger
using iText.Kernel.Pdf;
using iText.Kernel.Utils;
PdfDocument resultado = new PdfDocument(new PdfWriter("factura_completa.pdf"));
PdfMerger merger = new PdfMerger(resultado);
string[] archivos = { "factura_principal.pdf", "anexo_desglose.pdf" };
foreach (string archivo in archivos)
{
PdfDocument src = new PdfDocument(new PdfReader(archivo));
merger.Merge(src, 1, src.GetNumberOfPages());
src.Close();
}
resultado.Close();
// IronPDF: Merge estático
using IronPdf;
var pdfs = new[] { "factura_principal.pdf", "anexo_desglose.pdf" }
.Select(PdfDocument.FromFile).ToList();
var fusionado = PdfDocument.Merge(pdfs);
fusionado.SaveAs("factura_completa.pdf");
pdfs.ForEach(p => p.Dispose());
Imports iText.Kernel.Pdf
Imports iText.Kernel.Utils
Imports IronPdf
Dim resultado As New PdfDocument(New PdfWriter("factura_completa.pdf"))
Dim merger As New PdfMerger(resultado)
Dim archivos As String() = {"factura_principal.pdf", "anexo_desglose.pdf"}
For Each archivo As String In archivos
Dim src As New PdfDocument(New PdfReader(archivo))
merger.Merge(src, 1, src.GetNumberOfPages())
src.Close()
Next
resultado.Close()
Dim pdfs = {"factura_principal.pdf", "anexo_desglose.pdf"} _
.Select(Function(file) PdfDocument.FromFile(file)).ToList()
Dim fusionado = PdfDocument.Merge(pdfs)
fusionado.SaveAs("factura_completa.pdf")
pdfs.ForEach(Sub(p) p.Dispose())
Archivado PDF/A-3 para Facturae + FACe (Crea y Crece)
La ley Crea y Crece obliga a la facturación electrónica B2B. El calendario (>8 M€ en 2027; todos los demás en 2028) implica que los ISVs deben soportar PDF/A-3 con XML Facturae 3.2.2 embebido, conforme con EN 16931 / CIUS-ES:
// iText 7: PDF/A (sin soporte directo para PDF/A-3 con adjuntos en versión base)
// Requiere configuración adicional de PdfOutputIntent
// IronPDF: PDF/A-3B con archivo embebido — más directo
using IronPdf;
var pdf = PdfDocument.FromFile("factura_base.pdf");
// Embeber XML Facturae 3.2.2 firmado con XAdES
byte[] xmlFacturae = File.ReadAllBytes("factura_B12345678_2027001234.xml");
pdf.EmbedFile(xmlFacturae, "factura.xml", "application/xml");
// Convertir a PDF/A-3B — permite adjuntos y es el formato requerido por FACe
pdf.SaveAsPdfA("factura_face_pdfa3b.pdf", PdfAVersions.PdfA3B);
// iText 7: PDF/A (sin soporte directo para PDF/A-3 con adjuntos en versión base)
// Requiere configuración adicional de PdfOutputIntent
// IronPDF: PDF/A-3B con archivo embebido — más directo
using IronPdf;
var pdf = PdfDocument.FromFile("factura_base.pdf");
// Embeber XML Facturae 3.2.2 firmado con XAdES
byte[] xmlFacturae = File.ReadAllBytes("factura_B12345678_2027001234.xml");
pdf.EmbedFile(xmlFacturae, "factura.xml", "application/xml");
// Convertir a PDF/A-3B — permite adjuntos y es el formato requerido por FACe
pdf.SaveAsPdfA("factura_face_pdfa3b.pdf", PdfAVersions.PdfA3B);
Imports IronPdf
' iText 7: PDF/A (sin soporte directo para PDF/A-3 con adjuntos en versión base)
' Requiere configuración adicional de PdfOutputIntent
' IronPDF: PDF/A-3B con archivo embebido — más directo
Dim pdf = PdfDocument.FromFile("factura_base.pdf")
' Embeber XML Facturae 3.2.2 firmado con XAdES
Dim xmlFacturae As Byte() = File.ReadAllBytes("factura_B12345678_2027001234.xml")
pdf.EmbedFile(xmlFacturae, "factura.xml", "application/xml")
' Convertir a PDF/A-3B — permite adjuntos y es el formato requerido por FACe
pdf.SaveAsPdfA("factura_face_pdfa3b.pdf", PdfAVersions.PdfA3B)
Cifrado bajo LOPDGDD para protección de datos personales
// iText 7: WriterProperties + EncryptionConstants
using iText.Kernel.Pdf;
byte[] ownerPwd = System.Text.Encoding.UTF8.GetBytes("admin-password");
byte[] userPwd = System.Text.Encoding.UTF8.GetBytes("user-password");
WriterProperties wp = new WriterProperties()
.SetStandardEncryption(
userPwd, ownerPwd,
EncryptionConstants.ALLOW_PRINTING,
EncryptionConstants.ENCRYPTION_AES_256);
using (PdfDocument pdfDoc = new PdfDocument(new PdfReader("factura.pdf"),
new PdfWriter("factura_protegida.pdf", wp)))
{
pdfDoc.Close();
}
// IronPDF: SecuritySettings — más legible
using IronPdf;
var pdfSec = PdfDocument.FromFile("factura.pdf");
pdfSec.SecuritySettings.OwnerPassword = "admin-password";
pdfSec.SecuritySettings.UserPassword = "user-password";
pdfSec.SecuritySettings.AllowUserPrinting = IronPdf.Security.PdfPrintSecurity.FullPrintRights;
pdfSec.SecuritySettings.AllowUserCopyPasteContent = false;
pdfSec.SecuritySettings.AllowUserAnnotations = false;
pdfSec.SaveAs("factura_protegida_lopdgdd.pdf");
// iText 7: WriterProperties + EncryptionConstants
using iText.Kernel.Pdf;
byte[] ownerPwd = System.Text.Encoding.UTF8.GetBytes("admin-password");
byte[] userPwd = System.Text.Encoding.UTF8.GetBytes("user-password");
WriterProperties wp = new WriterProperties()
.SetStandardEncryption(
userPwd, ownerPwd,
EncryptionConstants.ALLOW_PRINTING,
EncryptionConstants.ENCRYPTION_AES_256);
using (PdfDocument pdfDoc = new PdfDocument(new PdfReader("factura.pdf"),
new PdfWriter("factura_protegida.pdf", wp)))
{
pdfDoc.Close();
}
// IronPDF: SecuritySettings — más legible
using IronPdf;
var pdfSec = PdfDocument.FromFile("factura.pdf");
pdfSec.SecuritySettings.OwnerPassword = "admin-password";
pdfSec.SecuritySettings.UserPassword = "user-password";
pdfSec.SecuritySettings.AllowUserPrinting = IronPdf.Security.PdfPrintSecurity.FullPrintRights;
pdfSec.SecuritySettings.AllowUserCopyPasteContent = false;
pdfSec.SecuritySettings.AllowUserAnnotations = false;
pdfSec.SaveAs("factura_protegida_lopdgdd.pdf");
Imports iText.Kernel.Pdf
Imports IronPdf
Dim ownerPwd As Byte() = System.Text.Encoding.UTF8.GetBytes("admin-password")
Dim userPwd As Byte() = System.Text.Encoding.UTF8.GetBytes("user-password")
Dim wp As New WriterProperties() _
.SetStandardEncryption(userPwd, ownerPwd, EncryptionConstants.ALLOW_PRINTING, EncryptionConstants.ENCRYPTION_AES_256)
Using pdfDoc As New PdfDocument(New PdfReader("factura.pdf"), New PdfWriter("factura_protegida.pdf", wp))
pdfDoc.Close()
End Using
Dim pdfSec = PdfDocument.FromFile("factura.pdf")
pdfSec.SecuritySettings.OwnerPassword = "admin-password"
pdfSec.SecuritySettings.UserPassword = "user-password"
pdfSec.SecuritySettings.AllowUserPrinting = IronPdf.Security.PdfPrintSecurity.FullPrintRights
pdfSec.SecuritySettings.AllowUserCopyPasteContent = False
pdfSec.SecuritySettings.AllowUserAnnotations = False
pdfSec.SaveAs("factura_protegida_lopdgdd.pdf")
Extracción de texto (validación de registros VeriFactu)
// iText 7: PdfTextExtractor
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas.Parser;
using (PdfDocument pdfDoc = new PdfDocument(new PdfReader("factura.pdf")))
{
for (int i = 1; i <= pdfDoc.GetNumberOfPages(); i++)
{
string texto = PdfTextExtractor.GetTextFromPage(pdfDoc.GetPage(i));
// Validar que VERI*FACTU está presente en el texto extraído
if (!texto.Contains("VERI*FACTU"))
throw new InvalidOperationException($"Leyenda VeriFactu ausente en página {i}");
}
}
// IronPDF: ExtractAllText o acceso por página
using IronPdf;
var pdf = PdfDocument.FromFile("factura.pdf");
string textoCompleto = pdf.ExtractAllText();
if (!textoCompleto.Contains("VERI*FACTU"))
throw new InvalidOperationException("Leyenda VERI*FACTU no encontrada en el PDF");
// iText 7: PdfTextExtractor
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas.Parser;
using (PdfDocument pdfDoc = new PdfDocument(new PdfReader("factura.pdf")))
{
for (int i = 1; i <= pdfDoc.GetNumberOfPages(); i++)
{
string texto = PdfTextExtractor.GetTextFromPage(pdfDoc.GetPage(i));
// Validar que VERI*FACTU está presente en el texto extraído
if (!texto.Contains("VERI*FACTU"))
throw new InvalidOperationException($"Leyenda VeriFactu ausente en página {i}");
}
}
// IronPDF: ExtractAllText o acceso por página
using IronPdf;
var pdf = PdfDocument.FromFile("factura.pdf");
string textoCompleto = pdf.ExtractAllText();
if (!textoCompleto.Contains("VERI*FACTU"))
throw new InvalidOperationException("Leyenda VERI*FACTU no encontrada en el PDF");
Imports iText.Kernel.Pdf
Imports iText.Kernel.Pdf.Canvas.Parser
Imports IronPdf
Using pdfDoc As New PdfDocument(New PdfReader("factura.pdf"))
For i As Integer = 1 To pdfDoc.GetNumberOfPages()
Dim texto As String = PdfTextExtractor.GetTextFromPage(pdfDoc.GetPage(i))
' Validar que VERI*FACTU está presente en el texto extraído
If Not texto.Contains("VERI*FACTU") Then
Throw New InvalidOperationException($"Leyenda VeriFactu ausente en página {i}")
End If
Next
End Using
Dim pdf = PdfDocument.FromFile("factura.pdf")
Dim textoCompleto As String = pdf.ExtractAllText()
If Not textoCompleto.Contains("VERI*FACTU") Then
Throw New InvalidOperationException("Leyenda VERI*FACTU no encontrada en el PDF")
End If
Tabla de referencia: mapeo de clases y namespaces
| iText 7 | IronPDF | Observación |
|---|---|---|
PdfWriter |
ChromePdfRenderer |
Paradigma HTML-first |
PdfDocument (escritura) |
ChromePdfRenderer.RenderHtmlAsPdf() |
|
PdfDocument (lectura) |
PdfDocument.FromFile() |
|
Document |
HTML template | |
Paragraph |
<p>, <h1>, etc. en HTML |
|
Table + Cell |
<table>, <td> en HTML |
|
PdfReader |
PdfDocument.FromFile() |
|
PdfMerger |
PdfDocument.Merge() |
|
PdfTextExtractor |
pdf.ExtractAllText() |
|
PdfSigner + Bouncy Castle |
PdfSignature(X509Certificate2) |
API más directa |
HtmlConverter (pdfHTML) |
Incluido en ChromePdfRenderer |
Sin paquete separado |
iText.Kernel.Pdf namespace |
IronPdf namespace |
|
iText.Layout namespace |
HTML/CSS | |
iText.Html2Pdf namespace |
IronPdf (integrado) |
|
iText.Signatures namespace |
IronPdf.Signing |
|
| Páginas 1-indexed | Páginas 0-indexed | Cambio crítico en la migración |
Rendimiento para SII: generación de alto volumen
El Suministro Inmediato de Información (SII) exige comunicar el detalle de facturas en tiempo casi real a la AEAT. Los ISVs que sirven a empresas con obligación SII deben generar miles de PDFs en el plazo de 4 días hábiles. El patrón recomendado con IronPDF reutiliza el renderer para minimizar la inicialización del motor Chromium:
// Servicio de generación para pipelines de alto volumen (SII / Crea y Crece)
public class ServicioFacturacionVeriFactu
{
// Instancia estática — inicialización única del motor Chromium
private static readonly ChromePdfRenderer _renderer = new ChromePdfRenderer
{
RenderingOptions = new ChromePdfRenderOptions
{
PaperSize = IronPdf.Rendering.PdfPaperSize.A4,
EnableJavaScript = false, // No necesario para facturas HTML estático
Timeout = 8000 // 8 segundos por factura
}
};
private readonly IPlantillaService _plantillas;
public ServicioFacturacionVeriFactu(IPlantillaService plantillas)
{
_plantillas = plantillas;
}
public async Task<byte[]> GenerarPdfFacturaAsync(RegistroVeriFactu registro)
{
// La plantilla incluye literales VERI*FACTU y placeholder de QR AEAT
string html = await _plantillas.RenderizarFacturaAsync(registro);
using var pdf = _renderer.RenderHtmlAsPdf(html);
return pdf.BinaryData;
}
public async Task<string> GenerarYFirmarAsync(RegistroVeriFactu registro,
X509Certificate2 certFNMT, string outputPath)
{
byte[] pdfBytes = await GenerarPdfFacturaAsync(registro);
using var pdf = PdfDocument.FromBytes(pdfBytes);
var firma = new PdfSignature(certFNMT)
{
SigningReason = $"TicketBAI — {registro.ProvinciaVasca}"
};
pdf.Sign(firma);
pdf.SaveAs(outputPath);
return outputPath;
}
}
// Servicio de generación para pipelines de alto volumen (SII / Crea y Crece)
public class ServicioFacturacionVeriFactu
{
// Instancia estática — inicialización única del motor Chromium
private static readonly ChromePdfRenderer _renderer = new ChromePdfRenderer
{
RenderingOptions = new ChromePdfRenderOptions
{
PaperSize = IronPdf.Rendering.PdfPaperSize.A4,
EnableJavaScript = false, // No necesario para facturas HTML estático
Timeout = 8000 // 8 segundos por factura
}
};
private readonly IPlantillaService _plantillas;
public ServicioFacturacionVeriFactu(IPlantillaService plantillas)
{
_plantillas = plantillas;
}
public async Task<byte[]> GenerarPdfFacturaAsync(RegistroVeriFactu registro)
{
// La plantilla incluye literales VERI*FACTU y placeholder de QR AEAT
string html = await _plantillas.RenderizarFacturaAsync(registro);
using var pdf = _renderer.RenderHtmlAsPdf(html);
return pdf.BinaryData;
}
public async Task<string> GenerarYFirmarAsync(RegistroVeriFactu registro,
X509Certificate2 certFNMT, string outputPath)
{
byte[] pdfBytes = await GenerarPdfFacturaAsync(registro);
using var pdf = PdfDocument.FromBytes(pdfBytes);
var firma = new PdfSignature(certFNMT)
{
SigningReason = $"TicketBAI — {registro.ProvinciaVasca}"
};
pdf.Sign(firma);
pdf.SaveAs(outputPath);
return outputPath;
}
}
Imports IronPdf
Imports System.Security.Cryptography.X509Certificates
Imports System.Threading.Tasks
' Servicio de generación para pipelines de alto volumen (SII / Crea y Crece)
Public Class ServicioFacturacionVeriFactu
' Instancia estática — inicialización única del motor Chromium
Private Shared ReadOnly _renderer As New ChromePdfRenderer With {
.RenderingOptions = New ChromePdfRenderOptions With {
.PaperSize = IronPdf.Rendering.PdfPaperSize.A4,
.EnableJavaScript = False, ' No necesario para facturas HTML estático
.Timeout = 8000 ' 8 segundos por factura
}
}
Private ReadOnly _plantillas As IPlantillaService
Public Sub New(plantillas As IPlantillaService)
_plantillas = plantillas
End Sub
Public Async Function GenerarPdfFacturaAsync(registro As RegistroVeriFactu) As Task(Of Byte())
' La plantilla incluye literales VERI*FACTU y placeholder de QR AEAT
Dim html As String = Await _plantillas.RenderizarFacturaAsync(registro)
Using pdf = _renderer.RenderHtmlAsPdf(html)
Return pdf.BinaryData
End Using
End Function
Public Async Function GenerarYFirmarAsync(registro As RegistroVeriFactu, certFNMT As X509Certificate2, outputPath As String) As Task(Of String)
Dim pdfBytes As Byte() = Await GenerarPdfFacturaAsync(registro)
Using pdf = PdfDocument.FromBytes(pdfBytes)
Dim firma As New PdfSignature(certFNMT) With {
.SigningReason = $"TicketBAI — {registro.ProvinciaVasca}"
}
pdf.Sign(firma)
pdf.SaveAs(outputPath)
Return outputPath
End Using
End Function
End Class
Checklist de migración
- [ ] Reemplazar
itext7yitext7.pdfhtmlen el archivo.csproj - [ ] Instalar
IronPdfy configurar la clave de licencia - [ ] Migrar constructores HTML (
HtmlConverter) aChromePdfRenderer.RenderHtmlAsPdf() - [ ] Actualizar indexación de páginas: iText 1-indexed → IronPDF 0-indexed
- [ ] Migrar firmas de Bouncy Castle +
PdfSigneraPdfSignature(X509Certificate2) - [ ] Implementar
SaveAsPdfA(PdfAVersions.PdfA3B)conEmbedFile()para XML Facturae - [ ] Validar literales
VERI*FACTUyFactura verificable en la sede electrónica de la AEATen las plantillas HTML - [ ] Verificar formato español de números (1.234,56 €) en las plantillas
- [ ] Migrar cifrado iText (
EncryptionConstants) a IronPDF (SecuritySettings) - [ ] Ejecutar tests de regresión sobre PDFs de factura generados
Recursos para el mercado español
- Documentación IronPDF en español
- Firma digital PAdES con certificados FNMT bajo eIDAS
- PDF/A-3 para Facturae y Crea y Crece
- HTML a PDF con Chromium — plantillas con VERI*FACTU
Inicia tu evaluación técnica con acceso completo 30 días para validar la migración en tu arquitectura VeriFactu.

