diff --git a/UtilitatPdf/MainWindow.xaml.cs b/UtilitatPdf/MainWindow.xaml.cs index 0d3df7b..230468c 100644 --- a/UtilitatPdf/MainWindow.xaml.cs +++ b/UtilitatPdf/MainWindow.xaml.cs @@ -128,48 +128,34 @@ namespace UtilitatPdf if (list.Count == 0) { MessageBox.Show("Per a poder fusionar, has de seleccionar un fitxer com a mínim", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return; } - else + if (string.IsNullOrWhiteSpace(MergeFileOut.Text)) { - string FinalPdfFile = MergeFileOut.Text; - if (string.IsNullOrEmpty(FinalPdfFile)) - { - MessageBox.Show("El Fitxer de destí no pot estar buit.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - else - { - if (Path.GetExtension(FinalPdfFile) != ".pdf") - { - FinalPdfFile += ".pdf"; - } - FileInfo currentFile = new FileInfo(FinalPdfFile); - if (currentFile.Exists) File.Delete(FinalPdfFile); - if(list.Count == 1) - { - File.Copy(list[0].FileName, FinalPdfFile); - } - else - { - try - { - PdfMergeFiles mClas = new PdfMergeFiles(FinalPdfFile); - mClas.AddFiles(list); - mClas.Generate(); - } - catch (IOException ex) - { - MessageBox.Show("S'ha produit un error quan s'intentava escriure el fitxer:\n" + ex.Message,"Error", MessageBoxButton.OK,MessageBoxImage.Error); - } - catch(Exception exc) - { - MessageBox.Show("S'ha produit un error desconegut:\n" + exc.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - } - MergeFileOut.Text = string.Empty; - list.Clear(); - MessageBox.Show("S'han fusionat correctament el fitxers.", "Info", MessageBoxButton.OK, MessageBoxImage.Information); - } + MessageBox.Show("El fitxer de destí no pot estar buit.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return; } + + try + { + string finalPdfFile = MergeFileOut.Text; + if (!string.Equals(Path.GetExtension(finalPdfFile), ".pdf", StringComparison.OrdinalIgnoreCase)) + finalPdfFile += ".pdf"; + + var merger = new PdfMergeFiles(finalPdfFile); + merger.AddFiles(list); + merger.Generate(); + } + catch (Exception ex) + { + MessageBox.Show("No s'han pogut fusionar els fitxers:\n" + ex.Message, + "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + MergeFileOut.Text = string.Empty; + list.Clear(); + MessageBox.Show("S'han fusionat correctament els fitxers.", "Info", MessageBoxButton.OK, MessageBoxImage.Information); } private void RemoveSign_Button_Click(object sender, RoutedEventArgs e) { diff --git a/UtilitatPdf/PdfMergeFiles.cs b/UtilitatPdf/PdfMergeFiles.cs index 145a756..25c8af0 100644 --- a/UtilitatPdf/PdfMergeFiles.cs +++ b/UtilitatPdf/PdfMergeFiles.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using iText.Kernel.Pdf; @@ -8,104 +9,97 @@ namespace UtilitatPdf { public class PdfMergeFiles { - private PdfDocument _pdfDocument = null; - private PdfDocument _pdfDocument2 = null; - private PdfMerger _pdfMerger = null; - - private static string tmpFolder = System.IO.Path.GetTempPath(); - private string _oldtmpFile = string.Empty; - private string _tmpFile = string.Empty; - private string _tmpFile2 = string.Empty; - private bool _mergeOK = false; - private string _FinalPdfFile = string.Empty; + private readonly List _sourceFiles = new List(); + private readonly string _finalPdfFile; + private readonly bool _overwriteExisting; public PdfMergeFiles(string FinalPdfFile, bool DeleteIfExists = true) { - if (string.IsNullOrEmpty(FinalPdfFile)) - { - throw new Exception(); - } - else - { - _FinalPdfFile = FinalPdfFile; - if (DeleteIfExists) - { - if (new FileInfo(_FinalPdfFile).Exists) File.Delete(_FinalPdfFile); - } - } + if (string.IsNullOrWhiteSpace(FinalPdfFile)) + throw new ArgumentException("El fitxer de destí no pot estar buit.", nameof(FinalPdfFile)); + + _finalPdfFile = Path.GetFullPath(FinalPdfFile); + _overwriteExisting = DeleteIfExists; } + public bool AddFiles(ObservableCollection sourceFiles) { - foreach (PdfInfo fitxer in sourceFiles) - { - if (!this.AddFile(fitxer.FileName)) - { - return false; - } - } + if (sourceFiles == null) throw new ArgumentNullException(nameof(sourceFiles)); + foreach (PdfInfo file in sourceFiles) + AddFile(file.FileName); return true; } + public bool AddFile(string PdfFilename) { - checkVars(PdfFilename); + if (string.IsNullOrWhiteSpace(PdfFilename)) + throw new ArgumentException("El fitxer d'origen no pot estar buit.", nameof(PdfFilename)); - if (_mergeOK) - { - Random numr = new Random(); - _oldtmpFile = tmpFolder + numr.Next(1, 99999).ToString() + "tmpfile.pdf"; - - try - { - _pdfDocument = new PdfDocument(new PdfReader(_tmpFile), new PdfWriter(_oldtmpFile), new StampingProperties().UseAppendMode()); - - _pdfDocument2 = new PdfDocument(new PdfReader(_tmpFile2)); - - _pdfMerger = new PdfMerger(_pdfDocument); - _pdfMerger.Merge(_pdfDocument2, 1, _pdfDocument2.GetNumberOfPages()); - - this.ClosePdfDocs(); - } - catch (IOException) - { - throw new IOException(); - } - catch(Exception) - { - throw new Exception(); - } - finally - { - _tmpFile = _oldtmpFile; - _tmpFile2 = string.Empty; - _oldtmpFile = string.Empty; - _mergeOK = false; - } - } + _sourceFiles.Add(Path.GetFullPath(PdfFilename)); return true; } + public void Generate() { - File.Copy(_tmpFile, _FinalPdfFile); - } - private void ClosePdfDocs() - { - _pdfDocument?.Close(); - _pdfDocument2?.Close(); - _pdfMerger?.Close(); - } - private void checkVars(string file) - { - if (string.IsNullOrEmpty(_tmpFile)) - { - _tmpFile = file; - } - else - { - _tmpFile2 = file; - } - - if (!string.IsNullOrEmpty(_tmpFile) && !string.IsNullOrEmpty(_tmpFile2)) _mergeOK = true; - } + if (_sourceFiles.Count == 0) + throw new InvalidOperationException("Has de seleccionar un fitxer com a mínim."); + // Validate all inputs before creating output or replacing an existing file. + foreach (string sourceFile in _sourceFiles) + { + if (string.Equals(sourceFile, _finalPdfFile, StringComparison.OrdinalIgnoreCase)) + throw new IOException("El fitxer de destí no pot ser un dels fitxers d'origen."); + if (!File.Exists(sourceFile)) + throw new FileNotFoundException("No s'ha trobat el fitxer d'origen.", sourceFile); + } + + if (!_overwriteExisting && File.Exists(_finalPdfFile)) + throw new IOException("El fitxer de destí ja existeix."); + + // Keep the temporary file on the destination filesystem for atomic publication. + string temporaryFile = Path.Combine(Path.GetDirectoryName(_finalPdfFile), + ".utilitatpdf-" + Guid.NewGuid().ToString("N") + ".tmp"); + bool temporaryFileCreated = false; + try + { + using (var output = new FileStream(temporaryFile, FileMode.CreateNew, FileAccess.Write)) + { + temporaryFileCreated = true; + if (_sourceFiles.Count == 1) + { + using (var input = File.OpenRead(_sourceFiles[0])) + input.CopyTo(output); + } + else + { + using (var document = new PdfDocument(new PdfWriter(output))) + { + var merger = new PdfMerger(document); + foreach (string sourceFile in _sourceFiles) + { + using (var source = new PdfDocument(new PdfReader(sourceFile))) + merger.Merge(source, 1, source.GetNumberOfPages()); + } + } + } + } + + // Publish only after all PDF documents and streams close successfully. + if (_overwriteExisting && File.Exists(_finalPdfFile)) + File.Replace(temporaryFile, _finalPdfFile, null); + else + File.Move(temporaryFile, _finalPdfFile); + } + finally + { + if (temporaryFileCreated) + { + // Cleanup must not hide the original error or report success as failure. + try { File.Delete(temporaryFile); } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + } + } + } } } diff --git a/tests/PdfMergeRegression/PdfMergeRegression.csproj b/tests/PdfMergeRegression/PdfMergeRegression.csproj new file mode 100644 index 0000000..b592bc1 --- /dev/null +++ b/tests/PdfMergeRegression/PdfMergeRegression.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + + + + + + + diff --git a/tests/PdfMergeRegression/Program.cs b/tests/PdfMergeRegression/Program.cs new file mode 100644 index 0000000..a75178a --- /dev/null +++ b/tests/PdfMergeRegression/Program.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Linq; +using iText.Kernel.Pdf; +using UtilitatPdf; + +internal static class Program +{ + private static string root; + private static int passed; + + private static void Main() + { + root = Path.Combine(Path.GetTempPath(), "utilitatpdf-tests-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + try + { + Run("single input is copied unchanged", () => + { + string input = Pdf("single.pdf", 101); + string output = At("single-out.pdf"); + Merge(output, input); + Equal(File.ReadAllBytes(input), File.ReadAllBytes(output)); + }); + Run("multiple inputs preserve page order", () => + { + string a = Pdf("a.pdf", 101, 102); + string b = Pdf("b.pdf", 201); + string c = Pdf("c.pdf", 301, 302); + byte[] original = File.ReadAllBytes(a); + Merge(At("merged.pdf"), a, b, c); + using (var result = new PdfDocument(new PdfReader(At("merged.pdf")))) + { + int[] widths = { 101, 102, 201, 301, 302 }; + Check(result.GetNumberOfPages() == widths.Length); + for (int i = 0; i < widths.Length; i++) + Check(result.GetPage(i + 1).GetPageSize().GetWidth() == widths[i]); + } + Equal(original, File.ReadAllBytes(a)); + }); + Run("same input and output is rejected", () => + { + string a = Pdf("same.pdf", 100); + byte[] original = File.ReadAllBytes(a); + Fails(() => Merge(a, a)); + Equal(original, File.ReadAllBytes(a)); + }); + Run("normalized source path is protected", () => + { + string a = Pdf("normalized.pdf", 100); + byte[] original = File.ReadAllBytes(a); + Fails(() => Merge(At("./normalized.pdf"), a, At("b.pdf"))); + Equal(original, File.ReadAllBytes(a)); + }); + Run("existing output survives corrupt later input", () => + { + string output = Pdf("existing.pdf", 999); + byte[] original = File.ReadAllBytes(output); + File.WriteAllText(At("broken.pdf"), "not a PDF"); + Fails(() => Merge(output, At("a.pdf"), At("broken.pdf"))); + Equal(original, File.ReadAllBytes(output)); + using (File.Open(At("a.pdf"), FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { } + }); + Run("failed merge creates no final output", () => + { + string output = At("failed.pdf"); + Fails(() => Merge(output, At("a.pdf"), At("broken.pdf"))); + Check(!File.Exists(output)); + }); + Run("missing source preserves existing output", () => + { + byte[] original = File.ReadAllBytes(At("existing.pdf")); + Fails(() => Merge(At("existing.pdf"), At("missing.pdf"))); + Equal(original, File.ReadAllBytes(At("existing.pdf"))); + }); + Run("successful merge replaces existing output", () => + { + Merge(At("existing.pdf"), At("a.pdf"), At("b.pdf")); + using (var result = new PdfDocument(new PdfReader(At("existing.pdf")))) + Check(result.GetNumberOfPages() == 3); + }); + Run("single input replaces existing output", () => + { + Merge(At("existing.pdf"), At("a.pdf")); + Equal(File.ReadAllBytes(At("a.pdf")), File.ReadAllBytes(At("existing.pdf"))); + }); + Run("overwrite disabled preserves destination", () => + { + byte[] original = File.ReadAllBytes(At("existing.pdf")); + var merge = new PdfMergeFiles(At("existing.pdf"), false); + merge.AddFile(At("b.pdf")); + Fails(merge.Generate); + Equal(original, File.ReadAllBytes(At("existing.pdf"))); + }); + Run("empty input list preserves destination", () => + { + byte[] original = File.ReadAllBytes(At("existing.pdf")); + Fails(new PdfMergeFiles(At("existing.pdf")).Generate); + Equal(original, File.ReadAllBytes(At("existing.pdf"))); + }); + Run("publish failure preserves destination directory", () => + { + string output = At("directory.pdf"); + Directory.CreateDirectory(output); + File.WriteAllText(Path.Combine(output, "keep.txt"), "keep"); + Fails(() => Merge(output, At("a.pdf"))); + Check(File.ReadAllText(Path.Combine(output, "keep.txt")) == "keep"); + }); + Console.WriteLine($"{passed} regression cases passed."); + } + finally { Directory.Delete(root, true); } + } + + private static string At(string name) => Path.Combine(root, name); + private static string Pdf(string name, params int[] widths) + { + string path = At(name); + using (var pdf = new PdfDocument(new PdfWriter(path))) + foreach (int width in widths) + pdf.AddNewPage(new iText.Kernel.Geom.PageSize(width, 400)); + return path; + } + private static void Merge(string output, params string[] inputs) + { + var merger = new PdfMergeFiles(output); + foreach (string input in inputs) merger.AddFile(input); + merger.Generate(); + } + private static void Check(bool condition) + { + if (!condition) throw new Exception("Assertion failed."); + } + private static void Equal(byte[] expected, byte[] actual) => Check(expected.SequenceEqual(actual)); + private static void Fails(Action action) + { + try { action(); } + catch (Exception) { return; } + throw new Exception("Expected the operation to fail."); + } + private static void Run(string name, Action action) + { + action(); + Check(Directory.GetFiles(root, ".utilitatpdf-*.tmp").Length == 0); + Console.WriteLine("PASS " + name); + passed++; + } +} diff --git a/tests/PdfMergeRegression/README.md b/tests/PdfMergeRegression/README.md new file mode 100644 index 0000000..1612039 --- /dev/null +++ b/tests/PdfMergeRegression/README.md @@ -0,0 +1,17 @@ +# PDF merge regression checks + +Run with the .NET 8 SDK: + +```sh +dotnet run --project tests/PdfMergeRegression +``` + +This console harness links the production merge class and uses the same iText +7.1.13 dependency, without loading WPF. It checks page order, single-file copies, +source-path protection, failed merges, replacement, and temporary-file cleanup. +It does not change the application's .NET Core 3.1 target. + +Also check the WPF application on Windows: after an invalid or inaccessible PDF, +the error must leave the selection and destination intact, without a success +message; after a successful merge, both fields should clear. Check uppercase +`.PDF` destinations and the single-input case as well.