Skip to content

Commit

Permalink
Merge pull request #13 from kevbite/issue/12
Browse files Browse the repository at this point in the history
#12: Implement attach files to PDF
  • Loading branch information
kevbite authored Oct 24, 2021
2 parents 81bb1d3 + 6c30f56 commit 7a38be1
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 10 deletions.
13 changes: 11 additions & 2 deletions src/Kevsoft.PDFtk/IPDFtk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public interface IPDFtk
/// </summary>
/// <param name="filePath">The PDF file path.</param>
/// <returns>A result with an enumeration of key value pair where the key is the filename and the value is a byte arrays.</returns>
Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> SplitAsync(string filePath);
Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> SplitAsync(string filePath);

/// <summary>
/// Applies a stamp to a PDF file.
Expand Down Expand Up @@ -93,6 +93,15 @@ Task<IPDFtkResult<byte[]>> FillFormAsync(string pdfFilePath,
/// </summary>
/// <param name="pdfFilePath">A PDF file path input.</param>
/// <returns>A result with the attachments.</returns>
Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> ExtractAttachments(string pdfFilePath);
Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> ExtractAttachments(string pdfFilePath);

/// <summary>
/// Attaches files to a PDF file.
/// </summary>
/// <param name="pdfFilePath">A PDF file path input.</param>
/// <param name="files">Files to attach to the PDF.</param>
/// <param name="page">The page to attach the given files, if null then files are attached to the document level.</param>
/// <returns>A result with the files attached to the PDF.</returns>
Task<IPDFtkResult<byte[]>> AttachFiles(string pdfFilePath, IEnumerable<string> files, int? page = null);
}
}
33 changes: 29 additions & 4 deletions src/Kevsoft.PDFtk/PDFtk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public async Task<IPDFtkResult<byte[]>> ConcatAsync(IEnumerable<string> filePath
}

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

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

private static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>>
private static async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>>
ResolveSingleDirectoryExecutionResultAsync(ExecutionResult executeProcessResult,
TempPDFtkDirectory outputDirectory, string searchPattern)
{
Expand All @@ -212,7 +212,7 @@ private static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>
}
}

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

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

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

return await ResolveSingleDirectoryExecutionResultAsync(executeProcessResult, outputDirectory, "*");
}


/// <inheritdoc/>
public async Task<IPDFtkResult<byte[]>> AttachFiles(string pdfFilePath, IEnumerable<string> files, int? page = null)
{
using var outputFile = TempPDFtkFile.Create();
var args = new List<string>(7)
{
pdfFilePath,
"attach_files"
};
args.AddRange(files);

if (page is { } p)
{
args.Add("to_page");
args.Add(p.ToString());
}

args.Add("output");
args.Add(outputFile.TempFileName);
var executeProcessResult = await _pdftkProcess.ExecuteAsync(args.ToArray());

return await ResolveSingleFileExecutionResultAsync(executeProcessResult, outputFile);
}

