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 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_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
+20 -9
View File
@@ -39,36 +39,37 @@ impl KElement {
} => {
if !super_script.is_empty() && !sub_script.is_empty() {
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)?)),
lower: Some(Rc::new(Self::parse_object(sub_script)?)),
});
} else if !super_script.is_empty() {
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)?)),
lower: None
});
} else if !sub_script.is_empty() {
root.push(KElement::SuperSub {
inner: Rc::new(KElement::Text(text.clone())),
inner: Rc::new(Self::parse_text(text)),
upper: None,
lower: Some(Rc::new(Self::parse_object(super_script)?))
});
} else {
root.push(KElement::Text(text.clone()));
root.push(Self::parse_text(text));
}
},
text_parser::ParsedObject::Operator {
text
} => {
root.push(KElement::Text(text.clone()));
root.push(KElement::from_symbol(text)?);
},
text_parser::ParsedObject::Parenthesis {
inner,
parenthesis_type,
super_script,
sub_script
// inner,
// parenthesis_type,
// super_script,
// sub_script
..
} => {
},
}
@@ -78,4 +79,14 @@ impl KElement {
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> {
assert_args(args.len(), 0, 0, "Symbol cannot take in any args!")?;
// 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!")?;
Ok(KElement::Text(symbol_str.to_string()))
}
// Ok(KElement::Text(symbol_str.to_string()))
// }
impl KElement {
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])?)
})
}
"pm" => derive_symbol("±", args),
"pm" => Ok(KElement::PlusMinus),
_ => Err(format!("Invalid function: \\{}", name))
}
}
+6
View File
@@ -4,6 +4,7 @@ mod rasterizer;
mod text_parser;
mod element_parser;
mod functions;
mod symbols;
pub enum KElement {
LinearGroup(Vec<KElement>),
@@ -19,4 +20,9 @@ pub enum KElement {
upper: Option<Rc<KElement>>,
lower: Option<Rc<KElement>>
},
Plus,
Minus,
Equals,
PlusMinus,
}
+50 -59
View File
@@ -44,13 +44,13 @@ impl KElement {
bitmap
}
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) => {
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) => {
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} => {
let padding = (FRACTION_PADDING * current_scale) as usize;
@@ -110,21 +110,38 @@ impl KElement {
unreachable!()
}
}
_ => self.rasterize_symbol(globals, current_scale),
}
}
pub fn get_bounds(&self, globals: &mut RusTeX, current_scale: f32) -> (usize, usize, usize) {
match self {
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) => {
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) => {
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) => {
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} => {
let (ax,ay, _) = upper.get_bounds(globals, current_scale * FRACTION_SCALE);
@@ -153,54 +170,35 @@ 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) {
// let common_centery = elems[0].get_bounds(globals, current_scale);
impl KElement {
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);
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);
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);
}
(
totalx,
maxbottom + mintop,
mintop
width + 2*(scale*x_padding) as usize,
height,
height/2 + ((center_offset)*scale) as usize
)
}
// impl KSymbol {
// // pub fn get_max_bounds(&self) -> (f32, f32) {
// // match self {
// // KSymbol::Text { x, y, ..} => (*x,*y),
// // _ => (0.,0.)
// // }
// // }
// pub fn rasterize(&self, globals.layout: &mut Layout, bitmap: &mut Bitmap) {
// match self {
// KSymbol::Text { data, x, y, scale } => {
// let new_bitmap = render_text_block(globals.layout, data, *scale);
// bitmap.overlay(&new_bitmap, *x as usize, *y as usize);
// },
// KSymbol::None => {},
// }
// }
// }
fn measure_text_bounds(layout: &mut Layout, text: &str, scale:f32) -> (usize, usize, usize) {
pub fn render_text_block(layout: &mut Layout, text: &str, font_index: usize, scale:f32, x_padding: f32) -> Bitmap {
layout.clear();
layout.append(&FONTS, &TextStyle::new(text, scale, 0));
layout.append(&FONTS, &TextStyle::new(text, scale, font_index));
let (mut width, mut height): (usize, usize) = (0,0);
for glyph in layout.glyphs() {
@@ -208,30 +206,23 @@ fn measure_text_bounds(layout: &mut Layout, text: &str, scale:f32) -> (usize, us
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);
let mut new_bitmap = Bitmap::new(width + 2*(scale*x_padding) as usize, 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.overlay(
&Bitmap::from_data(
char_bitmap,
glyph.width,
glyph.height
),
glyph.x as usize + (scale*x_padding) 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 lazy_static::lazy_static;
use std::collections::VecDeque;
+2
View File
@@ -2,9 +2,11 @@ use fontdue::Font;
use lazy_static::lazy_static;
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! {
pub static ref FONTS: Vec<Font> = vec![
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 consts;
use std::time::Instant;
use fontdue::{layout::{CoordinateSystem, Layout, LayoutSettings}};
use crate::{bitmap::Bitmap, element::{KElement}};
@@ -16,15 +18,30 @@ fn main() -> Result<(), std::fmt::Error> {
fn parse_test() -> Result<(), std::fmt::Error> {
let tex_input = &std::env::args().nth(1).unwrap();
let mut start = Instant::now();
match KElement::parse(tex_input) {
Ok(result) => {
println!("Parse time: {:?}", start.elapsed());
start = Instant::now();
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),
}
Ok(())
}