How OneDocument prints a workbook the way Excel does: which cells land on which page, how a cell's text is formed and placed, and what
is painted. Package RecursiveAI.OneDocument.Rendering, namespace RecursiveAI.OneDocument.Rendering.Spreadsheets. References are to
ECMA-376 Part 1 unless noted. The page layer is shared with layout.md (display list in CSS pixels, tagged paint, fonts and substitution
notes). Rules the specification leaves open are listed at the end as points to settle against Excel's own PDF export.
The pipeline
WorkbookRenderer.Layout(workbook, SheetRenderOptions?) returns a SheetLayoutDocument (the pages in print order, font and layout notes).
WorkbookRenderer.Paint(layout, page) returns the page's display list; WritePdf and RenderPng write it. `Office.Convert("in.xlsx",
"out.pdf")andworkbook.SaveAs("out.pdf")go through the same path. A PNG written throughSaveAs` is page 1.
Rendering reads the cached cell values and never recalculates. Nothing in the workbook changes. The hub gained three additive read-only
members for it: Worksheet.LayoutMarkup (the sheet part without its cells), Worksheet.TryGetSharedStringIndex with
SharedStringTable.GetItemXml (rich-text runs), and Workbook.EvaluateFormula (one formula on the stored values, for conditional formats).
One format handler per output format serves every kind that renders: RenderingFormats.Register dispatches on the file (document or
workbook), because the registry keeps one writer per format.
What prints
- Sheets: every visible worksheet in tab order. Hidden and very hidden sheets print only with
IncludeHiddenSheets. A chart or macro sheet is skipped withsheet-layout.sheet-kind-skipped. A sheet with nothing on it prints nothing; a workbook with nothing to print writes one blank Letter page (sheet-layout.empty). - Range:
_xlnm.Print_Area(several areas print one after another, each paginated by itself), else the used range from A1: the last cell with a value, or with a visible fill or border, merged ranges, and the cell after the last drawing anchor. A whole-row or whole-column area stops at the used cells. - Titles:
_xlnm.Print_Titlesrows and columns repeat on every page that starts below (or right of) them. The pages that contain them print them in place. - Page setup (
SheetPrintSettings.Read):paperSize(the ECMA-376 §18.3.1.51 list;paperWidth/paperHeightoverride),orientation,scale(10–400),fitToPagewithfitToWidth/fitToHeight(0 = as many as needed),firstPageNumberwithuseFirstPageNumber,pageOrder,errors,blackAndWhite;pageMarginswith header and footer distances;printOptionscentring, gridlines, headings;sheetFormatPr; manualrowBreaksandcolBreaks.
Pagination
Sizes are points before scaling. Available width = (paper − margins) / scale − heading width; likewise height.
- Column and row sizes. The default font's maximum digit width is the widest digit advance at 96 dpi rounded to whole pixels (7 for
Calibri 11). A stored width w is
trunc(((256·w + trunc(128/MDW)) / 256) · MDW)pixels; 1 px = 0.75 pt. WithoutdefaultColWidththe default column isceil((baseColWidth·MDW + 5) / 8) · 8px (64 px for the Calibri default). Row heights come fromht; a row without one takes the default row height, or the height its tallest cell needs (font line height, times the lines of wrapped text, rotated text by its rotated extent). The default row is the font's ascent, descent and line gap each rounded to pixels plus 2 px (20 px, 15 pt for Calibri 11). Hidden rows and columns have size 0 and take no room. A page that would hold only hidden lines is dropped. - Scale.
scale / 100, or with fit-to-page the largest whole percent (at most 100, at least 10) at which every area fits the requested pages across and down. Manual breaks are ignored with fit-to-page. - Breaks. Lines are packed until the next one would not fit (a page always holds at least one). A manual break
idis the last row (column) of its page (§18.3.1.1). Titles take room on the pages that repeat them. - Order and numbers. Down-then-over or over-then-down. Page numbers run on through the workbook; a sheet with
useFirstPageNumberrestarts at itsfirstPageNumber.&Nis the larger of the page count and the highest number. - Position. The range sits at the left and top margins, or centred between them. Headings, when printed, add a heading column and row (digits of the last row number plus padding wide; one default row high) inside the scaled content.
Cells
- Text. Numbers go through the shared number-format service (
NumberFormat.TryFormat): the section colour overrides the font colour,*cfill is expanded to the cell width,_xspacing comes from the service. General fits the number to the column by trying fewer digits and scientific form, down to a single digit. A number that still does not fit, or any other format that is too wide, prints#repeated to fill the width (####). Text with a text format goes through the format's text section. Booleans printTRUEorFALSE; errors print as stored, or blank,--,#N/AperpageSetup/@errors. Rich text (shared string runs, §18.4) keeps each run's font, size, bold, italic, underline, strike, colour and super/subscript. - Shaping and wrapping use OneFont: clusters shaped per run and bidi level, line breaks at UAX #14 opportunities (a word wider than the line breaks between characters), the visual order of each line by UAX #9 rule L2. Left-to-right text in a right-to-left reading order and right-to-left cells align right by default.
- Alignment. General: text left, numbers right, booleans and errors centred. Left, centre, right, fill (the text repeated across the width), justify and distributed (spread over the spaces of wrapped lines; the last justified line stays left), centre-across-selection (over the empty cells to the right with the same setting). Indent is MDW·0.75 pt per level. Vertical: bottom (default), top, centre, justify and distributed (as top). Cell padding is 2 px left and right, 1 px top and bottom.
- Wrap, shrink, rotation. Wrap wraps at the cell width less padding and indent. Shrink-to-fit scales the font size so one line fits.
textRotation1–90 is counter-clockwise, 91–180 clockwise (value − 90), 255 stacks the characters; rotated text is placed by the bounding box of the rotated block and clipped to the cell. Rotated 90° text wraps at the cell height. - Overflow. Unwrapped, non-numeric text that is wider than its cell spills into empty neighbouring cells (no value, not merged): to the right when left-aligned, to the left when right-aligned, both ways when centred. It stops at the first occupied cell and at the page. Text that cannot spill is clipped to its cell.
- Merged cells draw once, from the first of their cells on the page, over the whole range (clipped by the page). Only the range's outer edges carry borders.
- Fills. Solid colours; a pattern fill is its average colour (pattern colour over background at the pattern's ink density); gradient
fills are a linear gradient at
degree(a path gradient is its last stop colour). A differential fill's colour is itsbgColorwhen the pattern is solid or absent, as Excel writes it. - Borders. All 13 styles: widths 1, 2, 3 px for thin/medium/thick, hairline half a pixel, double as two 1 px lines with a 1 px gap, dash patterns in units of the line width (dotted 1:1, dashed 3:1, dash-dot 3:1:1:1, dash-dot-dot 3:1:1:1:1:1). Where two cells share an edge the heavier line wins (order: hair, dotted, dash-dot-dot, dash-dot, dashed, thin, then the medium variants, medium, double, thick); at equal weight the cell to the right or below wins. Diagonals run corner to corner of the cell or merged range.
- Gridlines (
printOptions/@gridLines) are 0.5 pt grey lines under fills and borders. Headings are grey boxes with the row numbers and column letters, drawn as artifacts. - Black and white (
pageSetup/@blackAndWhite) paints every colour as its grey level.
Conditional formats
Evaluated on stored values through Workbook.EvaluateFormula (relative references move from the top-left cell of the first range), in
priority order, honouring stopIfTrue; for properties a higher-priority rule has set, a lower one does not change them.
| Rule | Rendering |
|---|---|
cellIs, expression, containsText, notContainsText, beginsWith, endsWith, blanks, errors | the rule's dxf: font bold, italic, strike, underline, colour; fill; borders; number format |
top10, aboveAverage (incl. stdDev, equalAverage), duplicateValues, uniqueValues | as above, from the statistics of the rule's cells (ties with the k-th value are included) |
timePeriod | as above, against SheetRenderOptions.Clock (weeks run Sunday to Saturday) |
colorScale | 2 or 3 colours interpolated in RGB between the points (min, max, num, percent, percentile, formula) |
dataBar | a bar from minLength to maxLength percent of the cell width (10–90 for 2007-style bars, 0–100 with the x14 extension), gradient fading to a light tint at the tip unless gradient="0". With the x14 extension and negative values: an axis where zero falls, positive bars to the right, negative bars (default red, or the positive colour) to the left, optional border |
iconSet | an icon at the left of the cell, drawn as filled paths |
Icon shapes. Each set lists icons from the lowest level to the highest: arrows (up, down, right, and 45° up-right and down-right) in red,
yellow, green or grey; flags; traffic lights (circles, or lights in a dark rounded box); signs (red diamond, yellow warning triangle, green
circle); symbols (cross, exclamation, check, in a coloured disc, or bare in the …Symbols2 set); stars (empty, half, full); triangles and
a dash; ratings (ascending bars, filled up to the level); quarters (a disc with 0–4 quarters filled); boxes (a square filled from the
bottom in quarters); 4RedToBlack and 4TrafficLights discs. Colours are approximations of Excel's: red E0443E, yellow F5B623,
green 57A84A, grey 8C8C8C. Level i is the highest whose threshold the value reaches (the first point is the start of level 0).
showValue="0" hides the cell's text. reverse flips the levels.
Headers and footers
headerFooter strings with &L, &C, &R sections; &P, &N (with +n/-n), &D, &T, &F, &Z, &A; &G (a note
sheet-layout.header-picture and nothing drawn); &"font,style", &nn, &K colour, &B &I &U &E &S &X &Y toggles, &&. Odd, even and
first-page strings follow differentOddEven and differentFirst. The font is the workbook's default font; text scales with the page when
scaleWithDoc is on (the default). The header starts at the header margin and grows down; the footer ends at the footer margin and grows
up. Date and time come from SheetRenderOptions.Clock; &F is the workbook's file name (or FileName).
Tagging
Each page's text is one Table; a row is a TableRow, a cell a TableDataCell (a TableHeaderCell in the print-title rows and in the
header rows of tableParts tables), with column and row spans for merges. Rows and columns repeated from an earlier page, headings,
headers and footers, fills, borders, gridlines, data bars and icons are artifacts. Only cells that print text are tagged.
Drawing objects
Anchors in the sheet's drawing part (twoCellAnchor, oneCellAnchor) are resolved to boxes in page points (SheetPage.Objects: kind picture,
chart, shape, group, connector; name; X, Y, Width, Height after scaling; the anchor element and its drawing part). With
SheetRenderOptions.PaintDrawings (the default) each object is painted after the cells through the shared DrawingML painter
(drawingml-rendering.md): pictures (bitmaps and metafiles) from the drawing part's relationships, preset and custom shapes, groups, and charts
through ChartHost (charts.md) from their cached values. Objects are cut at the edge of the page's printed cells, so one that spans a page
break shows on both pages. Text inside shapes is not painted yet; absolute anchors are not placed. SheetRenderOptions.ObjectPainter is called for
each object after that with the page's display list (pixels, 1 pt = 4/3 px, no transform active) and the box in pixels, and ObjectPlaceholders
draws a thin outline where an object sits. An object the painter cannot draw is noted (sheet-layout.object-not-painted).
Registering a kind for PDF and PNG output
RenderingFormats.Register installs one writer per format that tries each kind's TryWrite(file, format, destination, cancellationToken) in turn
(DocumentRenderer.TryWrite, WorkbookRenderer.TryWrite); a TryWrite returns false, writing nothing, when the file is not of its kind. A new kind
writes its own TryWrite and adds it to the Kinds array in RenderingFormats: one line.
Notes
SheetLayoutDocument.Notes (SheetRenderNote): sheet-layout.sheet-kind-skipped, sheet-layout.empty, sheet-layout.page-limit,
sheet-layout.font-missing, sheet-layout.header-picture, sheet-layout.drawing-unreadable, sheet-layout.conditional-limit
(past 3 million cells evaluated), sheet-layout.object-not-painted. FontNotes lists every replaced family as for documents.
Points to settle against Excel
These are choices where the specification is silent; the generated x* workbooks in the layout differential carry each one, and Excel's
own PDF decides.
| Point | What is done |
|---|---|
Default row height formula (round(asc)+round(desc)+round(gap)+2 px) | matches Calibri 11 (20 px) and Arial 10 (17 px) from memory; other fonts unchecked |
| Page numbers across sheets | continue (Auto) unless useFirstPageNumber |
| Title rows on the first page when the area starts below them | repeated |
| Font codes in a header string carry over into later sections | yes (a stream) |
Header and footer left edge with alignWithMargins="0" | min(margin, 0.5 inch) |
| Printed gridline colour and weight | 0.5 pt, 808080 |
| Border conflict at equal weight | the right/lower cell wins |
| Indent unit | MDW · 0.75 pt per level |
| Pattern fills | average colour |
| Icon colours and shapes | as above |
| Data bar negative axis position and length rule | zero's position between min and max; max length applies to the longer side |
| Chart sheets, absolute anchors, text in shapes | not printed / not placed / not painted |