Skip to content

v3.4.0

Choose a tag to compare

@shbernal shbernal released this 26 Aug 07:22
· 785 commits to main since this release

This release makes SmartArt readable through the read model, and names what a frame the
reader still cannot decode actually holds. It refuses two option values that used to reach
the emitted XML as nonsense — a non-finite size or angle, a stringly-typed
legendFontSize — and gives three others the meaning their absence already carried: an
empty chartColors, a null side in a border tuple, and a table that named a built-in
style only to have the writer's own grid painted over it. It also takes Math.random() out of
the chart palette, so a chart with more series than colours emits the same bytes on every
build.

Added

  • SmartArt is readable: GraphicFrame.hasDiagram and GraphicFrame.diagram. A
    p:graphicFrame whose a:graphicData/@uri is the DrawingML diagram namespace answered
    false to all three host predicates and null to all three accessors, so its content was
    unreachable through the read model, and a consumer could not distinguish "this frame holds
    text I cannot get at" from "this frame holds a chart with no labels". Diagram decodes the
    dgm:dataModel part: points (every dgm:pt in document order, typed
    node/asst/doc/pres/parTrans/sibTrans), connections (the dgm:cxn edges that
    give the points their tree), and text (the authored node text, generated pres points,
    the doc root and unfilled placeholders excluded). A point's textFrame is an ordinary
    TextFrame over its dgm:t, so runs, formatting and the resolved* getters all apply.
    The layout, quick-style, colours and fallback drawing parts resolve as Parts, and the
    doc point's layoutTypeId names the SmartArt kind. Authoring SmartArt remains out of
    scope. See docs/reference/pptx-read.md.

  • GraphicFrame.graphicDataUri reports what an undecoded frame holds. Every has*
    predicate is a comparison against this URI, so a frame that matches none of them is one the
    read model does not decode (an OLE object, an ink annotation). Reading it lets a consumer
    say which construct it could not reach instead of inferring loss from four falses. The
    class comment had described this accessor since the frame reader was written; it had never
    actually been public.

  • resetDiagnosticState() clears the once-per-process warning record. Conditions that
    would flood a log are emitted once per distinct code and message, and that record was
    process-global with no way to clear it. So a service building a second deck got no warning
    for anything the first deck already tripped, which reads as "no problems found" rather than
    as "already mentioned"; and since most of those messages interpolate the offending value,
    the record grew by one entry per distinct bad value with nothing to release them. Call it
    between builds. It does not touch the handler — setDiagnosticHandler owns that, and the
    two reset independently because a host usually installs its handler once. See
    docs/diagnostics.md.

Changed

  • Slide.text now includes SmartArt node text. A slide whose whole message is a diagram
    flattened to the empty string, or to its title alone. Diagram nodes are body text that
    PowerPoint itself searches and spell-checks, so they contribute a block each, in shape-tree
    order, exactly as table cells already did. Chart data labels and speaker notes are still
    excluded, unchanged.

    Migration: if you were summing Slide.text lengths to detect empty slides, or diffing that
    string across versions, a deck containing SmartArt will now report more text. Read
    GraphicFrame.diagram directly if you need the diagram separated back out.

  • A converted script now distinguishes diagram.all from graphicFrame.unknown. SmartArt
    used to be reported under the latter, whose stated cause is unread. With a reader in front
    of it, the loss is the write leg's: pptxToScript emits diagram.all (unwritable,
    dropped), the same shape as chartEx.all. graphicFrame.unknown keeps its meaning and now
    names only genuinely undecoded frames.

  • A non-finite size or angle is now refused rather than serialized. fontSize,
    charSpacing, lineSpacing, paraSpaceBefore/After, rotate, a chart's
    catAxisLabelRotate/valAxisLabelRotate/titleRotate, and a custGeom connection-site or
    adjust-handle angle all threw Infinity straight through their converter and into the
    attribute — sz="Infinity", rot="Infinity" — which is not a legal value and made
    PowerPoint offer to repair the package. Each now throws an InvalidOptionError with code
    coord/non-finite at the point of conversion. A finite out-of-range value is unaffected:
    it still clamps into the schema range and warns, as before.

    Migration: if you were computing one of these from data that can produce Infinity or
    NaN (a division by a zero row count is the usual one), guard it at the call site. A NaN
    is unchanged on the size options — every caller reads them for truthiness, so a NaN has
    always meant "absent" there.

  • A rotation past a full turn now reduces correctly. convertRotationDegrees subtracted a
    single turn, so rotate: 800 became 440 degrees rather than 80, and rotate: 370 became 10
    while rotate: -400 was not reduced at all. It is now % 360, sign-preserving: a rotation
    already within a turn — including a negative one — is byte-for-byte unchanged, since both
    -45 and 315 are valid ST_Angle and the read side reports back what was authored.

    The same function was also being used for angles that are not modular: a custGeom
    connection site, a polar adjust handle's minAng/maxAng, an angleRange guide, and the
    chart label rotations. Those now go through a non-wrapping converter, so an adjust handle
    declared with maxAng: 540 keeps its full travel instead of silently collapsing to 180.

  • An empty chartColors now means the same as omitting it. chartColors: [] used to give
    every chart the bar palette, including a pie or doughnut, whose default when the option is
    omitted is PIECHART_COLORS. Nothing chose that: normalization defaulted a missing
    chartColors per chart type, Array.isArray([]) is true, so an explicit empty array walked
    through that pass untouched and met a later fallback that did not know the chart type. An
    empty array names no colours, which is what omitting the option means, so both now resolve to
    the built-in palette for the chart's own type. A combo subchart's palette resolves the same
    way for the first time — its options never went through that normalization pass at all.

    Migration: if you passed chartColors: [] to a pie or doughnut and wanted the bar palette,
    pass it explicitly. Every other chart type is unaffected: its default already was that
    palette.

  • A chart's legendFontSize is no longer coerced from a string. It was the one font-size
    option whose emitter wrapped the value in Number(), so legendFontSize: '12' from untyped
    JS worked there while the same string threw at catAxisLabelFontSize, dataLabelFontSize
    and the eight other spellings of the same option. The coercion is gone; a non-number now
    throws InvalidOptionError with code coord/non-finite wherever it is passed.

    Migration: pass a number, which is what the type has always said.

  • A null side in a cell's border tuple is now omitted rather than erased. A four-side
    TRBL tuple replaced every falsy side with { type: 'none' }, so border: [solid, null, solid, null] emitted four <a:lnX> elements — two drawn, two carrying an explicit
    <a:noFill/>. On <a:tcPr> those are not the same state: an absent edge inherits from the
    table style, theme banding and the master chain, while a present <a:noFill/> overrides
    that inheritance. So a hole in the tuple meant the opposite of the omission it reads as, and
    there was no spelling left for "draw the edges I name, leave the rest to the style".
    A null side now leaves its element absent, which is the distinction normalizeOuterBorder
    already documents for the perimeter ("a sparse side is not { type: 'none' }"), and the
    tuple type accepts null so the sparse form is authorable from TypeScript. An explicit
    { type: 'none' } still emits <a:noFill/>, so both states stay reachable.

    Migration: a tuple that relied on null to erase an edge should say { type: 'none' }.

  • A table with a tableStyle keeps that style's grid. Every cell with no border authored
    used to receive four explicit <a:lnX><a:noFill/></a:lnX> edges. That is direct formatting,
    so it beat the built-in style the caller had just selected, and a MEDIUM_STYLE_2_ACCENT_1
    table came out with the style's fills and banding but none of its rules — with no way to ask
    for them, since the only spelling for "leave this edge alone" was the one being overwritten.
    The default is now applied only to a table that named no style, which is where it earns its
    keep: PowerPoint's no-style look is a black hairline grid, and suppressing that is what the
    force-fill was for. A styled table emits no <a:tcPr> edges unless you author some, and
    border: { type: 'none' } still erases the grid explicitly. outerBorder composes as it
    always did: on a styled table it now draws the perimeter and leaves the interior rules to
    the style, rather than blanking them.

    Migration: a styled table that relied on the force-fill for a gridless look should author
    border: { type: 'none' }. Unstyled tables are byte-identical.

