Add native dynamic slide numbers (0.9.2)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""Bind template-declared slide-number placeholders to native Office fields."""
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
import posixpath
|
||||
import uuid
|
||||
import zipfile
|
||||
|
||||
from lxml import etree as OXML
|
||||
|
||||
|
||||
NS = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
|
||||
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
|
||||
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
|
||||
|
||||
|
||||
def prepare_slide_number(root, definition, number):
|
||||
"""Use the Layout's fixed carrier, including for older authored page sources.
|
||||
|
||||
The runtime converts a marked number only when its cache equals its actual
|
||||
initial slide position. The cache seeds the field; it is never its identity.
|
||||
"""
|
||||
if type(number) is not int or number < 1:
|
||||
raise ValueError("Initial slide number must be a positive integer")
|
||||
find = lambda tree: [node for node in tree.iter()
|
||||
if node.get("data-pptx-placeholder") == "slide-number"]
|
||||
declared, current = find(definition), find(root)
|
||||
if len(declared) > 1 or len(current) > 1:
|
||||
raise ValueError("Each Layout/page supports one slide-number placeholder")
|
||||
if not declared:
|
||||
if current:
|
||||
raise ValueError("Page declares a slide number absent from its Layout")
|
||||
return 0
|
||||
replacement = deepcopy(declared[0])
|
||||
carriers = [node for node in replacement.iter()
|
||||
if node.tag.rsplit("}", 1)[-1] == "text"
|
||||
and node.get("data-pptx-carrier") == "true"]
|
||||
if len(carriers) != 1:
|
||||
raise ValueError("Slide-number placeholder requires one explicit text carrier")
|
||||
carrier = carriers[0]
|
||||
for child in list(carrier):
|
||||
carrier.remove(child)
|
||||
carrier.text = str(number)
|
||||
if current:
|
||||
parent = next((node for node in root.iter() if current[0] in list(node)), None)
|
||||
if parent is None:
|
||||
raise ValueError("Slide-number placeholder must be a child of the page root")
|
||||
index = list(parent).index(current[0])
|
||||
parent.remove(current[0])
|
||||
parent.insert(index, replacement)
|
||||
else:
|
||||
root.append(replacement)
|
||||
return 1
|
||||
|
||||
|
||||
def finalize_slide_numbers(path: Path, expected: list[int]):
|
||||
"""Check Slide-local fields and give every page-number field a unique ID."""
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
parts = {item.filename: archive.read(item) for item in archive.infolist()}
|
||||
trees = {name: OXML.fromstring(value) for name, value in parts.items()
|
||||
if name.startswith("ppt/") and name.endswith(".xml")}
|
||||
presentation = trees["ppt/presentation.xml"]
|
||||
if int(presentation.get("firstSlideNum", "1")) != 1:
|
||||
raise ValueError("Native export expected the declared initial numbering origin 1")
|
||||
rels = OXML.fromstring(parts["ppt/_rels/presentation.xml.rels"])
|
||||
targets = {node.get("Id"): node.get("Target") for node in rels}
|
||||
slides = [posixpath.normpath(posixpath.join("ppt", targets[node.get("{" + NS["r"] + "}id")])).lstrip("/")
|
||||
for node in presentation.findall("p:sldIdLst/p:sldId", NS)]
|
||||
if len(slides) != len(expected):
|
||||
raise ValueError("Slide-number validation requires the actual presentation order")
|
||||
fields = 0
|
||||
for index, (name, count) in enumerate(zip(slides, expected), 1):
|
||||
root = trees[name]
|
||||
shapes = [shape for shape in root.findall(".//p:sp", NS)
|
||||
if shape.find("p:nvSpPr/p:nvPr/p:ph[@type='sldNum']", NS) is not None]
|
||||
if len(shapes) != count:
|
||||
raise ValueError(f"Slide {index}: slide-number placeholder count differs from its Layout")
|
||||
for shape in shapes:
|
||||
actual = shape.findall("p:txBody/a:p/a:fld", NS)
|
||||
if len(actual) != 1 or actual[0].get("type") != "slidenum":
|
||||
raise ValueError(f"Slide {index}: page number must be a native slidenum field")
|
||||
if [node.text for node in shape.findall(".//a:t", NS)] != [str(index)]:
|
||||
raise ValueError(f"Slide {index}: field cache differs from its actual initial position")
|
||||
fields += 1
|
||||
# The frozen runtime reuses a GUID for slide-local numbers. Office requires
|
||||
# distinct field identities; fix only these fields, leaving other fields alone.
|
||||
changed = {}
|
||||
for name, root in trees.items():
|
||||
numbers = root.findall(".//a:fld[@type='slidenum']", NS)
|
||||
if numbers:
|
||||
for field in numbers:
|
||||
field.set("id", "{" + str(uuid.uuid4()).upper() + "}")
|
||||
changed[name] = OXML.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||
if changed:
|
||||
temporary = path.with_suffix(".numbering.pptx")
|
||||
try:
|
||||
with zipfile.ZipFile(temporary, "x", zipfile.ZIP_DEFLATED) as archive:
|
||||
for name, value in parts.items():
|
||||
archive.writestr(name, changed.get(name, value))
|
||||
temporary.replace(path)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return {"slide_number_fields": fields, "slide_numbers_dynamic": bool(fields)}
|
||||
Reference in New Issue
Block a user