IRONSOFTWAREHOME

Procesamiento de PDF por lotes en C#: Automatice Flujos de Trabajo de Documentos a Gran Escala

Ahmad Sohail
Ahmad Sohail
Updated: 29 de junio de 2026

el procesamiento de PDF por lotes en C# con IronPDF permite a los desarrolladores .NET automatizar flujos de trabajo de documentos a escala, desde la conversión paralela de HTML a PDF y la fusión y división masivas hasta procesos de PDF asíncronos con gestión de errores, lógica de reintento y puntos de comprobación integrados. El motor Chromium seguro para subprocesos de IronPDF y la gestión de memoria basada en IDisposable lo hacen ideal para automatización de PDF de alto rendimiento, ya sea que esté ejecutando en las instalaciones, en Azure Functions, AWS Lambda o Kubernetes.

TL;DR: Guía de inicio rápido

Este tutorial cubre la automatización escalable de PDF en C#, desde la conversión paralela y las operaciones masivas hasta la implementación en la nube y los patrones de canalización resistentes.

  • A quién va dirigido: Desarrolladores y arquitectos .NET responsables de flujos de trabajo con gran cantidad de documentos: proyectos de migración de documentos, canales de generación de informes diarios, barridos de corrección de cumplimiento o esfuerzos de digitalización de archivos en los que el procesamiento secuencial no es factible.
  • Lo que crearás: Conversión de HTML a PDF en paralelo con Parallel.ForEach, operaciones de fusión y división por lotes, tuberías asincrónicas con SemaphoreSlim para control de concurrencia, manejo de errores con lógica de omisión en caso de fallo y reintento, patrones de punto de control/reanudación para recuperación en caso de fallos, y configuraciones de implementación en la nube para Azure Functions, AWS Lambda y Kubernetes.
  • Dónde funciona: .NET 6+, .NET Framework 4.6.2+, .NET Standard 2.0. Todo el renderizado utiliza el motor Chromium integrado de IronPDF: no se requieren dependencias de navegadores headless ni servicios externos.
  • Cuándo utilizar este enfoque: Cuando necesite procesar más PDF de los que permite la ejecución secuencial: migración de documentos a escala, trabajos por lotes programados con ventanas de tiempo ajustadas o plataformas multiusuario con cargas de documentos variables.
  • Por qué es importante técnicamente: El ChromePdfRenderer de IronPDF es seguro para subprocesos y sin estado por renderizado, lo que significa que múltiples subprocesos pueden compartir de manera segura una sola instancia de renderizador. Combinado con la biblioteca de tareas paralelas de .NET y IDisposable en PdfDocument, obtendrá un comportamiento de memoria predecible y saturación de CPU sin condiciones de carrera ni fugas de memoria.

Convierta por lotes un directorio entero de archivos HTML a PDF con sólo unas pocas líneas de código:

  1. 1Install IronPDF with NuGet Package Manager

    PM > Install-Package IronPdf

  2. 2Copie y ejecute este fragmento de código.

    using IronPdf;
    using System.IO;
    using System.Threading.Tasks;
    
    var renderer = new ChromePdfRenderer();
    var htmlFiles = Directory.GetFiles("input/", "*.html");
    
    Parallel.ForEach(htmlFiles, htmlFile =>
    {
        var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
        pdf.SaveAs($"output/{Path.GetFileNameWithoutExtension(htmlFile)}.pdf");
    });
    C#
  3. 3Despliegue para probar en su entorno real

    Comienza a usar IronPDF en tu proyecto hoy mismo con una prueba gratuita
    arrow pointer

Una vez que haya adquirido IronPDF o se haya suscrito a una versión de prueba de 30 días, añada su clave de licencia al inicio de su solicitud.

IronPdf.License.LicenseKey = "KEY";

Start using IronPDF in your project today with a free trial.

Primer Paso:
arrow pointer
NuGetInstalar con NuGet

PM > Install-Package IronPdf

Install IronPDF by running the command above in the NuGet Package Manager Console, or search for the package in the NuGet Package Manager.
Tabla de contenido

Cuando tiene que procesar miles de archivos PDF

El procesamiento de PDF por lotes no es un requisito de nicho, sino una parte rutinaria de la gestión de documentos empresariales. Las situaciones que lo exigen se dan en todos los sectores y comparten un rasgo común: hacer las cosas de una en una no es una opción.

Los proyectos de migración de documentos son uno de los desencadenantes más comunes. Cuando una organización pasa de un sistema de gestión documental a otro, es necesario convertir, reformatear o reetiquetar miles (a veces millones) de documentos. Una compañía de seguros que migra de un sistema de reclamaciones heredado puede necesitar convertir 500.000 documentos de reclamaciones basados en TIFF a PDF con capacidad de búsqueda. Un bufete de abogados que se muda a una nueva plataforma de gestión de casos puede necesitar fusionar correspondencia dispersa en archivos de casos unificados. Se trata de trabajos puntuales, pero de gran envergadura y que no perdonan los errores.

Generación de informes diarios es la versión en estado estacionario del mismo problema. Las instituciones financieras que producen informes de cartera al final del día para miles de clientes, las empresas de logística que generan manifiestos de embarque para cada contenedor saliente, los sistemas sanitarios que crean resúmenes diarios de pacientes en cientos de departamentos... todos ellos generan documentos PDF a una escala en la que el procesamiento secuencial sobrepasaría las ventanas de tiempo aceptables. Cuando 10.000 informes deben estar listos a las 6 de la mañana y los datos no son definitivos hasta medianoche, no se dispone de seis horas para traducirlos uno por uno.

La digitalización de archivos se encuentra en la intersección de la migración y el cumplimiento de normativas. Agencias gubernamentales, universidades y empresas con décadas de registros en papel se enfrentan a la obligación de digitalizar y archivar documentos en formatos conformes con los estándares (normalmente PDF/A). Los volúmenes son asombrosos -solo NARA recibe millones de páginas de registros federales para su conservación permanente- y el proceso debe ser lo suficientemente fiable como para no descubrir lagunas años después.

Cumplimiento de normativas suele ser el desencadenante más urgente. Cuando una auditoría revela que su archivo de documentos no cumple una norma de reciente aplicación -por ejemplo, sus facturas almacenadas no cumplen la normativa PDF/A-3 de facturación electrónica, o sus historiales médicos carecen del etiquetado de accesibilidad exigido por la Sección 508-, necesita procesar todo su archivo existente conforme a la nueva norma. En España, el escenario más frecuente en 2025–2026 es la adecuación al sistema VERI*FACTU (regulado por la AEAT bajo el RDL 1/2025): los ISVs de software de facturación con clientes que tienen NIF español deben garantizar que todos los PDFs de facturas emitidas incluyen la leyenda VERI*FACTU, el código QR de verificación y el hash SHA-256 encadenado. Un lote de adecuación retroactiva puede afectar a cientos de miles de documentos, donde el incumplimiento del proveedor de software conlleva sanciones de hasta 150.000 €/año. La presión es alta, el plazo ajustado y el volumen es el que contenga tu archivo.

En cada uno de estos escenarios, el reto principal es el mismo: ¿cómo procesar un gran número de operaciones PDF de forma fiable, eficiente y sin quedarse sin memoria o sin dejar el trabajo a medias cuando algo va mal?

Infografía que muestra cuatro escenarios de procesamiento por lotes — Migración de Documentos, Generación de Informes Diarios, Digitalización de Archivos, Remediación de Cumplimiento — cada uno con un ícono, rango típico de volumen e indicador de presión de tiempo


Arquitectura de procesamiento por lotes de IronPDF

Antes de sumergirse en operaciones específicas, es importante entender cómo IronPDF está diseñado para manejar cargas de trabajo concurrentes y qué decisiones arquitectónicas debe tomar al construir una canalización por lotes sobre él.

Instalación de IronPDF

Instale IronPDF a través de NuGet:

PM > Install-Package IronPdf

O utilizando la CLI de .NET:

dotnet add package IronPdf

IronPDF es compatible con .NET Framework 4.6.2+, .NET Core, .NET 5 hasta .NET 10, y .NET Standard 2.0. Se ejecuta en Windows, Linux, macOS y contenedores Docker, por lo que es adecuado tanto para trabajos por lotes locales como para la implementación nativa en la nube.

Para el procesamiento por lotes en producción, configure su clave de licencia con License.LicenseKey al inicio de la aplicación antes de que comiencen las operaciones de PDF. Esto garantiza que todas las llamadas de renderización en todos los subprocesos tengan acceso al conjunto completo de funciones sin marcas de agua por archivo.


Control de concurrencia y seguridad de subprocesos

El motor de renderizado basado en Chromium de IronPDF es seguro para hilos. Puede crear múltiples instancias de ChromePdfRenderer a través de los subprocesos, o compartir una sola instancia — IronPDF maneja la sincronización interna. La recomendación oficial para el procesamiento por lotes es usar el Parallel.ForEach incorporado en .NET, que distribuye el trabajo en todos los núcleos de CPU disponibles automáticamente.

Dicho esto, "seguro para subprocesos" no significa "usar subprocesos ilimitados". Cada operación de renderizado de PDF concurrente consume memoria (el motor Chromium necesita espacio de trabajo para el análisis del DOM, el diseño de CSS y la rasterización de imágenes), y lanzar demasiadas operaciones paralelas en un sistema con memoria limitada degradará el rendimiento o causará OutOfMemoryException. El nivel adecuado de concurrencia depende del hardware: un servidor de 16 núcleos con 64 GB de RAM puede manejar cómodamente entre 8 y 12 renderizaciones simultáneas; una VM de 4 núcleos con 8 GB podría estar limitada a 2–4. Controle esto con ParallelOptions.MaxDegreeOfParallelism — configúrelo a aproximadamente la mitad de sus núcleos de CPU disponibles como punto de partida, y luego ajuste según la presión de memoria observada.

Gestión de memoria a escala

La gestión de la memoria es la preocupación más importante en el procesamiento de PDF por lotes. Cada objeto PdfDocument almacena el contenido binario completo de un PDF en memoria, y no eliminar estos objetos causará que la memoria crezca linealmente con la cantidad de archivos procesados.

La regla crítica: siempre use declaraciones using o llame explícitamente a Dispose() en objetos PdfDocument. El PdfDocument de IronPDF implementa IDisposable, y no eliminarlo es la causa más común de problemas de memoria en escenarios por lotes. Cada iteración de su bucle de procesamiento debe crear un PdfDocument, hacer su trabajo y eliminarlo — nunca acumule objetos PdfDocument en una lista o colección a menos que tenga una razón específica y suficiente memoria para manejarlo.

Además de la eliminación, considere estas estrategias de gestión de memoria para grandes lotes:

Procesa en trozos en lugar de cargar todo a la vez. Si necesita procesar 50.000 archivos, no los enumere todos en una lista y luego itere: procéselos en lotes de 100 o 500, permitiendo que el recolector de basura recupere memoria entre trozos.

Forzar la recolección de basura entre chunks para lotes extremadamente grandes. Aunque generalmente debería dejar que el GC se maneje por sí mismo, el procesamiento por lotes es uno de los raros escenarios donde llamar a GC.Collect() entre los límites de los fragmentos puede evitar que se acumule la presión de memoria.

Monitoree el consumo de memoria usando GC.GetTotalMemory() o métricas a nivel de proceso. Si el uso de memoria supera un umbral (por ejemplo, el 80% de la RAM disponible), detenga el procesamiento para permitir que la GC se ponga al día.


Informes de progreso y registro

Cuando un trabajo por lotes tarda horas en completarse, la visibilidad de su progreso no es opcional, sino esencial. Como mínimo, debe registrar el inicio y la finalización de cada archivo, realizar un seguimiento del recuento de éxitos y fracasos y proporcionar una estimación del tiempo restante. Use Interlocked.Increment para contadores seguros para subprocesos al ejecutar operaciones paralelas, y registre intervalos regulares (cada 50 o 100 archivos) en lugar de cada archivo individual para evitar saturar su salida. Rastree su tiempo transcurrido con System.Diagnostics.Stopwatch y calcule una tasa en curso de archivos por segundo para dar un ETA significativo.

Para los trabajos por lotes de producción, considere la posibilidad de escribir el progreso en un almacén persistente (base de datos, archivo o cola de mensajes) para que los paneles de control puedan mostrar el estado en tiempo real sin conectarse directamente al proceso por lotes.


Operaciones por lotes comunes

Una vez establecida la base arquitectónica, repasemos las operaciones por lotes más comunes y sus implementaciones en IronPDF.

Conversión de HTML a PDF por lotes

La conversión de HTML a PDF es la operación por lotes más común. Ya se trate de generar facturas a partir de plantillas, convertir una biblioteca de documentación HTML a PDF o generar informes dinámicos desde una aplicación web, el patrón es el mismo: iterar sobre las entradas, generar cada una de ellas y guardar el resultado.

Entrada (5 archivos HTML)

HTML Invoice INV-2026-001

INV-2026-001

HTML Invoice INV-2026-002

INV-2026-002

HTML Invoice INV-2026-003

INV-2026-003

HTML Invoice INV-2026-004

INV-2026-004

HTML Invoice INV-2026-005

INV-2026-005

La implementación utiliza ChromePdfRenderer con Parallel.ForEach para procesar todos los archivos HTML simultáneamente, controlando el paralelismo a través de MaxDegreeOfParallelism para equilibrar el rendimiento con el consumo de memoria. Cada archivo se renderiza con RenderHtmlFileAsPdf y se guarda en el directorio de salida, con un seguimiento del progreso mediante contadores seguros para subprocesos Interlocked.

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

// Configure paths
string inputFolder = "input/";
string outputFolder = "output/";

Directory.CreateDirectory(outputFolder);

string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html");
Console.WriteLine($"Found {htmlFiles.Length} HTML files to convert");

// Create renderer instance (thread-safe, can be shared)
var renderer = new ChromePdfRenderer();

// Track progress
int processed = 0;
int failed = 0;

// Process in parallel with controlled concurrency
var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(htmlFiles, options, htmlFile =>
{
    try
    {
        string fileName = Path.GetFileNameWithoutExtension(htmlFile);
        string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf");

        using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
        pdf.SaveAs(outputPath);

        Interlocked.Increment(ref processed);
        Console.WriteLine($"[OK] {fileName}.pdf");
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref failed);
        Console.WriteLine($"[ERROR] {Path.GetFileName(htmlFile)}: {ex.Message}");
    }
});

Console.WriteLine($"\nComplete: {processed} succeeded, {failed} failed");

Resultado


Cada factura HTML se convierte en su correspondiente PDF. Arriba se muestra INV-2026-001.pdf, uno de los 5 resultados por lotes.

En el caso de la generación basada en plantillas (por ejemplo, facturas, informes), lo normal es combinar los datos en una plantilla HTML antes de la renderización. El enfoque es simple: cargue su plantilla HTML una vez, use string.Replace para inyectar datos por registro (nombre del cliente, totales, fechas), y pase el HTML completo a RenderHtmlAsPdf dentro de su bucle paralelo. IronPDF también proporciona RenderHtmlAsPdfAsync para escenarios donde desee usar async/await en lugar de Parallel.ForEach — cubriremos los patrones asincrónicos con detalle en una sección posterior.


Fusión de PDF por lotes

La fusión de grupos de PDF en documentos combinados es común en los flujos de trabajo jurídicos (fusión de documentos de expedientes), financieros (combinación de extractos mensuales en informes trimestrales) y editoriales.

using IronPdf;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;

string inputFolder = "documents/";
string outputFolder = "merged/";

Directory.CreateDirectory(outputFolder);

// Group PDFs by prefix (e.g., "invoice-2026-01-*.pdf" -> one merged file)
var pdfFiles = Directory.GetFiles(inputFolder, "*.pdf");
var groups = pdfFiles
    .GroupBy(f => Path.GetFileName(f).Split('-').Take(3).Aggregate((a, b) => $"{a}-{b}"))
    .Where(g => g.Count() > 1);

Console.WriteLine($"Found {groups.Count()} groups to merge");

foreach (var group in groups)
{
    string groupName = group.Key;
    var filesToMerge = group.OrderBy(f => f).ToList();

    Console.WriteLine($"Merging {filesToMerge.Count} files into {groupName}.pdf");

    try
    {
        // Load all PDFs for this group
        var pdfDocs = new List<PdfDocument>();
        foreach (string filePath in filesToMerge)
        {
            pdfDocs.Add(PdfDocument.FromFile(filePath));
        }

        // Merge all documents
        using var merged = PdfDocument.Merge(pdfDocs);
        merged.SaveAs(Path.Combine(outputFolder, $"{groupName}-merged.pdf"));

        // Dispose source documents
        foreach (var doc in pdfDocs)
        {
            doc.Dispose();
        }

        Console.WriteLine($"  [OK] Created {groupName}-merged.pdf ({merged.PageCount} pages)");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"  [ERROR] {groupName}: {ex.Message}");
    }
}

Console.WriteLine("\nMerge complete");

Para fusionar un gran número de archivos, tenga en cuenta la memoria: el método PdfDocument.Merge carga simultáneamente todos los documentos de origen en memoria. Si va a fusionar cientos de PDF de gran tamaño, considere la posibilidad de hacerlo por etapas: combine grupos de 10 a 20 archivos en documentos intermedios y, a continuación, fusione los intermedios.

Dividir PDF por lotes

Dividir PDF de varias páginas en páginas individuales (o intervalos de páginas) es la operación inversa a la fusión. Común en el procesamiento de la sala de correo, donde un lote escaneado de documentos debe separarse en registros individuales, y en flujos de trabajo de impresión donde los documentos compuestos deben separarse.

Entrada

