use crate::ops::TextRenderMode;
use kurbo::{Affine, Point};
use pdfrum_font::Font;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct TextState {
pub font: Option<(Arc<Font>, f32)>,
pub font_source: Option<pdfrum_object::ObjRef>,
pub char_space: f32,
pub word_space: f32,
pub horz_scale: f32,
pub leading: f32,
pub rise: f32,
pub render_mode: TextRenderMode,
pub stroke_ctm: [f32; 4],
}
impl PartialEq for TextState {
fn eq(&self, other: &Self) -> bool {
let same_font = match (&self.font, &other.font) {
(Some((a, sa)), Some((b, sb))) => a.id() == b.id() && sa == sb,
(None, None) => true,
_ => false,
};
same_font
&& self.char_space == other.char_space
&& self.word_space == other.word_space
&& self.horz_scale == other.horz_scale
&& self.leading == other.leading
&& self.rise == other.rise
&& self.render_mode == other.render_mode
&& self.stroke_ctm == other.stroke_ctm
}
}
impl Default for TextState {
fn default() -> Self {
Self {
font: None,
font_source: None,
char_space: 0.0,
word_space: 0.0,
horz_scale: 1.0,
leading: 0.0,
rise: 0.0,
render_mode: TextRenderMode::Fill,
stroke_ctm: [1.0, 0.0, 0.0, 1.0],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextCursor {
pub matrix: Affine,
pub pos: Point,
pub line_pos: Point,
}
impl Default for TextCursor {
fn default() -> Self {
Self {
matrix: Affine::IDENTITY,
pos: Point::ZERO,
line_pos: Point::ZERO,
}
}
}
impl TextCursor {
pub fn set_matrix(&mut self, matrix: Affine) {
self.matrix = matrix;
self.pos = Point::ZERO;
self.line_pos = Point::ZERO;
}
pub fn move_line(&mut self, tx: f64, ty: f64) {
self.line_pos.x += tx;
self.line_pos.y += ty;
self.pos = self.line_pos;
}
pub fn next_line(&mut self, leading: f64) {
self.line_pos.y -= leading;
self.pos = self.line_pos;
}
#[must_use]
pub fn device_position(&self, rise: f32, ctm: Affine) -> Point {
let in_text = Point::new(self.pos.x, self.pos.y + f64::from(rise));
ctm * (self.matrix * in_text)
}
pub fn advance(&mut self, amount: f64, vertical: bool) {
if vertical {
self.pos.y += amount;
} else {
self.pos.x += amount;
}
}
}
#[must_use]
pub fn kerning_shift(kerning: f32, font_size: f32, horz_scale: f32, vertical: bool) -> f64 {
let base = f64::from(kerning) * f64::from(font_size) / 1000.0;
if vertical {
base
} else {
base * f64::from(horz_scale)
}
}
#[must_use]
pub fn glyph_matrix(horz_scale: f32, text_matrix: Affine, ctm: Affine) -> Affine {
let scale = Affine::new([f64::from(horz_scale), 0.0, 0.0, 1.0, 0.0, 0.0]);
ctm * text_matrix * scale
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{TextCursor, TextState, glyph_matrix, kerning_shift};
use crate::ops::TextRenderMode;
use kurbo::{Affine, Point};
#[test]
fn the_default_horizontal_scale_is_one_not_a_hundred() {
let s = TextState::default();
assert!((s.horz_scale - 1.0).abs() < 1e-6);
assert_eq!(s.render_mode, TextRenderMode::Fill);
assert_eq!(s.stroke_ctm, [1.0, 0.0, 0.0, 1.0]);
assert!(s.font.is_none());
}
#[test]
fn two_states_that_differ_only_in_stroke_ctm_are_unequal() {
let a = TextState::default();
let b = TextState {
stroke_ctm: [2.0, 0.0, 0.0, 3.0],
..TextState::default()
};
assert_ne!(a, b);
}
#[test]
fn td_accumulates_onto_the_line_start() {
let mut c = TextCursor::default();
c.move_line(10.0, -14.0);
assert_eq!(c.pos, Point::new(10.0, -14.0));
assert_eq!(c.line_pos, Point::new(10.0, -14.0));
c.move_line(5.0, -14.0);
assert_eq!(c.pos, Point::new(15.0, -28.0));
}
#[test]
fn t_star_returns_to_the_line_start_one_leading_down() {
let mut c = TextCursor::default();
c.move_line(10.0, 0.0);
c.advance(50.0, false);
assert_eq!(c.pos.x, 60.0);
c.next_line(14.0);
assert_eq!(c.pos, Point::new(10.0, -14.0));
}
#[test]
fn setting_the_matrix_resets_both_positions() {
let mut c = TextCursor::default();
c.move_line(10.0, 20.0);
c.set_matrix(Affine::translate((100.0, 200.0)));
assert_eq!(c.pos, Point::ZERO);
assert_eq!(c.line_pos, Point::ZERO);
}
#[test]
fn the_rise_goes_through_the_text_matrix() {
let c = TextCursor {
matrix: Affine::rotate(std::f64::consts::FRAC_PI_2),
..TextCursor::default()
};
let p = c.device_position(10.0, Affine::IDENTITY);
assert!(p.x.abs() > 9.0, "the rise should have been rotated: {p:?}");
assert!(p.y.abs() < 1e-6);
let flat = TextCursor::default().device_position(10.0, Affine::IDENTITY);
assert!((flat.y - 10.0).abs() < 1e-6);
}
#[test]
fn kerning_scales_by_size_and_by_the_horizontal_scale() {
assert!((kerning_shift(1000.0, 12.0, 1.0, false) - 12.0).abs() < 1e-6);
assert!((kerning_shift(1000.0, 12.0, 2.0, false) - 24.0).abs() < 1e-6);
assert!((kerning_shift(1000.0, 12.0, 2.0, true) - 12.0).abs() < 1e-6);
}
#[test]
fn the_glyph_matrix_applies_the_horizontal_scale_first() {
let m = glyph_matrix(2.0, Affine::IDENTITY, Affine::IDENTITY);
let p = m * Point::new(1.0, 1.0);
assert!((p.x - 2.0).abs() < 1e-6);
assert!((p.y - 1.0).abs() < 1e-6);
}
#[test]
fn vertical_writing_advances_y() {
let mut c = TextCursor::default();
c.advance(20.0, true);
assert_eq!(c.pos, Point::new(0.0, 20.0));
}
}