197 lines
6.3 KiB
Python
197 lines
6.3 KiB
Python
"""Tests for PDF <-> PPTX document conversion routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
|
|
import pypdf
|
|
from pptx import Presentation
|
|
|
|
from app.services.convert.converters import all_plugins, pdf_to_pptx, pptx_to_pdf_convert
|
|
from app.services.convert.formatters.pptx_formatter import format_pptx
|
|
from app.services.convert.idm.model import BBox, Block, BlockType, Document, Page
|
|
from app.services.convert.writers.pdf_from_pptx import pptx_to_pdf
|
|
|
|
|
|
def _make_sample_idm() -> Document:
|
|
doc = Document()
|
|
page = Page(index=0, width=792.0, height=612.0) # Landscape letter
|
|
|
|
# Slide Title
|
|
page.blocks.append(
|
|
Block(
|
|
type=BlockType.heading,
|
|
text="Q3 Global Operations Review",
|
|
bbox=BBox(x=72.0, y=520.0, w=648.0, h=40.0),
|
|
level=1,
|
|
reading_order=0,
|
|
)
|
|
)
|
|
|
|
# Subtitle / Body paragraph
|
|
page.blocks.append(
|
|
Block(
|
|
type=BlockType.paragraph,
|
|
text="Executive summary of key regional performance indicators.",
|
|
bbox=BBox(x=72.0, y=470.0, w=648.0, h=25.0),
|
|
reading_order=1,
|
|
)
|
|
)
|
|
|
|
# Table
|
|
table_grid = [
|
|
["Region", "Target", "Actual", "Growth %"],
|
|
["North America", "$5.0M", "$5.8M", "+16%"],
|
|
["EMEA", "$4.2M", "$4.5M", "+7%"],
|
|
["APAC", "$3.8M", "$4.4M", "+15%"],
|
|
]
|
|
page.blocks.append(
|
|
Block(
|
|
type=BlockType.table,
|
|
cells=table_grid,
|
|
bbox=BBox(x=72.0, y=200.0, w=648.0, h=240.0),
|
|
reading_order=2,
|
|
table_confidence=0.95,
|
|
)
|
|
)
|
|
|
|
doc.pages.append(page)
|
|
return doc
|
|
|
|
|
|
def test_registry_contains_pptx_routes():
|
|
plugins = {f"{p.source}->{p.target}": p for p in all_plugins()}
|
|
assert "pdf->pptx" in plugins
|
|
assert "pptx->pdf" in plugins
|
|
|
|
|
|
def test_format_pptx_from_idm():
|
|
idm = _make_sample_idm()
|
|
pptx_bytes = format_pptx(idm)
|
|
|
|
assert pptx_bytes[:2] == b"PK", "Must produce valid OOXML zip archive"
|
|
|
|
prs = Presentation(io.BytesIO(pptx_bytes))
|
|
assert len(prs.slides) == 1
|
|
|
|
slide = prs.slides[0]
|
|
shape_types = [shape.has_table for shape in slide.shapes]
|
|
assert any(shape_types), "Slide must contain a table shape"
|
|
|
|
# Find table
|
|
table_shape = next(shape for shape in slide.shapes if shape.has_table)
|
|
table = table_shape.table
|
|
assert len(table.rows) == 4
|
|
assert len(table.columns) == 4
|
|
|
|
headers = [cell.text.strip() for cell in table.rows[0].cells]
|
|
assert headers == ["Region", "Target", "Actual", "Growth %"]
|
|
|
|
first_row = [cell.text.strip() for cell in table.rows[1].cells]
|
|
assert first_row == ["North America", "$5.0M", "$5.8M", "+16%"]
|
|
|
|
|
|
def test_pptx_to_pdf_round_trip():
|
|
idm = _make_sample_idm()
|
|
pptx_bytes = format_pptx(idm)
|
|
|
|
pdf_bytes, warnings = pptx_to_pdf(pptx_bytes)
|
|
assert pdf_bytes.startswith(b"%PDF-")
|
|
|
|
reader = pypdf.PdfReader(io.BytesIO(pdf_bytes))
|
|
assert len(reader.pages) == 1
|
|
page_text = reader.pages[0].extract_text()
|
|
|
|
assert "Q3 Global Operations Review" in page_text
|
|
assert "North America" in page_text
|
|
assert "EMEA" in page_text
|
|
assert "+16%" in page_text
|
|
|
|
|
|
def _ruled_table_pdf() -> bytes:
|
|
"""A one-page PDF holding a real, ruled table — built, not downloaded.
|
|
|
|
This test used to read ``corpus/convert/_tmp_samples/real_web/w3c_pdf_table.pdf``
|
|
and skip when it was absent. It was always absent: ``_tmp_samples`` is a
|
|
scratch download directory that is not in the repository and is not meant
|
|
to be, and the path was relative, so the test also depended on the working
|
|
directory pytest happened to be started from. The result was a permanent
|
|
skip — the pdf->pptx table path shipped with no test covering it at all.
|
|
|
|
Generating the fixture removes both problems. The table is drawn with real
|
|
vector text and real ruling lines, which is what the table detector keys
|
|
on, so the conversion exercised here is the same one a scanned-free
|
|
business PDF would take.
|
|
"""
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.pagesizes import LETTER
|
|
from reportlab.lib.styles import getSampleStyleSheet
|
|
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
|
|
|
buffer = io.BytesIO()
|
|
doc = SimpleDocTemplate(buffer, pagesize=LETTER)
|
|
styles = getSampleStyleSheet()
|
|
|
|
rows = [
|
|
["Cohort", "Participants", "Accuracy"],
|
|
["Disability", "128", "94%"],
|
|
["Control", "131", "96%"],
|
|
]
|
|
table = Table(rows, colWidths=[160, 110, 90])
|
|
table.setStyle(
|
|
TableStyle(
|
|
[
|
|
("GRID", (0, 0), (-1, -1), 0.75, colors.black),
|
|
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
|
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
|
("FONTSIZE", (0, 0), (-1, -1), 11),
|
|
("ALIGN", (1, 0), (-1, -1), "CENTER"),
|
|
]
|
|
)
|
|
)
|
|
|
|
doc.build(
|
|
[
|
|
Paragraph("Accessibility Conformance Summary", styles["Title"]),
|
|
Spacer(1, 18),
|
|
table,
|
|
]
|
|
)
|
|
return buffer.getvalue()
|
|
|
|
|
|
def _pptx_texts(pptx_bytes: bytes) -> list[str]:
|
|
"""Every string a slide carries, from tables and text frames alike."""
|
|
prs = Presentation(io.BytesIO(pptx_bytes))
|
|
texts: list[str] = []
|
|
for slide in prs.slides:
|
|
for shape in slide.shapes:
|
|
if shape.has_table:
|
|
for row in shape.table.rows:
|
|
texts.extend(cell.text for cell in row.cells)
|
|
elif shape.has_text_frame:
|
|
texts.append(shape.text)
|
|
return texts
|
|
|
|
|
|
def test_pdf_to_pptx_recovers_ruled_table():
|
|
"""A ruled table in a PDF must survive the trip to PPTX with its words."""
|
|
pdf_bytes = _ruled_table_pdf()
|
|
assert pdf_bytes.startswith(b"%PDF-")
|
|
|
|
pptx_bytes, fidelity, warnings, media, score = pdf_to_pptx(pdf_bytes, "conformance.pdf")
|
|
|
|
assert pptx_bytes[:2] == b"PK"
|
|
assert media == "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
|
|
prs = Presentation(io.BytesIO(pptx_bytes))
|
|
assert len(prs.slides) >= 1
|
|
|
|
joined = " ".join(_pptx_texts(pptx_bytes))
|
|
# The header row, a data label and a figure: enough to prove the cells
|
|
# travelled, not just that a shape was created.
|
|
assert "Disability" in joined
|
|
assert "Participants" in joined
|
|
assert "Accuracy" in joined
|
|
assert "128" in joined
|