El código a continuación demuestra cómo extraer páginas individuales usando CopyPage en un bucle paralelo, creando archivos PDF separados para cada página. Una función auxiliar SplitByRange alternativa muestra cómo extraer rangos de páginas en lugar de páginas individuales, útil para dividir grandes documentos en segmentos más pequeños.

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;

string inputFolder = "multipage/";
string outputFolder = "split/";

Directory.CreateDirectory(outputFolder);

string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf");
Console.WriteLine($"Found {pdfFiles.Length} PDFs to split");

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(pdfFiles, options, pdfFile =>
{
    string baseName = Path.GetFileNameWithoutExtension(pdfFile);

    try
    {
        using var pdf = PdfDocument.FromFile(pdfFile);
        int pageCount = pdf.PageCount;

        Console.WriteLine($"Splitting {baseName}.pdf ({pageCount} pages)");

        // Extract each page as a separate PDF
        for (int i = 0; i < pageCount; i++)
        {
            using var singlePage = pdf.CopyPage(i);
            string outputPath = Path.Combine(outputFolder, $"{baseName}-page-{i + 1:D3}.pdf");
            singlePage.SaveAs(outputPath);
        }

        Console.WriteLine($"  [OK] Created {pageCount} files from {baseName}.pdf");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"  [ERROR] {baseName}: {ex.Message}");
    }
});

// Alternative: Extract page ranges instead of individual pages
void SplitByRange(string inputFile, string outputFolder, int pagesPerChunk)
{
    using var pdf = PdfDocument.FromFile(inputFile);
    string baseName = Path.GetFileNameWithoutExtension(inputFile);
    int totalPages = pdf.PageCount;
    int chunkNumber = 1;

    for (int startPage = 0; startPage < totalPages; startPage += pagesPerChunk)
    {
        int endPage = Math.Min(startPage + pagesPerChunk - 1, totalPages - 1);
        using var chunk = pdf.CopyPages(startPage, endPage);
        chunk.SaveAs(Path.Combine(outputFolder, $"{baseName}-chunk-{chunkNumber:D3}.pdf"));
        chunkNumber++;
    }
}

Console.WriteLine("\nSplit complete");

Resultado


Página 2 extraída como PDF independiente (annual-report-page-2.pdf)

Los métodos CopyPage y CopyPages de IronPDF crean nuevos objetos PdfDocument que contienen las páginas especificadas. Recuerde eliminar tanto el documento fuente como cada documento de página extraído después de guardarlo.


Compresión por lotes

Cuando los costes de almacenamiento son importantes o cuando es necesario transmitir archivos PDF a través de conexiones con limitaciones de ancho de banda, la compresión por lotes puede reducir drásticamente la huella de archivo. IronPDF ofrece dos enfoques de compresión: CompressImages para reducir la calidad/tamaño de la imagen y CompressStructTree para eliminar metadatos estructurales. La nueva API CompressAndSaveAs (introducida en la versión 2025.12) ofrece una compresión superior al combinar múltiples técnicas de optimización.

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

string inputFolder = "originals/";
string outputFolder = "compressed/";

Directory.CreateDirectory(outputFolder);

string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf");
Console.WriteLine($"Found {pdfFiles.Length} PDFs to compress");

