Skip to content

Generate order and shipment PDFs with PdfPig instead of Scryber - #982

Merged
KrzysztofPajak merged 5 commits into
developfrom
feature/pdf-pdfpig
Oct 24, 2026
Merged

KrzysztofPajak merged 5 commits into
developfrom
feature/pdf-pdfpig

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Type: feature

Issue

Order invoices and packaging slips are rendered from Razor templates to HTML and converted to PDF by Scryber. Its latest release (9.7.5) still pins SixLabors.ImageSharp 3.1.12, so three ImageSharp advisories stay open in every scan and can only be suppressed in .trivyignore. Scryber's HTML subset has also produced several past bugs (logo paths, entity parsing, junk characters in header/footer).

The obvious replacement, PDFsharp/MigraDoc, changed its license to AGPL-3.0 in 6.5.0. GrandNode is GPL-3.0, and the AGPL network clause would oblige every shop running it to publish its source, including custom plugins. The last MIT version (6.2.4) is frozen.

Solution

  • PdfPig 0.1.16 (Apache-2.0, no dependencies) writes the PDF objects. Layout is ours: PdfComposer does text wrapping, tables with a header repeated after a page break, page breaks, a footer on every page and a PNG logo.
  • PdfService replaces HtmlToPdfService behind the unchanged IPdfService. It prints the same content with the same resource keys as the removed templates.
  • Noto Sans (OFL-1.1) is embedded in Grand.Business.Common, so output does not depend on the fonts installed on the host. Characters the font lacks print as ? instead of failing the document.
  • HTML stored in the database (attribute descriptions, invoice header and footer) is converted to plain text by PdfHtmlText. Markup is dropped, never rendered.
  • The logo is loaded from the picture service. Non-PNG pictures (usually WebP) are converted with SkiaSharp. A logo that cannot be read is skipped, so it no longer fails the PDF.
  • Packaging slips take the company name from the order's store, and filter a vendor's lines by Shipment.VendorId. Neither reads the ambient work context any more.
  • OrderPdfPaymentLinesEvent replaces the pdf_payment_method_below widget zone. A handler adds plain-text lines that are printed below the payment method.
  • ImageSharp: without Scryber, elFinder.Net.Core 1.5.0 resolves ImageSharp 2.0.0. Grand.Web.AdminShared now lifts it to 3.1.12 explicitly, following the existing Microsoft.AspNetCore.Http lift. The three .trivyignore entries are kept and now name elFinder as the source.
  • .ai docs are updated: PDF is code, not views, and the dependency table now lists PdfPig.

Breaking changes

  • Views/PdfTemplates/OrderPdfTemplate.cshtml and ShipmentPdfTemplate.cshtml are removed, in Grand.Web and in Grand.Web.Vendor. Installations that edited them lose those edits; the layout now lives in PdfService.
  • The pdf_payment_method_below widget zone is removed. Plugins that rendered into it need to handle OrderPdfPaymentLinesEvent instead.
  • Each shipment in a bulk packaging-slip PDF now starts on its own page.
  • A vendor's invoice now ends with the subtotal, tax and total of that vendor's own lines; before, it had no totals. Shipping, order discounts, gift vouchers and the payment fee belong to the whole order and are not shown to a vendor.
  • Text outside Latin, Greek and Cyrillic (for example CJK, Arabic, Hebrew) prints as ?, and right-to-left text is not supported.
  • HtmlToPdfService is replaced by PdfService, with different constructor dependencies. Code that resolved it through IPdfService is unaffected.
  • IViewRenderService and ViewRenderService are removed; the PDF templates were their only consumer. A plugin that rendered a Razor view to a string through them needs its own IRazorViewEngine call.

Testing

  1. Run dotnet test src/Tests/Grand.Business.Common.Tests --filter "FullyQualifiedName~Services.Pdf". 18 tests generate PDFs and read them back with PdfPig. They cover Polish characters, totals, the vendor filter, markup removal, missing glyphs, page breaks with a footer on every page, packaging-slip filtering, payment lines from the event, cancellation, the logo written once per document, an empty document, a row taller than a page, an over-long footer, header text beside the logo, a logo PNG PdfPig cannot decode, table cells in footer HTML, a vendor's own totals, and a very long unbroken word.
  2. In Admin → Settings → General → PDF settings, upload a logo (PNG or JPEG) and set the invoice header and footer text, using some HTML such as <p>Bank: <b>…</b></p>.
  3. Place an order containing a product with attributes, using a customer name with diacritics (e.g. Zażółć Gęślą).
  4. Admin → Sales → Orders → the order → Invoice (PDF). Check the logo, the header, both addresses, the product rows with attribute lines, the totals, and the footer at the bottom of the page.
  5. Orders list → Print PDF invoices (all) with more than one order selected. Each order starts on a new page.
  6. Add a shipment for the order, then Shipments → Print packaging slip (PDF). The slip lists the shipping address, the products, SKU and quantity.
  7. Log in as a vendor that owns one line of a mixed-vendor order, then open the order's invoice PDF and the vendor's packaging slip. Only that vendor's lines are listed, and the invoice has no totals.
  8. Storefront → My account → Orders → PDF invoice gives the same invoice as step 4.
  9. Run dotnet list src/Web/Grand.Web/Grand.Web.csproj package --include-transitive. Neither Scryber.* nor SixLabors.ImageSharp 2.0.0 is listed.

