pub struct TextPage {
pub chars: Vec<CharBox>,
pub search_text: Vec<char>,
pub runs: IndexMap,
pub fonts: BTreeMap<ObjectIndex, String>,
}Expand description
One page’s extracted text (ISO 32000-1 §14.8.2).
Holds two sequences that are not the same, and conflating them is the easiest way to get this crate wrong:
chars, addressed byCharIndex, is every character the page drew or the extractor invented, geometry attached. It keeps control characters,\0for an unmappable code, andU+0002where a word was hyphenated across a line.search_text, addressed byTextIndex, is what a search matches and a selection copies. It drops the control characters and the placeholders, expands ligatures the character stream keeps whole, and carriesU+00ADat a hyphenated break andU+FFFDfor an unmappable code, so it can disagree withcharsposition by position.
runs converts between the two spaces; every signature
names which one it counts in. Cheap to clone and Send + Sync, so a
document’s pages can be extracted in parallel.
§Examples
The fields are public, so a page can be built by hand — which is how the query side is exercised without a file:
use pdfrum_text::TextPage;
let page = TextPage {
search_text: "Hello, world!".chars().collect(),
..TextPage::default()
};
// `Display` writes the search-facing text, never the character stream.
assert_eq!(page.to_string(), "Hello, world!");
// …which is a different sequence, and here an empty one.
assert_eq!(page.char_count(), 0);Fields§
§chars: Vec<CharBox>Characters in reading order addressed by CharIndex.
search_text: Vec<char>Normalized search-facing text addressed by TextIndex.
runs: IndexMap§fonts: BTreeMap<ObjectIndex, String>The base font name of each text object whose font has one, by the
ObjectIndex its characters carry. A Type 3 font has no base name
and is absent. Read through a character with
font_name.
Implementations§
Source§impl TextPage
impl TextPage
Sourcepub fn words(&self) -> Vec<Word>
pub fn words(&self) -> Vec<Word>
The page’s words in reading order: the runs of
chars between whitespace, each with the box its
glyphs cover and the font of its first character.
A word’s text is what slice gives for
its range, so a hyphenated line break shows up as
two words, the first ending in U+00AD. A run whose characters all
vanish from the search text — the charcode-0 passthrough, the control
code points — is not a word.
§Examples
let page = TextPage::default();
assert!(page.words().is_empty());Source§impl TextPage
impl TextPage
Sourcepub fn char_count(&self) -> usize
pub fn char_count(&self) -> usize
How many characters the page drew.
This counts chars, not
search_text: the two sequences have different
lengths, and a page can have text and no characters or the reverse.
§Examples
use pdfrum_text::TextPage;
let page = TextPage {
search_text: "Hello".chars().collect(),
..TextPage::default()
};
assert_eq!(page.to_string().len(), 5);
assert_eq!(page.char_count(), 0);Sourcepub fn slice(&self, range: impl RangeBounds<CharIndex>) -> String
pub fn slice(&self, range: impl RangeBounds<CharIndex>) -> String
A run of the search_text, addressed in
CharIndex.
The bounds are widened onto real text, not filtered: a start on a
character the text does not hold scans forward to the next one it
does, and an end on one scans back. Characters inside the range are
never skipped. So on control_characters.pdf, asking for the fifteen
characters from character 17 returns "Goodbye, world!" even though
the text itself has no character 17.
This is the one call where the two spaces of TextPage meet: the
bounds count characters and the answer is text.
§Examples
A page holding "Hello, world!" on one line and "Goodbye, world!" on
the next, with the extractor’s generated \r\n between them:
let at = CharIndex::new;
assert_eq!(page.slice(at(0)..at(5)), "Hello");
// An unbounded end is "to the end of the page".
assert_eq!(page.slice(at(15)..), "Goodbye, world!");
assert_eq!(page.slice(..), "Hello, world!\r\nGoodbye, world!");Sourcepub fn find<'a>(
&'a self,
needle: &str,
options: FindOptions,
) -> impl Iterator<Item = Range<TextIndex>> + 'a
pub fn find<'a>( &'a self, needle: &str, options: FindOptions, ) -> impl Iterator<Item = Range<TextIndex>> + 'a
Searches the page.
Match ranges are TextIndex offsets into
search_text — not the CharIndex space
web_links reports; see TextPage.
§Examples
let page = TextPage {
search_text: "Hello, world!".chars().collect(),
..TextPage::default()
};
let at = TextIndex::new;
let hits: Vec<_> = page.find("world", FindOptions::default()).collect();
assert_eq!(hits, [at(7)..at(12)]);
// The default is case-insensitive.
let hits: Vec<_> = page.find("WORLD", FindOptions::default()).collect();
assert_eq!(hits, [at(7)..at(12)]);Sourcepub fn web_links(&self) -> Vec<WebLink>
pub fn web_links(&self) -> Vec<WebLink>
Every web and mail address in the page’s text.
Reported ranges are CharIndex spans into chars —
not the TextIndex space find returns. The two
index spaces are different sequences; see TextPage.
§Examples
The fixture below draws no address, so nothing is reported; a page that
draws www.example.com reports it with an http:// already prefixed.
assert!(page.web_links().is_empty());Sourcepub fn rects(&self, range: impl RangeBounds<CharIndex>) -> Vec<Rect>
pub fn rects(&self, range: impl RangeBounds<CharIndex>) -> Vec<Rect>
The boxes covering a run of CharIndex, one per run of consecutive
characters sharing a text object.
Generated characters and boxes under 0.01 in either dimension are skipped, and a box is pushed unconditionally at the end — so a run in which every character was skipped still yields one box, an all-zero rectangle. An unbounded end is “to the end of the page”, and a range running past the end takes what is there.
§Examples
Two lines set in two different fonts are two text objects, so the whole page yields two boxes rather than one:
let boxes = page.rects(..);
assert_eq!(boxes.len(), 2);
// The first line sits below the second in page space, which is y-up.
assert!(boxes[0].y1 < boxes[1].y0);
// A run inside one object is one box.
assert_eq!(page.rects(CharIndex::new(0)..CharIndex::new(5)).len(), 1);Sourcepub fn index_at(&self, point: Point, tolerance: Size) -> Option<CharIndex>
pub fn index_at(&self, point: Point, tolerance: Size) -> Option<CharIndex>
The character under a point in page space, or the nearest within tolerance. A point inside a character’s box wins outright and reports the first such character; failing that, and only when a tolerance is given, the nearest character within it.
§Examples
// Inside the first glyph's box.
assert_eq!(page.index_at(Point::new(24.0, 54.0), Size::ZERO), Some(CharIndex::new(0)));
// Far from every glyph, with no tolerance to fall back on.
assert_eq!(page.index_at(Point::new(500.0, 500.0), Size::ZERO), None);Sourcepub fn text_in_rect(&self, rect: Rect) -> String
pub fn text_in_rect(&self, rect: Rect) -> String
The text inside a rectangle, with \r\n where the selection crosses a
baseline.
§Examples
// A rectangle covering only the lower line takes only its text.
assert_eq!(page.text_in_rect(Rect::new(0.0, 0.0, 200.0, 70.0)), "Hello, world!");Sourcepub fn text_of_object(&self, object: ObjectIndex) -> String
pub fn text_of_object(&self, object: ObjectIndex) -> String
The text drawn by a specific text object.
The ObjectIndex is the one a character carries in
CharBox::object, counting text objects in content order.
§Examples
assert_eq!(page.text_of_object(ObjectIndex(0)), "Hello, world!");
assert_eq!(page.text_of_object(ObjectIndex(1)), "Goodbye, world!");
// An object the page does not have draws nothing.
assert_eq!(page.text_of_object(ObjectIndex(9)), "");Sourcepub fn char(&self, index: CharIndex) -> Result<&CharBox, Error>
pub fn char(&self, index: CharIndex) -> Result<&CharBox, Error>
Returns the character box at the given CharIndex.
§Errors
Returns Error::CharIndexOutOfRange when index is past the end of chars.
§Examples
let first = page.char(CharIndex::new(0))?;
assert_eq!(char::from_u32(first.unicode), Some('H'));
assert_eq!(first.char_type, CharType::Normal);
assert_eq!(first.font_size, 12.0);
// The error names both the index and the bound it broke.
assert_eq!(
page.char(CharIndex::new(999)),
Err(Error::CharIndexOutOfRange { index: CharIndex::new(999), len: 30 }),
);Sourcepub fn font_name(&self, index: CharIndex) -> Option<&str>
pub fn font_name(&self, index: CharIndex) -> Option<&str>
The base font name of the font the character at index was drawn
with, as the font crate normalized it — the subset tag stripped and a
standard-14 alias canonicalized, so ABCDEF+Arial,Bold reads as
Helvetica-Bold.
None past the end, for a character no text object drew (every
generated one), and for a font with no base name (Type 3).
§Examples
// Two lines, two fonts.
assert_eq!(page.font_name(CharIndex::new(0)), Some("Times-Roman"));
assert_eq!(page.font_name(CharIndex::new(15)), Some("Helvetica"));
// Character 13 is the line break the extractor generated: no text
// object drew it, so it has no font.
assert_eq!(page.font_name(CharIndex::new(13)), None);