Skip to content

Commit 5a1caae

Browse files
Merge pull request #1976 from EvotecIT/codex/word-pdf-engine-parity-main
Improve native Word PDF layout parity
2 parents 7f968d9 + ac8d61e commit 5a1caae

103 files changed

Lines changed: 5489 additions & 416 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

OfficeIMO.Drawing/OfficeChartDrawingRenderer.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,18 @@ public static OfficeDrawing Render(OfficeChartSnapshot snapshot) {
3131
double contentTop = 0D;
3232
if (!string.IsNullOrWhiteSpace(snapshot.Title)) {
3333
double titleHeight = Math.Min(22D, Math.Max(16D, height * 0.12D));
34+
double titleTop = Math.Min(layout.TitleTopPadding, Math.Max(0D, height - titleHeight));
3435
drawing.AddText(
3536
snapshot.Title!,
3637
8D,
37-
5D,
38+
titleTop,
3839
Math.Max(1D, width - 16D),
3940
Math.Max(1D, titleHeight - 4D),
4041
new OfficeFontInfo(style.FontFamily, Math.Min(12D, Math.Max(8D, titleHeight - 7D)), OfficeFontStyle.Bold),
4142
style.TitleColor,
4243
OfficeTextAlignment.Center);
4344
if (!layout.OverlayTitle) {
44-
contentTop = titleHeight;
45+
contentTop = titleHeight + Math.Max(0D, titleTop - 5D);
4546
}
4647
}
4748

OfficeIMO.Drawing/OfficeChartLayout.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public sealed class OfficeChartLayout {
5454
/// <param name="showCategoryAxisLabels">Whether category or horizontal tick labels should be rendered.</param>
5555
/// <param name="showValueAxisLabels">Whether value or vertical tick labels should be rendered.</param>
5656
/// <param name="overlayTitle">Whether the title should overlay the plot instead of reserving layout space.</param>
57+
/// <param name="titleTopPadding">Top padding before the chart title inside the chart canvas.</param>
5758
public OfficeChartLayout(
5859
double? seriesLegendWidthRatio = null,
5960
double? categoryLegendWidthRatio = null,
@@ -93,7 +94,8 @@ public OfficeChartLayout(
9394
bool showValueAxisLine = true,
9495
bool showCategoryAxisLabels = true,
9596
bool showValueAxisLabels = true,
96-
bool overlayTitle = false)
97+
bool overlayTitle = false,
98+
double? titleTopPadding = null)
9799
: this(
98100
overlayLegend: false,
99101
seriesLegendWidthRatio: seriesLegendWidthRatio,
@@ -134,7 +136,8 @@ public OfficeChartLayout(
134136
showValueAxisLine: showValueAxisLine,
135137
showCategoryAxisLabels: showCategoryAxisLabels,
136138
showValueAxisLabels: showValueAxisLabels,
137-
overlayTitle: overlayTitle) {
139+
overlayTitle: overlayTitle,
140+
titleTopPadding: titleTopPadding) {
138141
}
139142

140143
/// <summary>
@@ -180,6 +183,7 @@ public OfficeChartLayout(
180183
/// <param name="showCategoryAxisLabels">Whether category or horizontal tick labels should be rendered.</param>
181184
/// <param name="showValueAxisLabels">Whether value or vertical tick labels should be rendered.</param>
182185
/// <param name="overlayTitle">Whether the title should overlay the plot instead of reserving layout space.</param>
186+
/// <param name="titleTopPadding">Top padding before the chart title inside the chart canvas.</param>
183187
public OfficeChartLayout(
184188
bool overlayLegend,
185189
double? seriesLegendWidthRatio = null,
@@ -220,7 +224,8 @@ public OfficeChartLayout(
220224
bool showValueAxisLine = true,
221225
bool showCategoryAxisLabels = true,
222226
bool showValueAxisLabels = true,
223-
bool overlayTitle = false) {
227+
bool overlayTitle = false,
228+
double? titleTopPadding = null) {
224229
SeriesLegendWidthRatio = ValidateRatio(seriesLegendWidthRatio ?? 0.34D, nameof(seriesLegendWidthRatio));
225230
CategoryLegendWidthRatio = ValidateRatio(categoryLegendWidthRatio ?? 0.38D, nameof(categoryLegendWidthRatio));
226231
LegendRowHeight = ValidatePositiveFinite(legendRowHeight ?? 12D, nameof(legendRowHeight));
@@ -261,6 +266,7 @@ public OfficeChartLayout(
261266
ShowCategoryAxisLabels = showCategoryAxis && showCategoryAxisLabels;
262267
ShowValueAxisLabels = showValueAxis && showValueAxisLabels;
263268
OverlayTitle = overlayTitle;
269+
TitleTopPadding = ValidateNonNegativeFinite(titleTopPadding ?? 5D, nameof(titleTopPadding));
264270
}
265271

266272
/// <summary>Default premium OfficeIMO chart layout.</summary>
@@ -398,6 +404,9 @@ public OfficeChartLayout(
398404
/// <summary>Whether the chart title should overlay the plot area instead of reserving a title band.</summary>
399405
public bool OverlayTitle { get; }
400406

407+
/// <summary>Top padding before the chart title inside the chart canvas.</summary>
408+
public double TitleTopPadding { get; }
409+
401410
private static double ValidateRatio(double value, string paramName) {
402411
ValidatePositiveFinite(value, paramName);
403412
if (value > 0.75D) {
@@ -415,6 +424,14 @@ private static double ValidatePositiveFinite(double value, string paramName) {
415424
return value;
416425
}
417426

427+
private static double ValidateNonNegativeFinite(double value, string paramName) {
428+
if (double.IsNaN(value) || double.IsInfinity(value) || value < 0D) {
429+
throw new ArgumentOutOfRangeException(paramName, "Chart layout values must be finite non-negative numbers.");
430+
}
431+
432+
return value;
433+
}
434+
418435
private static int ValidatePositive(int value, string paramName) {
419436
if (value <= 0) {
420437
throw new ArgumentOutOfRangeException(paramName, "Chart layout counts must be positive.");

OfficeIMO.Markup.VSCode/package-lock.json

Lines changed: 11 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

OfficeIMO.Pdf/Model/PdfEmbeddedFontFamily.System.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,16 @@ private static bool IsMetadataFamilyMatch(TrueTypeNameMetadata metadata, string
218218
}
219219
}
220220

221+
foreach (string? faceName in metadata.GetFaceNames()) {
222+
if (string.IsNullOrWhiteSpace(faceName)) {
223+
continue;
224+
}
225+
226+
if (IsMetadataFamilyNameMatch(faceName!, normalizedMetadataFamily)) {
227+
return true;
228+
}
229+
}
230+
221231
return false;
222232
}
223233

@@ -580,6 +590,26 @@ public TrueTypeNameMetadata(
580590
yield return TypographicFamilyName;
581591
yield return FamilyName;
582592
}
593+
594+
public System.Collections.Generic.IEnumerable<string?> GetFaceNames() {
595+
yield return FullName;
596+
yield return PostScriptName;
597+
yield return CombineFamilyAndSubfamily(TypographicFamilyName, TypographicSubfamilyName);
598+
yield return CombineFamilyAndSubfamily(FamilyName, SubfamilyName);
599+
}
600+
601+
private static string? CombineFamilyAndSubfamily(string? familyName, string? subfamilyName) {
602+
if (string.IsNullOrWhiteSpace(familyName)) {
603+
return null;
604+
}
605+
606+
if (string.IsNullOrWhiteSpace(subfamilyName) ||
607+
string.Equals(subfamilyName, "Regular", System.StringComparison.OrdinalIgnoreCase)) {
608+
return familyName;
609+
}
610+
611+
return familyName + " " + subfamilyName;
612+
}
583613
}
584614

585615
private sealed class TrueTypeNameValue {

OfficeIMO.Pdf/Model/PdfHeadingStyle.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public double? SpacingAfter {
4848
/// <summary>Heading text color. A heading block color overrides this value.</summary>
4949
public PdfColor? Color { get; set; }
5050

51+
/// <summary>Heading font slot. When null the writer uses the document default font family.</summary>
52+
public PdfStandardFont? Font { get; set; }
53+
5154
/// <summary>When true, headings use the bold variant of the document font.</summary>
5255
public bool Bold { get; set; } = true;
5356

@@ -65,6 +68,7 @@ public PdfHeadingStyle Clone() {
6568
SpacingBefore = SpacingBefore,
6669
SpacingAfter = SpacingAfter,
6770
Color = Color,
71+
Font = Font,
6872
Bold = Bold,
6973
ApplySpacingBeforeAtTop = ApplySpacingBeforeAtTop,
7074
KeepWithNext = KeepWithNext

OfficeIMO.Pdf/Model/PdfTableCell.cs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public PdfTableCell(string? text, int columnSpan = 1, string? linkUri = null, st
1818
CheckBoxes = SnapshotCheckBoxes(checkBoxes, nameof(checkBoxes));
1919
FormFields = SnapshotFormFields(formFields, nameof(formFields));
2020
Images = SnapshotImages(images, nameof(images));
21+
Paragraphs = System.Array.AsReadOnly(System.Array.Empty<PdfTableCellParagraph>());
2122
}
2223

2324
/// <summary>Creates a table cell with rich text runs, optional column/row spans, optional link metadata, images, and form fields.</summary>
@@ -46,6 +47,35 @@ public PdfTableCell(System.Collections.Generic.IEnumerable<TextRun> runs, int co
4647
CheckBoxes = SnapshotCheckBoxes(checkBoxes, nameof(checkBoxes));
4748
FormFields = SnapshotFormFields(formFields, nameof(formFields));
4849
Images = SnapshotImages(images, nameof(images));
50+
Paragraphs = System.Array.AsReadOnly(System.Array.Empty<PdfTableCellParagraph>());
51+
}
52+
53+
internal PdfTableCell(System.Collections.Generic.IEnumerable<TextRun> runs, System.Collections.Generic.IEnumerable<PdfTableCellParagraph>? paragraphs, int columnSpan = 1, string? linkUri = null, string? linkContents = null, int rowSpan = 1, System.Collections.Generic.IEnumerable<PdfTableCellCheckBox>? checkBoxes = null, System.Collections.Generic.IEnumerable<PdfTableCellFormField>? formFields = null, System.Collections.Generic.IEnumerable<PdfTableCellImage>? images = null, string? linkDestinationName = null, string? namedDestinationName = null) {
54+
Guard.NotNull(runs, nameof(runs));
55+
Validate(columnSpan, rowSpan, linkUri, linkDestinationName, linkContents, namedDestinationName);
56+
var snapshot = new System.Collections.Generic.List<TextRun>();
57+
var text = new System.Text.StringBuilder();
58+
foreach (TextRun run in runs) {
59+
if (run is null) {
60+
throw new System.ArgumentException("Table cell text runs cannot contain null entries.", nameof(runs));
61+
}
62+
63+
snapshot.Add(run);
64+
text.Append(run.Text);
65+
}
66+
67+
Text = text.ToString();
68+
Runs = snapshot.AsReadOnly();
69+
ColumnSpan = columnSpan;
70+
RowSpan = rowSpan;
71+
LinkUri = linkUri;
72+
LinkDestinationName = linkDestinationName;
73+
NamedDestinationName = namedDestinationName;
74+
LinkContents = HasLinkTarget(linkUri, linkDestinationName) ? linkContents ?? Text : null;
75+
CheckBoxes = SnapshotCheckBoxes(checkBoxes, nameof(checkBoxes));
76+
FormFields = SnapshotFormFields(formFields, nameof(formFields));
77+
Images = SnapshotImages(images, nameof(images));
78+
Paragraphs = SnapshotParagraphs(paragraphs, nameof(paragraphs));
4979
}
5080

5181
/// <summary>Cell text content.</summary>
@@ -81,6 +111,8 @@ public PdfTableCell(System.Collections.Generic.IEnumerable<TextRun> runs, int co
81111
/// <summary>Images rendered inside this cell.</summary>
82112
public System.Collections.Generic.IReadOnlyList<PdfTableCellImage> Images { get; }
83113

114+
internal System.Collections.Generic.IReadOnlyList<PdfTableCellParagraph> Paragraphs { get; }
115+
84116
/// <summary>Creates a single-column text cell.</summary>
85117
public static PdfTableCell TextCell(string? text, string? linkUri = null, string? linkContents = null, string? linkDestinationName = null, string? namedDestinationName = null) => new PdfTableCell(text, linkUri: linkUri, linkContents: linkContents, linkDestinationName: linkDestinationName, namedDestinationName: namedDestinationName);
86118

@@ -118,9 +150,9 @@ public PdfTableCell(System.Collections.Generic.IEnumerable<TextRun> runs, int co
118150
public static PdfTableCell WithImages(string? text, System.Collections.Generic.IEnumerable<PdfTableCellImage> images, int columnSpan = 1, string? linkUri = null, string? linkContents = null, int rowSpan = 1, System.Collections.Generic.IEnumerable<PdfTableCellCheckBox>? checkBoxes = null, System.Collections.Generic.IEnumerable<PdfTableCellFormField>? formFields = null, string? linkDestinationName = null) => new PdfTableCell(text, columnSpan, linkUri, linkContents, rowSpan, checkBoxes, formFields, images, linkDestinationName);
119151

120152
/// <summary>Returns a copy of this cell with a PDF named destination defined at the cell.</summary>
121-
public PdfTableCell WithNamedDestination(string? namedDestinationName) => new PdfTableCell(Runs, ColumnSpan, LinkUri, LinkContents, RowSpan, CheckBoxes, FormFields, Images, LinkDestinationName, namedDestinationName);
153+
public PdfTableCell WithNamedDestination(string? namedDestinationName) => new PdfTableCell(Runs, Paragraphs, ColumnSpan, LinkUri, LinkContents, RowSpan, CheckBoxes, FormFields, Images, LinkDestinationName, namedDestinationName);
122154

123-
internal PdfTableCell Clone() => new PdfTableCell(Runs, ColumnSpan, LinkUri, LinkContents, RowSpan, CheckBoxes, FormFields, Images, LinkDestinationName, NamedDestinationName);
155+
internal PdfTableCell Clone() => new PdfTableCell(Runs, Paragraphs, ColumnSpan, LinkUri, LinkContents, RowSpan, CheckBoxes, FormFields, Images, LinkDestinationName, NamedDestinationName);
124156

125157
private static void Validate(int columnSpan, int rowSpan, string? linkUri, string? linkDestinationName, string? linkContents, string? namedDestinationName) {
126158
if (columnSpan < 1) {
@@ -206,4 +238,48 @@ private static System.Collections.ObjectModel.ReadOnlyCollection<PdfTableCellIma
206238

207239
return snapshot.AsReadOnly();
208240
}
241+
242+
private static System.Collections.ObjectModel.ReadOnlyCollection<PdfTableCellParagraph> SnapshotParagraphs(System.Collections.Generic.IEnumerable<PdfTableCellParagraph>? paragraphs, string paramName) {
243+
if (paragraphs == null) {
244+
return System.Array.AsReadOnly(System.Array.Empty<PdfTableCellParagraph>());
245+
}
246+
247+
var snapshot = new System.Collections.Generic.List<PdfTableCellParagraph>();
248+
foreach (PdfTableCellParagraph paragraph in paragraphs) {
249+
if (paragraph == null) {
250+
throw new System.ArgumentException("Table cell paragraphs cannot contain null entries.", paramName);
251+
}
252+
253+
snapshot.Add(paragraph.Clone());
254+
}
255+
256+
return snapshot.AsReadOnly();
257+
}
258+
}
259+
260+
internal sealed class PdfTableCellParagraph {
261+
public PdfTableCellParagraph(System.Collections.Generic.IEnumerable<TextRun> runs, double spacingAfter = 0D) {
262+
Guard.NotNull(runs, nameof(runs));
263+
if (spacingAfter < 0 || double.IsNaN(spacingAfter) || double.IsInfinity(spacingAfter)) {
264+
throw new System.ArgumentOutOfRangeException(nameof(spacingAfter), "Table cell paragraph spacing must be a non-negative finite value.");
265+
}
266+
267+
var snapshot = new System.Collections.Generic.List<TextRun>();
268+
foreach (TextRun run in runs) {
269+
if (run is null) {
270+
throw new System.ArgumentException("Table cell paragraph runs cannot contain null entries.", nameof(runs));
271+
}
272+
273+
snapshot.Add(run);
274+
}
275+
276+
Runs = snapshot.AsReadOnly();
277+
SpacingAfter = spacingAfter;
278+
}
279+
280+
public System.Collections.Generic.IReadOnlyList<TextRun> Runs { get; }
281+
282+
public double SpacingAfter { get; }
283+
284+
internal PdfTableCellParagraph Clone() => new PdfTableCellParagraph(Runs, SpacingAfter);
209285
}

OfficeIMO.Pdf/Model/PdfTableStyle.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class PdfTableStyle {
3838
private double _cellSpacing;
3939
private double _minRowHeight;
4040
private double _spacingBefore;
41+
private double _pageContinuationSpacingBefore;
4142
private double? _captionFontSize;
4243
private double _captionSpacingAfter = 4;
4344
private double _spacingAfter;
@@ -411,6 +412,14 @@ public double SpacingBefore {
411412
_spacingBefore = value;
412413
}
413414
}
415+
/// <summary>Vertical space to reserve before table content when the same table continues on a new page.</summary>
416+
public double PageContinuationSpacingBefore {
417+
get => _pageContinuationSpacingBefore;
418+
set {
419+
ValidateNonNegativeFiniteValue(value, nameof(PageContinuationSpacingBefore), "Table page continuation spacing before must be a non-negative finite value.");
420+
_pageContinuationSpacingBefore = value;
421+
}
422+
}
414423
/// <summary>Optional text rendered above the table grid as part of the table flow.</summary>
415424
public string? Caption { get; set; }
416425
/// <summary>Caption alignment inside the rendered table width.</summary>
@@ -463,6 +472,8 @@ public double? MaxWidth {
463472
_maxWidth = value;
464473
}
465474
}
475+
/// <summary>When true, the resolved table frame width is preserved even if measured columns would otherwise shrink to their content.</summary>
476+
public bool PreserveWidth { get; set; }
466477
/// <summary>Optional left indentation before table placement, in points.</summary>
467478
public double LeftIndent {
468479
get => _leftIndent;
@@ -581,6 +592,7 @@ public PdfTableStyle Clone() {
581592
MinRowHeight = MinRowHeight,
582593
RowMinHeights = RowMinHeights,
583594
SpacingBefore = SpacingBefore,
595+
PageContinuationSpacingBefore = PageContinuationSpacingBefore,
584596
Caption = Caption,
585597
CaptionAlign = CaptionAlign,
586598
CaptionColor = CaptionColor,
@@ -589,6 +601,7 @@ public PdfTableStyle Clone() {
589601
SpacingAfter = SpacingAfter,
590602
RowBaselineOffset = RowBaselineOffset,
591603
MaxWidth = MaxWidth,
604+
PreserveWidth = PreserveWidth,
592605
LeftIndent = LeftIndent,
593606
AutoFitColumns = AutoFitColumns,
594607
RightAlignNumeric = RightAlignNumeric,

0 commit comments

Comments
 (0)