Skip to content

Commit 2f8ff5e

Browse files
authored
Replace JPEG with PNG with background for export-as-image (#19305)
## Description Replace JPEG with PNG (with optional background) for the visual designer's export-as-image feature. ## Changes - Replace `ExportFormat` (`png` | `jpeg`) with `ExportBackgroundMode` (`transparent` | `solid`) to let users choose the background style instead of the file format. - Always export as PNG (with 2x pixel ratio for retina quality), removing JPEG support and the `toJpeg` import from `html-to-image`. - Simplify `captureGraphElement` and `saveDataUrl` by removing the format parameter; background color is now passed as `undefined` for transparent mode. - Update the export toolbar UI: - Replace the Format dropdown with a Background dropdown (Transparent / Solid). - Shorten high-contrast theme labels to `Dark HC` and `Light HC`. - Rename the export button from `Save As` to `Export`. - Clean up removed constants (`FORMAT_MIME`, `FORMAT_DESC`, `DEFAULT_EXPORT_FORMAT`) and update atom/type exports. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/19305)
1 parent 48cf443 commit 2f8ff5e

File tree

5 files changed

+45
-62
lines changed

5 files changed

+45
-62
lines changed

src/vscode-bicep-ui/apps/visual-designer/src/features/export/ExportToolbar.tsx

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
import type { DefaultTheme } from "styled-components";
5-
import type { ExportFormat } from "./types";
5+
import type { ExportBackgroundMode } from "./types";
66

77
import { Codicon } from "@vscode-bicep-ui/components";
88
import { VscodeOption, VscodeSingleSelect } from "@vscode-elements/react-elements";
@@ -12,9 +12,9 @@ import { styled } from "styled-components";
1212
import {
1313
closeExportOverlayAtom,
1414
exportBackgroundColorAtom,
15+
exportBackgroundModeAtom,
1516
exportCanvasElementAtom,
1617
exportFileStemAtom,
17-
exportFormatAtom,
1818
exportPaddingAtom,
1919
exportThemeOverrideAtom,
2020
isExportInProgressAtom,
@@ -50,12 +50,12 @@ const $ActionsGroup = styled($Group)`
5050
margin-left: auto;
5151
`;
5252

53-
const $FormatSelect = styled(VscodeSingleSelect)`
54-
width: 62px;
53+
const $BackgroundSelect = styled(VscodeSingleSelect)`
54+
width: 100px;
5555
`;
5656

5757
const $ThemeSelect = styled(VscodeSingleSelect)`
58-
width: 140px;
58+
width: 100px;
5959
`;
6060

6161
const $Separator = styled.div`
@@ -144,6 +144,10 @@ const $Label = styled.label`
144144
white-space: nowrap;
145145
`;
146146

147+
const $BackgroundLabel = styled($Label)`
148+
margin-left: 4px;
149+
`;
150+
147151
/* ---- Padding stepper -------------------------------------------- */
148152

149153
const $StepperGroup = styled.div`
@@ -213,14 +217,17 @@ const $PaddingInput = styled.input`
213217
/* Constants */
214218
/* ------------------------------------------------------------------ */
215219

216-
const FORMATS: ExportFormat[] = ["png", "jpeg"];
220+
const BACKGROUND_OPTIONS: { label: string; value: ExportBackgroundMode }[] = [
221+
{ label: "Transparent", value: "transparent" },
222+
{ label: "Solid", value: "solid" },
223+
];
217224

218225
const THEME_OPTIONS: { label: string; value: DefaultTheme["name"] | null }[] = [
219226
{ label: "Current", value: null },
220227
{ label: "Light", value: "light" },
221228
{ label: "Dark", value: "dark" },
222-
{ label: "High Contrast", value: "high-contrast" },
223-
{ label: "High Contrast Light", value: "high-contrast-light" },
229+
{ label: "Dark HC", value: "high-contrast" },
230+
{ label: "Light HC", value: "high-contrast-light" },
224231
];
225232

226233
const STEP = 10;
@@ -232,13 +239,13 @@ const STEP = 10;
232239
export function ExportToolbar() {
233240
const store = useStore();
234241
const canvasElement = useAtomValue(exportCanvasElementAtom);
235-
const format = useAtomValue(exportFormatAtom);
242+
const backgroundMode = useAtomValue(exportBackgroundModeAtom);
236243
const padding = useAtomValue(exportPaddingAtom);
237244
const exportThemeName = useAtomValue(exportThemeOverrideAtom);
238245
const exportFileStem = useAtomValue(exportFileStemAtom);
239246
const exportBackgroundColor = useAtomValue(exportBackgroundColorAtom);
240247
const exporting = useAtomValue(isExportInProgressAtom);
241-
const setFormat = useSetAtom(exportFormatAtom);
248+
const setBackgroundMode = useSetAtom(exportBackgroundModeAtom);
242249
const setTheme = useSetAtom(exportThemeOverrideAtom);
243250
const setPadding = useSetAtom(exportPaddingAtom);
244251
const setExportInProgress = useSetAtom(isExportInProgressAtom);
@@ -252,15 +259,16 @@ export function ExportToolbar() {
252259
setExportInProgress(true);
253260

254261
try {
255-
const dataUrl = await captureGraphElement(canvasElement, store, format, padding, exportBackgroundColor);
262+
const backgroundColor = backgroundMode === "solid" ? exportBackgroundColor : undefined;
263+
const dataUrl = await captureGraphElement(canvasElement, store, padding, backgroundColor);
256264
const fileStem = exportFileStem.trim() || "bicep-graph";
257-
await saveDataUrl(dataUrl, `${fileStem}.${format}`, format);
265+
await saveDataUrl(dataUrl, `${fileStem}.png`);
258266
} catch (error) {
259267
console.error("Export failed:", error);
260268
} finally {
261269
setExportInProgress(false);
262270
}
263-
}, [canvasElement, exporting, setExportInProgress, store, format, padding, exportBackgroundColor, exportFileStem]);
271+
}, [canvasElement, exporting, setExportInProgress, store, backgroundMode, padding, exportBackgroundColor, exportFileStem]);
264272

