-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Unitary Hack] Save Python qsharp-widgets RE output to PNG (#1604)
Submission for Unitary Hack issue: fixes #1063 This change adds the ability to save the resource estimates diagram locally as a PNG. For this I added a new dependency called `html2canvas` that takes a DOM and renders it to an image. This currently works only on IPython `qsharp-widgets`, could be extended to the VSCode version. Additionally we take into account the color theme within VSCode (which is provided in the stylesheet `qsharp-ux.css`) and this is passed in as the background color for rendering. Tested on (dark/light) Modern VSCode themes. https://github.com/microsoft/qsharp/assets/3744263/898de6d9-fba4-4fea-b729-a3bb698dc51c ~Current issues:~ - ~This button is placeholder, nicer version: microsoft/vscode-notebook-renderers#118 - ~Saved imaged is slightly truncated at the bottom~ --------- Co-authored-by: Mine Starks <[email protected]>
- Loading branch information
1 parent
68bb77d
commit 7985568
Showing
7 changed files
with
233 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
// Writes resource estimator output to PNG file | ||
|
||
import { | ||
getImageSize, | ||
createImage, | ||
nodeToDataURI, | ||
toArray, | ||
} from "./saveImageUtil.js"; | ||
|
||
async function cloneSingleNode<T extends HTMLElement>( | ||
node: T, | ||
): Promise<HTMLElement> { | ||
return node.cloneNode(false) as T; | ||
} | ||
|
||
async function cloneChildren<T extends HTMLElement>( | ||
nativeNode: T, | ||
clonedNode: T, | ||
): Promise<T> { | ||
let children: T[] = []; | ||
children = toArray<T>((nativeNode.shadowRoot ?? nativeNode).childNodes); | ||
|
||
// Depth-first traversal of DOM objects | ||
await children.reduce( | ||
(deferred, child) => | ||
deferred | ||
.then(() => cloneNode(child)) | ||
.then((clonedChild: HTMLElement | null) => { | ||
if (clonedChild) { | ||
clonedNode.appendChild(clonedChild); | ||
} | ||
}), | ||
Promise.resolve(), | ||
); | ||
|
||
return clonedNode; | ||
} | ||
|
||
function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) { | ||
const targetStyle = clonedNode.style; | ||
if (!targetStyle) { | ||
return; | ||
} | ||
|
||
const sourceStyle = window.getComputedStyle(nativeNode); | ||
toArray<string>(sourceStyle).forEach((name) => { | ||
const value = sourceStyle.getPropertyValue(name); | ||
targetStyle.setProperty(name, value, sourceStyle.getPropertyPriority(name)); | ||
}); | ||
} | ||
|
||
function decorate<T extends HTMLElement>(nativeNode: T, clonedNode: T): T { | ||
cloneCSSStyle(nativeNode, clonedNode); | ||
return clonedNode; | ||
} | ||
|
||
async function cloneNode<T extends HTMLElement>(node: T): Promise<T | null> { | ||
return Promise.resolve(node) | ||
.then((clonedNode) => cloneSingleNode(clonedNode) as Promise<T>) | ||
.then((clonedNode) => cloneChildren(node, clonedNode)) | ||
.then((clonedNode) => decorate(node, clonedNode)); | ||
} | ||
|
||
async function saveToPng<T extends HTMLElement>( | ||
node: T, | ||
backgroundColor: string, | ||
): Promise<string> { | ||
const { width, height } = getImageSize(node); | ||
const clonedNode = (await cloneNode(node)) as HTMLElement; | ||
const uri = await nodeToDataURI(clonedNode, width, height); | ||
const img = await createImage(uri); | ||
|
||
const ratio = window.devicePixelRatio || 1; | ||
const canvas = document.createElement("canvas"); | ||
canvas.width = width * ratio; | ||
canvas.height = height * ratio; | ||
|
||
const context = canvas.getContext("2d")!; | ||
context.fillStyle = backgroundColor; | ||
context.fillRect(0, 0, canvas.width, canvas.height); | ||
|
||
context.drawImage(img, 0, 0, canvas.width, canvas.height); | ||
return canvas.toDataURL(); | ||
} | ||
|
||
export async function saveToImage<T extends HTMLElement>( | ||
element: T, | ||
filename = "image.png", | ||
) { | ||
const backgroundColor = | ||
getComputedStyle(element).getPropertyValue("--main-background"); | ||
const data = await saveToPng(element, backgroundColor); | ||
const link = document.createElement("a"); | ||
if (typeof link.download === "string") { | ||
link.href = data; | ||
link.download = filename; | ||
|
||
document.body.appendChild(link); | ||
link.click(); | ||
document.body.removeChild(link); | ||
} else { | ||
window.open(data); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
export function toArray<T>(arrayLike: any): T[] { | ||
const arr: T[] = []; | ||
for (let i = 0, l = arrayLike.length; i < l; i++) { | ||
arr.push(arrayLike[i]); | ||
} | ||
|
||
return arr; | ||
} | ||
|
||
function px<T extends HTMLElement>(node: T, styleProperty: string) { | ||
const win = node.ownerDocument.defaultView || window; | ||
const val = win.getComputedStyle(node).getPropertyValue(styleProperty); | ||
return val ? parseFloat(val.replace("px", "")) : 0; | ||
} | ||
|
||
export function getImageSize<T extends HTMLElement>(node: T) { | ||
const leftBorder = px(node, "border-left-width"); | ||
const rightBorder = px(node, "border-right-width"); | ||
const topBorder = px(node, "border-top-width"); | ||
const bottomBorder = px(node, "border-bottom-width"); | ||
|
||
return { | ||
width: node.clientWidth + leftBorder + rightBorder, | ||
height: node.clientHeight + topBorder + bottomBorder + 12, // Fixes up truncated region | ||
}; | ||
} | ||
|
||
export function createImage(url: string): Promise<HTMLImageElement> { | ||
return new Promise((resolve, reject) => { | ||
const img = new Image(); | ||
img.decode = () => resolve(img) as any; | ||
img.onload = () => resolve(img); | ||
img.onerror = reject; | ||
img.crossOrigin = "anonymous"; | ||
img.decoding = "async"; | ||
img.src = url; | ||
}); | ||
} | ||
|
||
export async function svgToDataURI(svg: Element): Promise<string> { | ||
return Promise.resolve() | ||
.then(() => new XMLSerializer().serializeToString(svg)) | ||
.then(encodeURIComponent) | ||
.then((html) => `data:image/svg+xml;charset=utf-8,${html}`); | ||
} | ||
|
||
export async function nodeToDataURI( | ||
node: HTMLElement, | ||
width: number, | ||
height: number, | ||
): Promise<string> { | ||
const xmlns = "http://www.w3.org/2000/svg"; | ||
const svg = document.createElementNS(xmlns, "svg"); | ||
const foreignObject = document.createElementNS(xmlns, "foreignObject"); | ||
|
||
svg.setAttribute("width", `${width}`); | ||
svg.setAttribute("height", `${height}`); | ||
svg.setAttribute("viewBox", `0 0 ${width} ${height}`); | ||
|
||
foreignObject.setAttribute("width", "100%"); | ||
foreignObject.setAttribute("height", "100%"); | ||
foreignObject.setAttribute("x", "0"); | ||
foreignObject.setAttribute("y", "0"); | ||
foreignObject.setAttribute("externalResourcesRequired", "true"); | ||
|
||
svg.appendChild(foreignObject); | ||
foreignObject.appendChild(node); | ||
return svgToDataURI(svg); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters