Work on character centering

This commit is contained in:
Michael Mikovsky
2025-09-30 21:22:09 -06:00
parent 845b7bd0b4
commit 2ef44c1b27
10 changed files with 330 additions and 96 deletions
Binary file not shown.
+5 -3
View File
@@ -1,9 +1,11 @@
pub static LINE_WIDTH: f32 = 0.02; // Global scale pub static LINE_WIDTH: f32 = 0.02; // Global scale
pub static MIN_LOCAL_SCALE: f32 = 0.1; // Global scale // pub static MIN_LOCAL_SCALE: f32 = 0.1; // Global scale
pub static TEXT_OFFSET: f32 = 0.; // Local scale
pub static TEXT_X_PADDING: f32 = 0.0; // Local scale
pub static OPERATOR_X_PADDING: f32 = 0.1; // Local scale
pub static FRACTION_SCALE: f32 = 0.8; // Local scale pub static FRACTION_SCALE: f32 = 0.8; // Local scale
pub static FRACTION_PADDING: f32 = 0.2; // global scale pub static FRACTION_PADDING: f32 = 0.2; // global scale
pub static SUPERSCRIPT_SCALE: f32 = 0.6; // local scale pub static SUPERSCRIPT_SCALE: f32 = 0.8; // local scale
pub static SUPERSCRIPT_Y_OFFSET: f32 = 0.4; // local scale pub static SUPERSCRIPT_Y_OFFSET: f32 = 0.4; // local scale
+20 -9
View File
@@ -39,36 +39,37 @@ impl KElement {
} => { } => {
if !super_script.is_empty() && !sub_script.is_empty() { if !super_script.is_empty() && !sub_script.is_empty() {
root.push(KElement::SuperSub { root.push(KElement::SuperSub {
inner: Rc::new(KElement::Text(text.clone())), inner: Rc::new(Self::parse_text(text)),
upper: Some(Rc::new(Self::parse_object(super_script)?)), upper: Some(Rc::new(Self::parse_object(super_script)?)),
lower: Some(Rc::new(Self::parse_object(sub_script)?)), lower: Some(Rc::new(Self::parse_object(sub_script)?)),
}); });
} else if !super_script.is_empty() { } else if !super_script.is_empty() {
root.push(KElement::SuperSub { root.push(KElement::SuperSub {
inner: Rc::new(KElement::Text(text.clone())), inner: Rc::new(Self::parse_text(text)),
upper: Some(Rc::new(Self::parse_object(super_script)?)), upper: Some(Rc::new(Self::parse_object(super_script)?)),
lower: None lower: None
}); });
} else if !sub_script.is_empty() { } else if !sub_script.is_empty() {
root.push(KElement::SuperSub { root.push(KElement::SuperSub {
inner: Rc::new(KElement::Text(text.clone())), inner: Rc::new(Self::parse_text(text)),
upper: None, upper: None,
lower: Some(Rc::new(Self::parse_object(super_script)?)) lower: Some(Rc::new(Self::parse_object(super_script)?))
}); });
} else { } else {
root.push(KElement::Text(text.clone())); root.push(Self::parse_text(text));
} }
}, },
text_parser::ParsedObject::Operator { text_parser::ParsedObject::Operator {
text text
} => { } => {
root.push(KElement::Text(text.clone())); root.push(KElement::from_symbol(text)?);
}, },
text_parser::ParsedObject::Parenthesis { text_parser::ParsedObject::Parenthesis {
inner, // inner,
parenthesis_type, // parenthesis_type,
super_script, // super_script,
sub_script // sub_script
..
} => { } => {
}, },
} }
@@ -78,4 +79,14 @@ impl KElement {
return Ok(KElement::LinearGroup(root)); return Ok(KElement::LinearGroup(root));
} }
pub fn parse_text(str: &str) -> KElement {
if let Ok(num) = str.parse::<f64>() {
KElement::Decimal(num)
} else if let Ok(num) = str.parse::<i64>() {
KElement::Integer(num)
} else {
KElement::Text(str.to_string())
}
}
} }
+5 -5
View File
@@ -10,11 +10,11 @@ fn assert_args(n: usize, start: usize, end: usize, err: &str) -> Result<(), Stri
} }
} }
fn derive_symbol(symbol_str: &str, args: &Vec<Vec<ParsedObject>>) -> Result<KElement, String> { // fn derive_symbol(symbol_str: &str, args: &Vec<Vec<ParsedObject>>) -> Result<KElement, String> {
assert_args(args.len(), 0, 0, "Symbol cannot take in any args!")?; // assert_args(args.len(), 0, 0, "Symbol cannot take in any args!")?;
Ok(KElement::Text(symbol_str.to_string())) // Ok(KElement::Text(symbol_str.to_string()))
} // }
impl KElement { impl KElement {
pub fn from_function(name: &str, args: &Vec<Vec<ParsedObject>>) -> Result<KElement, String> { pub fn from_function(name: &str, args: &Vec<Vec<ParsedObject>>) -> Result<KElement, String> {
@@ -27,7 +27,7 @@ impl KElement {
lower: Rc::new(Self::parse_object(&args[1])?) lower: Rc::new(Self::parse_object(&args[1])?)
}) })
} }
"pm" => derive_symbol("±", args), "pm" => Ok(KElement::PlusMinus),
_ => Err(format!("Invalid function: \\{}", name)) _ => Err(format!("Invalid function: \\{}", name))
} }
} }
+6
View File
@@ -4,6 +4,7 @@ mod rasterizer;
mod text_parser; mod text_parser;
mod element_parser; mod element_parser;
mod functions; mod functions;
mod symbols;
pub enum KElement { pub enum KElement {
LinearGroup(Vec<KElement>), LinearGroup(Vec<KElement>),
@@ -19,4 +20,9 @@ pub enum KElement {
upper: Option<Rc<KElement>>, upper: Option<Rc<KElement>>,
lower: Option<Rc<KElement>> lower: Option<Rc<KElement>>
}, },
Plus,
Minus,
Equals,
PlusMinus,
} }
+69 -78
View File
@@ -44,13 +44,13 @@ impl KElement {
bitmap bitmap
} }
KElement::Integer(i) => { KElement::Integer(i) => {
render_text_block(&mut globals.layout, &i.to_string(), current_scale) Self::render_text_block(&mut globals.layout, &i.to_string(), 0, current_scale, TEXT_X_PADDING)
}, },
KElement::Decimal(i) => { KElement::Decimal(i) => {
render_text_block(&mut globals.layout, &i.to_string(), current_scale) Self::render_text_block(&mut globals.layout, &i.to_string(), 0, current_scale, TEXT_X_PADDING)
}, },
KElement::Text(str) => { KElement::Text(str) => {
render_text_block(&mut globals.layout, &str, current_scale) Self::render_text_block(&mut globals.layout, &str, 1, current_scale, TEXT_X_PADDING)
}, },
KElement::Fraction{upper,lower} => { KElement::Fraction{upper,lower} => {
let padding = (FRACTION_PADDING * current_scale) as usize; let padding = (FRACTION_PADDING * current_scale) as usize;
@@ -110,21 +110,38 @@ impl KElement {
unreachable!() unreachable!()
} }
} }
_ => self.rasterize_symbol(globals, current_scale),
} }
} }
pub fn get_bounds(&self, globals: &mut RusTeX, current_scale: f32) -> (usize, usize, usize) { pub fn get_bounds(&self, globals: &mut RusTeX, current_scale: f32) -> (usize, usize, usize) {
match self { match self {
KElement::LinearGroup(elems) => { KElement::LinearGroup(elems) => {
bounds_of_linear_group(elems, globals, current_scale) let (mut totalx, mut mintop, mut maxbottom): (usize, usize, usize) = (0,0,0);
for elem in elems {
let (width, height, centery) = elem.get_bounds(globals, current_scale);
totalx += width;
let top = centery;
let bottom = height - centery;
mintop = mintop.max(top);
maxbottom = maxbottom.max(bottom);
}
(
totalx,
maxbottom + mintop,
mintop
)
} }
KElement::Integer(i) => { KElement::Integer(i) => {
measure_text_bounds(&mut globals.layout, &i.to_string(), current_scale) Self::measure_text_bounds(&mut globals.layout, &i.to_string(), 0, current_scale, TEXT_X_PADDING, TEXT_OFFSET)
}, },
KElement::Decimal(i) => { KElement::Decimal(i) => {
measure_text_bounds(&mut globals.layout, &i.to_string(), current_scale) Self::measure_text_bounds(&mut globals.layout, &i.to_string(), 0, current_scale, TEXT_X_PADDING, TEXT_OFFSET)
}, },
KElement::Text(str) => { KElement::Text(str) => {
measure_text_bounds(&mut globals.layout, &str, current_scale) Self::measure_text_bounds(&mut globals.layout, &str, 1, current_scale, TEXT_X_PADDING, TEXT_OFFSET)
}, },
KElement::Fraction{upper,lower} => { KElement::Fraction{upper,lower} => {
let (ax,ay, _) = upper.get_bounds(globals, current_scale * FRACTION_SCALE); let (ax,ay, _) = upper.get_bounds(globals, current_scale * FRACTION_SCALE);
@@ -153,85 +170,59 @@ impl KElement {
} }
} }
_ => Self::get_symbol_bounds(&self, globals, current_scale),
} }
} }
} }
fn bounds_of_linear_group(elems: &Vec<KElement>, globals: &mut RusTeX, current_scale: f32) -> (usize, usize, usize) { impl KElement {
// let common_centery = elems[0].get_bounds(globals, current_scale); pub fn measure_text_bounds(layout: &mut Layout, text: &str, font_index: usize, scale:f32, x_padding: f32, center_offset:f32) -> (usize, usize, usize) {
layout.clear();
layout.append(&FONTS, &TextStyle::new(text, scale, font_index));
let (mut totalx, mut mintop, mut maxbottom): (usize, usize, usize) = (0,0,0); let (mut width, mut height): (usize, usize) = (0,0);
for elem in elems { for glyph in layout.glyphs() {
let (width, height, centery) = elem.get_bounds(globals, current_scale); width = width.max(glyph.x as usize + glyph.width);
totalx += width; height = height.max(glyph.y as usize + glyph.height);
let top = centery; }
let bottom = height - centery;
(
width + 2*(scale*x_padding) as usize,
height,
height/2 + ((center_offset)*scale) as usize
)
mintop = mintop.max(top);
maxbottom = maxbottom.max(bottom);
} }
( pub fn render_text_block(layout: &mut Layout, text: &str, font_index: usize, scale:f32, x_padding: f32) -> Bitmap {
totalx, layout.clear();
maxbottom + mintop,
mintop layout.append(&FONTS, &TextStyle::new(text, scale, font_index));
)
} let (mut width, mut height): (usize, usize) = (0,0);
for glyph in layout.glyphs() {
// impl KSymbol { width = width.max(glyph.x as usize + glyph.width);
// // pub fn get_max_bounds(&self) -> (f32, f32) { height = height.max(glyph.y as usize + glyph.height);
// // match self { }
// // KSymbol::Text { x, y, ..} => (*x,*y),
// // _ => (0.,0.) let mut new_bitmap = Bitmap::new(width + 2*(scale*x_padding) as usize, height);
// // }
// // } for glyph in layout.glyphs() {
// pub fn rasterize(&self, globals.layout: &mut Layout, bitmap: &mut Bitmap) {
// match self { let font = &FONTS[glyph.font_index];
// KSymbol::Text { data, x, y, scale } => { let (_, char_bitmap) = font.rasterize_config(glyph.key);
// let new_bitmap = render_text_block(globals.layout, data, *scale);
// bitmap.overlay(&new_bitmap, *x as usize, *y as usize); new_bitmap.overlay(
// }, &Bitmap::from_data(
// KSymbol::None => {}, char_bitmap,
// } glyph.width,
// } glyph.height
// } ),
glyph.x as usize + (scale*x_padding) as usize,
fn measure_text_bounds(layout: &mut Layout, text: &str, scale:f32) -> (usize, usize, usize) { glyph.y as usize);
layout.clear(); }
layout.append(&FONTS, &TextStyle::new(text, scale, 0));
new_bitmap
let (mut width, mut height): (usize, usize) = (0,0); }
for glyph in layout.glyphs() {
width = width.max(glyph.x as usize + glyph.width);
height = height.max(glyph.y as usize + glyph.height);
}
(width, height, height/2)
}
fn render_text_block(layout: &mut Layout, text: &str, scale:f32) -> Bitmap {
layout.clear();
layout.append(&FONTS, &TextStyle::new(text, scale, 0));
let (mut width, mut height): (usize, usize) = (0,0);
for glyph in layout.glyphs() {
width = width.max(glyph.x as usize + glyph.width);
height = height.max(glyph.y as usize + glyph.height);
}
let mut new_bitmap = Bitmap::new(width, height);
for glyph in layout.glyphs() {
let font = &FONTS[glyph.font_index];
let (_, char_bitmap) = font.rasterize_config(glyph.key);
new_bitmap.overlay(&Bitmap::from_data(char_bitmap, glyph.width, glyph.height), glyph.x as usize, glyph.y as usize);
}
new_bitmap
} }
+46
View File
@@ -0,0 +1,46 @@
use crate::{bitmap::Bitmap, consts::OPERATOR_X_PADDING, element::KElement, RusTeX};
pub const PLUS: &'static str = "+";
pub const MINUS: &'static str = "-";
pub const EQUALS: &'static str = "=";
pub const PLUS_MINUS: &'static str = "±";
pub const PLUS_YOFFSET: f32 = 0.15;
pub const MINUS_YOFFSET: f32 = 0.15;
pub const EQUALS_YOFFSET: f32 = 0.27;
pub const PLUSMMINUS_YOFFSET: f32 = 0.15;
impl KElement {
pub fn from_symbol(symbol: &str) -> Result<KElement, String> {
match symbol {
"+" => Ok(KElement::Plus),
"-" => Ok(KElement::Minus),
"=" => Ok(KElement::Equals),
"±" => Ok(KElement::PlusMinus),
_ => unreachable!("Unimplemented symbol: {}", symbol),
// _ => Ok(KElement::Text(symbol.to_string()))
// MINUS => KElement::Minus
}
}
pub fn rasterize_symbol(&self, globals: &mut RusTeX, current_scale: f32) -> Bitmap {
match self {
KElement::Plus => Self::render_text_block(&mut globals.layout, PLUS, 0, current_scale, OPERATOR_X_PADDING),
KElement::Minus => Self::render_text_block(&mut globals.layout, MINUS, 0, current_scale, OPERATOR_X_PADDING),
KElement::Equals => Self::render_text_block(&mut globals.layout, EQUALS, 0, current_scale, OPERATOR_X_PADDING),
KElement::PlusMinus => Self::render_text_block(&mut globals.layout, PLUS_MINUS, 0, current_scale, OPERATOR_X_PADDING),
_ => unreachable!()
}
}
pub fn get_symbol_bounds(&self, globals: &mut RusTeX, current_scale: f32) -> (usize, usize, usize) {
match self {
KElement::Plus => Self::measure_text_bounds(&mut globals.layout, PLUS, 0, current_scale, OPERATOR_X_PADDING, PLUS_YOFFSET),
KElement::Minus => Self::measure_text_bounds(&mut globals.layout, MINUS, 0, current_scale, OPERATOR_X_PADDING, MINUS_YOFFSET),
KElement::Equals => Self::measure_text_bounds(&mut globals.layout, EQUALS, 0, current_scale, OPERATOR_X_PADDING, EQUALS_YOFFSET),
KElement::PlusMinus => Self::measure_text_bounds(&mut globals.layout, PLUS_MINUS, 0, current_scale, OPERATOR_X_PADDING, PLUSMMINUS_YOFFSET),
_ => unreachable!()
}
}
}
+159
View File
@@ -1,3 +1,162 @@
// use regex::{Captures, Regex};
// use lazy_static::lazy_static;
// use std::collections::VecDeque;
// lazy_static! {
// static ref FUNCTION_REGEX: Regex = Regex::new(r"\\([a-zA-Z]+)(\s*(?:\{[^}]*\}|[^_^{}\s\\])*)\s*(?:_(\{[^}]*\}|[^_^{}\s\\]))?\s*(?:\^(\{[^}]*\}|[^_^{}\s\\]))?").unwrap();
// static ref TEXT_REGEX: Regex = Regex::new(r"^\{\}<>()\[\]]+").unwrap();
// }
// #[derive(Debug, Clone, PartialEq)]
// pub enum PerenthesisType {
// Round, // ()
// Square, // []
// // Curly, // {}
// // Angle, // <>
// }
// #[derive(Debug, Clone, PartialEq)]
// pub enum ParsedObject {
// Func {
// name: String,
// content: Vec<Vec<ParsedObject>>,
// super_script: Vec<ParsedObject>,
// sub_script: Vec<ParsedObject>,
// },
// Var {
// text: String,
// super_script: Vec<ParsedObject>,
// sub_script: Vec<ParsedObject>,
// },
// Operator {
// text: String,
// },
// Parenthesis {
// inner: Vec<ParsedObject>,
// parenthesis_type: PerenthesisType,
// super_script: Vec<ParsedObject>,
// sub_script: Vec<ParsedObject>,
// },
// }
// pub fn parse(input: &str) -> Result<Vec<ParsedObject>, String> {
// if input.is_empty() {
// return Ok(Vec::new())
// }
// let mut i = 0;
// let mut objects = Vec::new();
// for i in 0..input.len() {
// // let mut functions = FUNCTION_REGEX.is_match_at();
// // FUNCTION_REGEX.(&mut functions, input);
// // for i in 0..functions.len() {
// // println!("{:?}", functions.get(i));
// // }
// if TEXT_REGEX.is_match_at(input, i) {
// println!("TEXT!")
// } else if FUNCTION_REGEX.is_match_at(input, i) {
// println!("FUNCTION!")
// } else {
// break;
// }
// }
// // loop {
// // }
// // for captures in FUNCTION_REGEX.captures_iter(input) {
// // objects.push(parse_function(&captures)?);
// // }
// // println!("{:?}", objects);
// // let capture_locations = FUNCTION_REGEX.captures_read(locs, haystack);
// // loop {
// // }
// Ok(objects)
// }
// fn parse_function(captures: &Captures) -> Result<(usize, ParsedObject), String> {
// let name = captures.get(1).unwrap().as_str();
// let args_str = captures.get(2).map_or("", |m| m.as_str());
// let sub = captures.get(3).map(|m| m.as_str());
// let super_script = captures.get(4).map(|m| m.as_str());
// let len = captures.iter().map(|c| c.iter().len()).sum::<usize>();
// // Parse arguments
// let mut args = Vec::new();
// let args_clean = args_str.trim();
// if !args_clean.is_empty() {
// let mut i = 0;
// let chars: Vec<char> = args_clean.chars().collect();
// while i < chars.len() {
// if chars[i] == '{' {
// let mut brace_content = String::new();
// i += 1;
// while i < chars.len() && chars[i] != '}' {
// brace_content.push(chars[i]);
// i += 1;
// }
// args.push(brace_content);
// i += 1;
// } else if chars[i] != ' ' {
// args.push(chars[i].to_string());
// i += 1;
// } else {
// i += 1;
// }
// }
// }
// // Clean up sub/super (remove braces if present)
// let sub_clean = sub.map(|s| if s.starts_with('{') && s.ends_with('}') {
// &s[1..s.len()-1]
// } else { s });
// let super_clean = super_script.map(|s| if s.starts_with('{') && s.ends_with('}') {
// &s[1..s.len()-1]
// } else { s });
// Ok(
// (
// len,
// ParsedObject::Func {
// name: name.to_string(),
// content: parse_string_arr(&args)?,
// super_script: if let Some(super_clean) = super_clean {
// parse(super_clean)?
// } else {
// Vec::new()
// },
// sub_script: if let Some(sub_clean) = sub_clean {
// parse(sub_clean)?
// } else {
// Vec::new()
// },
// }
// ))
// }
// fn parse_string_arr(args: &Vec<String>) -> Result<Vec<Vec<ParsedObject>>, String> {
// let mut result_vec = Vec::new();
// for arg in args {
// result_vec.push(parse(arg)?);
// }
// Ok(result_vec)
// }
use regex::Regex; use regex::Regex;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::collections::VecDeque; use std::collections::VecDeque;
+2
View File
@@ -2,9 +2,11 @@ use fontdue::Font;
use lazy_static::lazy_static; use lazy_static::lazy_static;
pub static KaTeX_Main_Regular: &'static [u8] = include_bytes!("../fonts/KaTeX_Main-Regular.ttf"); pub static KaTeX_Main_Regular: &'static [u8] = include_bytes!("../fonts/KaTeX_Main-Regular.ttf");
pub static KaTeX_Main_Italic: &'static [u8] = include_bytes!("../fonts/KaTeX_Main-Italic.ttf");
lazy_static! { lazy_static! {
pub static ref FONTS: Vec<Font> = vec![ pub static ref FONTS: Vec<Font> = vec![
Font::from_bytes(KaTeX_Main_Regular, fontdue::FontSettings::default()).unwrap(), Font::from_bytes(KaTeX_Main_Regular, fontdue::FontSettings::default()).unwrap(),
Font::from_bytes(KaTeX_Main_Italic, fontdue::FontSettings::default()).unwrap(),
]; ];
} }
+18 -1
View File
@@ -5,6 +5,8 @@ mod element;
mod bitmap; mod bitmap;
mod consts; mod consts;
use std::time::Instant;
use fontdue::{layout::{CoordinateSystem, Layout, LayoutSettings}}; use fontdue::{layout::{CoordinateSystem, Layout, LayoutSettings}};
use crate::{bitmap::Bitmap, element::{KElement}}; use crate::{bitmap::Bitmap, element::{KElement}};
@@ -16,15 +18,30 @@ fn main() -> Result<(), std::fmt::Error> {
fn parse_test() -> Result<(), std::fmt::Error> { fn parse_test() -> Result<(), std::fmt::Error> {
let tex_input = &std::env::args().nth(1).unwrap(); let tex_input = &std::env::args().nth(1).unwrap();
let mut start = Instant::now();
match KElement::parse(tex_input) { match KElement::parse(tex_input) {
Ok(result) => { Ok(result) => {
println!("Parse time: {:?}", start.elapsed());
start = Instant::now();
let mut rustex = RusTeX::new(TeXSettings { scale: 100. }); let mut rustex = RusTeX::new(TeXSettings { scale: 100. });
rustex.rasterize(result).print();
let bitmap = rustex.rasterize(result);
println!("Raster time: {:?}", start.elapsed());
start = Instant::now();
bitmap.print();
println!("Display time: {:?}", start.elapsed());
} }
Err(e) => println!("Error: {}", e), Err(e) => println!("Error: {}", e),
} }
Ok(()) Ok(())
} }