Skip to content

Workflow screenshot as PNG - #9501

Open
JPPhoto wants to merge 7 commits into
invoke-ai:mainfrom
JPPhoto:workflow-screenshot
Open

Workflow screenshot as PNG#9501
JPPhoto wants to merge 7 commits into
invoke-ai:mainfrom
JPPhoto:workflow-screenshot

Conversation

@JPPhoto

@JPPhoto JPPhoto commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds camera-button workflow PNG export.

  • 2x resolution with 100px padding.
  • Workflow-name-based .png download.
  • Nodes deselected and fully opaque.
  • Connectors rendered beneath all nodes.
  • Edge/background SVG styles preserved.
  • Invoke color scheme and grid preserved.
  • MiniMap, node status, and info icons omitted.
  • Input labels remain one line with full text.
  • Export guarded against duplicate clicks.
  • Added html-to-image.
  • Updated user documentation and translations.

QA Instructions

Build the frontend, click the camera icon when in Workflows. An empty workflow will render a small grid to the PNG, while a real workflow will render the entire workflow.

Related Issues / Discussions

Closes #5076

Merge Plan

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

@github-actions github-actions Bot added frontend-deps PRs that change frontend dependencies frontend PRs that change frontend files docs PRs that change docs labels Aug 14, 2026
@JPPhoto JPPhoto added 6.14.1 6.14 Nice-to-Have frontend PRs that change frontend files frontend-deps PRs that change frontend dependencies docs PRs that change docs and removed frontend-deps PRs that change frontend dependencies frontend PRs that change frontend files docs PRs that change docs labels Aug 14, 2026
@JPPhoto
JPPhoto force-pushed the workflow-screenshot branch from f08ea4a to ab58adb Compare August 14, 2026 22:04
@Pfannkuchensack Pfannkuchensack self-assigned this Aug 14, 2026
@Pfannkuchensack

Copy link
Copy Markdown
Member

Findings

1. Medium - a non-settling toBlob permanently disables the export button and leaks a full workflow DOM clone into the live document

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:382-391 awaits toBlob with no timeout and no abort. invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx:62-74 relies entirely on that promise settling: .finally(() => setIsExportingWorkflow(false)).

Chain:

  1. html-to-image@1.11.13 createImage is r.onload = function(){ r.decode().then(...) } with no rejection handler on decode(). If decode() rejects (the realistic trigger is an oversized serialized SVG from a large workflow), the promise it returns never resolves and never rejects.
  2. exportWorkflowAsPng's try/finally never reaches finally, so stagingWrapper.remove() never runs. A complete second copy of #workflow-editor stays appended to flowElement.parentElement for the rest of the session.
  3. .catch(...) and .finally(...) in ViewportControls never run, so isExportingWorkflow stays true and the camera IconButton stays isDisabled + isLoading (ViewportControls.tsx:101-102) until the editor remounts. No toast, no retry.

To expose this issue, add a test that stubs toBlob with a promise that never settles, races exportWorkflowAsPng against a deadline, and asserts the call rejects and that the staging wrapper has been removed. That test can only pass once a timeout is added around toBlob.

2. Medium - one un-embeddable image anywhere in the workflow aborts the whole export

getWorkflowExportOptions (invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:178-187) sets neither imagePlaceholder nor onImageErrorHandler.

Chain:

  1. html-to-image walks every node and calls embedImageNode on each HTMLImageElement / SVGImageElement.
  2. Its resourceToDataURL catches the fetch failure, console.warns, and returns options.imagePlaceholder || ''.
  3. embedImageNode then does img.srcset = ''; img.src = '' and awaits onload/onerror. Empty src fires error, and because onImageErrorHandler is unset, onerror is the raw reject.
  4. The rejection propagates out of toBlob, so the entire PNG export fails.

Workflows routinely carry image references (invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx, invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx). A single deleted or 404 image makes the camera button produce only nodes.downloadWorkflowImageError with no indication of the cause. html-to-image also caches the empty result module-globally, so a transient failure poisons every later export in the session.

To expose this issue, add a test that asserts getWorkflowExportOptions returns an imagePlaceholder (or onImageErrorHandler) so a failed image degrades to a placeholder instead of failing the export.

3. Medium - skipFonts: true renders the PNG in a fallback typeface

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:186.

