feat(inbox): docx table cells + grouped pptx shapes (stage 2g)
Two structural regions the office extractors missed, no new dependency: - .docx table cells (they live outside doc.paragraphs); - grouped .pptx shapes (add_group_shape moves a shape inside the group, so the extractor recurses through MSO_SHAPE_TYPE.GROUP to reach it). Each proven by an injection-in-region REJECT plus a clean-region ADMIT. Tests 310 -> 314.
This commit is contained in:
parent
abcdfa3663
commit
6f32e70c6d
2 changed files with 77 additions and 1 deletions
|
|
@ -137,6 +137,13 @@ def _extract_docx_text(fs_path) -> str:
|
|||
doc = Document(str(fs_path))
|
||||
parts = [para.text for para in doc.paragraphs if para.text]
|
||||
|
||||
# Table cells live outside doc.paragraphs — iterate them explicitly.
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
if cell.text:
|
||||
parts.append(cell.text)
|
||||
|
||||
cp = doc.core_properties
|
||||
for attr in ("title", "subject", "keywords", "comments", "category", "author"):
|
||||
value = getattr(cp, attr, None)
|
||||
|
|
@ -165,11 +172,21 @@ def _extract_pptx_text(fs_path) -> str:
|
|||
image/shape alt-text. ``python-pptx`` is imported lazily (dev/showcase-scoped).
|
||||
"""
|
||||
from pptx import Presentation
|
||||
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
||||
|
||||
def walk(shapes):
|
||||
# Flatten grouped shapes: add_group_shape moves a shape inside the group,
|
||||
# so only recursion reaches its text/alt-text.
|
||||
for shape in shapes:
|
||||
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
|
||||
yield from walk(shape.shapes)
|
||||
else:
|
||||
yield shape
|
||||
|
||||
prs = Presentation(str(fs_path))
|
||||
parts: list = []
|
||||
for slide in prs.slides:
|
||||
for shape in slide.shapes:
|
||||
for shape in walk(slide.shapes):
|
||||
if shape.has_text_frame and shape.text_frame.text:
|
||||
parts.append(shape.text_frame.text) # incl. off-slide boxes
|
||||
alt = _shape_alt_text(shape)
|
||||
|
|
|
|||
|
|
@ -363,3 +363,62 @@ def test_pptx_notes_detach_proof(tmp_path):
|
|||
fp = _make_pptx(tmp_path, body="A routine slide.")
|
||||
_extracted, _result, verdict = receive([fp])
|
||||
assert verdict == "ADMIT"
|
||||
|
||||
|
||||
# --- slice 2g: office-extractor completeness (no new dep) -------------------
|
||||
# Two structural regions the earlier slices did not reach: .docx table cells (they
|
||||
# live outside doc.paragraphs) and grouped .pptx shapes (add_group_shape moves the
|
||||
# shape inside the group, so only recursion reaches it).
|
||||
|
||||
|
||||
def _docx_with_table(tmp_path, cell_text, name="table.docx"):
|
||||
from docx import Document
|
||||
|
||||
d = Document()
|
||||
d.add_paragraph("A normal paragraph.")
|
||||
table = d.add_table(rows=1, cols=2)
|
||||
table.cell(0, 0).text = "label"
|
||||
table.cell(0, 1).text = cell_text
|
||||
fp = tmp_path / name
|
||||
d.save(str(fp))
|
||||
return fp
|
||||
|
||||
|
||||
def _pptx_with_grouped_text(tmp_path, text, name="grouped.pptx"):
|
||||
from pptx import Presentation
|
||||
from pptx.util import Emu
|
||||
|
||||
prs = Presentation()
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
tb = slide.shapes.add_textbox(Emu(0), Emu(0), Emu(1000000), Emu(400000))
|
||||
tb.text_frame.text = text
|
||||
slide.shapes.add_group_shape([tb]) # moves tb inside the group (not top-level)
|
||||
fp = tmp_path / name
|
||||
prs.save(str(fp))
|
||||
return fp
|
||||
|
||||
|
||||
def test_docx_table_cell_injection_is_caught(tmp_path):
|
||||
fp = _docx_with_table(tmp_path, _INJECTION)
|
||||
_extracted, result, verdict = receive([fp])
|
||||
assert result.disposition is Disposition.FAIL_SECURE
|
||||
assert verdict == "REJECT"
|
||||
|
||||
|
||||
def test_clean_docx_table_admits(tmp_path):
|
||||
fp = _docx_with_table(tmp_path, "a clean value")
|
||||
_extracted, _result, verdict = receive([fp])
|
||||
assert verdict == "ADMIT"
|
||||
|
||||
|
||||
def test_pptx_grouped_shape_injection_is_caught(tmp_path):
|
||||
fp = _pptx_with_grouped_text(tmp_path, _INJECTION)
|
||||
_extracted, result, verdict = receive([fp])
|
||||
assert result.disposition is Disposition.FAIL_SECURE
|
||||
assert verdict == "REJECT"
|
||||
|
||||
|
||||
def test_clean_pptx_grouped_shape_admits(tmp_path):
|
||||
fp = _pptx_with_grouped_text(tmp_path, "clean grouped text")
|
||||
_extracted, _result, verdict = receive([fp])
|
||||
assert verdict == "ADMIT"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue