@@ -625,6 +625,74 @@ def title(self) -> Shape | None:
625625 return cast (Shape , self ._shape_factory (elm ))
626626 return None
627627
628+ @property
629+ def reading_order (self ) -> tuple [BaseShape , ...]:
630+ """Sequence of shapes in the order screen readers will narrate them.
631+
632+ Reading order on a slide that does not declare an explicit
633+ ``<p:tabLst>`` is the document order of children under
634+ ``<p:spTree>`` — i.e. the same order as iteration over
635+ :class:`SlideShapes`. Returned as a tuple so callers can compare
636+ against, slice, or index without affecting the underlying XML.
637+
638+ Assigning a reordered sequence reorders the underlying
639+ ``<p:spTree>`` children to match. The assigned sequence MUST be
640+ a permutation of this slide's existing shapes — same set, same
641+ length. Raises |ValueError| otherwise.
642+ """
643+ return tuple (self )
644+
645+ @reading_order .setter
646+ def reading_order (self , new_order ):
647+ """Reorder the slide's shape tree to match `new_order` (a permutation)."""
648+ new_list = list (new_order )
649+ existing = list (self )
650+ if len (new_list ) != len (existing ):
651+ raise ValueError (
652+ "reading_order must be a permutation of slide.shapes "
653+ "(got %d items, expected %d)" % (len (new_list ), len (existing ))
654+ )
655+ existing_elements = {s ._element for s in existing }
656+ new_elements = [s ._element for s in new_list ]
657+ if set (new_elements ) != existing_elements :
658+ raise ValueError ("reading_order must contain exactly the slide's existing shapes" )
659+ # ---reorder by removing-then-appending in new order. Children before the
660+ # first shape (e.g. nvGrpSpPr, grpSpPr) remain in place because we only
661+ # move the shape elements themselves.
662+ for elm in new_elements :
663+ self ._spTree .remove (elm )
664+ for elm in new_elements :
665+ self ._spTree .append (elm )
666+
667+ def accessibility_issues (self ) -> list [BaseShape ]:
668+ """Return shapes on this slide that fail basic accessibility lint.
669+
670+ A shape is flagged when it carries no alt text (neither
671+ ``alt_text`` nor ``alt_title`` is set) AND is not marked
672+ decorative (``is_decorative`` is False). Returned shapes are
673+ ordered by reading order so callers can iterate top-down.
674+
675+ This is a basic Section 508 / WCAG style check — adding alt text
676+ to every flagged shape, or marking it decorative, brings a slide
677+ to a baseline level of screen-reader friendliness. It does not
678+ cover every accessibility concern (color contrast, font size,
679+ complex tab order, etc.) — treat it as a fast first-pass.
680+ """
681+ issues : list [BaseShape ] = []
682+ for shape in self :
683+ try :
684+ if shape .is_decorative :
685+ continue
686+ if shape .alt_text or shape .alt_title :
687+ continue
688+ except (AttributeError , TypeError ):
689+ # ---accessibility properties live on _BaseShape; if a non-shape
690+ # sneaks into the iter (shouldn't, but guard) it cannot be
691+ # flagged.
692+ continue
693+ issues .append (shape )
694+ return issues
695+
628696 def _add_graphicFrame_containing_table (
629697 self , rows : int , cols : int , x : Length , y : Length , cx : Length , cy : Length
630698 ) -> CT_GraphicalObjectFrame :
0 commit comments