Scryber renders the Razor PDF templates as HTML, and its latest release
still pins SixLabors.ImageSharp 3.1.12, which carries known advisories.
PDFsharp/MigraDoc is not an option either: since 6.5.0 it is AGPL-3.0,
which would put every GPL-licensed GrandNode shop under the network
clause.

PdfPig (Apache-2.0, no dependencies) only writes PDF objects; layout lives
in PdfComposer (wrapping, tables, page breaks, footer, PNG logo), and
PdfService builds the invoice and packaging slip from the same resources
the templates used. Noto Sans (OFL-1.1) is embedded so the output does not
depend on the host's fonts; characters it lacks print as "?".

The pdf_payment_method_below widget zone cannot reach a PDF that is not a
view, so OrderPdfPaymentLinesEvent takes its place.

Without Scryber, elFinder.Net.Core resolves ImageSharp 2.0.0, so
Grand.Web.AdminShared now lifts it to 3.1.12 explicitly.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 17:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Comment on lines +370 to +374
catch (Exception ex)
{
_logger.LogWarning(ex, "PDF logo {PictureId} could not be loaded", _pdfSettings.LogoPictureId);
return null;
}
Comment on lines +260 to +265
foreach (var usage in await _giftVoucherService.GetAllGiftVoucherUsageHistory(order.Id))
{
if (usage.UsedValue <= 0) continue;
var giftVoucher = await _giftVoucherService.GetGiftVoucherById(usage.GiftVoucherId);
Add(Format("PDFInvoice.GiftVoucherInfo", lang, giftVoucher?.Code), usage.UsedValue);
}

var fileName = $"order_{order.OrderGuid}_{CommonHelper.GenerateRandomDigitCode(4)}.pdf";

var dir = Path.Combine(_webHostEnvironment.WebRootPath, "assets/files/exportimport");

if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);

var filePath = Path.Combine(dir, fileName);
support added 2 commits October 23, 2026 19:21
Every order in a bulk invoice starts a page with the logo, and PdfPig's AddPng
writes a new image object each time, so 100 invoices grew to about 1 MB.
The composer now reuses the image added on the first page.
- A table row taller than a page is split between its lines instead of
  running over the footer and off the page.
- A document with no orders or shipments gets one empty page; PdfPig
  failed to build a PDF without pages.
- Header text wraps beside the right-aligned logo, and the cursor moves
  below the logo before the address table.
- A logo PNG that PdfPig cannot decode is converted again, or left out,
  before the layout starts instead of failing the whole invoice.
- The footer is cut to a quarter of the page, so a very long footer no
  longer starts a new page for every line.
- Table cells in header/footer HTML keep a space between them; style,
  script and comments are dropped; hr breaks the line.
- Wrapping measures each word once and splits long words with a binary
  search.
- PrintPackagingSlipsToPdf no longer loads a language it never used.
/// </summary>
public void ClearImage()
{
if (_asideBottom == double.MaxValue) return;
}

//not even one line fits at the top of a page: stop rather than add pages forever
if (used == 0 && room >= pageRoom - 2 * CellPadding) break;
var space = Measure(" ", size, font);
foreach (var paragraph in Supported(text, font).Split('\n'))
{
var line = new StringBuilder();
Comment on lines +245 to +248
catch (Exception)
{
return false;
}
support added 2 commits October 24, 2026 06:21
…rom Razor views

HtmlToPdfService was its only consumer. Plugins that rendered a view to a string through it need their own IRazorViewEngine call.
…inear time

A vendor's copy of an invoice listed only that vendor's lines but no
totals. It now ends with the subtotal, tax and total of those lines.
Shipping, order discounts, gift vouchers and the payment fee belong to
the whole order and stay off it, as does the checkout attribute text.

A word wider than its column was measured again for every piece cut off
it, so a long unbroken customer text took quadratic time: a 50,000
character attribute value held the request for over a minute. The word
is now measured once and cut at the letter positions.
@KrzysztofPajak
KrzysztofPajak merged commit afc4d35 into develop Oct 24, 2026
9 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the feature/pdf-pdfpig branch October 24, 2026 04:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants