use crate::annotation::{Rect as AnnotRect, Shape};
use crate::djvu_document::{DjVuDocument, DjVuPage};
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) fn scale_at_dpi(page: &DjVuPage, target_dpi: f32) -> f32 {
target_dpi / page.dpi().max(1) as f32
}
pub(crate) fn size_at_dpi(page: &DjVuPage, target_dpi: f32) -> (u32, u32) {
scaled_size(
page.width() as u32,
page.height() as u32,
scale_at_dpi(page, target_dpi),
)
}
pub(crate) fn rgba_row_to_rgb(dst: &mut Vec<u8>, rgba_row: &[u8]) {
for rgba in rgba_row.chunks_exact(4) {
dst.extend_from_slice(&rgba[..3]);
}
}
#[allow(dead_code)]
pub(crate) fn render_rows_or_pixmap(
page: &DjVuPage,
opts: &crate::djvu_render::RenderOptions,
mut row_cb: impl FnMut(&[u8]),
) -> Result<(), crate::djvu_render::RenderError> {
if opts.can_stream(page) {
crate::djvu_render::render_streaming(page, opts, |_, rgba_row| row_cb(rgba_row))
} else {
let pixmap = crate::djvu_render::render_pixmap(page, opts)?;
let stride = pixmap.width as usize * 4;
if stride > 0 {
for row in pixmap.data.chunks_exact(stride) {
row_cb(row);
}
}
Ok(())
}
}
#[allow(dead_code)]
pub(crate) fn bookmark_page_index(url: &str) -> Option<usize> {
let frag = url.strip_prefix('#')?;
if let Some(rest) = frag.strip_prefix("page") {
let digits = rest
.strip_prefix('=')
.or_else(|| rest.strip_prefix('_'))
.unwrap_or(rest)
.trim();
if let Ok(n) = digits.parse::<usize>() {
return Some(n.saturating_sub(1));
}
}
if let Ok(n) = frag.trim().parse::<i64>() {
return Some((n.max(1) - 1) as usize);
}
None
}
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, y: u32, height: u32) -> u32 {
total_h.saturating_sub(y + height)
}
#[allow(dead_code)]
pub(crate) fn shape_bbox(shape: &Shape) -> Option<AnnotRect> {
let (x, y, width, height) = match shape {
Shape::Rect(r) | Shape::Oval(r) | Shape::Text(r) => (r.x, r.y, r.width, r.height),
Shape::Poly(points) => {
let (&(fx, fy), rest) = points.split_first()?;
let (min_x, min_y, max_x, max_y) =
rest.iter()
.fold((fx, fy, fx, fy), |(mnx, mny, mxx, mxy), &(px, py)| {
(mnx.min(px), mny.min(py), mxx.max(px), mxy.max(py))
});
(min_x, min_y, max_x - min_x, max_y - min_y)
}
Shape::Line(x1, y1, x2, y2) => {
let min_x = (*x1).min(*x2);
let min_y = (*y1).min(*y2);
let max_x = (*x1).max(*x2);
let max_y = (*y1).max(*y2);
(min_x, min_y, max_x - min_x, max_y - min_y)
}
};
if width == 0 || height == 0 {
return None;
}
Some(AnnotRect {
x,
y,
width,
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() {
assert_eq!(flip_y_bottom(100, 10, 20), 70);
assert_eq!(flip_y_bottom(15, 10, 20), 0);
}
fn arect(x: u32, y: u32, width: u32, height: u32) -> AnnotRect {
AnnotRect {
x,
y,
width,
height,
}
}
#[test]
fn shape_bbox_rect_oval_text_pass_through_when_non_degenerate() {
let r = arect(3, 7, 11, 13);
assert_eq!(shape_bbox(&Shape::Rect(r.clone())), Some(r.clone()));
assert_eq!(shape_bbox(&Shape::Oval(r.clone())), Some(r.clone()));
assert_eq!(shape_bbox(&Shape::Text(r.clone())), Some(r));
}
#[test]
fn shape_bbox_zero_area_rect_is_none() {
assert!(shape_bbox(&Shape::Rect(arect(5, 5, 0, 10))).is_none());
assert!(shape_bbox(&Shape::Rect(arect(5, 5, 10, 0))).is_none());
}
#[test]
fn shape_bbox_poly_is_the_point_cloud_bounding_box() {
let s = Shape::Poly(vec![(10, 4), (2, 20), (8, 1), (5, 12)]);
assert_eq!(shape_bbox(&s), Some(arect(2, 1, 8, 19)));
}
#[test]
fn shape_bbox_empty_poly_is_none() {
assert!(shape_bbox(&Shape::Poly(vec![])).is_none());
}
#[test]
fn shape_bbox_degenerate_poly_resolves_the_pdf_epub_divergence() {
assert!(shape_bbox(&Shape::Poly(vec![(0, 0)])).is_none());
assert!(shape_bbox(&Shape::Poly(vec![(9, 9)])).is_none());
assert!(shape_bbox(&Shape::Poly(vec![(4, 6), (4, 6)])).is_none());
}
#[test]
fn shape_bbox_diagonal_line_is_the_endpoint_bounding_box() {
assert_eq!(
shape_bbox(&Shape::Line(2, 3, 12, 18)),
Some(arect(2, 3, 10, 15))
);
assert_eq!(
shape_bbox(&Shape::Line(12, 18, 2, 3)),
Some(arect(2, 3, 10, 15))
);
}
#[test]
fn shape_bbox_axis_aligned_line_is_none() {
assert!(shape_bbox(&Shape::Line(2, 5, 20, 5)).is_none());
assert!(shape_bbox(&Shape::Line(5, 2, 5, 20)).is_none());
}
#[test]
fn bookmark_page_index_unparseable_returns_none() {
assert_eq!(bookmark_page_index("not-a-page-url"), None);
assert_eq!(bookmark_page_index("#pageXYZ"), None);
assert_eq!(bookmark_page_index(""), None);
}
#[test]
fn bookmark_page_index_page_underscore_form() {
assert_eq!(bookmark_page_index("#page_1"), Some(0));
assert_eq!(bookmark_page_index("#page_3"), Some(2));
}
#[test]
fn bookmark_page_index_numeric_fragment() {
assert_eq!(bookmark_page_index("#1"), Some(0));
assert_eq!(bookmark_page_index("#5"), Some(4));
}
#[test]
fn word_spans_character_leaf_outside_word_is_emitted() {
let layer = TextLayer {
text: String::new(),
zones: vec![TextZone {
kind: TextZoneKind::Line,
rect: Rect {
x: 0,
y: 0,
width: 100,
height: 20,
},
text: String::new(),
children: vec![TextZone {
kind: TextZoneKind::Character,
rect: Rect {
x: 0,
y: 0,
width: 10,
height: 20,
},
text: "A".to_string(),
children: vec![],
}],
}],
};
let spans = word_spans(&layer);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].text, "A");
}
}