use crate::ap::emit::Float;
use crate::ap::fmt;
use crate::vt::{Config, Layout, Metrics, Word, word_width};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Grouping {
Continuous,
PerCharacter,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Face {
pub index: i32,
pub alias: Vec<u8>,
pub bytes: Vec<u8>,
}
impl Face {
#[must_use]
pub fn single(alias: &[u8], bytes: Vec<u8>) -> Face {
Face {
index: 0,
alias: alias.to_vec(),
bytes,
}
}
}
#[must_use]
pub fn generate<F>(
layout: &Layout,
config: &Config,
metrics: &Metrics<'_>,
offset: (f32, f32),
grouping: Grouping,
face: F,
) -> String
where
F: Fn(u32) -> Face,
{
let mut out = String::new();
let mut line = String::new();
let mut words: Vec<u8> = Vec::new();
let (mut old_x, mut old_y) = (0.0_f32, 0.0_f32);
let mut current_font: i32 = -1;
let mut previous_place = (-1_i32, -1_i32);
for (section, line_index, word) in layout.words() {
let place = (
i32::try_from(section).unwrap_or(0),
i32::try_from(line_index).unwrap_or(0),
);
let (x, y) = position(layout, config, word, offset);
let face = face(shown(word, config));
if grouping == Grouping::Continuous && !word.is_rtl {
if place != previous_place {
if !words.is_empty() {
line.push_str(&render(&words));
out.push_str(&line);
line.clear();
words.clear();
}
if let Some(step) = step(x, y, old_x, old_y) {
line.push_str(&step);
old_x = x;
old_y = y;
}
} else if words.is_empty()
&& let Some(step) = step(x, y, old_x, old_y)
{
line.push_str(&step);
old_x = x;
old_y = y;
}
if face.index != current_font {
if !words.is_empty() {
line.push_str(&render(&words));
words.clear();
}
line.push_str(&font_op(&face.alias, layout.font_size));
current_font = face.index;
}
words.extend(face.bytes);
} else {
if !words.is_empty() {
line.push_str(&render(&words));
out.push_str(&line);
line.clear();
words.clear();
}
if let Some(step) = step(x, y, old_x, old_y) {
out.push_str(&step);
old_x = x;
old_y = y;
}
if face.index != current_font {
out.push_str(&font_op(&face.alias, layout.font_size));
current_font = face.index;
}
out.push_str(&render(&face.bytes));
}
previous_place = place;
let _ = word_width(word, config, metrics, layout.font_size);
}
line.push_str(&render(&words));
out.push_str(&line);
out
}
fn shown(word: &Word, config: &Config) -> u32 {
config.sub_word.map_or(word.ch, |sub| sub as u32)
}
fn position(layout: &Layout, config: &Config, word: &Word, offset: (f32, f32)) -> (f32, f32) {
let _ = layout;
let (x, y) = crate::vt::Layout::to_pdf(config.plate, word.x, word.y);
(x + offset.0, y + offset.1)
}
#[allow(clippy::float_cmp)]
fn step(x: f32, y: f32, old_x: f32, old_y: f32) -> Option<String> {
if x == old_x && y == old_y {
return None;
}
Some(format!(
"{} {} Td\n",
Float::Shortest.write(x - old_x),
Float::Shortest.write(y - old_y)
))
}
fn font_op(alias: &[u8], size: f32) -> String {
if alias.is_empty() || size <= 0.0 {
return String::new();
}
format!(
"/{} {} Tf\n",
String::from_utf8_lossy(alias),
fmt::shortest(size)
)
}
fn render(words: &[u8]) -> String {
if words.is_empty() {
return String::new();
}
let literal = pdfrum_object::encode_string_literal(words);
let mut out = String::with_capacity(literal.len() + 8);
for byte in literal {
if byte.is_ascii() {
out.push(char::from(byte));
} else {
use std::fmt::Write;
let _ = write!(out, "\\{byte:03o}");
}
}
out.push_str(" Tj\n");
out
}
#[cfg(test)]
mod tests {
use super::{Face, Grouping, generate};
use crate::geom;
use crate::vt::{Config, Layout, layout, stub};
fn one_byte(code: u32) -> Vec<u8> {
vec![u8::try_from(code).unwrap_or(b'?')]
}
fn laid(text: &str, config: &Config) -> Layout {
layout(text, config, &stub::metrics())
}
fn plate() -> Config {
Config {
plate: geom::rect(0.0, 0.0, 100.0, 100.0),
font_size: 10.0,
..Config::default()
}
}
#[test]
fn a_run_on_one_line_becomes_a_single_show_operator() {
let config = plate();
let got = generate(
&laid("hi", &config),
&config,
&stub::metrics(),
(0.0, 0.0),
Grouping::Continuous,
|code| Face::single(b"Helv", one_byte(code)),
);
assert_eq!(got.matches(" Tj\n").count(), 1);
assert!(got.contains("(hi) Tj\n"), "{got}");
assert!(got.contains("/Helv 10 Tf\n"), "{got}");
}
#[test]
fn per_character_grouping_writes_one_show_operator_each() {
let config = plate();
let got = generate(
&laid("hi", &config),
&config,
&stub::metrics(),
(0.0, 0.0),
Grouping::PerCharacter,
|code| Face::single(b"Helv", one_byte(code)),
);
assert_eq!(got.matches(" Tj\n").count(), 2);
}
#[test]
fn no_font_name_or_a_zero_size_suppresses_the_font_operator() {
let config = plate();
let nameless = generate(
&laid("hi", &config),
&config,
&stub::metrics(),
(0.0, 0.0),
Grouping::Continuous,
|code| Face::single(b"", one_byte(code)),
);
assert!(!nameless.contains(" Tf\n"), "{nameless}");
let mut zero = plate();
zero.plate = geom::rect(0.0, 0.0, 0.0, 100.0);
zero.font_size = 0.0;
let sized = generate(
&laid("hi", &zero),
&zero,
&stub::metrics(),
(0.0, 0.0),
Grouping::Continuous,
|code| Face::single(b"Helv", one_byte(code)),
);
assert!(!sized.contains(" Tf\n"), "{sized}");
}
#[test]
fn a_password_field_writes_its_substitute_rather_than_the_text() {
let config = Config {
sub_word: Some('*'),
..plate()
};
let got = generate(
&laid("hi", &config),
&config,
&stub::metrics(),
(0.0, 0.0),
Grouping::Continuous,
|code| Face::single(b"Helv", one_byte(code)),
);
assert!(got.contains("(**) Tj\n"), "{got}");
assert!(!got.contains('h'), "{got}");
}
#[test]
fn the_position_operator_is_relative_and_is_skipped_when_nothing_moves() {
let config = plate();
let got = generate(
&laid("hi", &config),
&config,
&stub::metrics(),
(0.0, 0.0),
Grouping::Continuous,
|code| Face::single(b"Helv", one_byte(code)),
);
assert_eq!(got.matches(" Td\n").count(), 1);
}
#[test]
fn each_line_gets_its_own_relative_move() {
let config = Config {
plate: geom::rect(0.0, 0.0, 0.25, 100.0),
auto_return: true,
..plate()
};
let got = generate(
&laid("hello", &config),
&config,
&stub::metrics(),
(0.0, 0.0),
Grouping::Continuous,
|code| Face::single(b"Helv", one_byte(code)),
);
assert_eq!(got.matches(" Td\n").count(), 3);
assert_eq!(got.matches(" Tj\n").count(), 3);
}
#[test]
fn the_offset_shifts_the_first_move_and_nothing_else() {
let config = plate();
let text = laid("hi", &config);
let plain = generate(
&text,
&config,
&stub::metrics(),
(0.0, 0.0),
Grouping::Continuous,
|code| Face::single(b"Helv", one_byte(code)),
);
let shifted = generate(
&text,
&config,
&stub::metrics(),
(3.0, -3.0),
Grouping::Continuous,
|code| Face::single(b"Helv", one_byte(code)),
);
assert_ne!(plain, shifted);
assert_eq!(
plain.matches(" Td\n").count(),
shifted.matches(" Td\n").count()
);
}
#[test]
fn empty_text_writes_nothing() {
let config = plate();
let got = generate(
&laid("", &config),
&config,
&stub::metrics(),
(0.0, 0.0),
Grouping::Continuous,
|code| Face::single(b"Helv", one_byte(code)),
);
assert_eq!(got, "");
}
}