265273
const [paddingText, setPaddingText] = useState(String(padding));
266274

@@ -297,11 +305,11 @@ export function ExportToolbar() {
297305
[padding, setPadding],
298306
);
299307

300-
const handleFormatSelect = useCallback(
308+
const handleBackgroundSelect = useCallback(
301309
(e: Event) => {
302-
setFormat((e.currentTarget as HTMLSelectElement).value as ExportFormat);
310+
setBackgroundMode((e.currentTarget as HTMLSelectElement).value as ExportBackgroundMode);
303311
},
304-
[setFormat],
312+
[setBackgroundMode],
305313
);
306314

307315
const handleThemeSelect = useCallback(
@@ -314,16 +322,16 @@ export function ExportToolbar() {
314322

315323
return (
316324
<$Toolbar role="toolbar" aria-label="Export settings">
317-
{/* Format */}
325+
{/* Background */}
318326
<$Group>
319-
<$Label>Format</$Label>
320-
<$FormatSelect onChange={handleFormatSelect}>
321-
{FORMATS.map((f) => (
322-
<VscodeOption key={f} value={f} selected={f === format}>
323-
{f.toUpperCase()}
327+
<$BackgroundLabel>Background</$BackgroundLabel>
328+
<$BackgroundSelect onChange={handleBackgroundSelect}>
329+
{BACKGROUND_OPTIONS.map((bg) => (
330+
<VscodeOption key={bg.value} value={bg.value} selected={bg.value === backgroundMode}>
331+
{bg.label}
324332
</VscodeOption>
325333
))}
326-
</$FormatSelect>
334+
</$BackgroundSelect>
327335
</$Group>
328336

329337
<$Separator />
@@ -368,8 +376,7 @@ export function ExportToolbar() {
368376

369377
<$ActionsGroup>
370378
<$ExportButton onClick={handleExport} disabled={exporting}>
371-
<Codicon name="desktop-download" size={13} />
372-
{exporting ? "Saving\u2026" : "Save As"}
379+
{exporting ? "Exporting\u2026" : "Export"}
373380
</$ExportButton>
374381
<$IconButton onClick={() => closeExportOverlay()} title="Close" aria-label="Close export toolbar">
375382
<Codicon name="close" size={14} />

src/vscode-bicep-ui/apps/visual-designer/src/features/export/atoms.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22
// Licensed under the MIT License.
33

44
import type { DefaultTheme } from "styled-components";
5-
import type { ExportFormat } from "./types";
5+
import type { ExportBackgroundMode } from "./types";
66

77
import { atom } from "jotai";
88
import { activeThemeAtom, getThemeByName } from "@/lib/theming";
99

1010
export const DEFAULT_EXPORT_FILE_STEM = "bicep-graph";
1111
export const DEFAULT_EXPORT_PADDING = 40;
12-
export const DEFAULT_EXPORT_FORMAT: ExportFormat = "png";
1312

1413
export const isExportOverlayOpenAtom = atom(false);
1514
export const exportPaddingAtom = atom(DEFAULT_EXPORT_PADDING);
16-
export const exportFormatAtom = atom<ExportFormat>(DEFAULT_EXPORT_FORMAT);
15+
export const exportBackgroundModeAtom = atom<ExportBackgroundMode>("transparent");
1716
export const exportThemeOverrideAtom = atom<DefaultTheme["name"] | null>(null);
1817
export const exportFileStemAtom = atom(DEFAULT_EXPORT_FILE_STEM);
1918
export const isExportInProgressAtom = atom(false);
@@ -30,7 +29,7 @@ export const exportBackgroundColorAtom = atom((get) => get(effectiveExportThemeA
3029
export const isExportPreviewVisibleAtom = atom((get) => get(isExportOverlayOpenAtom));
3130

3231
export const isExportCanvasCoverVisibleAtom = atom(
33-
(get) => get(isExportOverlayOpenAtom) && get(exportFormatAtom) === "jpeg",
32+
(get) => get(isExportOverlayOpenAtom) && get(exportBackgroundModeAtom) === "solid",
3433
);
3534

3635
export const openExportOverlayAtom = atom(null, (_, set) => {

src/vscode-bicep-ui/apps/visual-designer/src/features/export/capture-element.ts

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
import type { ExportFormat } from "./types";
5-
6-
import { toJpeg, toPng } from "html-to-image";
4+
import { toPng } from "html-to-image";
75
import { getDefaultStore } from "jotai";
86
import { nodesByIdAtom } from "@/lib/graph";
97

@@ -76,9 +74,8 @@ function findTransformedElement(root: HTMLElement): HTMLElement | null {
7674
export async function captureGraphElement(
7775
canvasElement: HTMLElement,
7876
store: Store,
79-
format: ExportFormat,
8077
padding: number,
81-
backgroundColor: string,
78+
backgroundColor: string | undefined,
8279
): Promise<string> {
8380
const bounds = computeGraphBounds(store);
8481
if (!bounds) throw new Error("No graph nodes to export");
@@ -131,7 +128,8 @@ export async function captureGraphElement(
131128
const options = {
132129
width: captureWidth,
133130
height: captureHeight,
134-
backgroundColor: format === "jpeg" ? backgroundColor : undefined,
131+
backgroundColor,
132+
pixelRatio: 2,
135133
filter: (node: HTMLElement) => {
136134
// Exclude the dot-pattern background SVGs from the export.
137135
if (node.tagName === "svg" && node.querySelector?.("pattern")) {
@@ -141,33 +139,12 @@ export async function captureGraphElement(
141139
},
142140
};
143141

144-
switch (format) {
145-
case "png":
146-
return await toPng(clone, { ...options, pixelRatio: 2 });
147-
case "jpeg":
148-
return await toJpeg(clone, {
149-
...options,
150-
quality: 0.95,
151-
backgroundColor,
152-
});
153-
}
142+
return await toPng(clone, options);
154143
} finally {
155144
document.body.removeChild(wrapper);
156145
}
157146
}
158147

159-
/** MIME types for each export format. */
160-
const FORMAT_MIME: Record<ExportFormat, string> = {
161-
png: "image/png",
162-
jpeg: "image/jpeg",
163-
};
164-
165-
/** File extension descriptions for the Save dialog. */
166-
const FORMAT_DESC: Record<ExportFormat, string> = {
167-
png: "PNG Image",
168-
jpeg: "JPEG Image",
169-
};
170-
171148
/**
172149
* Convert a data-URL to a Blob.
173150
*/
@@ -194,7 +171,7 @@ function dataUrlToBlob(dataUrl: string): Blob {
194171
* Falls back to a direct download if the File System Access API
195172
* is not available (e.g. non-Chromium browsers).
196173
*/
197-
export async function saveDataUrl(dataUrl: string, defaultName: string, format: ExportFormat): Promise<void> {
174+
export async function saveDataUrl(dataUrl: string, defaultName: string): Promise<void> {
198175
const blob = dataUrlToBlob(dataUrl);
199176

200177
// Try the File System Access API (Chromium-based browsers).
@@ -204,8 +181,8 @@ export async function saveDataUrl(dataUrl: string, defaultName: string, format:
204181
suggestedName: defaultName,
205182
types: [
206183
{
207-
description: FORMAT_DESC[format],
208-
accept: { [FORMAT_MIME[format]]: [`.${format}`] },
184+
description: "PNG Image",
185+
accept: { "image/png": [".png"] },
209186
},
210187
],
211188
});

src/vscode-bicep-ui/apps/visual-designer/src/features/export/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ export { ExportAreaCover } from "./ExportAreaCover";
55
export { ExportAreaPreview } from "./ExportAreaPreview";
66
export { ExportOverlay } from "./ExportOverlay";
77
export * from "./atoms";
8-
export type { ExportFormat } from "./types";
8+
export type { ExportBackgroundMode } from "./types";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
export type ExportFormat = "png" | "jpeg";
4+
export type ExportBackgroundMode = "transparent" | "solid";

0 commit comments

Comments
 (0)