Invoke ships a webfont: import '@fontsource-variable/inter' at invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx:1. html-to-image serializes the clone into an SVG foreignObject encoded as a data:image/svg+xml URL and loads it through an <img>. SVG in <img> is secure static mode: no external resource loading. The only mechanism that would make Inter available is html-to-image's @font-face inlining, and skipFonts: true disables exactly that. The fontsource CSS is same-origin and bundled by Vite, so cssRules is readable and the inlining path would in fact succeed here.

font-family is copied (line 69), so the PNG asks for Inter Variable and gets the browser default instead. Different metrics also shift every label width, compounding finding 4. This contradicts the PR's "Invoke color scheme ... preserved" framing, which holds for color but not for type.

This is primarily a rendered-text concern and there is no approved DOM testing framework in this repo (no jsdom/happy-dom in invokeai/frontend/web/package.json), so it needs manual verification: export a workflow and compare the label typeface against the editor.

4. Medium - forced one-line field titles can be clipped at the image edge or overlap neighbouring nodes

setWorkflowExportInputFieldTitleStyles (invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:278-285) forces white-space: nowrap, overflow: visible, text-overflow: clip on every [data-node-input-field-title="true"].

Chain:

  1. The node body has no clipping: invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx:24-37 sets no overflow: hidden, and invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx:66-79 sets only borderRadius.
  2. So a label wider than the node renders past the node's right edge onto the canvas.
  3. getWorkflowContentBounds (workflowImageExport.ts:129-158) derives the canvas from getNodesBounds plus .react-flow__edge-path getBBox() only. Rendered text extents are never measured.
  4. For a right-most node, any overflow beyond EXPORT_PADDING (100, workflowImageExport.ts:4) is cut off at the image boundary; elsewhere it silently overlaps whatever is to the right.

This defeats the PR's stated goal ("Input labels remain one line with full text") in exactly the case the override exists for - labels too long to fit.

To expose this issue, add a test that exports getWorkflowContentBounds and asserts it widens the returned rect to cover a measured overflowing label, not only node bounds and edge bounding boxes.

5. Low - the includeStyleProperties allowlist silently drops flex-wrap and direction

EXPORT_STYLE_PROPERTIES (invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:6-98) replaces html-to-image's default "all computed properties" with a fixed 90-entry list. Anything absent is dropped from the capture.

Two confirmed consumers:

  • flex-wrap: invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SavedWorkflowFieldInputComponent.tsx:146 renders <Flex alignItems="center" gap={1} flexWrap="wrap"> with a workflow name plus up to three badges inside a node body. In the export this collapses to nowrap and the badges are squashed or overflow.
  • direction: invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts:34 sets document.body.dir. RTL is a shipped feature (invokeai/frontend/web/public/locales/ar.json, invokeai/frontend/web/public/locales/he.json). The clone is serialized into a standalone foreignObject where that ancestor dir no longer applies, and direction is not in the list, so ar/he users get an LTR-laid-out PNG.

To expose this issue, add a test that asserts EXPORT_STYLE_PROPERTIES contains direction and flex-wrap.

6. Low - the grid geometry is duplicated from Flow.tsx with nothing binding the two

GRID_GAP = 25 at invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:112 duplicates const snapGrid: [number, number] = [25, 25] at invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx:91, which is what <Background gap={snapGrid} offset={snapGrid} /> (Flow.tsx:525) actually uses. setBackgroundGridForExport (workflowImageExport.ts:241-244) additionally hardcodes cx/cy/r = 0.5, which is only correct because <Background> is left at its default size={1} and the export pins zoom to 1.

Changing snapGrid, or passing size/color to <Background>, desyncs the exported grid from the editor with no failing test and no compile error.

To expose this issue, add a test that imports snapGrid from the flow module (exporting it if necessary) and asserts it equals the export module's grid gap.

7. Low - the export error is discarded

invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx:73 is .catch(handleWorkflowImageExportError), and the handler at lines 47-49 takes no arguments. The thrown error is never logged. Combined with findings 1 and 2, a field failure produces one generic toast string and nothing actionable.

8. Low - the export puts a duplicate id="workflow-editor" (and every other id in the subtree) into the live document

