use std::fmt::Write as _;
use crate::base::Rgba;
use super::cell::Attrs;
use super::screenshot::{Screenshot, ShotCell};
const CELL_W: i32 = 9;
const CELL_H: i32 = 18;
const FONT_SIZE: i32 = 15;
const BASELINE: i32 = 14;
const DEFAULT_FG: Rgba = Rgba::rgb(0xd0, 0xd0, 0xd0);
const DEFAULT_BG: Rgba = Rgba::rgb(0x00, 0x00, 0x00);
const FONT_STACK: &str =
"ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace";
impl Screenshot {
pub fn to_svg(&self) -> String {
self.to_svg_with(DEFAULT_FG, DEFAULT_BG)
}
pub fn to_svg_with(&self, default_fg: Rgba, default_bg: Rgba) -> String {
let (w, h) = (self.width(), self.height());
let (px_w, px_h) = (w.max(0) * CELL_W, h.max(0) * CELL_H);
let mut out = String::with_capacity(1024 + (w * h).max(0) as usize * 8);
let _ = writeln!(
out,
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {px_w} {px_h}\" \
width=\"{px_w}\" height=\"{px_h}\" font-family=\"{FONT_STACK}\" \
font-size=\"{FONT_SIZE}\" xml:space=\"preserve\">"
);
let _ = writeln!(
out,
"<rect width=\"{px_w}\" height=\"{px_h}\" fill=\"{}\"/>",
hex(default_bg)
);
self.svg_backgrounds(&mut out, default_fg, default_bg);
self.svg_text_runs(&mut out, default_fg, default_bg);
self.svg_decorations(&mut out, default_fg, default_bg);
self.svg_pixel_veils(&mut out);
out.push_str("</svg>\n");
out
}
fn svg_backgrounds(&self, out: &mut String, default_fg: Rgba, default_bg: Rgba) {
out.push_str("<g shape-rendering=\"crispEdges\">\n");
for y in 0..self.height() {
let mut x = 0;
while x < self.width() {
let bg = self
.cell(x, y)
.map(|c| resolve(c, default_fg, default_bg).1)
.unwrap_or(default_bg);
let start = x;
x += 1;
while x < self.width() {
let next = self
.cell(x, y)
.map(|c| resolve(c, default_fg, default_bg).1)
.unwrap_or(default_bg);
if next != bg {
break;
}
x += 1;
}
if bg != default_bg {
let _ = writeln!(
out,
"<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{CELL_H}\" fill=\"{}\"/>",
start * CELL_W,
y * CELL_H,
(x - start) * CELL_W,
hex(bg)
);
}
}
}
out.push_str("</g>\n");
}
fn svg_text_runs(&self, out: &mut String, default_fg: Rgba, default_bg: Rgba) {
let mut run = String::new();
for y in 0..self.height() {
let mut x = 0;
while x < self.width() {
let Some(cell) = self.cell(x, y) else {
x += 1;
continue;
};
if cell.is_continuation()
|| cell.attrs().contains(Attrs::HIDDEN)
|| cell.width() == 2
{
if cell.width() == 2 && !cell.attrs().contains(Attrs::HIDDEN) {
run.clear();
run.push_str(cell.text());
emit_text_run(out, x, y, 2, cell, &run, default_fg, default_bg);
}
x += cell.width().max(1);
continue;
}
let key = style_key(cell, default_fg, default_bg);
let start = x;
run.clear();
let mut has_ink = push_run_glyph(&mut run, cell.text());
x += 1;
while x < self.width() {
let Some(next) = self.cell(x, y) else { break };
if next.width() != 1
|| next.attrs().contains(Attrs::HIDDEN)
|| style_key(next, default_fg, default_bg) != key
{
break;
}
has_ink |= push_run_glyph(&mut run, next.text());
x += 1;
}
if has_ink {
emit_text_run(out, start, y, x - start, cell, &run, default_fg, default_bg);
}
}
}
}
fn svg_decorations(&self, out: &mut String, default_fg: Rgba, default_bg: Rgba) {
const DECOS: [(u16, i32); 2] = [(UNDERLINE_MASK, 15), (STRIKE_MASK, 9)];
for (mask_bits, dy) in DECOS {
let mask = Attrs::from_bits_truncate(mask_bits);
for y in 0..self.height() {
let mut x = 0;
while x < self.width() {
let color = self
.cell(x, y)
.filter(|c| c.attrs().intersects(mask))
.map(|c| deco_color(c, mask, default_fg, default_bg));
let Some(color) = color else {
x += 1;
continue;
};
let start = x;
x += 1;
while x < self.width() {
let same = self
.cell(x, y)
.filter(|c| c.attrs().intersects(mask))
.map(|c| deco_color(c, mask, default_fg, default_bg))
== Some(color);
if !same {
break;
}
x += 1;
}
let _ = writeln!(
out,
"<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"1\" fill=\"{}\"/>",
start * CELL_W,
y * CELL_H + dy,
(x - start) * CELL_W,
hex(color)
);
}
}
}
}
fn svg_pixel_veils(&self, out: &mut String) {
for r in self.pixel_regions() {
let (x, y) = (r.x * CELL_W, r.y * CELL_H);
let (w, h) = (r.w * CELL_W, r.h * CELL_H);
let _ = writeln!(
out,
"<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" fill=\"#7f7f7f\" \
fill-opacity=\"0.25\" stroke=\"#ffffff\" stroke-opacity=\"0.5\" \
stroke-dasharray=\"4 3\"/>"
);
let _ = writeln!(
out,
"<text x=\"{}\" y=\"{}\" text-anchor=\"middle\" font-size=\"12\" \
fill=\"#ffffff\" fill-opacity=\"0.8\">image (pixels)</text>",
x + w / 2,
y + h / 2 + 4
);
}
}
}
const UNDERLINE_MASK: u16 = Attrs::UNDERLINE.bits() | Attrs::UNDERCURL.bits();
const STRIKE_MASK: u16 = Attrs::STRIKE.bits();
fn resolve(c: &ShotCell, default_fg: Rgba, default_bg: Rgba) -> (Rgba, Rgba) {
let fg = c.fg().unwrap_or(default_fg);
let bg = c.bg().unwrap_or(default_bg);
if c.attrs().contains(Attrs::REVERSE) {
(bg, fg)
} else {
(fg, bg)
}
}
fn deco_color(c: &ShotCell, mask: Attrs, default_fg: Rgba, default_bg: Rgba) -> Rgba {
let ink = resolve(c, default_fg, default_bg).0;
if mask.intersects(Attrs::UNDERLINE | Attrs::UNDERCURL) {
c.ul().unwrap_or(ink)
} else {
ink
}
}
fn style_key(c: &ShotCell, default_fg: Rgba, default_bg: Rgba) -> (Rgba, bool, bool, bool) {
let ink = resolve(c, default_fg, default_bg).0;
(
ink,
c.attrs().contains(Attrs::BOLD),
c.attrs().contains(Attrs::DIM),
c.attrs().contains(Attrs::ITALIC),
)
}
fn push_run_glyph(run: &mut String, text: &str) -> bool {
match text {
"" | " " => {
run.push('\u{a0}');
false
}
t => {
run.push_str(t);
true
}
}
}
#[allow(clippy::too_many_arguments)]
fn emit_text_run(
out: &mut String,
x: i32,
y: i32,
cols: i32,
style_of: &ShotCell,
run: &str,
default_fg: Rgba,
default_bg: Rgba,
) {
let ink = resolve(style_of, default_fg, default_bg).0;
let _ = write!(
out,
"<text x=\"{}\" y=\"{}\" textLength=\"{}\" lengthAdjust=\"spacingAndGlyphs\" fill=\"{}\"",
x * CELL_W,
y * CELL_H + BASELINE,
cols * CELL_W,
hex(ink)
);
let attrs = style_of.attrs();
if attrs.contains(Attrs::BOLD) {
out.push_str(" font-weight=\"700\"");
}
if attrs.contains(Attrs::ITALIC) {
out.push_str(" font-style=\"italic\"");
}
if attrs.contains(Attrs::DIM) {
out.push_str(" fill-opacity=\"0.6\"");
}
out.push('>');
push_escaped(out, run);
out.push_str("</text>\n");
}
fn hex(c: Rgba) -> String {
format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)
}
fn push_escaped(out: &mut String, s: &str) {
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
}