Skip to content

Commit d6633d6

Browse files
authored
Merge pull request #132 from martinRenou/comm_send_optim
Optimize custom comm messages
2 parents f827da2 + bdf75c1 commit d6633d6

File tree

5 files changed

+59
-44
lines changed

5 files changed

+59
-44
lines changed

ipycanvas/canvas.py

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from ._frontend import module_name, module_version
1818

19-
from .utils import binary_image, populate_args, image_bytes_to_array
19+
from .utils import binary_image, populate_args, image_bytes_to_array, commands_to_buffer
2020

2121
COMMANDS = {
2222
'fillRect': 0, 'strokeRect': 1, 'fillRects': 2, 'strokeRects': 3, 'clearRect': 4, 'fillArc': 5,
@@ -259,14 +259,14 @@ def fill_rect(self, x, y, width, height=None):
259259
if height is None:
260260
height = width
261261

262-
self._send_canvas_command(COMMANDS['fillRect'], (x, y, width, height))
262+
self._send_canvas_command(COMMANDS['fillRect'], [x, y, width, height])
263263

264264
def stroke_rect(self, x, y, width, height=None):
265265
"""Draw a rectangular outline of size ``(width, height)`` at the ``(x, y)`` position."""
266266
if height is None:
267267
height = width
268268

269-
self._send_canvas_command(COMMANDS['strokeRect'], (x, y, width, height))
269+
self._send_canvas_command(COMMANDS['strokeRect'], [x, y, width, height])
270270

271271
def fill_rects(self, x, y, width, height=None):
272272
"""Draw filled rectangles of sizes ``(width, height)`` at the ``(x, y)`` positions.
@@ -313,24 +313,24 @@ def clear_rect(self, x, y, width, height=None):
313313
if height is None:
314314
height = width
315315

316-
self._send_canvas_command(COMMANDS['clearRect'], (x, y, width, height))
316+
self._send_canvas_command(COMMANDS['clearRect'], [x, y, width, height])
317317

318318
# Arc methods
319319
def fill_arc(self, x, y, radius, start_angle, end_angle, anticlockwise=False):
320320
"""Draw a filled arc centered at ``(x, y)`` with a radius of ``radius`` from ``start_angle`` to ``end_angle``."""
321-
self._send_canvas_command(COMMANDS['fillArc'], (x, y, radius, start_angle, end_angle, anticlockwise))
321+
self._send_canvas_command(COMMANDS['fillArc'], [x, y, radius, start_angle, end_angle, anticlockwise])
322322

323323
def fill_circle(self, x, y, radius):
324324
"""Draw a filled circle centered at ``(x, y)`` with a radius of ``radius``."""
325-
self._send_canvas_command(COMMANDS['fillCircle'], (x, y, radius))
325+
self._send_canvas_command(COMMANDS['fillCircle'], [x, y, radius])
326326

327327
def stroke_arc(self, x, y, radius, start_angle, end_angle, anticlockwise=False):
328328
"""Draw an arc outline centered at ``(x, y)`` with a radius of ``radius``."""
329-
self._send_canvas_command(COMMANDS['strokeArc'], (x, y, radius, start_angle, end_angle, anticlockwise))
329+
self._send_canvas_command(COMMANDS['strokeArc'], [x, y, radius, start_angle, end_angle, anticlockwise])
330330

331331
def stroke_circle(self, x, y, radius):
332332
"""Draw a circle centered at ``(x, y)`` with a radius of ``radius``."""
333-
self._send_canvas_command(COMMANDS['strokeCircle'], (x, y, radius))
333+
self._send_canvas_command(COMMANDS['strokeCircle'], [x, y, radius])
334334

335335
def fill_arcs(self, x, y, radius, start_angle, end_angle, anticlockwise=False):
336336
"""Draw filled arcs centered at ``(x, y)`` with a radius of ``radius``.
@@ -397,7 +397,7 @@ def stroke_circles(self, x, y, radius):
397397
# Lines methods
398398
def stroke_line(self, x1, y1, x2, y2):
399399
"""Draw a line from ``(x1, y1)`` to ``(x2, y2)``."""
400-
self._send_canvas_command(COMMANDS['strokeLine'], (x1, y1, x2, y2))
400+
self._send_canvas_command(COMMANDS['strokeLine'], [x1, y1, x2, y2])
401401

402402
# Paths methods
403403
def begin_path(self):
@@ -422,48 +422,48 @@ def fill(self, rule_or_path='nonzero'):
422422
Possible rules are ``nonzero`` and ``evenodd``.
423423
"""
424424
if isinstance(rule_or_path, Path2D):
425-
self._send_canvas_command(COMMANDS['fillPath'], (widget_serialization['to_json'](rule_or_path, None), ))
425+
self._send_canvas_command(COMMANDS['fillPath'], [widget_serialization['to_json'](rule_or_path, None)])
426426
else:
427-
self._send_canvas_command(COMMANDS['fill'], (rule_or_path, ))
427+
self._send_canvas_command(COMMANDS['fill'], [rule_or_path])
428428

429429
def move_to(self, x, y):
430430
"""Move the "pen" to the given ``(x, y)`` coordinates."""
431-
self._send_canvas_command(COMMANDS['moveTo'], (x, y))
431+
self._send_canvas_command(COMMANDS['moveTo'], [x, y])
432432

433433
def line_to(self, x, y):
434434
"""Add a straight line to the current path by connecting the path's last point to the specified ``(x, y)`` coordinates.
435435
436436
Like other methods that modify the current path, this method does not directly render anything. To
437437
draw the path onto the canvas, you can use the fill() or stroke() methods.
438438
"""
439-
self._send_canvas_command(COMMANDS['lineTo'], (x, y))
439+
self._send_canvas_command(COMMANDS['lineTo'], [x, y])
440440

441441
def rect(self, x, y, width, height):
442442
"""Add a rectangle of size ``(width, height)`` at the ``(x, y)`` position in the current path."""
443-
self._send_canvas_command(COMMANDS['rect'], (x, y, width, height))
443+
self._send_canvas_command(COMMANDS['rect'], [x, y, width, height])
444444

445445
def arc(self, x, y, radius, start_angle, end_angle, anticlockwise=False):
446446
"""Add a circular arc centered at ``(x, y)`` with a radius of ``radius`` to the current path.
447447
448448
The path starts at ``start_angle`` and ends at ``end_angle``, and travels in the direction given by
449449
``anticlockwise`` (defaulting to clockwise: ``False``).
450450
"""
451-
self._send_canvas_command(COMMANDS['arc'], (x, y, radius, start_angle, end_angle, anticlockwise))
451+
self._send_canvas_command(COMMANDS['arc'], [x, y, radius, start_angle, end_angle, anticlockwise])
452452

453453
def ellipse(self, x, y, radius_x, radius_y, rotation, start_angle, end_angle, anticlockwise=False):
454454
"""Add an ellipse centered at ``(x, y)`` with the radii ``radius_x`` and ``radius_y`` to the current path.
455455
456456
The path starts at ``start_angle`` and ends at ``end_angle``, and travels in the direction given by
457457
``anticlockwise`` (defaulting to clockwise: ``False``).
458458
"""
459-
self._send_canvas_command(COMMANDS['ellipse'], (x, y, radius_x, radius_y, rotation, start_angle, end_angle, anticlockwise))
459+
self._send_canvas_command(COMMANDS['ellipse'], [x, y, radius_x, radius_y, rotation, start_angle, end_angle, anticlockwise])
460460

461461
def arc_to(self, x1, y1, x2, y2, radius):
462462
"""Add a circular arc to the current path.
463463
464464
Using the given control points ``(x1, y1)`` and ``(x2, y2)`` and the ``radius``.
465465
"""
466-
self._send_canvas_command(COMMANDS['arcTo'], (x1, y1, x2, y2, radius))
466+
self._send_canvas_command(COMMANDS['arcTo'], [x1, y1, x2, y2, radius])
467467

468468
def quadratic_curve_to(self, cp1x, cp1y, x, y):
469469
"""Add a quadratic Bezier curve to the current path.
@@ -472,7 +472,7 @@ def quadratic_curve_to(self, cp1x, cp1y, x, y):
472472
The starting point is the latest point in the current path, which can be changed using move_to()
473473
before creating the quadratic Bezier curve.
474474
"""
475-
self._send_canvas_command(COMMANDS['quadraticCurveTo'], (cp1x, cp1y, x, y))
475+
self._send_canvas_command(COMMANDS['quadraticCurveTo'], [cp1x, cp1y, x, y])
476476

477477
def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y):
478478
"""Add a cubic Bezier curve to the current path.
@@ -481,16 +481,16 @@ def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y):
481481
The starting point is the latest point in the current path, which can be changed using move_to()
482482
before creating the Bezier curve.
483483
"""
484-
self._send_canvas_command(COMMANDS['bezierCurveTo'], (cp1x, cp1y, cp2x, cp2y, x, y))
484+
self._send_canvas_command(COMMANDS['bezierCurveTo'], [cp1x, cp1y, cp2x, cp2y, x, y])
485485

486486
# Text methods
487487
def fill_text(self, text, x, y, max_width=None):
488488
"""Fill a given text at the given ``(x, y)`` position. Optionally with a maximum width to draw."""
489-
self._send_canvas_command(COMMANDS['fillText'], (text, x, y, max_width))
489+
self._send_canvas_command(COMMANDS['fillText'], [text, x, y, max_width])
490490

491491
def stroke_text(self, text, x, y, max_width=None):
492492
"""Stroke a given text at the given ``(x, y)`` position. Optionally with a maximum width to draw."""
493-
self._send_canvas_command(COMMANDS['strokeText'], (text, x, y, max_width))
493+
self._send_canvas_command(COMMANDS['strokeText'], [text, x, y, max_width])
494494

495495
# Line methods
496496
def get_line_dash(self):
@@ -503,7 +503,7 @@ def set_line_dash(self, segments):
503503
self._line_dash = segments + segments
504504
else:
505505
self._line_dash = segments
506-
self._send_canvas_command(COMMANDS['setLineDash'], (self._line_dash, ))
506+
self._send_canvas_command(COMMANDS['setLineDash'], [self._line_dash])
507507

508508
# Image methods
509509
def draw_image(self, image, x=0, y=0, width=None, height=None):
@@ -516,7 +516,7 @@ def draw_image(self, image, x=0, y=0, width=None, height=None):
516516

517517
serialized_image = widget_serialization['to_json'](image, None)
518518

519-
self._send_canvas_command(COMMANDS['drawImage'], (serialized_image, x, y, width, height))
519+
self._send_canvas_command(COMMANDS['drawImage'], [serialized_image, x, y, width, height])
520520

521521
def put_image_data(self, image_data, x=0, y=0):
522522
"""Draw an image on the Canvas.
@@ -526,7 +526,7 @@ def put_image_data(self, image_data, x=0, y=0):
526526
matrix, and supports transparency.
527527
"""
528528
image_metadata, image_buffer = binary_image(image_data)
529-
self._send_canvas_command(COMMANDS['putImageData'], (image_metadata, x, y), (image_buffer, ))
529+
self._send_canvas_command(COMMANDS['putImageData'], [image_metadata, x, y], [image_buffer])
530530

531531
def create_image_data(self, width, height):
532532
"""Create a NumPy array of shape (width, height, 4) representing a table of pixel colors."""
@@ -556,11 +556,11 @@ def translate(self, x, y):
556556
``x`` indicates the horizontal distance to move,
557557
and ``y`` indicates how far to move the grid vertically.
558558
"""
559-
self._send_canvas_command(COMMANDS['translate'], (x, y))
559+
self._send_canvas_command(COMMANDS['translate'], [x, y])
560560

