OCR: Downsample to shrinks a page box given in pixels (fixes #1848) (#2135)

This commit is contained in:
Marco Arnulfo
2026-10-15 02:46:44 -04:00
committed by GitHub
parent 210fb904cc
commit b008c52291
+40 -5
View File
@@ -30,6 +30,10 @@ pub struct OcrSettings {
/// text read again, not a word in a picture.
const ALREADY_ON_PAGE: f32 = 0.5;
/// The long side of an A4 sheet in points (a letter page's, 792, is shorter): "Downsample to"
/// is a resolution on paper this size or smaller.
const SHEET: f32 = 842.0;
/// How much of `word` the `text` boxes cover, from 0 to 1. Boxes are `[x0, y0, x1, y1]` in page
/// view space. Overlaps between the text boxes are counted twice (glyphs rarely overlap), so the
/// sum is capped at the whole word.
@@ -121,11 +125,7 @@ impl OcrJob {
out.push(skip("the page already contains text"));
continue;
}
// A scan is read at its own resolution (enlarging it only blurs the letters), at most
// the chosen one.
let wanted = self.settings.dpi.clamp(72.0, 600.0) / 72.0;
let scale = self.native.get(page).copied().flatten().map_or(wanted, |n| n.clamp(1.0, wanted));
let shot = r.render(RenderRequest { page, scale, ..Default::default() });
let shot = r.render(RenderRequest { page, scale: self.scale(page, info), ..Default::default() });
if let Some(e) = shot.error {
out.push(skip(&e));
continue;
@@ -151,6 +151,18 @@ impl OcrJob {
progress(total, total);
out
}
/// The render scale (pixels per point) `page` is read at. A scan is read at its own
/// resolution (enlarging it only blurs the letters), at most the chosen one.
fn scale(&self, page: usize, info: &PageInfo) -> f32 {
let wanted = self.settings.dpi.clamp(72.0, 600.0) / 72.0;
let Some(native) = self.native.get(page).copied().flatten() else { return wanted };
// A scan whose page is larger than a sheet (often a box given in pixels, one point per
// pixel, #1848) is measured as if shrunk to one: otherwise 1 px/pt is already its full
// image, and no "Downsample to" choice could ask for less.
let fit = (SHEET / info.width.max(info.height).max(1e-3)).min(1.0);
native.clamp(fit, wanted * fit)
}
}
impl Session {
@@ -242,6 +254,7 @@ pub fn recognize_file(
#[cfg(test)]
mod tests {
use super::*;
use crate::export;
/// #1028: a recognised word over the page's own text is that text again; one in an empty
/// part of the page (a picture) is new.
@@ -260,6 +273,28 @@ mod tests {
assert_eq!(covered([100.5, 52.0, 134.0, 61.0], &[]), 0.0);
}
/// #1848: an A4 scan at 300 dpi whose page box is its pixel size (2480 x 3508 pt, one pixel
/// per point) gets a smaller bitmap for each lower "Downsample to" choice, and a low-resolution
/// letter page is still never shrunk below one pixel per point.
#[test]
fn downsample_shrinks_a_page_box_given_in_pixels() {
let mut s = Session::new();
let page = |w: u32, h: u32| export::encode_png(w, h, &vec![255; (w * h * 4) as usize]).unwrap();
let bytes = s.create_from_images(&[("a4.png".into(), page(2480, 3508)), ("letter.png".into(), page(612, 792))]).unwrap();
let id = s.open("scan.pdf", None, bytes, None).unwrap();
let mut r = PageRenderer::new(s.get(id).unwrap().bytes.clone(), RenderConfig::default());
for (dpi, a4) in [(72.0, [595, 842]), (150.0, [1240, 1754]), (300.0, [2480, 3508]), (600.0, [2480, 3508])] {
let job = s.ocr_job(id, &[], OcrSettings { dpi, ..Default::default() }).unwrap();
for (page, want) in [(0, a4), (1, [612, 792])] {
let scale = job.scale(page, &job.infos[page]);
let shot = r.render(RenderRequest { page, scale, ..Default::default() });
assert!(shot.error.is_none(), "{:?}", shot.error);
let near = |got: u32, want: u32| got.abs_diff(want) <= 1;
assert!(near(shot.width, want[0]) && near(shot.height, want[1]), "{dpi} dpi, page {page}: {}x{}", shot.width, shot.height);
}
}
}
#[test]
fn coverage_of_overlapping_or_broken_boxes_stays_between_zero_and_one() {
// A glyph drawn twice (fake bold) does not cover more than the whole word.