1use boxworks::ds;
9use std::collections::HashMap;
10use tfm::ligkern;
11
12#[derive(Debug)]
13struct Font {
14 default_space: common::Glue,
15 extra_space: common::Scaled,
16 lig_kern_program: tfm::ligkern::CompiledProgram,
17}
18
19pub struct Params {
20 pub space_factor_codes: SpaceFactorCodes,
21 pub space_skip: common::Glue,
22 pub extra_space_skip: common::Glue,
23}
24
25impl Params {
26 pub fn tex(&self) -> String {
28 let Params {
29 space_factor_codes,
30 space_skip,
31 extra_space_skip,
32 } = self;
33 _ = space_factor_codes;
34 format!(
35 r"
36 \spaceskip={space_skip}
37 \xspaceskip={extra_space_skip}
38 "
39 )
40 }
41}
42
43impl Default for Params {
44 fn default() -> Self {
45 Self::plain_tex_defaults()
46 }
47}
48
49impl Params {
50 pub fn plain_tex_defaults() -> Self {
51 Self {
52 space_factor_codes: SpaceFactorCodes::plain_tex_defaults(),
53 space_skip: common::Glue::ZERO,
54 extra_space_skip: common::Glue::ZERO,
55 }
56 }
57}
58pub struct TextPreprocessorImpl {
59 fonts: Vec<Font>,
60 current_font: common::FontId,
62 space_factor: SpaceFactor,
63 pub params: Params,
64}
65
66impl TextPreprocessorImpl {
67 pub fn new(params: Params) -> Self {
68 Self {
69 fonts: vec![],
70 current_font: common::FontId::ONE,
71 space_factor: Default::default(),
72 params,
73 }
74 }
75}
76
77pub struct SpaceFactorCodes(pub [i32; 256]);
78
79impl Default for SpaceFactorCodes {
80 fn default() -> Self {
81 Self::plain_tex_defaults()
82 }
83}
84
85impl SpaceFactorCodes {
86 pub fn plain_tex_defaults() -> Self {
87 let mut a = [1000_i32; 256];
88 for (c, value) in [
89 (')', 0),
91 ('\'', 0),
92 (']', 0),
93 ('.', 3000),
95 ('?', 3000),
96 ('!', 3000),
97 (':', 2000),
98 (';', 1500),
99 (',', 1250),
100 ] {
101 a[c as usize] = value;
102 }
103 for c in 'A'..='Z' {
104 a[c as usize] = 999;
106 }
107 Self(a)
108 }
109}
110
111#[derive(Debug, PartialEq, Eq, Clone, Copy)]
112pub struct SpaceFactor(pub i32);
113
114impl Default for SpaceFactor {
115 fn default() -> Self {
116 Self(1000)
117 }
118}
119
120impl SpaceFactor {
121 fn adjust(&mut self, c: char, codes: &SpaceFactorCodes) {
122 let new: i32 = codes.0.get(c as usize).copied().unwrap_or(1000);
124 if new > 0 && new <= 1000 {
125 self.0 = new;
126 } else if new > 1000 {
127 if self.0 < 1000 {
128 self.0 = 1000
129 } else {
130 self.0 = new
131 }
132 }
133 }
134}
135
136impl TextPreprocessorImpl {
137 pub fn activate_font(&mut self, font: common::FontId) {
138 self.current_font = font;
139 }
140
141 fn current_font(&self) -> &Font {
146 &self.fonts[self.current_font.0 as usize - 1]
147 }
148}
149
150impl boxworks::TextPreprocessor for TextPreprocessorImpl {
151 fn new_paragraph(&mut self) {
152 self.space_factor = Default::default();
153 }
154
155 fn add_word(&mut self, word: &str, list: &mut Vec<ds::Horizontal>) {
156 let font = self.current_font();
157 for elem in font.lig_kern_program.run(word) {
158 use ligkern::RunItem::*;
159 match elem {
160 Char(c) => {
161 list.push(
162 ds::Char {
163 char: c,
164 font: self.current_font,
165 }
166 .into(),
167 );
168 if c == '-' {
171 list.push(ds::Discretionary::default().into());
172 }
173 }
174 Kern(kern) => {
175 list.push(
176 ds::Kern {
177 width: kern,
178 kind: ds::KernKind::Normal,
179 }
180 .into(),
181 );
182 }
183 Ligature(ligature) => {
184 let ins_disc = ligature.original.as_ref().ends_with('-');
185 list.push(
186 ds::Ligature {
187 char: ligature.c,
188 font: self.current_font,
189 original_chars: ligature.original,
190 includes_left_boundary: ligature.includes_left_boundary,
191 includes_right_boundary: ligature.includes_right_boundary,
192 }
193 .into(),
194 );
195 if ins_disc {
198 list.push(ds::Discretionary::default().into());
199 }
200 }
201 }
202 }
203 for c in word.chars() {
207 self.space_factor.adjust(c, &self.params.space_factor_codes);
208 }
209 }
210
211 fn add_space(&mut self, list: &mut Vec<ds::Horizontal>) {
212 let g = if self.space_factor == SpaceFactor::default() {
213 if !self.params.space_skip.is_zero() {
215 self.params.space_skip
216 } else {
217 self.current_font().default_space
219 }
220 } else {
221 if self.space_factor.0 >= 2000 && !self.params.extra_space_skip.is_zero() {
223 self.params.extra_space_skip
224 } else if !self.params.space_skip.is_zero() {
225 self.params.space_skip
226 } else {
227 let mut g = self.current_font().default_space;
229 if self.space_factor.0 >= 2000 {
231 g.width += self.current_font().extra_space;
232 }
233 g.stretch = g.stretch.xn_over_d(self.space_factor.0, 1000).unwrap().0;
234 g.shrink = g.shrink.xn_over_d(1000, self.space_factor.0).unwrap().0;
235 g
236 }
237 };
238 list.push(ds::Horizontal::Glue(g.into()));
239 }
240}
241
242impl TextPreprocessorImpl {
243 pub fn register_font(
244 &mut self,
245 id: common::FontId,
246 tfm_file: &tfm::File,
247 lig_kern_program: tfm::ligkern::CompiledProgram,
248 ) {
249 assert_eq!(id.0 as usize, self.fonts.len() + 1);
250 self.fonts.push(Font {
251 default_space: common::Glue {
252 width: tfm_file
253 .named_param_scaled(tfm::NamedParameter::Space)
254 .unwrap(),
255 stretch: tfm_file
256 .named_param_scaled(tfm::NamedParameter::Stretch)
257 .unwrap(),
258 stretch_order: common::GlueOrder::Normal,
259 shrink: tfm_file
260 .named_param_scaled(tfm::NamedParameter::Shrink)
261 .unwrap(),
262 shrink_order: common::GlueOrder::Normal,
263 },
264 extra_space: tfm_file
265 .named_param_scaled(tfm::NamedParameter::ExtraSpace)
266 .unwrap(),
267 lig_kern_program,
268 });
269 }
270}
271
272#[derive(Debug, Default)]
273pub struct TfmFontRepo {
274 fonts: HashMap<common::FontId, tfm::File>,
275}
276
277impl TfmFontRepo {
278 pub fn register_font(&mut self, id: common::FontId, tfm_file: tfm::File) {
279 assert_eq!(id.0 as usize, self.fonts.len() + 1);
280 self.fonts.insert(id, tfm_file);
281 }
282}
283
284impl boxworks::FontRepo for TfmFontRepo {
285 fn width(&self, c: char, font: common::FontId) -> Option<common::Scaled> {
286 self.fonts[&font].width_utf8(c)
287 }
288 fn height(&self, c: char, font: common::FontId) -> Option<common::Scaled> {
289 self.fonts[&font].height_utf8(c)
290 }
291 fn depth(&self, c: char, font: common::FontId) -> Option<common::Scaled> {
292 self.fonts[&font].depth_utf8(c)
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use boxworks::TextPreprocessor;
300 use boxworks_testing;
301 use boxworks_testing::assert_box_eq;
302 use boxworks_testing::assert_box_lossy_eq;
303
304 macro_rules! preprocessor_tests {
305 (
306 $namespace: ident,
307 $tfm: ident,
308 $( (
309 $name: ident,
310 $input: expr,
311 $want: expr,
312 $( params: Params {
313 $( $param_name: ident: $param_value: expr, )+
314 }, )?
315 ), )+ ) => {
316 mod $namespace {
317 use super::*;
318 $(
319 #[test]
320 fn $name() {
321 let tfm = super::$tfm;
322 let input = $input;
323 let want = $want;
324 let params = Params {
325 $( $(
326 $param_name: $param_value,
327 )+ )?
328 .. Params::plain_tex_defaults()
329 };
330 run_preprocessor_test(tfm, params, input, want)
331 }
332 )+
333 }
334 };
335 }
336
337 const TFM_CMR10: &'static [u8] = include_bytes!("../../tfm/corpus/computer-modern/cmr10.tfm");
338
339 preprocessor_tests!(
340 cmr10,
341 TFM_CMR10,
342 (
343 basic,
344 "second",
345 r#"
346 chars("second")
347 "#,
348 ),
349 (
350 basic_with_space,
351 "sec ond",
352 r#"
353 chars("sec")
354 glue(3.33333pt, 1.66666pt, 1.11111pt)
355 chars("ond")
356 "#,
357 ),
358 (
359 kern_ao,
360 "AO",
361 r#"
362 chars("A")
363 kern(-0.27779pt)
364 chars("O")
365 "#,
366 ),
367 (
368 kern_av,
369 "AV",
370 r#"
371 chars("A")
372 kern(-1.11113pt)
373 chars("V")
374 "#,
375 ),
376 (
377 ligature_1,
378 "ff",
379 r#"
380 lig("\u{b}", "ff")
381 "#,
382 ),
383 (
384 ligature_2,
385 "ffi",
386 r#"
387 lig("\u{e}", "ffi")
388 "#,
389 ),
390 (
391 ragged_right,
392 "a b. c",
393 r##"
394 chars("a")
395 glue(3.33298pt, 0.0pt, 0.0pt)
396 chars("b.")
397 glue(5.0pt, 0.0pt, 0.0pt)
398 chars("c")
399 "##,
400 params: Params {
401 space_skip: common::Glue {
402 width: common::Scaled::parse_from_string("3.33298pt").unwrap(),
403 ..Default::default()
404 },
405 extra_space_skip: common::Glue {
406 width: common::Scaled::parse_from_string("5.0pt").unwrap(),
407 ..Default::default()
408 },
409 },
410 ),
411 );
412
413 macro_rules! spacing_tests {
414 ( $( ( $name: ident, $input: expr, $want: expr, ), )+ ) => {
415 mod spacing {
416 $(
417 #[test]
418 fn $name() {
419 let tfm = super::TFM_CMR10;
420 let input = format!["{} a", $input];
421 let want = format![r#"
422 chars("{}")
423 {}
424 chars("a")
425 "#, $input, $want];
426 super::run_preprocessor_test(tfm, Default::default(), &input, &want)
427 }
428 )+
429 }
430 };
431 }
432
433 spacing_tests!(
434 (default_1, "a;", "glue(3.33333pt, 2.49998pt, 0.74074pt)",),
436 (default_2, "a,", "glue(3.33333pt, 2.08331pt, 0.88889pt)",),
437 (default_3, "a.", "glue(4.44444pt, 4.99997pt, 0.37036pt)",),
438 (default_4, "a:", "glue(4.44444pt, 3.33331pt, 0.55556pt)",),
439 (
445 adjust_space_factor_zero_zero,
446 "))",
447 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
448 ),
449 (
450 adjust_space_factor_zero_small,
451 ")A",
452 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
453 ),
454 (
455 adjust_space_factor_zero_normal,
456 ")a",
457 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
458 ),
459 (
460 adjust_space_factor_zero_large,
461 ").",
462 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
463 ),
464 (
465 adjust_space_factor_small_zero,
466 "A)",
467 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
468 ),
469 (
470 adjust_space_factor_small_small,
471 "AA",
472 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
473 ),
474 (
475 adjust_space_factor_small_normal,
476 "Aa",
477 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
478 ),
479 (
480 adjust_space_factor_small_large,
481 "A.",
482 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
483 ),
484 (
485 adjust_space_factor_normal_zero,
486 "a)",
487 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
488 ),
489 (
490 adjust_space_factor_normal_small,
491 "aA",
492 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
493 ),
494 (
495 adjust_space_factor_normal_normal,
496 "aa",
497 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
498 ),
499 (
500 adjust_space_factor_normal_large,
501 "a.",
502 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
503 ),
504 (
505 adjust_space_factor_large_zero,
506 ".)",
507 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
508 ),
509 (
510 adjust_space_factor_large_small,
511 ".A",
512 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
513 ),
514 (
515 adjust_space_factor_large_normal,
516 ".a",
517 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
518 ),
519 (
520 adjust_space_factor_large_large,
521 "..",
522 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
523 ),
524 );
525
526 const TFM_SMFEBSL: &'static [u8] = include_bytes!("../../tfm/corpus/ctan/smfebsl10-3.tfm");
527
528 preprocessor_tests!(
529 smfebsl,
530 TFM_SMFEBSL,
531 (
532 basic_with_space,
533 "sec ond",
534 r#"
535 chars("sec")
536 glue(4.78204pt, 2.39102pt, 1.19551pt)
537 chars("on")
538 kern(-0.49814pt)
539 chars("d")
540 "#,
541 ),
542 (
543 numbers_start_of_word,
544 "123B",
545 r##"
546 lig("$", "", includes_left_boundary="true")
547 chars("123")
548 lig("#", "")
549 chars("B")
550 "##,
551 ),
552 (
553 numbers_mid_word,
554 "A123B",
555 r##"
556 chars("A")
557 lig("$", "")
558 chars("123")
559 lig("#", "")
560 chars("B")
561 "##,
562 ),
563 (
564 numbers_end_of_word,
565 "A123",
566 r##"
567 chars("A")
568 lig("$", "")
569 chars("123")
570 lig("#", "", includes_right_boundary="true")
571 "##,
572 ),
573 );
574
575 fn run_preprocessor_test(tfm_bytes: &[u8], params: Params, input: &str, want: &str) {
576 if std::env::var("TEXCRAFT_VERIFY").unwrap_or_default() == "tex" {
577 use std::path::PathBuf;
578 let mut auxiliary_files: HashMap<PathBuf, Vec<u8>> = Default::default();
579 auxiliary_files.insert("customFont.tfm".into(), tfm_bytes.to_vec());
580 let preamble = format!(
581 r"
582 {}
583 \font \customFont customFont
584 \customFont
585 ",
586 params.tex(),
587 );
588 let mut tex_engine = boxworks::tex::new_tex_engine_binary("tex".to_string()).unwrap();
589 let (_, mut tex_got) = boxworks::tex::build_horizontal_lists(
590 tex_engine.as_mut(),
591 &auxiliary_files,
592 &preamble,
593 &mut [input.to_string()].iter(),
594 false,
595 );
596 let tex_got = tex_got.remove(0).list;
597 assert_box_lossy_eq!(want, tex_got);
601 return;
602 }
603
604 let mut tfm_file = tfm::File::deserialize(tfm_bytes).0.unwrap();
605 let lig_kern_program =
606 tfm::ligkern::CompiledProgram::compile_from_tfm_file(&mut tfm_file).0;
607
608 let mut tp = TextPreprocessorImpl::new(params);
609 tp.register_font(common::FontId::ONE, &tfm_file, lig_kern_program);
610 tp.activate_font(common::FontId::ONE);
611 let mut got = vec![];
612 for word in input.split_inclusive(' ') {
613 tp.add_word(word.trim_matches(' '), &mut got);
614 if word.ends_with(" ") {
615 tp.add_space(&mut got);
616 }
617 }
618
619 assert_box_eq!(got, want);
620 }
621}