Skip to content

Commit 799b214

Browse files
committed
fix: #972 next-slide-id fails when max used
#972 Naive "max + 1" slide-id allocation algorithm assumed that slide-ids were assigned from bottom up. Apparently some client assigns slide ids from the top (2,147,483,647) down and the naive algorithm would assign an invalid slide-id in that case. Detect when the assigned id is out-of-range and fall-back to a robust algorithm for assigning a valid id based on a "first unused starting at bottom" policy.
1 parent d5c95be commit 799b214

File tree

2 files changed

+44
-4
lines changed

2 files changed

+44
-4
lines changed

src/pptx/oxml/presentation.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Callable
5+
from typing import TYPE_CHECKING, Callable, cast
66

77
from pptx.oxml.simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString
88
from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore, ZeroOrOne
@@ -67,14 +67,31 @@ def add_sldId(self, rId: str) -> CT_SlideId:
6767
return self._add_sldId(id=self._next_id, rId=rId)
6868

6969
@property
70-
def _next_id(self):
70+
def _next_id(self) -> int:
7171
"""The next available slide ID as an `int`.
7272
7373
Valid slide IDs start at 256. The next integer value greater than the max value in use is
7474
chosen, which minimizes that chance of reusing the id of a deleted slide.
7575
"""
76-
id_str_lst = self.xpath("./p:sldId/@id")
77-
return max([255] + [int(id_str) for id_str in id_str_lst]) + 1
76+
MIN_SLIDE_ID = 256
77+
MAX_SLIDE_ID = 2147483647
78+
79+
used_ids = [int(s) for s in cast(list[str], self.xpath("./p:sldId/@id"))]
80+
simple_next = max([MIN_SLIDE_ID - 1] + used_ids) + 1
81+
if simple_next <= MAX_SLIDE_ID:
82+
return simple_next
83+
84+
# -- fall back to search for next unused from bottom --
85+
valid_used_ids = sorted(id for id in used_ids if (MIN_SLIDE_ID <= id <= MAX_SLIDE_ID))
86+
return (
87+
next(
88+
candidate_id
89+
for candidate_id, used_id in enumerate(valid_used_ids, start=MIN_SLIDE_ID)
90+
if candidate_id != used_id
91+
)
92+
if valid_used_ids
93+
else 256
94+
)
7895

7996

8097
class CT_SlideMasterIdList(BaseOxmlElement):

tests/oxml/test_presentation.py

+23
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,26 @@ def it_can_add_a_sldId_element_as_a_child(self):
3838
def it_knows_the_next_available_slide_id(self, sldIdLst_cxml: str, expected_value: int):
3939
sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml))
4040
assert sldIdLst._next_id == expected_value
41+
42+
@pytest.mark.parametrize(
43+
("sldIdLst_cxml", "expected_value"),
44+
[
45+
("p:sldIdLst/p:sldId{id=2147483646}", 2147483647),
46+
("p:sldIdLst/p:sldId{id=2147483647}", 256),
47+
# -- 2147483648 is not a valid id but shouldn't stop us from finding a one that is --
48+
("p:sldIdLst/p:sldId{id=2147483648}", 256),
49+
("p:sldIdLst/(p:sldId{id=256},p:sldId{id=2147483647})", 257),
50+
("p:sldIdLst/(p:sldId{id=256},p:sldId{id=2147483647},p:sldId{id=257})", 258),
51+
# -- 245 is also not a valid id but that shouldn't change the result either --
52+
("p:sldIdLst/(p:sldId{id=245},p:sldId{id=2147483647},p:sldId{id=256})", 257),
53+
],
54+
)
55+
def and_it_chooses_a_valid_slide_id_when_max_slide_id_is_used_for_a_slide(
56+
self, sldIdLst_cxml: str, expected_value: int
57+
):
58+
sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml))
59+
60+
slide_id = sldIdLst._next_id
61+
62+
assert 256 <= slide_id <= 2147483647
63+
assert slide_id == expected_value

0 commit comments

Comments
 (0)