Merge branch 'fix/safe-pdf-merge'
This commit is contained in:
commit
ecbeaa26b4
5 files changed
+278
-123
No files matched your search
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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<string> _sourceFiles = new List<string>();
|
||||
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<PdfInfo> 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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="../../UtilitatPdf/PdfMergeFiles.cs" Link="PdfMergeFiles.cs" />
|
||||
<Compile Include="../../UtilitatPdf/PdfInfo.cs" Link="PdfInfo.cs" />
|
||||
<PackageReference Include="itext7" Version="7.1.13" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user