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) |
crash — System.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)
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.
- 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).
- 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).
- 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 (DocGenAiTemplateController → saveHtmlTemplateBody) |
❌ 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.mergeTemplate → mergeHtmlTemplate. 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).
- [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.
- 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.
- 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.
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 decodedata: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 directsaveHtmlTemplateBodycall, a file attached straight to the record, a metadata/package deploy, or theHeader_Html__c/Footer_Html__cfields — keeps itsdata: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.generatePdfBlobpath and measured the output PDF size:<img src="data:image/png;base64,…">(214 KB PNG)<img src="data:image/png;base64,…">(small 400×400 PNG)<img src="/sfc/servlet.shepherd/version/download/<cvId>">(same 214 KB PNG).html(2 embedded PNGs, ~380 KB body)System.LimitException: Regex too complicatedatDocGenService.stripStyleAndScriptBlocks:9982data:URIs swapped for/sfc/URLsThe
data:→/sfc/swap is a full fix for the render — no engine change needed.Mechanism (verified by running the code, not asserted)
Blob.toPdfsilently dropsdata: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 — seeCHANGELOG.md("Data-URI images are dropped (Blob.toPdfrejects them)") and thehtml-template-authoringskill.DocGenService.mergeHtmlTemplatedecodes the stored body (DocGenService.cls:9902-9903), strips<style>/<script>, runs the merge, wraps it, and hands it toBlob.toPdf— the<img src="data:…">is passed through untouched. There is no server-sidedata:→ 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).data:URIs get extracted is the LWC.docGenAdmin._processAndSaveHtmlBody(force-app/main/default/lwc/docGenAdmin/docGenAdmin.js:6684-6743) matchessrc="data:image/…;base64,…", uploads each as a ContentVersion, and rewrites thesrcto/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).DocGenAiHtmlValidatorflagsposition: absolute/fixed/sticky, flex, etc., but notdata: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)data:URIs?_processAndSaveHtmlBodyDocGenAiTemplateController→saveHtmlTemplateBody)saveHtmlTemplateBody/saveAndPublishHtmlBodycalled directly (both@AuraEnabled)DocGenControllerimportTemplateBundle, ~7862-7874)/sfc/URLs viarewriteShepherdCvIds; adata:body is stored verbatimdata:body passes through.htmlonto the template record, point a version at it (how the e2e tests build HTML templates)Header_Html__c/Footer_Html__c— plain long-text fields, editable by field edit / API / Flow /updateTemplateThe 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 throughDocGenService.mergeTemplate→mergeHtmlTemplate. A safety net there covers every path in one place, regardless of which write path let thedata:URI in. Patching the write side means touching every one of the ❌ rows above and hoping no tenth path is added.Fix options (ranked)
data:→ ContentVersion extraction inmergeHtmlTemplate, run right after the body is decoded (DocGenService.cls~9904) and beforestripStyleAndScriptBlocks/processXml. Mirror_processAndSaveHtmlBody: for eachsrc="data:image/…;base64,…", decode, insert adocgen_html_img_<templateId>_<ts>_<n>ContentVersion, rewrite thesrcto the relative/sfc/form. Notes:indexOf-based (likeextractHtmlStyleBlocks), notreplaceAll/Matcher, or it hits its own "Regex too complicated" on a large blob.docgen_html_img_*naming, or a guest FLS guard.DocGenAiHtmlValidatorat 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.Scope
System.LimitException: Regex too complicatedatDocGenService.stripStyleAndScriptBlocks:9982(lazy(?is)<style[^>]*>.*?</style\s*>replaceAllover a large body) is the same bug class as Word generation fails outright above ~450K of document.xml — "Regex too complicated", regardless of merge-tag count #325 / fix(merge): lift the ~450K document.xml ceiling on Word generation #341 and Third whole-document Matcher: extractLoopBody carries a ~900KRegex too complicatedceiling on the giant-query path #362 and is being addressed separately — convertstripStyleAndScriptBlocksto anindexOfsplice, as fix(merge): lift the ~450K document.xml ceiling on Word generation #341 did for the Word path (its siblingextractHtmlStyleBlocksis alreadyindexOf-based). That change is the unconditional backstop; this issue makes the images actually render.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-namespacesource deploy at repo HEAD, API 68.0, 2026-08-31. Customer's own.htmltemplates and embedded PNGs used as fixtures.