mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
M2.6: autotagging reads page content with its /DecodeParms given by reference
Autotagging a text-only page rewrites its content, marked, from what it read. It read each stream through its filters with /DecodeParms only when direct, so content behind a predictor whose parameters are given by reference was read with the predictor's tag bytes in it, and written back so: with eight-byte rows, a \0 every eight bytes split `24 Tf` into `2 4 Tf` and the text's string, and the tool returned ok. Found by the consumers review of the CodeRabbit round; inherited from main. It now reads through DecodeBudget::decode_strict, which reads the parameters in the document and is strict on damage as before, within the same 16 MiB. pdfcraft-a11y no longer uses pdfcraft-filters. Test: autotag_text_reads_content_with_parameters_given_by_reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
a4130c9973
commit
cbc3fc221a
4 files changed
+42
-12
No files matched your search
Generated
-1
@@ -4245,7 +4245,6 @@ version = "0.18.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"pdfcraft-content",
|
"pdfcraft-content",
|
||||||
"pdfcraft-cos",
|
"pdfcraft-cos",
|
||||||
"pdfcraft-filters",
|
|
||||||
"pdfcraft-fonts",
|
"pdfcraft-fonts",
|
||||||
"pdfcraft-model",
|
"pdfcraft-model",
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
|
|||||||
@@ -14,6 +14,5 @@ workspace = true
|
|||||||
pdfcraft-cos = { workspace = true }
|
pdfcraft-cos = { workspace = true }
|
||||||
pdfcraft-model = { workspace = true }
|
pdfcraft-model = { workspace = true }
|
||||||
pdfcraft-content = { workspace = true }
|
pdfcraft-content = { workspace = true }
|
||||||
pdfcraft-filters = { workspace = true }
|
|
||||||
pdfcraft-fonts = { workspace = true }
|
pdfcraft-fonts = { workspace = true }
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
//! which can then be retagged with the existing tag-property tool after human review.
|
//! which can then be retagged with the existing tag-property tool after human review.
|
||||||
|
|
||||||
use pdfcraft_content::{Op, parse, serialize_ops};
|
use pdfcraft_content::{Op, parse, serialize_ops};
|
||||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream};
|
use pdfcraft_cos::{DecodeBudget, Dict, Document, ObjRef, Object, Stream};
|
||||||
|
|
||||||
const MAX_PAGES: usize = 1_000;
|
const MAX_PAGES: usize = 1_000;
|
||||||
const MAX_PAGE_BYTES: usize = 16 * 1024 * 1024;
|
const MAX_PAGE_BYTES: usize = 16 * 1024 * 1024;
|
||||||
@@ -59,16 +59,15 @@ fn content_bytes(doc: &Document, page: &pdfcraft_model::Page, index: usize) -> R
|
|||||||
if remaining == 0 {
|
if remaining == 0 {
|
||||||
return Err(unsupported(index, "decoded content exceeds 16 MiB"));
|
return Err(unsupported(index, "decoded content exceeds 16 MiB"));
|
||||||
}
|
}
|
||||||
let chain = stream.filters();
|
// Strictly, and with `/DecodeParms` given by reference: the content is written back from it.
|
||||||
let decoded = if chain.is_empty() {
|
let mut budget = DecodeBudget::new(remaining, 0);
|
||||||
if stream.raw.len() > remaining {
|
let decoded = budget.decode_strict(doc, stream).map_err(|e| {
|
||||||
return Err(unsupported(index, "decoded content exceeds 16 MiB"));
|
if budget.remaining() == 0 {
|
||||||
}
|
unsupported(index, "decoded content exceeds 16 MiB")
|
||||||
stream.raw.to_vec()
|
|
||||||
} else {
|
} else {
|
||||||
pdfcraft_filters::decode(&chain, &stream.raw, remaining)
|
unsupported(index, format!("content cannot be decoded strictly: {e}"))
|
||||||
.map_err(|e| unsupported(index, format!("content cannot be decoded strictly: {e}")))?
|
}
|
||||||
};
|
})?;
|
||||||
all.extend_from_slice(&decoded);
|
all.extend_from_slice(&decoded);
|
||||||
all.push(b' ');
|
all.push(b' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,39 @@ fn autotag_text_writes_marked_content_and_parent_tree_across_pages() {
|
|||||||
assert_eq!(status(&check(&reopened, &Options::default()), Rule::TaggedContent), Status::Passed);
|
assert_eq!(status(&check(&reopened, &Options::default()), Rule::TaggedContent), Status::Passed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Page content behind a predictor whose `/DecodeParms` are given by reference is read through
|
||||||
|
/// them: autotagging writes the text back as it was, marked, not with the predictor's tag bytes in
|
||||||
|
/// it (a `\0` every eight bytes, which split `24` into `2 4`).
|
||||||
|
#[test]
|
||||||
|
fn autotag_text_reads_content_with_parameters_given_by_reference() {
|
||||||
|
use pdfcraft_cos::{Dict, ObjRef};
|
||||||
|
let mut content = b"BT /F1 24 Tf 72 700 Td (Hello World) Tj ET".to_vec();
|
||||||
|
content.resize(content.len().div_ceil(8) * 8, b' ');
|
||||||
|
// PNG rows of eight bytes, each tagged 0 (no prediction).
|
||||||
|
let rows: Vec<u8> = content.chunks(8).flat_map(|row| std::iter::once(0).chain(row.iter().copied())).collect();
|
||||||
|
let mut doc = pdf(
|
||||||
|
&[
|
||||||
|
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
||||||
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
||||||
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".into(),
|
||||||
|
stream(""),
|
||||||
|
FONT.into(),
|
||||||
|
],
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
let parms: Dict = [(b"Predictor".to_vec(), Object::Int(12)), (b"Columns".to_vec(), Object::Int(8))].into_iter().collect();
|
||||||
|
let parms = doc.add(Object::Dict(parms));
|
||||||
|
let mut predicted = pdfcraft_cos::Stream::flate(Dict::new(), &rows);
|
||||||
|
predicted.dict.set(b"DecodeParms".to_vec(), Object::Ref(parms));
|
||||||
|
doc.set(ObjRef::new(4, 0), predicted);
|
||||||
|
assert_eq!(autotag_text(&mut doc).unwrap(), AutoTagResult { pages: 1, paragraphs: 1 });
|
||||||
|
let page = &pdfcraft_model::pages(&doc)[0];
|
||||||
|
let written = doc.resolve(page.dict.get(b"Contents").unwrap());
|
||||||
|
let Object::Stream(written) = &*written else { panic!("page content") };
|
||||||
|
let text = String::from_utf8_lossy(&written.decoded_strict().unwrap()).into_owned();
|
||||||
|
assert!(text.contains("/F1 24 Tf") && text.contains("(Hello World) Tj"), "{text}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn autotag_text_refuses_paint_and_does_not_modify_the_document() {
|
fn autotag_text_refuses_paint_and_does_not_modify_the_document() {
|
||||||
let mut doc = pdf(
|
let mut doc = pdf(
|
||||||
|
|||||||
Reference in new issue
Block a user