Created
May 29, 2025 00:10
-
-
Save Hona/530a3261f142859f003d19a7b278a9a4 to your computer and use it in GitHub Desktop.
Fix mildly corrupt PDFs by doing a PDF to IMG to PDF conversion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Diagnostics; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Text; | |
| // Top-level statements for the console application | |
| if (args.Length < 2) | |
| { | |
| Console.Error.WriteLine("Usage: <program> <inputPdfPath> <outputPdfPath>"); | |
| return 1; | |
| } | |
| var inputPath = args[0]; | |
| var outputPath = args[1]; | |
| if (string.IsNullOrWhiteSpace(inputPath) || string.IsNullOrWhiteSpace(outputPath)) | |
| { | |
| Console.Error.WriteLine("Error: Input and output paths must be provided."); | |
| return 1; | |
| } | |
| if (!File.Exists(inputPath)) | |
| { | |
| Console.Error.WriteLine($"Error: Input file not found: {inputPath}"); | |
| return 1; | |
| } | |
| // Optional: Define paths to your CLI tools if not in PATH | |
| string? pdftoppmPath = @"C:\Users\LukeParker\Downloads\Release-24.08.0-0\poppler-24.08.0\Library\bin\pdftoppm.exe"; // Or full path e.g., @"C:\poppler\bin\pdftoppm.exe" | |
| string? img2pdfPath = @"C:\Users\LukeParker\Downloads\img2pdf-v1.8-windows10-bin\img2pdf.exe"; // Or full path e.g., @"C:\Python39\Scripts\img2pdf.exe" | |
| byte[]? outputBytes; | |
| try | |
| { | |
| outputBytes = await PdfCliSanitizer.SanitizePdfViaCliToolsAsync( | |
| inputPath, | |
| pdftoppmPath, | |
| img2pdfPath | |
| ); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.Error.WriteLine($"Sanitization Error with CLI tools: {ex.Message}"); | |
| if (ex.InnerException != null) | |
| { | |
| Console.Error.WriteLine($" Inner Exception: {ex.InnerException.Message}"); | |
| } | |
| Console.Error.WriteLine(ex.ToString()); | |
| return 1; | |
| } | |
| if (outputBytes == null || outputBytes.Length == 0) | |
| { | |
| Console.Error.WriteLine( | |
| "Error: CLI sanitization resulted in null or empty output. No file written." | |
| ); | |
| return 1; | |
| } | |
| try | |
| { | |
| await File.WriteAllBytesAsync(outputPath, outputBytes); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.Error.WriteLine($"Error writing output file {outputPath}: {ex.Message}"); | |
| return 1; | |
| } | |
| Console.WriteLine( | |
| $"CLI tool sanitization complete. Output saved to: {outputPath}" | |
| ); | |
| return 0; | |
| public static class PdfCliSanitizer | |
| { | |
| private static async Task<(int ExitCode, string Output, string Error)> | |
| RunProcessAsync( | |
| string fileName, | |
| string arguments, | |
| string? workingDirectory = null | |
| ) | |
| { | |
| using (var process = new Process()) | |
| { | |
| process.StartInfo = new ProcessStartInfo | |
| { | |
| FileName = fileName, | |
| Arguments = arguments, | |
| RedirectStandardOutput = true, | |
| RedirectStandardError = true, | |
| UseShellExecute = false, | |
| CreateNoWindow = true, | |
| WorkingDirectory = workingDirectory ?? string.Empty | |
| }; | |
| var outputBuilder = new StringBuilder(); | |
| var errorBuilder = new StringBuilder(); | |
| process.OutputDataReceived += (_, e) => | |
| { | |
| if (e.Data != null) | |
| outputBuilder.AppendLine(e.Data); | |
| }; | |
| process.ErrorDataReceived += (_, e) => | |
| { | |
| if (e.Data != null) | |
| errorBuilder.AppendLine(e.Data); | |
| }; | |
| process.Start(); | |
| process.BeginOutputReadLine(); | |
| process.BeginErrorReadLine(); | |
| await process.WaitForExitAsync(); // Use async version | |
| return (process.ExitCode, outputBuilder.ToString(), errorBuilder.ToString()); | |
| } | |
| } | |
| public static async Task<byte[]?> SanitizePdfViaCliToolsAsync( | |
| string inputPdfPath, | |
| string pdftoppmExecutable = "pdftoppm.exe", | |
| string img2pdfExecutable = "img2pdf.exe" | |
| ) | |
| { | |
| string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); | |
| Directory.CreateDirectory(tempDir); | |
| string imageFilePrefix = Path.Combine(tempDir, "page"); | |
| try | |
| { | |
| // Step 1: Convert PDF to images (PNG) using pdftoppm | |
| // -png: output PNG files | |
| // -r 150: set DPI to 150 (adjust as needed for quality/size) | |
| string pdftoppmArgs = | |
| $"-png -r 300 \"{inputPdfPath}\" \"{imageFilePrefix}\""; | |
| var pdftoppmResult = | |
| await RunProcessAsync(pdftoppmExecutable, pdftoppmArgs, tempDir); | |
| if (pdftoppmResult.ExitCode != 0) | |
| { | |
| throw new InvalidOperationException( | |
| $"pdftoppm failed (Exit Code: {pdftoppmResult.ExitCode}). " + | |
| $"Error: {pdftoppmResult.Error}" | |
| ); | |
| } | |
| var imageFiles = Directory.GetFiles(tempDir, "page-*.png") | |
| .OrderBy(f => // Natural sort for page numbers (page-1, page-2, ..., page-10) | |
| int.Parse( | |
| Path.GetFileNameWithoutExtension(f).Substring("page-".Length) | |
| ) | |
| ) | |
| .ToList(); | |
| if (!imageFiles.Any()) | |
| { | |
| throw new InvalidOperationException( | |
| "pdftoppm ran successfully but produced no image files." | |
| ); | |
| } | |
| // Step 2: Convert images back to PDF using img2pdf | |
| // img2pdf is good at preserving image quality without re-encoding if possible. | |
| string tempOutputPdf = Path.Combine(tempDir, "output_from_images.pdf"); | |
| string img2pdfArgs = | |
| $"--output \"{tempOutputPdf}\" {string.Join(" ", imageFiles.Select(f => $"\"{f}\""))}"; | |
| var img2pdfResult = | |
| await RunProcessAsync(img2pdfExecutable, img2pdfArgs, tempDir); | |
| if (img2pdfResult.ExitCode != 0) | |
| { | |
| throw new InvalidOperationException( | |
| $"img2pdf failed (Exit Code: {img2pdfResult.ExitCode}). " + | |
| $"Error: {img2pdfResult.Error}" | |
| ); | |
| } | |
| if (!File.Exists(tempOutputPdf)) | |
| { | |
| throw new InvalidOperationException( | |
| "img2pdf ran successfully but the output PDF was not created." | |
| ); | |
| } | |
| return await File.ReadAllBytesAsync(tempOutputPdf); | |
| } | |
| finally | |
| { | |
| if (Directory.Exists(tempDir)) | |
| { | |
| try | |
| { | |
| Directory.Delete(tempDir, true); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.Error.WriteLine( | |
| $"Warning: Failed to delete temporary directory {tempDir}: {ex.Message}" | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment