Recursive AI

SmartArt

All onedocument docs

How OneDocument paints SmartArt diagrams in Word, PowerPoint and Excel drawings. Namespace RecursiveAI.OneDocument.Rendering.SmartArt (src/OneDocument.Rendering/SmartArt/). A diagram is a graphic frame (dgm:relIds) that points at four parts, plus an optional fifth that Office stores as a cache:

PartRootRelationshipWhat it holds
data modeldgm:dataModel…/diagramData (r:dm)the text tree: points (nodes, transitions) and parent-of connections
layout definitiondgm:layoutDef…/diagramLayout (r:lo)the algorithms, constraints and rules that place the nodes
quick styledgm:styleDef…/diagramQuickStyle (r:qs)per style label: line, fill, effect and font references
colour transformdgm:colorsDef…/diagramColors (r:cs)per style label: the colour lists
stored drawingdsp:drawing…/diagramDrawing (the data model's dsp:dataModelExt/@relId)the shapes Office laid out, as ordinary DrawingML

ECMA-376 Part 1 §21.4 defines the first four; the stored drawing is Microsoft's extension ([MS-ODRAWXML] §2.3.1, dsp:drawing, dsp:sp, dsp:txXfrm, dsp:dataModelExt). Nothing is built in: layout, colours and style are always read from the file's own parts.

Which path paints

DiagramRegeneration (DiagramOptions.Regeneration):

  • WhenMissingOrStale (default): paint the stored drawing. Run the layout instead when the diagram has no readable stored drawing (missing), or when the text of a node in the data model does not appear in any shape of the stored drawing (stale: the data was edited without Office refreshing the drawing). A stale or missing drawing whose layout cannot be read paints the stale drawing if there is one, else nothing, and says so.
  • Always: run the layout and ignore the stored drawing (forced). This is the path after an edit through OneDocument.
  • Never: only the stored drawing; nothing is painted without one (regeneration-off).

Both paths end in the same code: the layout engine writes its shapes as a dsp:drawing part in memory (DiagramDrawingWriter), and DrawingPainter.DrawShapes paints that exactly as it paints a stored one (or a slide's p:spTree). A stored drawing and a regenerated drawing with the same shapes therefore give the same display list. DiagramOptions.Report receives a DiagramReport per frame: source (Cached, Regenerated, None), shape count and the reason.

Notes on DrawingContext.Notes (the file is fine, the renderer approximated): smartart.data-unreadable, smartart.layout-unreadable, smartart.not-painted, smartart.layout-truncated, drawing.diagram-not-painted, and drawing.graphic-frame when no painter is installed.

Hooking a host

The drawing painter calls DrawingContext.DiagramPainter for every dgm:relIds frame it meets, in a Word drawing (w:drawing → a:graphic), a slide or worksheet graphicFrame, or a group. A host installs one function:

C#
ctx.DiagramPainter = DiagramHost.CreatePainter(package, sourcePartName, options);

sourcePartName is the part that holds the frame (the relationship ids of dgm:relIds and of the data model's relId are relative to it). The Word renderer installs it for the document's main part (LayoutPainter through PaintOptions.Diagrams); the slide and worksheet renderers install it for the slide or drawing part they paint. Frames in headers, footers and notes use the part they sit in, which the Word renderer does not do yet.

The frame's box comes from the caller (the frame's xfrm in a slide, the layout's box in Word). The stored drawing's coordinates are relative to the frame's top-left corner and are not scaled; a regenerated drawing is laid out for the box's size.

Tagging: a frame is a Figure tag with the frame's alternative text (wp:docPr/@descr, p:cNvPr/@descr); the Word path gets its Figure from DrawingPainter.Draw, the others from the diagram painter (DiagramPaintRequest.InFigure). The text of every shape is tagged as paragraphs inside it, in drawing order (which is the data model's reading order).

Other entry points on DiagramHost: Find(package) lists the frames of a package with their stated size; Generate(...) runs the layout without painting and returns the shapes (DiagramShape: geometry, rectangle in points, rotation, text, font size, style label), the dsp:drawing bytes and an Outline of the layout nodes; ReadStored(...) reads the stored drawing's shapes.

The data model

DiagramData reads points and connections (§21.4.3). The tree is the parOf connections ordered by srcOrd; each connection owns a parent transition and a sibling transition (parTransId, sibTransId). Presentation points (pres) and presOf/presParOf connections are what a layout run produces; they are read only for their user formatting (below) and are never written. Limits: 20,000 points, 5,000 layout nodes, depth 64; a larger diagram is refused as unreadable or truncated with a note.

The axes of §21.4.2 (self, ch, des, desOrSelf, par, ancst, ancstOrSelf, followSib, precedSib, follow, preced, root) walk this tree. The sequence of a node's children is, for each connection, its parent transition, the node and the sibling transition that leads to the next one; ptType filters it, st/step/cnt select from it, and hideLastTrans (default true) drops the sibling transition after the last child. Iterator attributes are lists (axis="ch desOrSelf" applies the axes in turn). choose/if evaluate cnt, pos, revPos, posEven, posOdd, depth, maxDepth and var (dir, orgChart, chMax, chPref, bulEnabled, hierBranch, animOne, animLvl, resizeHandles); a variable takes the data model's root point value (presLayoutVars, which stores the user's choices, for instance dir="rev"), else the nearest layout node's varLst, else the default. forEach/@ref makes a layout recurse; recursion is bounded by the data and the node cap.

Layout

Each layout node is made for one point. A layout node's constrLst sets properties of itself, its children or its descendants (for, forName, ptType), to a literal (val; lengths are millimetres, font sizes points) or to a property of a referenced node times fact. op="equ" makes the targets share one value; gte/lte clamp. A constraint with neither value nor reference only names a property that a rule would grow, and states no size.

An algorithm places a node's children inside its rectangle. When the children's preferred sizes do not fit, they and the spacing shrink together by one factor (never above 1):

AlgorithmBehaviour here
lina row or column (linDir), in order or reversed; group aligned by horzAlign/vertAlign, each child across the line by nodeVertAlign/nodeHorzAlign; spacing from spacer/connector children, else the node's sp
snakerows (or columns, flowDir) that wrap; the row length that keeps the children largest, ties to more per row; each short row centred; grDir mirrors, contDir="revDir" reverses alternate rows
cyclechildren on a circle or arc (stAng, spanAng degrees clockwise, first at the top), scaled down until the gap between neighbours can hold an arrow (45 % of the smaller node side); rotPath="alongPath" rotates the nodes
pyralevels from the apex: a triangle, then trapezoids that continue its sides, each level height/n tall (linDir="fromB" reverses)
compositeevery child by its own l, t, w, h, r, b, ctrX, ctrY or diam
hierRoot, hierChilda root centred over its sub-trees; sub-trees side by side with sibSp (else a quarter of the root width) between them and sp (else half the root height) below the root; scaled to the frame
conna connector between the layout nodes that present the two ends of its transition: dim 2D is a block arrow (a rightArrow in the node's box, or between the two nodes when the algorithm gave it none), 1D a line routed stra, bend or curve with begPts/endPts sides and begSty/endSty arrowheads
spspace only
txthe node's text

A sibling transition after the last node leads back to the first (a cycle closes) when hideLastTrans="0".

Not done: rotPath/ctrShpMap beyond the above, autoTxRot, the secondary-child parameters of the hierarchy algorithms, chMax/chPref (they do not limit the children), accent-text positions of pyra, moveWith, chOrder, 3-D, and the custScale…/custLinFact… customisations stored on presentation points. Rules (ruleLst) are read, but only primFontSz acts (as the font-size floor); the growth rules (type="h"/"w" with val="INF") are not run, so layouts that rely on text growing a box (several built-in list layouts) come out with other proportions than Office draws.

Text

Shape text is DrawingML, so it does not go through the Word paragraph formatter. DiagramText formats runs (size, weight, style, colour, font), paragraphs (alignment, margins, hanging bullet, line spacing, space before and after) and wraps at spaces and between East Asian characters; a word wider than the line is split inside and reported to the fit. It paints through ShapeTextRequest, so the text rectangle is the shape's (or dsp:txXfrm when present) less its insets, anchored by anchor; text taller than the rectangle overflows evenly. Run colour is the run's own, else the shape style's fontRef colour. Right-to-left text is shaped left to right for now.

Font fitting: the layout's primFontSz constraint is the largest size (default 18 pt when absent) and the rule primFontSz val the smallest (default 5 pt). The size is the largest whole point at which every paragraph fits the text rectangle (the preset's text rectangle less lMarg/tMarg/rMarg/bMarg, where margins that reference primFontSz scale with the candidate size), and nodes bound by one op="equ" constraint take the smallest size any of them needs. A node with child text (presOf desOrSelf with bullets enabled) shows its own text at the primary size and its descendants' at 80 % of it, one level in, with a hanging bullet. Text paragraphs are written as Office writes them: 90 % line spacing, 35 % space after, kern 1200.

Colours and style

The style label of a layout node (styleLbl; node1 for text shapes, sibTrans2D1 for block arrows, parChTrans1D1 for lines when the node names none) selects the entry of the colour transform and of the quick style. The i-th of n shapes with one label takes the i-th of n colours: repeat and cycle walk the list round and round, span blends neighbours in HSL along hueDir (the standard gives no formula for the blend). A presentation point stored in the data model with the same presAssocID and presName overrides the label, the colour index and count, and the shape's fill and line. The quick style supplies the lnRef, fillRef, effectRef and fontRef of dsp:style; fills and lines are also written explicitly, as Office does.

Differences from Office

  • Office's algorithms are specified only in outline. Row length in snake, ring scale in cycle, hierarchy spacing, the span blend and the text margins' unit (points) are this engine's reading of the standard; the layout differential decides them.
  • Rules other than the font floor do not act (above), so the proportions of layouts that depend on them differ.
  • Measured against the drawings Office stored with 29 diagrams in public sample files (built-in layouts such as list, process, hierarchy and organisation chart; tools/SmartArtProbe compare): the engine makes a shape for 123 of their 135 text shapes, reproduces the stored drawing of the block-list layout almost exactly, and puts 2 of 27 frames within 5 % of Office's positions and 0.8 to 1.25 times its sizes. The built-in layouts lean on growth rules and on constructs this engine does not run (assistants, secondary children, INF rules), so regeneration of those is a fallback for missing or stale drawings, not a substitute for Office's own layout.
  • A diagram painted in a header, footer or note reads its parts through the main part's relationships.
  • Right-to-left and vertical text in diagram shapes is laid out left to right and horizontal.

Checks

tests/OneDocument.Rendering.Tests/SmartArtTests.cs builds diagrams with SmartArtSamples.cs (data models, a layout definition per family, a colour transform and a quick style, all written for these tests) and checks, for list, process, cycle, hierarchy, pyramid and composite layouts: shape counts, reading order, arrangement (positions, equal sizes, aspect, centring, ring radius, parents over children), fit inside the frame, text fitting (one size for all nodes, the largest that fits), the dir variable, the stale test, the three regeneration modes, damaged parts, slide and Word hooks, and that a stored drawing paints the same pixels as the same plain shapes. testdata/smartart holds the generated documents (Word and PowerPoint, each with and without a stored drawing) as inputs for the layout differential (tools/layout-differential.sh); they have not been compared with Office yet. tools/SmartArtProbe finds every diagram under a set of directories and paints each as stored and with the layout forced (sweep), compares the engine's shapes with the stored ones (compare) and prints both (dump). fuzz/OneDocument.Fuzz/SmartArtTarget.cs is the fuzz target.