Skip to content

v3.5.0

Choose a tag to compare

@shbernal shbernal released this 26 Aug 21:53
· 774 commits to main since this release

This release makes a text edit land where the text is actually read. Paragraph.text and
DiagramPoint.text are settable, and the SmartArt setter writes the fallback drawing cache
that every renderer without a layout engine paints from, not just the data model PowerPoint
reads. The diagram data model gains its tree (Diagram.nodes, Diagram.point()) and a link
from a node to the shape drawn for it. importSlide stops copying chrome the destination
already holds, which takes a duplicate layout and master off every import between decks
templated from one file, and with it off the carried slides ts-pptx/script emits. And two
read surfaces stop losing graphic frames: inspectPptx reports them instead of skipping
them, and a SmartArt slide is copied rather than transcribed into a script with a hole in it.

Added

  • Paragraph.text is settable. Run.text, TextFrame.text, Shape.text and
    TableCell.text could all be assigned; the paragraph in between could not, so replacing
    one bullet of a list meant either collapsing the whole frame or rewriting runs by hand.
    Setting it replaces that paragraph with a single run, keeping the first run's a:rPr and
    the paragraph's own a:pPr (level, alignment, bullet), and leaves sibling paragraphs
    untouched — the same rule the SmartArt drawing-cache mirror already ran on internally.

  • DiagramPoint.text has a setter, and it keeps the drawing cache honest. A SmartArt
    diagram stores every string twice: as authored nodes in ppt/diagrams/data{N}.xml, which
    is what PowerPoint reads, and as a copy of every drawn string in
    ppt/diagrams/drawing{N}.xml, which is what every renderer without a SmartArt layout
    engine paints. Editing a node through textFrame wrote only the first, so the change was
    invisible in LibreOffice, Google Slides, thumbnailers and web previews until someone opened
    the deck in PowerPoint and saved it. The new setter writes both, replacing the one paragraph
    of the drawn shape that belongs to that node and leaving its siblings byte-identical (one
    shape commonly draws three nodes at once). Geometry is not recomputed, which is inherent
    rather than a defect: the drawn shape keeps its cached size, so a much longer string
    overflows its box in those renderers until PowerPoint re-lays the diagram out.

    textFrame still edits the data model alone and now says so; it is the escape hatch for
    run-level formatting. See docs/reference/pptx-read.md.

  • Diagram.nodes gives the data model its tree, and Diagram.point() resolves an id.
    connections hands back the raw parOf edges, so every consumer that wanted "the top-level
    nodes, in order" wrote the same index-and-walk, and a hierarchy read as a flat list with no
    indication of depth. A DiagramNode carries its point, its children (in @srcOrd
    order), its parent and its level. Assistant (asst) points are in the tree and keep
    their own type; transition points are not, because they label an edge rather than a node and
    are reached through DiagramConnection.parentTransitionId / siblingTransitionId. A
    parOf cycle raises diagram/parent-edge-cycle rather than hanging or silently dropping
    the points it swallows.

  • DiagramPoint.drawnShape and DiagramPoint.presentationId link a node to what is drawn
    for it.
    drawnShape resolves the dsp:sp of the fallback drawing part that carries the
    point's text, plus the paragraph index inside it, as a TextFrame bound to the drawing
    part. The mapping is not the obvious one and is many-to-one in both directions: a
    dsp:sp/@modelId is always a generated pres point's id rather than the authored point's,
    one shape draws several points at paragraph indices that can contradict document order, and
    one point has several presentations of which at most one holds text. null is a defined
    outcome, covering a package with no drawing part, a point with no presOf edge (every
    unlabelled transition point), a pres point that draws nothing, and a drawn shape with no
    text body. Measured over five layout families and 90 authored points.

