diff --git a/src/render_common.rs b/src/render_common.rs
index 2833abb..81b2cce 100644
--- a/src/render_common.rs
+++ b/src/render_common.rs
@@ -33,6 +33,16 @@ pub struct ViewportConfig {
pub content_y: u32,
}
+/// Returns `true` if the character is a box drawing character.
+/// This includes the Box Drawing block (U+2500..=U+257F), Block Elements (U+2580..=U+259F),
+/// Symbols for Legacy Computing (U+1FB00..=U+1FBFF), and Symbols for Legacy Computing Supplement (U+1CC00..=U+1CEBF).
+pub fn is_box_drawing(c: char) -> bool {
+ let cp = c as u32;
+ (0x2500..=0x259F).contains(&cp) ||
+ (0x1FB00..=0x1FBFF).contains(&cp) ||
+ (0x1CC00..=0x1CEBF).contains(&cp)
+}
+
impl ViewportConfig {
pub fn new(
cols: u16,
@@ -119,3 +129,19 @@ impl Default for RenderOptions {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_is_box_drawing() {
+ assert!(is_box_drawing('╭')); // Box Drawing
+ assert!(is_box_drawing('█')); // Block Elements
+ assert!(is_box_drawing('\u{1FB00}')); // Legacy Computing
+ assert!(is_box_drawing('\u{1CC00}')); // Legacy Computing Supplement
+
+ assert!(!is_box_drawing('A'));
+ assert!(!is_box_drawing(' '));
+ }
+}
diff --git a/src/render_gif.rs b/src/render_gif.rs
index 69ee557..f127e02 100644
--- a/src/render_gif.rs
+++ b/src/render_gif.rs
@@ -12,6 +12,8 @@ use std::{
use ab_glyph::{Font, FontArc, Glyph, GlyphId, PxScale, ScaleFont};
use anyhow::{Context, Result, anyhow};
+
+use crate::render_common::is_box_drawing;
use crossbeam_channel::{Receiver, Sender, bounded};
use gifski::{Settings, progress};
use tracing::{debug, warn};
@@ -114,7 +116,8 @@ struct GlyphCacheKey {
/// ab_glyph glyph identifier within the face.
glyph_id: u16,
/// Uniform px-scale as `f32` bits (we only use uniform scales).
- scale_bits: u32,
+ scale_bits_x: u32,
+ scale_bits_y: u32,
}
/// Colour-independent coverage mask for one rasterised glyph.
@@ -491,13 +494,31 @@ fn rasterize_raw_frame(
font_idx <= u16::MAX as usize,
"font_idx {font_idx} exceeds u16 range"
);
+ let mut glyph_scale = scale;
+
+ if is_box_drawing(ch) {
+ let scaled = font.as_scaled(scale);
+ let advance = scaled.h_advance(glyph_id);
+ let bbox_w = cell_w as f32;
+ let bbox_h = cell_h as f32;
+
+ let glyph_w = advance.max(1.0);
+ let glyph_h = (scaled.ascent() - scaled.descent()).max(1.0);
+
+ // We want the box drawing character to exactly fill the cell width and height,
+ // so we stretch it accordingly.
+ glyph_scale.x = scale.x * (bbox_w / glyph_w);
+ glyph_scale.y = scale.y * (bbox_h / glyph_h);
+ }
+
let cache_key = GlyphCacheKey {
font_idx: font_idx as u16,
glyph_id: glyph_id.0,
- scale_bits: scale.x.to_bits(),
+ scale_bits_x: glyph_scale.x.to_bits(),
+ scale_bits_y: glyph_scale.y.to_bits(),
};
let bitmap = glyph_cache.entry(cache_key).or_insert_with(|| {
- let glyph: Glyph = glyph_id.with_scale(scale);
+ let glyph: Glyph = glyph_id.with_scale(glyph_scale);
font.outline_glyph(glyph).map(|outline| {
let bounds = outline.px_bounds();
let w = (bounds.max.x - bounds.min.x).ceil() as u32;
diff --git a/src/render_svg.rs b/src/render_svg.rs
index 16cd437..6db50fe 100644
--- a/src/render_svg.rs
+++ b/src/render_svg.rs
@@ -39,6 +39,7 @@ use std::{
thread::{self, JoinHandle},
};
+use crate::render_common::is_box_drawing;
use anyhow::{Context, Result, anyhow};
use crossbeam_channel::{Receiver, Sender, bounded};
@@ -418,11 +419,14 @@ fn emit_frame_body(s: &mut String, frame: &RawFrame, cfg: ViewportConfig, font_s
let style = cell.flags;
let mut run = String::new();
run.push_str(&cell.text);
+ let is_box = cell.text.chars().any(is_box_drawing);
let mut run_end = col + 1;
while run_end < frame.cols {
let next = &frame.cells[row as usize * frame.cols as usize + run_end as usize];
let (nfg, _) = effective_colors(next);
- if next.text.is_empty() || nfg != fg || next.flags != style {
+ let next_is_box = next.text.chars().any(is_box_drawing);
+ if next.text.is_empty() || nfg != fg || next.flags != style || is_box != next_is_box
+ {
break;
}
run.push_str(&next.text);
@@ -450,14 +454,47 @@ fn emit_frame_body(s: &mut String, frame: &RawFrame, cfg: ViewportConfig, font_s
} else {
""
};
- s.push_str(&format!(
- r#"{txt}"#,
- fg = rgb_hex(fg),
- w = weight,
- i = italic,
- d = decoration,
- txt = escape_text(&run),
- ));
+ if is_box {
+ let run_len = (run_end - col) as u32;
+ let text_length = run_len * cell_w;
+ let bbox_h = font_size * 0.8; // approximate bbox height
+ let scale_y = (cell_h as f32 / bbox_h).max(1.0);
+
+ // We use lengthAdjust="spacingAndGlyphs" combined with a vertical scale
+ // to make the box drawing glyphs fill the cell boundary.
+ let transform = if scale_y > 1.0 {
+ // SVG text coordinates are roughly on the baseline. Scaling by Y will stretch the ascent and descent.
+ // To keep the character centered vertically in the cell, we scale it relative to its vertical center.
+ let cy = y as f32 - (font_size * 0.3); // Approximate vertical center of the character, closer to baseline
+ format!(
+ r#" transform="translate(0, {cy}) scale(1, {scale_y}) translate(0, -{cy})""#,
+ cy = cy,
+ scale_y = scale_y
+ )
+ } else {
+ String::new()
+ };
+
+ s.push_str(&format!(
+ r#"{txt}"#,
+ fg = rgb_hex(fg),
+ w = weight,
+ i = italic,
+ d = decoration,
+ transform = transform,
+ text_length = text_length,
+ txt = escape_text(&run),
+ ));
+ } else {
+ s.push_str(&format!(
+ r#"{txt}"#,
+ fg = rgb_hex(fg),
+ w = weight,
+ i = italic,
+ d = decoration,
+ txt = escape_text(&run),
+ ));
+ }
col = run_end;
}
}