Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Unitary Hack] Save Python qsharp-widgets RE output to PNG #1604

Merged
merged 15 commits into from
Jun 14, 2024
Merged
50 changes: 48 additions & 2 deletions npm/qsharp/ux/estimatesOverview.tsx
nirajvenkat marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// The results table is also a legend for the scatter chart.

import { useState } from "preact/hooks";
import { createRef } from "preact";
import { ColorMap } from "./colormap.js";
import {
CreateSingleEstimateResult,
Expand All @@ -14,6 +15,7 @@ import {
} from "./data.js";
import { ResultsTable, Row } from "./resultsTable.js";
import { Axis, PlotItem, ScatterChart, ScatterSeries } from "./scatterChart.js";
import { saveToPng } from "./saveImage.js";

const columnNames = [
"Run name",
Expand Down Expand Up @@ -187,6 +189,27 @@ export function EstimatesOverview(props: {
const [selectedRow, setSelectedRow] = useState<string | null>(null);
const [selectedPoint, setSelectedPoint] = useState<[number, number]>();

const printRef = createRef();

const handleSaveImage = async () => {
const element = printRef.current;
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 = "image.png";

document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
window.open(data);
}
};

const runNameRenderingError =
props.runNames != null &&
props.runNames.length > 0 &&
Expand Down Expand Up @@ -306,8 +329,31 @@ export function EstimatesOverview(props: {
</>
) : (
<>
{getResultTable()}
{getScatterChart()}
<button
role="button"
onClick={handleSaveImage}
className={"qs-estimatesOverview-saveIcon"}
style="position: absolute; top: 5px; right: 5px;"
nirajvenkat marked this conversation as resolved.
Show resolved Hide resolved
>
<span>
<svg
width="75%"
height="75%"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
className={"qs-estimatesOverview-saveIconSvgPath"}
d="M12.0147 2.8595L13.1397 3.9845L13.25 4.25V12.875L12.875 13.25H3.125L2.75 12.875V3.125L3.125 2.75H11.75L12.0147 2.8595ZM3.5 3.5V12.5H12.5V4.406L11.5947 3.5H10.25V6.5H5V3.5H3.5ZM8 3.5V5.75H9.5V3.5H8Z"
/>
</svg>
</span>
</button>
<div ref={printRef}>
{getResultTable()}
{getScatterChart()}
</div>
</>
)}
</div>
Expand Down
12 changes: 12 additions & 0 deletions npm/qsharp/ux/qsharp-ux.css
Original file line number Diff line number Diff line change
Expand Up @@ -711,3 +711,15 @@ html {
.qs-estimatesOverview-error {
color: red;
}

.qs-estimatesOverview-saveIcon {
cursor: pointer;
width: 3em;
height: 3em;
}

.qs-estimatesOverview-saveIconSvgPath {
fill: #424242 !important;
nirajvenkat marked this conversation as resolved.
Show resolved Hide resolved
fill-rule: evenodd !important;
clip-rule: evenodd !important;
}
98 changes: 98 additions & 0 deletions npm/qsharp/ux/saveImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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;
}

export 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));
}

export async function toSvg<T extends HTMLElement>(
node: T,
width: number,
height: number,
): Promise<string> {
const clonedNode = (await cloneNode(node)) as HTMLElement;
const dataURI = await nodeToDataURI(clonedNode, width, height);
return dataURI;
}

export async function saveToPng<T extends HTMLElement>(
node: T,
backgroundColor: string,
): Promise<string> {
const { width, height } = getImageSize(node);
const svg = await toSvg(node, width, height);
const img = await createImage(svg);

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();
}
72 changes: 72 additions & 0 deletions npm/qsharp/ux/saveImageUtil.tsx
nirajvenkat marked this conversation as resolved.
Show resolved Hide resolved
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(node: HTMLElement, 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(node: HTMLElement) {
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 + 10, // 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: SVGElement): 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";
nirajvenkat marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Loading