docling-project / docling-project/docling
PPTX chart image rendering deletes charts nested in a group shape
- Dominant language
- Python
- Stars
- 66.4k
- Forks
- 4.8k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 84
Description
### Bug
`MsPowerpointDocumentBackend._isolate_chart_presentation` deletes the chart it is
supposed to isolate when that chart sits inside a group shape. With
`render_chart_images=True`, such a chart gets a blank white image attached instead of
a rendering.
The shape walk in `_walk_linear` recurses into groups, so `_handle_chart` is reached
with the `shape_id` of a *nested* shape:
```python
def handle_groups(shape, parent_slide, slide_ind, doc, slide_size):
if _safe_shape_type(shape) == MSO_SHAPE_TYPE.GROUP:
for groupedshape in self._iter_shapes_by_position(shape.shapes):
handle_shapes(groupedshape, parent_slide, slide_ind, doc, slide_size)
```
`_isolate_chart_presentation` then prunes only the slide's **top-level** shapes:
```python
for shp in list(target_slide.shapes):
if shp.shape_id != chart_shape_id:
shp._element.getparent().remove(shp._element)
```
`target_slide.shapes` never yields the nested chart, so the enclosing `` —
whose own id differs from the chart's — is removed along with everything it contains.
The isolated presentation is an empty slide, LibreOffice renders a blank page, and
`crop_whitespace` returns that page unchanged because `diff.getbbox()` is `None` on a
uniform image. A slide-sized white PNG is attached as the chart picture, with nothing
logged.
The docstring documents a fallback that the code does not implement:
> When the chart is not a top-level shape (e.g. nested in a group) its `shape_id` is
> not found among the slide's shapes, so the slide is left intact and the whole slide
> is rendered instead — a best-effort fallback.
Nothing checks whether the id was found; every non-matching shape is removed
regardless, so an unmatched id empties the slide rather than leaving it intact.
### Steps to reproduce
```python
from pathlib import Path
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
from pptx.util import Inches
from docling.backend.mspowerpoint_backend import MsPowerpointDocumentBackend
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])
group = slide.shapes.add_group_shape()
chart_data = CategoryChartData()
chart_data.categories = ["a", "b", "c"]
chart_data.add_series("s1", (1.0, 2.0, 3.0))
chart = group.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED,
Inches(1), Inches(1), Inches(4), Inches(3),
chart_data,
)
prs.save("grouped_chart.pptx")
backend = object.__new__(MsPowerpointDocumentBackend)
backend.pptx_obj = Presentation("grouped_chart.pptx")
backend._isolate_chart_presentation(0, chart.shape_id, Path("isolated.pptx"))
isolated = Presentation("isolated.pptx")
print("shapes left on the slide:", list(isolated.slides[0].shapes))
```
### Docling version
```
docling 2.126.0
```
### Actual behaviour
```
shapes left on the slide: []
```
The slide is empty; the chart and its group are gone. End to end
(`MsPowerpointBackendOptions(render_chart_images=True)` with LibreOffice installed),
the resulting `PictureItem` carries a blank white image the size of the slide.
### Expected behaviour
```
shapes left on the slide: []
```
The chart survives isolation and is rendered, exactly as a top-level chart is.
### Notes
The enclosing groups have to be kept rather than the chart hoisted to the slide: a
group's `chOff`/`chExt` define the child coordinate space that the nested
`graphicFrame`'s own `xfrm` is expressed in, so re-parenting it to `spTree` would
move or scale the chart. Pruning the siblings at every level of the ancestor chain
keeps the geometry untouched.
Introduced in #3794 / #3809. There is no test covering a grouped chart.
Contributor guide
Assessment
This issue has not been assessed yet.