Skip to content

Commit dc2ea2d

Browse files
committed
feat: Enhance DOCX to PDF conversion with text box fill and italic support
- Added support for reading and rendering text box fill colors from DOCX shapes. - Updated DocxFloatingTextBox to include a fill color property. - Enhanced bullet character mapping to support italic font variants. - Modified PDF text rendering to accommodate italic text alongside bold. - Introduced overlay rendering for text and rectangles to ensure proper layering in PDF output. - Updated PdfWriter to handle italic font variants and ensure correct font embedding.
1 parent e098502 commit dc2ea2d

5 files changed

Lines changed: 268 additions & 81 deletions

File tree

src/MiniPdf/DocxReader.cs

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,20 @@ internal static DocxDocument Read(Stream stream)
170170
extentWidthPt = cx / 914400f * 72f;
171171
}
172172

173-
// Read text box outline (border) from shape properties
173+
// Read text box outline (border) and fill from shape properties
174174
DocxTextBoxBorder? textBoxBorder = null;
175+
PdfColor? textBoxFillColor = null;
175176
var wsp = anchor.Descendants(WPS + "wsp").FirstOrDefault();
176177
if (wsp != null)
177178
{
178179
var spPr = wsp.Element(WPS + "spPr") ?? wsp.Element(A + "spPr");
180+
// Parse shape fill (background)
181+
var shapeFill = spPr?.Element(A + "solidFill");
182+
if (shapeFill != null)
183+
{
184+
var (fc, _) = ResolveSolidFill(shapeFill, themeColors);
185+
textBoxFillColor = fc;
186+
}
179187
var ln = spPr?.Element(A + "ln");
180188
if (ln != null)
181189
{
@@ -242,7 +250,7 @@ internal static DocxDocument Read(Stream stream)
242250
if (floatingParas.Count > 0)
243251
{
244252
floatingTextBoxes ??= new List<DocxFloatingTextBox>();
245-
floatingTextBoxes.Add(new DocxFloatingTextBox(anchorXPt, anchorOffsetPt, extentWidthPt, extentHeightPt, floatingParas, textBoxBorder, hRelativeFrom, vRelativeFrom));
253+
floatingTextBoxes.Add(new DocxFloatingTextBox(anchorXPt, anchorOffsetPt, extentWidthPt, extentHeightPt, floatingParas, textBoxBorder, hRelativeFrom, vRelativeFrom, textBoxFillColor));
246254
}
247255
}
248256
else
@@ -490,7 +498,8 @@ internal static DocxDocument Read(Stream stream)
490498
if (numDef.Format == "bullet")
491499
{
492500
isBulletList = true;
493-
listText = "\u2022"; // bullet character
501+
var lvlDef2 = numDef.Levels.FirstOrDefault(l => l.Ilvl == listLevel) ?? numDef.Levels.FirstOrDefault();
502+
listText = MapBulletChar(lvlDef2?.LvlText, lvlDef2?.FontName);
494503
}
495504
else
496505
{
@@ -2758,6 +2767,51 @@ private static (string? MajorLatinFont, string? MinorLatinFont, Dictionary<strin
27582767
return null;
27592768
}
27602769

2770+
/// <summary>
2771+
/// Maps a bullet character from Wingdings/Symbol font encoding to a Unicode equivalent.
2772+
/// </summary>
2773+
private static string MapBulletChar(string? lvlText, string? fontName)
2774+
{
2775+
if (string.IsNullOrEmpty(lvlText))
2776+
return "\u2022"; // fallback bullet
2777+
2778+
var ch = lvlText[0];
2779+
2780+
if (fontName != null && fontName.Contains("Wingdings", StringComparison.OrdinalIgnoreCase))
2781+
{
2782+
// Wingdings PUA → Unicode mappings (common bullets)
2783+
return ch switch
2784+
{
2785+
'\uf0d8' => "\u27A2", // ➢ right arrowhead
2786+
'\uf0a7' => "\u25AA", // ▪ small black square
2787+
'\uf0a8' => "\u25CB", // ○ white circle
2788+
'\uf076' => "\u2756", // ❖ black diamond minus white X
2789+
'\uf0FC' => "\u2714", // ✔ check mark
2790+
'\uf0FB' => "\u2718", // ✘ cross mark
2791+
'\uf0E8' => "\u25BA", // ► right-pointing triangle
2792+
'\uf0D2' => "\u27A4", // ➤ right arrowhead (filled)
2793+
_ => "\u2022", // fallback
2794+
};
2795+
}
2796+
2797+
if (fontName != null && fontName.Contains("Symbol", StringComparison.OrdinalIgnoreCase))
2798+
{
2799+
return ch switch
2800+
{
2801+
'\uf0b7' => "\u2022", // • bullet
2802+
'\uf0a7' => "\u2666", // ♦ diamond
2803+
'\uf0B0' => "\u2218", // ∘ ring operator
2804+
_ => "\u2022",
2805+
};
2806+
}
2807+
2808+
// For standard fonts, use the character as-is if printable
2809+
if (ch >= ' ')
2810+
return lvlText;
2811+
2812+
return "\u2022"; // fallback
2813+
}
2814+
27612815
private static Dictionary<string, DocxNumberingDef> ReadNumbering(ZipArchive archive)
27622816
{
27632817
var result = new Dictionary<string, DocxNumberingDef>();
@@ -2791,7 +2845,9 @@ private static Dictionary<string, DocxNumberingDef> ReadNumbering(ZipArchive arc
27912845
if (int.TryParse(lvlInd.Attribute(W + "hanging")?.Value, out var lh))
27922846
lvlHanging = lh / 20f;
27932847
}
2794-
levels.Add(new DocxNumberingLevelDef(ilvl, numFmt, lvlText, startVal, lvlIndentLeft, lvlHanging));
2848+
// Read bullet font name from rPr/rFonts (e.g. Wingdings, Symbol)
2849+
var lvlFontName = lvl.Element(W + "rPr")?.Element(W + "rFonts")?.Attribute(W + "ascii")?.Value;
2850+
levels.Add(new DocxNumberingLevelDef(ilvl, numFmt, lvlText, startVal, lvlIndentLeft, lvlHanging, lvlFontName));
27952851
}
27962852
abstractDefs[absId] = levels;
27972853
}
@@ -2957,7 +3013,8 @@ internal sealed record DocxFloatingTextBox(
29573013
List<DocxParagraph> Paragraphs,
29583014
DocxTextBoxBorder? Border = null,
29593015
string HRelativeFrom = "column",
2960-
string VRelativeFrom = "paragraph"
3016+
string VRelativeFrom = "paragraph",
3017+
PdfColor? FillColor = null
29613018
);
29623019

29633020
/// <summary>Represents a text box outline border (rectangle drawn around text box content).</summary>
@@ -3157,4 +3214,4 @@ private static string ToRoman(int num)
31573214
}
31583215
}
31593216

3160-
internal sealed record DocxNumberingLevelDef(int Ilvl, string NumFmt, string LvlText, int Start, float IndentLeft = 0, float Hanging = 0);
3217+
internal sealed record DocxNumberingLevelDef(int Ilvl, string NumFmt, string LvlText, int Start, float IndentLeft = 0, float Hanging = 0, string? FontName = null);

0 commit comments

Comments
 (0)