Procesamiento de PDF por lotes en C#: Automatice los flujos de trabajo de documentos a escala

This article was translated from English: Does it need improvement?
Translated
View the article in English

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 a prueba de subprocesos de IronPDF y la gestión de memoria basada en IDisposable hacen que esté diseñado para la automatización de PDF de alto rendimiento, tanto si se ejecuta en las instalaciones como 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 aprenderás: Conversión paralela de HTML a PDF con Parallel.ForEach, operaciones de fusión y división por lotes, canalizaciones asíncronas con SemaphoreSlim para el control de la concurrencia, gestión de errores con lógica de salto en caso de fallo y reintento, patrones de punto de comprobación/reanudación para la recuperación 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 no tiene estado por renderización, lo que significa que varios subprocesos pueden compartir de forma segura una única instancia de renderización. En combinación con la biblioteca Task Parallel Library de .NET y IDisposable en PdfDocument, se obtiene un comportamiento predecible de la memoria y una saturación de la 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:

Nuget IconEmpieza a crear PDF con NuGet ahora:

  1. Instalar IronPDF con el gestor de paquetes NuGet

    PM > Install-Package IronPdf

  2. Copie 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");
    });
  3. Despliegue para probar en su entorno real

    Empieza a utilizar 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";
IronPdf.License.LicenseKey = "KEY";
Imports IronPdf

IronPdf.License.LicenseKey = "KEY"
$vbLabelText   $csharpLabel

Comience a usar IronPDF en su proyecto hoy con una prueba gratuita.

Primer Paso:
green arrow pointer
NuGet Instalar con NuGet

PM >  Install-Package IronPdf

Echa un vistazo a IronPDF en NuGet para una instalación rápida. Con más de 10 millones de descargas, está transformando el desarrollo de PDF con C#. También puede descargar el DLL o el instalador de Windows.

Índice


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. 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, Corrección de la conformidad- cada uno con un icono, un intervalo de volumen típico y un 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:

Install-Package IronPdf
Install-Package IronPdf
SHELL

O utilizando la CLI de .NET:

dotnet add package IronPdf
dotnet add package IronPdf
SHELL

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 de producción, configure su clave de licencia con License.LicenseKey al iniciar la aplicación antes de iniciar cualquier operación 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 varias instancias de ChromePdfRenderer en distintos subprocesos o compartir una única instancia: IronPDF se encarga de la sincronización interna. La recomendación oficial para el procesamiento por lotes es utilizar el Parallel.ForEach integrado en .NET, que distribuye el trabajo entre todos los núcleos de CPU disponibles de forma automática.

Dicho esto, "thread-safe" no significa "usar hilos ilimitados" Cada operación de renderizado de PDF concurrente consume memoria (el motor Chromium necesita espacio de trabajo para el análisis DOM, el diseño CSS y la rasterización de imágenes), y lanzar demasiadas operaciones paralelas en un sistema con memoria limitada degradará el rendimiento o provocará 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 - ajústelo a aproximadamente la mitad de sus núcleos de CPU disponibles como punto de partida, luego ajústelo basándose en 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 contiene el contenido binario completo de un PDF en la memoria, y no disponer de estos objetos hará que la memoria crezca linealmente con el número de archivos procesados.

La regla crítica: utilizar siempre declaraciones using o llamar explícitamente a Dispose() en objetos PdfDocument. El PdfDocument de IronPDF implementa IDisposable, y no disponer de él 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 disponer - 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 por lo general se debe dejar que la GC se autogestione, el procesamiento por lotes es uno de los raros escenarios en los que llamar a GC.Collect() entre los límites de los trozos puede evitar que se acumule presión en la memoria.

Monitorizar el consumo de memoria utilizando 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. Utilice Interlocked.Increment para obtener contadores a prueba de hilos cuando ejecute operaciones paralelas, y registre a intervalos regulares (cada 50 o 100 archivos) en lugar de cada archivo para evitar inundar su salida. Realice un seguimiento del tiempo transcurrido con System.Diagnostics.Stopwatch y calcule una tasa de archivos por segundo en ejecución para obtener un tiempo estimado de llegada 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
HTML Invoice INV-2026-002
HTML Invoice INV-2026-003
HTML Invoice INV-2026-004
HTML Invoice INV-2026-005

La implementación utiliza ChromePdfRenderer con Parallel.ForEach para procesar todos los archivos HTML de forma concurrente, controlando el paralelismo mediante 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 seguimiento del progreso mediante contadores Interlocked a prueba de hilos.

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-html-to-pdf.cs
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");
Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Imports System.Threading

' Configure paths
Dim inputFolder As String = "input/"
Dim outputFolder As String = "output/"

Directory.CreateDirectory(outputFolder)

Dim htmlFiles As String() = Directory.GetFiles(inputFolder, "*.html")
Console.WriteLine($"Found {htmlFiles.Length} HTML files to convert")

' Create renderer instance (thread-safe, can be shared)
Dim renderer As New ChromePdfRenderer()

' Track progress
Dim processed As Integer = 0
Dim failed As Integer = 0

