Skip to content

HTML template bodies with data: URI images render blank — extraction only happens on Designer upload, not in the render path #377

Description

@untangleportwood

Summary

HTML-type templates whose body contains <img src="data:image/png;base64,…"> (embedded / "self-contained" images) render with blank spaces where every embedded image should be. Blob.toPdf (Flying Saucer) cannot decode data: URIs — this is a known engine constraint. Portwood already works around it, but only in the Designer's client-side upload flow (docGenAdmin._processAndSaveHtmlBody). Any body that reaches the renderer by another route — bundle import, AI generation, a direct saveHtmlTemplateBody call, a file attached straight to the record, a metadata/package deploy, or the Header_Html__c / Footer_Html__c fields — keeps its data: URIs and produces broken output with no error and no warning.

Reported by a customer via support: HTML templates render blank, the Word equivalents are fine, and it happens "when using the template bundle" (→ cross-org export/import).

Reproduction

Repro'd on docgen-verify (Developer scratch org, source-deployed HEAD), 2026-08-31, using the customer's actual embedded PNG (1224×1584, 214 KB decoded).

Rendered the same image three ways through the real DocGenService.generatePdfBlob path and measured the output PDF size:

Template body Output PDF Result
no image (baseline) 988 B
<img src="data:image/png;base64,…"> (214 KB PNG) 2,312 B image dropped — contributed +1.3 KB of nothing
<img src="data:image/png;base64,…"> (small 400×400 PNG) 2,475 B image dropped — +1.5 KB of nothing, no error
<img src="/sfc/servlet.shepherd/version/download/<cvId>"> (same 214 KB PNG) 215,154 B image embedded — renders correctly
full template generate from the customer's real .html (2 embedded PNGs, ~380 KB body) crashSystem.LimitException: Regex too complicated at DocGenService.stripStyleAndScriptBlocks:9982 tracked separately — see Scope
same file, data: URIs swapped for /sfc/ URLs 215,551 B renders correctly

The data:/sfc/ swap is a full fix for the render — no engine change needed.

Mechanism (verified by running the code, not asserted)

  1. Blob.toPdf silently drops data: URI images. No exception; blank space where the <img> is. A relative /sfc/servlet.shepherd/version/download/<cvId> URL for the identical image renders fine (proven above: +214 KB embedded vs. +0). Documented behaviour — see CHANGELOG.md ("Data-URI images are dropped (Blob.toPdf rejects them)") and the html-template-authoring skill.
  2. The render path does not sanitize the body. DocGenService.mergeHtmlTemplate decodes the stored body (DocGenService.cls:9902-9903), strips <style>/<script>, runs the merge, wraps it, and hands it to Blob.toPdf — the <img src="data:…"> is passed through untouched. There is no server-side data: → ContentVersion conversion for HTML template bodies (that logic exists for DOCX/PPTX template media and for rich-text merge values, not for an HTML body).
  3. The only place data: URIs get extracted is the LWC. docGenAdmin._processAndSaveHtmlBody (force-app/main/default/lwc/docGenAdmin/docGenAdmin.js:6684-6743) matches src="data:image/…;base64,…", uploads each as a ContentVersion, and rewrites the src to /sfc/…. It is described in-code as "the single choke point every body reaches" — but that is only true for the four Designer routes (file/ZIP upload, editor save, AI paste-back, switch-to-HTML).
  4. No warning. DocGenAiHtmlValidator flags position: absolute/fixed/sticky, flex, etc., but not data: URIs or absolute image URLs. The author gets a blank PDF (or a crash) with zero feedback.

Affected write paths (where a data: URI can get stored un-extracted)

Path Extracts data: URIs?
Designer file / ZIP upload _processAndSaveHtmlBody
Designer HTML editor save / paste-back / switch-to-HTML ✅ same choke point
AI template generation & edit (DocGenAiTemplateControllersaveHtmlTemplateBody) ❌ relies on the LLM not emitting them + a validator that doesn't check
saveHtmlTemplateBody / saveAndPublishHtmlBody called directly (both @AuraEnabled) ❌ stores the string as-is
Cross-org export / import — "template bundle" (DocGenController importTemplateBundle, ~7862-7874) ❌ only re-keys existing /sfc/ URLs via rewriteShepherdCvIds; a data: body is stored verbatim
Template Clone Partial — re-keys CV-backed images; a raw data: body passes through
Direct file attach — drop an .html onto the template record, point a version at it (how the e2e tests build HTML templates) ❌ total bypass
Metadata / package deploy of a template + body CV ❌ total bypass
Header_Html__c / Footer_Html__c — plain long-text fields, editable by field edit / API / Flow / updateTemplate ❌ no extraction

The customer's "when using the template bundle" points at the export/import row.

Why fix it in the render path

9 write paths, 1 render funnel. Every generation route — generatePdfBlob, generateDocument(WithFormat), processDocumentAsHtmlWithImageMap, DocGenBulkController / DocGenBatch / DocGenGiantQueryBatch, signature sender preview + packet render, Runner preview, Flow generate actions, generatePdfFromDataMap — funnels through DocGenService.mergeTemplatemergeHtmlTemplate. A safety net there covers every path in one place, regardless of which write path let the data: URI in. Patching the write side means touching every one of the ❌ rows above and hoping no tenth path is added.

Fix options (ranked)

⚠️ Updated — see the implementation-finding comment below. Option 1 was attempted and does not work: Blob.toPdf cannot fetch a ContentVersion inserted in the same transaction, so extracting image CVs during the render is impossible. Revised options are in the comment (recommended: a consolidated write-path helper plus a render-path persist-and-heal fallback).

  1. [recommended] Server-side data: → ContentVersion extraction in mergeHtmlTemplate, run right after the body is decoded (DocGenService.cls ~9904) and before stripStyleAndScriptBlocks / processXml. Mirror _processAndSaveHtmlBody: for each src="data:image/…;base64,…", decode, insert a docgen_html_img_<templateId>_<ts>_<n> ContentVersion, rewrite the src to the relative /sfc/ form. Notes:
    • The scan must be indexOf-based (like extractHtmlStyleBlocks), not replaceAll / Matcher, or it hits its own "Regex too complicated" on a large blob.
    • Creates a ContentVersion per image at render time (1 DML + heap each). Needs a cap (e.g. skip + warn above ~10 images or ~10 MB total). Ideally write the rewritten body back once so it isn't re-extracted on every render.
    • Guest / Experience Cloud render contexts may lack Create on ContentVersion — use a system-mode insert with the existing docgen_html_img_* naming, or a guest FLS guard.
  2. Author-facing warning as the fallback. When option 1 is capped/skipped, and in DocGenAiHtmlValidator at edit time: "This image is embedded as a data: URI and will not render in the PDF — upload it through the Designer or use a relative URL." Cheap; complements option 1, doesn't replace it.
  3. Extend extraction to each write path individually. Rejected — brittle, and misses direct-attach / metadata-deploy bodies entirely.

Scope

Open question

Need from the customer via support: (a) the exact Portwood package version their org is on, and (b) how these templates were created and loaded (hand-authored / a converter / a bundle export). Determines whether the current Designer extraction should already have covered them.

Environment

Reproduced on docgen-verify — Developer-edition scratch org, --no-namespace source deploy at repo HEAD, API 68.0, 2026-08-31. Customer's own .html templates and embedded PNGs used as fixtures.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdesignerVisual Designer / template editor LWC (docGenAdmin)help wantedExtra attention is neededpdfRelated to PDF output or Blob.toPdf()priority:P1Visible regression or significant bug, has workaroundseverity:visible-regressionQuality issue is visible to user, workaround usually exists

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions