Skip to content

Commit fcd85d8

Browse files
authored
Merge pull request #251 from martinRenou/put_image_data_improvements
Improve put_image_data performances
2 parents db71559 + 94f00fc commit fcd85d8

File tree

5 files changed

+46
-52
lines changed

5 files changed

+46
-52
lines changed

examples/binary_image.ipynb

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@
119119
" data = np.stack((np.zeros_like(z), vecscale(z), vecscale(z)), axis=2)\n",
120120
"\n",
121121
" with hold_canvas():\n",
122-
" canvas.clear()\n",
123122
" canvas.put_image_data(data, 0, 0)\n",
124123
"\n",
125124
"\n",
@@ -131,18 +130,11 @@
131130
"\n",
132131
"VBox((canvas, HBox((play, progress))))"
133132
]
134-
},
135-
{
136-
"cell_type": "code",
137-
"execution_count": null,
138-
"metadata": {},
139-
"outputs": [],
140-
"source": []
141133
}
142134
],
143135
"metadata": {
144136
"kernelspec": {
145-
"display_name": "Python 3",
137+
"display_name": "Python 3 (ipykernel)",
146138
"language": "python",
147139
"name": "python3"
148140
},
@@ -156,7 +148,14 @@
156148
"name": "python",
157149
"nbconvert_exporter": "python",
158150
"pygments_lexer": "ipython3",
159-
"version": "3.8.5"
151+
"version": "3.10.4"
152+
},
153+
"widgets": {
154+
"application/vnd.jupyter.widget-state+json": {
155+
"state": {},
156+
"version_major": 2,
157+
"version_minor": 0
158+
}
160159
}
161160
},
162161
"nbformat": 4,

ipycanvas/canvas.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,9 +1371,9 @@ def put_image_data(self, image_data, x=0, y=0):
13711371
draw. Unlike the CanvasRenderingContext2D.putImageData method, this method **is** affected by the canvas transformation
13721372
matrix, and supports transparency.
13731373
"""
1374-
image_metadata, image_buffer = binary_image(image_data)
1374+
image_buffer = binary_image(image_data)
13751375
_CANVAS_MANAGER.send_draw_command(
1376-
self, COMMANDS["putImageData"], [image_metadata, x, y], [image_buffer]
1376+
self, COMMANDS["putImageData"], [x, y], [image_buffer]
13771377
)
13781378

13791379
def create_image_data(self, width, height):

ipycanvas/utils.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,12 @@ def image_bytes_to_array(im_bytes):
2424
return np.array(im)
2525

2626

27-
def binary_image(ar):
28-
"""Turn a NumPy array representing an array of pixels into a binary buffer."""
29-
if ar is None:
30-
return None
31-
if ar.dtype != np.uint8:
32-
ar = ar.astype(np.uint8)
33-
if ar.ndim == 1:
34-
ar = ar[np.newaxis, :]
35-
if ar.ndim == 2:
36-
# extend grayscale to RGBA
37-
add_alpha = np.full((ar.shape[0], ar.shape[1], 4), 255, dtype=np.uint8)
38-
add_alpha[:, :, :3] = np.repeat(ar[:, :, np.newaxis], repeats=3, axis=2)
39-
ar = add_alpha
40-
if ar.ndim != 3:
41-
raise ValueError("Please supply an RGBA array with shape (width, height, 4).")
42-
if ar.shape[2] != 4 and ar.shape[2] == 3:
43-
add_alpha = np.full((ar.shape[0], ar.shape[1], 4), 255, dtype=np.uint8)
44-
add_alpha[:, :, :3] = ar
45-
ar = add_alpha
46-
if not ar.flags["C_CONTIGUOUS"]: # make sure it's contiguous
47-
ar = np.ascontiguousarray(ar, dtype=np.uint8)
48-
return {"shape": ar.shape, "dtype": str(ar.dtype)}, memoryview(ar)
27+
def binary_image(ar, quality=75):
28+
f = BytesIO()
29+
PILImage.fromarray(ar.astype(np.uint8), "RGB" if ar.shape[2] == 3 else "RGBA").save(
30+
f, "JPEG", quality=quality
31+
)
32+
return f.getvalue()
4933

5034

5135
def array_to_binary(ar):

src/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,18 @@ export async function fromBytes(
151151
img.src = URL.createObjectURL(blob);
152152
});
153153
}
154+
155+
export async function bufferToImage(buffer: any): Promise<HTMLImageElement> {
156+
let url: string;
157+
158+
const blob = new Blob([buffer], { type: 'image/jpeg' });
159+
url = URL.createObjectURL(blob);
160+
161+
const img = new Image();
162+
return new Promise(resolve => {
163+
img.onload = () => {
164+
resolve(img);
165+
};
166+
img.src = url;
167+
});
168+
}

src/widget.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ import { RoughCanvas } from 'roughjs/bin/canvas';
1616

1717
import { MODULE_NAME, MODULE_VERSION } from './version';
1818

19-
import { getArg, toBytes, fromBytes, getTypedArray } from './utils';
19+
import {
20+
getArg,
21+
toBytes,
22+
fromBytes,
23+
getTypedArray,
24+
bufferToImage
25+
} from './utils';
2026

2127
function getContext(canvas: HTMLCanvasElement) {
2228
const context = canvas.getContext('2d');
@@ -280,7 +286,7 @@ export class CanvasManagerModel extends WidgetModel {
280286
await this.currentCanvas.drawImage(args, buffers);
281287
break;
282288
case 'putImageData':
283-
this.currentCanvas.putImageData(args, buffers);
289+
await this.currentCanvas.putImageData(args, buffers);
284290
break;
285291
case 'set':
286292
await this.currentCanvas.setAttr(args[0], args[1]);
@@ -1050,8 +1056,8 @@ export class CanvasModel extends DOMWidgetModel {
10501056
image: HTMLCanvasElement | HTMLImageElement,
10511057
x: number,
10521058
y: number,
1053-
width: number | undefined,
1054-
height: number | undefined
1059+
width?: number,
1060+
height?: number
10551061
) {
10561062
if (width === undefined || height === undefined) {
10571063
this.ctx.drawImage(image, x, y);
@@ -1060,22 +1066,12 @@ export class CanvasModel extends DOMWidgetModel {
10601066
}
10611067
}
10621068

1063-
putImageData(args: any[], buffers: any) {
1064-
const [bufferMetadata, dx, dy] = args;
1065-
1066-
const width = bufferMetadata.shape[1];
1067-
const height = bufferMetadata.shape[0];
1068-
1069-
const data = new Uint8ClampedArray(buffers[0].buffer);
1070-
const imageData = new ImageData(data, width, height);
1069+
async putImageData(args: any[], buffers: any) {
1070+
const [x, y] = args;
10711071

1072-
// Draw on a temporary off-screen canvas. This is a workaround for `putImageData` to support transparency.
1073-
const offscreenCanvas = document.createElement('canvas');
1074-
offscreenCanvas.width = width;
1075-
offscreenCanvas.height = height;
1076-
getContext(offscreenCanvas).putImageData(imageData, 0, 0);
1072+
const image = await bufferToImage(buffers[0]);
10771073

1078-
this.ctx.drawImage(offscreenCanvas, dx, dy);
1074+
this._drawImage(image, x, y);
10791075
}
10801076

10811077
async setAttr(attr: number, value: any) {

0 commit comments

Comments
 (0)