workflowImageExport.ts:374 does flowElement.cloneNode(true), which copies ids, and line 380 appends it to flowElement.parentElement. flowElement is #workflow-editor itself (Flow.tsx:490; @xyflow/react applies the id prop to the wrapper div that also carries .react-flow).

This is currently benign: the only other consumer, invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts:26, uses document.querySelector, which returns the earlier original. But it is invalid HTML for the duration of the export, duplicates every Chakra-generated aria-labelledby/aria-describedby target and every react-flow SVG marker id, and breaks the moment anyone reaches for getElementById or a nth-match query. The pattern already applied to the background pattern id (workflowImageExport.ts:230) is not applied to the root or to markers.

9. Low - the added tests do not test the export

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.test.ts - 11 tests, all passing (verified, see Verification), but none of them can fail for a real defect in this feature:

  • getWorkflowContentBounds (workflowImageExport.ts:129), which decides what ends up cut off, is module-private and untested. So are prepareExportClone, setBackgroundGridForExport, inlineSvgStylesForExport, downloadPng, and exportWorkflowAsPng.
  • workflowImageExport.test.ts:66-70 ('preserves single-line field title styles in the export clone') asserts only that a literal array declared in the same module contains three strings. It touches no clone. It is also self-contradictory: setWorkflowExportInputFieldTitleStyles forces display: block, which makes the -webkit-line-clamp it asserts on inert.
  • The DOM helper tests (lines 87-138) stub querySelectorAll with a hand-rolled function that returns the element only for the exact selector string the implementation passes. A selector that stops matching real markup - for example if NodeWrapper gains a wrapper element and .react-flow__node > [data-is-selected] no longer matches - still passes.
  • sanitizeWorkflowImageFilename has two cases; there is no coverage for length capping.
  • The suite runs in vitest's default node environment (no jsdom/happy-dom in invokeai/frontend/web/package.json), so real-DOM assertions are not available today.

To expose the selector risk, add a test that asserts the exact selector strings the helpers query, pinned against the data-* attributes added in this PR, so removing data-node-status-indicator / data-node-info-icon / data-node-input-field-title from the components fails a test rather than silently degrading the image.

@JPPhoto
JPPhoto force-pushed the workflow-screenshot branch from ab58adb to 1f21fad Compare August 14, 2026 23:57
@JPPhoto

JPPhoto commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

@Pfannkuchensack Thanks for the review. In the current commit:

  • No. 6 is not currently broken: grid gap and snap grid both equal 25. This is a maintainability risk only.
  • No. 7 is a valid low-severity issue: export errors are reduced to a generic toast; details are discarded.
  • No. 9 is a valid coverage gap, not a product defect. I'm adding new focused tests to expose missing behavior coverage.

All of the others are valid and I'm working on fixes now.

@JPPhoto

JPPhoto commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

@Pfannkuchensack Ready for a re-review.

@JPPhoto
JPPhoto force-pushed the workflow-screenshot branch from d971897 to 56fe129 Compare August 15, 2026 15:02
@Pfannkuchensack

Copy link
Copy Markdown
Member

Findings

1. Medium-High: aspect-ratio is missing from the style allowlist, so the Current Image node collapses in the exported PNG

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:9-103 (EXPORT_STYLE_PROPERTIES), consumed at invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:216.

Evidence chain:

  1. getWorkflowExportOptions passes includeStyleProperties: [...EXPORT_STYLE_PROPERTIES].
  2. html-to-image serializes the clone into an SVG foreignObject data URL, which carries none of the app stylesheets, so every visual property must be inlined from computed style. In html-to-image@1.11.13 lib/clone-node.js:164-191, cloneCSSStyle only takes the sourceStyle.cssText fast path when computed cssText is non-empty. I measured this in Chrome: getComputedStyle(el).cssText === '' (length 0). So the getStyleProperties(options) branch always runs, and the allowlist is the complete styling contract - anything not listed is dropped.
  3. aspect-ratio is not in EXPORT_STYLE_PROPERTIES.
  4. invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx:74 sets aspectRatio="1/1" on the Flex that gives the Current Image node its entire body height (parent is NonInvocationNodeWrapper with width={384}; there is no explicit height). invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx:156 does the same for collection thumbnails.
  5. Measured in Chrome: a width:100px; aspect-ratio:1/1; display:flex; flex-direction:column box with a height:100% child is 100px tall; removing only aspect-ratio makes it 0px tall.

