Skip to content

Commit 7a38be1

Browse files
authored
Merge pull request #13 from kevbite/issue/12
#12: Implement attach files to PDF
2 parents 81bb1d3 + 6c30f56 commit 7a38be1

File tree

6 files changed

+262
-10
lines changed

6 files changed

+262
-10
lines changed

src/Kevsoft.PDFtk/IPDFtk.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public interface IPDFtk
4545
/// </summary>
4646
/// <param name="filePath">The PDF file path.</param>
4747
/// <returns>A result with an enumeration of key value pair where the key is the filename and the value is a byte arrays.</returns>
48-
Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> SplitAsync(string filePath);
48+
Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> SplitAsync(string filePath);
4949

5050
/// <summary>
5151
/// Applies a stamp to a PDF file.
@@ -93,6 +93,15 @@ Task<IPDFtkResult<byte[]>> FillFormAsync(string pdfFilePath,
9393
/// </summary>
9494
/// <param name="pdfFilePath">A PDF file path input.</param>
9595
/// <returns>A result with the attachments.</returns>
96-
Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> ExtractAttachments(string pdfFilePath);
96+
Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> ExtractAttachments(string pdfFilePath);
97+
98+
/// <summary>
99+
/// Attaches files to a PDF file.
100+
/// </summary>
101+
/// <param name="pdfFilePath">A PDF file path input.</param>
102+
/// <param name="files">Files to attach to the PDF.</param>
103+
/// <param name="page">The page to attach the given files, if null then files are attached to the document level.</param>
104+
/// <returns>A result with the files attached to the PDF.</returns>
105+
Task<IPDFtkResult<byte[]>> AttachFiles(string pdfFilePath, IEnumerable<string> files, int? page = null);
97106
}
98107
}

src/Kevsoft.PDFtk/PDFtk.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public async Task<IPDFtkResult<byte[]>> ConcatAsync(IEnumerable<string> filePath
126126
}
127127

128128
/// <inheritdoc/>
129-
public async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> SplitAsync(string filePath)
129+
public async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> SplitAsync(string filePath)
130130
{
131131
using var outputDirectory = TempPDFtkDirectory.Create();
132132

@@ -196,7 +196,7 @@ private static async Task<IPDFtkResult<byte[]>> ResolveSingleFileExecutionResult
196196
return new PDFtkResult<byte[]>(executeProcessResult, bytes);
197197
}
198198

199-
private static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>>
199+
private static async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>>
200200
ResolveSingleDirectoryExecutionResultAsync(ExecutionResult executeProcessResult,
201201
TempPDFtkDirectory outputDirectory, string searchPattern)
202202
{
@@ -212,7 +212,7 @@ private static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>
212212
}
213213
}
214214

215-
return new PDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>(executeProcessResult, outputFileBytes);
215+
return new PDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>(executeProcessResult, outputFileBytes.AsReadOnly());
216216
}
217217

218218
/// <inheritdoc/>
@@ -258,7 +258,7 @@ public async Task<IPDFtkResult<byte[]>> ReplacePages(string pdfFilePath, int sta
258258
}
259259

