Skip to content

Commit 569116a

Browse files
committed
Fallback to cairo graphics in webR
Handle the case where the `OffscreenCanvas` API is unavailable.
1 parent 6f4334e commit 569116a

File tree

3 files changed

+132
-43
lines changed

3 files changed

+132
-43
lines changed

_extensions/live/resources/live-runtime.js

Lines changed: 52 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

live-runtime/src/evaluate-webr.ts

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { WebREnvironmentManager } from './environment';
22
import { Indicator } from './indicator';
33
import { highlightR } from './highlighter';
44
import { renderHtmlDependency } from './render';
5-
import { isRList, isRObject, isRFunction, isRCall, isRNull } from 'webr';
5+
import { isRList, isRObject, isRFunction, isRCall, isRNull, isRRaw } from 'webr';
66
import type {
77
RCall,
88
RCharacter,
@@ -25,6 +25,7 @@ import {
2525
ExerciseEvaluator,
2626
OJSElement
2727
} from "./evaluate";
28+
import { arrayBufferToBase64 } from './utils';
2829

2930
declare global {
3031
interface Window {
@@ -91,7 +92,7 @@ export class WebREvaluator implements ExerciseEvaluator {
9192
const shelter = await this.shelter;
9293
shelter.purge();
9394
}
94-
95+
9596
getSetupCode(): string | undefined {
9697
const exId = this.options.exercise;
9798
const setup = document.querySelectorAll(
@@ -358,6 +359,17 @@ export class WebREvaluator implements ExerciseEvaluator {
358359
}
359360
};
360361

362+
const appendDataUrlImage = async (mime: string, data: string) => {
363+
if (options.output) {
364+
const outputDiv = document.createElement("div");
365+
const imageDiv = document.createElement("img");
366+
outputDiv.className = "cell-output-display cell-output-pyodide";
367+
imageDiv.src = `data:${mime};base64, ${data}`;
368+
outputDiv.appendChild(imageDiv);
369+
container.appendChild(outputDiv);
370+
}
371+
};
372+
361373
const shelter = await this.shelter;
362374
const result = await value.toArray() as RObject[];
363375
for (let i = 0; i < result.length; i++) {
@@ -405,11 +417,27 @@ export class WebREvaluator implements ExerciseEvaluator {
405417
height = Number(this.options["fig-height"]);
406418
}
407419

408-
const capturePlot = await shelter.captureR("replayPlot(plot)", {
409-
captureGraphics: { width, height },
410-
env: { plot: result[i] },
411-
});
412-
appendImage(capturePlot.images[0]);
420+
if (typeof OffscreenCanvas !== "undefined") {
421+
const capturePlot = await shelter.captureR("replayPlot(plot)", {
422+
captureGraphics: { width, height },
423+
env: { plot: result[i] },
424+
});
425+
appendImage(capturePlot.images[0]);
426+
} else {
427+
// Fallback to cairo graphics
428+
const data = await shelter.evalR(`
429+
tmp_dir <- tempdir()
430+
on.exit(unlink(tmp_dir, recursive = TRUE))
431+
filename <- paste0(tmp_dir, ".webr-plot.png")
432+
png(file = filename, width = width, height = height)
433+
replayPlot(plot)
434+
dev.off()
435+
filesize <- file.info(filename)[["size"]]
436+
readBin(filename, "raw", n = filesize)
437+
`, { env: { plot: result[i], width, height } }) as RRaw;
438+
const bytes = await data.toTypedArray();
439+
appendDataUrlImage("image/png", arrayBufferToBase64(bytes));
440+
}
413441
}
414442
break;
415443
}
@@ -459,15 +487,53 @@ export class WebREvaluator implements ExerciseEvaluator {
459487
try {
460488
const width = await this.webR.evalRNumber('72 * getOption("webr.fig.width")');
461489
const height = await this.webR.evalRNumber('72 * getOption("webr.fig.height")');
490+
let images: (ImageBitmap | HTMLImageElement)[] = [];
491+
492+
const hasOffscreenCanvas = typeof OffscreenCanvas !== "undefined";
493+
if (!hasOffscreenCanvas) {
494+
// Fallback to cairo graphics
495+
this.webR.evalRVoid(`
496+
while (dev.cur() > 1) dev.off()
497+
options(device = function() {
498+
png(file = "/tmp/.webr-plot.png", width = width, height = height)
499+
})
500+
`, {
501+
env: { width, height },
502+
});
503+
}
504+
462505
const capture = await robj.capture(
463506
{
464507
withAutoprint: true,
465-
captureGraphics: { width, height },
508+
captureGraphics: hasOffscreenCanvas ? { width, height } : false
466509
},
467510
...args
468511
);
469-
if (capture.images.length) {
470-
const el = await this.asOjs(capture.images[capture.images.length - 1]);
512+
513+
if (hasOffscreenCanvas) {
514+
images = capture.images;
515+
} else {
516+
// Fallback to canvas graphics
517+
const data = await this.webR.evalR(`
518+
while (dev.cur() > 1) dev.off()
519+
options(device = getOption("webr.device"))
520+
filename <- "/tmp/.webr-plot.png"
521+
if (file.exists(filename)) {
522+
filesize <- file.info(filename)[["size"]]
523+
readBin(filename, "raw", n = filesize)
524+
} else NULL
525+
`) as RRaw | RNull;
526+
527+
if (isRRaw(data)) {
528+
const bytes = await data.toTypedArray();
529+
const imageDiv = document.createElement("img");
530+
imageDiv.src = `data:image/png;base64, ${arrayBufferToBase64(bytes)}`;
531+
images = [ imageDiv ];
532+
}
533+
}
534+
535+
if (images.length) {
536+
const el = await this.asOjs(images[images.length - 1]);
471537
el.value = await this.asOjs(capture.result);
472538
return el;
473539
}
@@ -488,11 +554,11 @@ export class WebREvaluator implements ExerciseEvaluator {
488554
if (classes.includes('knit_asis')) {
489555
const html = await robj.toString();
490556
const meta = await (await robj.attrs()).get("knit_meta") as RList | RNull;
491-
557+
492558
const outputDiv = document.createElement("div");
493559
outputDiv.className = "cell-output";
494560
outputDiv.innerHTML = html;
495-
561+
496562
// Dynamically load any dependencies into page (await & maintain ordering)
497563
if (isRList(meta)) {
498564
const deps = await meta.toArray();

live-runtime/src/scripts/R/setup.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ ojs_define <- function(...) {
88

99
# webR graphics device settings
1010
options(webr.fig.width = 7, webr.fig.height = 5)
11-
options(device = function(...) {
11+
options(webr.device = function(...) {
1212
args <- list(bg = "white", ...)
1313
args <- args[!duplicated(names(args))]
1414
do.call(webr::canvas, args)
1515
})
16+
options(device = getOption("webr.device"))
1617

1718
# Custom pager for displaying e.g. help pages
1819
options(pager = function(files, ...) {

0 commit comments

Comments
 (0)