Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/MiniPdf/DocxReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
var xpath = dataBinding.Attribute(W + "xpath")?.Value;
var prefixMappings = dataBinding.Attribute(W + "prefixMappings")?.Value ?? "";
if (string.IsNullOrEmpty(storeItemID) || string.IsNullOrEmpty(xpath)) return null;
if (!ctx.XmlStores.TryGetValue(storeItemID, out var doc) || doc.Root == null) return null;

Check warning on line 132 in src/MiniPdf/DocxReader.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'bool Dictionary<string, XDocument>.TryGetValue(string key, out XDocument value)'.

var nsMgr = new System.Xml.XmlNamespaceManager(new System.Xml.NameTable());
foreach (System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(
Expand Down Expand Up @@ -852,7 +852,7 @@
if (string.IsNullOrWhiteSpace(style))
return values;

foreach (var declaration in style.Split(';', StringSplitOptions.RemoveEmptyEntries))

Check warning on line 855 in src/MiniPdf/DocxReader.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 's' in 'string[] NetFxPolyfills.Split(string s, char separator, StringSplitOptions options)'.
{
var separator = declaration.IndexOf(':');
if (separator <= 0)
Expand Down Expand Up @@ -4030,12 +4030,32 @@
}
}

// Text area of the box: extent minus bodyPr insets (OOXML default 0.1in left/right).
const float emuPerPt = 12700f;
var bodyPr = anchor.Descendants(WPS + "bodyPr").FirstOrDefault();
var lIns = (long.TryParse(bodyPr?.Attribute("lIns")?.Value, out var li) ? li : 91440) / emuPerPt;
var rIns = (long.TryParse(bodyPr?.Attribute("rIns")?.Value, out var ri) ? ri : 91440) / emuPerPt;
var boxWidth = long.TryParse(anchor.Element(WP + "extent")?.Attribute("cx")?.Value, out var cx) ? cx / emuPerPt : 0f;
// Only column/margin-relative offsets map onto the margin-based header layout;
// page-relative and aligned boxes keep the inferred-alignment path above.
long offEmu = 0;
var hasMarginOffset = alignment == null
&& posH?.Attribute("relativeFrom")?.Value is "column" or "margin"
&& long.TryParse(posH.Element(WP + "posOffset")?.Value, out offEmu);
var boxOffset = offEmu / emuPerPt;

foreach (var txbxP in txbxContent.Elements(W + "p"))
{
var txbxPara = ReadParagraph(txbxP, styles, numbering, hfRels, archive, null, defaultFontName, defaultEastAsiaFontName);
if (txbxPara == null) continue;
if (alignment != null)
txbxPara = txbxPara with { Alignment = alignment };
if (hasMarginOffset && boxWidth > lIns + rIns)
txbxPara = txbxPara with
{
IndentLeft = boxOffset + lIns + txbxPara.IndentLeft,
TextBoxWidth = boxWidth - lIns - rIns - txbxPara.IndentLeft - txbxPara.IndentRight,
};
elements.Add(txbxPara);
}
}
Expand Down
100 changes: 83 additions & 17 deletions src/MiniPdf/DocxToPdfConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ internal static PdfDocument Convert(Stream docxStream, ConversionOptions? option
// Adjust top margin when header content is taller than the default header area
if (!options.MarginTopOverride.HasValue && docxDoc.HeaderElements is { Count: > 0 })
{
var headerContentHeight = EstimateElementsHeight(TrimTrailingEmptyParagraphs(docxDoc.HeaderElements), options);
var headerContentHeight = EstimateElementsHeight(TrimTrailingEmptyParagraphs(docxDoc.HeaderElements), options, headerFooter: true);
var headerAreaHeight = options.MarginTop - options.HeaderMargin;
if (headerAreaHeight < 0) headerAreaHeight = 0; // header starts below marginTop
if (headerContentHeight > headerAreaHeight)
Expand All @@ -270,7 +270,7 @@ internal static PdfDocument Convert(Stream docxStream, ConversionOptions? option
// Adjust bottom margin when footer content is taller than the default footer area
if (!options.MarginBottomOverride.HasValue && docxDoc.FooterElements is { Count: > 0 })
{
var footerContentHeight = EstimateElementsHeight(TrimTrailingEmptyParagraphs(docxDoc.FooterElements), options);
var footerContentHeight = EstimateElementsHeight(TrimTrailingEmptyParagraphs(docxDoc.FooterElements), options, headerFooter: true);
var footerTopFromBottom = options.FooterMargin + footerContentHeight;
if (footerTopFromBottom > options.MarginBottom)
options.MarginBottom = footerTopFromBottom;
Expand Down Expand Up @@ -535,7 +535,7 @@ internal static PdfDocument Convert(Stream docxStream, ConversionOptions? option
|| (e is DocxParagraph pp && (pp.Runs.Any(r => !string.IsNullOrWhiteSpace(r.Text)) || pp.Images.Count > 0))))
{
var trimmedFooter = TrimTrailingEmptyParagraphs(pageFooterElements);
var footerContentHeight = EstimateElementsHeight(trimmedFooter, options);
var footerContentHeight = EstimateElementsHeight(trimmedFooter, options, headerFooter: true);
var footerStartY = options.FooterMargin + footerContentHeight;
RenderHeaderFooterElementsOnPage(page, options, trimmedFooter, footerStartY, pi, totalPages, sectionPageNum);
}
Expand Down Expand Up @@ -3530,7 +3530,7 @@ private static List<DocxElement> TrimTrailingEmptyParagraphs(List<DocxElement> e
/// Estimates the total height of a list of elements (paragraphs + tables)
/// for header/footer sizing calculations.
/// </summary>
private static float EstimateElementsHeight(List<DocxElement> elements, ConversionOptions options)
private static float EstimateElementsHeight(List<DocxElement> elements, ConversionOptions options, bool headerFooter = false)
{
const float emuPerPt = 914400f / 72f;
float totalHeight = 0;
Expand All @@ -3550,8 +3550,11 @@ private static float EstimateElementsHeight(List<DocxElement> elements, Conversi
totalHeight += gap;
}

var fontSize = para.FontSize > 0 ? para.FontSize : options.FontSize;
var estFont = para.Runs.FirstOrDefault(r => !string.IsNullOrEmpty(r.FontName))?.FontName;
var firstRun = headerFooter ? para.Runs.FirstOrDefault(r => !string.IsNullOrEmpty(r.Text)) : null;
var fontSize = firstRun?.FontSize > 0 ? firstRun.FontSize
: para.FontSize > 0 ? para.FontSize : options.FontSize;
var estFont = headerFooter ? firstRun?.FontName
: para.Runs.FirstOrDefault(r => !string.IsNullOrEmpty(r.FontName))?.FontName;
var lineHeight = ApplyLineSpacingRule(
para,
fontSize * GetFontMetricsFactor(estFont) * GetLineSpacingMultiple(para, options.LineSpacing));
Expand All @@ -3566,7 +3569,16 @@ private static float EstimateElementsHeight(List<DocxElement> elements, Conversi
}
var text = string.Concat(para.Runs.Select(r => r.Text));
if (!string.IsNullOrEmpty(text))
totalHeight += lineHeight;
{
// Header/footer paragraphs wrap at render time; reserve every line.
// Floating text-box paragraphs (TextBoxWidth > 0) overlay the page and
// must not grow the reserved area, so they keep the one-line estimate.
// Page numbers are unknown here, so placeholders resolve as page 1 of 1.
var lineCount = headerFooter && para.TextBoxWidth <= 0
? WrapHeaderFooterText(para, ResolvePagePlaceholders(text, 1, 1), options, usableW).Count
: 1;
totalHeight += lineHeight * lineCount;
}
else if (para.Images.Count == 0)
totalHeight += lineHeight;

Expand Down Expand Up @@ -3605,6 +3617,52 @@ private static float EstimateElementsHeight(List<DocxElement> elements, Conversi
return totalHeight;
}

/// <summary>
/// Splits a header/footer paragraph into the lines drawn by
/// <see cref="RenderHeaderFooterElementsOnPage"/>. Text that fits is kept verbatim
/// because WordWrap normalizes spacing.
/// </summary>
private static List<string> WrapHeaderFooterText(DocxParagraph para, string text, ConversionOptions options, float areaWidth)
{
var firstRun = para.Runs.FirstOrDefault(r => !string.IsNullOrEmpty(r.Text));
var runFontSize = firstRun?.FontSize > 0 ? firstRun.FontSize : (para.FontSize > 0 ? para.FontSize : options.FontSize);
var bold = firstRun?.Bold ?? false;
var useCalibri = options.UseCalibriWidths
&& (string.IsNullOrEmpty(firstRun?.FontName) || firstRun.FontName.Contains("Calibri", StringComparison.OrdinalIgnoreCase));
s_overrideWidths = GetFontOverrideWidths(firstRun?.FontName);
var lines = EstimateWrapTextWidth(text, runFontSize, bold, 0, useCalibri) <= areaWidth
? [text]
: WordWrap(text, areaWidth, areaWidth, runFontSize, para.TabStops, bold, useCalibriWidths: useCalibri);
var result = new List<string>();
foreach (var line in lines)
{
if (EstimateWrapTextWidth(line, runFontSize, bold, 0, useCalibri) <= areaWidth)
{
result.Add(line);
continue;
}

var segment = "";
var elements = System.Globalization.StringInfo.GetTextElementEnumerator(line);
while (elements.MoveNext())
{
var next = elements.GetTextElement();
if (segment.Length > 0 && Math.Max(
EstimateWrapTextWidth(segment + next, runFontSize, bold, 0, useCalibri),
EstimateTextWidth(segment + next, runFontSize)) > areaWidth)
{
result.Add(segment);
segment = "";
}
segment += next;
}
if (segment.Length > 0)
result.Add(segment);
}
s_overrideWidths = null;
return result;
}

/// <summary>
/// Renders header/footer elements (paragraphs + tables) directly on a page
/// at the specified starting Y position, flowing downward.
Expand Down Expand Up @@ -3692,17 +3750,25 @@ private static void RenderHeaderFooterElementsOnPage(PdfPage page, ConversionOpt
para,
runFontSize * GetFontMetricsFactor(firstRun?.FontName)
* GetLineSpacingMultiple(para, options.LineSpacing));
var textWidth = EstimateTextWidth(text, runFontSize);
var textX = para.Alignment switch
// Text-box paragraphs carry their own text area; others use the margins.
var areaLeft = options.MarginLeft + (para.TextBoxWidth > 0 ? para.IndentLeft : 0);
var areaWidth = para.TextBoxWidth > 0 ? para.TextBoxWidth : usableW;
var bold = firstRun?.Bold ?? false;
foreach (var line in WrapHeaderFooterText(para, text, options, areaWidth))
{
"center" => options.MarginLeft + (usableW - textWidth) / 2,
"right" => options.MarginLeft + usableW - textWidth,
_ => options.MarginLeft
};
y -= runFontSize;
page.AddText(text, textX, y, runFontSize, firstRun?.Color ?? para.Color,
bold: firstRun?.Bold ?? false, italic: firstRun?.Italic ?? false, preferredFontName: firstRun?.FontName);
y -= lineHeight - runFontSize;
var textWidth = EstimateTextWidth(line, runFontSize);
var textX = para.Alignment switch
{
"center" => areaLeft + (areaWidth - textWidth) / 2,
"right" => areaLeft + areaWidth - textWidth,
_ => areaLeft
};
y -= runFontSize;
page.AddText(line, textX, y, runFontSize, firstRun?.Color ?? para.Color,
maxWidth: para.TextBoxWidth > 0 ? areaWidth : null,
bold: bold, italic: firstRun?.Italic ?? false, preferredFontName: firstRun?.FontName);
y -= lineHeight - runFontSize;
}
}
lastSpacingAfter = para.SpacingAfter >= 0 ? para.SpacingAfter : 0f;
isFirst = false;
Expand Down
Loading
Loading