feat: Implement DevExpressPdfProcessor.MergePdfsAsync with page range support
- Implement MergePdfsAsync: merges multiple PDFs with optional page ranges - Add ParsePageRange helper: parses '1-3,5' format, validates page numbers - Stream-based pipeline (no byte[] buffering) - Validates: Position = 0, minimum 2 PDFs, page ranges count - Uses DevExpress PdfDocumentProcessor for actual merge operation - Returns merged PDF as byte array
This commit is contained in:
@@ -298,6 +298,98 @@ public class DevExpressPdfProcessor : IPdfProcessor
|
||||
|
||||
#endregion
|
||||
|
||||
#region PDF Merge Operations
|
||||
|
||||
public async Task<byte[]> MergePdfsAsync(IReadOnlyList<Stream> pdfStreams, IReadOnlyList<string?>? pageRanges = null)
|
||||
{
|
||||
// 1. Validate input: minimum 2 PDFs required
|
||||
if (pdfStreams == null || pdfStreams.Count < 2)
|
||||
throw new BadRequestException("At least 2 PDF files are required for merging");
|
||||
|
||||
// 2. Validate page ranges length (if provided)
|
||||
if (pageRanges != null && pageRanges.Count != pdfStreams.Count)
|
||||
throw new BadRequestException($"Page ranges count ({pageRanges.Count}) must match PDF files count ({pdfStreams.Count})");
|
||||
|
||||
// 3. Defensive validation: all streams must be at Position = 0
|
||||
for (int i = 0; i < pdfStreams.Count; i++)
|
||||
{
|
||||
var stream = pdfStreams[i];
|
||||
|
||||
if (stream == null)
|
||||
throw new BadRequestException($"PDF stream at index {i} is null");
|
||||
|
||||
if (stream.Length == 0)
|
||||
throw new BadRequestException($"PDF stream at index {i} is empty");
|
||||
|
||||
if (stream.CanSeek && stream.Position != 0)
|
||||
throw new BadRequestException(null, new ArgumentException(
|
||||
$"PDF stream at index {i} must be positioned at the beginning (Position = 0).",
|
||||
nameof(pdfStreams)));
|
||||
}
|
||||
|
||||
// 4. Create merged PDF using DevExpress
|
||||
using var mergedProcessor = new PdfDocumentProcessor();
|
||||
|
||||
// Load first PDF as base document
|
||||
mergedProcessor.LoadDocument(pdfStreams[0]);
|
||||
|
||||
// Apply page range to first PDF if specified
|
||||
if (pageRanges != null && !string.IsNullOrWhiteSpace(pageRanges[0]))
|
||||
{
|
||||
var pageIndices = ParsePageRange(pageRanges[0]!, mergedProcessor.Document.Pages.Count);
|
||||
// Remove pages not in range (process in reverse to maintain indices)
|
||||
for (int i = mergedProcessor.Document.Pages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!pageIndices.Contains(i))
|
||||
mergedProcessor.Document.Pages.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Append remaining PDFs
|
||||
for (int i = 1; i < pdfStreams.Count; i++)
|
||||
{
|
||||
string? pageRange = pageRanges?[i];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pageRange))
|
||||
{
|
||||
// Append all pages
|
||||
mergedProcessor.AppendDocument(pdfStreams[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse page range and append selected pages
|
||||
// Note: We need to load the document first to validate page range
|
||||
using var tempProcessor = new PdfDocumentProcessor();
|
||||
tempProcessor.LoadDocument(pdfStreams[i]);
|
||||
|
||||
var pageIndices = ParsePageRange(pageRange, tempProcessor.Document.Pages.Count);
|
||||
|
||||
// DevExpress AppendDocument doesn't support arbitrary page selection
|
||||
// Workaround: Create temp PDF with selected pages, then append
|
||||
using var tempStream = new MemoryStream();
|
||||
|
||||
// Remove unwanted pages from temp document (in reverse order)
|
||||
for (int j = tempProcessor.Document.Pages.Count - 1; j >= 0; j--)
|
||||
{
|
||||
if (!pageIndices.Contains(j))
|
||||
tempProcessor.Document.Pages.RemoveAt(j);
|
||||
}
|
||||
|
||||
tempProcessor.SaveDocument(tempStream);
|
||||
tempStream.Position = 0;
|
||||
mergedProcessor.AppendDocument(tempStream);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Save merged PDF to byte array
|
||||
using var outputStream = new MemoryStream();
|
||||
mergedProcessor.SaveDocument(outputStream);
|
||||
|
||||
return await Task.FromResult(outputStream.ToArray());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
@@ -408,5 +500,77 @@ public class DevExpressPdfProcessor : IPdfProcessor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses page range string into list of zero-based page indices.
|
||||
/// </summary>
|
||||
/// <param name="pageRange">Page range string (e.g., "1-3,5" or "1,3,5")</param>
|
||||
/// <param name="totalPages">Total page count in PDF (for validation)</param>
|
||||
/// <returns>List of zero-based page indices</returns>
|
||||
/// <exception cref="BadRequestException">Invalid format or page number out of range</exception>
|
||||
private static List<int> ParsePageRange(string pageRange, int totalPages)
|
||||
{
|
||||
var pageIndices = new HashSet<int>(); // Use HashSet to avoid duplicates
|
||||
|
||||
try
|
||||
{
|
||||
// Split by comma
|
||||
string[] parts = pageRange.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
foreach (string part in parts)
|
||||
{
|
||||
if (part.Contains('-'))
|
||||
{
|
||||
// Range format: "1-3"
|
||||
string[] rangeParts = part.Split('-', StringSplitOptions.TrimEntries);
|
||||
if (rangeParts.Length != 2)
|
||||
throw new BadRequestException($"Invalid page range format: '{part}'. Expected format: '1-3'");
|
||||
|
||||
if (!int.TryParse(rangeParts[0], out int start) || !int.TryParse(rangeParts[1], out int end))
|
||||
throw new BadRequestException($"Invalid page numbers in range: '{part}'");
|
||||
|
||||
if (start < 1 || end < 1)
|
||||
throw new BadRequestException($"Page numbers must be >= 1 in range: '{part}'");
|
||||
|
||||
if (start > end)
|
||||
throw new BadRequestException($"Start page must be <= end page in range: '{part}'");
|
||||
|
||||
if (start > totalPages || end > totalPages)
|
||||
throw new BadRequestException($"Page range '{part}' exceeds document page count ({totalPages})");
|
||||
|
||||
// Add pages (convert to zero-based indices)
|
||||
for (int i = start; i <= end; i++)
|
||||
pageIndices.Add(i - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single page: "5"
|
||||
if (!int.TryParse(part, out int pageNum))
|
||||
throw new BadRequestException($"Invalid page number: '{part}'");
|
||||
|
||||
if (pageNum < 1)
|
||||
throw new BadRequestException($"Page number must be >= 1: '{part}'");
|
||||
|
||||
if (pageNum > totalPages)
|
||||
throw new BadRequestException($"Page number {pageNum} exceeds document page count ({totalPages})");
|
||||
|
||||
pageIndices.Add(pageNum - 1); // Convert to zero-based index
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (BadRequestException)
|
||||
{
|
||||
throw; // Re-throw BadRequestException as-is
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new BadRequestException($"Invalid page range format: '{pageRange}'. Error: {ex.Message}");
|
||||
}
|
||||
|
||||
if (pageIndices.Count == 0)
|
||||
throw new BadRequestException($"Page range '{pageRange}' resulted in no pages");
|
||||
|
||||
return pageIndices.OrderBy(x => x).ToList(); // Return sorted list
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user