' Process in parallel with controlled concurrency
Dim options As New ParallelOptions With {
    .MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
}

Parallel.ForEach(htmlFiles, options, Sub(htmlFile)
    Try
        Dim fileName As String = Path.GetFileNameWithoutExtension(htmlFile)
        Dim outputPath As String = Path.Combine(outputFolder, $"{fileName}.pdf")

        Using pdf = renderer.RenderHtmlFileAsPdf(htmlFile)
            pdf.SaveAs(outputPath)
        End Using

        Interlocked.Increment(processed)
        Console.WriteLine($"[OK] {fileName}.pdf")
    Catch ex As Exception
        Interlocked.Increment(failed)
        Console.WriteLine($"[ERROR] {Path.GetFileName(htmlFile)}: {ex.Message}")
    End Try
End Sub)

Console.WriteLine($"{vbCrLf}Complete: {processed} succeeded, {failed} failed")
$vbLabelText   $csharpLabel

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 sencillo: cargue su plantilla HTML una vez, utilice string.Replace para inyectar datos por registro (nombre del cliente, totales, fechas) y pase el HTML rellenado a RenderHtmlAsPdf dentro de su bucle paralelo. IronPDF también proporciona RenderHtmlAsPdfAsync para escenarios en los que desee utilizar async/await en lugar de Parallel.ForEach - cubriremos los patrones async en 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.

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-merge.cs
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");
Imports IronPdf
Imports System
Imports System.IO
Imports System.Linq
Imports System.Collections.Generic

Module Program
    Sub Main()
        Dim inputFolder As String = "documents/"
        Dim outputFolder As String = "merged/"

        Directory.CreateDirectory(outputFolder)

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

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

        For Each group In groups
            Dim groupName As String = group.Key
            Dim filesToMerge = group.OrderBy(Function(f) f).ToList()

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

            Try
                ' Load all PDFs for this group
                Dim pdfDocs As New List(Of PdfDocument)()
                For Each filePath As String In filesToMerge
                    pdfDocs.Add(PdfDocument.FromFile(filePath))
                Next

                ' Merge all documents
                Using merged = PdfDocument.Merge(pdfDocs)
                    merged.SaveAs(Path.Combine(outputFolder, $"{groupName}-merged.pdf"))
                End Using

                ' Dispose source documents
                For Each doc In pdfDocs
                    doc.Dispose()
                Next

                Console.WriteLine($"  [OK] Created {groupName}-merged.pdf ({merged.PageCount} pages)")
            Catch ex As Exception
                Console.WriteLine($"  [ERROR] {groupName}: {ex.Message}")
            End Try
        Next

        Console.WriteLine(vbCrLf & "Merge complete")
    End Sub
End Module
$vbLabelText   $csharpLabel

Para combinar un gran número de archivos, tenga en cuenta la memoria: el método PdfDocument.Merge carga todos los documentos de origen en la memoria simultáneamente. 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 siguiente código muestra la extracción de páginas individuales mediante CopyPage en un bucle paralelo, creando archivos PDF independientes para cada página. Una función de ayuda alternativa SplitByRange muestra cómo extraer intervalos de páginas en lugar de páginas individuales, lo que resulta útil para dividir documentos grandes en segmentos más pequeños.

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-split.cs
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");
Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks

Module Program
    Sub Main()
        Dim inputFolder As String = "multipage/"
        Dim outputFolder As String = "split/"

        Directory.CreateDirectory(outputFolder)

        Dim pdfFiles As String() = Directory.GetFiles(inputFolder, "*.pdf")
        Console.WriteLine($"Found {pdfFiles.Length} PDFs to split")

        Dim options As New ParallelOptions With {
            .MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
        }

        Parallel.ForEach(pdfFiles, options, Sub(pdfFile)
                                                Dim baseName As String = Path.GetFileNameWithoutExtension(pdfFile)

                                                Try
                                                    Using pdf = PdfDocument.FromFile(pdfFile)
                                                        Dim pageCount As Integer = pdf.PageCount

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

                                                        ' Extract each page as a separate PDF
                                                        For i As Integer = 0 To pageCount - 1
                                                            Using singlePage = pdf.CopyPage(i)
                                                                Dim outputPath As String = Path.Combine(outputFolder, $"{baseName}-page-{i + 1:D3}.pdf")
                                                                singlePage.SaveAs(outputPath)
                                                            End Using
                                                        Next

                                                        Console.WriteLine($"  [OK] Created {pageCount} files from {baseName}.pdf")
                                                    End Using
                                                Catch ex As Exception
                                                    Console.WriteLine($"  [ERROR] {baseName}: {ex.Message}")
                                                End Try
                                            End Sub)

        Console.WriteLine(vbCrLf & "Split complete")
    End Sub

    ' Alternative: Extract page ranges instead of individual pages
    Sub SplitByRange(inputFile As String, outputFolder As String, pagesPerChunk As Integer)
        Using pdf = PdfDocument.FromFile(inputFile)
            Dim baseName As String = Path.GetFileNameWithoutExtension(inputFile)
            Dim totalPages As Integer = pdf.PageCount
            Dim chunkNumber As Integer = 1

            For startPage As Integer = 0 To totalPages - 1 Step pagesPerChunk
                Dim endPage As Integer = Math.Min(startPage + pagesPerChunk - 1, totalPages - 1)
                Using chunk = pdf.CopyPages(startPage, endPage)
                    chunk.SaveAs(Path.Combine(outputFolder, $"{baseName}-chunk-{chunkNumber:D3}.pdf"))
                    chunkNumber += 1
                End Using
            Next
        End Using
    End Sub
