返回 ppt-master
notes.py
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
115 lines PYTHON