diff --git a/.github/workflows/publish_nuget.yml b/.github/workflows/publish_nuget.yml
index 4289dd9..5759fe7 100644
--- a/.github/workflows/publish_nuget.yml
+++ b/.github/workflows/publish_nuget.yml
@@ -32,6 +32,17 @@ jobs:
runs-on: ubuntu-latest
steps:
+ # validate the input.version as Github do not yet support regex validation
+ - name: Validate version input
+ run: |
+ REGEXP="^[1-9]{1,2}.[0-9]+.[0-9]+(-beta\d+)?$"
+ if [[ ! "$UNTRUSTED_INPUT" =~ $REGEXP ]]; then
+ echo "::error:: Invalid version format"
+ exit 1
+ fi
+ env:
+ UNTRUSTED_INPUT: ${{ github.event.inputs.version }}
+
- uses: actions/checkout@v6
# Only validate tag is on master for auto-triggered runs
@@ -57,14 +68,16 @@ jobs:
- name: Build
run: |
dotnet build src/Html2OpenXml/HtmlToOpenXml.csproj \
- --configuration Release /p:SourceRevisionId=${{ github.sha }}
+ --configuration Release /p:SourceRevisionId=${{ github.sha }} \
+ /p:ContinuousIntegrationBuild=true /p:Deterministic=true \
- name: Pack NuGet Package
run: |
dotnet pack src/Html2OpenXml/HtmlToOpenXml.csproj \
--configuration Release \
--output ./nupkg \
- -- /p:PackageVersion=${{ inputs.version }}
+ /p:PackageVersion=${{ inputs.version }}
+
- name: Verify Package Created
run: |
if [ ! -f ./nupkg/*.nupkg ]; then
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24566d8..18e815a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## 3.5.0
+
+- Support css margin/padding inline/block
+- Bump AngleSharp to 1.5 due to CVE
+
## 3.4.0
- Numbering list now support `list-style-type: dash`
diff --git a/Directory.Build.props b/Directory.Build.props
index bb7f8b1..8dd648a 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,7 @@
enable
latest
enable
- 3.4.1
+ 3.5.0
diff --git a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
index 340ae61..d8e6652 100755
--- a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
+++ b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
@@ -177,18 +177,32 @@ public Margin GetMargin(string name)
// shortcut to avoid resolving each individual side when we know this collection is empty
if (IsEmpty) return margin;
+ Unit u;
+
if (attributes.TryGetValue(name, out var range))
margin = Margin.Parse(rawValue.AsSpan().Slice(range));
-
- Unit u;
+ if (attributes.TryGetValue(name + "-inline", out range))
+ {
+ u = Unit.Parse(rawValue.AsSpan().Slice(range));
+ if (u.IsValid) margin.Left = margin.Right = u;
+ }
+ if (attributes.TryGetValue(name + "-block", out range))
+ {
+ u = Unit.Parse(rawValue.AsSpan().Slice(range));
+ if (u.IsValid) margin.Top = margin.Bottom = u;
+ }
u = GetUnit(name + "-top", UnitMetric.Pixel);
+ if (!u.IsValid) u = GetUnit(name + "-block-start");
if (u.IsValid) margin.Top = u;
u = GetUnit(name + "-right", UnitMetric.Pixel);
+ if (!u.IsValid) u = GetUnit(name + "-inline-end");
if (u.IsValid) margin.Right = u;
u = GetUnit(name + "-bottom", UnitMetric.Pixel);
+ if (!u.IsValid) u = GetUnit(name + "-block-end");
if (u.IsValid) margin.Bottom = u;
u = GetUnit(name + "-left", UnitMetric.Pixel);
+ if (!u.IsValid) u = GetUnit(name + "-inline-start");
if (u.IsValid) margin.Left = u;
return margin;
diff --git a/src/Html2OpenXml/Expressions/BlockElementExpression.cs b/src/Html2OpenXml/Expressions/BlockElementExpression.cs
index b1607c6..2ee2178 100644
--- a/src/Html2OpenXml/Expressions/BlockElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/BlockElementExpression.cs
@@ -324,7 +324,7 @@ internal static IEnumerable ComposeChildren(ParsingContext conte
foreach (var element in expression.Interpret(context))
{
context.CascadeStyles(element);
- if (element is Run r || element is Hyperlink)
+ if (element is Run || element is Hyperlink)
{
runs.Add(element);
continue;
diff --git a/src/Html2OpenXml/Expressions/BodyExpression.cs b/src/Html2OpenXml/Expressions/BodyExpression.cs
index 5b643c3..b372628 100644
--- a/src/Html2OpenXml/Expressions/BodyExpression.cs
+++ b/src/Html2OpenXml/Expressions/BodyExpression.cs
@@ -27,8 +27,8 @@ sealed class BodyExpression(IHtmlElement node, ParagraphStyleId? defaultStyle)
private const uint PortraitPageWidth = 11906U;
private const uint PortraitPageHeight = 16838U;
private bool shouldRegisterTopBookmark;
- private ParsingContext? overridenContext;
-
+
+
public override IEnumerable Interpret(ParsingContext context)
{
MarkAllBookmarks();
@@ -89,9 +89,6 @@ protected override void ComposeStyles(ParsingContext context)
SectionProperties validSectionProp = ChangePageOrientation(orientation);
pageSize?.Remove();
sectionProperties.PrependChild(validSectionProp.GetFirstChild()!.CloneNode(true));
-
- overridenContext = context.CreateChild(this);
- overridenContext.IsLandscape = orientation == PageOrientationValues.Landscape;
}
}
}
diff --git a/src/Html2OpenXml/Expressions/HtmlDomExpression.cs b/src/Html2OpenXml/Expressions/HtmlDomExpression.cs
index 0cffb79..f058570 100644
--- a/src/Html2OpenXml/Expressions/HtmlDomExpression.cs
+++ b/src/Html2OpenXml/Expressions/HtmlDomExpression.cs
@@ -30,7 +30,7 @@ private static Dictionary> InitKnownTa
{
// A complete list of HTML tags can be found here: http://www.w3schools.com/tags/default.asp
- var knownTags = new Dictionary>(StringComparer.InvariantCultureIgnoreCase) {
+ return new Dictionary>(StringComparer.InvariantCultureIgnoreCase) {
{ TagNames.A, el => new HyperlinkExpression((IHtmlAnchorElement) el) },
{ TagNames.Abbr, el => new AbbreviationExpression((IHtmlElement) el) },
{ "acronym", el => new AbbreviationExpression((IHtmlElement) el) },
@@ -82,8 +82,6 @@ private static Dictionary> InitKnownTa
{ TagNames.Ul, el => new ListExpression((IHtmlElement) el) },
{ TagNames.Var, el => new PhrasingElementExpression((IHtmlElement) el) }
};
-
- return knownTags;
}
///
diff --git a/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs b/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
index c0b415f..630faff 100644
--- a/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
@@ -106,32 +106,34 @@ internal static (uint imageObjId, uint drawingObjId) IncrementDrawingObjId(Parsi
imageObjId ??= 1;
var mainPart = context.MainPart;
- foreach (var part in new[] {
+ foreach (var part in new[] {
mainPart.Document!.Body!.Descendants(),
mainPart.HeaderParts.Where(f => f.Header != null).SelectMany(f => f.Header!.Descendants()),
mainPart.FooterParts.Where(f => f.Footer != null).SelectMany(f => f.Footer!.Descendants())
})
- foreach (Drawing d in part)
{
- wp.DocProperties? docProperties = null;
- pic.NonVisualPictureProperties? nvPr = null;
-
- if (d.Anchor != null)
- {
- docProperties = d.Anchor.GetFirstChild();
- nvPr = d.Anchor.GetFirstChild()?.GraphicData?.GetFirstChild()?.GetFirstChild();
- }
- else if (d.Inline != null)
+ foreach (Drawing d in part)
{
- docProperties = d.Inline!.DocProperties;
- nvPr = d.Inline!.Graphic?.GraphicData?.GetFirstChild();
+ wp.DocProperties? docProperties = null;
+ pic.NonVisualPictureProperties? nvPr = null;
+
+ if (d.Anchor != null)
+ {
+ docProperties = d.Anchor.GetFirstChild();
+ nvPr = d.Anchor.GetFirstChild()?.GraphicData?.GetFirstChild()?.GetFirstChild();
+ }
+ else if (d.Inline != null)
+ {
+ docProperties = d.Inline!.DocProperties;
+ nvPr = d.Inline!.Graphic?.GraphicData?.GetFirstChild();
+ }
+
+ if (docProperties?.Id != null && docProperties.Id.Value > drawingObjId)
+ drawingObjId = docProperties.Id.Value;
+
+ if (nvPr != null && nvPr.NonVisualDrawingProperties?.Id?.Value > imageObjId)
+ imageObjId = nvPr.NonVisualDrawingProperties.Id;
}
-
- if (docProperties?.Id != null && docProperties.Id.Value > drawingObjId)
- drawingObjId = docProperties.Id.Value;
-
- if (nvPr != null && nvPr.NonVisualDrawingProperties?.Id?.Value > imageObjId)
- imageObjId = nvPr.NonVisualDrawingProperties.Id;
}
}
diff --git a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
index 199d0ae..557e41f 100644
--- a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
@@ -167,7 +167,7 @@ private ListContext ConcretiseInstance(ParsingContext context, int abstractNumId
overrideLevelIndex = currentLevel;
listContext = new ListContext(listStyle, abstractNumId, instanceId.Value, currentLevel + 1, dir);
}
- else if (!instanceId.HasValue || context.Converter.ContinueNumbering == false)
+ else if (!instanceId.HasValue || !context.Converter.ContinueNumbering)
{
// create a new instance of that list template
instanceId = IncrementInstanceId(context, abstractNumId, isReusable: context.Converter.ContinueNumbering);
diff --git a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
index 95c953c..9837537 100644
--- a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
@@ -191,7 +191,6 @@ private void InitNumberingIds(ParsingContext context)
knownAbsNumIds = [];
knownInstanceIds = [];
- int absNumIdRef = 0;
NumberingDefinitionsPart numberingPart = context.MainPart.NumberingDefinitionsPart
?? context.MainPart.AddNewPart();
@@ -201,21 +200,11 @@ private void InitNumberingIds(ParsingContext context)
}
var numbering = numberingPart.Numbering!;
-
- // The absNumIdRef Id is a required field and should be unique. We will loop through the existing Numbering definition
- // to retrieve the highest Id and reconstruct our own list definition template.
- foreach (var abs in numbering.Elements())
- {
- if (abs.AbstractNumberId != null && abs.AbstractNumberId > absNumIdRef)
- absNumIdRef = abs.AbstractNumberId;
- }
- absNumIdRef++;
-
IEnumerable existingAbstractNums = numbering.ChildElements
- .Where(e => e != null && e is AbstractNum).Cast();
+ .OfType();
knownAbsNumIds = existingAbstractNums
- .Where(a => a.AbstractNumDefinitionName != null && a.AbstractNumDefinitionName.Val != null)
+ .Where(a => a.AbstractNumDefinitionName?.Val != null)
.ToDictionary(a => a.AbstractNumDefinitionName!.Val!.Value!, a => a.AbstractNumberId!.Value);
foreach (NumberingInstance inst in numbering.Elements())
@@ -239,7 +228,7 @@ private AbstractNum CreateCustomBulletAbstractNum(string customSymbol)
AbstractNumDefinitionName = new() { Val = customSymbol },
MultiLevelType = new() { Val = MultiLevelValues.HybridMultilevel }
};
-
+
for (var lvlIndex = 0; lvlIndex <= MaxLevel; lvlIndex++)
{
abstractNum.Append(new Level {
diff --git a/src/Html2OpenXml/Expressions/Table/TableExpression.cs b/src/Html2OpenXml/Expressions/Table/TableExpression.cs
index abcf010..44c2ebb 100644
--- a/src/Html2OpenXml/Expressions/Table/TableExpression.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableExpression.cs
@@ -143,7 +143,7 @@ private static int GuessColumnsCount(IHtmlTableElement tableNode)
for(int i = 0; i < rows.Length; i++)
{
- foreach (var cell in rowNodes.ElementAt(i).Cells)
+ foreach (var cell in rowNodes[i].Cells)
{
var colSpan = Math.Max(1, cell.ColumnSpan);
for (int r = i; r < i + cell.RowSpan; r++)
diff --git a/src/Html2OpenXml/HtmlConverter.cs b/src/Html2OpenXml/HtmlConverter.cs
index 792353c..1833da3 100755
--- a/src/Html2OpenXml/HtmlConverter.cs
+++ b/src/Html2OpenXml/HtmlConverter.cs
@@ -30,15 +30,6 @@ public partial class HtmlConverter
private readonly IWebRequest webRequester;
- ///
- /// Constructor.
- ///
- /// The mainDocumentPart of a document where to write the conversion to.
- /// We preload some configuration from inside the document such as style, bookmarks,...
- public HtmlConverter(MainDocumentPart mainPart) : this(mainPart, null)
- {
- }
-
///
/// Constructor.
///
diff --git a/src/Html2OpenXml/HtmlToOpenXml.csproj b/src/Html2OpenXml/HtmlToOpenXml.csproj
index 4294ecd..7551b31 100644
--- a/src/Html2OpenXml/HtmlToOpenXml.csproj
+++ b/src/Html2OpenXml/HtmlToOpenXml.csproj
@@ -17,8 +17,6 @@
A library to convert simple or advanced html to plain OpenXml document
Olivier Nizet
GIT
- true
- embedded
$(NoWarn);CS8981
@@ -41,7 +39,7 @@
-
+
@@ -50,14 +48,9 @@
false
- true
- snupkg
+ portable
+ true
true
- true
-
-
-
- true
\ No newline at end of file
diff --git a/src/Html2OpenXml/IO/ImageHeader.cs b/src/Html2OpenXml/IO/ImageHeader.cs
index 013da2a..5bf5464 100755
--- a/src/Html2OpenXml/IO/ImageHeader.cs
+++ b/src/Html2OpenXml/IO/ImageHeader.cs
@@ -157,7 +157,7 @@ private static FileType DetectFileType (SequentialBinaryReader reader)
private static Size DecodeBitmap(SequentialBinaryReader reader)
{
- var magicNumber = reader.ReadUInt16();
+ var _ = reader.ReadUInt16();
// skip past the rest of the file header
reader.Skip(4 + 2 + 2 + 4);
@@ -200,7 +200,7 @@ private static Size DecodeGif(SequentialBinaryReader reader)
private static Size DecodeJfif(SequentialBinaryReader reader)
{
reader.IsBigEndian = true;
- var magicNumber = reader.ReadUInt16(); // first two bytes should be JPEG magic number
+ var _ = reader.ReadUInt16(); // first two bytes should be JPEG magic number
do
{
diff --git a/src/Html2OpenXml/IO/ImagePrefetcher.cs b/src/Html2OpenXml/IO/ImagePrefetcher.cs
index 36f21d3..459bd80 100644
--- a/src/Html2OpenXml/IO/ImagePrefetcher.cs
+++ b/src/Html2OpenXml/IO/ImagePrefetcher.cs
@@ -25,26 +25,9 @@ interface IImageLoader
///
/// Download and provison the metadata of a requested image.
///
-sealed class ImagePrefetcher : IImageLoader
+sealed class ImagePrefetcher : ImagePrefetcherBase, IImageLoader
where T: OpenXmlPartContainer, ISupportedRelationship
{
- // Map extension to PartTypeInfo
- private static readonly Dictionary knownExtensions = new(StringComparer.OrdinalIgnoreCase) {
- { ".gif", ImagePartType.Gif },
- { ".bmp", ImagePartType.Bmp },
- { ".emf", ImagePartType.Emf },
- { ".ico", ImagePartType.Icon },
- { ".jp2", ImagePartType.Jp2 },
- { ".jpeg", ImagePartType.Jpeg },
- { ".jpg", ImagePartType.Jpeg },
- { ".jpe", ImagePartType.Jpeg },
- { ".pcx", ImagePartType.Pcx },
- { ".png", ImagePartType.Png },
- { ".svg", ImagePartType.Svg },
- { ".tif", ImagePartType.Tif },
- { ".tiff", ImagePartType.Tiff },
- { ".wmf", ImagePartType.Wmf }
- };
private readonly T hostingPart;
private readonly IWebRequest resourceLoader;
private readonly ConcurrentDictionary prefetchedImages;
@@ -137,7 +120,7 @@ public ImagePrefetcher(T hostingPart, IWebRequest resourceLoader, ImageProcessin
return null;
}
- return SaveImageAssert(src, type, response.Content.CopyTo);
+ return SaveImageAssert(type, response.Content.CopyTo);
}
///
@@ -188,15 +171,15 @@ public ImagePrefetcher(T hostingPart, IWebRequest resourceLoader, ImageProcessin
{
if (DataUri.TryCreate(src, out var dataUri))
{
- knownContentType.TryGetValue(dataUri!.Mime, out PartTypeInfo type);
+ TryInspectMimeType(dataUri!.Mime, out PartTypeInfo type);
- return SaveImageAssert(src, type, stream => stream.Write(dataUri.Data, 0, dataUri.Data.Length));
+ return SaveImageAssert(type, stream => stream.Write(dataUri.Data, 0, dataUri.Data.Length));
}
return null;
}
- private HtmlImageInfo SaveImageAssert(string src, PartTypeInfo type, Action writeImage)
+ private HtmlImageInfo SaveImageAssert(PartTypeInfo type, Action writeImage)
{
ImagePart ipart;
string relationshipId = "img_" + Guid.NewGuid().ToString("N");
@@ -220,98 +203,4 @@ private HtmlImageInfo SaveImageAssert(string src, PartTypeInfo type, Action knownContentType = new(StringComparer.OrdinalIgnoreCase) {
- { "image/gif", ImagePartType.Gif },
- { "image/pjpeg", ImagePartType.Jpeg },
- { "image/jp2", ImagePartType.Jp2 },
- { "image/jpg", ImagePartType.Jpeg },
- { "image/jpeg", ImagePartType.Jpeg },
- { "image/x-png", ImagePartType.Png },
- { "image/png", ImagePartType.Png },
- { "image/tiff", ImagePartType.Tiff },
- { "image/emf", ImagePartType.Emf },
- { "image/x-emf", ImagePartType.Emf },
- { "image/vnd.microsoft.icon", ImagePartType.Icon },
- // these icons mime type are wrong but we should nevertheless take care (http://en.wikipedia.org/wiki/ICO_%28file_format%29#MIME_type)
- { "image/x-icon", ImagePartType.Icon },
- { "image/icon", ImagePartType.Icon },
- { "image/ico", ImagePartType.Icon },
- { "text/ico", ImagePartType.Icon },
- { "text/application-ico", ImagePartType.Icon },
- { "image/bmp", ImagePartType.Bmp },
- { "image/svg+xml", ImagePartType.Svg },
- };
-
- ///
- /// Inspect the response headers of a web request and decode the mime type if provided
- ///
- /// Returns the extension of the image if provideds.
- private static bool TryInspectMimeType(string? contentType, out PartTypeInfo type)
- {
- // can be null when the protocol used doesn't allow response headers
- if (contentType != null &&
- knownContentType.TryGetValue(contentType, out type))
- return true;
-
- type = default;
- return false;
- }
-
- ///
- /// Gets the OpenXml PartTypeInfo associated to an image.
- ///
- private static bool TryGuessTypeFromUri(Uri uri, out PartTypeInfo type)
- {
- string extension = Path.GetExtension(uri.IsAbsoluteUri ? uri.Segments[uri.Segments.Length - 1] : uri.OriginalString);
- if (knownExtensions.TryGetValue(extension, out type)) return true;
-
- // extension not recognized, try with checking the query string. Expecting to resolve something like:
- // ./image.axd?picture=img1.jpg
- extension = Path.GetExtension(uri.IsAbsoluteUri ? uri.AbsoluteUri : uri.ToString());
- if (knownExtensions.TryGetValue(extension, out type)) return true;
-
- return false;
- }
-
- ///
- /// Gets the OpenXml PartTypeInfo associated to an image.
- ///
- private static bool TryGuessTypeFromStream(Stream stream, out PartTypeInfo type)
- {
- if (ImageHeader.TryDetectFileType(stream, out ImageHeader.FileType guessType))
- {
- switch (guessType)
- {
- case ImageHeader.FileType.Bitmap: type = ImagePartType.Bmp; return true;
- case ImageHeader.FileType.Emf: type = ImagePartType.Emf; return true;
- case ImageHeader.FileType.Gif: type = ImagePartType.Gif; return true;
- case ImageHeader.FileType.Jpeg: type = ImagePartType.Jpeg; return true;
- case ImageHeader.FileType.Png: type = ImagePartType.Png; return true;
- }
- }
- type = ImagePartType.Bmp;
- return false;
- }
-
- ///
- /// Loads an image from a stream and grab its size.
- ///
- private static Size GetImageSize(Stream imageStream)
- {
- // Read only the size of the image
- try
- {
- return ImageHeader.GetDimensions(imageStream);
- }
- catch (ArgumentException)
- {
- return Size.Empty;
- }
- }
}
diff --git a/src/Html2OpenXml/IO/ImagePrefetcherBase.cs b/src/Html2OpenXml/IO/ImagePrefetcherBase.cs
new file mode 100644
index 0000000..93e0020
--- /dev/null
+++ b/src/Html2OpenXml/IO/ImagePrefetcherBase.cs
@@ -0,0 +1,127 @@
+/* Copyright (C) Olivier Nizet https://github.com/onizet/html2openxml - All Rights Reserved
+ *
+ * This source is subject to the Microsoft Permissive License.
+ * Please see the License.txt file for more information.
+ * All other rights reserved.
+ *
+ * THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
+ * KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ */
+using DocumentFormat.OpenXml.Packaging;
+
+namespace HtmlToOpenXml.IO;
+
+///
+/// Download and provison the metadata of a requested image.
+///
+abstract class ImagePrefetcherBase
+{
+ // Map extension to PartTypeInfo
+ private static readonly Dictionary knownExtensions = new(StringComparer.OrdinalIgnoreCase) {
+ { ".gif", ImagePartType.Gif },
+ { ".bmp", ImagePartType.Bmp },
+ { ".emf", ImagePartType.Emf },
+ { ".ico", ImagePartType.Icon },
+ { ".jp2", ImagePartType.Jp2 },
+ { ".jpeg", ImagePartType.Jpeg },
+ { ".jpg", ImagePartType.Jpeg },
+ { ".jpe", ImagePartType.Jpeg },
+ { ".pcx", ImagePartType.Pcx },
+ { ".png", ImagePartType.Png },
+ { ".svg", ImagePartType.Svg },
+ { ".tif", ImagePartType.Tif },
+ { ".tiff", ImagePartType.Tiff },
+ { ".wmf", ImagePartType.Wmf }
+ };
+
+ // http://stackoverflow.com/questions/58510/using-net-how-can-you-find-the-mime-type-of-a-file-based-on-the-file-signature
+ private static readonly Dictionary knownContentType = new(StringComparer.OrdinalIgnoreCase) {
+ { "image/gif", ImagePartType.Gif },
+ { "image/pjpeg", ImagePartType.Jpeg },
+ { "image/jp2", ImagePartType.Jp2 },
+ { "image/jpg", ImagePartType.Jpeg },
+ { "image/jpeg", ImagePartType.Jpeg },
+ { "image/x-png", ImagePartType.Png },
+ { "image/png", ImagePartType.Png },
+ { "image/tiff", ImagePartType.Tiff },
+ { "image/emf", ImagePartType.Emf },
+ { "image/x-emf", ImagePartType.Emf },
+ { "image/vnd.microsoft.icon", ImagePartType.Icon },
+ // these icons mime type are wrong but we should nevertheless take care (http://en.wikipedia.org/wiki/ICO_%28file_format%29#MIME_type)
+ { "image/x-icon", ImagePartType.Icon },
+ { "image/icon", ImagePartType.Icon },
+ { "image/ico", ImagePartType.Icon },
+ { "text/ico", ImagePartType.Icon },
+ { "text/application-ico", ImagePartType.Icon },
+ { "image/bmp", ImagePartType.Bmp },
+ { "image/svg+xml", ImagePartType.Svg },
+ };
+
+ ///
+ /// Inspect the response headers of a web request and decode the mime type if provided
+ ///
+ /// Returns the extension of the image if provideds.
+ protected static bool TryInspectMimeType(string? contentType, out PartTypeInfo type)
+ {
+ // can be null when the protocol used doesn't allow response headers
+ if (contentType != null && knownContentType.TryGetValue(contentType, out type))
+ return true;
+
+ type = default;
+ return false;
+ }
+
+ ///
+ /// Gets the OpenXml PartTypeInfo associated to an image.
+ ///
+ protected static bool TryGuessTypeFromUri(Uri uri, out PartTypeInfo type)
+ {
+ string extension = Path.GetExtension(uri.IsAbsoluteUri ? uri.Segments[uri.Segments.Length - 1] : uri.OriginalString);
+ if (knownExtensions.TryGetValue(extension, out type)) return true;
+
+ // extension not recognized, try with checking the query string. Expecting to resolve something like:
+ // ./image.axd?picture=img1.jpg
+ extension = Path.GetExtension(uri.IsAbsoluteUri ? uri.AbsoluteUri : uri.ToString());
+ if (knownExtensions.TryGetValue(extension, out type)) return true;
+
+ return false;
+ }
+
+ ///
+ /// Gets the OpenXml PartTypeInfo associated to an image.
+ ///
+ protected static bool TryGuessTypeFromStream(Stream stream, out PartTypeInfo type)
+ {
+ if (ImageHeader.TryDetectFileType(stream, out ImageHeader.FileType guessType))
+ {
+ switch (guessType)
+ {
+ case ImageHeader.FileType.Bitmap: type = ImagePartType.Bmp; return true;
+ case ImageHeader.FileType.Emf: type = ImagePartType.Emf; return true;
+ case ImageHeader.FileType.Gif: type = ImagePartType.Gif; return true;
+ case ImageHeader.FileType.Jpeg: type = ImagePartType.Jpeg; return true;
+ case ImageHeader.FileType.Png: type = ImagePartType.Png; return true;
+ }
+ }
+ type = ImagePartType.Bmp;
+ return false;
+ }
+
+ ///
+ /// Loads an image from a stream and grab its size.
+ ///
+ protected static Size GetImageSize(Stream imageStream)
+ {
+ // Read only the size of the image
+ try
+ {
+ return ImageHeader.GetDimensions(imageStream);
+ }
+ catch (ArgumentException)
+ {
+ return Size.Empty;
+ }
+ }
+}
diff --git a/src/Html2OpenXml/IO/Resource.cs b/src/Html2OpenXml/IO/Resource.cs
index bdf30e4..78273ad 100755
--- a/src/Html2OpenXml/IO/Resource.cs
+++ b/src/Html2OpenXml/IO/Resource.cs
@@ -26,16 +26,36 @@ public class Resource : IDisposable
///
/// Gets the headers that have been send with the response.
///
- public IDictionary Headers { get; private set; } = new Dictionary();
+ public IDictionary Headers { get; } = new Dictionary();
///
/// Gets the content that has been send with the response.
///
public Stream Content { get; set; } = Stream.Null;
- void IDisposable.Dispose()
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Release the unmanaged resources used by this class and optionally releases the managed resources.
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
{
Content?.Dispose();
Headers.Clear();
}
+
+ ///
+ /// Release the both the managed and unmanaged resources used by this class.
+ ///
+ ~Resource()
+ {
+ Dispose(false);
+ }
}
diff --git a/src/Html2OpenXml/PredefinedStyles.cs b/src/Html2OpenXml/PredefinedStyles.cs
index 993307a..d94372f 100755
--- a/src/Html2OpenXml/PredefinedStyles.cs
+++ b/src/Html2OpenXml/PredefinedStyles.cs
@@ -6,7 +6,7 @@ namespace HtmlToOpenXml;
///
/// Helper class to get chunks of OpenXml predefined style.
///
-internal class PredefinedStyles
+static class PredefinedStyles
{
private static ResourceManager? resourceMan;
diff --git a/src/Html2OpenXml/Primitives/HtmlColor.cs b/src/Html2OpenXml/Primitives/HtmlColor.cs
index 1076280..8846757 100755
--- a/src/Html2OpenXml/Primitives/HtmlColor.cs
+++ b/src/Html2OpenXml/Primitives/HtmlColor.cs
@@ -50,7 +50,7 @@ public static HtmlColor Parse(ReadOnlySpan span)
span = span.Trim();
if (span.Length < 3)
return Empty;
-
+
try
{
// Is it in hexa? Note: we no more accept hexa value without preceding the '#'
@@ -155,21 +155,6 @@ private static HtmlColor ParseHsl(ReadOnlySpan span)
};
}
- ///
- /// Convert a potential percentage value to its numeric representation.
- /// Saturation and Lightness can contains both a percentage value or a value comprised between 0.0 and 1.0.
- ///
- private static double ParsePercent (string value)
- {
- double parsedValue;
- if (value.IndexOf('%') > -1)
- parsedValue = double.Parse(value.Replace('%', ' '), CultureInfo.InvariantCulture) / 100d;
- else
- parsedValue = double.Parse(value, CultureInfo.InvariantCulture);
-
- return Math.Min(1, Math.Max(0, parsedValue));
- }
-
///
/// Creates a structure from the four RGB component values.
///
@@ -217,7 +202,7 @@ public static HtmlColor FromHsl(double alpha, double hue, double saturation, dou
if (luminosity < 0 || luminosity > 1)
throw new ArgumentOutOfRangeException(nameof(luminosity), luminosity, "Brightness should be comprised between 0.0 and 1.0");
- if (0 == saturation)
+ if (saturation == 0)
{
return FromArgb(alpha, Convert.ToByte(luminosity * 255),
Convert.ToByte(luminosity * 255), Convert.ToByte(luminosity * 255));
@@ -273,7 +258,15 @@ public static HtmlColor FromHsl(double alpha, double hue, double saturation, dou
///
public bool Equals(HtmlColor color)
{
- return color.A == A && color.R == R && color.G == G && color.B == B;
+ return AreEqual(color.A, A) && AreEqual(color.R, R) && AreEqual(color.G, G) && AreEqual(color.B, B);
+ }
+
+ ///
+ /// Compare float-point with a 2-digit precisions
+ ///
+ private static bool AreEqual(double a, double b, double tolerance = 0.01)
+ {
+ return Math.Abs(a - b) <= tolerance;
}
///
diff --git a/src/Html2OpenXml/Primitives/Unit.cs b/src/Html2OpenXml/Primitives/Unit.cs
index e54b116..b5dc8ad 100755
--- a/src/Html2OpenXml/Primitives/Unit.cs
+++ b/src/Html2OpenXml/Primitives/Unit.cs
@@ -114,25 +114,19 @@ private static long ComputeInEmus(UnitMetric metric, double value)
* The list of units supported are explained here: http://www.w3schools.com/css/css_units.asp
*/
- switch (metric)
+ return metric switch
{
- case UnitMetric.Auto:
- case UnitMetric.Unitless:
- case UnitMetric.Percent: return 0L; // not applicable
- case UnitMetric.Emus: return (long) value;
- case UnitMetric.Inch: return (long) (value * 914400L);
- case UnitMetric.Centimeter: return (long) (value * 360000L);
- case UnitMetric.Millimeter: return (long) (value * 36000L);
- case UnitMetric.EM:
- // well this is a rough conversion but considering 1em = 12pt (http://sureshjain.wordpress.com/2007/07/06/53/)
- return (long) (value / 72 * 914400L * 12);
- case UnitMetric.Ex:
- return (long) (value / 72 * 914400L * 12) / 2;
- case UnitMetric.Point: return (long) (value * 12700L);
- case UnitMetric.Pica: return (long) (value / 72 * 914400L) * 12;
- case UnitMetric.Pixel: return (long) (value / 96 * 914400L);
- default: goto case UnitMetric.Pixel;
- }
+ UnitMetric.Auto or UnitMetric.Unitless or UnitMetric.Percent => 0L,// not applicable
+ UnitMetric.Emus => (long)value,
+ UnitMetric.Inch => (long)(value * 914400L),
+ UnitMetric.Centimeter => (long)(value * 360000L),
+ UnitMetric.Millimeter => (long)(value * 36000L),
+ UnitMetric.EM => (long)(value / 72 * 914400L * 12),// well this is a rough conversion but considering 1em = 12pt (http://sureshjain.wordpress.com/2007/07/06/53/)
+ UnitMetric.Ex => (long)(value / 72 * 914400L * 12) / 2,
+ UnitMetric.Point => (long)(value * 12700L),
+ UnitMetric.Pica => (long)(value / 72 * 914400L) * 12,
+ _ => (long)(value / 96 * 914400L),
+ };
}
//____________________________________________________________________
diff --git a/src/Html2OpenXml/Utilities/OpenXmlExtensions.cs b/src/Html2OpenXml/Utilities/OpenXmlExtensions.cs
index 905ba21..bcd2ec2 100755
--- a/src/Html2OpenXml/Utilities/OpenXmlExtensions.cs
+++ b/src/Html2OpenXml/Utilities/OpenXmlExtensions.cs
@@ -33,8 +33,8 @@ public static bool HasChild(this OpenXmlElement element) where T : OpenXmlEle
for (int i = element.ChildElements.Count - 1; i >= 0; i--)
{
- if (element.ChildElements[i] is T)
- return element.ChildElements[i] as T;
+ if (element.ChildElements[i] is T t)
+ return t;
}
return null;
diff --git a/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs b/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs
index ecd90ad..2946bf4 100644
--- a/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs
+++ b/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs
@@ -74,5 +74,22 @@ public void ParseWithAuto_ShouldSucceed ()
Assert.That(margin.Right.Metric, Is.EqualTo(UnitMetric.Auto));
}
}
+
+ [TestCase("margin: 25px;margin-block:100px", 100, 25, 100, 25)]
+ [TestCase("margin: 25px;margin-inline:100px", 25, 100, 25, 100)]
+ [TestCase("margin-inline:100px;margin-inline-start:25px", 0, 100, 0, 25)]
+ public void CombinedMarginStyle_ShouldSucceed(string html, int top, int right, int bottom, int left)
+ {
+ var styleAttributes = HtmlAttributeCollection.ParseStyle(html);
+ var margin = styleAttributes.GetMargin("margin");
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(margin.Top.ValueInPx, Is.EqualTo(top));
+ Assert.That(margin.Right.ValueInPx, Is.EqualTo(right));
+ Assert.That(margin.Bottom.ValueInPx, Is.EqualTo(bottom));
+ Assert.That(margin.Left.ValueInPx, Is.EqualTo(left));
+ }
+ }
}
}