End Module
$vbLabelText   $csharpLabel

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 métodos de compresión: CompressImages para reducir la calidad/tamaño de las imágenes, y CompressStructTree para eliminar los metadatos estructurales. La nueva API CompressAndSaveAs (introducida en la versión 2025.12) proporciona una compresión superior combinando múltiples técnicas de optimización.

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-compression.cs
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}%");
Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Imports System.Threading

Module Program
    Sub Main()
        Dim inputFolder As String = "originals/"
        Dim outputFolder As String = "compressed/"

        Directory.CreateDirectory(outputFolder)

        Dim pdfFiles As String() = Directory.GetFiles(inputFolder, "*.pdf")
        Console.WriteLine($"Found {pdfFiles.Length} PDFs to compress")

        Dim totalOriginalSize As Long = 0
        Dim totalCompressedSize As Long = 0
        Dim processed As Integer = 0

        Dim options As New ParallelOptions With {
            .MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
        }

        Parallel.ForEach(pdfFiles, options, Sub(pdfFile)
                                                Dim fileName As String = Path.GetFileName(pdfFile)
                                                Dim outputPath As String = Path.Combine(outputFolder, fileName)

                                                Try
                                                    Dim originalSize As Long = New FileInfo(pdfFile).Length
                                                    Interlocked.Add(totalOriginalSize, originalSize)

                                                    Using pdf = PdfDocument.FromFile(pdfFile)
                                                        ' Apply compression with JPEG quality setting (0-100, lower = more compression)
                                                        pdf.CompressAndSaveAs(outputPath, 60)
                                                    End Using

                                                    Dim compressedSize As Long = New FileInfo(outputPath).Length
                                                    Interlocked.Add(totalCompressedSize, compressedSize)
                                                    Interlocked.Increment(processed)

                                                    Dim reduction As Double = (1 - CDbl(compressedSize) / originalSize) * 100
                                                    Console.WriteLine($"[OK] {fileName}: {originalSize \ 1024}KB → {compressedSize \ 1024}KB ({reduction:F1}% reduction)")
                                                Catch ex As Exception
                                                    Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
                                                End Try
                                            End Sub)

        Dim totalReduction As Double = (1 - CDbl(totalCompressedSize) / totalOriginalSize) * 100
        Console.WriteLine(vbCrLf & "Compression 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}%")
    End Sub
End Module
$vbLabelText   $csharpLabel

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 provocar distorsiones en algunas configuraciones: prueba con muestras representativas antes de ejecutar un lote completo. Y la eliminación del árbol de estructura (CompressStructTree) afectará a la selección de texto y a la búsqueda en los PDF comprimidos, así que utilícelo sólo cuando no se necesiten esas capacidades.


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 conforme a la accesibilidad utilizando SaveAsPdfUA, aunque PDF/UA requiere documentos de origen con el etiquetado estructural adecuado.

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-format-conversion.cs
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}");
        }
    });
}
Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Imports System.Threading

Module Program
    Sub Main()
        Dim inputFolder As String = "originals/"
        Dim outputFolder As String = "pdfa-archive/"

        Directory.CreateDirectory(outputFolder)

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

        Dim converted As Integer = 0
        Dim failed As Integer = 0

        Dim options As New ParallelOptions With {
            .MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
        }

        Parallel.ForEach(pdfFiles, options, Sub(pdfFile)
                                                Dim fileName As String = Path.GetFileName(pdfFile)
                                                Dim outputPath As String = Path.Combine(outputFolder, fileName)

                                                Try
                                                    Using pdf = PdfDocument.FromFile(pdfFile)
                                                        ' Convert to PDF/A-3b for long-term archival
                                                        pdf.SaveAsPdfA(outputPath, PdfAVersions.PdfA3b)

                                                        Interlocked.Increment(converted)
                                                        Console.WriteLine($"[OK] {fileName} → PDF/A-3b")
                                                    End Using
                                                Catch ex As Exception
                                                    Interlocked.Increment(failed)
                                                    Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
                                                End Try
                                            End Sub)

        Console.WriteLine($"{vbCrLf}Conversion complete: {converted} succeeded, {failed} failed")
    End Sub

    ' Alternative: Convert to PDF/UA for accessibility compliance
    Sub ConvertToPdfUA(inputFolder As String, outputFolder As String)
        Directory.CreateDirectory(outputFolder)
        Dim files As String() = Directory.GetFiles(inputFolder, "*.pdf")

        Parallel.ForEach(files, Sub(pdfFile)
                                    Dim fileName As String = Path.GetFileName(pdfFile)
                                    Try
                                        Using 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")
                                        End Using
                                    Catch ex As Exception
                                        Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
                                    End Try
                                End Sub)
    End Sub
