Repository navigation
v3.4.0
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.hasDiagramandGraphicFrame.diagram. A
p:graphicFramewhosea:graphicData/@uriis the DrawingML diagram namespace answered
falseto all three host predicates andnullto 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".Diagramdecodes the
dgm:dataModelpart:points(everydgm:ptin document order, typed
node/asst/doc/pres/parTrans/sibTrans),connections(thedgm:cxnedges that
give the points their tree), andtext(the authored node text, generatedprespoints,
thedocroot and unfilled placeholders excluded). A point'stextFrameis an ordinary
TextFrameover itsdgm:t, so runs, formatting and theresolved*getters all apply.
The layout, quick-style, colours and fallback drawing parts resolve asParts, and the
docpoint'slayoutTypeIdnames the SmartArt kind. Authoring SmartArt remains out of
scope. Seedocs/reference/pptx-read.md. -
GraphicFrame.graphicDataUrireports what an undecoded frame holds. Everyhas*
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 fourfalses. 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 —setDiagnosticHandlerowns that, and the
two reset independently because a host usually installs its handler once. See
docs/diagnostics.md.
Changed
-
Slide.textnow 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.textlengths to detect empty slides, or diffing that
string across versions, a deck containing SmartArt will now report more text. Read
GraphicFrame.diagramdirectly if you need the diagram separated back out. -
A converted script now distinguishes
diagram.allfromgraphicFrame.unknown. SmartArt
used to be reported under the latter, whose stated cause isunread. With a reader in front
of it, the loss is the write leg's:pptxToScriptemitsdiagram.all(unwritable,
dropped), the same shape aschartEx.all.graphicFrame.unknownkeeps 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 acustGeomconnection-site or
adjust-handle angle all threwInfinitystraight 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 anInvalidOptionErrorwith code
coord/non-finiteat 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
Infinityor
NaN(a division by a zero row count is the usual one), guard it at the call site. ANaN
is unchanged on the size options — every caller reads them for truthiness, so aNaNhas
always meant "absent" there. -
A rotation past a full turn now reduces correctly.
convertRotationDegreessubtracted a
single turn, sorotate: 800became 440 degrees rather than 80, androtate: 370became 10
whilerotate: -400was 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
-45and315are validST_Angleand 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'sminAng/maxAng, anangleRangeguide, and the
chart label rotations. Those now go through a non-wrapping converter, so an adjust handle
declared withmaxAng: 540keeps its full travel instead of silently collapsing to 180. -
An empty
chartColorsnow 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 isPIECHART_COLORS. Nothing chose that: normalization defaulted a missing
chartColorsper 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
legendFontSizeis no longer coerced from a string. It was the one font-size
option whose emitter wrapped the value inNumber(), solegendFontSize: '12'from untyped
JS worked there while the same string threw atcatAxisLabelFontSize,dataLabelFontSize
and the eight other spellings of the same option. The coercion is gone; a non-number now
throwsInvalidOptionErrorwith codecoord/non-finitewherever it is passed.Migration: pass a number, which is what the type has always said.
-
A
nullside in a cell's border tuple is now omitted rather than erased. A four-side
TRBL tuple replaced every falsy side with{ type: 'none' }, soborder: [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".
Anullside now leaves its element absent, which is the distinctionnormalizeOuterBorder
already documents for the perimeter ("a sparse side is not{ type: 'none' }"), and the
tuple type acceptsnullso 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
nullto erase an edge should say{ type: 'none' }. -
A table with a
tableStylekeeps 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 aMEDIUM_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.outerBordercomposes 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 figureautoPagewraps on comes from the column's width in points, and
the conversion went through EMU:(colWidth / EMU_PER_POINT) * EMU_PER_INCHis the same 72
ascolWidth * POINTS_PER_INCH, but not exactly, and theMath.flooraround 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 ana:fldis 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*.xmland 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 thanchartColorshas entries used to pick the
overflow colours withMath.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.
genXmlPresetGeomgated
rectRadius,angleRangeandarcThicknessRatioon 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: 0lostadj3while
angleRange: [0, 0]keptadj1/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 atval 0; every non-zero
input is byte-identical. -
TextPropsOptionsdeclares the geometry keys the text-frame emitter already reads.
A styled text frame carries preset geometry through the samegenXmlPresetGeoma shape
does, and it honouredangleRange,arcThicknessRatio,pointsandshapeAdjustat
runtime while the declarations named onlyshapeandrectRadius. SoaddTextwith 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 vocabularyShapePropsalready carries. Declaration-only: no emitted bytes move.