@@ -2,7 +2,7 @@ import { WebREnvironmentManager } from './environment';
22import { Indicator } from './indicator' ;
33import { highlightR } from './highlighter' ;
44import { renderHtmlDependency } from './render' ;
5- import { isRList , isRObject , isRFunction , isRCall , isRNull } from 'webr' ;
5+ import { isRList , isRObject , isRFunction , isRCall , isRNull , isRRaw } from 'webr' ;
66import type {
77 RCall ,
88 RCharacter ,
@@ -25,6 +25,7 @@ import {
2525 ExerciseEvaluator ,
2626 OJSElement
2727} from "./evaluate" ;
28+ import { arrayBufferToBase64 } from './utils' ;
2829
2930declare 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 ( ) ;
0 commit comments