Skip to content

Commit b1d9516

Browse files
committed
Support tables in DOCX renderer (part 2)
1 parent 385dca9 commit b1d9516

File tree

9 files changed

+187
-157
lines changed

9 files changed

+187
-157
lines changed

documentation/Supported_features.MD

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ However, some charts may already be OLE objects (Excel/MS Graph object; usually
101101
⌛ Work in progress
102102

103103
- ✅ Character formatting
104-
- Not supported: small caps, text effects, font spacing, horizontal font scaling, borders around text runs, dash-dot/dash-dot-dot/double wavy underline types
104+
- Not supported: small caps, text effects, letters spacing, horizontal font scaling, FitText, borders around text runs, dash-dot/dash-dot-dot/double wavy underline types
105105
- ✅ Paragraph formatting
106-
- Not supported: hanging indentation (negative first line indentation, blocked by lack of support in QuestPDF), paragraph borders, KeepNext, some advanced position properties
106+
- Not supported: hanging indentation (negative first line indentation, blocked by lack of support in QuestPDF), custom tab stops, some options such as KeepNext, some advanced text wrapping / position properties
107107
- Limitation: the paragraph border (if present) has the same color for top/bottom/left/right due to a limitation in the QuestPDF API
108108
- ✅ Section properties
109109
- Supported: page size, orientation, margins, background color color, multi-column layout
@@ -118,8 +118,8 @@ However, some charts may already be OLE objects (Excel/MS Graph object; usually
118118
- ⌛ Pictures
119119
- ⌛ Math formulas
120120
- ✅ Tables
121-
- Not supported: external spacing between cells; cell border styles (double, dashed, shadow, ...); diagonal internal borders; fit text, no wrap and other properties; vertical text
122-
- Limitation: the border color is the same for top/bottom/left/right of each cell due to a limitation in the QuestPDF API
121+
- Not supported: external spacing between cells; cell border styles (double, dashed, shadow, ...); diagonal internal borders; vertical text; some options such as CantSplit, NoWrap, TableCellFitText; some advanced text wrapping / position properties; some types of cell/row width (percentage, or TableLayout set to Auto); space before/after single row (different from table)
122+
- Limitations: the border color is the same for top/bottom/left/right of each cell due to a limitation in the QuestPDF API
123123
- ✅ Fields, page numbers, table of contents
124124
- Limitations:
125125
- Field values are not updated in some cases, we need to re-calculate at least page numbers
@@ -131,7 +131,6 @@ However, some charts may already be OLE objects (Excel/MS Graph object; usually
131131
- footnotes are always rendered at page bottom, which is the default in DOCX, but can be changed to *below text* or *end of section*. This is currently ignored by the renderer, while the setting *end of section*/*end of document* for endnotes is implemented.
132132
- footnotes and endnotes IDs are always rendered as arabic numbers and roman numbers respectively. The number format can be customized in DOCX, but is not implemented yet in the renderer.
133133

134-
135134
## XLSX to QuestPDF renderer
136135

137136
⌛ Work in progress

src/DocSharp.Docx/DocxToHtml/DocxToHtmlConverter.Table.cs

Lines changed: 6 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ internal void ProcessTableRow(TableRow row, HtmlTextWriter sb, int rowNumber, in
170170
//var tj = row.GetEffectiveProperty<TableJustification>();
171171
// Not supported for individual rows in HTML
172172

173-
ProcessTableWidthType(row.GetEffectiveProperty<WidthBeforeTableRow>(), ref rowStyles, "margin-top");
174-
ProcessTableWidthType(row.GetEffectiveProperty<WidthAfterTableRow>(), ref rowStyles, "margin-bottom");
173+
ProcessTableWidthType(row.GetEffectiveProperty<WidthBeforeTableRow>(), ref rowStyles, "margin-left");
174+
ProcessTableWidthType(row.GetEffectiveProperty<WidthAfterTableRow>(), ref rowStyles, "margin-right");
175175

176176
ProcessTableWidthType(row.GetEffectiveMargin<TopMargin>(), ref defaultCellStyles, "padding-top");
177177
ProcessTableWidthType(row.GetEffectiveMargin<BottomMargin>(), ref defaultCellStyles, "padding-bottom");
@@ -229,47 +229,10 @@ internal bool ProcessTableCellProperties(TableCell cell, ref List<string> cellSt
229229
bool isFirstColumn = columnNumber == 1;
230230
bool isLastRow = rowNumber == rowCount;
231231
bool isLastColumn = columnNumber == columnCount;
232-
rowSpan = 1;
233-
columnSpan = 1;
234-
var vMerge = cell.TableCellProperties?.VerticalMerge;
235-
if (vMerge != null)
236-
{
237-
if (vMerge.Val != null && vMerge.Val == MergedCellValues.Restart)
238-
{
239-
rowSpan = CalculateRowSpan(cell);
240-
}
241-
else
242-
{
243-
// If the val attribute is omitted, its value should be assumed as "continue"
244-
// (https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.verticalmerge.val)
245-
// Don't generate a new <td> in this case.
246-
return false;
247-
}
248-
}
249-
250-
var gridSpan = cell.TableCellProperties?.GridSpan;
251-
if (gridSpan?.Val != null)
252-
{
253-
columnSpan = gridSpan.Val.Value;
254-
}
255-
else
256-
{
257-
var hMerge = cell.TableCellProperties?.HorizontalMerge;
258-
if (hMerge != null)
259-
{
260-
if (hMerge?.Val != null && hMerge.Val == MergedCellValues.Restart)
261-
{
262-
columnSpan = CalculateColumnSpan(cell);
263-
}
264-
else
265-
{
266-
// If the val attribute is omitted, its value should be assumed as "continue"
267-
// (https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.horizontalmerge.val)
268-
// Don't generate a new <td> in this case.
269-
return false;
270-
}
271-
}
272-
}
232+
rowSpan = cell.GetRowSpan();
233+
columnSpan = cell.GetColumnSpan();
234+
if (cell.IsInMergedRangeNotFirst())
235+
return false; // Don't generate a new <td> in this case
273236

274237
if (cell.GetEffectiveProperty<NoWrap>().ToBool() || cell.GetEffectiveProperty<TableCellFitText>().ToBool())
275238
{
@@ -448,48 +411,6 @@ internal bool ProcessTableCellProperties(TableCell cell, ref List<string> cellSt
448411
return true;
449412
}
450413

451-
private int CalculateRowSpan(TableCell cell)
452-
{
453-
var row = cell.Ancestors<TableRow>().FirstOrDefault();
454-
if (row == null)
455-
{
456-
return 1;
457-
}
458-
int cellIndex = row.Elements<TableCell>().ToList().IndexOf(cell);
459-
460-
int rowSpan = 1;
461-
// While the next row has a cell at the same index with a vertical merge, increment the row span
462-
var nextRow = row.NextSibling<TableRow>();
463-
while (nextRow != null)
464-
{
465-
var nextCell = nextRow.Elements<TableCell>().ElementAtOrDefault(cellIndex);
466-
if (nextCell?.TableCellProperties?.VerticalMerge is VerticalMerge vMerge &&
467-
(vMerge.Val == null || vMerge.Val == MergedCellValues.Continue))
468-
{
469-
rowSpan++;
470-
nextRow = nextRow.NextSibling<TableRow>();
471-
}
472-
else
473-
{
474-
break;
475-
}
476-
}
477-
return rowSpan;
478-
}
479-
480-
private int CalculateColumnSpan(TableCell cell)
481-
{
482-
int colSpan = 1;
483-
var currentCell = cell;
484-
while (currentCell?.NextSibling<TableCell>()?.TableCellProperties?.HorizontalMerge is HorizontalMerge hMerge &&
485-
(hMerge.Val == null || hMerge.Val == MergedCellValues.Continue))
486-
{
487-
colSpan++;
488-
currentCell = currentCell.NextSibling<TableCell>();
489-
}
490-
return colSpan;
491-
}
492-
493414
public void RemoveStyleIfPresent(ref List<string> styles, string style)
494415
{
495416
for (int i = styles.Count - 1; i >= 0; --i)

src/DocSharp.Docx/Helpers/TableHelpers.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,61 @@ public static int GetGridSpan(this TableCell cell)
6363

6464
public static int GetColumnSpan(this TableCell cell)
6565
{
66-
return cell.TableCellProperties?.GridSpan?.Val ?? GetHorizontalMargeSpan(cell);
66+
return cell.TableCellProperties?.GridSpan?.Val ?? GetHorizontalMergeSpan(cell);
6767
}
6868

69-
private static int GetHorizontalMargeSpan(this TableCell cell)
69+
public static int GetColumnCount(this Table table)
70+
{
71+
int maxCellsPerRow = 0;
72+
foreach (var row in table.Elements<TableRow>())
73+
{
74+
int cellsPerRow = 0;
75+
foreach (var cell in row.Elements<TableCell>())
76+
{
77+
cellsPerRow += cell.GetColumnSpan();
78+
}
79+
maxCellsPerRow = Math.Max(maxCellsPerRow, cellsPerRow);
80+
}
81+
return maxCellsPerRow;
82+
}
83+
84+
public static float GetCellWidthInPoints(this TableCell cell)
85+
{
86+
var cellWidth = cell.GetEffectiveProperty<TableCellWidth>();
87+
if (cellWidth?.Type != null && cellWidth.Type.Value == TableWidthUnitValues.Dxa &&
88+
cellWidth.Width != null && cellWidth.Width.ToLong() is long width)
89+
{
90+
return width / 20f; // convert twips to points
91+
}
92+
return 0; // Auto, Pct, Nil or unspecified width; should be handled depending on the context
93+
}
94+
95+
public static List<float> GetColumnsWidth(this Table table)
96+
{
97+
var columnWidths = new List<float>();
98+
foreach (var row in table.Elements<TableRow>())
99+
{
100+
int cellIndex = 0;
101+
foreach (var cell in row.Elements<TableCell>())
102+
{
103+
var gridSpan = cell.GetGridSpan();
104+
float width = cell.GetCellWidthInPoints() / gridSpan;
105+
106+
for (int i = 0; i < gridSpan; i++)
107+
{
108+
if (columnWidths.Count > cellIndex)
109+
columnWidths[cellIndex] = Math.Max(columnWidths[cellIndex], width);
110+
else
111+
columnWidths.Add(width);
112+
113+
++cellIndex;
114+
}
115+
}
116+
}
117+
return columnWidths;
118+
}
119+
120+
internal static int GetHorizontalMergeSpan(this TableCell cell)
70121
{
71122
int columnSpan = 1;
72123
if (cell.TableCellProperties?.HorizontalMerge?.Val != null &&

src/WIP/DocSharp.Renderer/DocxRenderer.Tables.cs

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,20 @@ internal override void ProcessTable(Table table, QuestPdfModel output)
2222
// Process table properties and create a new QuestPdfTable object
2323
var t = new QuestPdfTable()
2424
{
25-
ColumnsCount = table.Elements<TableRow>().Max(c => c.Elements<TableCell>().Count())
25+
ColumnsWidth = table.GetColumnsWidth(),
2626
// TODO: check SdtRow/CustomXmlRow and SdtCell/CustomXmlCell too.
2727
};
28+
if (table.GetEffectiveProperty<TableJustification>() is TableJustification jc && jc.Val != null)
29+
{
30+
if (jc.Val == TableRowAlignmentValues.Center)
31+
t.Alignment = HorizontalAlignment.Center;
32+
else if (jc.Val == TableRowAlignmentValues.Right)
33+
t.Alignment = HorizontalAlignment.Right;
34+
else
35+
t.Alignment = HorizontalAlignment.Left;
36+
}
37+
38+
2839
// Add table to the current container.
2940
if (currentContainer.Count > 0)
3041
currentContainer.Peek().Content.Add(t);
@@ -70,7 +81,12 @@ internal override void ProcessTableCell(TableCell tableCell, QuestPdfModel outpu
7081
{
7182
cell.ColumnSpan = (uint)columnSpan;
7283
}
73-
var rowSpan = tableCell.GetRowSpan();
84+
var rowSpan = tableCell.GetRowSpan();
85+
// The sum of GridSpans can exceed the total number of cells in DOCX,
86+
// while in QuestPDF this causes an exception.
87+
// So, it is ignored and only the cell width is used;
88+
// while HorizontalMerge/VerticalMerge are handled normally.
89+
// var rowSpan = tableCell.GetHorizontalMergeSpan();
7490
if (rowSpan > 1)
7591
{
7692
cell.RowSpan = (uint)rowSpan;
@@ -81,6 +97,7 @@ internal override void ProcessTableCell(TableCell tableCell, QuestPdfModel outpu
8197
return;
8298
else
8399
cell.ColumnNumber = (uint)columnNumber;
100+
84101
var rowNumber = tableCell.GetRowNumber();
85102
if (rowNumber < 1)
86103
return;
@@ -144,69 +161,45 @@ internal override void ProcessTableCell(TableCell tableCell, QuestPdfModel outpu
144161
if (topMargin?.Type != null)
145162
{
146163
if (topMargin.Type.Value == TableWidthUnitValues.Nil)
147-
{
148164
cell.PaddingTop = 0;
149-
}
150165
else if (topMargin.Type.Value == TableWidthUnitValues.Dxa && topMargin.Width.ToLong() is long top)
151-
{
152166
cell.PaddingTop = top / 20f; // convert twips to points
153-
}
154167
// TODO: Auto and Pct types
155168
}
156169
if (bottomMargin?.Type != null)
157170
{
158171
if (bottomMargin.Type.Value == TableWidthUnitValues.Nil)
159-
{
160172
cell.PaddingBottom = 0;
161-
}
162173
else if (bottomMargin.Type.Value == TableWidthUnitValues.Dxa && bottomMargin.Width.ToLong() is long bottom)
163-
{
164174
cell.PaddingBottom = bottom / 20f;
165-
}
166175
}
167176
if (leftMargin is TableWidthType twt1 && twt1?.Type != null)
168177
{
169178
if (twt1.Type.Value == TableWidthUnitValues.Nil)
170-
{
171179
cell.PaddingLeft = 0;
172-
}
173180
else if (twt1.Type.Value == TableWidthUnitValues.Dxa && twt1.Width.ToLong() is long left)
174-
{
175181
cell.PaddingLeft = left / 20f;
176-
}
177182
}
178183
else if (leftMargin is TableWidthDxaNilType dxaNilType1 && dxaNilType1?.Type != null)
179184
{
180185
if (dxaNilType1.Type.Value == TableWidthValues.Nil)
181-
{
182186
cell.PaddingLeft = 0;
183-
}
184187
else if (dxaNilType1.Type.Value == TableWidthValues.Dxa && dxaNilType1.Width != null)
185-
{
186188
cell.PaddingLeft = dxaNilType1.Width.Value / 20f;
187-
}
188189
}
189190
if (rightMargin is TableWidthType twt2 && twt2?.Type != null)
190191
{
191192
if (twt2.Type.Value == TableWidthUnitValues.Nil)
192-
{
193193
cell.PaddingRight = 0;
194-
}
195194
else if (twt2.Type.Value == TableWidthUnitValues.Dxa && twt2.Width.ToLong() is long right)
196-
{
197195
cell.PaddingRight = right / 20f;
198-
}
199196
}
200197
else if (rightMargin is TableWidthDxaNilType dxaNilType2 && dxaNilType2?.Type != null)
201198
{
202199
if (dxaNilType2.Type.Value == TableWidthValues.Nil)
203-
{
204200
cell.PaddingRight = 0;
205-
}
206201
else if (dxaNilType2.Type.Value == TableWidthValues.Dxa && dxaNilType2.Width != null)
207-
{
208202
cell.PaddingRight = dxaNilType2.Width.Value / 20f;
209-
}
210203
}
211204

212205
var verticalAlignment = tableCell.GetEffectiveProperty<TableCellVerticalAlignment>();
@@ -220,6 +213,37 @@ internal override void ProcessTableCell(TableCell tableCell, QuestPdfModel outpu
220213
cell.VertAlignment = VerticalAlignment.Bottom;
221214
}
222215

216+
// Calculate cell height considering row height and rowSpan
217+
if (row != null)
218+
{
219+
int rowSpanCounter = rowSpan;
220+
TableRow? currentRow = row;
221+
while (rowSpanCounter > 0 && currentRow != null)
222+
{
223+
var height = currentRow.GetEffectiveProperty<TableRowHeight>();
224+
if (height != null &&
225+
height.Val != null &&
226+
(height.HeightType == null || // if HeightType is not specified but a value is present, assume it means "Exact"
227+
height.HeightType.Value == HeightRuleValues.AtLeast ||
228+
height.HeightType.Value == HeightRuleValues.Exact))
229+
// if HeightType is "Auto" instead, the row should automatically resize to fit the content,
230+
// so don't set an height in QuestPdf
231+
{
232+
if (height.HeightType != null && height.HeightType.Value == HeightRuleValues.AtLeast)
233+
{
234+
cell.MinHeight += height.Val.Value / 20f; // Convert twips to points
235+
}
236+
else
237+
{
238+
cell.Height += height.Val.Value / 20f;
239+
cell.MinHeight += height.Val.Value / 20f;
240+
}
241+
}
242+
currentRow = currentRow.NextSibling<TableRow>();
243+
--rowSpanCounter;
244+
}
245+
}
246+
223247
// Add cell to the row model.
224248
if (currentRow.Count > 0)
225249
currentRow.Peek().Cells.Add(cell);

src/WIP/DocSharp.Renderer/Model/Enums.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ internal enum StrikethroughStyle
4141
Double
4242
}
4343

44+
internal enum HorizontalAlignment
45+
{
46+
Left,
47+
Center,
48+
Right
49+
}
50+
4451
internal enum VerticalAlignment
4552
{
4653
Top,

0 commit comments

Comments
 (0)