Skip to content

Commit 480a236

Browse files
authored
Merge pull request #406 from martinRenou/image_embedding_refactor
Refactor image embedding logic
2 parents 84bf0aa + 9aad6fc commit 480a236

File tree

2 files changed

+102
-35
lines changed

2 files changed

+102
-35
lines changed

ipympl/backend_nbagg.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
Float,
5252
Instance,
5353
List,
54+
Tuple,
5455
Unicode,
5556
default,
5657
)
@@ -184,8 +185,7 @@ class Canvas(DOMWidget, FigureCanvasWebAggCore):
184185
# This will still be used by ipywidgets in the case of embedding.
185186
_data_url = Any(None).tag(sync=True)
186187

187-
_width = CInt().tag(sync=True)
188-
_height = CInt().tag(sync=True)
188+
_size = Tuple([0, 0]).tag(sync=True)
189189

190190
_figure_label = Unicode('Figure').tag(sync=True)
191191
_message = Unicode().tag(sync=True)
@@ -265,9 +265,11 @@ def send_json(self, content):
265265
self._figure_label = content['label']
266266

267267
elif content['type'] == 'resize':
268-
self._width = content['size'][0]
269-
self._height = content['size'][1]
270-
# Send resize message anyway
268+
self._size = content['size']
269+
# Send resize message anyway:
270+
# We absolutely need this instead of a `_size` trait change listening
271+
# on the front-end, otherwise ipywidgets might squash multiple changes
272+
# and the resizing protocol is not respected anymore
271273
self.send({'data': json.dumps(content)})
272274

273275
elif content['type'] == 'image_mode':
@@ -306,28 +308,36 @@ def _repr_mimebundle_(self, **kwargs):
306308

307309
buf = io.BytesIO()
308310
self.figure.savefig(buf, format='png', dpi='figure')
309-
self._data_url = b64encode(buf.getvalue()).decode('utf-8')
310-
# Figure width in pixels
311+
312+
base64_image = b64encode(buf.getvalue()).decode('utf-8')
313+
self._data_url = f'data:image/png;base64,{base64_image}'
314+
# Figure size in pixels
311315
pwidth = self.figure.get_figwidth() * self.figure.get_dpi()
316+
pheight = self.figure.get_figheight() * self.figure.get_dpi()
312317
# Scale size to match widget on HiDPI monitors.
313318
if hasattr(self, 'device_pixel_ratio'): # Matplotlib 3.5+
314319
width = pwidth / self.device_pixel_ratio
320+
height = pheight / self.device_pixel_ratio
315321
else:
316322
width = pwidth / self._dpi_ratio
323+
height = pheight / self._dpi_ratio
317324
html = """
318325
<div style="display: inline-block;">
319326
<div class="jupyter-widgets widget-label" style="text-align: center;">
320327
{}
321328
</div>
322-
<img src='data:image/png;base64,{}' width={}/>
329+
<img src='{}' width={}/>
323330
</div>
324331
""".format(
325332
self._figure_label, self._data_url, width
326333
)
327334

335+
# Update the widget model properly for HTML embedding
336+
self._size = (width, height)
337+
328338
data = {
329339
'text/plain': plaintext,
330-
'image/png': self._data_url,
340+
'image/png': base64_image,
331341
'text/html': html,
332342
'application/vnd.jupyter.widget-view+json': {
333343
'version_major': 2,

src/mpl_widget.ts

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ export class MPLCanvasModel extends DOMWidgetModel {
3939
capture_scroll: false,
4040
pan_zoom_throttle: 33,
4141
_data_url: null,
42-
_width: 0,
43-
_height: 0,
42+
_size: [0, 0],
4443
_figure_label: 'Figure',
4544
_message: '',
4645
_cursor: 'pointer',
@@ -78,7 +77,7 @@ export class MPLCanvasModel extends DOMWidgetModel {
7877
this.resize_requested = false;
7978
this.ratio = (window.devicePixelRatio || 1) / backingStore;
8079

81-
this.resize_canvas(this.get('_width'), this.get('_height'));
80+
this.resize_canvas();
8281

8382
this._init_image();
8483

@@ -88,13 +87,21 @@ export class MPLCanvasModel extends DOMWidgetModel {
8887
view.update_canvas();
8988
});
9089
});
90+
this.on('change:_size', () => {
91+
this.resize_canvas();
92+
this.offscreen_context.drawImage(this.image, 0, 0);
93+
});
9194
this.on('comm_live_update', this.update_disabled.bind(this));
9295

9396
this.update_disabled();
9497

9598
this.send_initialization_message();
9699
}
97100

101+
get size(): [number, number] {
102+
return this.get('_size');
103+
}
104+
98105
get disabled(): boolean {
99106
return !this.comm_live;
100107
}
@@ -149,13 +156,12 @@ export class MPLCanvasModel extends DOMWidgetModel {
149156
}
150157

151158
handle_resize(msg: { [index: string]: any }) {
152-
const size = msg['size'];
153-
this.resize_canvas(size[0], size[1]);
159+
this.resize_canvas();
154160
this.offscreen_context.drawImage(this.image, 0, 0);
155161

156162
if (!this.resize_requested) {
157163
this._for_each_view((view: MPLCanvasView) => {
158-
view.resize_canvas(size[0], size[1]);
164+
view.resize_and_update_canvas(this.size);
159165
});
160166
}
161167

@@ -169,6 +175,9 @@ export class MPLCanvasModel extends DOMWidgetModel {
169175
}
170176
}
171177

