DinkToPdf in Docker container - dotnet core 2.2
I was busy with a project and I was required to create a pdf “report” in the project and then serve it to a user through the Angular app. So as with all things I created a POC.
Once I got the POC working then I decided to get ready to deploy so naturally I put it into a container. Thats when things got interesting, after googling around, I found a solution and here is what I came up with:
Prep
- Create the development space.
mkdir PdfDemoSpace
cd PdfDemoSpace
Dotnet
- Create the dotnet webapi project
dotnet new webapi --name PdfDemo
Dependencies Management
- Add Dependencies for production
Copy the lib folder found the git repository:
DinkToPdf-DotnetCore-Docker
- Add Dependencies for development
cd PdfDemo
dotnet add package DinkToPdf
dotnet restore
Coding
- Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSingleton(typeof(IConverter), new SynchronizedConverter(new PdfTools()));
}
- Adding the dll’s required by the package into the output folder:
<ItemGroup Condition=" '$(CONFIGURATION)' == 'Debug' ">
<ContentWithTargetPath Include="..\lib\64bit\libwkhtmltox.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libwkhtmltox.dll</TargetPath>
</ContentWithTargetPath>
</ItemGroup>
<ItemGroup Condition=" '$(CONFIGURATION)' == 'Release' ">
<ContentWithTargetPath Include="..\lib\64bit\libwkhtmltox.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libwkhtmltox.dll</TargetPath>
</ContentWithTargetPath>
</ItemGroup>
- Inject the IConverter into the controller
private IConverter _converter;
public ValuesController(IConverter converter)
{
_converter = converter;
}
- Update the get action method to work with the directory and do some demo work:
[HttpGet]
public async Task<IActionResult> Get()
{
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "reports")))
Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "reports"));
if(!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "reports", "instanceId")))
Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "reports", "instanceId"));
var globalSettings = new GlobalSettings
{
ColorMode = ColorMode.Color,
Orientation = Orientation.Portrait,
PaperSize = PaperKind.A4,
Margins = new MarginSettings { Top = 10 },
DocumentTitle = "PDF Report",
Out = $@"{Path.Combine(Directory.GetCurrentDirectory(), "reports","instanceId")}/Employee_Report.{Guid.NewGuid()}.pdf"
};
var objectSettings = new ObjectSettings
{
PagesCount = true,
HtmlContent = TemplateGenerator.GetHTMLString(),
WebSettings = { DefaultEncoding = "utf-8", UserStyleSheet = Path.Combine(Directory.GetCurrentDirectory(), "assets", "styles.css") },
HeaderSettings = { FontName = "Arial", FontSize = 9, Right = "Page [page] of [toPage]", Line = true },
FooterSettings = { FontName = "Arial", FontSize = 9, Line = true, Center = "Report Footer" }
};
var pdf = new HtmlToPdfDocument()
{
GlobalSettings = globalSettings,
Objects = { objectSettings }
};
_converter.Convert(pdf);
var memory = new MemoryStream();
using (var stream = new FileStream(globalSettings.Out, FileMode.Open))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
return File(memory, GetContentType(globalSettings.Out), Path.GetFileName(globalSettings.Out));
}
private string GetContentType(string path)
{
var types = GetMimeTypes();
var ext = Path.GetExtension(path).ToLowerInvariant();
return types[ext];
}
private Dictionary<string, string> GetMimeTypes()
{
return new Dictionary<string, string>
{
{".txt", "text/plain"},
{".pdf", "application/pdf"},
{".doc", "application/vnd.ms-word"},
{".docx", "application/vnd.ms-word"},
{".xls", "application/vnd.ms-excel"},
{".xlsx", "application/vnd.openxmlformatsofficedocument.spreadsheetml.sheet"},
{".png", "image/png"},
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".gif", "image/gif"},
{".csv", "text/csv"}
};
}
Some Demo data:
public class DataStorage
{
public List<Employee> GetAllEmployess()
{
return new List<Employee>
{
new Employee { Name="Mike", LastName="Turner", Age=35, Gender="Male"},
new Employee { Name="Sonja", LastName="Markus", Age=22, Gender="Female"},
new Employee { Name="Luck", LastName="Martins", Age=40, Gender="Male"},
new Employee { Name="Sofia", LastName="Packner", Age=30, Gender="Female"},
new Employee { Name="John", LastName="Doe", Age=45, Gender="Male"}
};
}
}
A Demo object to work with:
public class Employee
{
public string Name { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Gender { get; set; }
}
Create a template generator class
public class TemplateGenerator
{
public static string GetHTMLString()
{
var employees = new DataStorage().GetAllEmployess();
var sb = new StringBuilder();
sb.Append(@"
<html>
<head>
</head>
<body>
<div class='header'><h1>This is the generated PDF report!!!</h1></div>
<table align='center'>
<tr>
<th>Name</th>
<th>LastName</th>
<th>Age</th>
<th>Gender</th>
</tr>");
foreach (var emp in employees)
{
sb.AppendFormat(@"<tr>
<td>{0}</td>
<td>{1}</td>
<td>{2}</td>
<td>{3}</td>
</tr>", emp.Name, emp.LastName, emp.Age, emp.Gender);
}
sb.Append(@"
</table>
</body>
</html>");
return sb.ToString();
}
}
Docker
- The Dockerfile
FROM microsoft/dotnet:2.2-sdk AS build-env
WORKDIR /app
COPY . ./
RUN dotnet publish PdfDemo.sln -c Release -o out
FROM microsoft/dotnet:2.2-aspnetcore-runtime
RUN apt update
RUN apt install -y libgdiplus
RUN ln -s /usr/lib/libgdiplus.so /lib/x86_64-linux-gnu/libgdiplus.so
RUN apt-get install -y --no-install-recommends zlib1g fontconfig libfreetype6 libx11-6 libxext6 libxrender1 wget gdebi
RUN wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.stretch_amd64.deb
RUN gdebi --n wkhtmltox_0.12.5-1.stretch_amd64.deb
RUN apt install libssl1.0-dev
RUN ln -s /usr/local/lib/libwkhtmltox.so /usr/lib/libwkhtmltox.so
WORKDIR /app
COPY --from=build-env /app/PdfDemo/out .
COPY lib/64bit .
ENTRYPOINT ["dotnet", "PdfDemo.dll"]
Conclusion
This is not a optimised solution but highlights the dependencies required and a basic workflow in getting the items working together.