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: u32,
63 space_factor: SpaceFactor,
64 pub params: Params,
65}
66
67impl TextPreprocessorImpl {
68 pub fn new(params: Params) -> Self {
69 Self {
70 fonts: vec![],
71 current_font: 0,
72 space_factor: Default::default(),
73 params,
74 }
75 }
76}
77
78pub struct SpaceFactorCodes(pub [i32; 256]);
79
80impl Default for SpaceFactorCodes {
81 fn default() -> Self {
82 Self::plain_tex_defaults()
83 }
84}
85
86impl SpaceFactorCodes {
87 pub fn plain_tex_defaults() -> Self {
88 let mut a = [1000_i32; 256];
89 for (c, value) in [
90 (')', 0),
92 ('\'', 0),
93 (']', 0),
94 ('.', 3000),
96 ('?', 3000),
97 ('!', 3000),
98 (':', 2000),
99 (';', 1500),
100 (',', 1250),
101 ] {
102 a[c as usize] = value;
103 }
104 for c in 'A'..='Z' {
105 a[c as usize] = 999;
107 }
108 Self(a)
109 }
110}
111
112#[derive(Debug, PartialEq, Eq, Clone, Copy)]
113pub struct SpaceFactor(pub i32);
114
115impl Default for SpaceFactor {
116 fn default() -> Self {
117 Self(1000)
118 }
119}
120
121impl SpaceFactor {
122 fn adjust(&mut self, c: char, codes: &SpaceFactorCodes) {
123 let new: i32 = codes.0.get(c as usize).copied().unwrap_or(1000);
125 if new > 0 && new <= 1000 {
126 self.0 = new;
127 } else if new > 1000 {
128 if self.0 < 1000 {
129 self.0 = 1000
130 } else {
131 self.0 = new
132 }
133 }
134 }
135}
136
137impl TextPreprocessorImpl {
138 pub fn activate_font(&mut self, font: u32) {
139 self.current_font = font;
140 }
141}
142
143impl boxworks::TextPreprocessor for TextPreprocessorImpl {
144 fn new_paragraph(&mut self) {
145 self.space_factor = Default::default();
146 }
147
148 fn add_word(&mut self, word: &str, list: &mut Vec<ds::Horizontal>) {
149 let font = &self.fonts[self.current_font as usize];
150
151 struct Emitter<'a>(&'a mut Vec<ds::Horizontal>, u32);
152 impl<'a> ligkern::Emitter for Emitter<'a> {
153 fn emit_character(&mut self, c: char) {
154 self.0.push(
155 ds::Char {
156 char: c,
157 font: self.1,
158 }
159 .into(),
160 );
161 if c == '-' {
164 self.0.push(ds::Discretionary::default().into());
165 }
166 }
167 fn emit_kern(&mut self, kern: common::Scaled) {
168 self.0.push(
169 ds::Kern {
170 width: kern,
171 kind: ds::KernKind::Normal,
172 }
173 .into(),
174 );
175 }
176 fn emit_ligature(&mut self, ligature: ligkern::Ligature) {
177 let ins_disc = ligature.original.as_ref().ends_with('-');
178 self.0.push(
179 ds::Ligature {
180 included_left_boundary: false,
181 included_right_boundary: false,
182 char: ligature.c,
183 font: self.1,
184 original_chars: ligature.original,
185 }
186 .into(),
187 );
188 if ins_disc {
191 self.0.push(ds::Discretionary::default().into());
192 }
193 }
194 }
195
196 let mut e = Emitter(list, self.current_font);
197 font.lig_kern_program.run(word, &mut e);
198 for c in word.chars() {
202 self.space_factor.adjust(c, &self.params.space_factor_codes);
203 }
204 }
205
206 fn add_space(&mut self, list: &mut Vec<ds::Horizontal>) {
207 let g = if self.space_factor == SpaceFactor::default() {
208 if !self.params.space_skip.is_zero() {
210 self.params.space_skip
211 } else {
212 self.fonts[self.current_font as usize].default_space
214 }
215 } else {
216 if self.space_factor.0 >= 2000 && !self.params.extra_space_skip.is_zero() {
218 self.params.extra_space_skip
219 } else if !self.params.space_skip.is_zero() {
220 self.params.space_skip
221 } else {
222 let mut g = self.fonts[self.current_font as usize].default_space;
224 if self.space_factor.0 >= 2000 {
226 g.width += self.fonts[self.current_font as usize].extra_space;
227 }
228 g.stretch = g.stretch.xn_over_d(self.space_factor.0, 1000).unwrap().0;
229 g.shrink = g.shrink.xn_over_d(1000, self.space_factor.0).unwrap().0;
230 g
231 }
232 };
233 list.push(ds::Horizontal::Glue(g.into()));
234 }
235}
236
237impl TextPreprocessorImpl {
238 pub fn register_font(
239 &mut self,
240 id: u32,
241 tfm_file: &tfm::File,
242 lig_kern_program: tfm::ligkern::CompiledProgram,
243 ) {
244 assert_eq!(id as usize, self.fonts.len());
245 self.fonts.push(Font {
246 default_space: common::Glue {
247 width: tfm_file
248 .named_param_scaled(tfm::NamedParameter::Space)
249 .unwrap(),
250 stretch: tfm_file
251 .named_param_scaled(tfm::NamedParameter::Stretch)
252 .unwrap(),
253 stretch_order: common::GlueOrder::Normal,
254 shrink: tfm_file
255 .named_param_scaled(tfm::NamedParameter::Shrink)
256 .unwrap(),
257 shrink_order: common::GlueOrder::Normal,
258 },
259 extra_space: tfm_file
260 .named_param_scaled(tfm::NamedParameter::ExtraSpace)
261 .unwrap(),
262 lig_kern_program,
263 });
264 }
265}
266
267#[derive(Debug, Default)]
268pub struct TfmFontRepo {
269 fonts: HashMap<u32, tfm::File>,
270}
271
272impl TfmFontRepo {
273 pub fn register_font(&mut self, id: u32, tfm_file: tfm::File) {
274 assert_eq!(id as usize, self.fonts.len());
275 self.fonts.insert(id, tfm_file);
276 }
277}
278
279impl boxworks::FontRepo for TfmFontRepo {
280 fn width(&self, c: char, font: u32) -> Option<common::Scaled> {
281 self.fonts[&font].width_utf8(c)
282 }
283 fn height(&self, c: char, font: u32) -> Option<common::Scaled> {
284 self.fonts[&font].height_utf8(c)
285 }
286 fn depth(&self, c: char, font: u32) -> Option<common::Scaled> {
287 self.fonts[&font].depth_utf8(c)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use boxworks::TextPreprocessor;
295 use boxworks_testing;
296 use boxworks_testing::assert_box_eq;
297
298 macro_rules! preprocessor_tests {
299 (
300 $namespace: ident,
301 $tfm: ident,
302 $( (
303 $name: ident,
304 $input: expr,
305 $want: expr,
306 $( params: Params {
307 $( $param_name: ident: $param_value: expr, )+
308 }, )?
309 ), )+ ) => {
310 mod $namespace {
311 use super::*;
312 $(
313 #[test]
314 fn $name() {
315 let tfm = super::$tfm;
316 let input = $input;
317 let want = $want;
318 let params = Params {
319 $( $(
320 $param_name: $param_value,
321 )+ )?
322 .. Params::plain_tex_defaults()
323 };
324 run_preprocessor_test(tfm, params, input, want)
325 }
326 )+
327 }
328 };
329 }
330
331 const TFM_CMR10: &'static [u8] = include_bytes!("../../tfm/corpus/computer-modern/cmr10.tfm");
332
333 preprocessor_tests!(
334 cmr10,
335 TFM_CMR10,
336 (
337 basic,
338 "second",
339 r#"
340 chars("second", font=0)
341 "#,
342 ),
343 (
344 basic_with_space,
345 "sec ond",
346 r#"
347 chars("sec", font=0)
348 glue(3.33333pt, 1.66666pt, 1.11111pt)
349 chars("ond", font=0)
350 "#,
351 ),
352 (
353 kern_ao,
354 "AO",
355 r#"
356 chars("A", font=0)
357 kern(-0.27779pt)
358 chars("O", font=0)
359 "#,
360 ),
361 (
362 kern_av,
363 "AV",
364 r#"
365 chars("A", font=0)
366 kern(-1.11113pt)
367 chars("V", font=0)
368 "#,
369 ),
370 (
371 ligature_1,
372 "ff",
373 r#"
374 lig("\u{b}", "ff", font=0)
375 "#,
376 ),
377 (
378 ligature_2,
379 "ffi",
380 r#"
381 lig("\u{e}", "ffi", font=0)
382 "#,
383 ),
384 (
385 ragged_right,
386 "a b. c",
387 r##"
388 chars("a", font=0)
389 glue(3.33298pt, 0.0pt, 0.0pt)
390 chars("b.", font=0)
391 glue(5.0pt, 0.0pt, 0.0pt)
392 chars("c", font=0)
393 "##,
394 params: Params {
395 space_skip: common::Glue {
396 width: common::Scaled::parse_from_string("3.33298pt").unwrap(),
397 ..Default::default()
398 },
399 extra_space_skip: common::Glue {
400 width: common::Scaled::parse_from_string("5.0pt").unwrap(),
401 ..Default::default()
402 },
403 },
404 ),
405 );
406
407 macro_rules! spacing_tests {
408 ( $( ( $name: ident, $input: expr, $want: expr, ), )+ ) => {
409 mod spacing {
410 $(
411 #[test]
412 fn $name() {
413 let tfm = super::TFM_CMR10;
414 let input = format!["{} a", $input];
415 let want = format![r#"
416 chars("{}", font=0)
417 {}
418 chars("a", font=0)
419 "#, $input, $want];
420 super::run_preprocessor_test(tfm, Default::default(), &input, &want)
421 }
422 )+
423 }
424 };
425 }
426
427 spacing_tests!(
428 (default_1, "a;", "glue(3.33333pt, 2.49998pt, 0.74074pt)",),
430 (default_2, "a,", "glue(3.33333pt, 2.08331pt, 0.88889pt)",),
431 (default_3, "a.", "glue(4.44444pt, 4.99997pt, 0.37036pt)",),
432 (default_4, "a:", "glue(4.44444pt, 3.33331pt, 0.55556pt)",),
433 (
439 adjust_space_factor_zero_zero,
440 "))",
441 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
442 ),
443 (
444 adjust_space_factor_zero_small,
445 ")A",
446 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
447 ),
448 (
449 adjust_space_factor_zero_normal,
450 ")a",
451 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
452 ),
453 (
454 adjust_space_factor_zero_large,
455 ").",
456 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
457 ),
458 (
459 adjust_space_factor_small_zero,
460 "A)",
461 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
462 ),
463 (
464 adjust_space_factor_small_small,
465 "AA",
466 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
467 ),
468 (
469 adjust_space_factor_small_normal,
470 "Aa",
471 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
472 ),
473 (
474 adjust_space_factor_small_large,
475 "A.",
476 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
477 ),
478 (
479 adjust_space_factor_normal_zero,
480 "a)",
481 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
482 ),
483 (
484 adjust_space_factor_normal_small,
485 "aA",
486 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
487 ),
488 (
489 adjust_space_factor_normal_normal,
490 "aa",
491 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
492 ),
493 (
494 adjust_space_factor_normal_large,
495 "a.",
496 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
497 ),
498 (
499 adjust_space_factor_large_zero,
500 ".)",
501 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
502 ),
503 (
504 adjust_space_factor_large_small,
505 ".A",
506 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
507 ),
508 (
509 adjust_space_factor_large_normal,
510 ".a",
511 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
512 ),
513 (
514 adjust_space_factor_large_large,
515 "..",
516 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
517 ),
518 );
519
520 const TFM_SMFEBSL: &'static [u8] = include_bytes!("../../tfm/corpus/ctan/smfebsl10-3.tfm");
521
522 preprocessor_tests!(
523 smfebsl,
524 TFM_SMFEBSL,
525 (
526 basic_with_space,
527 "sec ond",
528 r#"
529 chars("sec", font=0)
530 glue(4.78204pt, 2.39102pt, 1.19551pt)
531 chars("on", font=0)
532 kern(-0.49814pt)
533 chars("d", font=0)
534 "#,
535 ),
536 (
537 numbers_start_of_word,
538 "123B",
539 r##"
540 lig("$", "|", font=0)
541 chars("123", font=0)
542 lig("#", "", font=0)
543 chars("B", font=0)
544 "##,
545 ),
546 (
547 numbers_mid_word,
548 "A123B",
549 r##"
550 chars("A", font=0)
551 lig("$", "", font=0)
552 chars("123", font=0)
553 lig("#", "", font=0)
554 chars("B", font=0)
555 "##,
556 ),
557 );
570
571 fn run_preprocessor_test(tfm_bytes: &[u8], params: Params, input: &str, want: &str) {
572 let mut tfm_file = tfm::File::deserialize(tfm_bytes).0.unwrap();
573 let lig_kern_program =
574 tfm::ligkern::CompiledProgram::compile_from_tfm_file(&mut tfm_file).0;
575
576 let mut tp = TextPreprocessorImpl::new(params);
577 tp.register_font(0, &tfm_file, lig_kern_program);
578 tp.activate_font(0);
579 let mut got = vec![];
580 for word in input.split_inclusive(' ') {
581 tp.add_word(word.trim_matches(' '), &mut got);
582 if word.ends_with(" ") {
583 tp.add_space(&mut got);
584 }
585 }
586
587 assert_box_eq!(got, want);
588 }
589}