Generate PDF's in an ABP Framework Project using Scryber.Core
In this article, we'll create a basic application with blazor-server for UI to demonstrate how generating PDF's can be implemented in an ABP project.
To archieve this, we will use Scryber.Core.
For the sake of simplicity I will reduce everything to the bare minimum just so that you get a picture of how everything works together.
PDF's will be generated server side. This post will show how to download the generated PDF file. If you are using a different UI layer, you just need to implement your download a bit different.
The source code for this can be found here.
Creating the Solution
For this article, we will create a simple application with blazor-server for our UI.
Feel free to use something different. I'll showcase a download of the pdf file
We can create a new startup template with EF Core as a database provider and MVC for the UI Framework.
If you already have a project, you don't need to create a new startup template, you can directly implement the following steps to your project. So you can skip this section.
We can create a new startup template by using the ABP CLI.
abp new BookStorePdf -t app -u blazor-server -d ef
After running the above command, our project boilerplate will be downloaded. Then we can open the solution and start the development.
Starting the Development
First of all we add the initial migration. There are multiple ways doing that. I like to do it like this:
- Set
BookStorePdf.EntityFrameworkCore
as startup project - Open Package Manager Console and ensure Default Project is the same
- Execute
Add-Migration "Initial"
- Execute
BookStorePdf.DbMigrator
We can now start up BookStorePdf.Blazor
and ensure that everything works correctly.
Add Scryber.core
We are now ready to install Scryber.Core.
Add the Scryber.Core
NuGet-Package to BookStorePdf.Application
.
BookStorePdf.Application.csproj
now looks like this:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>BookStorePdf</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BookStorePdf.Domain\BookStorePdf.Domain.csproj" />
<ProjectReference Include="..\BookStorePdf.Application.Contracts\BookStorePdf.Application.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Scryber.Core" Version="5.0.7" />
<PackageReference Include="Volo.Abp.Account.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.Identity.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.PermissionManagement.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.TenantManagement.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.FeatureManagement.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.SettingManagement.Application" Version="5.2.0" />
</ItemGroup>
</Project>
Managing PDF templates
To generate PDF's with scryber, we need to have templates.
For this tutorial we will use html files which we ship along with the application.
You can however write a complete ecosystem around this to be able to have the templates changeable by your users.
Let's implement everything we need to store and serve our html file.
Create IResourceAppService.cs
in the BookStorePdf.Application.Contracts
project with the following content:
using Microsoft.Extensions.FileProviders;
using Volo.Abp.Application.Services;
using Volo.Abp.DependencyInjection;
namespace BookStorePdf;
public interface IResourceAppService : IApplicationService, ITransientDependency
{
IFileInfo GetFileInfo(string path);
}
After that you create ResourceAppService
in the BookStorePdf.Application
project with this content:
using Microsoft.Extensions.FileProviders;
using Volo.Abp.VirtualFileSystem;
namespace BookStorePdf;
public class ResourceAppService : BookStorePdfAppService, IResourceAppService
{
private readonly IVirtualFileProvider _virtualFileProvider;
public ResourceAppService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
public IFileInfo GetFileInfo(string path)
{
return _virtualFileProvider.GetFileInfo(path);
}
}
Change BookStorePdfApplicationModule.cs
like this:
using Volo.Abp.Account;
using Volo.Abp.AutoMapper;
using Volo.Abp.FeatureManagement;
using Volo.Abp.Identity;
using Volo.Abp.Modularity;
using Volo.Abp.PermissionManagement;
using Volo.Abp.SettingManagement;
using Volo.Abp.TenantManagement;
using Volo.Abp.VirtualFileSystem;
namespace BookStorePdf;
[DependsOn(
typeof(BookStorePdfDomainModule),
typeof(AbpAccountApplicationModule),
typeof(BookStorePdfApplicationContractsModule),
typeof(AbpIdentityApplicationModule),
typeof(AbpPermissionManagementApplicationModule),
typeof(AbpTenantManagementApplicationModule),
typeof(AbpFeatureManagementApplicationModule),
typeof(AbpSettingManagementApplicationModule)
)]
public class BookStorePdfApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<BookStorePdfApplicationModule>();
});
// add this here
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<BookStorePdfApplicationModule>();
});
}
}
Change BookStorePdf.Application.csproj
:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>BookStorePdf</RootNamespace>
<!-- add this line -->
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BookStorePdf.Domain\BookStorePdf.Domain.csproj" />
<ProjectReference Include="..\BookStorePdf.Application.Contracts\BookStorePdf.Application.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<!-- add this line -->
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.3" />
<PackageReference Include="Scryber.Core" Version="5.0.7" />
<PackageReference Include="Volo.Abp.Account.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.Identity.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.PermissionManagement.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.TenantManagement.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.FeatureManagement.Application" Version="5.2.0" />
<PackageReference Include="Volo.Abp.SettingManagement.Application" Version="5.2.0" />
</ItemGroup>
<!-- add this -->
<ItemGroup>
<EmbeddedResource Include="MyResources\**\*.*" />
</ItemGroup>
<!-- and this -->
<Choose>
<When Condition="'$(Configuration)'=='Debug'">
<ItemGroup>
<Content Include="MyResources\**\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</When>
<When Condition="'$(Configuration)'!='Debug'">
<ItemGroup>
<Content Remove="MyResources\**\*.*" />
</ItemGroup>
</When>
</Choose>
</Project>
This is needed to setup our file provider. We store our templates in the MyResources
directory. If we debug, we want the templates to be copied to the output directory so that we can change them on runtime and see changes.
The debugging-part is possible because we have the following code in BookStorePdfBlazorModule.cs
:
private void ConfigureVirtualFileSystem(IWebHostEnvironment hostingEnvironment)
{
if (hostingEnvironment.IsDevelopment())
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.ReplaceEmbeddedByPhysical<BookStorePdfDomainSharedModule>(Path.Combine(hostingEnvironment.ContentRootPath, $"..{Path.DirectorySeparatorChar}BookStorePdf.Domain.Shared"));
options.FileSets.ReplaceEmbeddedByPhysical<BookStorePdfDomainModule>(Path.Combine(hostingEnvironment.ContentRootPath, $"..{Path.DirectorySeparatorChar}BookStorePdf.Domain"));
options.FileSets.ReplaceEmbeddedByPhysical<BookStorePdfApplicationContractsModule>(Path.Combine(hostingEnvironment.ContentRootPath, $"..{Path.DirectorySeparatorChar}BookStorePdf.Application.Contracts"));
options.FileSets.ReplaceEmbeddedByPhysical<BookStorePdfApplicationModule>(Path.Combine(hostingEnvironment.ContentRootPath, $"..{Path.DirectorySeparatorChar}BookStorePdf.Application"));
options.FileSets.ReplaceEmbeddedByPhysical<BookStorePdfBlazorModule>(hostingEnvironment.ContentRootPath);
});
}
}
When we deploy the application, we want to have everything stored as an embedded resource.
Now we create a folder MyResources
and create a template file called Test.html
there for our PDF's.
<!DOCTYPE HTML>
<html lang='en' xmlns='http://www.w3.org/1999/xhtml'>
<head>
<title>Hello World</title>
</head>
<body>
<div style='padding:10px'>Hello World from scryber.</div>
</body>
</html>
Create the printing service
Now we start to implement a service which we can consume to generate a pdf.
We will start with something simple.
Create IPrintingAppService.cs
in the BookStorePdf.Application.Contracts
project with the following content:
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.DependencyInjection;
namespace BookStorePdf;
public interface IPrintingAppService : IApplicationService, ITransientDependency
{
Task<byte[]> PrintAsync();
}
After that you create PrintingAppService
in the BookStorePdf.Application
project with this content:
using Scryber.Components;
using System;
using System.IO;
using System.Threading.Tasks;
namespace BookStorePdf;
public class PrintingAppService : BookStorePdfAppService, IPrintingAppService
{
private readonly IResourceAppService _resourceAppService;
public PrintingAppService(IResourceAppService resourceAppService)
{
_resourceAppService = resourceAppService;
}
public async Task<byte[]> PrintAsync()
{
// get desired template from resource service into a stream
var info = _resourceAppService.GetFileInfo("/MyResources/Test.html");
using var stream = info.CreateReadStream();
// use scryber to parse the stream into a document
using var doc = Document.ParseDocument(stream, Scryber.ParseSourceType.Resource);
// create some file to write the pdf to.
// in a real world scenario you would remove the file after you're done
var tmpDir = System.IO.Path.GetTempPath();
var name = Guid.NewGuid().ToString() + ".pdf";
var targetFilename = System.IO.Path.Combine(tmpDir, name);
// let scryber write the pdf file
doc.SaveAsPDF(targetFilename, FileMode.OpenOrCreate);
// read created pdf file into bytes and return to consumer
return await File.ReadAllBytesAsync(targetFilename);
}
}
Download PDF
The last step is getting our UI ready to consume the IPrintingAppService and download our pdf.
Add this to your Blazor project:
export async function downloadFileFromStream(fileName, contentStreamReference) {
const arrayBuffer = await contentStreamReference.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
triggerFileDownload(fileName, url);
URL.revokeObjectURL(url);
}
function triggerFileDownload(fileName, url) {
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
}
Change Index.razor
to this:
@page "/"
@inherits BookStorePdfComponentBase
<Button Color="Color.Primary" Clicked="PrintTestAsync">Test</Button>
Change Index.razor.cs
to this:
using Microsoft.JSInterop;
using System.IO;
using System.Threading.Tasks;
namespace BookStorePdf.Blazor.Pages;
public partial class Index
{
private readonly IJSRuntime _jsRuntime;
private readonly IPrintingAppService _printingAppService;
private IJSObjectReference _jsModule;
public Index(IJSRuntime jsRuntime, IPrintingAppService printingAppService)
{
_jsRuntime = jsRuntime;
_printingAppService = printingAppService;
}
protected override async Task OnInitializedAsync()
{
_jsModule = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./scripts/myscripts.js");
}
private async Task PrintTestAsync()
{
var result = await _printingAppService.PrintAsync();
var fileName = "Test.pdf";
using var fileStream = new MemoryStream(result);
using var streamRef = new DotNetStreamReference(stream: fileStream);
await _jsModule.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
}
}
Result
Further Reading
This tutorial scratched the surface of generating a PDF.
Let me know if you're interested in another part where I show how to pass objects which then get printed.
Comments
Alper Ebiçoğlu 135 weeks ago
Is Scryber.core cross-platform?
Jack Fistelmann 135 weeks ago
Yes.
t.alkathiri@gmail.com 134 weeks ago
Yes please do advanced one Thank you very much.
Jack Fistelmann 133 weeks ago
Sure :) as soon as I find the time I will do part 2.