private class Range
{
Expand Down
20 changes: 18 additions & 2 deletions src/Kevsoft.PDFtk/PDFtkByteArrayExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public static async Task<IPDFtkResult<byte[]>> ConcatAsync(this IPDFtk pdftk, IE
/// <param name="pdftk">The IPDFtk object.</param>
/// <param name="pdfFile">A byte array of the PDF file input.</param>
/// <returns>A result with an enumeration of key value pair where the key is the filename and the value is a byte arrays.</returns>
public static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> SplitAsync(this IPDFtk pdftk, byte[] pdfFile)
public static async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> SplitAsync(this IPDFtk pdftk, byte[] pdfFile)
{
using var inputFile = await TempPDFtkFile.FromAsync(pdfFile);

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

return await pdftk.ExtractAttachments(inputFile.TempFileName);
}

/// <summary>
/// Attaches files to a PDF file.
/// </summary>
/// <param name="pdftk">The IPDFtk object.</param>
/// <param name="fileBytes">A byte array of the PDF file input.</param>
/// <param name="attachments">Files to attach to the PDF.</param>
/// <param name="page">The page to attach the given files, if null then files are attached to the document level.</param>
/// <returns>A result with the files attached to the PDF.</returns>
public static async Task<IPDFtkResult<byte[]>> AttachFiles(this IPDFtk pdftk, byte[] fileBytes, IEnumerable<KeyValuePair<string, byte[]>> attachments, int? page = null)
{
using var inputFile = await TempPDFtkFile.FromAsync(fileBytes);
using var attachmentFiles = await TempPDFtkFiles.FromAsync(attachments);

return await pdftk.AttachFiles(inputFile.TempFileName, attachmentFiles.FileNames, page);
}
}
}
20 changes: 18 additions & 2 deletions src/Kevsoft.PDFtk/PDFtkStreamExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public static async Task<IPDFtkResult<byte[]>> ConcatAsync(this IPDFtk pdftk, IE
/// <param name="pdftk">The IPDFtk object.</param>
/// <param name="pdfFile">A stream of the PDF file input.</param>
/// <returns>A result with an enumeration of key value pair where the key is the filename and the value is a byte arrays.</returns>
public static async Task<IPDFtkResult<IEnumerable<KeyValuePair<string, byte[]>>>> SplitAsync(this IPDFtk pdftk, Stream pdfFile)
public static async Task<IPDFtkResult<IReadOnlyCollection<KeyValuePair<string, byte[]>>>> SplitAsync(this IPDFtk pdftk, Stream pdfFile)
{
using var inputFile = await TempPDFtkFile.FromAsync(pdfFile);

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

return await pdftk.ExtractAttachments(inputFile.TempFileName);
}

/// <summary>
/// Attaches files to a PDF file.
/// </summary>
/// <param name="pdftk">The IPDFtk object.</param>
/// <param name="pdfFile">A stream of the PDF file input.</param>
/// <param name="attachments">Streams of files to attach to the PDF.</param>
/// <param name="page">The page to attach the given files, if null then files are attached to the document level.</param>
/// <returns>A result with the files attached to the PDF.</returns>
public static async Task<IPDFtkResult<byte[]>> AttachFiles(this IPDFtk pdftk,Stream pdfFile, IEnumerable<KeyValuePair<string, Stream>> attachments, int? page = null)
{
using var inputFile = await TempPDFtkFile.FromAsync(pdfFile);
using var attachmentFiles = await TempPDFtkFiles.FromAsync(attachments);

return await pdftk.AttachFiles(inputFile.TempFileName, attachmentFiles.FileNames, page);
}
}
}
50 changes: 50 additions & 0 deletions src/Kevsoft.PDFtk/TempPDFtkFiles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace Kevsoft.PDFtk
{
internal sealed class TempPDFtkFiles : IDisposable
{
private readonly TempPDFtkDirectory _directory;

private TempPDFtkFiles()
{
_directory = TempPDFtkDirectory.Create();
}

public IEnumerable<string> FileNames => Directory.EnumerateFiles(_directory.TempDirectoryFullName);

public void Dispose()
{
_directory.Dispose();
}

public static async Task<TempPDFtkFiles> FromAsync(IEnumerable<KeyValuePair<string, byte[]>> attachments)
{
var tempPdFtkFiles = new TempPDFtkFiles();
foreach (var (fileName, content) in attachments)
{
await File.WriteAllBytesAsync(Path.Combine(tempPdFtkFiles._directory.TempDirectoryFullName, fileName),
content);
}

return tempPdFtkFiles;
}

public static async Task<TempPDFtkFiles> FromAsync(IEnumerable<KeyValuePair<string, Stream>> attachments)
{
var tempPdFtkFiles = new TempPDFtkFiles();
foreach (var (fileName, stream) in attachments)
{
await using var openWrite =
File.OpenWrite(Path.Combine(tempPdFtkFiles._directory.TempDirectoryFullName, fileName));

await stream.CopyToAsync(openWrite);
}

return tempPdFtkFiles;
}
}
}
136 changes: 136 additions & 0 deletions test/Kevsoft.PDFtk.Tests/AttachFilesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;

