QuestPDF powered by custom Skia build #622
Replies: 52 comments 164 replies
-
It's been a rewarding three months since we initiated the GitHub discussion on creating a custom Skia build specifically for QuestPDF usage. I'm excited to share the progress with you. Building the Skia library, a substantial part of the Chromium project, has been quite an adventure. It's a complex process, and customizing the build for specific features/modules to keep the native library file size down was challenging. But, I've managed to build Skia locally, use it in testing as a vcpkg package, and get it built through GitHub Actions. I am still working on building for different OS (Windows, Linux, MacOS) and CPU architectures (x86, ARM), but I'm confident we'll get there. In terms of .NET runtime integration, I chose a direct approach: manually creating and maintaining a C language interface (CLI). This method, while more labor-intensive than SkiaSharp's automated code generation, is better suited for QuestPDF's limited use of Skia features, offering greater flexibility. I'm pleased to report that the majority of the API has been successfully ported. One of the primary advantages of a custom Skia build, as discussed in the original post, is the built-in font subsetting engine. This feature significantly reduces the size of embedded font assets, thereby decreasing overall PDF file sizes. I can confirm that this functionality is performing as expected, addressing one of QuestPDF's key problems. The initial iteration of the custom Skia build will likely incorporate the SkParagraph module, which promises to enhance QuestPDF's text-handling capabilities. Despite the equal complexity of SkParagraph and SkText + Harfbuzz, opting for the more mature technology seems like a wise decision. Additional features, which are possible thanks to the custom Skia build, such as PDF tags and native SVG support, will be investigated and introduced in subsequent releases. These are not critical at the moment, and delaying their implementation allows us to focus on more immediate priorities. Given the learning curve associated with this effort, I anticipate that integrating these features in the future will be more straightforward. I want to emphasize that this effort has the highest priority. I'm targeting an alpha release in January, with a production version will follow shortly thereafter. Thank you for your continued trust and patience. We're on track for some exciting advancements in QuestPDF! |
Beta Was this translation helpful? Give feedback.
-
非常棒,祝你顺利,早日成功。期待早一天可用上它。 |
Beta Was this translation helpful? Give feedback.
-
Is there any update? how soon it could be available? |
Beta Was this translation helpful? Give feedback.
-
After a month of hard work, I am excited to present the latest developments related to this effort. This update brings a mix of promising advancements and a few challenges. Let’s dive in. Firstly, the process of building Skia is a nightmare. We often overlook the convenience provided by technologies like NuGet and npm platforms. A significant factor contributing to this difficulty is the vast scope and complexity of Skia. After some partially successful attempts at building Skia on GitHub Actions, I opted for VCPKG packages, a creation of Microsoft that offers a more streamlined approach akin to NuGet. This shift has been essential for making substantial progress. I have developed a project similar to a simplified version of the SkiaSharp library - combining native C wrapping functions and corresponding C# layer encapsulating all p/invoke calls. Following my previous update, I chose to integrate the SkParagraph module, thereby incorporating all its text-related enhancements into the upcoming release. I am pleased to report that the following features seem to work as expected: font subsetting, text shaping, bi-directional text, font fallback, custom font registration, text justification, and more. This work is being carried out in a dedicated C# repository to streamline the workflow and testing process. The forthcoming phase involves integrating this code into the QuestPDF project and replacing all SkiaSharp invocations. Adapting the new text-related APIs will require additional work, but the benefits justify the effort. In terms of distribution, the native code occupies approximately 40 MB per platform. I aim to support windows-x64, linux-x64, macos-x64, and macos-arm64, which equates to around 65 MB after ZIP compression. Notably, the ICU library (required for text-related functionalities based on Unicode standards) accounts for about 75% of this size. Would you favor multiple smaller packages tailored to specific runtimes or a single larger package for a more streamlined experience? A significant problem has been found related to the windows-x64/shared-library VCPKG build, which currently does not support the ICU library. This limitation impacts advanced text features like bi-directionality and sophisticated line breaks. Such limitation is unacceptable given the importance of Windows in C# development and deployments. I plan to revisit building Skia myself, potentially utilizing alternative dependencies like the newly released ICU4X, which is a comprehensive rewrite of the ICU library. Although early alpha releases might rely on the VCPKG package, full compatibility with the latest ICU version is the goal. Rest assured, I am dedicating every day to this project. The code is progressively reaching a production-ready state, and I anticipate making it publicly available soon. Thank you for your continued support! |
Beta Was this translation helpful? Give feedback.
-
Recent activitiesI am excited to share with you the enormous progress I have made over the last two weeks. The primary priority was integrating the new QuestPDF-specific Skia wrapper with the library. This effort is not only about replacing existing APIs but also involves completely redesigning the text rendering solution. The code complexity has been greatly reduced while new features have been introduced. This is awesome! I consider this effort as completed. This effort is developed in this branch. Additionally, this week, I have been investigating the building process of the native dependency. VCPKG does not support the ICU library when building a shared library on Windows. Consequently, the SkParagraph module is unavailable on such a platform, making the VCPKG approach useless. However, I have successfully built the Skia library on MacOS with all the necessary features and flags. Then, I added the compiled static library to the QuestPDF native layer. As a result, I successfully went through the entire process on MacOS. I have also adjusted the build flags so Skia builds successfully on all platforms (Windows, MacOS, Linux) on GitHub Actions. Current objectiveThe following primary goal is to adjust the configuration for all platforms and test whether the compiled native dependency works as expected. This is an ongoing process, and I am optimistic about its progress so far! Building Skia manually has also allowed me to minimize the native library size from 40MB to around 16MB per platform, where 8MB corresponds to ICU. I still need to confirm if the smaller-size artifacts are indeed fully featured. Completed tasksI understand this effort has been active for many months, and it may be unclear where exactly things are. So, let me present a short list that hopefully shows that we are very close. Things already completed:
Next tasks
Release roadmap
As mentioned before, this effort is enormous. I dare say that the most challenging part is already completed. We are slowly starting tasks where the expertise is much higher and the associated risk is limited. It is difficult to predict the exact release date. I would like to release the production version sometime in March. That said, I consider the Beta release as already stable and feature-complete. Thank you for your patience and support. |
Beta Was this translation helpful? Give feedback.
-
🎊 It has finally happened! The
|
Beta Was this translation helpful? Give feedback.
-
how to truncate text in table cell to prevent it from growing vertically?. I always run into out of boundary errors. |
Beta Was this translation helpful? Give feedback.
-
Hello, It works great on windows and OSX, but when I deployed in a Docker container, can not load shared library 'QuestPdfSkia', I tried to copy manually, putting the file in my solution folder and then in the Docker file using "COPY ./QuestPdfSkia.so /usr/lib", but still can not load the shared library. This is the container error: 2024-02-20 13:22:51 Unhandled exception. System.TypeInitializationException: The type initializer for 'QuestPDF.Settings' threw an exception. |
Beta Was this translation helpful? Give feedback.
-
I'm using .NET 8.0 trying to make this work. Running in Windows 11. I can't install the pre-release version of QuestPdf.Preview because it complains about not being supported by .NET 8.0. :( I modified the code to generate and save to file but then I got this exception: `QuestPDF.Drawing.Exceptions.DocumentDrawingException: Could not find an appropriate font fallback for the following glyphs: Possible solutions:
I disabled it. Still got missing icudtl.dat file. I had to remove some I will keep testing it and reporting if I find anything broken for me. Thanks!! |
Beta Was this translation helpful? Give feedback.
-
@MarcinZiabek are you planning to make the QuestPDF native code public already for the next alpha release? I'd like to have a look at introducing OpenType FontFeatures. |
Beta Was this translation helpful? Give feedback.
-
FontManager.RegisterFontWithCustomName(Lettertypes.Standaard, File.OpenRead($"{AppDomain.CurrentDomain.BaseDirectory}\\Assets\\Fonts\\SourceSansPro-Regular.otf")); Throws exception
with inner exception
Getting this error when the application tries to register the custom otf fonts, just upgraded from nuget. Did not change anything else. |
Beta Was this translation helpful? Give feedback.
-
On x64 the application starts without the error, but generating a pdf gives me the following error.
I did find the icudtl.dat in the runtimes/win-x64/native folder |
Beta Was this translation helpful? Give feedback.
-
Hello, I am encountering a dependency issue while running QuestPDF on a Linux environment. The specific error is as follows:
I’m using Ubuntu 22.04.4 LTS (64-bit) and .net6. Any help resolving this dependency issue would be greatly appreciated. Thank you in advance for your time and help. |
Beta Was this translation helpful? Give feedback.
-
🎊 The 2024.3.0-beta is now live! 🎊Beta releaseAfter two weeks of hard work, I have finished the next milestone. Please take a look at the release notes:
Public native codeI have also open-sourced the native layer code. It consists of the following elements:
The code can be found here: https://github.com/QuestPDF/QuestPDF.Native Integrating with SkiaSharpI have also prepared two extension methods to show how QuestPDF can be integrated with SkiaSharp-based code and libraries. The first approach is to render content as image: public static void SkiaSharpRasterized(this IContainer container, Action<SKCanvas, Size> drawOnCanvas)
{
container.Image(payload =>
{
using var bitmap = new SKBitmap(payload.ImageSize.Width, payload.ImageSize.Height);
using (var canvas = new SKCanvas(bitmap))
{
var scalingFactor = payload.Dpi / (float)DocumentSettings.DefaultRasterDpi;
canvas.Scale(scalingFactor);
drawOnCanvas(canvas, payload.AvailableSpace);
}
return bitmap.Encode(SKEncodedImageFormat.Png, 100).ToArray();
});
} And the second is to use the built-in SVG support. Generally speaking, Skia is excellent at parsing and displaying SVG but much worse at producing SVG (misses more advanced features). However, it should be enough for integrating less visually complex content like charts. public static void SkiaSharpCanvas(this IContainer container, Action<SKCanvas, Size> drawOnCanvas)
{
container.Svg(size =>
{
using var stream = new MemoryStream();
using (var canvas = SKSvgCanvas.Create(new SKRect(0, 0, size.Width, size.Height), stream))
drawOnCanvas(canvas, size);
var svgData = stream.ToArray();
return Encoding.UTF8.GetString(svgData);
});
} How can you helpI consider the beta release to be the last version with new features. From now on, I would like to focus on quality and library stability. I am especially interested in the following things:
|
Beta Was this translation helpful? Give feedback.
-
Hello! If I understand correctly, the new version of QuestPDF planned to add the ability to justify text. Please tell me whether this has already been implemented in this beta version and if so, how can it be used? Thanks for the work you've done! |
Beta Was this translation helpful? Give feedback.
-
@MarcinZiabek I am unable to do Canvas operations now with the rc build. The code as suggested in #83 (comment) no longer works. Any suggestions? Also, someday the docs also would needs to update (https://www.questpdf.com/api-reference/canvas.html) |
Beta Was this translation helpful? Give feedback.
-
🎊 The 2024.3.0-rc1 is now live! 🎊Release notes
Memory usageWhile this release significantly improves memory handling, some low-hanging fruits remain. In the next major release, I would like to explore the following optimizations:
|
Beta Was this translation helpful? Give feedback.
-
If no further problems arise, I am planning to release the |
Beta Was this translation helpful? Give feedback.
-
Hi @MarcinZiabek, 1 question, noticed in my automated tests that there are some changes in padding (only upgraded questPdf version) Personally, I like the new report more Any changes done here? |
Beta Was this translation helpful? Give feedback.
-
Hi @MarcinZiabek, I updated my code to version 2024.3.0-rc2. All seems to work ok except the text embedded in my SVG charts doesn't display. The charts were created with OxyPlot and exported to an SVG string. They displayed fine in prior versions of QuestPDF. The Page level default text style is setup to use "Arial" as is OxyPlot. What am I missing? Is the .Svg extension no longer the correct method to use for displaying SVGs? You mentioned two new extension methods for using SkiaSharp code in this comment but I cannot find them in the code base.
|
Beta Was this translation helpful? Give feedback.
-
Using 2024.3.0-rc2 SVG with gradient color not rendered properly, or am I missing something? . . . |
Beta Was this translation helpful? Give feedback.
-
@MarcinZiabek Asp.Net Core 8 在 2024.3.0-rc2 上使用 svg,抛出异常:QuestPDF cannot instantiate native object. |
Beta Was this translation helpful? Give feedback.
-
Hi @MarcinZiabek, When I try to add an empty text to a column, it gets ignored. I am able to do so in 2023 but not in the latest one. sample code below, page.Content()
|
Beta Was this translation helpful? Give feedback.
-
Is this new version supported on Azure Functions? I just tried upgrading and get the error: Unable to load DLL 'QuestPdfSkia' or one of its dependencies I am on the professional licensed version. |
Beta Was this translation helpful? Give feedback.
-
Hi, do I understand this correctly that x86 won't be supported anymore? |
Beta Was this translation helpful? Give feedback.
-
Hi, When can we expect a release for the memory consumption issue? I am on licensed version. Thank you! |
Beta Was this translation helpful? Give feedback.
-
Where can I get QuestPdfSkia.dll? |
Beta Was this translation helpful? Give feedback.
-
Hello! Is this new version supported on "AWS Lambda functions"? I updated the QuestPDF package to version
It is weird, because the current runtime detected was AWS Lambda function configuration:
Project configuration:
Please, let me know in case you need more information. 😃 |
Beta Was this translation helpful? Give feedback.
-
Hi there! Thank you for providing this library. After creating and visualizing my file into previewer, I can't seem to generate the PDF file itself, currently is throwing the Exception:
And here it's the Template file: using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using Intracred.Core.Integration.Compliance.PDF.Models;
using Intracred.Core.Integration.Compliance.PDF.DataSource;
using IntraCred.Core.Infrastructure.Data;
using System.Globalization;
namespace Intracred.Core.Integration.Compliance.PDF.Template;
public class RelatorioSimplificadoComplianceTemplate : IDocument
{
private readonly AppDbContext _context;
private readonly RelatorioSimplificadoComplianceDataSource _dataSource;
private Guid AnaliseSimplificadaId { get; set; }
private RelatorioSimplificadoComplianceModel Model { get; set; }
public RelatorioSimplificadoComplianceTemplate(RelatorioSimplificadoComplianceModel model, AppDbContext context, Guid analiseSimplificadaId)
{
_context = context;
_dataSource = new RelatorioSimplificadoComplianceDataSource(_context);
AnaliseSimplificadaId = analiseSimplificadaId;
Model = model;
}
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public DocumentSettings GetSettings() => DocumentSettings.Default;
public void Compose(IDocumentContainer container)
{
Model = _dataSource.RecuperarDadosTemplateAnaliseSimplificada(AnaliseSimplificadaId).Result;
var tituloStyle = TextStyle.Default.FontSize(22).FontColor("#05385F").FontFamily("Roboto").Bold();
var subTituloStyle = TextStyle.Default.FontSize(16).FontColor("#05385F").FontFamily("Roboto").Bold();
var thirdTitleStyle = TextStyle.Default.FontSize(14).FontColor("#05385F").FontFamily("Roboto").SemiBold();
var smallText = TextStyle.Default.FontSize(10).FontColor("#05385F").FontFamily("Roboto");
var normalText = TextStyle.Default.FontSize(10).FontColor("#000000").FontFamily("Roboto");
var document = Document.Create(container =>
container.Page(page =>
{
page.Margin(15);
page.Header().Height(80).Background("#E5E7E9").Element(header => {
header.Column(column => {
column.Item().AlignCenter().PaddingBottom(10).Row(row => {
row.RelativeItem().AlignLeft().Column(topColumn => {
topColumn.Item().MaxWidth(135).Image(Path.Combine(Directory.GetCurrentDirectory(), "Application", "Services", "Integracao", "QuestPDF", "Template", "logo.png")).FitArea();
});
});
column.Item().PaddingTop(2).AlignCenter().Text("RELATÓRIO DE COMPLIANCE").Style(tituloStyle);
column.Item().AlignCenter().Text($"{Model.Nome}").Style(subTituloStyle);
});
});
page.Content().Background(Colors.Grey.Lighten3).Padding(10).Element(content => {
content.Column(column => {
column.Item().AlignCenter().PaddingBottom(10).Text("O Presente Relatório de Compliance tem por objetivo realizar a due diligence do Cedente acima identificado, bem como de seus sócios/acionistas. Aludida due diligence está alinhada com as melhoras práticas de Governança Corporativa e visa atender aos riscos legais e operacionais da atividade desempenhada pela INTRABANK ASSET MANAGEMENT LTDA. bem como de empresas do mesmo Grupo Econômico. Com a finalidade de otimizar as análises e consultas, as pesquisas são inicialmente realizadas com base em Big Datas especializados em Compliance.")
.AlignCenter().Style(normalText);
column.Item().PaddingBottom(2).Text("Informações da Análise").Style(subTituloStyle);
column.Item().PaddingBottom(10).Text($"Data da Pesquisa AML | {DateTime.Parse(Model.DataPesquisaAml, CultureInfo.GetCultureInfo("pt-BR")):dd/MM/yyyy}").Style(smallText);
column.Item().Text("1. Razão Social").Style(thirdTitleStyle);
column.Item().PaddingBottom(20).Text($"{Model.Nome}").Style(normalText);
column.Item().Text("2. CNPJ").Style(thirdTitleStyle);
column.Item().PaddingBottom(20).Text($"{Model.Cnpj}").Style(normalText);
column.Item().Text("3. Outros nomes").Style(thirdTitleStyle);
column.Item().PaddingBottom(20).Text($"{Model.ConhecidoComo}").Style(normalText);
column.Item().Text("4. Envolvimentos").Style(thirdTitleStyle);
column.Item().PaddingBottom(20).Text($"{Model.Envolvimento}").Justify().LineHeight(1.5f).Style(normalText);
column.Item().Text("5. Crimes").Style(thirdTitleStyle);
column.Item().PaddingBottom(20).Text($"{Model.Crimes}").Justify().LineHeight(1.5f).Style(normalText);
column.Item().AlignCenter().PaddingLeft(50).PaddingRight(50).Column(detailsColumn => {
detailsColumn.Item().Row(row => {
row.RelativeItem().AlignCenter().Text("Citado na Mídia?").Style(thirdTitleStyle);
row.RelativeItem().AlignCenter().Text("PEP?").Style(thirdTitleStyle);
row.RelativeItem().AlignCenter().Text("Score AML").Style(thirdTitleStyle);
});
detailsColumn.Item().Row(row => {
row.RelativeItem().AlignCenter().Text(Model.CitadoMidia == "Sim" ? "✔️" : "❌").Style(tituloStyle);
row.RelativeItem().AlignCenter().Text(Model.PessoaExpostaPoliticamente == "Sim" ? "✔️" : "❌").Style(tituloStyle);
row.RelativeItem().AlignCenter().Text(Model.ScoreAml).Style(tituloStyle);
});
});
column.Item().PageBreak();
column.Item().PaddingBottom(10).Text("Análise do DEJUR").Style(subTituloStyle);
column.Item().Text("1. Comentários do Analista").Style(thirdTitleStyle);
column.Item().PaddingBottom(20).Text($"{Model.ObservacaoAnalista}").Justify().LineHeight(1.5f).Style(normalText);
column.Item().PageBreak();
column.Item().Text("2. Comentários do Diretor").Style(thirdTitleStyle);
column.Item().PaddingBottom(20).Text($"{Model.ObservacaoDiretoria}").Justify().LineHeight(1.5f).Style(normalText);
});
});
page.Footer().Height(50).Background(Colors.Grey.Lighten1).Element(footer => {
footer.AlignBottom().Column(column => {
column.Item().AlignCenter().Row(row => {
row.RelativeItem().AlignLeft().Column(topColumn => {
topColumn.Item().Text("Data do Documento: " + DateTime.Now.ToString("dd/MM/yyyy | HH:mm:ss")).Style(smallText);
});
row.RelativeItem().AlignRight().Column(topColumn => {
topColumn.Item().Text("PROIBIDA CIRCULAÇÃO EXTERNA").Style(smallText);
});
});
});
});
})
);
}
} I've have tried for a few days now to make this work, but of course I'm doing something wrong. Any chance that you can help me with this? Thank you 😊 |
Beta Was this translation helpful? Give feedback.
-
I have some news to share about WASM support, and I am seeking Community feedback 😄 QuestPDF uses the It seems that using If the attempt with the Based on SkiaSharp statistics, less than 3% of developers use its WASM package, which is truly low. What are your thoughts? |
Beta Was this translation helpful? Give feedback.
-
Introduction
QuestPDF serves as an advanced layout engine leveraging the SkiaSharp library to facilitate the generation of final PDF documents. However, the dependency on SkiaSharp does come with certain inherited constraints.
Limitations with SkiaSharp
SkiaSharp is an exceptional library, a foundation for many libraries and projects, including those developed by Microsoft. Huge shout out to the SkiaSharp team!
Nonetheless, from the perspective of QuestPDF, SkiaSharp does present three fundamental limitations:
The project lags significantly behind the Skia project (as of now, SkiaSharp is on m88 from Feb 2021, while the latest Skia version is m115). Between those versions, numerous improvements to PDF rendering have been made by Skia developers.
SkiaSharp does not fully expose all the features of Skia, understandably due to the trade-off between features and library size.
Additional nuget packages are necessary when running on various platforms.
Just to make sure you know, these attributes are not shortcomings of SkiaSharp per se, especially considering its size and usage. They are merely constraints from QuestPDF's standpoint.
Advantages of a custom Skia Build
Creating a custom Skia build brings several advantages. It's important to note that Skia offers a variety of optional modules. We can disable the unneeded ones to minimize the final binary size, for instance, related to GPU-accelerated rendering.
The optional but useful modules are as follows:
ICU - provides advanced Unicode features like multi-directional text.
SkParagraph - supports advanced text features, including TextStyle, Unicode, line breaking, support for advanced languages, and text justification. It also is more stable and of higher quality than current QuestPDF text rendering capabilities.
SVG - built-in SVG support.
sfntly - font subsetting to minimize output PDF file size regardless of the font used.
PDF Tags - adds accessibility features to the generated PDF file.
Achieving a comparable level of functionality as the features mentioned above is undeniably challenging. Text rendering, in particular, has proven to be one of the most complex aspects of QuestPDF. Using additional Skia capabilities could significantly enhance the quality and feature set of QuestPDF.
Available approaches
Interacting with a C++ library from managed code is not straightforward. Typically, the native library is enhanced with additional C wrappers, which are then invoked from the managed code (e.g., C#). To maintain the object-oriented experience, extra class proxies are created to call these wrappers. This technique is what SkiaSharp aims to do. Sounds complex, doesn't it?
There are several approaches to tackle this task:
SWIG - it is an industry-standard library that can generate all the necessary code based on a custom configuration. I have successfully used SWIG with a simple C++ project, although its compatibility with Skia still needs to be tested.
CppSharp - this well-known library generates appropriate structures and bindings based on the compiler output. I have yet to gain experience with it.
SkiaSharp-based approach involves building a custom implementation by following the SkiaSharp convention. Important: it is not needed to wrap the entire Skia API, but rather a minimal set of functionalities.
Why not just extend SkiaSharp?
QuestPDF employs only a tiny fraction of the SkiaSharp API. The library does not expose Skia objects in its public API (with the
Canvas
element being the only exception). Therefore, it is an entirely different task to expose a few Skia interfaces and functions just for QuestPDF usage, in contrast to implementing an actual SkiaSharp feature with all available options and capabilities.Moreover, we cannot expect the Skia team to include additional packages in the official build exclusively for QuestPDF. There are currently several unavailable modules, including SkPicture, SkSVG (support for parsing this format), sfntly, etc.
However, as we become more adept at building native-to-managed code wrappers, we may consider assisting the Skia team in adding certain requested features, including support for PDF tags.
How to preserve the Canvas element?
Switching to a custom Skia build implies that the QuestPDF library will no longer recognize SkiaSharp objects. This could be seen as a substantial breaking change, particularly without any alternatives.
That being said, there are two potential solutions:
Utilize the SkPicture object - it can serialize all Skia drawing commands in a binary format. This approach is already utilized by the Previewer application to render the document pixel-perfectly. Assuming Skia is backward compatible (this remains untested), creating a simple NuGet package may be feasible that serializes SkiaSharp commands into SkPicture, passing the binary data to QuestPDF, where they are deserialized and drawn.
Utilize SVG - similarly to SkPicture, SkiaSharp can generate SVG files. This format is promising, especially given its high compatibility between Skia versions, and it could be the optimal solution.
Afterword
In the upcoming weeks, I'll update you on the progress regularly and hopefully gather valuable feedback.
Beta Was this translation helpful? Give feedback.
All reactions