Skip to content

Commit 86308db

Browse files
authored
Release 3.8
2 parents 3071e62 + 3170892 commit 86308db

19 files changed

+559
-113
lines changed

.github/workflows/publish_nuget.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ jobs:
3131
- name: Pack
3232
run: dotnet pack src/Html2OpenXml/HtmlToOpenXml.csproj --configuration Release --output ./nupkg
3333
- name: Push nuget to NuGet.org
34-
run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json
34+
run: dotnet nuget push ./nupkg/*.nupkg --api-key $NUGET_API_KEY -s https://api.nuget.org/v3/index.json
35+
env:
36+
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
3537
- name: Create Release and Upload Artifact to Release
3638
run: gh release create ${{github.ref_name}} -t "Release ${{github.ref_name}}" *.nupkg --generate-notes --draft

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
# Changelog
22

3-
## 3.2.6
3+
## 3.2.8
4+
5+
- Fix a fatal crash when trying to convert multiple images #215
6+
- New feature to allow to reference external image instead of embedding them #216
7+
- Fix a potential issue on image streams that are disposed too early.
8+
- Support table col with percentage width #206
9+
10+
## 3.2.7
411

512
- Fix handling Uri with an anchor #209
613
- New option DefaultStyles.NumberedHeadingStyle to support an alternate heading style #210
714

15+
## 3.2.6
16+
17+
(wrong packaging, same code as 3.2.5)
18+
819
## 3.2.5
920

1021
- Fix a crash with the new whitespace handling introduced in 3.2.3 #191

examples/Demo/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ static async Task Main(string[] args)
2424
// instead of creating it from scratch.
2525
using (var buffer = ResourceHelper.GetStream("Resources.template.docx"))
2626
{
27-
buffer.CopyTo(generatedDocument);
27+
await buffer.CopyToAsync(generatedDocument);
2828
}
2929

3030
generatedDocument.Position = 0L;

src/Html2OpenXml/Configuration enum.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,27 @@ public readonly struct QuoteChars(string begin, string end)
4646

4747
internal string Prefix { get; } = begin;
4848
internal string Suffix { get; } = end;
49-
}
49+
}
50+
51+
/// <summary>
52+
/// Specifies how images should be processed during HTML to OpenXML conversion.
53+
/// </summary>
54+
public enum ImageProcessingMode
55+
{
56+
/// <summary>
57+
/// Downloads and embeds all images into the document (default behaviour).
58+
/// This creates self-contained documents but may result in large file sizes.
59+
/// </summary>
60+
Embed = 0,
61+
/// <summary>
62+
/// Links to external images via external relationships instead of downloading them.
63+
/// This keeps document size small but images won't display offline or if URLs become unavailable.
64+
/// Data URI images (base64 encoded) are still embedded.
65+
/// </summary>
66+
LinkExternal = 1,
67+
/// <summary>
68+
/// Only embeds data URI images (base64 encoded inline images).
69+
/// External images (http/https/file) are skipped entirely.
70+
/// </summary>
71+
EmbedDataUriOnly = 2,
72+
}

src/Html2OpenXml/Expressions/BlockElementExpression.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,14 @@ public override void CascadeStyles(OpenXmlElement element)
143143
return;
144144

145145
var knownTags = new HashSet<string>();
146-
foreach (var prop in props)
146+
foreach (var prop in props.Where(p => !knownTags.Contains(p.LocalName)))
147147
{
148-
if (!knownTags.Contains(prop.LocalName))
149-
knownTags.Add(prop.LocalName);
148+
knownTags.Add(prop.LocalName);
150149
}
151150

152-
foreach (var prop in tableProperties)
151+
foreach (var prop in tableProperties.Where(p => !knownTags.Contains(p.LocalName)))
153152
{
154-
if (!knownTags.Contains(prop.LocalName))
155-
props.AddChild(prop.CloneNode(true));
153+
props.AddChild(prop.CloneNode(true));
156154
}
157155
}
158156
}

src/Html2OpenXml/Expressions/BodyExpression.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,27 @@ namespace HtmlToOpenXml.Expressions;
2626
sealed class BodyExpression(IHtmlElement node, ParagraphStyleId? defaultStyle)
2727
: BlockElementExpression(node, defaultStyle)
2828
{
29+
private const uint PortraitPageWidth = 11906U;
30+
private const uint PortraitPageHeight = 16838U;
2931
private bool shouldRegisterTopBookmark;
32+
private ParsingContext? overridenContext;
3033

3134
public override IEnumerable<OpenXmlElement> Interpret(ParsingContext context)
3235
{
3336
MarkAllBookmarks();
3437

38+
var body = context.MainPart.Document.Body!;
39+
var sectionProperties = body.Descendants<SectionProperties>().LastOrDefault();
40+
if (sectionProperties != null)
41+
{
42+
context.IsLandscape = sectionProperties.GetFirstChild<PageSize>()?.Width?.Value >= PortraitPageHeight;
43+
}
44+
3545
var elements = base.Interpret(context);
3646

3747
if (shouldRegisterTopBookmark && elements.Any())
3848
{
3949
// Check whether it already exists
40-
var body = context.MainPart.Document.Body!;
4150
if (body.Descendants<BookmarkStart>().Where(b => b.Name?.Value == "_top").Any())
4251
{
4352
return elements;
@@ -70,6 +79,7 @@ protected override void ComposeStyles(ParsingContext context)
7079
var sectionProperties = mainPart.Document.Body!.GetFirstChild<SectionProperties>();
7180
if (sectionProperties == null || sectionProperties.GetFirstChild<PageSize>() == null)
7281
{
82+
context.IsLandscape = orientation == PageOrientationValues.Landscape;
7383
mainPart.Document.Body.Append(ChangePageOrientation(orientation));
7484
}
7585
else
@@ -80,6 +90,9 @@ protected override void ComposeStyles(ParsingContext context)
8090
SectionProperties validSectionProp = ChangePageOrientation(orientation);
8191
pageSize?.Remove();
8292
sectionProperties.PrependChild(validSectionProp.GetFirstChild<PageSize>()!.CloneNode(true));
93+
94+
overridenContext = context.CreateChild(this);
95+
overridenContext.IsLandscape = orientation == PageOrientationValues.Landscape;
8396
}
8497
}
8598
}
@@ -101,14 +114,14 @@ protected override void ComposeStyles(ParsingContext context)
101114
/// </summary>
102115
private static SectionProperties ChangePageOrientation(PageOrientationValues orientation)
103116
{
104-
PageSize pageSize = new() { Width = (UInt32Value) 16838U, Height = (UInt32Value) 11906U };
117+
PageSize pageSize = new() { Width = PortraitPageWidth, Height = PortraitPageHeight };
105118
if (orientation == PageOrientationValues.Portrait)
106119
{
107-
(pageSize.Height, pageSize.Width) = (pageSize.Width, pageSize.Height);
120+
pageSize.Orient = orientation;
108121
}
109122
else
110123
{
111-
pageSize.Orient = orientation;
124+
(pageSize.Height, pageSize.Width) = (pageSize.Width, pageSize.Height);
112125
}
113126

114127
return new SectionProperties (

src/Html2OpenXml/Expressions/FigureCaptionExpression.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public override IEnumerable<OpenXmlElement> Interpret (ParsingContext context)
5252
}
5353

5454
//Add the figure number references to the start of the first paragraph.
55-
if(childElements.FirstOrDefault() is Paragraph p)
55+
if(childElements.First() is Paragraph p)
5656
{
5757
var properties = p.GetFirstChild<ParagraphProperties>();
5858
p.InsertAfter(new Run(

src/Html2OpenXml/Expressions/Image/ImageExpression.cs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ class ImageExpression(IHtmlImageElement node) : ImageExpressionBase(node)
9494
preferredSize = ImageHeader.KeepAspectRatio(actualSize, preferredSize);
9595
}
9696

97+
// If size is still empty (e.g., for external linked images), use default dimensions
98+
if (preferredSize.IsEmpty || preferredSize.Width <= 0 || preferredSize.Height <= 0)
99+
{
100+
// Use default size for external images or when size cannot be determined
101+
// Default to a reasonable size (similar to how browsers handle images with unknown dimensions)
102+
preferredSize = new Size(300, 200);
103+
}
104+
97105
long widthInEmus = new Unit(UnitMetric.Pixel, preferredSize.Width).ValueInEmus;
98106
long heightInEmus = new Unit(UnitMetric.Pixel, preferredSize.Height).ValueInEmus;
99107

@@ -103,22 +111,26 @@ class ImageExpression(IHtmlImageElement node) : ImageExpressionBase(node)
103111
new wp.Extent() { Cx = widthInEmus, Cy = heightInEmus },
104112
new wp.EffectExtent() { LeftEdge = 19050L, TopEdge = 0L, RightEdge = 0L, BottomEdge = 0L },
105113
new wp.DocProperties() { Id = drawingObjId, Name = "Picture " + imageObjId, Description = string.Empty },
106-
new wp.NonVisualGraphicFrameDrawingProperties {
114+
new wp.NonVisualGraphicFrameDrawingProperties
115+
{
107116
GraphicFrameLocks = new a.GraphicFrameLocks() { NoChangeAspect = true }
108117
},
109118
new a.Graphic(
110119
new a.GraphicData(
111120
new pic.Picture(
112-
new pic.NonVisualPictureProperties {
113-
NonVisualDrawingProperties = new pic.NonVisualDrawingProperties() {
121+
new pic.NonVisualPictureProperties
122+
{
123+
NonVisualDrawingProperties = new pic.NonVisualDrawingProperties()
124+
{
114125
Id = imageObjId,
115126
Name = DataUri.IsWellFormed(src) ? string.Empty : src,
116-
Description = alt },
127+
Description = alt
128+
},
117129
NonVisualPictureDrawingProperties = new pic.NonVisualPictureDrawingProperties(
118130
new a.PictureLocks() { NoChangeAspect = true, NoChangeArrowheads = true })
119131
},
120132
new pic.BlipFill(
121-
new a.Blip() { Embed = iinfo.ImagePartId },
133+
CreateBlip(iinfo),
122134
new a.SourceRectangle(),
123135
new a.Stretch(
124136
new a.FillRectangle())),
@@ -128,10 +140,14 @@ class ImageExpression(IHtmlImageElement node) : ImageExpressionBase(node)
128140
new a.Extents() { Cx = widthInEmus, Cy = heightInEmus }),
129141
new a.PresetGeometry(
130142
new a.AdjustValueList()
131-
) { Preset = a.ShapeTypeValues.Rectangle }
132-
) { BlackWhiteMode = a.BlackWhiteModeValues.Auto })
133-
) { Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" })
134-
) { DistanceFromTop = (UInt32Value) 0U, DistanceFromBottom = (UInt32Value) 0U, DistanceFromLeft = (UInt32Value) 0U, DistanceFromRight = (UInt32Value) 0U }
143+
)
144+
{ Preset = a.ShapeTypeValues.Rectangle }
145+
)
146+
{ BlackWhiteMode = a.BlackWhiteModeValues.Auto })
147+
)
148+
{ Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" })
149+
)
150+
{ DistanceFromTop = (UInt32Value)0U, DistanceFromBottom = (UInt32Value)0U, DistanceFromLeft = (UInt32Value)0U, DistanceFromRight = (UInt32Value)0U }
135151
);
136152

137153
return img;
@@ -147,11 +163,28 @@ private static int GetDimension(HtmlAttributeCollection styles, string primarySt
147163

148164
if (unit.IsValid)
149165
{
150-
return unit.Type == UnitMetric.Percent?
166+
return unit.Type == UnitMetric.Percent ?
151167
(int)(unit.Value * percentageBase / 100) :
152168
unit.ValueInPx;
153169
}
154170

155171
return 0;
156172
}
157-
}
173+
174+
/// <summary>
175+
/// Creates a Blip element with either an embedded or external image reference.
176+
/// </summary>
177+
private static a.Blip CreateBlip(HtmlImageInfo iinfo)
178+
{
179+
if (iinfo.IsExternal)
180+
{
181+
// Use Link property for external images
182+
return new a.Blip() { Link = iinfo.ImagePartId };
183+
}
184+
else
185+
{
186+
// Use Embed property for embedded images (default behaviour)
187+
return new a.Blip() { Embed = iinfo.ImagePartId };
188+
}
189+
}
190+
}

src/Html2OpenXml/Expressions/Table/TableColExpression.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,33 @@ namespace HtmlToOpenXml.Expressions;
2323
/// </summary>
2424
sealed class TableColExpression(IHtmlTableColumnElement node) : TableElementExpressionBase(node)
2525
{
26+
private const int MaxTablePortraitWidth = 9622;
27+
private const int MaxTableLandscapeWidth = 12996;
2628
private readonly IHtmlTableColumnElement colNode = node;
29+
private double? percentWidth;
2730

2831

2932
/// <inheritdoc/>
30-
public override IEnumerable<OpenXmlElement> Interpret (ParsingContext context)
33+
public override IEnumerable<OpenXmlElement> Interpret(ParsingContext context)
3134
{
3235
ComposeStyles(context);
3336

3437
var column = new GridColumn();
3538
var width = styleAttributes!.GetUnit("width");
36-
if (width.IsValid && width.IsFixed)
39+
if (width.IsValid)
3740
{
38-
// This value is specified in twentieths of a point.
39-
// If this attribute is omitted, then the last saved width of the grid column is assumed to be zero.
40-
column.Width = Math.Round( width.ValueInPoint * 20 ).ToString(CultureInfo.InvariantCulture);
41+
if (width.IsFixed)
42+
{
43+
// This value is specified in twentieths of a point.
44+
// If this attribute is omitted, then the last saved width of the grid column is assumed to be zero.
45+
column.Width = Math.Round(width.ValueInPoint * 20).ToString(CultureInfo.InvariantCulture);
46+
}
47+
else if (width.Type == UnitMetric.Percent)
48+
{
49+
var maxWidth = context.IsLandscape ? MaxTableLandscapeWidth : MaxTablePortraitWidth;
50+
percentWidth = Math.Max(0, Math.Min(100, width.Value));
51+
column.Width = Math.Ceiling(maxWidth / 100d * percentWidth.Value).ToString(CultureInfo.InvariantCulture);
52+
}
4153
}
4254

4355
if (colNode.Span == 0)
@@ -51,4 +63,18 @@ public override IEnumerable<OpenXmlElement> Interpret (ParsingContext context)
5163

5264
return elements;
5365
}
66+
67+
public override void CascadeStyles(OpenXmlElement element)
68+
{
69+
base.CascadeStyles(element);
70+
71+
if (percentWidth.HasValue && element is TableCell cell &&
72+
cell.TableCellProperties?.TableCellWidth is null)
73+
{
74+
cell.TableCellProperties!.TableCellWidth = new() {
75+
Type = TableWidthUnitValues.Pct,
76+
Width = ((int) (percentWidth.Value * 50)).ToString(CultureInfo.InvariantCulture)
77+
};
78+
}
79+
}
5480
}

src/Html2OpenXml/Expressions/TextExpression.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,11 @@ public override IEnumerable<OpenXmlElement> Interpret (ParsingContext context)
9696
}
9797

9898
// if previous element is an image, append a space separator
99-
// if this is a non-empty phrasing element, append a space separator
100-
if (startsWithSpace && node.PreviousSibling is IHtmlImageElement)
101-
{
102-
text = " " + text;
103-
}
104-
else if (startsWithSpace && prevIsPhrasing
99+
if ((startsWithSpace && node.PreviousSibling is IHtmlImageElement)
100+
// if this is a non-empty phrasing element, append a space separator
101+
|| (startsWithSpace && prevIsPhrasing
105102
&& node.PreviousSibling!.TextContent.Length > 0
106-
&& !node.PreviousSibling!.TextContent[node.PreviousSibling.TextContent.Length - 1].IsWhiteSpaceCharacter())
103+
&& !node.PreviousSibling!.TextContent[node.PreviousSibling.TextContent.Length - 1].IsWhiteSpaceCharacter()))
107104
{
108105
text = " " + text;
109106
}

0 commit comments

Comments
 (0)