namespace Kevsoft.PDFtk.Tests
{
public class AttachFilesTests : IAsyncLifetime
{
private readonly PDFtk _pdFtk = new();

private readonly IReadOnlyDictionary<string, string>
_attachments = new Dictionary<string, string>
{
[Path.GetTempFileName()] = "Hello World",
[Path.GetTempFileName()] =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque aliquet sagittis felis eget pharetra.",
};

public async Task InitializeAsync()
{
foreach (var (fileName, content) in _attachments)
{
await File.WriteAllTextAsync(fileName, content);
}
}

[Fact]
public async Task ShouldReturnPdfWithAttachments()
{
var result =
await _pdFtk.AttachFiles(TestFiles.TestFileWith3PagesPath, _attachments.Keys);

result.Success.Should().BeTrue();
result.Result.Should().NotBeEmpty();

await AssertPdfFileAttachments(result, _attachments);
}


[Fact]
public async Task ShouldReturnPdfWithAttachments_ForInputFilesAsBytes()
{
var input = await File.ReadAllBytesAsync(TestFiles.TestFileWith3PagesPath);
var attachments = new Dictionary<string, byte[]>
{
["test-file1.txt"] = Encoding.ASCII.GetBytes("Hello"),
["test-file2.txt"] = Encoding.ASCII.GetBytes("World")
};

var result = await _pdFtk.AttachFiles(input, attachments);

result.Success.Should().BeTrue();
result.Result.Should().NotBeEmpty();

var extractAttachments = await _pdFtk.ExtractAttachments(result.Result);
extractAttachments.Result.Count.Should().Be(attachments.Count);

extractAttachments.Result.Should().BeEquivalentTo(attachments);
}


[Fact]
public async Task ShouldReturnPdfWithAttachments_ForInputFilesAsStreams()
{
await using var input = File.OpenRead(TestFiles.TestFileWith3PagesPath);
var attachments = new Dictionary<string, byte[]>
{
["test-file1.txt"] = Encoding.ASCII.GetBytes("Hello"),
["test-file2.txt"] = Encoding.ASCII.GetBytes("World")
};

var result = await _pdFtk.AttachFiles(input, attachments
.Select(kvp => KeyValuePair.Create<string, Stream>(kvp.Key, new MemoryStream(kvp.Value))));

result.Success.Should().BeTrue();
result.Result.Should().NotBeEmpty();

var extractAttachments = await _pdFtk.ExtractAttachments(result.Result);
extractAttachments.Result.Count.Should().Be(attachments.Count);

extractAttachments.Result.Should().BeEquivalentTo(attachments);
}

[Fact]
public async Task ShouldReturnPdfWithAttachments_ForGivenPage()
{
var result = await _pdFtk.AttachFiles(TestFiles.TestFileWith3PagesPath,
_attachments.Keys, 2);

result.Success.Should().BeTrue();
result.Result.Should().NotBeEmpty();

var page2Result = await _pdFtk.GetPagesAsync(result.Result, 2);
await AssertPdfFileAttachments(page2Result, _attachments);
}

[Fact]
public async Task ShouldReturnPdfWithNoAttachments_ForGivenPage()
{
var result = await _pdFtk.AttachFiles(TestFiles.TestFileWith3PagesPath,
_attachments.Keys, 1);

result.Success.Should().BeTrue();
result.Result.Should().NotBeEmpty();

var page2Result = await _pdFtk.GetPagesAsync(result.Result, 2);
await AssertPdfFileAttachments(page2Result, new Dictionary<string, string>());
}

private async Task AssertPdfFileAttachments(IPDFtkResult<byte[]> result,
IReadOnlyDictionary<string, string> attachments)
{
var extractAttachments = await _pdFtk.ExtractAttachments(result.Result);
extractAttachments.Result.Count.Should().Be(attachments.Count);

extractAttachments.Result.Should().BeEquivalentTo(
attachments.ToDictionary(kvp => Path.GetFileName(kvp.Key),
kvp => Encoding.ASCII.GetBytes(kvp.Value)
));
}

public Task DisposeAsync()
{
foreach (var (fileName, _) in _attachments)
{
File.Delete(fileName);
}

return Task.CompletedTask;
}
}
}

0 comments on commit 7a38be1

Please sign in to comment.