561561
def rotate(self, angle):
562562
"""Rotate the canvas clockwise around the current origin by the ``angle`` number of radians."""
563-
self._send_canvas_command(COMMANDS['rotate'], (angle, ))
563+
self._send_canvas_command(COMMANDS['rotate'], [angle])
564564

565565
def scale(self, x, y=None):
566566
"""Scale the canvas units by ``x`` horizontally and by ``y`` vertically. Both parameters are real numbers.
@@ -571,22 +571,22 @@ def scale(self, x, y=None):
571571
"""
572572
if y is None:
573573
y = x
574-
self._send_canvas_command(COMMANDS['scale'], (x, y))
574+
self._send_canvas_command(COMMANDS['scale'], [x, y])
575575

576576
def transform(self, a, b, c, d, e, f):
577577
"""Multiply the current transformation matrix with the matrix described by its arguments.
578578
579579
The transformation matrix is described by:
580580
``[[a, c, e], [b, d, f], [0, 0, 1]]``.
581581
"""
582-
self._send_canvas_command(COMMANDS['transform'], (a, b, c, d, e, f))
582+
self._send_canvas_command(COMMANDS['transform'], [a, b, c, d, e, f])
583583

584584
def set_transform(self, a, b, c, d, e, f):
585585
"""Reset the current transform to the identity matrix, and then invokes the transform() method with the same arguments.
586586
587587
This basically undoes the current transformation, then sets the specified transform, all in one step.
588588
"""
589-
self._send_canvas_command(COMMANDS['setTransform'], (a, b, c, d, e, f))
589+
self._send_canvas_command(COMMANDS['setTransform'], [a, b, c, d, e, f])
590590

