use pdfium::{Font, FontType, Page, RectF, TextPage};
use crate::GlyphResolver;
use crate::extract::{
CharInfoChunks, CharView, decompose_scale, 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 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,
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 flush = |run: &mut Vec<Glyph>, items: &mut Vec<RawTextItem>| {
if run.is_empty() {
return;
}
if let Some(item) = build_item(text_page, run, glyph_resolver) {
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);
Glyph {
index,
text_object: cv.text_object(),
generated,
char_code,
unicode,
loose: page.bounds_to_viewport(view_box, &loose),
angle: normalize_angle(cv.ch.angle(), page_rotation),
}
}
fn normalize_angle(angle_radians: f32, page_rotation: i32) -> f32 {
use std::f64::consts::PI;
let mut angle = f64::from(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(
text_page: &TextPage,
glyphs: &[Glyph],
glyph_resolver: Option<&dyn GlyphResolver>,
) -> 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(&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 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,
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),
})
}
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::f32::consts::PI;
let close = |a: f32, b: f32| (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));
}
#[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);
}
}