End Module
$vbLabelText   $csharpLabel

Resultado

Comparación de resultados de conversión PDF/A con metadatos de conformidad

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.


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 suena obvio, pero es sorprendentemente fácil de pasar por alto cuando se utiliza Parallel.ForEach - una excepción no manejada en cualquier tarea paralela se propagará como una AggregateException y terminará el bucle.

El siguiente ejemplo muestra tanto la lógica de omisión en caso de fallo como la de reintento juntas, envolviendo cada archivo en un try-catch para una gestión de errores elegante, con un bucle de reintento interno que utiliza un backoff exponencial para excepciones transitorias como IOException y OutOfMemoryException:

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-error-handling-retry.cs
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);
}
Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading
Imports System.Collections.Concurrent

Module Program
    Sub Main()
        Dim inputFolder As String = "input/"
        Dim outputFolder As String = "output/"
        Dim errorLogPath As String = "error-log.txt"

        Directory.CreateDirectory(outputFolder)

        Dim htmlFiles As String() = Directory.GetFiles(inputFolder, "*.html")
        Dim renderer As New ChromePdfRenderer()
        Dim errorLog As New ConcurrentBag(Of String)()

        Dim processed As Integer = 0
        Dim failed As Integer = 0
        Dim retried As Integer = 0

        Const maxRetries As Integer = 3

        Dim options As New ParallelOptions With {
            .MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
        }

        Parallel.ForEach(htmlFiles, options, Sub(htmlFile)
                                                 Dim fileName As String = Path.GetFileNameWithoutExtension(htmlFile)
                                                 Dim outputPath As String = Path.Combine(outputFolder, $"{fileName}.pdf")
                                                 Dim attempt As Integer = 0
                                                 Dim success As Boolean = False

                                                 While attempt < maxRetries AndAlso Not success
                                                     attempt += 1
                                                     Try
                                                         Using pdf = renderer.RenderHtmlFileAsPdf(htmlFile)
                                                             pdf.SaveAs(outputPath)
                                                             success = True
                                                             Interlocked.Increment(processed)

                                                             If attempt > 1 Then
                                                                 Interlocked.Increment(retried)
                                                                 Console.WriteLine($"[OK] {fileName}.pdf (succeeded on attempt {attempt})")
                                                             Else
                                                                 Console.WriteLine($"[OK] {fileName}.pdf")
                                                             End If
                                                         End Using
                                                     Catch ex As Exception When IsTransientException(ex) AndAlso attempt < maxRetries
                                                         ' Transient error - wait and retry with exponential backoff
                                                         Dim delayMs As Integer = CInt(Math.Pow(2, attempt)) * 500
                                                         Console.WriteLine($"[RETRY] {fileName}: {ex.Message} (attempt {attempt}, waiting {delayMs}ms)")
                                                         Thread.Sleep(delayMs)
                                                     Catch ex As Exception
                                                         ' Non-transient error or max retries exceeded
                                                         Interlocked.Increment(failed)
                                                         Dim errorMessage As String = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.GetType().Name} | {ex.Message}"
                                                         errorLog.Add(errorMessage)
                                                         Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
                                                     End Try
                                                 End While
                                             End Sub)

        ' Write error log
        If errorLog.Count > 0 Then
            File.WriteAllLines(errorLogPath, errorLog)
        End If

        Console.WriteLine(vbCrLf & "Batch complete:")
        Console.WriteLine($"  Processed: {processed}")
        Console.WriteLine($"  Failed: {failed}")
        Console.WriteLine($"  Retried: {retried}")
        If failed > 0 Then
            Console.WriteLine($"  Error log: {errorLogPath}")
        End If
    End Sub

    ' Helper to identify transient exceptions worth retrying
    Function IsTransientException(ex As Exception) As Boolean
        Return TypeOf ex Is IOException OrElse
               TypeOf ex Is OutOfMemoryException OrElse
               ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) OrElse
               ex.Message.Contains("locked", StringComparison.OrdinalIgnoreCase)
    End Function
End Module
$vbLabelText   $csharpLabel

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. Una IOException (archivo bloqueado) o OutOfMemoryException (presión temporal) vale la pena reintentar. Una ArgumentException (entrada no válida) o un error de renderización consistente no lo son - reintentar no ayudará, y perderás 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ó.

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-checkpointing.cs
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}");
Imports IronPdf
Imports System
Imports System.IO
Imports System.Linq
Imports System.Threading.Tasks
Imports System.Threading
Imports System.Collections.Generic

