| 1 | """Speaker-notes parts for cloned slides. |
| 2 | |
| 3 | Builds native PowerPoint notes-slide XML and the slide<->notesSlide<->notesMaster |
| 4 | relationships from a plan's ``notes`` field, reusing the SVG pipeline's notes |
| 5 | renderer so embedded notes also feed ``notes_to_audio.py``. |
| 6 | """ |
| 7 | |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | import posixpath |
| 11 | from xml.etree import ElementTree as ET |
| 12 | |
| 13 | from svg_to_pptx.pptx_package.notes import create_notes_slide_xml, markdown_to_plain_text |
| 14 | |
| 15 | from .ooxml import NOTES_SLIDE_REL_TYPE, REL_NS, SLIDE_REL_TYPE, _qn, _xml_bytes |
| 16 | from .package import _empty_relationships_root, _max_numeric_rid |
| 17 | |
| 18 | |
| 19 | def _find_notes_master_target(entries: dict[str, bytes]) -> str | None: |
| 20 | notes_master_rel_type = ( |
| 21 | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" |
| 22 | ) |
| 23 | |
| 24 | for name, data in entries.items(): |
| 25 | if not name.startswith("ppt/notesSlides/_rels/notesSlide") or not name.endswith(".xml.rels"): |
| 26 | continue |
| 27 | try: |
| 28 | root = ET.fromstring(data) |
| 29 | except ET.ParseError: |
| 30 | continue |
| 31 | for rel in root.findall(_qn(REL_NS, "Relationship")): |
| 32 | if rel.attrib.get("Type") == notes_master_rel_type: |
| 33 | return rel.attrib.get("Target") |
| 34 | |
| 35 | presentation_rels = entries.get("ppt/_rels/presentation.xml.rels") |
| 36 | if not presentation_rels: |
| 37 | return None |
| 38 | try: |
| 39 | root = ET.fromstring(presentation_rels) |
| 40 | except ET.ParseError: |
| 41 | return None |
| 42 | for rel in root.findall(_qn(REL_NS, "Relationship")): |
| 43 | if rel.attrib.get("Type") != notes_master_rel_type: |
| 44 | continue |
| 45 | target = rel.attrib.get("Target") |
| 46 | if not target: |
| 47 | return None |
| 48 | if target.startswith("/"): |
| 49 | target = target.lstrip("/") |
| 50 | else: |
| 51 | target = posixpath.normpath(posixpath.join("ppt", target)) |
| 52 | return posixpath.relpath(target, "ppt/notesSlides") |
| 53 | return None |
| 54 | |
| 55 | |
| 56 | def _create_notes_rels_xml(slide_number: int, notes_master_target: str | None) -> bytes: |
| 57 | root = _empty_relationships_root() |
| 58 | if notes_master_target: |
| 59 | ET.SubElement( |
| 60 | root, |
| 61 | _qn(REL_NS, "Relationship"), |
| 62 | { |
| 63 | "Id": "rId1", |
| 64 | "Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster", |
| 65 | "Target": notes_master_target, |
| 66 | }, |
| 67 | ) |
| 68 | slide_rel_id = "rId2" |
| 69 | else: |
| 70 | slide_rel_id = "rId1" |
| 71 | ET.SubElement( |
| 72 | root, |
| 73 | _qn(REL_NS, "Relationship"), |
| 74 | { |
| 75 | "Id": slide_rel_id, |
| 76 | "Type": SLIDE_REL_TYPE, |
| 77 | "Target": f"../slides/slide{slide_number}.xml", |
| 78 | }, |
| 79 | ) |
| 80 | return _xml_bytes(root) |
| 81 | |
| 82 | |
| 83 | def _slide_rels_with_notes( |
| 84 | rels_bytes: bytes | None, |
| 85 | *, |
| 86 | slide_number: int, |
| 87 | notes_text: str, |
| 88 | notes_master_target: str | None, |
| 89 | ) -> tuple[bytes, dict[str, bytes]]: |
| 90 | root = ET.fromstring(rels_bytes) if rels_bytes else _empty_relationships_root() |
| 91 | for rel in list(root.findall(_qn(REL_NS, "Relationship"))): |
| 92 | if rel.attrib.get("Type") == NOTES_SLIDE_REL_TYPE: |
| 93 | root.remove(rel) |
| 94 | |
| 95 | note_entries: dict[str, bytes] = {} |
| 96 | notes_text = notes_text.strip() |
| 97 | if notes_text: |
| 98 | rel_id = f"rId{_max_numeric_rid(root) + 1}" |
| 99 | notes_part = f"ppt/notesSlides/notesSlide{slide_number}.xml" |
| 100 | notes_rels_part = f"ppt/notesSlides/_rels/notesSlide{slide_number}.xml.rels" |
| 101 | ET.SubElement( |
| 102 | root, |
| 103 | _qn(REL_NS, "Relationship"), |
| 104 | { |
| 105 | "Id": rel_id, |
| 106 | "Type": NOTES_SLIDE_REL_TYPE, |
| 107 | "Target": f"../notesSlides/notesSlide{slide_number}.xml", |
| 108 | }, |
| 109 | ) |
| 110 | plain_notes = markdown_to_plain_text(notes_text) |
| 111 | note_entries[notes_part] = create_notes_slide_xml(slide_number, plain_notes).encode("utf-8") |
| 112 | note_entries[notes_rels_part] = _create_notes_rels_xml(slide_number, notes_master_target) |
| 113 | |
| 114 | return _xml_bytes(root), note_entries |
| 115 |