Skip to content

Commit 775d363

Browse files
committed
Prepare structure for DOCX QuestPDF renderer
1 parent e892b5b commit 775d363

18 files changed

+607
-305
lines changed

src/DocSharp.Docx/DocxToRtf/DocxToRtfConverter.Paragraph.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ internal void ProcessParagraphFormatting(Paragraph paragraph, RtfStringWriter sb
160160
sb.Write(@"\qk10");
161161
else if (alignment.Val == JustificationValues.HighKashida)
162162
sb.Write(@"\qk20");
163+
// else if (alignment.Val == JustificationValues.NumTab)
163164
}
164165

165166
var spacing = paragraph.GetEffectiveSpacing();

src/DocSharp.Docx/Helpers/ColorHelpers.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,11 @@ public static string ConvertSystemColorToHex(A.SystemColor systemColor)
965965
return string.Empty;
966966
}
967967

968+
/// <summary>
969+
/// Removes '#' from hex color string; converts the short RGB format to RRGGBB; converts named color to hex (e.g. "Red" to FF0000).
970+
/// </summary>
971+
/// <param name="value">The hex string to normalize.</param>
972+
/// <returns></returns>
968973
internal static string? EnsureHexColor(string? value)
969974
{
970975
if (value == null)

src/DocSharp.Renderer/DocxRenderer.cs

Lines changed: 193 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ namespace DocSharp.Renderer;
1414

1515
internal class DocxRenderer : DocxEnumerator<QuestPdfModel>, IDocumentRenderer<QuestPDF.Fluent.Document>
1616
{
17+
private QuestPdfPageSet? currentPageSet; // Current section
18+
private Stack<QuestPdfContainer> currentContainer = new(); // Container can be the main document body, header, footer, table cell, ...
19+
private Stack<IQuestPdfRunContainer> currentRunContainer = new(); // Spans can only be added to a paragraph or hyperlink
20+
private Stack<QuestPdfParagraph> currentParagraph = new(); // Hyperlinks can only be added to a paragraph
21+
private Stack<QuestPdfTable> currentTable = new(); // Rows can only be added to a table
22+
private Stack<QuestPdfTableRow> currentRow = new(); // Cells can only be added to a table row
23+
private Stack<QuestPdfSpan> currentSpan = new(); // Text can only be added to a span
24+
private QuestPDF.Infrastructure.Color? pageColor; // Page color is the same for all sections in DOCX
25+
1726
/// <summary>
1827
/// Render a DOCX document to a QuestPDF document.
1928
/// </summary>
@@ -65,10 +74,18 @@ internal override void ProcessDocument(W.Document document, QuestPdfModel output
6574
base.ProcessDocument(document, output);
6675
}
6776

77+
internal override void ProcessDocumentBackground(DocumentBackground background, QuestPdfModel output)
78+
{
79+
if (ColorHelpers.EnsureHexColor(background.Color?.Value) is string color && !string.IsNullOrWhiteSpace(color))
80+
{
81+
pageColor = QuestPDF.Infrastructure.Color.FromHex(color);
82+
// Page background color is the same for all sections in DOCX, save the value.
83+
}
84+
}
85+
6886
internal override void ProcessBody(W.Body body, QuestPdfModel output)
69-
{
87+
{
7088
Sections = body.GetSections(); // Split content in sections (implemented in the base class)
71-
7289
foreach(var sect in Sections)
7390
{
7491
ProcessSection(sect, body.GetMainDocumentPart(), output);
@@ -77,14 +94,17 @@ internal override void ProcessBody(W.Body body, QuestPdfModel output)
7794

7895
internal override void ProcessSection((List<OpenXmlElement> content, SectionProperties properties) section, MainDocumentPart? mainPart, QuestPdfModel output)
7996
{
97+
if (mainPart == null)
98+
return;
99+
80100
// Process section properties here and add them to a new QuestPdfPageSet object
81101
var sectionProperties = section.properties;
82-
float w = (float)DocSharp.Primitives.PageSize.Default.WidthMm;
83-
float h = (float)DocSharp.Primitives.PageSize.Default.HeightMm;
84-
float l = (float)DocSharp.Primitives.PageMargins.Default.LeftMm;
85-
float t = (float)DocSharp.Primitives.PageMargins.Default.TopMm;
86-
float r = (float)DocSharp.Primitives.PageMargins.Default.RightMm;
87-
float b = (float)DocSharp.Primitives.PageMargins.Default.BottomMm;
102+
float w = Primitives.PageSize.Default.WidthTwips();
103+
float h = Primitives.PageSize.Default.HeightTwips();
104+
float l = Primitives.PageMargins.Default.LeftTwips();
105+
float t = Primitives.PageMargins.Default.TopTwips();
106+
float r = Primitives.PageMargins.Default.RightTwips();
107+
float b = Primitives.PageMargins.Default.BottomTwips();
88108

89109
if (sectionProperties.GetFirstChild<PageSize>() is PageSize size)
90110
{
@@ -112,65 +132,212 @@ internal override void ProcessSection((List<OpenXmlElement> content, SectionProp
112132
}
113133
}
114134
var pageSet = new QuestPdfPageSet(w, h, l, t, r, b, QuestPDF.Infrastructure.Unit.Millimetre);
135+
if (pageColor.HasValue)
136+
pageSet.BackgroundColor = pageColor.Value;
137+
138+
// Add page set to PageSets collection
115139
output.PageSets.Add(pageSet);
116140

117-
// Then, enumerate elements in the section (paragraphs, tables, ...)
141+
// Process headers for this section
142+
var headerRefs = sectionProperties.Elements<HeaderReference>();
143+
// QuestPDF can't produce different header and footer on odd/even pages.
144+
// For now, handle the default header and footer only.
145+
var headerRef = headerRefs.FirstOrDefault(h => h.Type == null || !h.Type.HasValue || h.Type.Value == HeaderFooterValues.Default);
146+
if (headerRef?.Id?.Value is string headerId && mainPart.GetPartById(headerId) is HeaderPart headerPart)
147+
{
148+
currentContainer.Push(pageSet.Header);
149+
base.ProcessHeader(headerPart.Header, output);
150+
if (currentContainer.Count > 0)
151+
currentContainer.Pop();
152+
}
153+
154+
// Process footers for this section
155+
var footerRefs = sectionProperties.Elements<FooterReference>();
156+
// QuestPDF can't produce different header and footer on odd/even pages.
157+
// For now, handle the default header and footer only.
158+
var footerRef = footerRefs.FirstOrDefault(h => h.Type == null || !h.Type.HasValue || h.Type.Value == HeaderFooterValues.Default);
159+
if (footerRef?.Id?.Value is string footerId && mainPart.GetPartById(footerId) is FooterPart footerPart)
160+
{
161+
currentContainer.Push(pageSet.Footer);
162+
base.ProcessFooter(footerPart.Footer, output);
163+
if (currentContainer.Count > 0)
164+
currentContainer.Pop();
165+
}
166+
167+
// Process elements in the section body itself (paragraphs, tables, ...)
168+
currentContainer.Push(pageSet.Content);
118169
base.ProcessSection(section, mainPart, output);
170+
if (currentContainer.Count > 0)
171+
currentContainer.Pop();
119172
}
120173

121174
internal override void ProcessParagraph(Paragraph paragraph, QuestPdfModel output)
122175
{
123-
// Paragraph properties can be processed here.
124-
var alignment = paragraph.GetEffectiveProperty<TextAlignment>();
125-
126-
// Then, enumerate elements in the paragraph (runs, hyperlinks, math formulas).
176+
// Process paragraph properties here and add them to a new QuestPdfParagraph object
177+
var p = new QuestPdfParagraph();
178+
if (paragraph.GetEffectiveProperty<Justification>() is Justification jc && jc.Val != null)
179+
{
180+
if (jc.Val == JustificationValues.Center)
181+
p.Alignment = ParagraphAlignment.Center;
182+
else if (jc.Val == JustificationValues.Right)
183+
p.Alignment = ParagraphAlignment.Right;
184+
else if (jc.Val == JustificationValues.Both || jc.Val == JustificationValues.Distribute || jc.Val == JustificationValues.ThaiDistribute)
185+
p.Alignment = ParagraphAlignment.Justify;
186+
else if (jc.Val == JustificationValues.Start)
187+
p.Alignment = ParagraphAlignment.Start;
188+
else if (jc.Val == JustificationValues.End)
189+
p.Alignment = ParagraphAlignment.End;
190+
else
191+
p.Alignment = ParagraphAlignment.Left;
192+
}
193+
194+
// Add paragraph to the current container (body, header, footer, table cell, ...)
195+
if (currentContainer.Count > 0)
196+
currentContainer.Peek().Content.Add(p);
197+
198+
// Enumerate and process paragraph elements (runs, hyperlinks, math formulas, ...)
199+
currentRunContainer.Push(p);
200+
currentParagraph.Push(p);
127201
base.ProcessParagraph(paragraph, output);
202+
if (currentRunContainer.Count > 0)
203+
currentRunContainer.Pop();
204+
if (currentParagraph.Count > 0)
205+
currentParagraph.Pop();
128206
}
129207

130208
internal override void ProcessHyperlink(Hyperlink hyperlink, QuestPdfModel output)
131209
{
132-
// The hyperlink URL/anchor can be processed here.
210+
// Retrieve the URL or anchor for this hyperlink and add it to a new QuestPdfHyperlink object
211+
var h = new QuestPdfHyperlink();
133212

134-
// Then, enumerate runs in the hyperlink
213+
// Add hyperlink to the paragraph model.
214+
if (currentParagraph.Count > 0)
215+
currentParagraph.Peek().Elements.Add(h);
216+
217+
// Enumerate and process runs in this hyperlink
218+
currentRunContainer.Push(h);
135219
base.ProcessHyperlink(hyperlink, output);
220+
if (currentRunContainer.Count > 0)
221+
currentRunContainer.Pop();
136222
}
137223

138224
internal override void ProcessRun(Run run, QuestPdfModel output)
139225
{
140-
// Run properties can be processed here.
226+
// Process run properties and add them to a new QuestPdfSpan object
141227
bool bold = run.GetEffectiveProperty<Bold>() is Bold b && (b.Val == null || b.Val);
142228
bool italic = run.GetEffectiveProperty<Italic>() is Italic i && (i.Val == null || i.Val);
143-
229+
UnderlineStyle underline = UnderlineStyle.None;
230+
StrikethroughStyle strikethrough = StrikethroughStyle.None;
231+
SubSuperscript supSuperscript = SubSuperscript.Normal;
232+
CapsType caps = CapsType.Normal;
233+
string? fontFamily = null;
234+
int? fontSize = null;
235+
QuestPDF.Infrastructure.Color? fontColor = null;
236+
QuestPDF.Infrastructure.Color? bgColor = null;
237+
QuestPDF.Infrastructure.Color? underlineColor = null;
238+
float? letterSpacing = null;
239+
var span = new QuestPdfSpan(null, bold, italic, underline, strikethrough, supSuperscript, caps, fontFamily, fontSize, fontColor, bgColor, underlineColor, letterSpacing);
240+
241+
// Add span to the paragraph/hyperlink.
242+
if (currentRunContainer.Count > 0)
243+
currentRunContainer.Peek().AddSpan(span);
244+
144245
// Then, enumerate run elements (text, picture, break, page number, footnote reference...)
246+
currentSpan.Push(span);
145247
base.ProcessRun(run, output);
248+
if (currentSpan.Count > 0)
249+
currentSpan.Pop();
146250
}
147251

148252
internal override void ProcessText(Text text, QuestPdfModel output)
149253
{
150-
var textString = text.Text;
254+
if (currentSpan.Count > 0 && !string.IsNullOrEmpty(text.Text))
255+
currentSpan.Peek().Text += Environment.NewLine;
256+
}
257+
258+
internal override void ProcessBreak(Break @break, QuestPdfModel output)
259+
{
260+
if (@break.Type == null || !@break.Type.HasValue || @break.Type.Value == BreakValues.TextWrapping)
261+
{
262+
if (currentSpan.Count > 0)
263+
currentSpan.Peek().Text += Environment.NewLine;
264+
}
265+
// TODO: page/column break
151266
}
152267

153268
internal override void ProcessTable(Table table, QuestPdfModel output)
154269
{
155-
// Enumerate rows and cells
156-
base.ProcessTable(table, output);
270+
// Process table properties and create a new QuestPdfTable object
271+
var t = new QuestPdfTable()
272+
{
273+
ColumnsCount = table.Elements<TableRow>().Max(c => c.Elements<TableCell>().Count())
274+
// TODO: check SdtRow/CustomXmlRow and SdtCell/CustomXmlCell too.
275+
};
276+
// Add table to the current container.
277+
if (currentContainer.Count > 0)
278+
currentContainer.Peek().Content.Add(t);
279+
280+
// Enumerate rows and cells
281+
currentTable.Push(t);
282+
base.ProcessTable(table, output);
283+
if (currentTable.Count > 0)
284+
currentTable.Pop();
157285
}
158286

159287
internal override void ProcessTableRow(TableRow tableRow, QuestPdfModel output)
160288
{
161-
// Enumerate cells
289+
// Create a new QuestPdfTableRow object
290+
var row = new QuestPdfTableRow();
291+
292+
// Add row to the table model.
293+
if (currentTable.Count > 0)
294+
currentTable.Peek().Rows.Add(row);
295+
296+
// Enumerate cells
297+
currentRow.Push(row);
162298
base.ProcessTableRow(tableRow, output);
299+
if (currentRow.Count > 0)
300+
currentRow.Pop();
163301
}
164302

165303
internal override void ProcessTableCell(TableCell tableCell, QuestPdfModel output)
166304
{
305+
// Create a new QuestPdfTableCell object
306+
var cell = new QuestPdfTableCell();
307+
308+
// Process cell properties
309+
if (tableCell.TableCellProperties?.GridSpan?.Val != null)
310+
{
311+
if (tableCell.TableCellProperties.GridSpan.Val.Value > 1)
312+
cell.ColumnSpan = (uint)tableCell.TableCellProperties.GridSpan.Val.Value;
313+
}
314+
if (tableCell.GetEffectiveProperty<Shading>() is Shading shading)
315+
{
316+
if ((shading.Val == null || (shading.Val.Value != ShadingPatternValues.Nil && shading.Val.Value != ShadingPatternValues.Solid)) &&
317+
ColorHelpers.EnsureHexColor(shading.Color?.Value) is string color && !string.IsNullOrWhiteSpace(color))
318+
{
319+
cell.BackgroundColor = QuestPDF.Infrastructure.Color.FromHex(color);
320+
// TODO: recognize other patterns. The pure primary color is displayed for ShadingPatternValues.Clear,
321+
// pure secondary color is displayed for ShadingPatternValues.Solid.
322+
// For now, we use the primary color for all patterns except Solid and Nil, and the secondary color for Solid.
323+
}
324+
else if ((shading.Val == null || shading.Val.Value == ShadingPatternValues.Solid) &&
325+
ColorHelpers.EnsureHexColor(shading.Fill?.Value) is string bgColor && !string.IsNullOrWhiteSpace(bgColor))
326+
{
327+
cell.BackgroundColor = QuestPDF.Infrastructure.Color.FromHex(bgColor);
328+
}
329+
}
330+
// TODO: vertical merge (set Cell.RowSpan); borders
331+
332+
// Add cell to the row model.
333+
if (currentRow.Count > 0)
334+
currentRow.Peek().Cells.Add(cell);
335+
167336
// Enumerate paragraphs (or nested tables) in the cell
337+
currentContainer.Push(cell);
168338
base.ProcessTableCell(tableCell, output);
169-
}
170-
171-
internal override void ProcessBreak(Break @break, QuestPdfModel output)
172-
{
173-
// Process line/page/column break
339+
if (currentContainer.Count > 0)
340+
currentContainer.Pop();
174341
}
175342

176343
internal override void ProcessBookmarkStart(BookmarkStart bookmarkStart, QuestPdfModel output)
@@ -197,10 +364,6 @@ internal override void ProcessCommentEnd(CommentRangeEnd commentEnd, QuestPdfMod
197364
{
198365
}
199366

200-
internal override void ProcessDocumentBackground(DocumentBackground background, QuestPdfModel output)
201-
{
202-
}
203-
204367
internal override void ProcessDrawing(Drawing picture, QuestPdfModel output)
205368
{
206369
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace DocSharp.Renderer;
2+
3+
internal enum SubSuperscript
4+
{
5+
Normal,
6+
Subscript,
7+
Superscript
8+
}
9+
10+
internal enum CapsType
11+
{
12+
Normal,
13+
SmallCaps,
14+
AllCaps
15+
}
16+
17+
internal enum ParagraphAlignment
18+
{
19+
Left,
20+
Center,
21+
Right,
22+
Justify,
23+
Start,
24+
End
25+
}
26+
27+
internal enum UnderlineStyle
28+
{
29+
None,
30+
Solid,
31+
Dashed,
32+
Dotted,
33+
Double,
34+
Wavy
35+
}
36+
37+
internal enum StrikethroughStyle
38+
{
39+
None,
40+
Single,
41+
Double
42+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace DocSharp.Renderer;
2+
3+
internal interface IQuestPdfRunContainer
4+
{
5+
void AddSpan(QuestPdfSpan span);
6+
}
7+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace DocSharp.Renderer;
2+
3+
internal abstract class QuestPdfBlock
4+
{
5+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Collections.Generic;
2+
3+
namespace DocSharp.Renderer;
4+
5+
internal class QuestPdfContainer
6+
{
7+
internal List<QuestPdfBlock> Content = new();
8+
}

0 commit comments

Comments
 (0)