Trigger: export any workflow containing a Current Image node, or a node with an image-collection input. The node renders collapsed in the PNG, while getNodesBounds reserved its full on-screen size and the edge paths were baked from live coordinates - so the node is both wrong-sized and detached from its connectors.

To expose this issue, add a test that asserts EXPORT_STYLE_PROPERTIES contains every CSS property the workflow node tree depends on for layout, seeded with aspect-ratio (the same shape as the existing invokeai/frontend/web/src/features/nodes/util/workflowImageExport.test.ts:68-72 assertion, which already guards text-overflow / -webkit-line-clamp / -webkit-box-orient).

2. Medium: input-label overflow is measured against the pre-export layout, so wide labels are clipped by the canvas edge

Measurement at invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:171-186; the conflicting mutation at invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:311-318.

getWorkflowContentBounds grows the bounds using Math.max(labelRect.width, label.scrollWidth) read from the live DOM. The live label (invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx:99-111) is noOfLines={1} plus labelSx display: 'flex', i.e. overflow:hidden with wrapping text. Measured in Chrome for a 120px-wide box with the label text "Positive Prompt Conditioning Collection Field":

  • live layout (either the flex or the -webkit-box variant): clientWidth 120, scrollWidth 120 -> overflowWidth === 0, bounds unchanged.
  • after setWorkflowExportInputFieldTitleStyles applies display:block; white-space:nowrap; overflow:visible to the clone: scrollWidth 236.

So the clone renders the label ~116px wider than anything the bounds calculation could see, and EXPORT_PADDING is only 100. Trigger: a node on the right edge of the workflow with a multi-word input label wider than its field column - its label is cut off at the PNG border. The label-bounds branch is effectively dead code as written.

The test that claims to cover this, invokeai/frontend/web/src/features/nodes/util/workflowImageExport.edgeCases.test.ts:132-148 ("includes overflowing input labels in content bounds"), passes scrollWidth: 100 for a rect of width: 100 - zero overflow. It never exercises the branch it is named after.

To expose this issue, add a test that feeds getWorkflowContentBounds a label whose scrollWidth exceeds its getBoundingClientRect().width and asserts the returned width grows by the overflow amount; then add a test that fixes the ordering contract - that the value fed into bounds is measured with the export label styles applied, not the live clamped ones.

3. Low: exports wider than 8192 logical px silently drop below the documented 2x resolution

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:191-198 and :209-220; documented as 2x in the docs card added by this PR.

canvasWidth = width * EXPORT_SCALE with pixelRatio: 1. In html-to-image@1.11.13 lib/index.js:89-95, canvas.width = canvasWidth * ratio, then checkCanvasDimensions runs because skipAutoScale is not set. lib/util.js:153-177 clamps any dimension above 16384 and rescales the other proportionally. So once padded content exceeds 8192 logical px in either axis (roughly 25 nodes laid out horizontally at NODE_WIDTH 320 plus spacing - routine for real workflows), the output silently falls below 2x, with no log line and no user-visible notice. The claim "2x resolution" in the PR description and in docs/src/content/docs/features/Workflows/editor-interface.mdx is unconditional.

To expose this issue, add a test that asserts getWorkflowImageDimensions either clamps canvasWidth/canvasHeight to the 16384 limit itself or reports the effective scale, so the degradation is explicit rather than delegated to library-internal auto-scaling.

4. Low: hardcoded English 'My Workflow' filename fallback bypasses the existing translation key

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:117 and :231-241.

getInitialWorkflow() in invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts:112-114 sets name: '', so every unsaved workflow exports as My Workflow.png. The filename is user-visible (download dialog, file system), and the repository already localizes this exact concept - workflows.unnamedWorkflow = "Unnamed Workflow" exists at invokeai/frontend/web/public/locales/en.json:2747. The new string is not in en.json at all. The two strings the PR did add (nodes.downloadWorkflowImage, nodes.downloadWorkflowImageError) are correctly keyed and consumed via t() at invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx:58,112-113.

To expose this issue, add a test that asserts the blank-name fallback resolves through a translation key rather than the module-local DEFAULT_WORKFLOW_IMAGE_FILENAME constant (sanitizeWorkflowImageFilename would need to take the fallback as an argument).

5. Low: the click handler has no test coverage at all

invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx:64-88.

Three behaviors the PR description calls out are untested: the duplicate-click guard (isExportingWorkflow), the missing-#workflow-editor path that fires the error toast with no logged error, and the .catch -> toast wiring. All export tests target invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts helpers only. There is no approved DOM testing framework in this repo, so a rendered-component test is not available; the reachable coverage is to extract the guard/dispatch logic into a plain function under invokeai/frontend/web/src/features/nodes/util/ and unit-test it with vitest. Otherwise this needs an explicit manual-verification note: double-click the camera button during a slow export and confirm only one download fires, and delete #workflow-editor from the DOM and confirm the error toast appears.

@JPPhoto
JPPhoto force-pushed the workflow-screenshot branch from 56fe129 to c7e952c Compare August 15, 2026 22:34
@Pfannkuchensack

Copy link
Copy Markdown
Member

Findings

1. Medium-High: aspect-ratio is missing from the style allowlist, so the Current Image node collapses in the exported PNG

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:9-103 (EXPORT_STYLE_PROPERTIES), consumed at invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:216.

Evidence chain:

  1. getWorkflowExportOptions passes includeStyleProperties: [...EXPORT_STYLE_PROPERTIES].
  2. html-to-image serializes the clone into an SVG foreignObject data URL, which carries none of the app stylesheets, so every visual property must be inlined from computed style. In html-to-image@1.11.13 lib/clone-node.js:164-191, cloneCSSStyle only takes the sourceStyle.cssText fast path when computed cssText is non-empty. I measured this in Chrome: getComputedStyle(el).cssText === '' (length 0). So the getStyleProperties(options) branch always runs, and the allowlist is the complete styling contract - anything not listed is dropped.
  3. aspect-ratio is not in EXPORT_STYLE_PROPERTIES.
  4. invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx:74 sets aspectRatio="1/1" on the Flex that gives the Current Image node its entire body height (parent is NonInvocationNodeWrapper with width={384}; there is no explicit height). invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx:156 does the same for collection thumbnails.
  5. Measured in Chrome: a width:100px; aspect-ratio:1/1; display:flex; flex-direction:column box with a height:100% child is 100px tall; removing only aspect-ratio makes it 0px tall.

Trigger: export any workflow containing a Current Image node, or a node with an image-collection input. The node renders collapsed in the PNG, while getNodesBounds reserved its full on-screen size and the edge paths were baked from live coordinates - so the node is both wrong-sized and detached from its connectors.

To expose this issue, add a test that asserts EXPORT_STYLE_PROPERTIES contains every CSS property the workflow node tree depends on for layout, seeded with aspect-ratio (the same shape as the existing invokeai/frontend/web/src/features/nodes/util/workflowImageExport.test.ts:68-72 assertion, which already guards text-overflow / -webkit-line-clamp / -webkit-box-orient).

2. Medium: input-label overflow is measured against the pre-export layout, so wide labels are clipped by the canvas edge

Measurement at invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:171-186; the conflicting mutation at invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:311-318.

getWorkflowContentBounds grows the bounds using Math.max(labelRect.width, label.scrollWidth) read from the live DOM. The live label (invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx:99-111) is noOfLines={1} plus labelSx display: 'flex', i.e. overflow:hidden with wrapping text. Measured in Chrome for a 120px-wide box with the label text "Positive Prompt Conditioning Collection Field":

  • live layout (either the flex or the -webkit-box variant): clientWidth 120, scrollWidth 120 -> overflowWidth === 0, bounds unchanged.
  • after setWorkflowExportInputFieldTitleStyles applies display:block; white-space:nowrap; overflow:visible to the clone: scrollWidth 236.

So the clone renders the label ~116px wider than anything the bounds calculation could see, and EXPORT_PADDING is only 100. Trigger: a node on the right edge of the workflow with a multi-word input label wider than its field column - its label is cut off at the PNG border. The label-bounds branch is effectively dead code as written.

The test that claims to cover this, invokeai/frontend/web/src/features/nodes/util/workflowImageExport.edgeCases.test.ts:132-148 ("includes overflowing input labels in content bounds"), passes scrollWidth: 100 for a rect of width: 100 - zero overflow. It never exercises the branch it is named after.

