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

Migración de iText a IronPDF bajo VeriFactu: eliminar la exposición AGPL en software de facturación español

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
$vbLabelText   $csharpLabel

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")
$vbLabelText   $csharpLabel

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())
$vbLabelText   $csharpLabel

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)
$vbLabelText   $csharpLabel

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")
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

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
$vbLabelText   $csharpLabel

Checklist de migración

  • [ ] Reemplazar itext7 y itext7.pdfhtml en el archivo .csproj
  • [ ] Instalar IronPdf y configurar la clave de licencia
  • [ ] Migrar constructores HTML (HtmlConverter) a ChromePdfRenderer.RenderHtmlAsPdf()
  • [ ] Actualizar indexación de páginas: iText 1-indexed → IronPDF 0-indexed
  • [ ] Migrar firmas de Bouncy Castle + PdfSigner a PdfSignature(X509Certificate2)
  • [ ] Implementar SaveAsPdfA(PdfAVersions.PdfA3B) con EmbedFile() para XML Facturae
  • [ ] Validar literales VERI*FACTU y Factura verificable en la sede electrónica de la AEAT en 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

Inicia tu evaluación técnica con acceso completo 30 días para validar la migración en tu arquitectura VeriFactu.

Por favor notaiText es una marca registrada de sus respectivos propietarios. Este sitio no está afiliado, patrocinado ni respaldado por iText Group. Todos los nombres de producto, logotipos y marcas son propiedad de sus respectivos dueños. Las comparaciones son a título informativo. IronPDF es un componente de software que se integra dentro de sistemas de facturación; no es en sí mismo un sistema de facturación certificado bajo VeriFactu.

Preguntas Frecuentes

¿Por qué la licencia AGPL de iText crea un riesgo específico para los ISVs españoles bajo VeriFactu?

En España, los proveedores de software de facturación no conforme con VeriFactu (RDL 15/2025) pueden ser sancionados con hasta €150.000/año. Usar iText AGPL sin licencia comercial en ese software añade simultáneamente obligación de divulgación copyleft (riesgo copyleft) y riesgo de sanción regulatoria (riesgo AEAT) sobre el mismo componente — dos vectores independientes que se activan juntos.

¿IronPDF es un sistema de facturación certificado VeriFactu?

No. IronPDF es un componente de software que se integra dentro del sistema de facturación del ISV. La certificación VeriFactu es responsabilidad del sistema de facturación completo del proveedor.

¿Cuál es el cambio de paradigma principal al migrar de iText a IronPDF?

iText usa construcción programática bloque a bloque (Document, Paragraph, Table) — cada elemento del PDF se construye mediante API. IronPDF usa el motor Chromium para renderizar plantillas HTML/CSS directamente a PDF. Para facturas con plantillas HTML existentes, IronPDF simplifica el mantenimiento; para documentos construidos dinámicamente sin plantillas HTML, iText requiere una refactorización completa del enfoque de generación.

¿Cómo se compara la firma PAdES entre iText y IronPDF para TicketBAI?

iText requiere integración manual con Bouncy Castle para gestionar certificados X.509. IronPDF expone PdfSignature con soporte directo para X509Certificate2, compatible con certificados FNMT y otras CAs acreditadas bajo eIDAS. La firma PAdES resultante es válida para TicketBAI en Bizkaia (BATUZ), Gipuzkoa y Araba.

¿IronPDF soporta PDF/A-3 con XML Facturae adjunto para FACe y Crea y Crece?

Sí. IronPDF incluye SaveAsPdfA() con soporte para PDF/A-3B y EmbedFile() para adjuntar el XML Facturae 3.2.2. Este formato es el requerido para envíos a FACe (B2G) y archivado conforme con Crea y Crece (EN 16931/CIUS-ES).

¿Cómo se auditan las dependencias iText AGPL en un proyecto .NET antes de migrar?

Con PowerShell: Get-ChildItem -Recurse -Filter '*.csproj' | Select-String 'itext' -CaseSensitive:$false para localizar referencias; y dotnet list package para verificar versiones. Una vez identificadas, se reemplazan con IronPDF y se actualiza el código siguiendo la tabla de equivalencias de clases.

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