mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-22 19:08:29 +00:00
fix(xtask): the text oracle scored us zero for honouring a no-copy document
A PDF can forbid copying its text. `pdfcraft-cli text` honours that and refuses, naming the owner password as the way through. pdftotext ignores the flag and extracts anyway, so the oracle was comparing our refusal against its text and scoring 0.000 -- measuring obedience, not quality. bug1815476.pdf is the case: a one-page Spanish form we render perfectly, whose text we decline to extract and pdftotext hands over regardless. It sat at the bottom of the worst list looking like a font bug, which is what sent me looking at it. Those files are now skipped and counted, not scored, and the count is printed rather than passed over in silence, on the same rule the oracle crate exists for: a comparison that did not happen must not look like one that succeeded. Over the first 250 corpus files that moves the mean from 0.811 to 0.854 and drops one file from the sample, with the median unchanged at 0.991. Permissions come from `Document::permissions`, so this is not a match on our own error text. A file that cannot be opened at all counts as permitted, because that is a different problem and dropping it quietly would hide it. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
f802257be6
commit
ac89a11391
3 files changed
+25
No files matched your search
Generated
+1
@@ -7959,6 +7959,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lopdf",
|
||||
"pdfcraft-cos",
|
||||
"pdfcraft-oracle",
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pdfcraft-oracle = { workspace = true }
|
||||
pdfcraft-cos = { workspace = true }
|
||||
anyhow = "1"
|
||||
lopdf = "0.45"
|
||||
png = "0.18"
|
||||
|
||||
@@ -212,7 +212,16 @@ pub fn text_oracle(args: &[String]) -> anyhow::Result<()> {
|
||||
let cli = release_cli();
|
||||
let mut scores = Vec::new();
|
||||
let mut worst: Vec<(f64, String)> = Vec::new();
|
||||
let mut not_permitted = 0usize;
|
||||
for f in files.iter().take(limit) {
|
||||
// A document may forbid copying its text, and we honour that: `pdfcraft-cli text` refuses
|
||||
// and says how to override it with the owner password. pdftotext ignores the flag and
|
||||
// extracts anyway, so scoring us against it there measures obedience, not quality --
|
||||
// bug1815476.pdf scored 0.000 for behaving correctly on a page we render perfectly.
|
||||
if !copying_allowed(f) {
|
||||
not_permitted = not_permitted.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
let ours = run_capture(Command::new(&cli).arg("text").arg(f), 20);
|
||||
let theirs = run_capture(pdftotext.command().arg("-q").arg(f).arg("-"), 20);
|
||||
let (Some(ours), Some(theirs)) = (ours, theirs) else { continue };
|
||||
@@ -232,6 +241,9 @@ pub fn text_oracle(args: &[String]) -> anyhow::Result<()> {
|
||||
let median = scores[scores.len() / 2];
|
||||
let mean = scores.iter().sum::<f64>() / scores.len() as f64;
|
||||
println!("text oracle: {} files with text; median F1 {:.3}, mean {:.3}, p10 {:.3}", scores.len(), median, mean, scores[scores.len() / 10]);
|
||||
if not_permitted > 0 {
|
||||
println!(" {not_permitted} skipped: the document forbids copying its text, which we honour and pdftotext does not");
|
||||
}
|
||||
println!("lowest:");
|
||||
for (s, n) in worst.iter().take(12) {
|
||||
println!(" {s:.3} {n}");
|
||||
@@ -350,6 +362,17 @@ pub fn render_oracle(args: &[String]) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the document lets its text be copied.
|
||||
///
|
||||
/// `true` when it is unencrypted, when the flag is set, and when the file cannot be opened at all
|
||||
/// — a file we cannot read is a different problem, and silently dropping it from the comparison
|
||||
/// would hide it.
|
||||
fn copying_allowed(path: &Path) -> bool {
|
||||
let Ok(bytes) = std::fs::read(path) else { return true };
|
||||
let Ok(doc) = pdfcraft_cos::Document::open(std::sync::Arc::new(bytes)) else { return true };
|
||||
doc.permissions().is_none_or(|p| p.copy())
|
||||
}
|
||||
|
||||
/// The value after `name` on the command line, parsed, or `None` when it is absent or malformed.
|
||||
fn flag_num<T: std::str::FromStr>(args: &[String], name: &str) -> Option<T> {
|
||||
args.iter().position(|a| a == name).and_then(|i| args.get(i.checked_add(1)?)).and_then(|v| v.parse().ok())
|
||||
|
||||
Reference in new issue
Block a user