pub mod backend;
pub mod comm;
pub mod quarto;
pub mod sizing;
pub use backend::{PlotBackend, RenderOutcome};
pub use sizing::Canvas;
use crate::display::SessionKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Png,
Jpeg,
Svg,
Pdf,
Tiff,
}
impl Format {
pub fn mime(self) -> &'static str {
match self {
Self::Png => "image/png",
Self::Jpeg => "image/jpeg",
Self::Svg => "image/svg+xml",
Self::Pdf => "application/pdf",
Self::Tiff => "image/tiff",
}
}
pub fn is_text(self) -> bool {
matches!(self, Self::Svg)
}
pub fn needs_raster(self) -> bool {
matches!(self, Self::Png | Self::Jpeg | Self::Tiff)
}
pub fn available(self, backend_raster: bool) -> Self {
if self.needs_raster() && !backend_raster {
Self::Svg
} else {
self
}
}
}
#[derive(Debug, Clone)]
pub struct RenderTicket {
pub comm_id: String,
pub rpc_id: serde_json::Value,
pub params: comm::RenderParams,
pub parent: crate::message::JupyterMessage,
pub identities: Vec<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RenderRequest {
pub format: Format,
pub canvas: Canvas,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Delivery {
Static(RenderRequest),
Comm,
}
pub fn choose(kind: SessionKind, backend_raster: bool, canvas: Canvas) -> Delivery {
if kind == SessionKind::PositronConsole {
return Delivery::Comm;
}
if kind == SessionKind::Standalone {
if let Some(figure) = quarto::from_env() {
return Delivery::Static(RenderRequest {
format: figure.format.available(backend_raster),
canvas: figure.canvas,
});
}
}
Delivery::Static(RenderRequest {
format: Format::Png.available(backend_raster),
canvas,
})
}
#[cfg(test)]
mod tests {
use super::*;
const CANVAS: Canvas = Canvas {
width: 800,
height: 600,
dpi: 96.0,
};
#[test]
fn a_console_session_always_opens_a_plot_comm() {
for backend_raster in [true, false] {
assert_eq!(
choose(SessionKind::PositronConsole, backend_raster, CANVAS),
Delivery::Comm,
"backend_raster={backend_raster}"
);
}
}
#[test]
fn a_format_degrades_only_when_it_needs_a_writer_this_build_lacks() {
for format in [Format::Png, Format::Jpeg, Format::Tiff] {
assert_eq!(format.available(false), Format::Svg, "{format:?}");
assert_eq!(format.available(true), format, "{format:?}");
}
for format in [Format::Svg, Format::Pdf] {
assert_eq!(format.available(false), format, "{format:?}");
}
}
#[test]
fn a_notebook_gets_a_static_image() {
let Delivery::Static(request) = choose(SessionKind::PositronNotebook, true, CANVAS) else {
panic!("a notebook should get a static bundle");
};
assert_eq!(request.format, Format::Png);
assert_eq!(request.canvas, CANVAS);
}
#[test]
fn svg_is_the_fallback_wherever_raster_is_unavailable() {
for kind in [SessionKind::PositronNotebook, SessionKind::Standalone] {
let Delivery::Static(request) = choose(kind, false, CANVAS) else {
panic!("{kind:?} should get a static bundle");
};
assert_eq!(request.format, Format::Svg, "{kind:?}");
assert_eq!(request.canvas, CANVAS, "{kind:?}");
}
}
#[test]
fn every_format_has_positrons_mime_type() {
assert_eq!(Format::Png.mime(), "image/png");
assert_eq!(Format::Jpeg.mime(), "image/jpeg");
assert_eq!(Format::Svg.mime(), "image/svg+xml");
assert_eq!(Format::Pdf.mime(), "application/pdf");
assert_eq!(Format::Tiff.mime(), "image/tiff");
}
#[test]
fn only_svg_travels_as_text() {
assert!(Format::Svg.is_text());
for format in [Format::Png, Format::Jpeg, Format::Pdf, Format::Tiff] {
assert!(!format.is_text(), "{format:?}");
}
}
#[test]
fn the_vector_formats_need_no_adapter() {
assert!(!Format::Svg.needs_raster());
assert!(!Format::Pdf.needs_raster());
for format in [Format::Png, Format::Jpeg, Format::Tiff] {
assert!(format.needs_raster(), "{format:?}");
}
}
}