Skip to content

Commit

Permalink
fix: #972 next-slide-id fails when max used
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
scanny committed Aug 3, 2024
1 parent d5c95be commit 6360f3f
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 4 deletions.
21 changes: 17 additions & 4 deletions src/pptx/oxml/presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING, Callable, cast

from pptx.oxml.simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString
from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore, ZeroOrOne
Expand Down Expand Up @@ -67,14 +67,27 @@ def add_sldId(self, rId: str) -> CT_SlideId:
return self._add_sldId(id=self._next_id, rId=rId)

@property
def _next_id(self):
def _next_id(self) -> int:
"""The next available slide ID as an `int`.
Valid slide IDs start at 256. The next integer value greater than the max value in use is
chosen, which minimizes that chance of reusing the id of a deleted slide.
"""
id_str_lst = self.xpath("./p:sldId/@id")
return max([255] + [int(id_str) for id_str in id_str_lst]) + 1
MIN_SLIDE_ID = 256
MAX_SLIDE_ID = 2147483647

used_ids = [int(s) for s in cast(list[str], self.xpath("./p:sldId/@id"))]
simple_next = max([MIN_SLIDE_ID - 1] + used_ids) + 1
if simple_next <= MAX_SLIDE_ID:
return simple_next

# -- fall back to search for next unused from bottom --
used_ids = sorted(used_ids)
return next(
candidate_id
for candidate_id, used_id in enumerate(used_ids, start=MIN_SLIDE_ID)
if candidate_id != used_id
)


class CT_SlideMasterIdList(BaseOxmlElement):
Expand Down
21 changes: 21 additions & 0 deletions tests/oxml/test_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,24 @@ def it_can_add_a_sldId_element_as_a_child(self):
def it_knows_the_next_available_slide_id(self, sldIdLst_cxml: str, expected_value: int):
sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml))
assert sldIdLst._next_id == expected_value

@pytest.mark.parametrize(
("sldIdLst_cxml", "expected_value"),
[
("p:sldIdLst/p:sldId{id=2147483646}", 2147483647),
("p:sldIdLst/p:sldId{id=2147483647}", 256),
# -- 2147483648 is not a valid slide-id but shouldn't stop us from finding a valid id --
("p:sldIdLst/p:sldId{id=2147483648}", 256),
("p:sldIdLst/(p:sldId{id=256},p:sldId{id=2147483648})", 257),
("p:sldIdLst/(p:sldId{id=256},p:sldId{id=257},p:sldId{id=2147483648})", 258),
],
)
def and_it_chooses_a_valid_slide_id_when_max_slide_id_is_used_for_a_slide(
self, sldIdLst_cxml: str, expected_value: int
):
sldIdLst = cast(CT_SlideIdList, element(sldIdLst_cxml))

slide_id = sldIdLst._next_id

assert 256 <= slide_id <= 2147483647
assert slide_id == expected_value

0 comments on commit 6360f3f

Please sign in to comment.