Fixed

  • Auto-paged tables no longer lose a point of column width to floating-point rounding.
    The chars-per-line figure autoPage wraps on comes from the column's width in points, and
    the conversion went through EMU: (colWidth / EMU_PER_POINT) * EMU_PER_INCH is the same 72
    as colWidth * POINTS_PER_INCH, but not exactly, and the Math.floor around it took the
    hit. Of the 200,000 widths from 0.001in to 200in, 51 came out one point narrow — 6.625in is
    exactly 477 points and was measured as 476.

    The effect is not cosmetic on those widths: a line that fits is wrapped to a second one, so
    every row of the table is measured as twice as tall as it is and the table pages onto more
    slides than it needs. At 6.625in and 15pt a sixteen-row table now fits on one slide where it
    used to split across two. Only those 51 widths change; every other one is byte-identical, and
    no showcase deck reaches one.

  • A slide-number field on a master or layout no longer caches a fake page number.
    <a:t> inside an a:fld is the field's cached rendering, and a master has no slide number
    while a layout carries the library's internal 1000-and-up counter. So every master shipped
    <a:t>null</a:t> and every layout something like <a:t>1004</a:t>. PowerPoint recomputes
    the field on open, so nothing ever looked wrong there — but anything that reads the cache
    without recomputing (a text extractor, a search indexer, this library's own read path) saw
    it. Both now cache the placeholder glyph ‹#›, which is verbatim what PowerPoint itself
    writes: every en-US master and layout in the read fixture corpus has exactly that. A real
    slide still caches its own number.

    This changes emitted bytes for slideMaster*.xml and any layout carrying a slide-number
    placeholder, and for nothing else.

  • Chart colours past the end of the palette now cycle instead of being drawn at random.
    A chart with more series or data points than chartColors has entries used to pick the
    overflow colours with Math.random(), so the same deck built twice emitted different
    bytes and a 17-slice pie was recoloured on every build. All palette lookups now wrap back
    to the start of the palette, which is what the majority of them already did.

    Visible if you author more series or points than your palette covers: those now take the
    palette's own colours in order rather than an arbitrary one. The two built-in palettes
    each held their entries twice over, which only postponed the moment the broken wraparound
    was reached; they now hold each colour once, and a chart past their end repeats them in
    order. Output is unchanged for any chart that stayed within the palette.

  • A zero-valued geometry shortcut now emits its adjustment guide. genXmlPresetGeom gated
    rectRadius, angleRange and arcThicknessRatio on truthiness, so a deliberate zero was
    read as an unset option. addShape('roundRect', { rectRadius: 0 }) emitted no <a:gd> at
    all and PowerPoint fell back to the preset's own ~16.7% rounding, which is the one radius a
    caller asking for zero does not want; arcThicknessRatio: 0 lost adj3 while
    angleRange: [0, 0] kept adj1/adj2, an array being truthy whatever it holds. Each gate
    now asks whether the option was supplied. Radius 0 is a sharp corner, [0, 0] a closed arc
    and thickness 0 a zero-thickness band, and each builds the guide at val 0; every non-zero
    input is byte-identical.

  • TextPropsOptions declares the geometry keys the text-frame emitter already reads.
    A styled text frame carries preset geometry through the same genXmlPresetGeom a shape
    does, and it honoured angleRange, arcThicknessRatio, points and shapeAdjust at
    runtime while the declarations named only shape and rectRadius. So addText with an
    arc's angles built exactly the right <a:gd> guides and TypeScript rejected the identical
    object literal with TS2353 — and any consumer whose option map is derived exhaustively from
    the published types could not compile against the writer's real surface. The four keys now
    reuse the vocabulary ShapeProps already carries. Declaration-only: no emitted bytes move.