use crate::djvu_document::DjVuDocument;
use crate::text::{Rect, TextLayer, TextZone, TextZoneKind};
pub(crate) fn page_indices(
doc: &DjVuDocument,
only: Option<usize>,
) -> Box<dyn Iterator<Item = usize>> {
match only {
Some(i) => Box::new(core::iter::once(i)),
None => Box::new(0..doc.page_count()),
}
}
pub(crate) fn scaled_size(w: u32, h: u32, scale: f32) -> (u32, u32) {
let sw = ((w as f32 * scale).round() as u32).max(1);
let sh = ((h as f32 * scale).round() as u32).max(1);
(sw, sh)
}
pub(crate) struct WordSpan<'a> {
pub rect: &'a Rect,
pub text: &'a str,
}
pub(crate) fn word_spans(layer: &TextLayer) -> Vec<WordSpan<'_>> {
fn walk<'a>(zones: &'a [TextZone], out: &mut Vec<WordSpan<'a>>) {
for zone in zones {
match zone.kind {
TextZoneKind::Word | TextZoneKind::Character => {
if !zone.text.is_empty() {
out.push(WordSpan {
rect: &zone.rect,
text: &zone.text,
});
}
}
_ => walk(&zone.children, out),
}
}
}
let mut out = Vec::new();
walk(&layer.zones, &mut out);
out
}
#[allow(dead_code)]
pub(crate) fn flip_y_bottom(total_h: u32, rect: &Rect) -> u32 {
total_h.saturating_sub(rect.y + rect.height)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::{Rect, TextZone, TextZoneKind};
fn zone(kind: TextZoneKind, text: &str, children: Vec<TextZone>) -> TextZone {
TextZone {
kind,
rect: Rect {
x: 1,
y: 2,
width: 3,
height: 4,
},
text: text.to_string(),
children,
}
}
#[test]
fn word_spans_collects_leaf_words_in_order_skipping_empty() {
let layer = TextLayer {
text: String::new(),
zones: vec![zone(
TextZoneKind::Page,
"",
vec![
zone(
TextZoneKind::Line,
"",
vec![
zone(TextZoneKind::Word, "a", vec![]),
zone(TextZoneKind::Word, "", vec![]),
zone(TextZoneKind::Word, "b", vec![]),
],
),
zone(
TextZoneKind::Line,
"",
vec![zone(TextZoneKind::Character, "c", vec![])],
),
],
)],
};
let spans = word_spans(&layer);
let texts: Vec<&str> = spans.iter().map(|s| s.text).collect();
assert_eq!(texts, ["a", "b", "c"]);
}
#[test]
fn word_spans_does_not_descend_into_an_emitted_word() {
let layer = TextLayer {
text: String::new(),
zones: vec![zone(
TextZoneKind::Word,
"word",
vec![
zone(TextZoneKind::Character, "w", vec![]),
zone(TextZoneKind::Character, "o", vec![]),
],
)],
};
let spans = word_spans(&layer);
let texts: Vec<&str> = spans.iter().map(|s| s.text).collect();
assert_eq!(texts, ["word"]);
}
#[test]
fn word_spans_empty_word_drops_its_character_children() {
let layer = TextLayer {
text: String::new(),
zones: vec![zone(
TextZoneKind::Word,
"",
vec![zone(TextZoneKind::Character, "x", vec![])],
)],
};
assert!(word_spans(&layer).is_empty());
}
#[test]
fn scaled_size_rounds_and_clamps_to_one() {
assert_eq!(scaled_size(100, 200, 0.5), (50, 100));
assert_eq!(scaled_size(3, 3, 0.5), (2, 2)); assert_eq!(scaled_size(10, 10, 0.001), (1, 1));
assert_eq!(scaled_size(10, 10, 0.0), (1, 1));
}
#[test]
fn flip_y_bottom_saturates_and_matches_formula() {
let r = Rect {
x: 0,
y: 10,
width: 5,
height: 20,
};
assert_eq!(flip_y_bottom(100, &r), 70);
assert_eq!(flip_y_bottom(15, &r), 0);
}
}