Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/render_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(' '));
}
}
27 changes: 24 additions & 3 deletions src/render_gif.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
55 changes: 46 additions & 9 deletions src/render_svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -450,14 +454,47 @@ fn emit_frame_body(s: &mut String, frame: &RawFrame, cfg: ViewportConfig, font_s
} else {
""
};
s.push_str(&format!(
r#"<text x="{x}" y="{y}" fill="{fg}"{w}{i}{d}>{txt}</text>"#,
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#"<text x="{x}" y="{y}" fill="{fg}"{w}{i}{d}{transform} textLength="{text_length}" lengthAdjust="spacingAndGlyphs">{txt}</text>"#,
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#"<text x="{x}" y="{y}" fill="{fg}"{w}{i}{d}>{txt}</text>"#,
fg = rgb_hex(fg),
w = weight,
i = italic,
d = decoration,
txt = escape_text(&run),
));
}
col = run_end;
}
}
Expand Down
Loading