Module Program
    Sub Main()
        Dim inputFolder As String = "input/"
        Dim outputFolder As String = "output/"
        Dim checkpointPath As String = "checkpoint.txt"
        Dim errorLogPath As String = "errors.txt"

        Directory.CreateDirectory(outputFolder)

        ' Load checkpoint - files already processed successfully
        Dim completedFiles As New HashSet(Of String)()
        If File.Exists(checkpointPath) Then
            completedFiles = New HashSet(Of String)(File.ReadAllLines(checkpointPath))
            Console.WriteLine($"Resuming from checkpoint: {completedFiles.Count} files already processed")
        End If

        ' Get files to process (excluding already completed)
        Dim allFiles As String() = Directory.GetFiles(inputFolder, "*.html")
        Dim filesToProcess As String() = allFiles _
            .Where(Function(f) Not completedFiles.Contains(Path.GetFileName(f))) _
            .ToArray()

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

        Dim renderer As New ChromePdfRenderer()
        Dim checkpointLock As New Object()
        Dim processed As Integer = 0
        Dim failed As Integer = 0

        Dim options As New ParallelOptions With {
            .MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
        }

        Parallel.ForEach(filesToProcess, options, Sub(htmlFile)
                                                      Dim fileName As String = Path.GetFileName(htmlFile)
                                                      Dim baseName As String = Path.GetFileNameWithoutExtension(htmlFile)
                                                      Dim outputPath As String = Path.Combine(outputFolder, $"{baseName}.pdf")

                                                      Try
                                                          Using pdf = renderer.RenderHtmlFileAsPdf(htmlFile)
                                                              pdf.SaveAs(outputPath)
                                                          End Using

                                                          ' Record success in checkpoint (thread-safe)
                                                          SyncLock checkpointLock
                                                              File.AppendAllText(checkpointPath, fileName & Environment.NewLine)
                                                          End SyncLock

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

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

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

        Console.WriteLine(vbCrLf & "Batch complete:")
        Console.WriteLine($"  Newly processed: {processed}")
        Console.WriteLine($"  Failed: {failed}")
        Console.WriteLine($"  Total completed: {completedFiles.Count + processed}")
        Console.WriteLine($"  Checkpoint saved to: {checkpointPath}")
    End Sub
End Module
$vbLabelText   $csharpLabel

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 las funciones de ayuda 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.

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-validation.cs
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;
    }
}
Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading
Imports System.Collections.Concurrent

Module Program
    Sub Main()
        Dim inputFolder As String = "input/"
        Dim outputFolder As String = "output/"
        Dim validatedFolder As String = "validated/"
        Dim rejectedFolder As String = "rejected/"

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

        Dim inputFiles As String() = Directory.GetFiles(inputFolder, "*.html")
        Dim renderer As New ChromePdfRenderer()

        Dim preValidationFailed As Integer = 0
        Dim processingFailed As Integer = 0
        Dim postValidationFailed As Integer = 0
        Dim succeeded As Integer = 0

        Dim options As New ParallelOptions With {
            .MaxDegreeOfParallelism = Environment.ProcessorCount \ 2
        }

        Parallel.ForEach(inputFiles, options, Sub(inputFile)
                                                  Dim fileName As String = Path.GetFileNameWithoutExtension(inputFile)
                                                  Dim outputPath As String = Path.Combine(outputFolder, $"{fileName}.pdf")

                                                  ' Pre-validation: Check input file
                                                  If Not PreValidate(inputFile) Then
                                                      Interlocked.Increment(preValidationFailed)
                                                      Console.WriteLine($"[SKIP] {fileName}: Failed pre-validation")
                                                      Return
                                                  End If

                                                  Try
                                                      ' Process
                                                      Using pdf = renderer.RenderHtmlFileAsPdf(inputFile)
                                                          pdf.SaveAs(outputPath)

                                                          ' Post-validation: Check output file
                                                          If PostValidate(outputPath) Then
                                                              ' Move to validated folder
                                                              Dim validatedPath As String = Path.Combine(validatedFolder, $"{fileName}.pdf")
                                                              File.Move(outputPath, validatedPath, overwrite:=True)
                                                              Interlocked.Increment(succeeded)
                                                              Console.WriteLine($"[OK] {fileName}.pdf (validated)")
                                                          Else
                                                              ' Move to rejected folder for manual review
                                                              Dim rejectedPath As String = Path.Combine(rejectedFolder, $"{fileName}.pdf")
                                                              File.Move(outputPath, rejectedPath, overwrite:=True)
                                                              Interlocked.Increment(postValidationFailed)
                                                              Console.WriteLine($"[REJECT] {fileName}.pdf: Failed post-validation")
                                                          End If
                                                      End Using
                                                  Catch ex As Exception
                                                      Interlocked.Increment(processingFailed)
                                                      Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
                                                  End Try
                                              End Sub)

        Console.WriteLine(vbCrLf & "Validation summary:")
        Console.WriteLine($"  Succeeded: {succeeded}")
        Console.WriteLine($"  Pre-validation failed: {preValidationFailed}")
        Console.WriteLine($"  Processing failed: {processingFailed}")
        Console.WriteLine($"  Post-validation failed: {postValidationFailed}")
    End Sub

    ' Pre-validation: Quick checks on input file
    Function PreValidate(filePath As String) As Boolean
        Try
            Dim fileInfo As New FileInfo(filePath)

            ' Check file exists and is readable
            If Not fileInfo.Exists Then Return False

            ' Check file is not empty
            If fileInfo.Length = 0 Then Return False

            ' Check file is not too large (e.g., 50MB limit)
            If fileInfo.Length > 50 * 1024 * 1024 Then Return False

            ' Quick content check - must be valid HTML
            Dim content As String = File.ReadAllText(filePath)
            If String.IsNullOrWhiteSpace(content) Then Return False
            If Not content.Contains("<html", StringComparison.OrdinalIgnoreCase) AndAlso
               Not content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase) Then
                Return False
            End If

            Return True
        Catch
            Return False
        End Try
    End Function

    ' Post-validation: Verify output PDF meets requirements
    Function PostValidate(pdfPath As String) As Boolean
        Try
            Using pdf = PdfDocument.FromFile(pdfPath)
                ' Check PDF has at least one page
                If pdf.PageCount < 1 Then Return False

                ' Check file size is reasonable (not just header, not corrupted)
                Dim fileInfo As New FileInfo(pdfPath)
                If fileInfo.Length < 1024 Then Return False

                Return True
            End Using
        Catch
            Return False
        End Try
    End Function