260260
/// <inheritdoc/>
261-
public async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> ExtractAttachments(
261+
public async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> ExtractAttachments(
262262
string pdfFilePath)
263263
{
264264
using var outputDirectory = TempPDFtkDirectory.Create();
@@ -272,6 +272,31 @@ public async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> Extra
272272

273273
return await ResolveSingleDirectoryExecutionResultAsync(executeProcessResult, outputDirectory, "*");
274274
}
275+
276+
277+
/// <inheritdoc/>
278+
public async Task<IPDFtkResult<byte[]>> AttachFiles(string pdfFilePath, IEnumerable<string> files, int? page = null)
279+
{
280+
using var outputFile = TempPDFtkFile.Create();
281+
var args = new List<string>(7)
282+
{
283+
pdfFilePath,
284+
"attach_files"
285+
};
286+
args.AddRange(files);
287+
288+
if (page is { } p)
289+
{
290+
args.Add("to_page");
291+
args.Add(p.ToString());
292+
}
293+
294+
args.Add("output");
295+
args.Add(outputFile.TempFileName);
296+
var executeProcessResult = await _pdftkProcess.ExecuteAsync(args.ToArray());
297+
298+
return await ResolveSingleFileExecutionResultAsync(executeProcessResult, outputFile);
299+
}
275300

276301
private class Range
277302
{

src/Kevsoft.PDFtk/PDFtkByteArrayExtensions.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public static async Task<IPDFtkResult<byte[]>> ConcatAsync(this IPDFtk pdftk, IE
8181
/// <param name="pdftk">The IPDFtk object.</param>
8282
/// <param name="pdfFile">A byte array of the PDF file input.</param>
8383
/// <returns>A result with an enumeration of key value pair where the key is the filename and the value is a byte arrays.</returns>
84-
public static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> SplitAsync(this IPDFtk pdftk, byte[] pdfFile)
84+
public static async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> SplitAsync(this IPDFtk pdftk, byte[] pdfFile)
8585
{
8686
using var inputFile = await TempPDFtkFile.FromAsync(pdfFile);
8787

@@ -166,11 +166,27 @@ public static async Task<IPDFtkResult<byte[]>> ReplacePages(this IPDFtk pdftk, b
166166
/// <param name="pdftk">The IPDFtk object.</param>
167167
/// <param name="fileBytes">A byte array of the PDF file input.</param>
168168
/// <returns>A result with the attachments.</returns>
169-
public static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> ExtractAttachments(this IPDFtk pdftk, byte[] fileBytes)
169+
public static async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> ExtractAttachments(this IPDFtk pdftk, byte[] fileBytes)
170170
{
171171
using var inputFile = await TempPDFtkFile.FromAsync(fileBytes);
172172

173173
return await pdftk.ExtractAttachments(inputFile.TempFileName);
174174
}
175+
176+
/// <summary>
177+
/// Attaches files to a PDF file.
178+
/// </summary>
179+
/// <param name="pdftk">The IPDFtk object.</param>
180+
/// <param name="fileBytes">A byte array of the PDF file input.</param>
181+
/// <param name="attachments">Files to attach to the PDF.</param>
182+
/// <param name="page">The page to attach the given files, if null then files are attached to the document level.</param>
183+
/// <returns>A result with the files attached to the PDF.</returns>
184+
public static async Task<IPDFtkResult<byte[]>> AttachFiles(this IPDFtk pdftk, byte[] fileBytes, IEnumerable<KeyValuePair<string, byte[]>> attachments, int? page = null)
185+
{
186+
using var inputFile = await TempPDFtkFile.FromAsync(fileBytes);
187+
using var attachmentFiles = await TempPDFtkFiles.FromAsync(attachments);
188+
189+
return await pdftk.AttachFiles(inputFile.TempFileName, attachmentFiles.FileNames, page);
190+
}
175191
}
176192
}

src/Kevsoft.PDFtk/PDFtkStreamExtensions.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public static async Task<IPDFtkResult<byte[]>> ConcatAsync(this IPDFtk pdftk, IE
7979
/// <param name="pdftk">The IPDFtk object.</param>
8080
/// <param name="pdfFile">A stream of the PDF file input.</param>
8181
/// <returns>A result with an enumeration of key value pair where the key is the filename and the value is a byte arrays.</returns>
82-
public static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> SplitAsync(this IPDFtk pdftk, Stream pdfFile)
82+
public static async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> SplitAsync(this IPDFtk pdftk, Stream pdfFile)
8383
{
8484
using var inputFile = await TempPDFtkFile.FromAsync(pdfFile);
8585

@@ -162,11 +162,27 @@ public static async Task<IPDFtkResult<byte[]>> ReplacePage(this IPDFtk pdftk, St
162162
/// <param name="pdftk">The IPDFtk object.</param>
163163
/// <param name="pdfFile">A stream of the PDF file input.</param>
164164
/// <returns>A result with the attachments.</returns>
165-
public static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> ExtractAttachments(this IPDFtk pdftk, Stream pdfFile)
165+
public static async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> ExtractAttachments(this IPDFtk pdftk, Stream pdfFile)
166166
{
167167
using var inputFile = await TempPDFtkFile.FromAsync(pdfFile);
168168

169169
return await pdftk.ExtractAttachments(inputFile.TempFileName);
170170
}
171+
172+
/// <summary>
173+
/// Attaches files to a PDF file.
174+
/// </summary>
175+
/// <param name="pdftk">The IPDFtk object.</param>
176+
/// <param name="pdfFile">A stream of the PDF file input.</param>
177+
/// <param name="attachments">Streams of files to attach to the PDF.</param>
178+
/// <param name="page">The page to attach the given files, if null then files are attached to the document level.</param>
179+
/// <returns>A result with the files attached to the PDF.</returns>
180+
public static async Task<IPDFtkResult<byte[]>> AttachFiles(this IPDFtk pdftk,Stream pdfFile, IEnumerable<KeyValuePair<string, Stream>> attachments, int? page = null)
181+
{
182+
using var inputFile = await TempPDFtkFile.FromAsync(pdfFile);
183+
using var attachmentFiles = await TempPDFtkFiles.FromAsync(attachments);
184+
185+
return await pdftk.AttachFiles(inputFile.TempFileName, attachmentFiles.FileNames, page);
186+
}
171187
}
172188
}

src/Kevsoft.PDFtk/TempPDFtkFiles.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Threading.Tasks;
5+
6+
namespace Kevsoft.PDFtk
7+
{
8+
internal sealed class TempPDFtkFiles : IDisposable
9+
{
10+
private readonly TempPDFtkDirectory _directory;
11+
12+
private TempPDFtkFiles()
13+
{
14+
_directory = TempPDFtkDirectory.Create();
15+
}
16+
17+
public IEnumerable<string> FileNames => Directory.EnumerateFiles(_directory.TempDirectoryFullName);
18+
19+
public void Dispose()
20+
{
21+
_directory.Dispose();
22+
}
23+
24+
public static async Task<TempPDFtkFiles> FromAsync(IEnumerable<KeyValuePair<string, byte[]>> attachments)
25+
{
26+
var tempPdFtkFiles = new TempPDFtkFiles();
27+
foreach (var (fileName, content) in attachments)
28+
{
29+
await File.WriteAllBytesAsync(Path.Combine(tempPdFtkFiles._directory.TempDirectoryFullName, fileName),
30+
content);
31+
}
32+
33+
return tempPdFtkFiles;
34+
}
35+
36+
public static async Task<TempPDFtkFiles> FromAsync(IEnumerable<KeyValuePair<string, Stream>> attachments)
37+
{
38+
var tempPdFtkFiles = new TempPDFtkFiles();
39+
foreach (var (fileName, stream) in attachments)
40+
{
41+
await using var openWrite =
42+
File.OpenWrite(Path.Combine(tempPdFtkFiles._directory.TempDirectoryFullName, fileName));
43+
44+
await stream.CopyToAsync(openWrite);
45+
}
46+
47+
return tempPdFtkFiles;
48+
}
49+
}
50+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using FluentAssertions;
7+
using Xunit;
8+
9+
namespace Kevsoft.PDFtk.Tests
10+
{
11+
public class AttachFilesTests : IAsyncLifetime
12+
{
13+
private readonly PDFtk _pdFtk = new();
14+
15+
private readonly IReadOnlyDictionary<string, string>
16+
_attachments = new Dictionary<string, string>
17+
{
18+
[Path.GetTempFileName()] = "Hello World",
19+
[Path.GetTempFileName()] =
20+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque aliquet sagittis felis eget pharetra.",
21+
};
22+
23+
public async Task InitializeAsync()
24+
{
25+
foreach (var (fileName, content) in _attachments)
26+
{
27+
await File.WriteAllTextAsync(fileName, content);
28+
}
29+
}
30+
31+
[Fact]
32+
public async Task ShouldReturnPdfWithAttachments()
33+
{
34+
var result =
35+
await _pdFtk.AttachFiles(TestFiles.TestFileWith3PagesPath, _attachments.Keys);
36+
37+
result.Success.Should().BeTrue();
38+
result.Result.Should().NotBeEmpty();
39+
40+
await AssertPdfFileAttachments(result, _attachments);
41+
}
42+
43+
44+
[Fact]
45+
public async Task ShouldReturnPdfWithAttachments_ForInputFilesAsBytes()
46+
{
47+
var input = await File.ReadAllBytesAsync(TestFiles.TestFileWith3PagesPath);
48+
var attachments = new Dictionary<string, byte[]>
49+
{
50+
["test-file1.txt"] = Encoding.ASCII.GetBytes("Hello"),
51+
["test-file2.txt"] = Encoding.ASCII.GetBytes("World")
52+
};
53+
54+
var result = await _pdFtk.AttachFiles(input, attachments);
55+
56+
result.Success.Should().BeTrue();
57+
result.Result.Should().NotBeEmpty();
58+
59+
var extractAttachments = await _pdFtk.ExtractAttachments(result.Result);
60+
extractAttachments.Result.Count.Should().Be(attachments.Count);
61+
62+
extractAttachments.Result.Should().BeEquivalentTo(attachments);
63+
}
64+
65+
66+
[Fact]
67+
public async Task ShouldReturnPdfWithAttachments_ForInputFilesAsStreams()
68+
{
69+
await using var input = File.OpenRead(TestFiles.TestFileWith3PagesPath);
70+
var attachments = new Dictionary<string, byte[]>
71+
{
72+
["test-file1.txt"] = Encoding.ASCII.GetBytes("Hello"),
73+
["test-file2.txt"] = Encoding.ASCII.GetBytes("World")
74+
};
75+
76+
var result = await _pdFtk.AttachFiles(input, attachments
77+
.Select(kvp => KeyValuePair.Create<string, Stream>(kvp.Key, new MemoryStream(kvp.Value))));
78+
79+
result.Success.Should().BeTrue();
80+
result.Result.Should().NotBeEmpty();
81+
82+
var extractAttachments = await _pdFtk.ExtractAttachments(result.Result);
83+
extractAttachments.Result.Count.Should().Be(attachments.Count);
84+
85+
extractAttachments.Result.Should().BeEquivalentTo(attachments);
86+
}
87+
88+
[Fact]
89+
public async Task ShouldReturnPdfWithAttachments_ForGivenPage()
90+
{
91+
var result = await _pdFtk.AttachFiles(TestFiles.TestFileWith3PagesPath,
92+
_attachments.Keys, 2);
93+
94+
result.Success.Should().BeTrue();
95+
result.Result.Should().NotBeEmpty();
96+
97+
var page2Result = await _pdFtk.GetPagesAsync(result.Result, 2);
98+
await AssertPdfFileAttachments(page2Result, _attachments);
99+
}
100+
101+
[Fact]
102+
public async Task ShouldReturnPdfWithNoAttachments_ForGivenPage()
103+
{
104+
var result = await _pdFtk.AttachFiles(TestFiles.TestFileWith3PagesPath,
105+
_attachments.Keys, 1);
106+
107+
result.Success.Should().BeTrue();
108+
result.Result.Should().NotBeEmpty();
109+
110+
var page2Result = await _pdFtk.GetPagesAsync(result.Result, 2);
111+
await AssertPdfFileAttachments(page2Result, new Dictionary<string, string>());
112+
}
113+
114+
private async Task AssertPdfFileAttachments(IPDFtkResult<byte[]> result,
115+
IReadOnlyDictionary<string, string> attachments)
116+
{
117+
var extractAttachments = await _pdFtk.ExtractAttachments(result.Result);
118+
extractAttachments.Result.Count.Should().Be(attachments.Count);
119+
120+
extractAttachments.Result.Should().BeEquivalentTo(
121+
attachments.ToDictionary(kvp => Path.GetFileName(kvp.Key),
122+
kvp => Encoding.ASCII.GetBytes(kvp.Value)
123+
));
124+
}
125+
126+
public Task DisposeAsync()
127+
{
128+
foreach (var (fileName, _) in _attachments)
129+
{
130+
File.Delete(fileName);
131+
}
132+
133+
return Task.CompletedTask;
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)