mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 21:17:55 -06:00
refactor(a11y): extract text_spans and xobject_bbox helpers (Sonar S3776) (#4407)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
ff267d6883
commit
486949424c
+20
-14
@@ -211,6 +211,25 @@ fn close(marks: &mut Vec<Mark>, matching: &mut usize, out: &mut HashMap<i64, [f6
|
||||
draw(marks, b);
|
||||
}
|
||||
|
||||
/// The page-space box a `Do` operator paints: a form's transformed `/BBox`, or the unit square for an image.
|
||||
/// `None` when the operand is not a resolvable XObject or the form's box is malformed.
|
||||
fn xobject_bbox(doc: &Document, xobjects: &Dict, op: &pdfcraft_content::Op, top: &Matrix) -> Option<[f64; 4]> {
|
||||
let r = op.name(0).and_then(|n| xobjects.get(n)).and_then(Object::as_ref)?;
|
||||
Some(match &*doc.get(r) {
|
||||
Object::Stream(s) if s.dict.name(b"Subtype") == Some(b"Form") => {
|
||||
let bb: Vec<f64> =
|
||||
s.dict.get(b"BBox").and_then(|b| b.as_array().map(|a| a.iter().filter_map(Object::as_f64).collect())).unwrap_or_default();
|
||||
let m = Matrix::from_operands(s.dict.get(b"Matrix").and_then(|m| m.as_array()).map(Vec::as_slice).unwrap_or(&[]))
|
||||
.unwrap_or(Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
|
||||
match bb[..] {
|
||||
[a, b, c, d] => m.then(top).bbox([a, b, c, d]),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
_ => top.bbox([0.0, 0.0, 1.0, 1.0]),
|
||||
})
|
||||
}
|
||||
|
||||
/// Where each of the marked content `ids` is drawn on a page (images, forms and paths), in one pass
|
||||
/// over the page. What a sequence draws counts for every sequence of `ids` around it too.
|
||||
pub(crate) fn content_bboxes(doc: &Document, page: &Dict, ids: &HashSet<i64>) -> HashMap<i64, [f64; 4]> {
|
||||
@@ -274,20 +293,7 @@ pub(crate) fn content_bboxes(doc: &Document, page: &Dict, ids: &HashSet<i64>) ->
|
||||
path.clear();
|
||||
}
|
||||
b"Do" if inside => {
|
||||
let Some(r) = op.name(0).and_then(|n| xobjects.get(n)).and_then(Object::as_ref) else { continue };
|
||||
let unit = match &*doc.get(r) {
|
||||
Object::Stream(s) if s.dict.name(b"Subtype") == Some(b"Form") => {
|
||||
let bb: Vec<f64> =
|
||||
s.dict.get(b"BBox").and_then(|b| b.as_array().map(|a| a.iter().filter_map(Object::as_f64).collect())).unwrap_or_default();
|
||||
let m = Matrix::from_operands(s.dict.get(b"Matrix").and_then(|m| m.as_array()).map(Vec::as_slice).unwrap_or(&[]))
|
||||
.unwrap_or(Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
|
||||
match bb[..] {
|
||||
[a, b, c, d] => m.then(&top).bbox([a, b, c, d]),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ => top.bbox([0.0, 0.0, 1.0, 1.0]),
|
||||
};
|
||||
let Some(unit) = xobject_bbox(doc, &xobjects, &op, &top) else { continue };
|
||||
draw(&mut marks, unit);
|
||||
}
|
||||
b"BI" if inside => draw(&mut marks, top.bbox([0.0, 0.0, 1.0, 1.0])),
|
||||
|
||||
+33
-27
@@ -132,6 +132,38 @@ fn harmless_inside(op: &[u8]) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// The `(BT, ET)` operator index pairs of each text object that shows text, or why the page cannot be tagged.
|
||||
fn text_spans(ops: &[Op], index: usize) -> Result<Vec<(usize, usize)>, AutoTagError> {
|
||||
let mut start = None;
|
||||
let mut has_text = false;
|
||||
let mut spans = Vec::new();
|
||||
for (at, op) in ops.iter().enumerate() {
|
||||
match op.op.as_slice() {
|
||||
b"BT" if start.is_none() => {
|
||||
start = Some(at);
|
||||
has_text = false;
|
||||
}
|
||||
b"ET" => {
|
||||
let Some(first) = start.take() else { return Err(unsupported(index, "unmatched ET operator")) };
|
||||
if has_text {
|
||||
spans.push((first, at));
|
||||
if spans.len() > MAX_TEXT_OBJECTS {
|
||||
return Err(unsupported(index, "too many text objects (maximum 4096)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
b"Tj" | b"TJ" | b"'" | b"\"" if start.is_some() => has_text = true,
|
||||
name if start.is_some() && harmless_inside(name) => {}
|
||||
name if start.is_none() && harmless_outside(name) => {}
|
||||
_ => return Err(unsupported(index, format!("unsupported or painted operator {}", String::from_utf8_lossy(&op.op)))),
|
||||
}
|
||||
}
|
||||
if start.is_some() {
|
||||
return Err(unsupported(index, "unterminated BT text object"));
|
||||
}
|
||||
Ok(spans)
|
||||
}
|
||||
|
||||
fn plan(doc: &Document) -> Result<Vec<Plan>, AutoTagError> {
|
||||
let root = doc.root().ok_or(AutoTagError::NoCatalog)?;
|
||||
let catalog = doc.get(root);
|
||||
@@ -163,33 +195,7 @@ fn plan(doc: &Document) -> Result<Vec<Plan>, AutoTagError> {
|
||||
if parsed.skipped > 0 || parsed.ops.len() > MAX_OPERATORS {
|
||||
return Err(unsupported(index, "content has malformed or too many operators"));
|
||||
}
|
||||
let mut start = None;
|
||||
let mut has_text = false;
|
||||
let mut spans = Vec::new();
|
||||
for (at, op) in parsed.ops.iter().enumerate() {
|
||||
match op.op.as_slice() {
|
||||
b"BT" if start.is_none() => {
|
||||
start = Some(at);
|
||||
has_text = false;
|
||||
}
|
||||
b"ET" => {
|
||||
let Some(first) = start.take() else { return Err(unsupported(index, "unmatched ET operator")) };
|
||||
if has_text {
|
||||
spans.push((first, at));
|
||||
if spans.len() > MAX_TEXT_OBJECTS {
|
||||
return Err(unsupported(index, "too many text objects (maximum 4096)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
b"Tj" | b"TJ" | b"'" | b"\"" if start.is_some() => has_text = true,
|
||||
name if start.is_some() && harmless_inside(name) => {}
|
||||
name if start.is_none() && harmless_outside(name) => {}
|
||||
_ => return Err(unsupported(index, format!("unsupported or painted operator {}", String::from_utf8_lossy(&op.op)))),
|
||||
}
|
||||
}
|
||||
if start.is_some() {
|
||||
return Err(unsupported(index, "unterminated BT text object"));
|
||||
}
|
||||
let spans = text_spans(&parsed.ops, index)?;
|
||||
total = total.saturating_add(spans.len());
|
||||
out.push(Plan { page: page.obj, ops: parsed.ops, spans });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user