long totalOriginalSize = 0;
long totalCompressedSize = 0;
int processed = 0;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(pdfFiles, options, pdfFile =>
{
    string fileName = Path.GetFileName(pdfFile);
    string outputPath = Path.Combine(outputFolder, fileName);

    try
    {
        long originalSize = new FileInfo(pdfFile).Length;
        Interlocked.Add(ref totalOriginalSize, originalSize);

        using var pdf = PdfDocument.FromFile(pdfFile);

        // Apply compression with JPEG quality setting (0-100, lower = more compression)
        pdf.CompressAndSaveAs(outputPath, 60);

        long compressedSize = new FileInfo(outputPath).Length;
        Interlocked.Add(ref totalCompressedSize, compressedSize);
        Interlocked.Increment(ref processed);

        double reduction = (1 - (double)compressedSize / originalSize) * 100;
        Console.WriteLine($"[OK] {fileName}: {originalSize / 1024}KB → {compressedSize / 1024}KB ({reduction:F1}% reduction)");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

double totalReduction = (1 - (double)totalCompressedSize / totalOriginalSize) * 100;
Console.WriteLine($"\nCompression complete:");
Console.WriteLine($"  Files processed: {processed}");
Console.WriteLine($"  Total original: {totalOriginalSize / 1024 / 1024}MB");
Console.WriteLine($"  Total compressed: {totalCompressedSize / 1024 / 1024}MB");
Console.WriteLine($"  Overall reduction: {totalReduction:F1}%");

Algunas cosas a tener en cuenta sobre la compresión: Los ajustes de calidad JPEG por debajo de 60 producirán artefactos visibles en la mayoría de las imágenes. La opción ShrinkImage puede causar distorsión en algunas configuraciones — pruebe con muestras representativas antes de ejecutar un lote completo. Y eliminar el árbol de estructura (CompressStructTree) afectará la selección de texto y la búsqueda en los PDF comprimidos, así que úselo solo cuando esas capacidades no sean necesarias.


Conversión de formatos por lotes (PDF/A, PDF/UA)

Convertir un archivo existente a un formato que cumpla los estándares - PDF/A para archivado a largo plazo o PDF/UA para accesibilidad - es una de las operaciones por lotes de mayor valor. IronPDF es compatible con todas las versiones de PDF/A (incluida PDF/A-4, añadida en la versión 2025.11) y PDF/UA (incluida PDF/UA-2, añadida en la versión 2025.12).

Entrada

El ejemplo carga cada PDF con PdfDocument.FromFile, luego lo convierte a PDF/A-3b usando SaveAsPdfA con el parámetro PdfAVersions.PdfA3b. Una función alternativa ConvertToPdfUA demuestra la conversión de cumplimiento de accesibilidad usando SaveAsPdfUA, aunque PDF/UA requiere documentos de origen con etiquetado estructural adecuado.

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

string inputFolder = "originals/";
string outputFolder = "pdfa-archive/";

Directory.CreateDirectory(outputFolder);

string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf");
Console.WriteLine($"Found {pdfFiles.Length} PDFs to convert to PDF/A-3b");

int converted = 0;
int failed = 0;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(pdfFiles, options, pdfFile =>
{
    string fileName = Path.GetFileName(pdfFile);
    string outputPath = Path.Combine(outputFolder, fileName);

    try
    {
        using var pdf = PdfDocument.FromFile(pdfFile);

        // Convert to PDF/A-3b for long-term archival
        pdf.SaveAsPdfA(outputPath, PdfAVersions.PdfA3b);

        Interlocked.Increment(ref converted);
        Console.WriteLine($"[OK] {fileName} → PDF/A-3b");
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref failed);
        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

Console.WriteLine($"\nConversion complete: {converted} succeeded, {failed} failed");

// Alternative: Convert to PDF/UA for accessibility compliance
void ConvertToPdfUA(string inputFolder, string outputFolder)
{
    Directory.CreateDirectory(outputFolder);
    string[] files = Directory.GetFiles(inputFolder, "*.pdf");

    Parallel.ForEach(files, pdfFile =>
    {
        string fileName = Path.GetFileName(pdfFile);
        try
        {
            using var pdf = PdfDocument.FromFile(pdfFile);

            // PDF/UA requires proper tagging - ensure source is well-structured
            pdf.SaveAsPdfUA(Path.Combine(outputFolder, fileName));
            Console.WriteLine($"[OK] {fileName} → PDF/UA");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
        }
    });
}

Resultado

Comparación de salida de conversión PDF/A mostrando metadatos de cumplimiento

El PDF resultante es idéntico en apariencia byte a byte, pero ahora lleva metadatos de conformidad PDF/A-3b para sistemas de archivo.

La conversión de formatos es especialmente importante para los proyectos de corrección de la conformidad, en los que una organización descubre que su archivo existente no cumple una norma reglamentaria. El patrón por lotes es sencillo, pero el paso de validación es fundamental: comprueba siempre que cada archivo convertido supera realmente las comprobaciones de conformidad antes de considerarlo completo. La validación se trata en detalle en la sección de resiliencia.

Contexto regulatorio español: En el marco de Crea y Crece (Ley 18/2022, B2B obligatorio desde 2027 para empresas con facturación > 8 M€) y Facturae para la plataforma FACe (Administración Pública), los PDF archivados de facturas electrónicas deben cumplir el estándar PDF/A-3b para garantizar la integridad a largo plazo bajo el ENI (Esquema Nacional de Interoperabilidad). Las empresas con obligación de conservación documental de 5 años bajo la normativa fiscal española (AEAT, Código de Comercio) que tienen archivos existentes con facturas en formatos no conformes pueden usar IronPDF para ejecutar una conversión masiva a PDF/A-3b, generando un informe de conformidad por lotes que acredite el cumplimiento ante la AEPD o en auditorías fiscales. Los archivos Facturae (.xsig, XML) se complementan con el PDF/A-3b como documento de representación visual, firmado con certificados FNMT-RCM bajo eIDAS para garantizar la autenticidad ante el SII de la AEAT.


Construcción de procesos por lotes resistentes

Un proceso por lotes que funciona perfectamente con 100 archivos y se bloquea con el archivo 4.327 de 50.000 no es útil. La resiliencia, es decir, la capacidad de gestionar errores con elegancia, reintentar fallos transitorios y reanudar la actividad después de un fallo, es lo que diferencia un canal de producción de un prototipo.

Manejo de errores y omisión en caso de fallo

El patrón de resiliencia más básico es el skip-on-failure: si falla el procesamiento de un único archivo, se registra el error y se continúa con el siguiente archivo en lugar de abortar todo el lote. Esto parece obvio, pero sorprendentemente es fácil pasarlo por alto cuando usa Parallel.ForEach — una excepción no controlada en cualquier tarea paralela se propagarán como un AggregateException y terminará el bucle.

El siguiente ejemplo demuestra juntos la lógica de omisión en caso de fallo y de reintento — envolviendo cada archivo en un try-catch para un manejo de errores elegante, con un bucle de reintento interno usando retroceso exponencial para excepciones transitorias como IOException y OutOfMemoryException:

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;

string inputFolder = "input/";
string outputFolder = "output/";
string errorLogPath = "error-log.txt";

Directory.CreateDirectory(outputFolder);

string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html");
var renderer = new ChromePdfRenderer();
var errorLog = new ConcurrentBag<string>();

int processed = 0;
int failed = 0;
int retried = 0;

const int maxRetries = 3;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(htmlFiles, options, htmlFile =>
{
    string fileName = Path.GetFileNameWithoutExtension(htmlFile);
    string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf");
    int attempt = 0;
    bool success = false;

    while (attempt < maxRetries && !success)
    {
        attempt++;
        try
        {
            using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
            pdf.SaveAs(outputPath);
            success = true;
            Interlocked.Increment(ref processed);

            if (attempt > 1)
            {
                Interlocked.Increment(ref retried);
                Console.WriteLine($"[OK] {fileName}.pdf (succeeded on attempt {attempt})");
            }
            else
            {
                Console.WriteLine($"[OK] {fileName}.pdf");
            }
        }
        catch (Exception ex) when (IsTransientException(ex) && attempt < maxRetries)
        {
            // Transient error - wait and retry with exponential backoff
            int delayMs = (int)Math.Pow(2, attempt) * 500;
            Console.WriteLine($"[RETRY] {fileName}: {ex.Message} (attempt {attempt}, waiting {delayMs}ms)");
            Thread.Sleep(delayMs);
        }
        catch (Exception ex)
        {
            // Non-transient error or max retries exceeded
            Interlocked.Increment(ref failed);
            string errorMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.GetType().Name} | {ex.Message}";
            errorLog.Add(errorMessage);
            Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
        }
    }
});

// Write error log
if (errorLog.Count > 0)
{
    File.WriteAllLines(errorLogPath, errorLog);
}

Console.WriteLine($"\nBatch complete:");
Console.WriteLine($"  Processed: {processed}");
Console.WriteLine($"  Failed: {failed}");
Console.WriteLine($"  Retried: {retried}");
if (failed > 0)
{
    Console.WriteLine($"  Error log: {errorLogPath}");
}

// Helper to identify transient exceptions worth retrying
bool IsTransientException(Exception ex)
{
    return ex is IOException ||
           ex is OutOfMemoryException ||
           ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) ||
           ex.Message.Contains("locked", StringComparison.OrdinalIgnoreCase);
}

Una vez completado el lote, revise el registro de errores para saber qué archivos fallaron y por qué. Entre las causas de fallo más comunes se encuentran los archivos fuente dañados, los PDF protegidos con contraseña, las funciones no compatibles en el contenido fuente y las condiciones de memoria insuficiente en documentos muy grandes.

Lógica de respuesta para fallos transitorios

Algunos fallos son pasajeros - tendrán éxito si lo intentas de nuevo. Por ejemplo, la contención del sistema de archivos (otro proceso tiene el archivo bloqueado), la presión de la memoria temporal (la GC aún no se ha puesto al día) y los tiempos de espera de la red al cargar recursos externos en contenido HTML. El ejemplo de código anterior gestiona estos reintentos con un retardo exponencial: empieza con un retardo corto y lo duplica en cada intento de reintento, hasta un número máximo de reintentos (normalmente 3).

La clave está en distinguir entre fallos reintentables y no reintentables. Vale la pena reintentar un IOException (archivo bloqueado) o OutOfMemoryException (presión temporal). Un ArgumentException (entrada no válida) o un error de renderizado consistente no lo es — reintentar no ayudará, y perderá tiempo y recursos.


Checkpointing para reanudar después de un fallo

Cuando un trabajo por lotes procesa 50.000 archivos a lo largo de varias horas, un fallo en el archivo 35.000 no debería significar volver a empezar desde el principio. Los puntos de comprobación, es decir, el registro de los archivos que se han procesado correctamente, permiten reanudar el proceso desde el punto en que se dejó.

using IronPdf;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Generic;

string inputFolder = "input/";
string outputFolder = "output/";
string checkpointPath = "checkpoint.txt";
string errorLogPath = "errors.txt";

Directory.CreateDirectory(outputFolder);

// Load checkpoint - files already processed successfully
var completedFiles = new HashSet<string>();
if (File.Exists(checkpointPath))
{
    completedFiles = new HashSet<string>(File.ReadAllLines(checkpointPath));
    Console.WriteLine($"Resuming from checkpoint: {completedFiles.Count} files already processed");
}

// Get files to process (excluding already completed)
string[] allFiles = Directory.GetFiles(inputFolder, "*.html");
string[] filesToProcess = allFiles
    .Where(f => !completedFiles.Contains(Path.GetFileName(f)))
    .ToArray();

Console.WriteLine($"Files to process: {filesToProcess.Length} (skipping {completedFiles.Count} already done)");

var renderer = new ChromePdfRenderer();
var checkpointLock = new object();
int processed = 0;
int failed = 0;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(filesToProcess, options, htmlFile =>
{
    string fileName = Path.GetFileName(htmlFile);
    string baseName = Path.GetFileNameWithoutExtension(htmlFile);
    string outputPath = Path.Combine(outputFolder, $"{baseName}.pdf");

    try
    {
        using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
        pdf.SaveAs(outputPath);

        // Record success in checkpoint (thread-safe)
        lock (checkpointLock)
        {
            File.AppendAllText(checkpointPath, fileName + Environment.NewLine);
        }

        Interlocked.Increment(ref processed);
        Console.WriteLine($"[OK] {baseName}.pdf");
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref failed);

        // Log error for review
        lock (checkpointLock)
        {
            File.AppendAllText(errorLogPath,
                $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.Message}{Environment.NewLine}");
        }

        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

Console.WriteLine($"\nBatch complete:");
Console.WriteLine($"  Newly processed: {processed}");
Console.WriteLine($"  Failed: {failed}");
Console.WriteLine($"  Total completed: {completedFiles.Count + processed}");
Console.WriteLine($"  Checkpoint saved to: {checkpointPath}");

El archivo de control actúa como registro permanente del trabajo realizado. Cuando se inicia el proceso, lee el archivo de control y omite los archivos que ya se han procesado correctamente. Cuando un archivo termina de procesarse, su ruta se añade al archivo de punto de control. Este método es sencillo, se basa en archivos y no requiere dependencias externas.

Para situaciones más sofisticadas, considere la posibilidad de utilizar una tabla de base de datos o una caché distribuida (como Redis) como almacén de puntos de control, especialmente si varios trabajadores procesan archivos en paralelo en diferentes máquinas.


Validación antes y después del procesamiento

La validación es la parte final de un proceso resistente. La validación previa al procesamiento detecta las entradas problemáticas antes de que pierdan tiempo de procesamiento; la validación posterior al procesamiento garantiza que el resultado cumpla sus requisitos de calidad y conformidad.

Entrada

Esta implementación envuelve el bucle de procesamiento con funciones auxiliares PreValidate y PostValidate. La validación previa comprueba el tamaño del archivo, el tipo de contenido y la estructura HTML básica antes de procesarlo. La posvalidación verifica que el PDF de salida tenga un número de páginas válido y un tamaño de archivo razonable, moviendo los archivos validados a una carpeta separada y enviando los fallos a una carpeta de rechazo para su revisión manual.

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;

string inputFolder = "input/";
string outputFolder = "output/";
string validatedFolder = "validated/";
string rejectedFolder = "rejected/";

Directory.CreateDirectory(outputFolder);
Directory.CreateDirectory(validatedFolder);
Directory.CreateDirectory(rejectedFolder);

string[] inputFiles = Directory.GetFiles(inputFolder, "*.html");
var renderer = new ChromePdfRenderer();

int preValidationFailed = 0;
int processingFailed = 0;
int postValidationFailed = 0;
int succeeded = 0;

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount / 2
};

Parallel.ForEach(inputFiles, options, inputFile =>
{
    string fileName = Path.GetFileNameWithoutExtension(inputFile);
    string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf");

    // Pre-validation: Check input file
    if (!PreValidate(inputFile))
    {
        Interlocked.Increment(ref preValidationFailed);
        Console.WriteLine($"[SKIP] {fileName}: Failed pre-validation");
        return;
    }

    try
    {
        // Process
        using var pdf = renderer.RenderHtmlFileAsPdf(inputFile);
        pdf.SaveAs(outputPath);

        // Post-validation: Check output file
        if (PostValidate(outputPath))
        {
            // Move to validated folder
            string validatedPath = Path.Combine(validatedFolder, $"{fileName}.pdf");
            File.Move(outputPath, validatedPath, overwrite: true);
            Interlocked.Increment(ref succeeded);
            Console.WriteLine($"[OK] {fileName}.pdf (validated)");
        }
        else
        {
            // Move to rejected folder for manual review
            string rejectedPath = Path.Combine(rejectedFolder, $"{fileName}.pdf");
            File.Move(outputPath, rejectedPath, overwrite: true);
            Interlocked.Increment(ref postValidationFailed);
            Console.WriteLine($"[REJECT] {fileName}.pdf: Failed post-validation");
        }
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref processingFailed);
        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

Console.WriteLine($"\nValidation summary:");
Console.WriteLine($"  Succeeded: {succeeded}");
Console.WriteLine($"  Pre-validation failed: {preValidationFailed}");
Console.WriteLine($"  Processing failed: {processingFailed}");
Console.WriteLine($"  Post-validation failed: {postValidationFailed}");

// Pre-validation: Quick checks on input file
bool PreValidate(string filePath)
{
    try
    {
        var fileInfo = new FileInfo(filePath);

        // Check file exists and is readable
        if (!fileInfo.Exists) return false;

        // Check file is not empty
        if (fileInfo.Length == 0) return false;

        // Check file is not too large (e.g., 50MB limit)
        if (fileInfo.Length > 50 * 1024 * 1024) return false;

        // Quick content check - must be valid HTML
        string content = File.ReadAllText(filePath);
        if (string.IsNullOrWhiteSpace(content)) return false;
        if (!content.Contains("<html", StringComparison.OrdinalIgnoreCase) &&
            !content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        return true;
    }
    catch
    {
        return false;
    }
}

// Post-validation: Verify output PDF meets requirements
bool PostValidate(string pdfPath)
{
    try
    {
        using var pdf = PdfDocument.FromFile(pdfPath);

        // Check PDF has at least one page
        if (pdf.PageCount < 1) return false;

        // Check file size is reasonable (not just header, not corrupted)
        var fileInfo = new FileInfo(pdfPath);
        if (fileInfo.Length < 1024) return false;

        return true;
    }
    catch
    {
        return false;
    }
}

Resultado

Captura de pantalla de procesamiento de entrada de validación

Estructura de carpeta que muestra un directorio validado con 5 archivos y directorio de errores vacío

Los 5 archivos pasaron la validación y se trasladaron a la carpeta validada.

La validación previa al procesamiento debe ser rápida: se trata de comprobar si hay entradas obviamente defectuosas, no de realizar un procesamiento completo. La validación posterior al procesamiento puede ser más exhaustiva, especialmente en el caso de las conversiones de conformidad, en las que el resultado debe cumplir normas específicas (PDF/A, PDF/UA). Cualquier archivo que no supere la validación posterior al procesamiento deberá marcarse para su revisión manual en lugar de aceptarse de forma silenciosa.


Mi biblioteca favorita de este tipo es IronPDF. Permite una manipulación rápida y eficiente de archivos PDF. También tiene muchas funciones valiosas, como exportar al formato PDF/A y firmar documentos PDF digitalmente.

Milan Jovanovic

Microsoft MVP

Ver estudio de caso

IronOCR significa que podemos ahorrar $40,000 anualmente en el procesamiento manual, mientras mejoramos la productividad y liberamos recursos para tareas de alto impacto. Lo recomendaría encarecidamente.

Brent Matzelle

Director de Tecnología, OPYN

Ver estudio de caso

El Iron Suite juega un papel crucial en nuestras operaciones. Estas son herramientas que aumentan la eficiencia en toda la empresa, incluyendo la creación de planos y la mejora en la gestión de inventario.

David Jones

Ingeniero de Software Principal, Agorus Build

Ver estudio de caso

Patrones de procesamiento asíncrono y paralelo

IronPDF admite tanto Parallel.ForEach (paralelismo basado en subprocesos) como async/await (E/S asincrónica). Entender cuándo utilizar cada una de ellas -y cómo combinarlas de forma eficaz- es clave para maximizar el rendimiento.

Integración de bibliotecas de tareas paralelas

Parallel.ForEach es el enfoque más sencillo y efectivo para operaciones por lotes con uso intensivo de CPU. El motor de renderizado de IronPDF requiere mucho de la CPU (análisis de HTML, diseño de CSS, rasterización de imágenes), y Parallel.ForEach distribuye automáticamente este trabajo en todos los núcleos disponibles.

using IronPdf;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;

string inputFolder = "input/";
string outputFolder = "output/";

Directory.CreateDirectory(outputFolder);

string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html");
var renderer = new ChromePdfRenderer();

Console.WriteLine($"Processing {htmlFiles.Length} files with {Environment.ProcessorCount} CPU cores");

int processed = 0;
var stopwatch = Stopwatch.StartNew();

// Configure parallelism based on system resources
// Rule of thumb: ProcessorCount / 2 for memory-intensive operations
var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2)
};

Console.WriteLine($"Max parallelism: {options.MaxDegreeOfParallelism}");

// Use Parallel.ForEach for CPU-bound batch operations
Parallel.ForEach(htmlFiles, options, htmlFile =>
{
    string fileName = Path.GetFileNameWithoutExtension(htmlFile);
    string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf");

    try
    {
        // Render HTML to PDF
        using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile);
        pdf.SaveAs(outputPath);

        int current = Interlocked.Increment(ref processed);

        // Progress reporting every 10 files
        if (current % 10 == 0)
        {
            double elapsed = stopwatch.Elapsed.TotalSeconds;
            double rate = current / elapsed;
            double remaining = (htmlFiles.Length - current) / rate;
            Console.WriteLine($"Progress: {current}/{htmlFiles.Length} ({rate:F1} files/sec, ~{remaining:F0}s remaining)");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}");
    }
});

stopwatch.Stop();
double totalRate = processed / stopwatch.Elapsed.TotalSeconds;

Console.WriteLine($"\nComplete:");
Console.WriteLine($"  Files processed: {processed}/{htmlFiles.Length}");
Console.WriteLine($"  Total time: {stopwatch.Elapsed.TotalSeconds:F1}s");
Console.WriteLine($"  Average rate: {totalRate:F1} files/sec");
Console.WriteLine($"  Time per file: {stopwatch.Elapsed.TotalMilliseconds / processed:F0}ms");

// Memory monitoring helper (call between chunks for large batches)
void CheckMemoryPressure()
{
    const long memoryThreshold = 4L * 1024 * 1024 * 1024; // 4 GB
    long currentMemory = GC.GetTotalMemory(forceFullCollection: false);

    if (currentMemory > memoryThreshold)
    {
        Console.WriteLine($"Memory pressure detected ({currentMemory / 1024 / 1024}MB), forcing GC...");
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
}

La opción MaxDegreeOfParallelism es crítica. Sin ella, el TPL intentará utilizar todos los núcleos disponibles, lo que puede saturar la memoria si cada renderización consume muchos recursos. Ajústalo en función de la RAM disponible en tu sistema dividida por el consumo típico de memoria por renderizado (normalmente 100-300 MB por renderizado concurrente para HTML complejo).

Control de la concurrencia (SemaphoreSlim)

Cuando necesite un control más fino sobre la concurrencia de lo que Parallel.ForEach ofrece — por ejemplo, al mezclar E/S asincrónica con renderizado que requiere mucho de la CPU — SemaphoreSlim le da control explícito sobre cuántas operaciones se ejecutan simultáneamente. El patrón es sencillo: cree un SemaphoreSlim con su límite de concurrencia deseado (ej., 4 renderizados concurrentes), llame a WaitAsync antes de cada renderizado, y Release en un bloque finally después. Luego lance todas las tareas con Task.WhenAll.

Este patrón es especialmente útil cuando el proceso incluye tanto pasos de E/S (lectura de archivos de almacenamiento blob, escritura de resultados en una base de datos) como pasos de CPU (renderización de PDF). El semáforo limita la concurrencia de renderizado vinculada a la CPU al tiempo que permite que los pasos vinculados a la E/S avancen sin estrangulamiento.

Bestes prácticas de Async/Await

IronPDF proporciona variantes asincrónicas de sus métodos de renderizado, incluyendo RenderHtmlAsPdfAsync, RenderUrlAsPdfAsync y RenderHtmlFileAsPdfAsync. Son ideales para aplicaciones web (donde el bloqueo del hilo de solicitud es inaceptable) y para pipelines que mezclan el renderizado de PDF con operaciones de E/S asíncronas.

Algunas prácticas recomendadas importantes de async para el procesamiento por lotes:

No use Task.Run para envolver métodos sincrónicos de IronPDF — use las variantes asincrónicas nativas en su lugar. Envolver métodos sincrónicos en Task.Run desperdicia un hilo de pool de hilos y añade sobrecarga sin ningún beneficio.

No use .Result o .Wait() en tareas asincrónicas — esto bloquea el hilo de llamada y puede causar bloqueos en contextos UI o ASP.NET. Siempre use await.

Lotee sus llamadas Task.WhenAll en lugar de esperar todas las tareas a la vez. Si tiene 10,000 tareas y llama a Task.WhenAll en todas ellas al mismo tiempo, lanzará 10,000 operaciones concurrentes. En su lugar, use .Chunk(10) o un enfoque similar para procesarlas en grupos, esperando cada grupo secuencialmente.

Evitar el agotamiento de memoria

El agotamiento de la memoria es el modo de fallo más común en el procesamiento de PDF por lotes. La estrategia defensiva es monitorear el uso de memoria con GC.GetTotalMemory() antes de cada renderizado y desencadenar una recolección cuando el consumo supere un umbral (ej., 4 GB o 80% de la RAM disponible). Llame a GC.Collect() seguido por GC.WaitForPendingFinalizers() y un segundo GC.Collect() para recuperar la mayor cantidad de memoria posible antes de continuar. Esto añade una pequeña pausa pero previene la alternativa catastrófica de un OutOfMemoryException que haga que todo su lote falle en el archivo #30,000.

Combine esto con el MaxDegreeOfParallelism control de la sección TPL y el patrón de eliminación using de la sección de gestión de memoria, y tendrá una defensa de tres capas contra problemas de memoria: límite de concurrencia, eliminación agresiva y monitoreo con una válvula de seguridad.


Despliegue en la nube para trabajos por lotes

El procesamiento por lotes moderno se ejecuta cada vez más en la nube, donde se pueden escalar los recursos informáticos para adaptarse a las demandas de carga de trabajo y pagar sólo por lo que se utiliza. IronPDF se ejecuta en las principales plataformas en la nube. A continuación se explica cómo diseñar procesos por lotes para cada una de ellas.

Azure Functions con Durable Functions

Azure Durable Functions proporciona una orquestación integrada para patrones fan-out/fan-in, lo que las convierte en una opción natural para el procesamiento de PDF por lotes. La función orquestadora distribuye el trabajo entre varias instancias de funciones de actividad, cada una de las cuales procesa un subconjunto de archivos. Su orquestador llama a CallActivityAsync en un bucle de distribución, cada función de actividad instanciará un ChromePdfRenderer, procesará su porción de archivos, y el orquestador recogerá los resultados.

Consideraciones clave para Azure Functions: el plan de consumo predeterminado tiene un tiempo de espera de 5 minutos por invocación de función y memoria limitada. Para el procesamiento por lotes, utilice el plan Premium o Dedicado, que admite tiempos de espera más largos y más memoria. IronPDF requiere el tiempo de ejecución completo de .NET (no recortado), así que asegúrese de que su aplicación funcional está configurada para .NET 8+ con el identificador de tiempo de ejecución adecuado.

AWS Lambda con funciones escalonadas

AWS Step Functions ofrece una capacidad de orquestación similar a Azure Durable Functions. Cada paso de la máquina de estados invoca una función Lambda que procesa un fragmento de archivos. Su controlador Lambda recibe un lote de claves de objetos S3, carga cada PDF con PdfDocument.FromFile, aplica su canal de procesamiento (compresión, conversión de formato, etc.), y escribe los resultados de vuelta a un bucket de salida S3.

AWS Lambda tiene un tiempo de ejecución máximo de 15 minutos y almacenamiento /tmp limitado (512 MB por defecto, configurable hasta 10 GB). Para trabajos por lotes de gran tamaño, utilice Step Functions para dividir la carga de trabajo en partes y procesar cada parte en una invocación Lambda independiente. Almacenar los resultados intermedios en S3 en lugar de almacenamiento local.

Programación de trabajos de Kubernetes

Para las organizaciones que ejecutan sus propios clústeres Kubernetes, el procesamiento de PDF por lotes se adapta bien a Kubernetes Jobs y CronJobs. Cada pod ejecuta un trabajador por lotes que extrae archivos de una cola (Azure Service Bus, RabbitMQ o SQS), los procesa con IronPDF y escribe los resultados en el almacenamiento de objetos. El bucle del trabajador sigue el mismo patrón cubierto en secciones anteriores: desencolar un mensaje, usar ChromePdfRenderer.RenderHtmlAsPdf() o PdfDocument.FromFile() para procesar el documento, subir el resultado y reconocer el mensaje. Envolviendo el procesamiento en el mismo try-catch con la lógica de reintento de los patrones de resiliencia, y usar SemaphoreSlim para controlar la concurrencia por pod.

IronPDF es compatible con Docker y se ejecuta en contenedores Linux. Use el paquete NuGet IronPdf con los paquetes de tiempo de ejecución nativos apropiados para el sistema operativo de su contenedor (ej., IronPdf.Linux para imágenes basadas en Linux). Para Kubernetes, defina solicitudes y límites de recursos que coincidan con los requisitos de memoria de IronPDF (normalmente 512 MB-2 GB por pod en función de la concurrencia). Horizontal Pod Autoscaler puede escalar los trabajadores en función de la profundidad de la cola, y el patrón de checkpointing garantiza que no se pierda ningún trabajo si se desalojan los pods.

Diagrama de arquitectura que muestra un pipeline de procesamiento por lotes nativo de la nube con cola de trabajo, capa de cálculo, almacenamiento de objetos y panel de monitoreo


Estrategias de optimización de costes

El procesamiento por lotes en la nube puede resultar caro si no se tiene en cuenta la asignación de recursos. Estas son las estrategias que tienen mayor impacto:

Dimensiona correctamente tu computación. El renderizado de PDF es intensivo en CPU y memoria, no en GPU. Utilice instancias optimizadas para computación (serie C en Azure, tipo C en AWS) en lugar de instancias de uso general u optimizadas para memoria. Obtendrá una mejor relación precio-rendimiento.

Utilizar instancias puntuales/prefabricadas para cargas de trabajo por lotes que puedan tolerar interrupciones. El procesamiento de PDF por lotes es inherentemente reanudable (gracias a los puntos de control), lo que lo convierte en un candidato ideal para la fijación de precios al contado, que suele ofrecer descuentos del 60-90% con respecto a los servicios bajo demanda.

Procese durante las horas de menor actividad si su calendario lo permite. Muchos proveedores de servicios en la nube ofrecen precios más bajos o mayor disponibilidad puntual durante las noches y los fines de semana.

Comprima antes, almacene una vez. Ejecute la compresión como parte de su canal de procesamiento en lugar de como un paso separado. Almacenar los PDF comprimidos desde el principio reduce los costes de almacenamiento durante toda la vida útil del archivo.

Equipar su almacenamiento Los PDF procesados a los que se accede con frecuencia deben ir en almacenamiento en caliente; los PDF archivados a los que se accede con poca frecuencia deben trasladarse a niveles fríos o de archivo (Azure Cool/Archive, AWS S3 Glacier). Esto por sí solo puede reducir los costes de almacenamiento en un 50-80%.


Ejemplo de canalización en el mundo real

Vamos a atar todo junto con una tubería por lotes completa, de grado de producción que demuestra el flujo de trabajo completo: Ingest → Validate → Process → Archive → Report.

Este ejemplo procesa un directorio de plantillas de facturas HTML, las convierte a PDF, comprime el resultado, lo convierte a PDF/A-3b para cumplir los requisitos de archivado, valida el resultado y produce un informe resumido al final.

Utilizando las mismas 5 facturas HTML del ejemplo de conversión por lotes anterior...

using IronPdf;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;

// Configuration
var config = new PipelineConfig
{
    InputFolder = "input/",
    OutputFolder = "output/",
    ArchiveFolder = "archive/",
    ErrorFolder = "errors/",
    CheckpointPath = "pipeline-checkpoint.json",
    ReportPath = "pipeline-report.json",
    MaxConcurrency = Math.Max(1, Environment.ProcessorCount / 2),
    MaxRetries = 3,
    JpegQuality = 70
};

// Initialize folders
Directory.CreateDirectory(config.OutputFolder);
Directory.CreateDirectory(config.ArchiveFolder);
Directory.CreateDirectory(config.ErrorFolder);

// Load checkpoint for resume capability
var checkpoint = LoadCheckpoint(config.CheckpointPath);
var results = new ConcurrentBag<ProcessingResult>();
var stopwatch = Stopwatch.StartNew();

// Get files to process
string[] allFiles = Directory.GetFiles(config.InputFolder, "*.html");
string[] filesToProcess = allFiles
    .Where(f => !checkpoint.CompletedFiles.Contains(Path.GetFileName(f)))
    .ToArray();

Console.WriteLine($"Pipeline starting:");
Console.WriteLine($"  Total files: {allFiles.Length}");
Console.WriteLine($"  Already processed: {checkpoint.CompletedFiles.Count}");
Console.WriteLine($"  To process: {filesToProcess.Length}");
Console.WriteLine($"  Concurrency: {config.MaxConcurrency}");

var renderer = new ChromePdfRenderer();
var checkpointLock = new object();

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = config.MaxConcurrency
};

Parallel.ForEach(filesToProcess, options, inputFile =>
{
    var result = new ProcessingResult
    {
        FileName = Path.GetFileName(inputFile),
        StartTime = DateTime.UtcNow
    };

    try
    {
        // Stage: Pre-validation
        if (!ValidateInput(inputFile))
        {
            result.Status = "PreValidationFailed";
            result.Error = "Input file failed validation";
            results.Add(result);
            return;
        }

        string baseName = Path.GetFileNameWithoutExtension(inputFile);
        string tempPath = Path.Combine(config.OutputFolder, $"{baseName}.pdf");
        string archivePath = Path.Combine(config.ArchiveFolder, $"{baseName}.pdf");

        // Stage: Process with retry
        PdfDocument pdf = null;
        int attempt = 0;
        bool success = false;

        while (attempt < config.MaxRetries && !success)
        {
            attempt++;
            try
            {
                pdf = renderer.RenderHtmlFileAsPdf(inputFile);
                success = true;
            }
            catch (Exception ex) when (IsTransient(ex) && attempt < config.MaxRetries)
            {
                Thread.Sleep((int)Math.Pow(2, attempt) * 500);
            }
        }

        if (!success || pdf == null)
        {
            result.Status = "ProcessingFailed";
            result.Error = "Max retries exceeded";
            results.Add(result);
            return;
        }

        using (pdf)
        {
            // Stage: Compress and convert to PDF/A-3b for archival
            pdf.SaveAsPdfA(tempPath, PdfAVersions.PdfA3b);
        }

        // Stage: Post-validation
        if (!ValidateOutput(tempPath))
        {
            File.Move(tempPath, Path.Combine(config.ErrorFolder, $"{baseName}.pdf"), overwrite: true);
            result.Status = "PostValidationFailed";
            result.Error = "Output file failed validation";
            results.Add(result);
            return;
        }

        // Stage: Archive
        File.Move(tempPath, archivePath, overwrite: true);

        // Update checkpoint
        lock (checkpointLock)
        {
            checkpoint.CompletedFiles.Add(result.FileName);
            SaveCheckpoint(config.CheckpointPath, checkpoint);
        }

        result.Status = "Success";
        result.OutputSize = new FileInfo(archivePath).Length;
        result.EndTime = DateTime.UtcNow;
        results.Add(result);

        Console.WriteLine($"[OK] {baseName}.pdf ({result.OutputSize / 1024}KB)");
    }
    catch (Exception ex)
    {
        result.Status = "Error";
        result.Error = ex.Message;
        result.EndTime = DateTime.UtcNow;
        results.Add(result);
        Console.WriteLine($"[ERROR] {result.FileName}: {ex.Message}");
    }
});

stopwatch.Stop();

// Generate report
var report = new PipelineReport
{
    TotalFiles = allFiles.Length,
    ProcessedThisRun = results.Count,
    Succeeded = results.Count(r => r.Status == "Success"),
    PreValidationFailed = results.Count(r => r.Status == "PreValidationFailed"),
    ProcessingFailed = results.Count(r => r.Status == "ProcessingFailed"),
    PostValidationFailed = results.Count(r => r.Status == "PostValidationFailed"),
    Errors = results.Count(r => r.Status == "Error"),
    TotalDuration = stopwatch.Elapsed,
    AverageFileTime = results.Any() ? TimeSpan.FromMilliseconds(stopwatch.Elapsed.TotalMilliseconds / results.Count) : TimeSpan.Zero
};

string reportJson = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(config.ReportPath, reportJson);

Console.WriteLine($"\n=== Pipeline Complete ===");
Console.WriteLine($"Succeeded: {report.Succeeded}");
Console.WriteLine($"Failed: {report.PreValidationFailed + report.ProcessingFailed + report.PostValidationFailed + report.Errors}");
Console.WriteLine($"Duration: {report.TotalDuration.TotalMinutes:F1} minutes");
Console.WriteLine($"Report: {config.ReportPath}");

// Helper methods
bool ValidateInput(string path)
{
    try
    {
        var info = new FileInfo(path);
        if (!info.Exists || info.Length == 0 || info.Length > 50 * 1024 * 1024) return false;
        string content = File.ReadAllText(path);
        return content.Contains("<html", StringComparison.OrdinalIgnoreCase) ||
               content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase);
    }
    catch { return false; }
}

bool ValidateOutput(string path)
{
    try
    {
        using var pdf = PdfDocument.FromFile(path);
        return pdf.PageCount > 0 && new FileInfo(path).Length > 1024;
    }
    catch { return false; }
}

bool IsTransient(Exception ex) =>
    ex is IOException || ex is OutOfMemoryException ||
    ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase);

Checkpoint LoadCheckpoint(string path)
{
    if (File.Exists(path))
    {
        string json = File.ReadAllText(path);
        return JsonSerializer.Deserialize<Checkpoint>(json) ?? new Checkpoint();
    }
    return new Checkpoint();
}

void SaveCheckpoint(string path, Checkpoint cp) =>
    File.WriteAllText(path, JsonSerializer.Serialize(cp));


// Data classes
public class PipelineConfig
{
    public string InputFolder { get; set; } = "";
    public string OutputFolder { get; set; } = "";
    public string ArchiveFolder { get; set; } = "";
    public string ErrorFolder { get; set; } = "";
    public string CheckpointPath { get; set; } = "";
    public string ReportPath { get; set; } = "";
    public int MaxConcurrency { get; set; }
    public int MaxRetries { get; set; }
    public int JpegQuality { get; set; }
}

public class Checkpoint
{
    public HashSet<string> CompletedFiles { get; set; } = new();
}

public class ProcessingResult
{
    public string FileName { get; set; } = "";
    public string Status { get; set; } = "";
    public string Error { get; set; } = "";
    public long OutputSize { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
}

public class PipelineReport
{
    public int TotalFiles { get; set; }
    public int ProcessedThisRun { get; set; }
    public int Succeeded { get; set; }
    public int PreValidationFailed { get; set; }
    public int ProcessingFailed { get; set; }
    public int PostValidationFailed { get; set; }
    public int Errors { get; set; }
    public TimeSpan TotalDuration { get; set; }
    public TimeSpan AverageFileTime { get; set; }
}
C#

Resultado

Informe del pipeline en JSON mostrando resultados de procesamiento por lotes con cuenta de éxitos, cuenta de fallas y duración total

Informe de canalización que muestra los resultados del procesamiento por lotes.

Este pipeline incorpora todos los patrones que hemos tratado en este tutorial: procesamiento paralelo con concurrencia controlada, gestión de errores por archivo con skip-on-failure, lógica de reintento para errores transitorios, checkpointing para reanudar después de un fallo, validación previa y posterior al procesamiento, gestión de memoria con eliminación explícita y registro completo con un informe de resumen final.

El resultado de este proceso es un directorio de archivos comprimidos compatibles con PDF/A-3b, un archivo de control para reanudar el proceso, un registro de errores para los archivos que no se han podido procesar y un informe resumido con estadísticas de procesamiento. Este es el patrón que desea para cualquier carga de trabajo seria de procesamiento de PDF por lotes.


Procesamiento de PDF por lotes en el contexto regulatorio español: VeriFactu, TicketBAI y AEAT SII

El procesamiento por lotes de PDFs tiene un peso especial para los ISVs españoles que desarrollan software de facturación bajo la normativa VeriFactu y TicketBAI, donde el incumplimiento puede suponer sanciones de hasta 150.000 €/año para los proveedores de software.

Generación masiva de facturas VeriFactu

El sistema VeriFactu (anti-fraude de software de facturación, regulado por la AEAT) exige que cada factura emitida genere un registro XML con huella encadenada (hash SHA-256 del registro anterior). En escenarios de facturación masiva —cierre mensual de ERPs, generación nocturna de facturas recurrentes— el procesamiento por lotes con IronPDF permite:

  1. Generar el XML VeriFactu con la huella encadenada (responsabilidad del motor de facturación del ISV).
  2. Aplicar la plantilla XSLT para transformar el XML en HTML.
  3. Renderizar el HTML como PDF con ChromePdfRenderer en paralelo, controlando la concurrencia con SemaphoreSlim para evitar el consumo excesivo de memoria en AWS Lambda o Azure Functions.
  4. Estampar la leyenda VERI*FACTU (con asterisco obligatorio) y el código QR de verificación de la AEAT mediante TextStamper e ImageStamper.

El procesamiento paralelo con checkpointing es especialmente relevante aquí: si el proceso de generación por lotes se interrumpe, los registros VERI*FACTU ya generados deben preservarse para mantener la integridad de la cadena de huella.

TicketBAI: lotes por territorio foral con configuración por hacienda

El sistema TicketBAI requiere presentación separada a las haciendas forales de Bizkaia (bizkaia.eus), Gipuzkoa (gipuzkoa.eus) y Araba (araba.eus), cada una con su propio endpoint, certificado admitido y especificación de campos obligatorios. En un procesamiento por lotes de tiques TicketBAI:

  • Particione el lote de tiques por territorio foral antes del procesamiento.
  • Aplique la configuración de renderizado específica de cada foral (campos obligatorios diferenciados según el reglamento de cada diputación).
  • Genere el PDF de representación del tique con IronPDF y, si el entorno es AWS Lambda (Linux), asegúrese de que las fuentes para caracteres castellanos y opcionalmente euskera estén instaladas en el contenedor (ver fuentes rotas en AWS Lambda).

AEAT SII: procesamiento nocturno de grandes volúmenes de facturas

El SII (Suministro Inmediato de Información) exige a las grandes empresas el envío casi en tiempo real de datos de facturas a la AEAT. Los sistemas internos de auditoría generan informes PDF del SII por períodos (mensual, trimestral). El procesamiento por lotes nocturno con IronPDF, ejecutado en Azure Functions con Durable Functions para el checkpointing, permite generar miles de informes PDF del SII de forma fiable con reintentos automáticos en caso de fallo transitorio.

Consideración sobre licencias: iText AGPL en entornos de facturación VeriFactu

En el contexto español, el uso de iText bajo licencia AGPL en software de facturación sujeto a VeriFactu añade una capa de riesgo adicional: la distribución del código fuente bajo AGPL puede entrar en conflicto con los requisitos de software propietario de los ISVs. IronPDF, bajo licencia comercial propietaria, elimina este riesgo de exposición AGPL en entornos de procesamiento por lotes de facturas VeriFactu y TicketBAI.


Próximos pasos

El procesamiento de PDF por lotes a escala no consiste únicamente en llamar a un método de renderización en un bucle. Requiere una arquitectura bien pensada en torno a la concurrencia, la gestión de memoria, la gestión de errores y la implementación, así como la biblioteca adecuada para que todo funcione. IronPDF proporciona el motor de renderizado a prueba de hilos, la superficie API asíncrona, herramientas de compresión y capacidades de conversión de formato que forman la base de cualquier canalización de PDF por lotes .NET.

Tanto si está creando un generador de informes nocturno que produce miles de PDF antes del amanecer, como si está migrando un archivo de documentos heredado a cumplimiento de PDF/A, o poniendo en marcha un servicio de procesamiento nativo en la nube en Kubernetes, los patrones de este tutorial le ofrecen un marco probado sobre el que construir. El procesamiento paralelo con concurrencia controlada mantiene un alto rendimiento. La lógica de omisión en caso de fallo y de reintento mantiene el proceso en marcha cuando algunos archivos causan problemas. Los puntos de control garantizan que nunca se pierda el progreso. Además, los patrones de despliegue en la nube permiten escalar la computación para adaptarla a la carga de trabajo.

¿Listo para empezar a construir? Descarga IronPDF y pruébalo con una prueba gratuita: la misma biblioteca maneja todo, desde el renderizado de un solo archivo hasta pipelines por lotes de cien mil archivos. Si tiene alguna pregunta sobre el escalado, el despliegue o la arquitectura para su caso de uso específico, póngase en contacto con nuestro equipo de soporte de ingeniería: hemos ayudado a equipos a crear procesos por lotes a todas las escalas y estaremos encantados de ayudarle a hacerlo bien.

Preguntas Frecuentes

¿Qué es el procesamiento de PDF por lotes en C#?

El procesamiento de PDF por lotes en C# se refiere al manejo automatizado de numerosos documentos PDF simultáneamente, utilizando el lenguaje de programación C#. Este método es ideal para automatizar flujos de trabajo de documentos a escala.

¿Cómo puede IronPDF ayudar en el procesamiento de PDF por lotes?

IronPDF proporciona sólidas herramientas y bibliotecas que agilizan el procesamiento de PDF por lotes en C#. Es compatible con el procesamiento en paralelo, lo que permite la gestión eficaz de miles de archivos PDF simultáneamente.

¿Cuáles son las ventajas de utilizar el procesamiento paralelo con IronPDF?

El procesamiento paralelo con IronPDF permite un procesamiento por lotes de PDF más rápido y eficaz. Este enfoque maximiza la utilización de recursos y reduce significativamente el tiempo de procesamiento.

¿Se puede implementar IronPDF en plataformas en la nube para el procesamiento por lotes?

Sí, IronPDF se puede implementar en plataformas en la nube como Azure Functions, AWS Lambda y Kubernetes, lo que permite un procesamiento de PDF por lotes escalable y flexible.

¿Cómo gestiona IronPDF los errores durante el procesamiento de PDF por lotes?

IronPDF incluye funciones de gestión de errores y lógica de reintento que garantizan la fiabilidad durante el procesamiento de PDF por lotes. Estas funciones ayudan a gestionar y rectificar errores sin intervención manual.

¿Cuál es la función de la lógica de reintento en el procesamiento de PDF con IronPDF?

La lógica de reintento de IronPDF garantiza que los problemas temporales no interrumpan el flujo de trabajo de procesamiento por lotes. Si se produce un error, IronPDF puede intentar automáticamente volver a procesar el documento que ha fallado.

¿Por qué C# es un lenguaje adecuado para el procesamiento de PDF por lotes?

C# es un potente lenguaje de programación con amplias bibliotecas y marcos de trabajo que lo hacen ideal para el procesamiento de PDF por lotes. Se integra perfectamente con IronPDF para una automatización eficaz de los documentos.

¿Cómo garantiza IronPDF la seguridad de los documentos PDF durante su procesamiento?

IronPDF es compatible con el manejo seguro de documentos PDF mediante funciones de cifrado y protección por contraseña, lo que garantiza la confidencialidad y seguridad de los documentos procesados.

¿Cuáles son algunos de los casos de uso del procesamiento de PDF por lotes en las empresas?

Las empresas utilizan el procesamiento de PDF por lotes para tareas como la generación masiva de facturas, la digitalización de documentos y la distribución de informes a gran escala. IronPDF facilita estos casos de uso automatizando y agilizando los flujos de trabajo de documentos.

¿Puede IronPDF manejar diferentes formatos y versiones de PDF?

Sí, IronPDF está diseñado para manejar varios formatos y versiones de PDF, lo que garantiza la compatibilidad y la flexibilidad en las tareas de procesamiento por lotes.

Ahmad Sohail
Desarrollador Full Stack

Ahmad es un desarrollador full-stack con una sólida base en C#, Python y tecnologías web. Tiene un profundo interés en construir soluciones de software escalables y disfruta explorando cómo el diseño y la funcionalidad se encuentran en aplicaciones del mundo real.

...
Leer más

¿Listo para empezar?

Nuget Downloads 20,667,543Versión:2026.7recién publicada

Obtenga su Clave de Prueba de 30 días gratis al instante.
No se requiere tarjeta de crédito ni creación de cuenta
Biblioteca C# NuGet para PDF
Instalar con NuGet

Versión: 2026.7

PM > Install-Package IronPdf
nuget.org/packages/IronPdf/
  1. En el Explorador de Soluciones, haga clic derecho en Referencias, Administrar Paquetes NuGet
  2. Seleccione Examinar y busque "IronPDF"
  3. Seleccione el paquete e instale
C# PDF DLL
Descargar DLL

Versión: 2026.7

Descargar ahora

o descargue el Instalador de Windows aquí.

  1. Descargue y descomprima IronPDF en una ubicación como ~/Libs dentro del directorio de su solución
  2. En el Explorador de Soluciones de Visual Studio, haga clic derecho en Referencias. Seleccione Examinar, "IronPDF.dll"

Licencias desde $999

Key in blue circle

Obtenga su clave de prueba gratuita de 30 días al instante.

Sin limitaciones. 100 % desbloqueado. Sin tarjeta de crédito.

bullet_checkedNo se requiere tarjeta de crédito ni creación de cuentaSin limitaciones. 100 % desbloqueado. Sin tarjeta de crédito.
  • Logo Aetna
  • Logo NASA
  • Logo GE
  • Logo Porsche
  • Logo USDA
  • Logo Qatar
Join Millions of Engineers who’ve tried IronPDF
Reserve su Demostración en Vivo gratuita
Booking Badge

Confiado por millones de ingenieros en todo el mundo

Logos de clientes de Iron Software
Obtén tu Consulta Sin Compromiso
Completa el formulario a continuación o envía un correo a sales@ironsoftware.com
Tus detalles siempre serán mantenidos confidenciales.
Confiado por millones de ingenieros en todo el mundo
Logos de clientes de Iron Software
Obtenga su Clave de Prueba de 30 días gratis al instante.
No se requiere tarjeta de crédito ni creación de cuenta