Changed

  • importSlide binds to chrome the destination already holds instead of copying it in
    again.
    copyPart walked the source slide's slideLayout -> slideMaster -> theme
    subgraph and copied every part of it under a fresh partname, with no notion of what the
    destination arrived with. For the deck templated from its own source (fromTemplate keeps
    a package's chrome byte-identical and strips only its slides, which is exactly what
    ts-pptx/script's template-anchored tier emits) every one of those parts was already there
    under its own partname, so each imported slide grew the deck a duplicate layout and master:
    one extra entry in PowerPoint's layout picker per slide, and a later
    appendSlides({ layout: <name> }) that threw layout/ambiguous-name because two layouts
    answered to the name.

    A part is now reused when the destination holds it at the same partname with the same
    content type, the same bytes as they would be written, the same relationships, and the same
    test passing recursively; a master must also be registered in p:sldMasterIdLst and a
    layout must sit in a registered master's gallery. The decision is made at the page boundary
    and never below it, so a part reached from something this import copied is copied too and
    copied chrome stays self-contained. Anything short of identical still copies.

    Migration. Importing between two decks that share byte-identical chrome (the same file
    opened twice, or two decks made from one template) now adds fewer parts: no new layout,
    master or theme, and the imported page resolves to the destination's own. Rendering is
    unchanged, since reuse only happens where the bytes were the same. A consumer asserting on
    part counts after such an import sees the smaller numbers.

  • A converted script's carried slide no longer costs a layout, and batches bind by name
    again.
    ts-pptx/script's template-anchored tier copies a slide the write API cannot
    author (slide.carried) with importSlide, out of the very file it templated the deck
    from. That import used to duplicate the slide's layout and master, and because the duplicate
    repeated a layout name, it demoted every appendSlides binding in the emitted script from
    the name to a gallery position. With importSlide reusing chrome the destination already
    holds, layout names stay unambiguous and bindings print as { layout: 'Titelfolie' }. A
    multi-master deck that genuinely repeats a layout name still falls back to a position, which
    is the case the fallback was always for.

  • inspectPptx reports graphic frames. A p:graphicFrame (table, chart, SmartArt, or a
    payload this library does not model) was skipped outright and consumed no zIndex, so a
    deck whose slides are SmartArt or tables inspected as elements: [] with wordCount: 0,
    indistinguishable from a deck of blank slides. It also put the two read surfaces in flat
    disagreement: Slide.text on the deep model has always flattened table cells and SmartArt
    node text.

    A frame now comes back as one element with kind: 'graphicFrame', its own box, a zIndex
    in the shape-tree walk, and a new graphicKind field ('table' | 'chart' | 'chartEx' | 'diagram' | 'other'). Its text is what a reader sees on the slide, so it counts toward
    the slide's text and wordCount; a chart contributes none, matching Slide.text, which
    treats data labels as chart data rather than slide body text. The structure is still not
    flattened: textRuns and paragraphs stay empty, and cells, series, and nodes are reached
    through ts-pptx/read.

    Migration. PptxSlideElementKind gains 'graphicFrame', so an exhaustive switch over
    it must handle the new member; PptxSlideElement gains graphicKind, null on every other
    kind. A consumer that counted elements, summed wordCount, or read zIndex values sees
    different numbers on decks containing graphic frames. Filter on kind !== 'graphicFrame' to
    get the old element set back (the old zIndex numbering is not recoverable, and was itself
    a walk with holes in it).

Fixed

  • A slide holding SmartArt is no longer emitted as a script with a hole in it.
    ts-pptx/script decides per slide whether the emitted script can describe it or whether the
    printer must copy the source slide verbatim, and that test named extended charts and nothing
    else. But three graphic-frame payloads produce no call, not one: an extended chart, a
    SmartArt diagram, and a frame the reader does not decode at all (an OLE object, ink, a 3-D
    model). So a chartEx slide round-tripped correctly by being copied while a SmartArt slide
    was transcribed and silently lost its diagram, with only a per-shape fidelity note recording
    the loss. Both sites now share one predicate, so they cannot drift apart again.

    The copy costs nothing in the layout gallery, because importSlide now binds to chrome the
    destination already holds (see above) and the template-anchored tier copies out of the very
    file it templated the deck from. Layout handles are still resolved up front rather than
    where first used, since an importSlide between two appendSlides calls moves the gallery
    underneath them.

    Migration: a deck whose SmartArt, OLE or ink slides previously came out authored now comes
    out carried. The emitted script imports those slides from the template instead of
    rebuilding them, which needs the source deck present (it already did for the template
    itself). The standalone tier is unchanged: it has no source package to copy from, so it
    still transcribes such a slide and still reports the per-shape note.