Skip to content

Commit 2e49ed9

Browse files
authored
fix(pptx): add slide number and depth to email addresses (#4534)
## What Email addresses in PowerPoint files now get the same slide number and heading depth as the text around them. ``` Before: {"type": "EmailAddress", "text": "sales@example.com", "metadata": {"filename": "deck.pptx"}} After: {"type": "EmailAddress", "text": "sales@example.com", "metadata": {"filename": "deck.pptx", "page_number": 2, "category_depth": 1}} ``` ## Why Every other text element on a slide was created with page and depth metadata. Email addresses were the one case that was skipped, so consumers could not tell which slide an email came from. Email detection and the depth of nearby headings are unchanged. ## Notes for consumers Element IDs change for PPTX email addresses, and for any element that follows an email on the same slide. IDs are a hash that includes the page number and the element's position on its page. Emails used to have no page number, which also reset the position count for the elements after them. Re-processing a deck that contains email addresses will produce some new IDs, which matters if you upsert or deduplicate by `element_id`. ## Validation New tests cover two slides, a custom starting page, emails in titles and body text, and both file and filename input. They fail on `main`. All 55 PPTX tests pass.
1 parent 771caf4 commit 2e49ed9

5 files changed

Lines changed: 72 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.27.22
2+
3+
### Fixes
4+
5+
- **Preserve slide and category-depth metadata on PPTX email addresses.** Emails in title and body shapes now receive the same text metadata as neighboring elements, including custom starting page numbers. Email classification and title-depth progression are unchanged. Element IDs change for these emails and for elements that follow an email on the same slide, because IDs include the page number and the element's position on its page.
6+
7+
18
## 0.27.21
29

310
### Fixes

‎scripts/ci/install-system-deps.sh‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ install_packages() {
3030
sudo apt-get "${apt_options[@]}" --no-download install -y "$@"
3131
}
3232

33+
# GitHub's Azure mirror can stall large LibreOffice downloads. Use Ubuntu's
34+
# primary HTTPS mirror for both legacy and deb822 source files.
35+
for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
36+
if [[ -f "$source_file" ]]; then
37+
sudo sed -i -E 's|https?://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' "$source_file"
38+
fi
39+
done
40+
3341
retry_download apt-get "${apt_options[@]}" -o APT::Update::Error-Mode=any update
3442
if [[ "${1:-}" == "--packages-only" ]]; then
3543
shift

‎test_unstructured/partition/test_pptx.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from unstructured.documents.elements import (
2929
Element,
3030
ElementMetadata,
31+
EmailAddress,
3132
Image,
3233
ListItem,
3334
NarrativeText,
@@ -797,3 +798,52 @@ def last_modified_prop_(self, request: FixtureRequest):
797798
@pytest.fixture()
798799
def metadata_file_path_prop_(self, request: FixtureRequest):
799800
return property_mock(request, PptxPartitionerOptions, "metadata_file_path")
801+
802+
803+
@pytest.mark.parametrize("input_kind", ["filename", "file"])
804+
def test_partition_pptx_preserves_email_slide_and_depth_metadata(tmp_path, input_kind):
805+
presentation = pptx.Presentation()
806+
for slide_number in range(2):
807+
slide = presentation.slides.add_slide(presentation.slide_layouts[1])
808+
title_frame = slide.shapes.title.text_frame
809+
title_frame.text = "First heading"
810+
title_frame.add_paragraph().text = f"title{slide_number}@example.com"
811+
title_frame.add_paragraph().text = "Next heading"
812+
body_frame = slide.placeholders[1].text_frame
813+
body_frame.text = f"body{slide_number}@example.com"
814+
body_frame.paragraphs[0].level = 2
815+
816+
filename = tmp_path / "emails.pptx"
817+
presentation.save(filename)
818+
kwargs = (
819+
{"filename": str(filename)}
820+
if input_kind == "filename"
821+
else {
822+
"file": io.BytesIO(filename.read_bytes()),
823+
"metadata_filename": filename.name,
824+
}
825+
)
826+
elements = partition_pptx(
827+
**kwargs,
828+
starting_page_number=10,
829+
include_page_breaks=False,
830+
metadata_last_modified="2024-04-02T20:32:35",
831+
languages=["eng"],
832+
)
833+
834+
emails = [element for element in elements if isinstance(element, EmailAddress)]
835+
assert [(e.text, e.metadata.page_number, e.metadata.category_depth) for e in emails] == [
836+
("title0@example.com", 10, 1),
837+
("body0@example.com", 10, 2),
838+
("title1@example.com", 11, 1),
839+
("body1@example.com", 11, 2),
840+
]
841+
assert all(e.metadata.filename == filename.name for e in emails)
842+
assert all(e.metadata.last_modified == "2024-04-02T20:32:35" for e in emails)
843+
titles = [element for element in elements if isinstance(element, Title)]
844+
assert [(e.text, e.metadata.category_depth) for e in titles] == [
845+
("First heading", 0),
846+
("Next heading", 1),
847+
("First heading", 0),
848+
("Next heading", 1),
849+
]

‎unstructured/__version__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.27.21" # pragma: no cover
1+
__version__ = "0.27.22" # pragma: no cover

‎unstructured/partition/pptx.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def _iter_shape_elements(self, shape: Shape) -> Iterator[Element]:
232232
if self._is_bulleted_paragraph(paragraph):
233233
yield ListItem(text=text, metadata=metadata, detection_origin=DETECTION_ORIGIN)
234234
elif is_email_address(text):
235-
yield EmailAddress(text=text, detection_origin=DETECTION_ORIGIN)
235+
yield EmailAddress(text=text, metadata=metadata, detection_origin=DETECTION_ORIGIN)
236236
elif is_possible_narrative_text(text):
237237
yield NarrativeText(
238238
text=text,
@@ -290,7 +290,11 @@ def _iter_title_shape_element(self, shape: Shape) -> Iterator[Element]:
290290
detection_origin=DETECTION_ORIGIN,
291291
)
292292
elif is_email_address(text):
293-
yield EmailAddress(text=text, detection_origin=DETECTION_ORIGIN)
293+
yield EmailAddress(
294+
text=text,
295+
metadata=self._opts.text_metadata(category_depth=depth),
296+
detection_origin=DETECTION_ORIGIN,
297+
)
294298
else:
295299
# increment the category depth by the paragraph increment in the shape
296300
yield Title(

0 commit comments

Comments
 (0)