Release standalone PPT template workflow 0.5.0

This commit is contained in:
2026-10-10 21:02:47 +08:00
commit 4e3271ff62
303 changed files with 190946 additions and 0 deletions
@@ -0,0 +1,114 @@
"""Speaker-notes parts for cloned slides.
Builds native PowerPoint notes-slide XML and the slide<->notesSlide<->notesMaster
relationships from a plan's ``notes`` field, reusing the SVG pipeline's notes
renderer so embedded notes also feed ``notes_to_audio.py``.
"""
from __future__ import annotations
import posixpath
from xml.etree import ElementTree as ET
from svg_to_pptx.pptx_package.notes import create_notes_slide_xml, markdown_to_plain_text
from .ooxml import NOTES_SLIDE_REL_TYPE, REL_NS, SLIDE_REL_TYPE, _qn, _xml_bytes
from .package import _empty_relationships_root, _max_numeric_rid
def _find_notes_master_target(entries: dict[str, bytes]) -> str | None:
notes_master_rel_type = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster"
)
for name, data in entries.items():
if not name.startswith("ppt/notesSlides/_rels/notesSlide") or not name.endswith(".xml.rels"):
continue
try:
root = ET.fromstring(data)
except ET.ParseError:
continue
for rel in root.findall(_qn(REL_NS, "Relationship")):
if rel.attrib.get("Type") == notes_master_rel_type:
return rel.attrib.get("Target")
presentation_rels = entries.get("ppt/_rels/presentation.xml.rels")
if not presentation_rels:
return None
try:
root = ET.fromstring(presentation_rels)
except ET.ParseError:
return None
for rel in root.findall(_qn(REL_NS, "Relationship")):
if rel.attrib.get("Type") != notes_master_rel_type:
continue
target = rel.attrib.get("Target")
if not target:
return None
if target.startswith("/"):
target = target.lstrip("/")
else:
target = posixpath.normpath(posixpath.join("ppt", target))
return posixpath.relpath(target, "ppt/notesSlides")
return None
def _create_notes_rels_xml(slide_number: int, notes_master_target: str | None) -> bytes:
root = _empty_relationships_root()
if notes_master_target:
ET.SubElement(
root,
_qn(REL_NS, "Relationship"),
{
"Id": "rId1",
"Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster",
"Target": notes_master_target,
},
)
slide_rel_id = "rId2"
else:
slide_rel_id = "rId1"
ET.SubElement(
root,
_qn(REL_NS, "Relationship"),
{
"Id": slide_rel_id,
"Type": SLIDE_REL_TYPE,
"Target": f"../slides/slide{slide_number}.xml",
},
)
return _xml_bytes(root)
def _slide_rels_with_notes(
rels_bytes: bytes | None,
*,
slide_number: int,
notes_text: str,
notes_master_target: str | None,
) -> tuple[bytes, dict[str, bytes]]:
root = ET.fromstring(rels_bytes) if rels_bytes else _empty_relationships_root()
for rel in list(root.findall(_qn(REL_NS, "Relationship"))):
if rel.attrib.get("Type") == NOTES_SLIDE_REL_TYPE:
root.remove(rel)
note_entries: dict[str, bytes] = {}
notes_text = notes_text.strip()
if notes_text:
rel_id = f"rId{_max_numeric_rid(root) + 1}"
notes_part = f"ppt/notesSlides/notesSlide{slide_number}.xml"
notes_rels_part = f"ppt/notesSlides/_rels/notesSlide{slide_number}.xml.rels"
ET.SubElement(
root,
_qn(REL_NS, "Relationship"),
{
"Id": rel_id,
"Type": NOTES_SLIDE_REL_TYPE,
"Target": f"../notesSlides/notesSlide{slide_number}.xml",
},
)
plain_notes = markdown_to_plain_text(notes_text)
note_entries[notes_part] = create_notes_slide_xml(slide_number, plain_notes).encode("utf-8")
note_entries[notes_rels_part] = _create_notes_rels_xml(slide_number, notes_master_target)
return _xml_bytes(root), note_entries