To expose this issue, add a test that feeds getWorkflowContentBounds a label whose scrollWidth exceeds its getBoundingClientRect().width and asserts the returned width grows by the overflow amount; then add a test that fixes the ordering contract - that the value fed into bounds is measured with the export label styles applied, not the live clamped ones.

3. Low: exports wider than 8192 logical px silently drop below the documented 2x resolution

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:191-198 and :209-220; documented as 2x in the docs card added by this PR.

canvasWidth = width * EXPORT_SCALE with pixelRatio: 1. In html-to-image@1.11.13 lib/index.js:89-95, canvas.width = canvasWidth * ratio, then checkCanvasDimensions runs because skipAutoScale is not set. lib/util.js:153-177 clamps any dimension above 16384 and rescales the other proportionally. So once padded content exceeds 8192 logical px in either axis (roughly 25 nodes laid out horizontally at NODE_WIDTH 320 plus spacing - routine for real workflows), the output silently falls below 2x, with no log line and no user-visible notice. The claim "2x resolution" in the PR description and in docs/src/content/docs/features/Workflows/editor-interface.mdx is unconditional.

To expose this issue, add a test that asserts getWorkflowImageDimensions either clamps canvasWidth/canvasHeight to the 16384 limit itself or reports the effective scale, so the degradation is explicit rather than delegated to library-internal auto-scaling.

4. Low: hardcoded English 'My Workflow' filename fallback bypasses the existing translation key

invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts:117 and :231-241.

getInitialWorkflow() in invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts:112-114 sets name: '', so every unsaved workflow exports as My Workflow.png. The filename is user-visible (download dialog, file system), and the repository already localizes this exact concept - workflows.unnamedWorkflow = "Unnamed Workflow" exists at invokeai/frontend/web/public/locales/en.json:2747. The new string is not in en.json at all. The two strings the PR did add (nodes.downloadWorkflowImage, nodes.downloadWorkflowImageError) are correctly keyed and consumed via t() at invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx:58,112-113.

To expose this issue, add a test that asserts the blank-name fallback resolves through a translation key rather than the module-local DEFAULT_WORKFLOW_IMAGE_FILENAME constant (sanitizeWorkflowImageFilename would need to take the fallback as an argument).

5. Low: the click handler has no test coverage at all

invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx:64-88.

Three behaviors the PR description calls out are untested: the duplicate-click guard (isExportingWorkflow), the missing-#workflow-editor path that fires the error toast with no logged error, and the .catch -> toast wiring. All export tests target invokeai/frontend/web/src/features/nodes/util/workflowImageExport.ts helpers only. There is no approved DOM testing framework in this repo, so a rendered-component test is not available; the reachable coverage is to extract the guard/dispatch logic into a plain function under invokeai/frontend/web/src/features/nodes/util/ and unit-test it with vitest. Otherwise this needs an explicit manual-verification note: double-click the camera button during a slow export and confirm only one download fires, and delete #workflow-editor from the DOM and confirm the error toast appears.

@JPPhoto

JPPhoto commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

@Pfannkuchensack Fixed:

  • Missing aspect-ratio export styling.
  • Label measurement after export styles are applied.
  • Canvas-size limit with preserved aspect ratio.
  • Localized unnamed-workflow filename fallback.
  • Added regression tests.

I'm not making the click-handler DOM test. There's no approved DOM test framework, and this is simple guarded/error-handled logic. Utility and integration coverage are present.

@lstein lstein added the 7.0.0 label Aug 17, 2026
@lstein lstein moved this to 7.0 Theme: Tabbed Layout UI in Invoke - Community Roadmap Aug 17, 2026
@JPPhoto
JPPhoto force-pushed the workflow-screenshot branch 15 times, most recently from 09f8325 to ad02b23 Compare August 25, 2026 03:15
@JPPhoto
JPPhoto force-pushed the workflow-screenshot branch from ad02b23 to 217d68a Compare August 25, 2026 23:15
@JPPhoto
JPPhoto force-pushed the workflow-screenshot branch from 217d68a to a693c70 Compare August 26, 2026 02:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

7.0.0 docs PRs that change docs frontend PRs that change frontend files frontend-deps PRs that change frontend dependencies

Projects

Status: 7.0 Theme: Tabbed Layout UI

Development

Successfully merging this pull request may close these issues.

[enhancement]: Export workflow as image

3 participants