End Module
$vbLabelText   $csharpLabel

Resultado

Captura de pantalla del procesamiento de entrada de validación

Estructura de carpetas mostrando el directorio validado con 5 archivos y el 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.


Patrones de procesamiento asíncrono y paralelo

IronPDF soporta tanto Parallel.ForEach (paralelismo basado en hilos) como async/await (E/S asíncrona). 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 eficaz para las operaciones por lotes limitadas por la CPU. El motor de renderizado de IronPDF consume mucha CPU (análisis de HTML, diseño de CSS, rasterización de imágenes), y Parallel.ForEach distribuye automáticamente este trabajo entre todos los núcleos disponibles.

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-tpl.cs
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();
    }
}
Imports IronPdf
Imports System
Imports System.IO
Imports System.Threading.Tasks
Imports System.Threading
Imports System.Diagnostics

Module Program
    Sub Main()
        Dim inputFolder As String = "input/"
        Dim outputFolder As String = "output/"

        Directory.CreateDirectory(outputFolder)

        Dim htmlFiles As String() = Directory.GetFiles(inputFolder, "*.html")
        Dim renderer As New ChromePdfRenderer()

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

        Dim processed As Integer = 0
        Dim stopwatch As Stopwatch = Stopwatch.StartNew()

        ' Configure parallelism based on system resources
        ' Rule of thumb: ProcessorCount / 2 for memory-intensive operations
        Dim options As New ParallelOptions With {
            .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, Sub(htmlFile)
                                                 Dim fileName As String = Path.GetFileNameWithoutExtension(htmlFile)
                                                 Dim outputPath As String = Path.Combine(outputFolder, $"{fileName}.pdf")

                                                 Try
                                                     ' Render HTML to PDF
                                                     Using pdf = renderer.RenderHtmlFileAsPdf(htmlFile)
                                                         pdf.SaveAs(outputPath)
                                                     End Using

                                                     Dim current As Integer = Interlocked.Increment(processed)

                                                     ' Progress reporting every 10 files
                                                     If current Mod 10 = 0 Then
                                                         Dim elapsed As Double = stopwatch.Elapsed.TotalSeconds
                                                         Dim rate As Double = current / elapsed
                                                         Dim remaining As Double = (htmlFiles.Length - current) / rate
                                                         Console.WriteLine($"Progress: {current}/{htmlFiles.Length} ({rate:F1} files/sec, ~{remaining:F0}s remaining)")
                                                     End If
                                                 Catch ex As Exception
                                                     Console.WriteLine($"[ERROR] {fileName}: {ex.Message}")
                                                 End Try
                                             End Sub)

        stopwatch.Stop()
        Dim totalRate As Double = processed / stopwatch.Elapsed.TotalSeconds

        Console.WriteLine(vbCrLf & "Complete:")
        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)
        CheckMemoryPressure()
    End Sub

    Sub CheckMemoryPressure()
        Const memoryThreshold As Long = 4L * 1024 * 1024 * 1024 ' 4 GB
        Dim currentMemory As Long = GC.GetTotalMemory(forceFullCollection:=False)

        If currentMemory > memoryThreshold Then
            Console.WriteLine($"Memory pressure detected ({currentMemory \ 1024 \ 1024}MB), forcing GC...")
            GC.Collect()
            GC.WaitForPendingFinalizers()
            GC.Collect()
        End If
    End Sub
End Module
$vbLabelText   $csharpLabel

La opción MaxDegreeOfParallelism es fundamental. 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 se necesita un control más preciso sobre la concurrencia que el que proporciona Parallel.ForEach - por ejemplo, cuando se mezcla la E/S asíncrona con el renderizado limitado a la CPU - SemaphoreSlim proporciona un control explícito sobre cuántas operaciones se ejecutan simultáneamente. El patrón es sencillo: crear un SemaphoreSlim con el límite de concurrencia deseado (por ejemplo, 4 renderizaciones concurrentes), llamar a WaitAsync antes de cada renderización, y Release en un bloque finally después. A continuación, 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.

Mejores prácticas de Async/Await

