use std::fmt::Write as _;
use super::{Graphics, PixelCanvas, Protocol, iterm, kitty, sixel};
use crate::plot::{Frame, Plot};
use crate::render::PlotRect;
pub(crate) fn render(plot: &Plot<'_>, frame: &Frame, graphics: &Graphics, column: usize) -> String {
try_render(plot, frame, graphics, column)
.unwrap_or_else(|_| at_column(&plot.render(frame), column).unwrap_or_default())
}
pub(crate) fn try_render(
plot: &Plot<'_>,
frame: &Frame,
graphics: &Graphics,
column: usize,
) -> crate::Result<String> {
try_render_mapped(plot, frame, graphics, column, None).map(|(block, _)| block)
}
pub(crate) fn try_render_mapped(
plot: &Plot<'_>,
frame: &Frame,
graphics: &Graphics,
column: usize,
kitty_id: Option<u32>,
) -> crate::Result<(String, crate::plot::Mapping)> {
let cell = (graphics.cell_size.0 as usize, graphics.cell_size.1 as usize);
validate_geometry(frame, graphics.protocol, cell)?;
if cell.0 == 0 || cell.1 == 0 {
let block = at_column(&plot.try_render_unvalidated(frame)?, column)?;
return Ok((block, plot.mapping(frame)));
}
let (surface, canvas, rect, mapping) =
plot.try_rasterize_hybrid(frame, cell, graphics.stroke, None)?;
let mut out = at_column(&surface.try_encode_full_width(frame.color)?, column)?;
if rect.columns == 0 || rect.rows == 0 {
return Ok((out, mapping));
}
let payload = if graphics.protocol == Protocol::Sixel {
let image = crop(&canvas, rect)?;
if image.width == 0 || image.height == 0 {
return Ok((out, mapping));
}
sixel::encode(&image)
} else {
let (width, height, rgba) = crop_rgba(&canvas, rect)?;
if width == 0 || height == 0 {
return Ok((out, mapping));
}
match graphics.protocol {
Protocol::Kitty => {
kitty::encode_rgba(width, height, (rect.columns, rect.rows), &rgba, kitty_id)
}
Protocol::ITerm2 => iterm::encode_rgba(width, height, rect.columns, rect.rows, &rgba),
Protocol::Sixel => unreachable!("sixel took the image path above"),
}
};
let extra = payload
.len()
.checked_add(96)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel output bytes",
requested: usize::MAX,
limit: crate::render::MAX_OUTPUT_BYTES,
})?;
let total = out
.len()
.checked_add(extra)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel output bytes",
requested: usize::MAX,
limit: crate::render::MAX_OUTPUT_BYTES,
})?;
crate::render::checked_dimension("pixel output bytes", total, crate::render::MAX_OUTPUT_BYTES)?;
crate::render::reserve_string(&mut out, extra, "pixel output")?;
out.push_str("\x1b7");
let anchor = column
.checked_add(1)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel column anchor",
requested: usize::MAX,
limit: usize::MAX - 1,
})?;
let _ = write!(out, "\x1b[{anchor}G");
let up = frame.height - 1 - rect.top;
if up > 0 {
let _ = write!(out, "\x1b[{up}A");
}
if rect.gutter > 0 {
let _ = write!(out, "\x1b[{}C", rect.gutter);
}
out.push_str(&payload);
out.push_str("\x1b8");
Ok((out, mapping))
}
fn validate_geometry(frame: &Frame, protocol: Protocol, cell: (usize, usize)) -> crate::Result<()> {
crate::render::frame_cells(frame.width, frame.height)?;
if cell.0 == 0 || cell.1 == 0 {
return Ok(());
}
let width = frame
.width
.checked_mul(cell.0)
.ok_or(crate::Error::DimensionTooLarge {
what: "device-pixel width",
requested: usize::MAX,
limit: crate::render::MAX_DEVICE_PIXELS,
})?;
let height = frame
.height
.checked_mul(cell.1)
.ok_or(crate::Error::DimensionTooLarge {
what: "device-pixel height",
requested: usize::MAX,
limit: crate::render::MAX_DEVICE_PIXELS,
})?;
let pixels = crate::render::checked_area(
"device-pixel count",
width,
height,
crate::render::MAX_DEVICE_PIXELS,
)?;
let factor = match protocol {
Protocol::Sixel => 64,
Protocol::Kitty | Protocol::ITerm2 => 6,
};
let estimated = pixels
.checked_mul(factor)
.and_then(|bytes| {
frame
.height
.checked_mul(32)
.and_then(|rows| bytes.checked_add(rows))
})
.and_then(|bytes| bytes.checked_add(65_536))
.ok_or(crate::Error::DimensionTooLarge {
what: "estimated pixel output bytes",
requested: usize::MAX,
limit: crate::render::MAX_OUTPUT_BYTES,
})?;
crate::render::checked_dimension(
"estimated pixel output bytes",
estimated,
crate::render::MAX_OUTPUT_BYTES,
)
}
fn at_column(text: &str, column: usize) -> crate::Result<String> {
let anchor = column
.checked_add(1)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel column anchor",
requested: usize::MAX,
limit: usize::MAX - 1,
})?;
let jump = format!("\x1b[{anchor}G");
let rows = text.split('\n').count();
let capacity = jump
.len()
.checked_mul(rows)
.and_then(|jumps| text.len().checked_add(jumps))
.ok_or(crate::Error::DimensionTooLarge {
what: "anchored pixel text bytes",
requested: usize::MAX,
limit: crate::render::MAX_OUTPUT_BYTES,
})?;
let mut out = String::new();
crate::render::reserve_string(&mut out, capacity, "anchored pixel text")?;
for (index, row) in text.split('\n').enumerate() {
if index > 0 {
out.push('\n');
}
out.push_str(&jump);
out.push_str(row);
}
Ok(out)
}
pub(crate) type Rgb = (u8, u8, u8);
pub(crate) struct Image {
pub width: usize,
pub height: usize,
pub pixels: Vec<Option<Rgb>>,
}
pub(crate) fn crop_rgba(
canvas: &PixelCanvas,
rect: PlotRect,
) -> crate::Result<(usize, usize, Vec<u8>)> {
let (x0, y0, width, height, count) = crop_geometry(canvas, rect)?;
let bytes = count
.checked_mul(4)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel crop bytes",
requested: usize::MAX,
limit: crate::render::MAX_DEVICE_PIXELS,
})?;
let mut rgba = Vec::new();
crate::render::reserve_vec(&mut rgba, bytes, "pixel crop")?;
for y in 0..height {
for x in 0..width {
rgba.extend_from_slice(&canvas.rgba(x0 + x, y0 + y));
}
}
Ok((width, height, rgba))
}
fn crop(canvas: &PixelCanvas, rect: PlotRect) -> crate::Result<Image> {
let (x0, y0, width, height, count) = crop_geometry(canvas, rect)?;
let mut pixels = Vec::new();
crate::render::reserve_vec(&mut pixels, count, "pixel crop")?;
for y in 0..height {
for x in 0..width {
pixels.push(canvas.get(x0 + x, y0 + y).map(|color| color.to_rgb()));
}
}
Ok(Image {
width,
height,
pixels,
})
}
fn crop_geometry(
canvas: &PixelCanvas,
rect: PlotRect,
) -> crate::Result<(usize, usize, usize, usize, usize)> {
let (cw, ch) = canvas.cell();
let x0 = rect
.gutter
.checked_mul(cw)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel crop x offset",
requested: usize::MAX,
limit: crate::render::MAX_DEVICE_PIXELS,
})?;
let y0 = rect
.top
.checked_mul(ch)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel crop y offset",
requested: usize::MAX,
limit: crate::render::MAX_DEVICE_PIXELS,
})?;
let width = rect
.columns
.checked_mul(cw)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel crop width",
requested: usize::MAX,
limit: crate::render::MAX_DEVICE_PIXELS,
})?;
let height = rect
.rows
.checked_mul(ch)
.ok_or(crate::Error::DimensionTooLarge {
what: "pixel crop height",
requested: usize::MAX,
limit: crate::render::MAX_DEVICE_PIXELS,
})?;
let count = crate::render::checked_area(
"pixel crop count",
width,
height,
crate::render::MAX_DEVICE_PIXELS,
)?;
Ok((x0, y0, width, height, count))
}
#[cfg(test)]
#[path = "tests/render_tests.rs"]
mod tests;