Skip to content

Commit 1c0cbf4

Browse files
authored
Use a custom renderer to render png/jpeg (microsoft#12978)
For #12977
1 parent 4cd5df9 commit 1c0cbf4

File tree

10 files changed

+146
-77
lines changed

10 files changed

+146
-77
lines changed

news/3 Code Health/12977.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Custom renderers for `png/jpeg` images in `Notebooks`.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3153,6 +3153,8 @@
31533153
"application/vnd.vegalite.v4+json",
31543154
"application/x-nteract-model-debug+json",
31553155
"image/gif",
3156+
"image/png",
3157+
"image/jpeg",
31563158
"text/latex",
31573159
"text/vnd.plotly.v1+html"
31583160
]

src/client/datascience/notebook/helpers/helpers.ts

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export function createCellFromVSCNotebookCell(vscCell: NotebookCell, model: INot
107107

108108
// Add the metadata back to the cell if we have any.
109109
// Refer to `addCellMetadata` to see how metadata is stored in VSC Cells.
110+
// This metadata would exist if the user copied and pasted an existing cell.
110111
if (vscCell.metadata.custom?.vscodeMetadata) {
111112
cell.data = {
112113
...cell.data,
@@ -120,17 +121,19 @@ export function createCellFromVSCNotebookCell(vscCell: NotebookCell, model: INot
120121
}
121122

122123
/**
123-
* Updates the VSC Cell metadata with metadata from our cells.
124-
* If user exits without saving, then we have all metadata in VSC document.
125-
* This way when users copy a cell, we have everything in the old cell to create a duplicate of the Jupyter cell.
126-
* (Remember: VSC Cells less information compared to Jupyter cells).
124+
* Stores the Jupyter Cell metadata into the VSCode Cells.
125+
* This is used to facilitate:
126+
* 1. When a user copies and pastes a cell, then the corresponding metadata is also copied across.
127+
* 2. Diffing (VSC knows about metadata & stuff that contributes changes to a cell).
127128
*/
128129
export function updateVSCNotebookCellMetadata(cellMetadata: NotebookCellMetadata, cell: ICell) {
129130
cellMetadata.custom = cellMetadata.custom ?? {};
130131
// tslint:disable-next-line: no-any
131132
const metadata: Record<string, any> = {};
132133
cellMetadata.custom.vscodeMetadata = metadata;
133-
const propertiesToClone = ['metadata', 'attachments', 'outputs'];
134+
// We put this only for VSC to display in diff view.
135+
// Else we don't use this.
136+
const propertiesToClone = ['metadata', 'attachments'];
134137
propertiesToClone.forEach((propertyToClone) => {
135138
if (cell.data[propertyToClone]) {
136139
metadata[propertyToClone] = cloneDeep(cell.data[propertyToClone]);
@@ -146,6 +149,21 @@ export function getDefaultCodeLanguage(model: INotebookModel) {
146149
}
147150

148151
export function createVSCNotebookCellDataFromCell(model: INotebookModel, cell: ICell): NotebookCellData | undefined {
152+
if (cell.data.cell_type === 'raw') {
153+
const rawCell = cell.data;
154+
return {
155+
cellKind: vscodeNotebookEnums.CellKind.Code,
156+
language: 'raw',
157+
metadata: {
158+
custom: {
159+
metadata: rawCell.metadata,
160+
attachments: rawCell.attachments
161+
}
162+
},
163+
outputs: [],
164+
source: concatMultilineStringInput(cell.data.source)
165+
};
166+
}
149167
if (cell.data.cell_type !== 'code' && cell.data.cell_type !== 'markdown') {
150168
traceError(`Conversion of Cell into VS Code NotebookCell not supported ${cell.data.cell_type}`);
151169
return;
@@ -250,56 +268,23 @@ export function cellOutputToVSCCellOutput(output: nbformat.IOutput): CellOutput
250268
* @param {nbformat.IDisplayData} output
251269
* @returns {(CellDisplayOutput | undefined)}
252270
*/
253-
function translateDisplayDataOutput(output: nbformat.IDisplayData): CellDisplayOutput | undefined {
254-
const mimeTypes = Object.keys(output.data || {});
271+
function translateDisplayDataOutput(
272+
output: nbformat.IDisplayData | nbformat.IDisplayUpdate | nbformat.IExecuteResult
273+
): CellDisplayOutput | undefined {
255274
// If no mimeType data, then there's nothing to display.
256-
if (!mimeTypes.length) {
275+
if (!Object.keys(output.data || {}).length) {
257276
return;
258277
}
259-
// If we have images, then process those images.
260-
// If we have PNG or JPEG images with a background, then add that background as HTML
261278
const data = { ...output.data };
262-
if (mimeTypes.some(isImagePngOrJpegMimeType) && shouldConvertImageToHtml(output) && !output.data['text/html']) {
263-
const mimeType = 'image/png' in data ? 'image/png' : 'image/jpeg';
264-
const metadata = output.metadata || {};
265-
const needsBackground = typeof metadata.needs_background === 'string';
266-
const backgroundColor = metadata.needs_background === 'light' ? 'white' : 'black';
267-
const divStyle = needsBackground ? `background-color:${backgroundColor};` : '';
268-
const imgSrc = `data:${mimeType};base64,${output.data[mimeType]}`;
269-
270-
let height = '';
271-
let width = '';
272-
let imgStyle = '';
273-
if (metadata[mimeType] && typeof metadata[mimeType] === 'object') {
274-
// tslint:disable-next-line: no-any
275-
const imageMetadata = metadata[mimeType] as any;
276-
height = imageMetadata.height ? `height=${imageMetadata.height}` : '';
277-
width = imageMetadata.width ? `width=${imageMetadata.width}` : '';
278-
if (imageMetadata.unconfined === true) {
279-
imgStyle = `style="max-width:none"`;
280-
}
281-
}
282-
283-
// Hack, use same classes as used in VSCode for images.
284-
// This is to maintain consistently in displaying images (if we hadn't used HTML).
285-
// See src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts
286-
data[
287-
'text/html'
288-
] = `<div class='display' style="overflow:scroll;${divStyle}"><img src="${imgSrc}" ${imgStyle} ${height} ${width}></div>`;
289-
}
279+
// tslint:disable-next-line: no-any
280+
const metadata = output.metadata ? ({ custom: output.metadata } as any) : undefined;
290281
return {
291282
outputKind: vscodeNotebookEnums.CellOutputKind.Rich,
292-
data
283+
data,
284+
metadata // Used be renderers & VS Code for diffing (it knows what has changed).
293285
};
294286
}
295287

296-
function shouldConvertImageToHtml(output: nbformat.IDisplayData) {
297-
const metadata = output.metadata || {};
298-
return typeof metadata.needs_background === 'string' || metadata['image/png'] || metadata['image/jpeg'];
299-
}
300-
function isImagePngOrJpegMimeType(mimeType: string) {
301-
return mimeType === 'image/png' || mimeType === 'image/jpeg';
302-
}
303288
function translateStreamOutput(output: nbformat.IStream): CellStreamOutput | CellDisplayOutput {
304289
// Do not return as `CellOutputKind.Text`. VSC will not translate ascii output correctly.
305290
// Instead format the output as rich.

src/client/datascience/notebook/integration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export class NotebookIntegration implements IExtensionSingleActivationService {
8181
'application/vnd.vegalite.v4+json',
8282
'application/x-nteract-model-debug+json',
8383
'image/gif',
84+
'image/png',
85+
'image/jpeg',
8486
'text/latex',
8587
'text/vnd.plotly.v1+html'
8688
]

src/client/datascience/notebook/renderer.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,28 @@ export class NotebookOutputRenderer implements VSCNotebookOutputRenderer {
2020
public render(document: NotebookDocument, request: NotebookRenderRequest) {
2121
let outputToSend = request.output;
2222
if (request.output.outputKind === CellOutputKind.Rich && request.mimeType in request.output.data) {
23+
// tslint:disable-next-line: no-any
24+
const metadata: Record<string, any> = {};
25+
// Send metadata only for the mimeType we are interested in.
26+
const customMetadata = request.output.metadata?.custom;
27+
if (customMetadata) {
28+
if (customMetadata[request.mimeType]) {
29+
metadata[request.mimeType] = customMetadata[request.mimeType];
30+
}
31+
if (customMetadata.needs_background) {
32+
metadata.needs_background = customMetadata.needs_background;
33+
}
34+
if (customMetadata.unconfined) {
35+
metadata.unconfined = customMetadata.unconfined;
36+
}
37+
}
2338
outputToSend = {
2439
...request.output,
25-
// Send only what we need & ignore other mimetypes.
40+
// Send only what we need & ignore other mimeTypes.
2641
data: {
2742
[request.mimeType]: request.output.data[request.mimeType]
28-
}
43+
},
44+
metadata
2945
};
3046
}
3147
const id = uuid();

src/datascience-ui/interactive-common/cellOutput.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -484,14 +484,14 @@ export class CellOutput extends React.Component<ICellOutputProps> {
484484
const transformedList = outputs.map(this.transformOutput.bind(this));
485485

486486
transformedList.forEach((transformed, index) => {
487-
const mimetype = transformed.output.mimeType;
487+
const mimeType = transformed.output.mimeType;
488488
if (isIPyWidgetOutput(transformed.output.mimeBundle)) {
489489
// Create a view for this output if not already there.
490490
this.renderWidget(transformed.output);
491-
} else if (mimetype && isMimeTypeSupported(mimetype)) {
491+
} else if (mimeType && isMimeTypeSupported(mimeType)) {
492492
// If that worked, use the transform
493493
// Get the matching React.Component for that mimetype
494-
const Transform = getTransform(mimetype);
494+
const Transform = getTransform(mimeType);
495495

496496
let className = transformed.output.isText ? 'cell-output-text' : 'cell-output-html';
497497
className = transformed.output.isError ? `${className} cell-output-error` : className;
@@ -525,14 +525,14 @@ export class CellOutput extends React.Component<ICellOutputProps> {
525525
}
526526
}
527527
} else if (
528-
!mimetype ||
529-
mimetype.startsWith('application/scrapbook.scrap.') ||
530-
mimetype.startsWith('application/aml')
528+
!mimeType ||
529+
mimeType.startsWith('application/scrapbook.scrap.') ||
530+
mimeType.startsWith('application/aml')
531531
) {
532532
// Silently skip rendering of these mime types, render an empty div so the user sees the cell was executed.
533533
buffer.push(<div key={index}></div>);
534534
} else {
535-
const str: string = this.getUnknownMimeTypeFormatString().format(mimetype);
535+
const str: string = this.getUnknownMimeTypeFormatString().format(mimeType);
536536
buffer.push(<div key={index}>{str}</div>);
537537
}
538538
});

src/datascience-ui/ipywidgets/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Hosting ipywidgets in non-notebook contexxt
1+
# Hosting ipywidgets in non-notebook context
22

33
- Much of the work is influenced by sample `web3` from https://github.com/jupyter-widgets/ipywidgets/blob/master/examples/web3
44
- Displaying `ipywidgets` in non notebook context requires 3 things:
@@ -13,5 +13,5 @@
1313
- Thats what the code in this folder does (wraps the html manager + custom kernel connection)
1414
- Kernel messages from the extension are sent to this layer using the `postoffice`
1515
- Similarly messages from sent from html manager via the kernel are sent to the actual kernel via the postoffice.
16-
- However, the sequence and massaging of the kernel messages requires a lot of work. Baiscally majority of the message processing from `/node_modules/@jupyterlab/services/lib/kernel/*.js`
16+
- However, the sequence and massaging of the kernel messages requires a lot of work. Basically majority of the message processing from `/node_modules/@jupyterlab/services/lib/kernel/*.js`
1717
- Much of the message processing logic is borrowed from `comm.js`, `default.js`, `future.js`, `kernel.js` and `manager.js`.

src/datascience-ui/renderers/render.tsx

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,79 @@ import { fixMarkdown } from '../interactive-common/markdownManipulation';
1111
import { getTransform } from '../interactive-common/transforms';
1212

1313
export interface ICellOutputProps {
14-
output: nbformat.IOutput;
14+
output: nbformat.IExecuteResult | nbformat.IDisplayData;
1515
mimeType: string;
1616
}
1717

1818
export class CellOutput extends React.Component<ICellOutputProps> {
19-
// tslint:disable-next-line: no-any
2019
constructor(prop: ICellOutputProps) {
2120
super(prop);
2221
}
2322
public render() {
24-
const mimeBundle = this.props.output.data as nbformat.IMimeBundle; // NOSONAR
25-
let data: nbformat.MultilineString | JSONObject = mimeBundle[this.props.mimeType!];
23+
const mimeBundle = this.props.output.data;
24+
const data: nbformat.MultilineString | JSONObject = mimeBundle[this.props.mimeType!];
2625

27-
// Fixup latex to make sure it has the requisite $$ around it
28-
if (this.props.mimeType! === 'text/latex') {
29-
data = fixMarkdown(concatMultilineStringOutput(data as nbformat.MultilineString), true);
26+
switch (this.props.mimeType) {
27+
case 'text/latex':
28+
return this.renderLatex(data);
29+
case 'image/png':
30+
case 'image/jpeg':
31+
return this.renderImage(mimeBundle, this.props.output.metadata);
32+
33+
default:
34+
return this.renderLatex(data);
3035
}
36+
}
37+
/**
38+
* Custom rendering of image/png and image/jpeg to handle custom Jupyter metadata.
39+
* Behavior adopted from Jupyter lab.
40+
*/
41+
// tslint:disable-next-line: no-any
42+
private renderImage(mimeBundle: nbformat.IMimeBundle, metadata: Record<string, any> = {}) {
43+
const mimeType = 'image/png' in mimeBundle ? 'image/png' : 'image/jpeg';
44+
45+
const imgStyle: Record<string, string | number> = {};
46+
const divStyle: Record<string, string | number> = { overflow: 'scroll' }; // This is the default style used by Jupyter lab.
47+
const imgSrc = `data:${mimeType};base64,${mimeBundle[mimeType]}`;
3148

49+
if (typeof metadata.needs_background === 'string') {
50+
divStyle.backgroundColor = metadata.needs_background === 'light' ? 'white' : 'black';
51+
}
52+
// tslint:disable-next-line: no-any
53+
const imageMetadata = metadata[mimeType] as Record<string, any> | undefined;
54+
if (imageMetadata) {
55+
if (imageMetadata.height) {
56+
imgStyle.height = imageMetadata.height;
57+
}
58+
if (imageMetadata.width) {
59+
imgStyle.width = imageMetadata.width;
60+
}
61+
if (imageMetadata.unconfined === true) {
62+
imgStyle.maxWidth = 'none';
63+
}
64+
}
65+
66+
// Hack, use same classes as used in VSCode for images (keep things as similar as possible).
67+
// This is to maintain consistently in displaying images (if we hadn't used HTML).
68+
// See src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts
69+
// tslint:disable: react-a11y-img-has-alt
70+
return (
71+
<div className={'display'} style={divStyle}>
72+
<img src={imgSrc} style={imgStyle}></img>
73+
</div>
74+
);
75+
}
76+
private renderOutput(data: nbformat.MultilineString | JSONObject) {
3277
const Transform = getTransform(this.props.mimeType!);
3378
return (
3479
<div>
3580
<Transform data={data} />
3681
</div>
3782
);
3883
}
84+
private renderLatex(data: nbformat.MultilineString | JSONObject) {
85+
// Fixup latex to make sure it has the requisite $$ around it
86+
data = fixMarkdown(concatMultilineStringOutput(data as nbformat.MultilineString), true);
87+
return this.renderOutput(data);
88+
}
3989
}

src/test/datascience/dsTestSetup.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ if (
3838
'application/vnd.vegalite.v4+json',
3939
'application/x-nteract-model-debug+json',
4040
'image/gif',
41+
'image/png',
42+
'image/jpeg',
4143
'text/latex',
4244
'text/vnd.plotly.v1+html'
4345
]

0 commit comments

Comments
 (0)