IronPDF ofrece variantes asíncronas de sus métodos de renderizado, como 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 utilice Task.Run para envolver métodos IronPDF síncronos - utilice las variantes nativas async en su lugar. Envolver métodos síncronos en Task.Run desperdicia un hilo del grupo de hilos y añade sobrecarga sin ningún beneficio.

No utilices .Result o .Wait() en tareas asíncronas - esto bloquea el hilo de llamada y puede causar bloqueos en contextos UI o ASP.NET. Utilice siempre await.

Lotea tus llamadas Task.WhenAll en lugar de esperar todas las tareas a la vez. Si tienes 10.000 tareas y llamas a Task.WhenAll en todas ellas simultáneamente, lanzarás 10.000 operaciones concurrentes. En su lugar, utilice .Chunk(10) o un enfoque similar para procesarlos 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. El enfoque defensivo consiste en monitorizar el uso de memoria con GC.GetTotalMemory() antes de cada renderización y activar una recolección cuando el consumo cruza un umbral (por ejemplo, 4 GB u 80% de la RAM disponible). Llame a GC.Collect() seguido de GC.WaitForPendingFinalizers() y un segundo GC.Collect() para recuperar tanta memoria como sea posible antes de continuar. Esto añade una pequeña pausa, pero evita la alternativa catastrófica de una OutOfMemoryException que bloquea todo el lote en el archivo #30.000.

Combine esto con el MaxDegreeOfParallelism de la sección TPL y el patrón de eliminación de la sección de gestión de memoria, y tendrá una defensa de tres capas contra los problemas de memoria: limitar la concurrencia, eliminar agresivamente y supervisar 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.

Asegure las funciones con funciones duraderas

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 en abanico, cada función de actividad instancia un ChromePdfRenderer, procesa su trozo de archivos y el orquestador recopila 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 objeto S3, carga cada PDF con PdfDocument.FromFile, aplica su canal de procesamiento (compresión, conversión de formato, etc.) y escribe los resultados en un cubo S3 de salida.

AWS Lambda tiene un tiempo máximo de ejecución de 15 minutos y un 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 worker sigue el mismo patrón descrito en secciones anteriores: poner en cola un mensaje, utilizar ChromePdfRenderer.RenderHtmlAsPdf() o PdfDocument.FromFile() para procesar el documento, cargar el resultado y acusar recibo del mensaje. Envuelve el procesamiento en el mismo try-catch con lógica de reintento de los patrones de resiliencia, y utiliza SemaphoreSlim para controlar la concurrencia por pod.

IronPDF es compatible con Docker y se ejecuta en contenedores Linux. Utilice el paquete NuGet IronPdf con los paquetes de tiempo de ejecución nativos adecuados para el sistema operativo de su contenedor (por ejemplo, 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 canal de procesamiento por lotes nativo de la nube con cola de trabajos, capa de cálculo, almacenamiento de objetos y panel de control de supervisión


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...

:path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-full-pipeline.cs
using IronPdf;
using System;
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
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; }
}

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

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; }
}

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; }
}
Imports IronPdf
Imports System
Imports System.IO
Imports System.Linq
Imports System.Threading.Tasks
Imports System.Threading
Imports System.Collections.Concurrent
Imports System.Diagnostics
Imports System.Text.Json

