use pdfium::{Document, Font, FontType, Matrix, Page, RectF, TextPage};
use crate::GlyphResolver;
use crate::extract::{CharInfoChunks, CharView, is_buggy_codepoint, is_buggy_font};
use crate::glyph_names::resolve_glyph_name_codepoint;
const ANGLE_SPLIT_RADIANS: f32 = 0.01;
#[derive(Debug, Clone, PartialEq)]
pub struct RawTextItem {
pub text: String,
pub char_codes: Vec<u32>,
pub glyph_names: Option<Vec<String>>,
pub angle_radians: f32,
pub text_width: f32,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub grounding_bounds: Option<RectF>,
pub baseline_gap: Option<f64>,
pub mcid: Option<i32>,
pub font_name: String,
pub font_size: f32,
pub font_weight: i32,
pub font_height: f32,
pub font_ascent: f32,
pub font_descent: f32,
pub font_is_buggy: bool,
pub trailing_space_generated: bool,
pub fill_color: Option<u32>,
pub stroke_color: Option<u32>,
}
struct Glyph {
index: i32,
text_object: Option<pdfium::pdfium_sys::FPDF_PAGEOBJECT>,
generated: bool,
char_code: u32,
unicode: u32,
loose: RectF,
strict: RectF,
angle: f32,
}
pub fn extract_raw_text_items(
page: &Page,
text_page: &TextPage,
view_box: &RectF,
glyph_resolver: Option<&dyn GlyphResolver>,
) -> Vec<RawTextItem> {
let char_count = text_page.char_count();
let page_rotation = page.rotation();
let mut chunks = CharInfoChunks::new(text_page);
let mut items = Vec::new();
let mut run: Vec<Glyph> = Vec::new();
let mut outline_cache = OutlineSupportCache::default();
let mut flush = |run: &mut Vec<Glyph>, items: &mut Vec<RawTextItem>| {
if run.is_empty() {
return;
}
if let Some(item) = build_item(
page,
text_page,
view_box,
run,
glyph_resolver,
&mut outline_cache,
) {
items.push(item);
}
run.clear();
};
for i in 0..char_count {
let ch = text_page.char_at_unchecked(i);
let cv = CharView {
ch: &ch,
rec: chunks.as_mut().and_then(|chunks| chunks.record(i)),
};
let glyph = load_glyph(page, view_box, page_rotation, &cv, i);
if let Some(first) = run.first()
&& (first.text_object != glyph.text_object
|| (first.angle - glyph.angle).abs() > ANGLE_SPLIT_RADIANS)
{
flush(&mut run, &mut items);
}
if glyph.generated && matches!(glyph.unicode, 0x0A | 0x0D) {
flush(&mut run, &mut items);
continue;
}
let closes_item = glyph.generated || is_c_locale_space(glyph.unicode);
run.push(glyph);
if closes_item {
flush(&mut run, &mut items);
}
}
flush(&mut run, &mut items);
items
}
fn load_glyph(
page: &Page,
view_box: &RectF,
page_rotation: i32,
cv: &CharView<'_, '_>,
index: i32,
) -> Glyph {
let generated = cv.is_generated();
let char_code = if generated { 0 } else { cv.char_code() };
let unicode = if cv.has_unicode_map_error() {
0
} else {
cv.unicode()
};
let strict = cv.strict_char_box().unwrap_or_default();
let loose = cv.loose_char_box().unwrap_or(strict);
let angle = match cv.ch.matrix() {
Some(matrix) => baseline_angle_radians(&matrix),
None => f64::from(cv.ch.angle()),
};
Glyph {
index,
text_object: cv.text_object(),
generated,
char_code,
unicode,
loose: page.bounds_to_viewport(view_box, &loose),
strict: page.bounds_to_viewport(view_box, &strict),
angle: normalize_angle(angle, page_rotation),
}
}
fn baseline_angle_radians(m: &Matrix) -> f64 {
use std::f64::consts::PI;
let degrees = (f64::from(m.b).atan2(f64::from(m.a)) * 180.0 / PI + 360.0) % 360.0;
-degrees * PI / 180.0
}
fn decompose_scale_single_precision_products(m: &pdfium::Matrix) -> (f32, f32) {
let a = f64::from(m.a * m.a + m.b * m.b);
let b = f64::from(m.a * m.c + m.b * m.d);
let c = f64::from(m.c * m.a + m.d * m.b);
let d = f64::from(m.c * m.c + m.d * m.d);
let first = (a + d) / 2.0;
let second = ((a + d) * (a + d) - 4.0 * (a * d - c * b)).sqrt() / 2.0;
let sx = first + second;
let sx = if sx.is_nan() { 1.0 } else { sx };
let sy = first - second;
let sy = if sy.is_nan() { 1.0 } else { sy };
(sx.sqrt() as f32, sy.sqrt() as f32)
}
fn normalize_angle(angle_radians: f64, page_rotation: i32) -> f32 {
use std::f64::consts::PI;
let mut angle = angle_radians;
match page_rotation {
1 => angle -= 3.0 * PI / 2.0,
2 => angle -= PI,
3 => angle -= PI / 2.0,
_ => {}
}
angle %= 2.0 * PI;
if angle < 0.0 {
angle += 2.0 * PI;
}
angle as f32
}
fn is_c_locale_space(unicode: u32) -> bool {
matches!(unicode, 0x09..=0x0D | 0x20)
}
fn pack_argb(color: pdfium::Color) -> u32 {
u32::from_be_bytes([color.a, color.r, color.g, color.b])
}
fn build_item(
page: &Page,
text_page: &TextPage,
view_box: &RectF,
glyphs: &[Glyph],
glyph_resolver: Option<&dyn GlyphResolver>,
outline_cache: &mut OutlineSupportCache,
) -> Option<RawTextItem> {
let first = glyphs.first()?;
let first_char = text_page.char_at_unchecked(first.index);
let font_size = first_char.font_size() as f32;
let font = first
.text_object
.and_then(|obj| unsafe { Font::from_text_object(obj) });
let mut bounds = first.loose;
let mut text_width = 0.0f32;
let mut font_name = String::new();
let mut font_ascent = 0.0f32;
let mut font_descent = 0.0f32;
let mut font_is_embedded = false;
let mut font_is_buggy = false;
if let Some(font) = &font {
font_name = font.base_name().unwrap_or_default();
font_ascent = font.ascent(font_size).unwrap_or(0.0);
font_descent = font.descent(font_size).unwrap_or(0.0);
font_is_embedded = font.is_embedded();
font_is_buggy = font_is_embedded && is_buggy_font(&font_name, font.font_type());
if let Some(width) = font.glyph_width_from_char_code(first.char_code, font_size) {
text_width += width;
}
}
let font_height = if first.text_object.is_some() {
let scale_y = first_char
.matrix()
.map(|matrix| decompose_scale_single_precision_products(&matrix).1)
.unwrap_or(1.0);
font_size * scale_y
} else {
0.0
};
for glyph in &glyphs[1..] {
bounds.left = bounds.left.min(glyph.loose.left);
bounds.top = bounds.top.min(glyph.loose.top);
bounds.right = bounds.right.max(glyph.loose.right);
bounds.bottom = bounds.bottom.max(glyph.loose.bottom);
if let Some(font) = &font {
let width = if glyph.generated {
font.glyph_width(glyph.unicode, font_size)
} else {
font.glyph_width_from_char_code(glyph.char_code, font_size)
};
if let Some(width) = width {
text_width += width;
}
}
}
if font_is_embedded && !font_is_buggy {
font_is_buggy = glyphs
.iter()
.any(|glyph| !glyph.generated && is_buggy_codepoint(glyph.unicode));
}
let mut codepoints: Vec<u32> = glyphs.iter().map(|glyph| glyph.unicode).collect();
if font_is_buggy && let (Some(font), Some(resolver)) = (&font, glyph_resolver) {
let is_type3 = font.font_type() == FontType::Type3;
for (glyph, codepoint) in glyphs.iter().zip(codepoints.iter_mut()) {
if glyph.generated {
continue;
}
let identified = is_type3
.then(|| type3_glyph_codepoint(font, glyph.char_code))
.flatten()
.or_else(|| identify_glyph(resolver, font, glyph.char_code));
*codepoint = identified.unwrap_or(u32::from(' '));
}
}
let text = c_string_from_utf32(&codepoints)?;
let grounding_bounds = grounding_bounds(glyphs, &codepoints, first.angle, |glyph, c, s| {
glyph_outline_support(page, text_page, view_box, glyph, c, s, outline_cache)
});
let baseline_gap = if glyphs.len() == 1 && first.generated && first.unicode == u32::from(' ') {
measure_baseline_gap(text_page, first.index)
} else {
None
};
let glyph_names = font
.as_ref()
.filter(|font| font.font_type() == FontType::Type3)
.map(|font| {
glyphs
.iter()
.map(|glyph| font.char_glyph_name(glyph.char_code).unwrap_or_default())
.collect()
});
Some(RawTextItem {
text,
char_codes: glyphs.iter().map(|glyph| glyph.char_code).collect(),
glyph_names,
angle_radians: first.angle,
text_width,
x: bounds.left,
y: bounds.top,
width: bounds.right - bounds.left,
height: bounds.bottom - bounds.top,
grounding_bounds,
baseline_gap,
mcid: first_char.marked_content_id(),
font_name,
font_size,
font_weight: first_char.font_weight(),
font_height,
font_ascent,
font_descent,
font_is_buggy,
trailing_space_generated: glyphs.last().is_some_and(|glyph| glyph.generated),
fill_color: first_char.fill_color().map(pack_argb),
stroke_color: first_char.stroke_color().map(pack_argb),
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct BaselineSupport {
min_along: f64,
max_along: f64,
min_across: f64,
max_across: f64,
}
impl BaselineSupport {
const EMPTY: Self = Self {
min_along: f64::INFINITY,
max_along: f64::NEG_INFINITY,
min_across: f64::INFINITY,
max_across: f64::NEG_INFINITY,
};
fn include(&mut self, along: f64, across: f64) {
self.min_along = self.min_along.min(along);
self.max_along = self.max_along.max(along);
self.min_across = self.min_across.min(across);
self.max_across = self.max_across.max(across);
}
fn translate(self, along: f64, across: f64) -> Self {
Self {
min_along: self.min_along + along,
max_along: self.max_along + along,
min_across: self.min_across + across,
max_across: self.max_across + across,
}
}
}
struct OutlineShape {
font: usize,
char_code: u32,
size_bits: u32,
matrix_bits: [u32; 4],
cos_bits: u64,
sin_bits: u64,
support: BaselineSupport,
}
pub(crate) struct OutlineSupportCache {
slots: Vec<Option<OutlineShape>>,
}
impl Default for OutlineSupportCache {
fn default() -> Self {
Self {
slots: (0..256).map(|_| None).collect(),
}
}
}
fn glyph_outline_support(
page: &Page,
text_page: &TextPage,
view_box: &RectF,
glyph: &Glyph,
c: f64,
s: f64,
cache: &mut OutlineSupportCache,
) -> Option<BaselineSupport> {
let font = unsafe { Font::from_text_object(glyph.text_object?) }?;
let ch = text_page.char_at_unchecked(glyph.index);
let matrix = ch.matrix()?;
let linear = [matrix.a, matrix.b, matrix.c, matrix.d];
if !linear.iter().all(|v| v.is_finite()) {
return None;
}
let axes = f64::from(matrix.a).hypot(f64::from(matrix.b))
* f64::from(matrix.c).hypot(f64::from(matrix.d));
let shear = f64::from(matrix.a * matrix.c + matrix.b * matrix.d);
if (c * s).abs() <= 1e-6 && shear.abs() <= 1e-6 * axes {
return None;
}
match ch.text_render_mode() {
Some(0 | 3 | 4 | 7) => {}
_ => return None,
}
match font.font_type() {
FontType::Type1 | FontType::TrueType => {}
FontType::CidType0 | FontType::CidType2 => {
if font.encoding().as_deref() != Some("Identity-H") {
return None;
}
}
_ => return None,
}
let size = ch.font_size() as f32;
if !size.is_finite() || size <= 0.0 {
return None;
}
let font_address = font.handle() as usize;
let matrix_bits = linear.map(f32::to_bits);
let slot = &mut cache.slots[(glyph.char_code % 256) as usize];
let cached = slot.as_ref().filter(|shape| {
shape.font == font_address
&& shape.char_code == glyph.char_code
&& shape.size_bits == size.to_bits()
&& shape.matrix_bits == matrix_bits
&& shape.cos_bits == c.to_bits()
&& shape.sin_bits == s.to_bits()
});
let support = match cached {
Some(shape) => shape.support,
None => {
let segments = font.glyph_path_segments(glyph.char_code, size)?;
if segments.is_empty() {
return None;
}
let (vx, vy) = page.page_to_viewport(view_box, 0.0, 0.0);
let (ux, uy) = page.page_to_viewport(view_box, size * matrix.a, size * matrix.b);
let (wx, wy) = page.page_to_viewport(view_box, size * matrix.c, size * matrix.d);
let mut support = BaselineSupport::EMPTY;
for &(_, x, y) in &segments {
if !x.is_finite() || !y.is_finite() {
return None;
}
let px = f64::from(x * (ux - vx) + y * (wx - vx));
let py = f64::from(x * (uy - vy) + y * (wy - vy));
support.include(px * c + py * s, py * c - px * s);
}
if !(support.min_along < support.max_along && support.min_across < support.max_across) {
return None;
}
*slot = Some(OutlineShape {
font: font_address,
char_code: glyph.char_code,
size_bits: size.to_bits(),
matrix_bits,
cos_bits: c.to_bits(),
sin_bits: s.to_bits(),
support,
});
support
}
};
let (ox, oy) = ch.origin()?;
if !ox.is_finite() || !oy.is_finite() {
return None;
}
let (x, y) = page.page_to_viewport(view_box, ox as f32, oy as f32);
let (x, y) = (f64::from(x), f64::from(y));
Some(support.translate(x * c + y * s, y * c - x * s))
}
fn grounding_bounds(
glyphs: &[Glyph],
codepoints: &[u32],
angle_radians: f32,
mut outline: impl FnMut(&Glyph, f64, f64) -> Option<BaselineSupport>,
) -> Option<RectF> {
let (c, s) = (
f64::from(angle_radians).cos(),
f64::from(angle_radians).sin(),
);
let (mut min_along, mut max_along) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut min_across, mut max_across) = (f64::INFINITY, f64::NEG_INFINITY);
for (glyph, &codepoint) in glyphs.iter().zip(codepoints) {
let b = glyph.strict;
if glyph.generated
|| codepoint == 0
|| is_c_locale_space(codepoint)
|| !(b.left.is_finite()
&& b.right.is_finite()
&& b.top.is_finite()
&& b.bottom.is_finite())
|| b.right <= b.left
|| b.bottom <= b.top
{
continue;
}
if let Some(support) = outline(glyph, c, s) {
min_along = min_along.min(support.min_along);
max_along = max_along.max(support.max_along);
min_across = min_across.min(support.min_across);
max_across = max_across.max(support.max_across);
continue;
}
for corner in 0..4 {
let x = f64::from(if corner & 1 != 0 { b.right } else { b.left });
let y = f64::from(if corner & 2 != 0 { b.bottom } else { b.top });
let along = x * c + y * s;
let across = y * c - x * s;
min_along = min_along.min(along);
max_along = max_along.max(along);
min_across = min_across.min(across);
max_across = max_across.max(across);
}
}
if min_along > max_along {
return None;
}
let along = (min_along + max_along) / 2.0;
let across = (min_across + max_across) / 2.0;
let cx = along * c - across * s;
let cy = along * s + across * c;
let w = max_along - min_along;
let h = max_across - min_across;
Some(RectF {
left: (cx - w / 2.0) as f32,
right: (cx + w / 2.0) as f32,
top: (cy - h / 2.0) as f32,
bottom: (cy + h / 2.0) as f32,
})
}
struct GapNeighbour {
font: Font,
char_code: u32,
matrix: Matrix,
scale: f64,
x: f64,
y: f64,
}
impl GapNeighbour {
fn read(text_page: &TextPage, index: i32) -> Option<Self> {
let ch = text_page.char_at_unchecked(index);
let codepoint = ch.unicode();
if ch.is_generated() || codepoint == 0 || is_c_locale_space(codepoint) {
return None;
}
let font = unsafe { Font::from_text_object(ch.text_object()?) }?;
match font.font_type() {
FontType::Type1 | FontType::TrueType => {}
FontType::CidType0 | FontType::CidType2 => {
if font.encoding().as_deref() != Some("Identity-H") {
return None;
}
}
_ => return None,
}
let matrix = ch.matrix()?;
let (x, y) = ch.origin()?;
let scale = f64::from(matrix.a).hypot(f64::from(matrix.b));
if !scale.is_finite() || scale <= 0.0 || !x.is_finite() || !y.is_finite() {
return None;
}
Some(Self {
font,
char_code: ch.char_code(),
matrix,
scale,
x,
y,
})
}
}
fn measure_baseline_gap(text_page: &TextPage, index: i32) -> Option<f64> {
let (previous, next) = (index - 1, index + 1);
if previous < 0 || next >= text_page.char_count() {
return None;
}
let p = GapNeighbour::read(text_page, previous)?;
let n = GapNeighbour::read(text_page, next)?;
let same_direction = (f64::from(p.matrix.a) / p.scale - f64::from(n.matrix.a) / n.scale).abs()
< 1e-6
&& (f64::from(p.matrix.b) / p.scale - f64::from(n.matrix.b) / n.scale).abs() < 1e-6;
if !same_direction {
return None;
}
let size = text_page.char_at_unchecked(previous).font_size() as f32;
let width = p.font.glyph_width_from_char_code(p.char_code, size)?;
let displacement =
((n.x - p.x) * f64::from(p.matrix.a) + (n.y - p.y) * f64::from(p.matrix.b)) / p.scale;
let gap = displacement - f64::from(width) * p.scale;
(displacement >= 0.0 && size.is_finite() && width.is_finite() && gap.is_finite()).then_some(gap)
}
pub fn append_raw_widget_text_items(
doc: &Document<'_>,
page: &Page<'_, '_>,
page_index: i32,
glyph_resolver: Option<&dyn GlyphResolver>,
items: &mut Vec<RawTextItem>,
) -> bool {
if !page.has_form_widget_text() {
return true;
}
let Some(copy) = doc.widget_appearance_copy(page_index) else {
return false;
};
let Ok(copy_page) = copy.page(0) else {
return false;
};
let Some(view_box) = copy_page.view_box() else {
return false;
};
let Ok(text_page) = copy_page.text() else {
return false;
};
let widgets = extract_raw_text_items(©_page, &text_page, &view_box, glyph_resolver);
let original_count = items.len();
for mut item in widgets {
if items[..original_count]
.iter()
.any(|existing| same_text_item(&item, existing))
{
continue;
}
item.mcid = None;
items.push(item);
}
true
}
fn same_text_item(a: &RawTextItem, b: &RawTextItem) -> bool {
let a_text = a.text.trim_end_matches(is_c_locale_space_char);
let b_text = b.text.trim_end_matches(is_c_locale_space_char);
!a_text.is_empty()
&& a_text == b_text
&& (a.x - b.x).abs() < 0.01
&& (a.y - b.y).abs() < 0.01
&& ((a.x + a.width) - (b.x + b.width)).abs() < 0.01
&& ((a.y + a.height) - (b.y + b.height)).abs() < 0.01
&& (a.angle_radians - b.angle_radians).abs() < 0.001
}
fn is_c_locale_space_char(c: char) -> bool {
is_c_locale_space(u32::from(c))
}
fn type3_glyph_codepoint(font: &Font, char_code: u32) -> Option<u32> {
let name = font.char_glyph_name(char_code)?;
resolve_glyph_name_codepoint(&name)
}
fn identify_glyph(resolver: &dyn GlyphResolver, font: &Font, char_code: u32) -> Option<u32> {
let segments = font.glyph_path_segments(char_code, crate::GLYPH_RESOLVER_FONT_SIZE)?;
resolver.resolve_codepoint(&segments)
}
fn c_string_from_utf32(codepoints: &[u32]) -> Option<String> {
let mut chars = Vec::with_capacity(codepoints.len());
for &codepoint in codepoints {
match char::from_u32(codepoint) {
Some(c) => chars.push(c),
None => return None,
}
}
Some(chars.into_iter().take_while(|&c| c != '\0').collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn c_locale_space_is_ascii_only() {
for space in [0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20] {
assert!(is_c_locale_space(space), "{space:#x}");
}
for not_space in [
0x00, 0x08, 0x0E, 0x1F, 0x21, 0x85, 0xA0, 0x2003, 0x202F, 0x3000,
] {
assert!(!is_c_locale_space(not_space), "{not_space:#x}");
}
}
#[test]
fn utf32_conversion_keeps_c_string_semantics() {
assert_eq!(
c_string_from_utf32(&[0x48, 0x69, 0x20]).as_deref(),
Some("Hi ")
);
assert_eq!(
c_string_from_utf32(&[0x48, 0x00, 0x69]).as_deref(),
Some("H")
);
assert_eq!(c_string_from_utf32(&[0x00]).as_deref(), Some(""));
assert_eq!(c_string_from_utf32(&[]).as_deref(), Some(""));
assert_eq!(c_string_from_utf32(&[0x48, 0xD800]), None);
assert_eq!(c_string_from_utf32(&[0x48, 0x00, 0x110000]), None);
assert_eq!(c_string_from_utf32(&[0xFFFE]).as_deref(), Some("\u{FFFE}"));
}
#[test]
fn angle_folds_page_rotation_and_wraps() {
use std::f64::consts::PI;
let close = |a: f32, b: f64| (f64::from(a) - b).abs() < 1e-5;
assert!(close(normalize_angle(0.0, 0), 0.0));
assert!(close(normalize_angle(PI / 2.0, 0), PI / 2.0));
assert!(close(normalize_angle(0.0, 1), PI / 2.0));
assert!(close(normalize_angle(0.0, 2), PI));
assert!(close(normalize_angle(0.0, 3), 3.0 * PI / 2.0));
assert!(close(normalize_angle(-0.5, 0), 2.0 * PI - 0.5));
assert!(close(normalize_angle(2.0 * PI + 0.25, 0), 0.25));
}
fn matrix(a: f32, b: f32, c: f32, d: f32) -> Matrix {
Matrix {
a,
b,
c,
d,
e: 0.0,
f: 0.0,
}
}
#[test]
fn baseline_angle_ignores_shear_and_keeps_the_c_wrap() {
use std::f64::consts::PI;
let close = |a: f64, b: f64| (a - b).abs() < 1e-9;
assert!(close(
baseline_angle_radians(&matrix(12.0, 0.0, 0.0, 12.0)),
0.0
));
assert!(close(
baseline_angle_radians(&matrix(12.0, 0.0, 3.0, 12.0)),
0.0
));
assert!(close(
baseline_angle_radians(&matrix(0.0, 12.0, -12.0, 0.0)),
-PI / 2.0
));
let radians = baseline_angle_radians(&matrix(12.0, -1e-6, 0.0, 12.0));
assert!(radians < -2.0 * PI + 1e-6 && radians > -2.0 * PI);
assert!(normalize_angle(radians, 0) > 0.0 && normalize_angle(radians, 0) < 1e-6);
let past_quarter = baseline_angle_radians(&matrix(-1e-6, 12.0, -12.0, 0.0));
let folded = normalize_angle(past_quarter, 1);
assert!(f64::from(folded) > 2.0 * PI - 1e-6 && f64::from(folded) <= 2.0 * PI + 1e-6);
}
fn glyph(strict: RectF, generated: bool) -> Glyph {
Glyph {
index: 0,
text_object: None,
generated,
char_code: 0,
unicode: 0,
loose: strict,
strict,
angle: 0.0,
}
}
fn rect(left: f32, top: f32, right: f32, bottom: f32) -> RectF {
RectF {
left,
top,
right,
bottom,
}
}
#[test]
fn grounding_bounds_skip_spaces_and_generated_glyphs() {
let glyphs = [
glyph(rect(10.0, 20.0, 15.0, 30.0), false),
glyph(rect(15.0, 20.0, 20.0, 30.0), false),
glyph(rect(20.0, 20.0, 25.0, 30.0), true),
glyph(rect(25.0, 20.0, 30.0, 30.0), false),
glyph(rect(30.0, 20.0, 35.0, 30.0), false),
glyph(rect(35.0, 0.0, 40.0, 40.0), false),
];
let codepoints = [b'H', b'i', b' ', b' ', b'!', 0].map(u32::from);
let bounds = grounding_bounds(&glyphs, &codepoints, 0.0, |_, _, _| None).unwrap();
assert_eq!(bounds, rect(10.0, 20.0, 35.0, 30.0));
assert!(grounding_bounds(&glyphs[2..3], &codepoints[2..3], 0.0, |_, _, _| None).is_none());
let flat = [glyph(rect(1.0, 1.0, 1.0, 5.0), false)];
assert!(grounding_bounds(&flat, &[u32::from(b'x')], 0.0, |_, _, _| None).is_none());
}
#[test]
fn grounding_bounds_rotate_about_the_centre() {
use std::f32::consts::FRAC_PI_2;
let glyphs = [glyph(rect(0.0, 0.0, 10.0, 4.0), false)];
let bounds =
grounding_bounds(&glyphs, &[u32::from(b'x')], FRAC_PI_2, |_, _, _| None).unwrap();
let close = |a: f32, b: f32| (a - b).abs() < 1e-5;
assert!(close(bounds.left, 3.0) && close(bounds.right, 7.0));
assert!(close(bounds.top, -3.0) && close(bounds.bottom, 7.0));
}
fn item(text: &str, x: f32, y: f32, width: f32, height: f32, angle: f32) -> RawTextItem {
RawTextItem {
text: text.into(),
char_codes: Vec::new(),
glyph_names: None,
angle_radians: angle,
text_width: 0.0,
x,
y,
width,
height,
grounding_bounds: None,
baseline_gap: None,
mcid: Some(3),
font_name: String::new(),
font_size: 0.0,
font_weight: 0,
font_height: 0.0,
font_ascent: 0.0,
font_descent: 0.0,
font_is_buggy: false,
trailing_space_generated: false,
fill_color: None,
stroke_color: None,
}
}
#[test]
fn same_text_item_needs_equal_text_and_geometry() {
let a = item("Total ", 10.0, 20.0, 30.0, 8.0, 0.0);
assert!(same_text_item(
&a,
&item("Total", 10.005, 20.0, 29.995, 8.0, 0.0005)
));
assert!(!same_text_item(
&a,
&item("Total", 10.02, 20.0, 29.98, 8.0, 0.0)
));
assert!(!same_text_item(
&a,
&item("Total", 10.0, 20.0, 30.0, 8.0, 0.002)
));
assert!(!same_text_item(
&a,
&item("Totals", 10.0, 20.0, 30.0, 8.0, 0.0)
));
let blank = item(" ", 10.0, 20.0, 30.0, 8.0, 0.0);
assert!(!same_text_item(&blank, &blank));
}
#[test]
fn argb_packing_matches_fpdf_argb() {
let color = pdfium::Color {
r: 0x12,
g: 0x34,
b: 0x56,
a: 0xFF,
};
assert_eq!(pack_argb(color), 0xFF12_3456);
}
}