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)); + } + } } }