' Configuration
Dim config As New PipelineConfig With {
    .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
Dim checkpoint = LoadCheckpoint(config.CheckpointPath)
Dim results As New ConcurrentBag(Of ProcessingResult)()
Dim stopwatch = Stopwatch.StartNew()

' Get files to process
Dim allFiles As String() = Directory.GetFiles(config.InputFolder, "*.html")
Dim filesToProcess As String() = allFiles.
    Where(Function(f) Not 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}")

Dim renderer As New ChromePdfRenderer()
Dim checkpointLock As New Object()

Dim options As New ParallelOptions With {
    .MaxDegreeOfParallelism = config.MaxConcurrency
}

Parallel.ForEach(filesToProcess, options, Sub(inputFile)
    Dim result As New ProcessingResult With {
        .FileName = Path.GetFileName(inputFile),
        .StartTime = DateTime.UtcNow
    }

    Try
        ' Stage: Pre-validation
        If Not ValidateInput(inputFile) Then
            result.Status = "PreValidationFailed"
            result.Error = "Input file failed validation"
            results.Add(result)
            Return
        End If

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

        ' Stage: Process with retry
        Dim pdf As PdfDocument = Nothing
        Dim attempt As Integer = 0
        Dim success As Boolean = False

        While attempt < config.MaxRetries AndAlso Not success
            attempt += 1
            Try
                pdf = renderer.RenderHtmlFileAsPdf(inputFile)
                success = True
            Catch ex As Exception When IsTransient(ex) AndAlso attempt < config.MaxRetries
                Thread.Sleep(CInt(Math.Pow(2, attempt)) * 500)
            End Try
        End While

        If Not success OrElse pdf Is Nothing Then
            result.Status = "ProcessingFailed"
            result.Error = "Max retries exceeded"
            results.Add(result)
            Return
        End If

        Using pdf
            ' Stage: Compress and convert to PDF/A-3b for archival
            pdf.SaveAsPdfA(tempPath, PdfAVersions.PdfA3b)
        End Using

        ' Stage: Post-validation
        If Not ValidateOutput(tempPath) Then
            File.Move(tempPath, Path.Combine(config.ErrorFolder, $"{baseName}.pdf"), overwrite:=True)
            result.Status = "PostValidationFailed"
            result.Error = "Output file failed validation"
            results.Add(result)
            Return
        End If

        ' Stage: Archive
        File.Move(tempPath, archivePath, overwrite:=True)

        ' Update checkpoint
        SyncLock checkpointLock
            checkpoint.CompletedFiles.Add(result.FileName)
            SaveCheckpoint(config.CheckpointPath, checkpoint)
        End SyncLock

        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 ex As Exception
        result.Status = "Error"
        result.Error = ex.Message
        result.EndTime = DateTime.UtcNow
        results.Add(result)
        Console.WriteLine($"[ERROR] {result.FileName}: {ex.Message}")
    End Try
End Sub)

stopwatch.Stop()

' Generate report
Dim report As New PipelineReport With {
    .TotalFiles = allFiles.Length,
    .ProcessedThisRun = results.Count,
    .Succeeded = results.Count(Function(r) r.Status = "Success"),
    .PreValidationFailed = results.Count(Function(r) r.Status = "PreValidationFailed"),
    .ProcessingFailed = results.Count(Function(r) r.Status = "ProcessingFailed"),
    .PostValidationFailed = results.Count(Function(r) r.Status = "PostValidationFailed"),
    .Errors = results.Count(Function(r) r.Status = "Error"),
    .TotalDuration = stopwatch.Elapsed,
    .AverageFileTime = If(results.Any(), TimeSpan.FromMilliseconds(stopwatch.Elapsed.TotalMilliseconds / results.Count), TimeSpan.Zero)
}

Dim reportJson As String = JsonSerializer.Serialize(report, New JsonSerializerOptions With {.WriteIndented = True})
File.WriteAllText(config.ReportPath, reportJson)

Console.WriteLine(vbCrLf & "=== 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
Function ValidateInput(path As String) As Boolean
    Try
        Dim info As New FileInfo(path)
        If Not info.Exists OrElse info.Length = 0 OrElse info.Length > 50 * 1024 * 1024 Then Return False
        Dim content As String = File.ReadAllText(path)
        Return content.Contains("<html", StringComparison.OrdinalIgnoreCase) OrElse
               content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase)
    Catch
        Return False
    End Try
End Function

Function ValidateOutput(path As String) As Boolean
    Try
        Using pdf = PdfDocument.FromFile(path)
            Return pdf.PageCount > 0 AndAlso New FileInfo(path).Length > 1024
        End Using
    Catch
        Return False
    End Try
End Function

Function IsTransient(ex As Exception) As Boolean
    Return TypeOf ex Is IOException OrElse TypeOf ex Is OutOfMemoryException OrElse
           ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase)
End Function

Function LoadCheckpoint(path As String) As Checkpoint
    If File.Exists(path) Then
        Dim json As String = File.ReadAllText(path)
        Return JsonSerializer.Deserialize(Of Checkpoint)(json) OrElse New Checkpoint()
    End If
    Return New Checkpoint()
End Function

Sub SaveCheckpoint(path As String, cp As Checkpoint)
    File.WriteAllText(path, JsonSerializer.Serialize(cp))
End Sub

' Data classes
Class PipelineConfig
    Public Property InputFolder As String = ""
    Public Property OutputFolder As String = ""
    Public Property ArchiveFolder As String = ""
    Public Property ErrorFolder As String = ""
    Public Property CheckpointPath As String = ""
    Public Property ReportPath As String = ""
    Public Property MaxConcurrency As Integer
    Public Property MaxRetries As Integer
    Public Property JpegQuality As Integer
End Class

Class Checkpoint
    Public Property CompletedFiles As HashSet(Of String) = New HashSet(Of String)()
End Class

Class ProcessingResult
    Public Property FileName As String = ""
    Public Property Status As String = ""
    Public Property Error As String = ""
    Public Property OutputSize As Long
    Public Property StartTime As DateTime
    Public Property EndTime As DateTime
End Class

Class PipelineReport
    Public Property TotalFiles As Integer
    Public Property ProcessedThisRun As Integer
    Public Property Succeeded As Integer
    Public Property PreValidationFailed As Integer
    Public Property ProcessingFailed As Integer
    Public Property PostValidationFailed As Integer
    Public Property Errors As Integer
    Public Property TotalDuration As TimeSpan
    Public Property AverageFileTime As TimeSpan
End Class
$vbLabelText   $csharpLabel

Resultado

Informe JSON de línea de producción que muestra los resultados del procesamiento por lotes con el recuento de éxitos, fracasos 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.


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? Descargue IronPDF y pruébelo con una versión de prueba gratuita: la misma biblioteca se encarga de todo, desde el renderizado de un solo archivo hasta los procesos por lotes de cientos de miles de 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.

Antes ...

Leer más
¿Listo para empezar?
Nuget Descargas 17,386,124 | Versión: 2026.2 recién lanzado