591591
def reset_transform(self):
592592
"""Reset the current transform to the identity matrix.
@@ -605,7 +605,7 @@ def flush(self):
605605
if not self.caching or not len(self._commands_cache):
606606
return
607607

608-
self.send(self._commands_cache, self._buffers_cache)
608+
self._send_custom(self._commands_cache, self._buffers_cache)
609609

610610
self._commands_cache = []
611611
self._buffers_cache = []
@@ -660,22 +660,20 @@ def __setattr__(self, name, value):
660660
self._send_command([COMMANDS['set'], [self.ATTRS[name], value]])
661661

662662
def _send_canvas_command(self, name, args=[], buffers=[]):
663-
command = [name]
664-
665-
if len(args):
666-
command.append([arg for arg in args if arg is not None])
667-
668-
if len(buffers):
669-
command.append(len(buffers))
670-
671-
self._send_command(command, buffers)
663+
while len(args) and args[len(args) - 1] is None:
664+
args.pop()
665+
self._send_command([name, args, len(buffers)], buffers)
672666

673667
def _send_command(self, command, buffers=[]):
674668
if self.caching:
675669
self._commands_cache.append(command)
676670
self._buffers_cache += buffers
677671
else:
678-
self.send(command, buffers)
672+
self._send_custom(command, buffers)
673+
674+
def _send_custom(self, command, buffers=[]):
675+
metadata, command_buffer = commands_to_buffer(command)
676+
self.send(metadata, buffers=[command_buffer] + buffers)
679677

680678
def _handle_frontend_event(self, _, content, buffers):
681679
if content.get('event', '') == 'client_ready':

ipycanvas/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import numpy as np
77

8+
import orjson
9+
810

911
def image_bytes_to_array(im_bytes):
1012
"""Turn raw image bytes into a NumPy array."""
@@ -65,3 +67,11 @@ def populate_args(arg, args, buffers):
6567
buffers.append(arg_buffer)
6668
else:
6769
args.append(arg)
70+
71+
72+
def commands_to_buffer(commands):
73+
# Turn the commands list into a binary buffer
74+
return array_to_binary(np.frombuffer(
75+
bytes(orjson.dumps(commands, option=orjson.OPT_SERIALIZE_NUMPY)),
76+
dtype=np.uint8)
77+
)

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@
8787
install_requires = [
8888
'ipywidgets>=7.5.0',
8989
'pillow>=6.0',
90-
'numpy'
90+
'numpy',
91+
'orjson'
9192
],
9293
extras_require = {
9394
'examples': [

src/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
function getTypedArray(dataview: any, metadata: any) {
1+
export function getTypedArray(dataview: any, metadata: any) {
22
switch (metadata.dtype) {
33
case 'int8':
44
return new Int8Array(dataview.buffer);
55
break;
6+
case 'uint8':
7+
return new Uint8Array(dataview.buffer);
8+
break;
69
case 'int16':
710
return new Int16Array(dataview.buffer);
811
break;

src/widget.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from './version';
1515

1616
import {
17-
getArg, toBytes, fromBytes
17+
getArg, toBytes, fromBytes, getTypedArray
1818
} from './utils';
1919

2020

@@ -141,7 +141,10 @@ class CanvasModel extends DOMWidgetModel {
141141
}
142142

143143
private async onCommand(command: any, buffers: any) {
144-
await this.processCommand(command, buffers);
144+
// Retrieve the commands buffer as an object (list of commands)
145+
const commands = JSON.parse(Buffer.from(getTypedArray(buffers[0], command)).toString('utf-8'));
146+
147+
await this.processCommand(commands, buffers.slice(1, buffers.length));
145148

146149
this.forEachView((view: CanvasView) => {
147150
view.updateCanvas();

0 commit comments

Comments
 (0)