178+
/*
179+
* Request a resize to the backend
180+
*/
172181
resize(width: number, height: number) {
173182
// Do not request a super small size, as it seems to break the back-end
174183
if (width <= 5 || height <= 5) {
@@ -177,7 +186,7 @@ export class MPLCanvasModel extends DOMWidgetModel {
177186

178187
this._for_each_view((view: MPLCanvasView) => {
179188
// Do an initial resize of each view, stretching the old canvas.
180-
view.resize_canvas(width, height);
189+
view.resize_and_update_canvas([width, height]);
181190
});
182191

183192
if (this.resize_requested) {
@@ -189,9 +198,12 @@ export class MPLCanvasModel extends DOMWidgetModel {
189198
}
190199
}
191200

192-
resize_canvas(width: number, height: number) {
193-
this.offscreen_canvas.width = width * this.ratio;
194-
this.offscreen_canvas.height = height * this.ratio;
201+
/*
202+
* Resize the offscreen canvas
203+
*/
204+
resize_canvas() {
205+
this.offscreen_canvas.width = this.size[0] * this.ratio;
206+
this.offscreen_canvas.height = this.size[1] * this.ratio;
195207
}
196208

197209
handle_rubberband(msg: any) {
@@ -275,17 +287,49 @@ export class MPLCanvasModel extends DOMWidgetModel {
275287
this.image = new Image();
276288

277289
this.image.onload = () => {
290+
// In case of an embedded widget, the initial size is not correct
291+
// and we are not receiving any resize event from the server
292+
if (this.disabled) {
293+
this.offscreen_canvas.width = this.image.width;
294+
this.offscreen_canvas.height = this.image.height;
295+
296+
this.offscreen_context.drawImage(this.image, 0, 0);
297+
298+
this._for_each_view((view: MPLCanvasView) => {
299+
// TODO Make this part of the CanvasView API?
300+
// It feels out of place in the model
301+
view.canvas.width = this.image.width / this.ratio;
302+
view.canvas.height = this.image.height / this.ratio;
303+
view.canvas.style.width = view.canvas.width + 'px';
304+
view.canvas.style.height = view.canvas.height + 'px';
305+
306+
view.top_canvas.width = this.image.width / this.ratio;
307+
view.top_canvas.height = this.image.height / this.ratio;
308+
view.top_canvas.style.width = view.top_canvas.width + 'px';
309+
view.top_canvas.style.height =
310+
view.top_canvas.height + 'px';
311+
312+
view.canvas_div.style.width = view.canvas.width + 'px';
313+
view.canvas_div.style.height = view.canvas.height + 'px';
314+
315+
view.update_canvas(true);
316+
});
317+
318+
return;
319+
}
320+
321+
// Full images could contain transparency (where diff images
322+
// almost always do), so we need to clear the canvas so that
323+
// there is no ghosting.
278324
if (this.get('_image_mode') === 'full') {
279-
// Full images could contain transparency (where diff images
280-
// almost always do), so we need to clear the canvas so that
281-
// there is no ghosting.
282325
this.offscreen_context.clearRect(
283326
0,
284327
0,
285328
this.offscreen_canvas.width,
286329
this.offscreen_canvas.height
287330
);
288331
}
332+
289333
this.offscreen_context.drawImage(this.image, 0, 0);
290334

291335
this._for_each_view((view: MPLCanvasView) => {
@@ -556,19 +600,32 @@ export class MPLCanvasView extends DOMWidgetView {
556600
return false;
557601
});
558602

559-
this.resize_canvas(this.model.get('_width'), this.model.get('_height'));
560-
this.update_canvas();
603+
this.resize_and_update_canvas(this.model.size);
561604
}
562605

563-
update_canvas() {
606+
/*
607+
* Update the canvas view
608+
*/
609+
update_canvas(stretch = false) {
564610
if (this.canvas.width === 0 || this.canvas.height === 0) {
565611
return;
566612
}
567613

568614
this.top_context.save();
569615

570616
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
571-
this.context.drawImage(this.model.offscreen_canvas, 0, 0);
617+
618+
if (stretch) {
619+
this.context.drawImage(
620+
this.model.offscreen_canvas,
621+
0,
622+
0,
623+
this.canvas.width,
624+
this.canvas.height
625+
);
626+
} else {
627+
this.context.drawImage(this.model.offscreen_canvas, 0, 0);
628+
}
572629

573630
this.top_context.clearRect(
574631
0,
@@ -651,18 +708,18 @@ export class MPLCanvasView extends DOMWidgetView {
651708
this.footer.textContent = this.model.get('_message');
652709
}
653710

654-
resize_canvas(width: number, height: number) {
711+
resize_and_update_canvas(size: [number, number]) {
655712
// Keep the size of the canvas, and rubber band canvas in sync.
656-
this.canvas.setAttribute('width', `${width * this.model.ratio}`);
657-
this.canvas.setAttribute('height', `${height * this.model.ratio}`);
658-
this.canvas.style.width = width + 'px';
659-
this.canvas.style.height = height + 'px';
713+
this.canvas.setAttribute('width', `${size[0] * this.model.ratio}`);
714+
this.canvas.setAttribute('height', `${size[1] * this.model.ratio}`);
715+
this.canvas.style.width = size[0] + 'px';
716+
this.canvas.style.height = size[1] + 'px';
660717

661-
this.top_canvas.setAttribute('width', String(width));
662-
this.top_canvas.setAttribute('height', String(height));
718+
this.top_canvas.setAttribute('width', String(size[0]));
719+
this.top_canvas.setAttribute('height', String(size[1]));
663720

664-
this.canvas_div.style.width = width + 'px';
665-
this.canvas_div.style.height = height + 'px';
721+
this.canvas_div.style.width = size[0] + 'px';
722+
this.canvas_div.style.height = size[1] + 'px';
666723

667724
this.update_canvas();
668725
}

0 commit comments

Comments
 (0)