From cc0dccf6319f8d47456951e89be7cb2747274508 Mon Sep 17 00:00:00 2001
From: Justin Haygood
Date: Thu, 16 Jul 2026 18:59:43 -0400
Subject: [PATCH 01/21] Port of CSS engine from PeachPDF (ExCSS-based)
Port of CSS engine from PeachPDF
---
Source/HtmlRenderer/Adapters/RAdapter.cs | 11 +-
Source/HtmlRenderer/Core/CssData.cs | 935 +++++++--
Source/HtmlRenderer/Core/CssDefaults.cs | 111 ++
.../CssEngine/BackgroundPositionGrammar.cs | 189 ++
.../Core/CssEngine/BackgroundSizeGrammar.cs | 88 +
.../Core/CssEngine/Calc/CalcCategory.cs | 25 +
.../Core/CssEngine/Calc/CalcEvaluator.cs | 121 ++
.../Core/CssEngine/Calc/CalcNode.cs | 98 +
.../Core/CssEngine/Calc/CalcParser.cs | 238 +++
.../Core/CssEngine/Calc/CalcSerializer.cs | 259 +++
.../Core/CssEngine/Calc/CalcTypeChecker.cs | 150 ++
.../Core/CssEngine/Calc/CalcValue.cs | 33 +
.../Core/CssEngine/Calc/CalcValueConverter.cs | 41 +
.../Core/CssEngine/Conditions/AndCondition.cs | 31 +
.../Conditions/DeclarationCondition.cs | 28 +
.../CssEngine/Conditions/EmptyCondition.cs | 16 +
.../CssEngine/Conditions/GroupCondition.cs | 34 +
.../Core/CssEngine/Conditions/NotCondition.cs | 33 +
.../Core/CssEngine/Conditions/OrCondition.cs | 35 +
.../CssEngine/Enumerations/AlignContent.cs | 18 +
.../Core/CssEngine/Enumerations/AlignItem.cs | 17 +
.../Enumerations/AnimationDirection.cs | 10 +
.../Enumerations/AnimationFillStyle.cs | 10 +
.../Enumerations/BackgroundAttachment.cs | 9 +
.../Enumerations/BackgroundRepeat.cs | 10 +
.../Core/CssEngine/Enumerations/BlendMode.cs | 22 +
.../CssEngine/Enumerations/BorderRepeat.cs | 9 +
.../Core/CssEngine/Enumerations/BoxModel.cs | 9 +
.../Core/CssEngine/Enumerations/BreakMode.cs | 16 +
.../Core/CssEngine/Enumerations/ClearMode.cs | 10 +
.../Core/CssEngine/Enumerations/Colors.cs | 205 ++
.../CssEngine/Enumerations/Combinators.cs | 20 +
.../CssEngine/Enumerations/ContainerType.cs | 9 +
.../CssEngine/Enumerations/DirectionMode.cs | 8 +
.../CssEngine/Enumerations/DisplayMode.cs | 25 +
.../CssEngine/Enumerations/FeatureNames.cs | 48 +
.../Core/CssEngine/Enumerations/FillRule.cs | 8 +
.../CssEngine/Enumerations/FlexDirection.cs | 10 +
.../Core/CssEngine/Enumerations/FlexWrap.cs | 9 +
.../Core/CssEngine/Enumerations/Floating.cs | 9 +
.../Core/CssEngine/Enumerations/FontSize.cs | 16 +
.../CssEngine/Enumerations/FontStretch.cs | 15 +
.../Core/CssEngine/Enumerations/FontStyle.cs | 9 +
.../CssEngine/Enumerations/FontVariant.cs | 8 +
.../Core/CssEngine/Enumerations/FontWeight.cs | 10 +
.../CssEngine/Enumerations/FunctionNames.cs | 57 +
.../Enumerations/HorizontalAlignment.cs | 10 +
.../CssEngine/Enumerations/HoverAbility.cs | 9 +
.../CssEngine/Enumerations/IntrinsicSizing.cs | 10 +
.../CssEngine/Enumerations/JustifyContent.cs | 18 +
.../Core/CssEngine/Enumerations/Keywords.cs | 358 ++++
.../Core/CssEngine/Enumerations/LineStyle.cs | 16 +
.../CssEngine/Enumerations/ListPosition.cs | 8 +
.../Core/CssEngine/Enumerations/ListStyle.cs | 19 +
.../CssEngine/Enumerations/ObjectFitting.cs | 11 +
.../Core/CssEngine/Enumerations/Overflow.cs | 10 +
.../CssEngine/Enumerations/OverflowWrap.cs | 8 +
.../Core/CssEngine/Enumerations/ParseError.cs | 20 +
.../Core/CssEngine/Enumerations/PdfTagType.cs | 53 +
.../Core/CssEngine/Enumerations/PlayState.cs | 8 +
.../CssEngine/Enumerations/PointerAccuracy.cs | 9 +
.../CssEngine/Enumerations/PositionMode.cs | 11 +
.../CssEngine/Enumerations/PropertyFlags.cs | 15 +
.../CssEngine/Enumerations/PropertyNames.cs | 248 +++
.../Enumerations/PseudoClassNames.cs | 55 +
.../Enumerations/PseudoElementNames.cs | 14 +
.../Core/CssEngine/Enumerations/QuirksMode.cs | 9 +
.../Core/CssEngine/Enumerations/RuleNames.cs | 17 +
.../Core/CssEngine/Enumerations/RuleType.cs | 24 +
.../CssEngine/Enumerations/ScriptingState.cs | 9 +
.../CssEngine/Enumerations/StrokeLinecap.cs | 9 +
.../CssEngine/Enumerations/StrokeLinejoin.cs | 9 +
.../CssEngine/Enumerations/SystemCursor.cs | 42 +
.../Core/CssEngine/Enumerations/SystemFont.cs | 12 +
.../CssEngine/Enumerations/TextAlignLast.cs | 13 +
.../Core/CssEngine/Enumerations/TextAnchor.cs | 9 +
.../Enumerations/TextDecorationLine.cs | 10 +
.../Enumerations/TextDecorationStyle.cs | 11 +
.../CssEngine/Enumerations/TextJustify.cs | 15 +
.../CssEngine/Enumerations/TextTransform.cs | 11 +
.../Core/CssEngine/Enumerations/TokenType.cs | 39 +
.../CssEngine/Enumerations/UnicodeMode.cs | 12 +
.../Core/CssEngine/Enumerations/UnitNames.cs | 32 +
.../CssEngine/Enumerations/UpdateFrequency.cs | 9 +
.../Enumerations/VerticalAlignment.cs | 14 +
.../Core/CssEngine/Enumerations/Visibility.cs | 9 +
.../Core/CssEngine/Enumerations/Whitespace.cs | 11 +
.../Core/CssEngine/Enumerations/WordBreak.cs | 9 +
.../CssEngine/Extensions/CharExtensions.cs | 130 ++
.../Extensions/CollectionExtensions.cs | 36 +
.../CssEngine/Extensions/FormatExtensions.cs | 28 +
.../Extensions/PortableExtensions.cs | 27 +
.../CssEngine/Extensions/StringExtensions.cs | 106 +
.../Extensions/ValueConverterExtensions.cs | 190 ++
.../CssEngine/Extensions/ValueExtensions.cs | 514 +++++
.../Factories/AttributeSelectorFactory.cs | 52 +
.../Factories/MediaFeatureFactory.cs | 85 +
.../CssEngine/Factories/PropertyFactory.cs | 450 +++++
.../Factories/PseudoClassSelectorFactory.cs | 84 +
.../Factories/PseudoElementSelectorFactory.cs | 48 +
.../Formatting/CompressedStyleFormatter.cs | 123 ++
.../CssEngine/Functions/DocumentFunction.cs | 23 +
.../CssEngine/Functions/DomainFunction.cs | 20 +
.../CssEngine/Functions/IConditionFunction.cs | 7 +
.../CssEngine/Functions/IDocumentFunction.cs | 8 +
.../CssEngine/Functions/RegexpFunction.cs | 19 +
.../Core/CssEngine/Functions/UrlFunction.cs | 17 +
.../CssEngine/Functions/UrlPrefixFunction.cs | 16 +
.../MediaFeatures/AspectRatioMediaFeature.cs | 11 +
.../MediaFeatures/ColorIndexMediaFeature.cs | 16 +
.../MediaFeatures/ColorMediaFeature.cs | 16 +
.../DeviceAspectRatioMediaFeature.cs | 11 +
.../MediaFeatures/DeviceHeightMediaFeature.cs | 11 +
.../MediaFeatures/DevicePixelRatioFeature.cs | 11 +
.../MediaFeatures/DeviceWidthMediaFeature.cs | 11 +
.../MediaFeatures/GridMediaFeature.cs | 11 +
.../MediaFeatures/HeightMediaFeature.cs | 11 +
.../MediaFeatures/HoverMediaFeature.cs | 13 +
.../CssEngine/MediaFeatures/MediaFeature.cs | 69 +
.../MediaFeatures/MonochromeMediaFeature.cs | 14 +
.../MediaFeatures/OrientationMediaFeature.cs | 13 +
.../MediaFeatures/PointerMediaFeature.cs | 13 +
.../MediaFeatures/ResolutionMediaFeature.cs | 11 +
.../MediaFeatures/ScanMediaFeature.cs | 14 +
.../MediaFeatures/ScriptingMediaFeature.cs | 13 +
.../MediaFeatures/SizeMediaFeature.cs | 11 +
.../MediaFeatures/UnknownMediaFeature.cs | 11 +
.../UpdateFrequencyMediaFeature.cs | 13 +
.../MediaFeatures/WidthMediaFeature.cs | 11 +
.../Core/CssEngine/Model/Combinator.cs | 80 +
.../Core/CssEngine/Model/Comment.cs | 19 +
.../Core/CssEngine/Model/Converters.cs | 513 +++++
.../Core/CssEngine/Model/IMediaFeature.cs | 9 +
.../Core/CssEngine/Model/ISelector.cs | 8 +
.../Core/CssEngine/Model/IStyleFormattable.cs | 9 +
.../Core/CssEngine/Model/IStyleFormatter.cs | 18 +
.../Core/CssEngine/Model/IStylesheetNode.cs | 10 +
.../Core/CssEngine/Model/IValueConverter.cs | 21 +
.../HtmlRenderer/Core/CssEngine/Model/Map.cs | 651 +++++++
.../Core/CssEngine/Model/MediaList.cs | 76 +
.../Core/CssEngine/Model/Medium.cs | 49 +
.../Core/CssEngine/Model/ParseException.cs | 11 +
.../Core/CssEngine/Model/ParserExtensions.cs | 162 ++
.../Core/CssEngine/Model/ParserOptions.cs | 13 +
.../HtmlRenderer/Core/CssEngine/Model/Pool.cs | 78 +
.../Core/CssEngine/Model/Priority.cs | 103 +
.../Core/CssEngine/Model/StyleDeclaration.cs | 1706 +++++++++++++++++
.../Core/CssEngine/Model/Stylesheet.cs | 70 +
.../Core/CssEngine/Model/StylesheetNode.cs | 92 +
.../Core/CssEngine/Model/StylesheetText.cs | 31 +
.../Core/CssEngine/Model/Symbols.cs | 90 +
.../Core/CssEngine/Model/TaskEx.cs | 26 +
.../Core/CssEngine/Model/TextPosition.cs | 97 +
.../Core/CssEngine/Model/TextRange.cs | 55 +
.../Core/CssEngine/Model/TextSource.cs | 230 +++
.../Core/CssEngine/Model/TokenValue.cs | 58 +
.../Core/CssEngine/Model/TransformMatrix.cs | 106 +
.../HtmlRenderer/Core/CssEngine/Model/Url.cs | 873 +++++++++
.../Core/CssEngine/Model/ValueBuilder.cs | 136 ++
.../Core/CssEngine/Parser/Lexer.cs | 1225 ++++++++++++
.../Core/CssEngine/Parser/LexerBase.cs | 173 ++
.../CssEngine/Parser/SelectorConstructor.cs | 918 +++++++++
.../CssEngine/Parser/StylesheetComposer.cs | 1313 +++++++++++++
.../Core/CssEngine/Parser/StylesheetParser.cs | 213 ++
.../Core/CssEngine/Parser/TokenizerError.cs | 17 +
.../Core/CssEngine/Rules/CharsetRule.cs | 26 +
.../Core/CssEngine/Rules/ConditionRule.cs | 10 +
.../Core/CssEngine/Rules/ContainerRule.cs | 32 +
.../Core/CssEngine/Rules/DeclarationRule.cs | 130 ++
.../Core/CssEngine/Rules/DocumentRule.cs | 41 +
.../Core/CssEngine/Rules/FontFaceRule.cs | 63 +
.../Core/CssEngine/Rules/GroupingRule.cs | 34 +
.../Core/CssEngine/Rules/ICharsetRule.cs | 7 +
.../Core/CssEngine/Rules/IConditionRule.cs | 7 +
.../Core/CssEngine/Rules/IContainer.cs | 8 +
.../Core/CssEngine/Rules/IFontFaceRule.cs | 14 +
.../Core/CssEngine/Rules/IGroupingRule.cs | 9 +
.../Core/CssEngine/Rules/IImportRule.cs | 8 +
.../Core/CssEngine/Rules/IKeyframeRule.cs | 9 +
.../Core/CssEngine/Rules/IKeyframesRule.cs | 11 +
.../Core/CssEngine/Rules/IMarginRule.cs | 8 +
.../Core/CssEngine/Rules/IMediaRule.cs | 7 +
.../Core/CssEngine/Rules/INamespaceRule.cs | 8 +
.../Core/CssEngine/Rules/IPageRule.cs | 8 +
.../Core/CssEngine/Rules/IRule.cs | 10 +
.../Core/CssEngine/Rules/IRuleCreator.cs | 7 +
.../Core/CssEngine/Rules/IRuleList.cs | 10 +
.../Core/CssEngine/Rules/IStyleRule.cs | 9 +
.../Core/CssEngine/Rules/ISupportsRule.cs | 7 +
.../Core/CssEngine/Rules/ImportRule.cs | 36 +
.../Core/CssEngine/Rules/KeyframeRule.cs | 37 +
.../Core/CssEngine/Rules/KeyframesRule.cs | 53 +
.../Core/CssEngine/Rules/MarginStyleRule.cs | 32 +
.../Core/CssEngine/Rules/MediaRule.cs | 27 +
.../Core/CssEngine/Rules/NamespaceRule.cs | 66 +
.../Core/CssEngine/Rules/PageRule.cs | 47 +
.../HtmlRenderer/Core/CssEngine/Rules/Rule.cs | 65 +
.../Core/CssEngine/Rules/RuleList.cs | 78 +
.../Core/CssEngine/Rules/StyleRule.cs | 33 +
.../Core/CssEngine/Rules/SupportsRule.cs | 43 +
.../Core/CssEngine/Rules/UnknownRule.cs | 20 +
.../Core/CssEngine/Rules/ViewportRule.cs | 15 +
.../Core/CssEngine/Selectors/AllSelector.cs | 14 +
.../Selectors/AttrAvailableSelector.cs | 10 +
.../CssEngine/Selectors/AttrBeginsSelector.cs | 10 +
.../Selectors/AttrContainsSelector.cs | 10 +
.../CssEngine/Selectors/AttrEndsSelector.cs | 10 +
.../CssEngine/Selectors/AttrHyphenSelector.cs | 10 +
.../CssEngine/Selectors/AttrListSelector.cs | 10 +
.../CssEngine/Selectors/AttrMatchSelector.cs | 10 +
.../Selectors/AttrNotMatchSelector.cs | 10 +
.../CssEngine/Selectors/AttrSelectorBase.cs | 13 +
.../Core/CssEngine/Selectors/ChildSelector.cs | 49 +
.../Core/CssEngine/Selectors/ClassSelector.cs | 17 +
.../CssEngine/Selectors/CombinatorSelector.cs | 8 +
.../CssEngine/Selectors/ComplexSelector.cs | 80 +
.../CssEngine/Selectors/CompoundSelector.cs | 12 +
.../CssEngine/Selectors/FirstChildSelector.cs | 11 +
.../Selectors/FirstColumnSelector.cs | 10 +
.../CssEngine/Selectors/FirstTypeSelector.cs | 10 +
.../Core/CssEngine/Selectors/HasSelector.cs | 33 +
.../Core/CssEngine/Selectors/IAttrSelector.cs | 8 +
.../Core/CssEngine/Selectors/IdSelector.cs | 17 +
.../CssEngine/Selectors/KeyframeSelector.cs | 75 +
.../CssEngine/Selectors/LastChildSelector.cs | 10 +
.../CssEngine/Selectors/LastColumnSelector.cs | 10 +
.../CssEngine/Selectors/LastTypeSelector.cs | 10 +
.../Core/CssEngine/Selectors/ListSelector.cs | 34 +
.../CssEngine/Selectors/MatchesSelector.cs | 37 +
.../CssEngine/Selectors/NamespaceSelector.cs | 14 +
.../Core/CssEngine/Selectors/NotSelector.cs | 30 +
.../CssEngine/Selectors/OnlyChildSelector.cs | 10 +
.../CssEngine/Selectors/OnlyOfTypeSelector.cs | 10 +
.../Selectors/PseudoClassSelector.cs | 17 +
.../Selectors/PseudoElementSelector.cs | 17 +
.../Core/CssEngine/Selectors/SelectorBase.cs | 21 +
.../Core/CssEngine/Selectors/Selectors.cs | 55 +
.../Core/CssEngine/Selectors/TypeSelector.cs | 17 +
.../CssEngine/Selectors/UnknownSelector.cs | 16 +
.../Animation/AnimationDelayProperty.cs | 15 +
.../Animation/AnimationDirectionProperty.cs | 15 +
.../Animation/AnimationDurationProperty.cs | 14 +
.../Animation/AnimationFillModeProperty.cs | 15 +
.../AnimationIterationCountProperty.cs | 15 +
.../Animation/AnimationNameProperty.cs | 15 +
.../Animation/AnimationPlayStateProperty.cs | 15 +
.../Animation/AnimationProperty.cs | 26 +
.../AnimationTimingFunctionProperty.cs | 15 +
.../BackgroundAttachmentProperty.cs | 15 +
.../Background/BackgroundClipProperty.cs | 15 +
.../Background/BackgroundColorProperty.cs | 14 +
.../Background/BackgroundImageProperty.cs | 14 +
.../Background/BackgroundOriginProperty.cs | 15 +
.../Background/BackgroundPositionProperty.cs | 15 +
.../Background/BackgroundProperty.cs | 38 +
.../Background/BackgroundRepeatProperty.cs | 15 +
.../Background/BackgroundSizeProperty.cs | 15 +
.../Border/BorderBottomColorProperty.cs | 15 +
.../Border/BorderBottomProperty.cs | 20 +
.../Border/BorderBottomStyleProperty.cs | 15 +
.../Border/BorderBottomWidthProperty.cs | 14 +
.../Border/BorderCollapseProperty.cs | 14 +
.../Border/BorderColorProperty.cs | 16 +
.../Border/BorderLeftColorProperty.cs | 15 +
.../Border/BorderLeftProperty.cs | 20 +
.../Border/BorderLeftStyleProperty.cs | 15 +
.../Border/BorderLeftWidthProperty.cs | 14 +
.../StyleProperties/Border/BorderProperty.cs | 26 +
.../Border/BorderRightColorProperty.cs | 15 +
.../Border/BorderRightProperty.cs | 20 +
.../Border/BorderRightStyleProperty.cs | 15 +
.../Border/BorderRightWidthProperty.cs | 14 +
.../Border/BorderSpacingProperty.cs | 15 +
.../Border/BorderStyleProperty.cs | 16 +
.../Border/BorderTopColorProperty.cs | 15 +
.../Border/BorderTopProperty.cs | 20 +
.../Border/BorderTopStyleProperty.cs | 15 +
.../Border/BorderTopWidthProperty.cs | 14 +
.../Border/BorderWidthProperty.cs | 16 +
.../BorderImage/BorderImageOutsetProperty.cs | 15 +
.../BorderImage/BorderImageProperty.cs | 26 +
.../BorderImage/BorderImageRepeatProperty.cs | 15 +
.../BorderImage/BorderImageSliceProperty.cs | 23 +
.../BorderImage/BorderImageSourceProperty.cs | 14 +
.../BorderImage/BorderImageWidthProperty.cs | 15 +
.../BorderBottomLeftRadiusProperty.cs | 15 +
.../BorderBottomRightRadiusProperty.cs | 15 +
.../BorderRadius/BorderRadiusProperty.cs | 14 +
.../BorderTopLeftRadiusProperty.cs | 15 +
.../BorderTopRightRadiusProperty.cs | 15 +
.../StyleProperties/Box/BoxDecorationBreak.cs | 14 +
.../StyleProperties/Box/BoxShadowProperty.cs | 14 +
.../StyleProperties/Box/BoxSizingProperty.cs | 13 +
.../Box/MarginBottomProperty.cs | 15 +
.../StyleProperties/Box/MarginLeftProperty.cs | 15 +
.../StyleProperties/Box/MarginProperty.cs | 17 +
.../Box/MarginRightProperty.cs | 15 +
.../StyleProperties/Box/MarginTopProperty.cs | 15 +
.../Box/PaddingBottomProperty.cs | 15 +
.../Box/PaddingLeftProperty.cs | 15 +
.../StyleProperties/Box/PaddingProperty.cs | 17 +
.../Box/PaddingRightProperty .cs | 15 +
.../StyleProperties/Box/PaddingTopProperty.cs | 15 +
.../Break/BreakAfterProperty.cs | 15 +
.../Break/BreakBeforeProperty.cs | 15 +
.../Break/BreakInsideProperty.cs | 15 +
.../Break/PageBreakAfterProperty.cs | 15 +
.../Break/PageBreakBeforeProperty.cs | 15 +
.../Break/PageBreakInsideProperty.cs | 17 +
.../StyleProperties/CaptionSideProperty.cs | 13 +
.../Columns/ColumnCountProperty.cs | 14 +
.../Columns/ColumnFillProperty.cs | 14 +
.../Columns/ColumnGapProperty.cs | 17 +
.../Columns/ColumnRuleColorProperty.cs | 14 +
.../Columns/ColumnRuleProperty.cs | 19 +
.../Columns/ColumnRuleStyleProperty.cs | 15 +
.../Columns/ColumnRuleWidthProperty.cs | 14 +
.../Columns/ColumnSpanProperty.cs | 14 +
.../Columns/ColumnWidthProperty.cs | 15 +
.../Columns/ColumnsProperty.cs | 18 +
.../Container/ContainerNameProperty.cs | 15 +
.../Container/ContainerTypeProperty.cs | 15 +
.../StyleProperties/ContentProperty.cs | 61 +
.../Coordinate/BottomProperty.cs | 15 +
.../Coordinate/HeightProperty.cs | 15 +
.../Coordinate/LeftProperty.cs | 15 +
.../Coordinate/MaxHeightProperty.cs | 15 +
.../Coordinate/MaxWidthProperty.cs | 15 +
.../Coordinate/MinHeightProperty.cs | 15 +
.../Coordinate/MinWidthProperty.cs | 15 +
.../Coordinate/RightProperty .cs | 15 +
.../StyleProperties/Coordinate/TopProperty.cs | 15 +
.../Coordinate/WidthProperty.cs | 15 +
.../StyleProperties/CursorProperty.cs | 21 +
.../StyleProperties/CustomProperty.cs | 12 +
.../StyleProperties/EmptyCellsProperty.cs | 14 +
.../StyleProperties/FeatureProperty.cs | 16 +
.../Fill/FillOpacityProperty.cs | 14 +
.../StyleProperties/Fill/FillProperty.cs | 14 +
.../StyleProperties/Fill/FillRuleProperty.cs | 15 +
.../Flexbox/AlignContentProperty.cs | 13 +
.../Flexbox/AlignItemsProperty.cs | 14 +
.../Flexbox/AlignSelfProperty.cs | 13 +
.../Flexbox/FlexBasisProperty.cs | 13 +
.../Flexbox/FlexDirectionProperty.cs | 13 +
.../Flexbox/FlexFlowProperty.cs | 13 +
.../Flexbox/FlexGrowProperty.cs | 13 +
.../StyleProperties/Flexbox/FlexProperty.cs | 13 +
.../Flexbox/FlexShrinkProperty.cs | 13 +
.../Flexbox/FlexWrapProperty.cs | 13 +
.../Flexbox/JustifyContentProperty.cs | 14 +
.../StyleProperties/Flexbox/OrderProperty.cs | 15 +
.../StyleProperties/Flow/ClearProperty.cs | 15 +
.../StyleProperties/Flow/DisplayProperty.cs | 15 +
.../StyleProperties/Flow/FloatProperty.cs | 14 +
.../StyleProperties/Flow/OverflowProperty.cs | 15 +
.../StyleProperties/Flow/PositionProperty.cs | 15 +
.../StyleProperties/Flow/ZIndexProperty.cs | 14 +
.../StyleProperties/Font/ColorProperty.cs | 14 +
.../Font/FontFamilyProperty.cs | 48 +
.../StyleProperties/Font/FontProperty.cs | 27 +
.../Font/FontSizeAdjustProperty.cs | 14 +
.../StyleProperties/Font/FontSizeProperty.cs | 15 +
.../Font/FontStretchProperty.cs | 15 +
.../StyleProperties/Font/FontStyleProperty.cs | 15 +
.../Font/FontVariantProperty.cs | 15 +
.../Font/FontWeightProperty.cs | 17 +
.../Font/LetterSpacingProperty.cs | 14 +
.../Font/LineHeightProperty.cs | 15 +
.../StyleProperties/Font/SrcProperty.cs | 12 +
.../Font/UnicodeRangeProperty.cs | 12 +
.../Font/WordSpacingProperty.cs | 15 +
.../CssEngine/StyleProperties/GapProperty.cs | 15 +
.../StyleProperties/HyphensProperty.cs | 18 +
.../CssEngine/StyleProperties/IProperties .cs | 14 +
.../CssEngine/StyleProperties/IProperty.cs | 10 +
.../StyleProperties/IPropertyValue.cs | 9 +
.../List/CounterIncrementProperty.cs | 17 +
.../List/CounterResetProperty.cs | 17 +
.../List/ListStyleImageProperty.cs | 14 +
.../List/ListStylePositionProperty.cs | 15 +
.../StyleProperties/List/ListStyleProperty.cs | 19 +
.../List/ListStyleTypeProperty.cs | 15 +
.../StyleProperties/OrphansProperty.cs | 14 +
.../Outline/OutlineColorProperty.cs | 15 +
.../Outline/OutlineProperty.cs | 19 +
.../Outline/OutlineStyleProperty.cs | 15 +
.../Outline/OutlineWidthProperty.cs | 14 +
.../StyleProperties/PageNameProperty.cs | 13 +
.../StyleProperties/PageSizeProperty.cs | 13 +
.../StyleProperties/PdfTagTypeProperty.cs | 11 +
.../CssEngine/StyleProperties/Property.cs | 65 +
.../StyleProperties/QuotesProperty.cs | 15 +
.../StyleProperties/RowGapProperty.cs | 15 +
.../StyleProperties/ShorthandProperty.cs | 25 +
.../Sizing/ObjectFitProperty.cs | 15 +
.../Sizing/ObjectPositionProperty.cs | 14 +
.../StyleProperties/StringSetProperty.cs | 18 +
.../Stroke/StrokeDasharrayProperty.cs | 14 +
.../Stroke/StrokeDashoffsetProperty.cs | 14 +
.../Stroke/StrokeLinecapProperty.cs | 15 +
.../Stroke/StrokeLinejoinProperty.cs | 14 +
.../Stroke/StrokeMiterlimitProperty.cs | 14 +
.../Stroke/StrokeOpacityProperty.cs | 14 +
.../StyleProperties/Stroke/StrokeProperty.cs | 14 +
.../Stroke/StrokeWidthProperty.cs | 14 +
.../StyleProperties/TableLayoutProperty.cs | 14 +
.../StyleProperties/Text/DirectionProperty.cs | 15 +
.../Text/OverflowWrapProperty.cs | 14 +
.../Text/TextAlignLastProperty.cs | 14 +
.../StyleProperties/Text/TextAlignProperty.cs | 15 +
.../Text/TextAnchorProperty.cs | 14 +
.../Text/TextDecorationColorProperty.cs | 14 +
.../Text/TextDecorationLineProperty.cs | 14 +
.../Text/TextDecorationProperty.cs | 19 +
.../Text/TextDecorationStyleProperty.cs | 15 +
.../Text/TextIndentProperty.cs | 15 +
.../Text/TextJustifyProperty.cs | 14 +
.../Text/TextShadowProperty.cs | 14 +
.../Text/TextTransformProperty.cs | 15 +
.../Text/VerticalAlignProperty.cs | 17 +
.../Text/WhiteSpaceProperty.cs | 15 +
.../StyleProperties/Text/WordBreakProperty.cs | 14 +
.../Transform/PerspectiveOriginProperty.cs | 31 +
.../Transform/PerspectiveProperty.cs | 15 +
.../Transform/TransformOriginProperty.cs | 35 +
.../Transform/TransformProperty.cs | 15 +
.../Transform/TransformStyleProperty.cs | 15 +
.../Transition/TransitionDelayProperty.cs | 15 +
.../Transition/TransitionDurationProperty.cs | 15 +
.../Transition/TransitionProperty.cs | 23 +
.../Transition/TransitionPropertyProperty.cs | 15 +
.../TransitionTimingFunctionProperty.cs | 15 +
.../UnicodeBidirectionalProperty.cs | 15 +
.../StyleProperties/UnknownProperty.cs | 12 +
.../Visibility/BackfaceVisibilityProperty.cs | 14 +
.../Visibility/ClipProperty.cs | 14 +
.../Visibility/OpacityProperty.cs | 13 +
.../Visibility/VisibilityProperty.cs | 15 +
.../StyleProperties/WidowsProperty.cs | 14 +
.../Core/CssEngine/TextEncoding.cs | 305 +++
.../Core/CssEngine/Tokens/ColorToken.cs | 17 +
.../Core/CssEngine/Tokens/CommentToken.cs | 19 +
.../Core/CssEngine/Tokens/FunctionToken.cs | 49 +
.../Core/CssEngine/Tokens/KeywordToken.cs | 25 +
.../Core/CssEngine/Tokens/NumberToken.cs | 39 +
.../Core/CssEngine/Tokens/RangeToken.cs | 51 +
.../Core/CssEngine/Tokens/StringToken.cs | 20 +
.../Core/CssEngine/Tokens/Token.cs | 24 +
.../Core/CssEngine/Tokens/UnitToken.cs | 22 +
.../Core/CssEngine/Tokens/UrlToken.cs | 21 +
.../ValueConverters/AnyValueConverter.cs | 34 +
.../ArgumentsValueConverter.cs | 67 +
.../BackgroundPositionValueConverter.cs | 73 +
.../BackgroundSizeValueConverter.cs | 54 +
.../ValueConverters/BorderRadiusConverter.cs | 127 ++
.../ConditionalStartsWithValueConverter.cs | 92 +
.../ConstraintValueConverter.cs | 62 +
.../ContentFunctionConverter.cs | 73 +
.../ContinuousValueConverter.cs | 79 +
.../DictionaryValueConverter.cs | 46 +
.../ValueConverters/EndListValueConverter.cs | 104 +
.../ValueConverters/FallbackValueConverter.cs | 44 +
.../ValueConverters/FunctionValueConverter.cs | 59 +
.../ValueConverters/GradientConverter.cs | 249 +++
.../IdentifierValueConverter.cs | 84 +
.../ValueConverters/ListValueConverter.cs | 107 ++
.../OneOrMoreValueConverter.cs | 101 +
.../ValueConverters/OptionValueConverter.cs | 79 +
.../ValueConverters/OrValueConverter.cs | 26 +
.../OrderedOptionsConverter.cs | 105 +
.../ValueConverters/PeriodicValueConverter.cs | 132 ++
.../ValueConverters/RequiredValueConverter.cs | 25 +
.../StartsWithValueConverter.cs | 78 +
.../StringFunctionConverter.cs | 88 +
.../StringSetValueConverter.cs | 136 ++
.../ValueConverters/StringValueConverter.cs | 38 +
.../ValueConverters/StringsValueConverter.cs | 55 +
.../ValueConverters/StructValueConverter.cs | 48 +
.../TimeBasedShorthandConverter.cs | 209 ++
.../UnorderedOptionsConverter.cs | 151 ++
.../ValueConverters/UrlValueConverter.cs | 38 +
.../Core/CssEngine/Values/Angle.cs | 217 +++
.../Core/CssEngine/Values/Color.cs | 384 ++++
.../Core/CssEngine/Values/Counter.cs | 16 +
.../Values/CubicBezierTimingFunction.cs | 18 +
.../Core/CssEngine/Values/Frequency.cs | 139 ++
.../Core/CssEngine/Values/GradientStop.cs | 14 +
.../Core/CssEngine/Values/IGradient.cs | 10 +
.../Core/CssEngine/Values/IImageSource.cs | 6 +
.../Core/CssEngine/Values/ITimingFunction.cs | 6 +
.../Core/CssEngine/Values/ITransform.cs | 7 +
.../Core/CssEngine/Values/Length.cs | 364 ++++
.../Core/CssEngine/Values/LinearGradient.cs | 21 +
.../Core/CssEngine/Values/MatrixTransform.cs | 17 +
.../Core/CssEngine/Values/Number.cs | 102 +
.../Core/CssEngine/Values/Percent.cs | 125 ++
.../CssEngine/Values/PerspectiveTransform.cs | 32 +
.../Core/CssEngine/Values/Point.cs | 41 +
.../Core/CssEngine/Values/PortNumbers.cs | 26 +
.../Core/CssEngine/Values/ProtocolNames.cs | 50 +
.../Core/CssEngine/Values/RadialGradient.cs | 40 +
.../Core/CssEngine/Values/Resolution.cs | 121 ++
.../Core/CssEngine/Values/RotateTransform.cs | 38 +
.../Core/CssEngine/Values/ScaleTransform.cs | 21 +
.../Core/CssEngine/Values/Shadow.cs | 22 +
.../Core/CssEngine/Values/Shape.cs | 18 +
.../Core/CssEngine/Values/SkewTransform.cs | 23 +
.../CssEngine/Values/StepsTimingFunction.cs | 16 +
.../Core/CssEngine/Values/Time.cs | 161 ++
.../CssEngine/Values/TranslateTransform.cs | 24 +
.../HtmlRenderer/Core/CssEngine/license.txt | 21 +
Source/HtmlRenderer/Core/Dom/CssBox.cs | 18 +
.../HtmlRenderer/Core/Dom/CssBoxProperties.cs | 13 +
.../HtmlRenderer/Core/Dom/CssContentEngine.cs | 131 ++
Source/HtmlRenderer/Core/Dom/HoverBoxBlock.cs | 16 +-
Source/HtmlRenderer/Core/Entities/CssBlock.cs | 235 ---
.../Core/Entities/CssBlockSelectorItem.cs | 74 -
Source/HtmlRenderer/Core/HtmlContainerInt.cs | 8 +-
Source/HtmlRenderer/Core/Parse/CssParser.cs | 942 ++-------
.../HtmlRenderer/Core/Parse/CssValueParser.cs | 216 ++-
Source/HtmlRenderer/Core/Parse/DomParser.cs | 723 +++++--
.../Core/Parse/RegexParserHelper.cs | 227 ---
.../HtmlRenderer/Core/Utils/CssConstants.cs | 6 +
Source/HtmlRenderer/Core/Utils/CssUtils.cs | 53 +
Source/HtmlRenderer/Core/Utils/DomUtils.cs | 126 +-
526 files changed, 27397 insertions(+), 1649 deletions(-)
create mode 100644 Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Calc/CalcValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/AndCondition.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/EmptyCondition.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/GroupCondition.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/NotCondition.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/OrCondition.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/AlignContent.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/AlignItem.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/AnimationDirection.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/AnimationFillStyle.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/BackgroundAttachment.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/BackgroundRepeat.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/BlendMode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/BorderRepeat.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/BoxModel.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/ContainerType.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/DirectionMode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FillRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FlexDirection.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FlexWrap.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FontSize.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FontStretch.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FontStyle.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FontVariant.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FontWeight.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/HoverAbility.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/IntrinsicSizing.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/JustifyContent.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/LineStyle.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/ListPosition.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/ListStyle.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/ObjectFitting.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/ParseError.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PlayState.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PointerAccuracy.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PositionMode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyFlags.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/QuirksMode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/ScriptingState.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/StrokeLinecap.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/StrokeLinejoin.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/SystemCursor.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/SystemFont.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAnchor.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextDecorationLine.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextDecorationStyle.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextJustify.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextTransform.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TokenType.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/UnicodeMode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/UpdateFrequency.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/VerticalAlignment.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/Visibility.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/Whitespace.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/WordBreak.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/FormatExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/StringExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Factories/MediaFeatureFactory.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Formatting/CompressedStyleFormatter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Functions/DocumentFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Functions/DomainFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Functions/IConditionFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Functions/IDocumentFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Functions/RegexpFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Functions/UrlFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Functions/UrlPrefixFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AspectRatioMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ColorIndexMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ColorMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/DeviceAspectRatioMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/DeviceHeightMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/DevicePixelRatioFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/DeviceWidthMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/GridMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/HeightMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/HoverMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/MediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/MonochromeMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OrientationMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PointerMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ResolutionMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ScanMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ScriptingMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/SizeMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UnknownMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateFrequencyMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/WidthMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Combinator.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Comment.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/IMediaFeature.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/ISelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/IStyleFormattable.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/IStyleFormatter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/IStylesheetNode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/IValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Map.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/MediaList.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Medium.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/ParseException.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/ParserExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/ParserOptions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Pool.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Priority.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/StyleDeclaration.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Stylesheet.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/StylesheetNode.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/StylesheetText.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Symbols.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/TaskEx.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/TextPosition.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/TextRange.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/TextSource.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/TokenValue.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/TransformMatrix.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/Url.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/ValueBuilder.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Parser/Lexer.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Parser/LexerBase.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Parser/SelectorConstructor.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetComposer.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetParser.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Parser/TokenizerError.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/CharsetRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/ConditionRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/ContainerRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/DeclarationRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/DocumentRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/FontFaceRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/GroupingRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/ICharsetRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IConditionRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IContainer.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IFontFaceRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IGroupingRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IImportRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IKeyframeRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IKeyframesRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IMarginRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IMediaRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/INamespaceRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IPageRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IRuleCreator.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IRuleList.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IStyleRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/ISupportsRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/ImportRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/KeyframeRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/KeyframesRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/MarginStyleRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/MediaRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/NamespaceRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/PageRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/Rule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/RuleList.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/SupportsRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/UnknownRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/ViewportRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AllSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrAvailableSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrBeginsSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrContainsSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrEndsSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrHyphenSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrListSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrMatchSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrNotMatchSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/AttrSelectorBase.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/ChildSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/ClassSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/CombinatorSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/ComplexSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/CompoundSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/FirstChildSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/FirstColumnSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/FirstTypeSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/IAttrSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/IdSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/KeyframeSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/LastChildSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/LastColumnSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/LastTypeSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/NamespaceSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyChildSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyOfTypeSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/PseudoClassSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/PseudoElementSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/SelectorBase.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/Selectors.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/TypeSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/UnknownSelector.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationDelayProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationDirectionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationDurationProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationFillModeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationIterationCountProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationNameProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationPlayStateProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationTimingFunctionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundAttachmentProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundClipProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundImageProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundOriginProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundPositionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundRepeatProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundSizeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderBottomColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderBottomProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderBottomStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderBottomWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderCollapseProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderLeftColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderLeftProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderLeftStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderLeftWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderRightColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderRightProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderRightStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderRightWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderSpacingProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderTopColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderTopProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderTopStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderTopWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Border/BorderWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderImage/BorderImageOutsetProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderImage/BorderImageProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderImage/BorderImageRepeatProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderImage/BorderImageSliceProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderImage/BorderImageSourceProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderImage/BorderImageWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderBottomLeftRadiusProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderBottomRightRadiusProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderRadiusProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderTopLeftRadiusProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderTopRightRadiusProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxDecorationBreak.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxSizingProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/MarginBottomProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/MarginLeftProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/MarginProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/MarginRightProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/MarginTopProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/PaddingBottomProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/PaddingLeftProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/PaddingProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/PaddingRightProperty .cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/PaddingTopProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Break/BreakAfterProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Break/BreakBeforeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Break/BreakInsideProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Break/PageBreakAfterProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Break/PageBreakBeforeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Break/PageBreakInsideProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/CaptionSideProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnCountProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnFillProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnGapProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnRuleColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnRuleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnRuleStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnRuleWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnSpanProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainerNameProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainerTypeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/ContentProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/BottomProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/HeightProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/LeftProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/MaxHeightProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/MaxWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/MinHeightProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/MinWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/RightProperty .cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/TopProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Coordinate/WidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/CursorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/CustomProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/EmptyCellsProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/FeatureProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillOpacityProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillRuleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/AlignContentProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/AlignItemsProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/AlignSelfProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/FlexBasisProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/FlexDirectionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/FlexFlowProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/FlexGrowProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/FlexProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/FlexShrinkProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/FlexWrapProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/JustifyContentProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/OrderProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/ClearProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/DisplayProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FloatProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/PositionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/ZIndexProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/ColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFamilyProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSizeAdjustProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSizeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontStretchProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontWeightProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/LetterSpacingProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/LineHeightProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/SrcProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/UnicodeRangeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/WordSpacingProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/GapProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/HyphensProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/IProperties .cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/IProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/IPropertyValue.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/CounterIncrementProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/CounterResetProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleImageProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStylePositionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleTypeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/OrphansProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Outline/OutlineColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Outline/OutlineProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Outline/OutlineStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Outline/OutlineWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageNameProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageSizeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/PdfTagTypeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Property.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/QuotesProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/RowGapProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/ShorthandProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Sizing/ObjectFitProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Sizing/ObjectPositionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDasharrayProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDashoffsetProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinecapProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinejoinProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeMiterlimitProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeOpacityProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeWidthProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/TableLayoutProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/DirectionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/OverflowWrapProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignLastProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAnchorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationColorProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationLineProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextIndentProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextJustifyProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextShadowProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextTransformProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/VerticalAlignProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WordBreakProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveOriginProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformStyleProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionDelayProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionDurationProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionPropertyProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionTimingFunctionProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/UnicodeBidirectionalProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/UnknownProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/BackfaceVisibilityProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/OpacityProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/VisibilityProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/WidowsProperty.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/ColorToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/CommentToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/FunctionToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/KeywordToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/NumberToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/StringToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/Token.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/UnitToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Tokens/UrlToken.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/AnyValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ArgumentsValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundSizeValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/BorderRadiusConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ConditionalStartsWithValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ConstraintValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContinuousValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/DictionaryValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/EndListValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/FallbackValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/FunctionValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/IdentifierValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ListValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/OneOrMoreValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/OptionValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/OrValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/OrderedOptionsConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/PeriodicValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/RequiredValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/StartsWithValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/StringFunctionConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/StringSetValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/StringValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/StringsValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/StructValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TimeBasedShorthandConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/UnorderedOptionsConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/UrlValueConverter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Angle.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Color.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Counter.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/CubicBezierTimingFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Frequency.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/GradientStop.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/IGradient.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/IImageSource.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/ITimingFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/ITransform.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Length.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/LinearGradient.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/MatrixTransform.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Number.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Percent.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/PerspectiveTransform.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Point.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/PortNumbers.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/ProtocolNames.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/RadialGradient.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Resolution.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/RotateTransform.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/ScaleTransform.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Shadow.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Shape.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/SkewTransform.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/StepsTimingFunction.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/Time.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/TranslateTransform.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/license.txt
create mode 100644 Source/HtmlRenderer/Core/Dom/CssContentEngine.cs
delete mode 100644 Source/HtmlRenderer/Core/Entities/CssBlock.cs
delete mode 100644 Source/HtmlRenderer/Core/Entities/CssBlockSelectorItem.cs
delete mode 100644 Source/HtmlRenderer/Core/Parse/RegexParserHelper.cs
diff --git a/Source/HtmlRenderer/Adapters/RAdapter.cs b/Source/HtmlRenderer/Adapters/RAdapter.cs
index 6b13459c1..8a5afc3b9 100644
--- a/Source/HtmlRenderer/Adapters/RAdapter.cs
+++ b/Source/HtmlRenderer/Adapters/RAdapter.cs
@@ -85,7 +85,16 @@ protected RAdapter()
///
public CssData DefaultCssData
{
- get { return _defaultCssData ?? (_defaultCssData = CssData.Parse(this, CssDefaults.DefaultStyleSheet, false)); }
+ get
+ {
+ if (_defaultCssData == null)
+ {
+ _defaultCssData = CssData.Parse(this, CssDefaults.DefaultStyleSheet, false);
+ foreach (var stylesheet in _defaultCssData.Stylesheets)
+ stylesheet.IsUserAgent = true;
+ }
+ return _defaultCssData;
+ }
}
///
diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs
index f0e4a8106..179956178 100644
--- a/Source/HtmlRenderer/Core/CssData.cs
+++ b/Source/HtmlRenderer/Core/CssData.cs
@@ -6,54 +6,51 @@
// like the days and months;
// they die and are reborn,
// like the four seasons."
-//
+//
// - Sun Tsu,
// "The Art of War"
using System;
using System.Collections.Generic;
+using System.Linq;
using TheArtOfDev.HtmlRenderer.Adapters;
-using TheArtOfDev.HtmlRenderer.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Core.CssEngine;
+using TheArtOfDev.HtmlRenderer.Core.Dom;
using TheArtOfDev.HtmlRenderer.Core.Parse;
using TheArtOfDev.HtmlRenderer.Core.Utils;
namespace TheArtOfDev.HtmlRenderer.Core
{
///
- /// Holds parsed stylesheet css blocks arranged by media and classes.
- ///
+ /// Holds parsed stylesheets (UA default + author + inline), and provides origin-aware,
+ /// specificity-ordered selector matching against the box tree.
///
///
- /// To learn more about CSS blocks visit CSS spec: http://www.w3.org/TR/CSS21/syndata.html#block
+ /// Ported from PeachPDF's Html/Core/CssData.cs (itself a fork of Tyler Brinks' ExCSS), adapted to
+ /// HTML-Renderer's CssBox/HtmlTag shape. One deliberate omission from the source: the
+ /// ::marker pseudo-element box-synthesis path is removed - ::before/::after
+ /// synthesis is kept, but this codebase has no CssBox.IsMarkerPseudoElement concept and no
+ /// consumer for it (list markers are painted procedurally, not via a synthesized box).
///
public sealed class CssData
{
- #region Fields and Consts
-
///
- /// used to return empty array
+ /// The parsed stylesheets that make up this CssData, in the order they were added (UA default
+ /// first, then author/`<link>`/`<style>` stylesheets in document order). Each carries its own
+ /// flag so origin-aware cascade phases can be resolved.
///
- private static readonly List _emptyArray = new List();
-
- ///
- /// dictionary of media type to dictionary of css class name to the cssBlocks collection with all the data.
- ///
- private readonly Dictionary>> _mediaBlocks = new Dictionary>>(StringComparer.InvariantCultureIgnoreCase);
-
- #endregion
-
+ internal List Stylesheets { get; } = new List();
///
/// Init.
///
internal CssData()
{
- _mediaBlocks.Add("all", new Dictionary>(StringComparer.InvariantCultureIgnoreCase));
}
///
/// Parse the given stylesheet to object.
- /// If is true the parsed css blocks are added to the
+ /// If is true the parsed css blocks are added to the
/// default css data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned.
///
///
@@ -63,152 +60,872 @@ internal CssData()
/// the parsed css data
public static CssData Parse(RAdapter adapter, string stylesheet, bool combineWithDefault = true)
{
- CssParser parser = new CssParser(adapter);
+ var parser = new CssParser(adapter);
return parser.ParseStyleSheet(stylesheet, combineWithDefault);
}
///
- /// dictionary of media type to dictionary of css class name to the cssBlocks collection with all the data
+ /// Combine this CSS data's stylesheets with 's.
+ ///
+ /// the CSS data to combine with
+ public void Combine(CssData other)
+ {
+ ArgChecker.AssertArgNotNull(other, "other");
+ Stylesheets.AddRange(other.Stylesheets);
+ }
+
+ ///
+ /// Create a shallow copy of this css data - the returned instance has its own
+ /// list (so appending a new stylesheet to the clone doesn't affect the original), but the
+ /// instances themselves are shared (they're never mutated after parsing).
///
- internal IDictionary>> MediaBlocks
+ /// cloned object
+ public CssData Clone()
+ {
+ var clone = new CssData();
+ clone.Stylesheets.AddRange(Stylesheets);
+ return clone;
+ }
+
+ // --- Rule index -----------------------------------------------------------------------
+ //
+ // Matching every stylesheet rule against every CssBox in the document (the naive approach)
+ // is O(rules x boxes) and dominates cascade cost on large documents. Real browser engines
+ // avoid this by bucketing rules by the "subject" simple selector (the one that must match the
+ // box itself, e.g. the tag/class/id) so a box only needs to test the handful of rules that
+ // could plausibly match it, instead of the whole stylesheet. DoesSelectorMatch remains the
+ // source of truth for whether a rule actually matches - the index only narrows the candidates.
+ //
+ // Built lazily, once, the first time this CssData's rules are queried. Safe because by the
+ // time CascadeApplyStyles starts querying rules for the box tree, DomParser has already
+ // finished building/cloning CssData from ");
}
From f63ff1cfaa5a8eab300ae9c3f12afd7d9f8cd287 Mon Sep 17 00:00:00 2001
From: Justin Haygood
Date: Thu, 16 Jul 2026 19:02:18 -0400
Subject: [PATCH 02/21] Fix CalcSerializer
---
Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs
index 511bf39d2..39edbb0fa 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs
@@ -48,7 +48,7 @@ public static string Serialize(CalcNode node, CalcCategory category)
case UnaryCalcNode unary:
{
var value = FoldAngle(unary.Operand);
- return value is null ? null : unary.Negative ? -value.Value : value.Value;
+ return value is null ? null : (double?)(unary.Negative ? -value.Value : value.Value);
}
case BinaryCalcNode binary when binary.Operator == '+' || binary.Operator == '-':
From 0179a9ab1314b35f9c1c33a833f46c1b6e779a15 Mon Sep 17 00:00:00 2001
From: Justin Haygood
Date: Thu, 16 Jul 2026 21:11:58 -0400
Subject: [PATCH 03/21] Replace custom CSS with standards
The Demo app depending on 2 HTML-Renderer extensions for background gradients and border radius.
This ports a minimal version of the CSS spec compliant versions from PeachPDF
---
Source/Demo/Common/DemoUtils.cs | 4 +-
Source/Demo/Common/Samples/00.Intro.htm | 2 +-
.../Common/Samples/07.Additional features.htm | 81 ++--
Source/Demo/Common/Samples/08.Tooltip.htm | 5 +-
Source/Demo/Common/TestSamples/12.Text.htm | 16 +-
Source/Demo/Common/TestSamples/16.Borders.htm | 12 +-
.../Adapters/GradientBrushAdapter.cs | 46 ++
.../Adapters/GraphicsAdapter.cs | 72 ++++
.../Adapters/GraphicsPathAdapter.cs | 8 +-
.../Adapters/PdfSharpAdapter.cs | 13 +-
.../Adapters/GraphicsPathAdapter.cs | 4 +-
.../HtmlRenderer.WPF/Adapters/WpfAdapter.cs | 21 +-
.../Adapters/GraphicsPathAdapter.cs | 8 +-
.../Adapters/WinFormsAdapter.cs | 28 +-
Source/HtmlRenderer/Adapters/RAdapter.cs | 24 +-
Source/HtmlRenderer/Adapters/RGraphics.cs | 13 +-
Source/HtmlRenderer/Adapters/RGraphicsPath.cs | 15 +-
Source/HtmlRenderer/Core/CssDefaults.cs | 16 +-
Source/HtmlRenderer/Core/Dom/CssBox.cs | 22 +-
.../HtmlRenderer/Core/Dom/CssBoxProperties.cs | 396 +++++++++++-------
Source/HtmlRenderer/Core/Entities/CssImage.cs | 32 ++
.../Core/Entities/ParsedLinearGradient.cs | 18 +
.../Core/Handlers/BordersDrawHandler.cs | 59 +--
.../Core/Handlers/CssImagePainter.cs | 234 +++++++++++
Source/HtmlRenderer/Core/Parse/CssParser.cs | 10 +
.../HtmlRenderer/Core/Parse/CssValueParser.cs | 215 ++++++++++
Source/HtmlRenderer/Core/Utils/CssUtils.cs | 51 +--
Source/HtmlRenderer/Core/Utils/RenderUtils.cs | 56 ++-
28 files changed, 1112 insertions(+), 369 deletions(-)
create mode 100644 Source/HtmlRenderer.PdfSharp/Adapters/GradientBrushAdapter.cs
create mode 100644 Source/HtmlRenderer/Core/Entities/CssImage.cs
create mode 100644 Source/HtmlRenderer/Core/Entities/ParsedLinearGradient.cs
create mode 100644 Source/HtmlRenderer/Core/Handlers/CssImagePainter.cs
diff --git a/Source/Demo/Common/DemoUtils.cs b/Source/Demo/Common/DemoUtils.cs
index 682f325f5..a14b33b3f 100644
--- a/Source/Demo/Common/DemoUtils.cs
+++ b/Source/Demo/Common/DemoUtils.cs
@@ -74,8 +74,8 @@ public static string GetStylesheet(string src)
a:link { text-decoration: none; }
a:hover { text-decoration: underline; }
.gray { color:gray; }
- .example { background-color:#efefef; corner-radius:5px; padding:0.5em; }
- .whitehole { background-color:white; corner-radius:10px; padding:15px; }
+ .example { background-color:#efefef; border-radius:5px; padding:0.5em; }
+ .whitehole { background-color:white; border-radius:10px; padding:15px; }
.caption { font-size: 1.1em }
.comment { color: green; margin-bottom: 5px; margin-left: 3px; }
.comment2 { color: green; }";
diff --git a/Source/Demo/Common/Samples/00.Intro.htm b/Source/Demo/Common/Samples/00.Intro.htm
index d310818b4..7f9f9f565 100644
--- a/Source/Demo/Common/Samples/00.Intro.htm
+++ b/Source/Demo/Common/Samples/00.Intro.htm
@@ -3,7 +3,7 @@
Intro
-
+
HTML Renderer Project - $$Platform$$
diff --git a/Source/Demo/Common/Samples/07.Additional features.htm b/Source/Demo/Common/Samples/07.Additional features.htm
index 764e9511c..bd07880e9 100644
--- a/Source/Demo/Common/Samples/07.Additional features.htm
+++ b/Source/Demo/Common/Samples/07.Additional features.htm
@@ -5,20 +5,18 @@
@@ -54,7 +54,7 @@ Transparent text
elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu.
-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer_sagittis_Fusce_elementum_commodo_felis_Vivamus_lacinia_eleifend_libero_Donec
lacus.
diff --git a/Source/Demo/Common/TestSamples/16.Borders.htm b/Source/Demo/Common/TestSamples/16.Borders.htm
index 6fae0757c..117dfbcae 100644
--- a/Source/Demo/Common/TestSamples/16.Borders.htm
+++ b/Source/Demo/Common/TestSamples/16.Borders.htm
@@ -12,13 +12,13 @@
- border 1px with corner-radius 5px
+
+ border 1px with border-radius 5px
-
- border 2px with corner-radius 10px
+
+ border 2px with border-radius 10px
@@ -37,8 +37,8 @@
-
- dashed border 2px with corner-radius 10px
+
+ dashed border 2px with border-radius 10px
diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/GradientBrushAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/GradientBrushAdapter.cs
new file mode 100644
index 000000000..b92bf73d3
--- /dev/null
+++ b/Source/HtmlRenderer.PdfSharp/Adapters/GradientBrushAdapter.cs
@@ -0,0 +1,46 @@
+// "Therefore those skilled at the unorthodox
+// are infinite as heaven and earth,
+// inexhaustible as the great rivers.
+// When they come to an end,
+// they begin again,
+// like the days and months;
+// they die and are reborn,
+// like the four seasons."
+//
+// - Sun Tsu,
+// "The Art of War"
+
+using TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+
+namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters
+{
+ ///
+ /// A multi-stop linear gradient "brush" for the PdfSharp backend. Unlike ,
+ /// this does not wrap a real PdfSharp.Drawing.XBrush - XLinearGradientBrush in the
+ /// PDFsharp 1.50 package this project depends on only supports 2 colors, no stop list. Instead this
+ /// just carries the gradient line and stops; 's DrawPath/
+ /// DrawRectangle special-case this type and paint it as a series of adjacent 2-color
+ /// XLinearGradientBrush bands, one per consecutive stop pair - each band is a real 2-color
+ /// linear gradient by definition, so the composite is an exact piecewise-linear rendering, not an
+ /// approximation.
+ ///
+ internal sealed class GradientBrushAdapter : RBrush
+ {
+ public GradientBrushAdapter(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops)
+ {
+ P1 = p1;
+ P2 = p2;
+ Stops = stops;
+ }
+
+ public RPoint P1 { get; }
+
+ public RPoint P2 { get; }
+
+ public (RColor Color, double Position)[] Stops { get; }
+
+ public override void Dispose()
+ { }
+ }
+}
diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsAdapter.cs
index c6d406c58..76ede5ea1 100644
--- a/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsAdapter.cs
+++ b/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsAdapter.cs
@@ -155,6 +155,14 @@ public override void DrawRectangle(RPen pen, double x, double y, double width, d
public override void DrawRectangle(RBrush brush, double x, double y, double width, double height)
{
+ if (brush is GradientBrushAdapter gradient)
+ {
+ var rectPath = new XGraphicsPath();
+ rectPath.AddRectangle(x, y, width, height);
+ FillGradient(gradient, rectPath);
+ return;
+ }
+
var xBrush = ((BrushAdapter)brush).Brush;
var xTextureBrush = xBrush as XTextureBrush;
if (xTextureBrush != null)
@@ -188,6 +196,12 @@ public override void DrawPath(RPen pen, RGraphicsPath path)
public override void DrawPath(RBrush brush, RGraphicsPath path)
{
+ if (brush is GradientBrushAdapter gradient)
+ {
+ FillGradient(gradient, ((GraphicsPathAdapter)path).GraphicsPath);
+ return;
+ }
+
_g.DrawPath((XBrush)((BrushAdapter)brush).Brush, ((GraphicsPathAdapter)path).GraphicsPath);
}
@@ -199,6 +213,64 @@ public override void DrawPolygon(RBrush brush, RPoint[] points)
}
}
+ ///
+ /// Paints a multi-stop linear gradient by clipping to and drawing
+ /// one real 2-color band per consecutive stop pair, each
+ /// spanning the full perpendicular extent needed to cover the target - see
+ /// for why this backend needs banding instead of a single brush.
+ ///
+ private void FillGradient(GradientBrushAdapter gradient, XGraphicsPath targetPath)
+ {
+ var stops = gradient.Stops;
+ if (stops.Length == 0)
+ return;
+
+ _g.Save();
+ _g.IntersectClip(targetPath);
+
+ double dx = gradient.P2.X - gradient.P1.X;
+ double dy = gradient.P2.Y - gradient.P1.Y;
+ double len = Math.Sqrt(dx * dx + dy * dy);
+
+ if (stops.Length == 1 || len < 1e-6)
+ {
+ // Degenerate gradient line (single stop, or a zero-size box) - just flat-fill with the
+ // last color, matching what a real linear gradient converges to in that case.
+ var flatBrush = new XSolidBrush(Utils.Convert(stops[stops.Length - 1].Color));
+ _g.DrawRectangle(flatBrush, -1e5, -1e5, 2e5, 2e5);
+ _g.Restore();
+ _g.DrawRectangle(XBrushes.White, 0, 0, 0.1, 0.1);
+ return;
+ }
+
+ double ux = dx / len, uy = dy / len;
+ double perpX = -uy, perpY = ux;
+ double perpHalf = Math.Max(len, 1.0) * 4.0;
+
+ for (int i = 0; i < stops.Length - 1; i++)
+ {
+ double t1 = stops[i].Position, t2 = stops[i + 1].Position;
+ var bp1 = new XPoint(gradient.P1.X + ux * len * t1, gradient.P1.Y + uy * len * t1);
+ var bp2 = new XPoint(gradient.P1.X + ux * len * t2, gradient.P1.Y + uy * len * t2);
+
+ var band = new[]
+ {
+ new XPoint(bp1.X - perpX * perpHalf, bp1.Y - perpY * perpHalf),
+ new XPoint(bp1.X + perpX * perpHalf, bp1.Y + perpY * perpHalf),
+ new XPoint(bp2.X + perpX * perpHalf, bp2.Y + perpY * perpHalf),
+ new XPoint(bp2.X - perpX * perpHalf, bp2.Y - perpY * perpHalf),
+ };
+
+ var bandBrush = new XLinearGradientBrush(bp1, bp2, Utils.Convert(stops[i].Color), Utils.Convert(stops[i + 1].Color));
+ _g.DrawPolygon(bandBrush, band, XFillMode.Winding);
+ }
+
+ _g.Restore();
+
+ // handle bug in PdfSharp that keeps the brush color for next string draw
+ _g.DrawRectangle(XBrushes.White, 0, 0, 0.1, 0.1);
+ }
+
#endregion
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsPathAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsPathAdapter.cs
index 48290543c..9e057d8e9 100644
--- a/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsPathAdapter.cs
+++ b/Source/HtmlRenderer.PdfSharp/Adapters/GraphicsPathAdapter.cs
@@ -51,11 +51,11 @@ public override void LineTo(double x, double y)
_lastPoint = new RPoint(x, y);
}
- public override void ArcTo(double x, double y, double size, Corner corner)
+ public override void ArcTo(double x, double y, double radiusX, double radiusY, Corner corner)
{
- float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? size : 0));
- float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? size : 0));
- _graphicsPath.AddArc(left, top, (float)size * 2, (float)size * 2, GetStartAngle(corner), 90);
+ float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? radiusX : 0));
+ float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? radiusY : 0));
+ _graphicsPath.AddArc(left, top, (float)radiusX * 2, (float)radiusY * 2, GetStartAngle(corner), 90);
_lastPoint = new RPoint(x, y);
}
diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs
index e5fa309c0..f286da15b 100644
--- a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs
+++ b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs
@@ -93,18 +93,9 @@ protected override RBrush CreateSolidBrush(RColor color)
return new BrushAdapter(solidBrush);
}
- protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle)
+ protected override RBrush CreateLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops)
{
- XLinearGradientMode mode;
- if (angle < 45)
- mode = XLinearGradientMode.ForwardDiagonal;
- else if (angle < 90)
- mode = XLinearGradientMode.Vertical;
- else if (angle < 135)
- mode = XLinearGradientMode.BackwardDiagonal;
- else
- mode = XLinearGradientMode.Horizontal;
- return new BrushAdapter(new XLinearGradientBrush(Utils.Convert(rect), Utils.Convert(color1), Utils.Convert(color2), mode));
+ return new GradientBrushAdapter(p1, p2, stops);
}
protected override RImage ConvertImageInt(object image)
diff --git a/Source/HtmlRenderer.WPF/Adapters/GraphicsPathAdapter.cs b/Source/HtmlRenderer.WPF/Adapters/GraphicsPathAdapter.cs
index a6ffb66bb..af19357b4 100644
--- a/Source/HtmlRenderer.WPF/Adapters/GraphicsPathAdapter.cs
+++ b/Source/HtmlRenderer.WPF/Adapters/GraphicsPathAdapter.cs
@@ -46,9 +46,9 @@ public override void LineTo(double x, double y)
_geometryContext.LineTo(new Point(x, y), true, true);
}
- public override void ArcTo(double x, double y, double size, Corner corner)
+ public override void ArcTo(double x, double y, double radiusX, double radiusY, Corner corner)
{
- _geometryContext.ArcTo(new Point(x, y), new Size(size, size), 0, false, SweepDirection.Clockwise, true, true);
+ _geometryContext.ArcTo(new Point(x, y), new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, true);
}
///
diff --git a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs
index 3ea88d9c4..4de4c8777 100644
--- a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs
+++ b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs
@@ -102,14 +102,19 @@ protected override RBrush CreateSolidBrush(RColor color)
return new BrushAdapter(solidBrush);
}
- protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle)
- {
- var startColor = angle <= 180 ? Utils.Convert(color1) : Utils.Convert(color2);
- var endColor = angle <= 180 ? Utils.Convert(color2) : Utils.Convert(color1);
- angle = angle <= 180 ? angle : angle - 180;
- double x = angle < 135 ? Math.Max((angle - 45) / 90, 0) : 1;
- double y = angle <= 45 ? Math.Max(0.5 - angle / 90, 0) : angle > 135 ? Math.Abs(1.5 - angle / 90) : 0;
- return new BrushAdapter(new LinearGradientBrush(startColor, endColor, new Point(x, y), new Point(1 - x, 1 - y)));
+ protected override RBrush CreateLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops)
+ {
+ var gradientStops = new GradientStopCollection(stops.Length);
+ foreach (var stop in stops)
+ gradientStops.Add(new GradientStop(Utils.Convert(stop.Color), stop.Position));
+
+ var brush = new LinearGradientBrush(gradientStops, 0)
+ {
+ MappingMode = BrushMappingMode.Absolute,
+ StartPoint = Utils.Convert(p1),
+ EndPoint = Utils.Convert(p2)
+ };
+ return new BrushAdapter(brush);
}
protected override RImage ConvertImageInt(object image)
diff --git a/Source/HtmlRenderer.WinForms/Adapters/GraphicsPathAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/GraphicsPathAdapter.cs
index 6c4bbd061..d3a7b3f2c 100644
--- a/Source/HtmlRenderer.WinForms/Adapters/GraphicsPathAdapter.cs
+++ b/Source/HtmlRenderer.WinForms/Adapters/GraphicsPathAdapter.cs
@@ -51,11 +51,11 @@ public override void LineTo(double x, double y)
_lastPoint = new RPoint(x, y);
}
- public override void ArcTo(double x, double y, double size, Corner corner)
+ public override void ArcTo(double x, double y, double radiusX, double radiusY, Corner corner)
{
- float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? size : 0));
- float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? size : 0));
- _graphicsPath.AddArc(left, top, (float)size * 2, (float)size * 2, GetStartAngle(corner), 90);
+ float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? radiusX : 0));
+ float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? radiusY : 0));
+ _graphicsPath.AddArc(left, top, (float)radiusX * 2, (float)radiusY * 2, GetStartAngle(corner), 90);
_lastPoint = new RPoint(x, y);
}
diff --git a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
index 16bc83db8..10da9891a 100644
--- a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
+++ b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
@@ -10,6 +10,7 @@
// - Sun Tsu,
// "The Art of War"
+using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
@@ -83,9 +84,32 @@ protected override RBrush CreateSolidBrush(RColor color)
return new BrushAdapter(solidBrush, false);
}
- protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle)
+ protected override RBrush CreateLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops)
{
- return new BrushAdapter(new LinearGradientBrush(Utils.Convert(rect), Utils.Convert(color1), Utils.Convert(color2), (float)angle), true);
+ var brush = new LinearGradientBrush(Utils.Convert(p1), Utils.Convert(p2), Color.Black, Color.Black);
+
+ var colors = new Color[stops.Length];
+ var positions = new float[stops.Length];
+ for (int i = 0; i < stops.Length; i++)
+ {
+ colors[i] = Utils.Convert(stops[i].Color);
+ var pos = (float)Math.Min(Math.Max(stops[i].Position, 0.0), 1.0);
+ // GDI+ requires strictly increasing positions - nudge any duplicate up by an epsilon.
+ positions[i] = i > 0 && pos <= positions[i - 1] ? positions[i - 1] + 0.0001f : pos;
+ }
+ // GDI+ requires the first/last position to be exactly 0/1 - forcing them here (rather than
+ // requiring the caller to pre-normalize) also matches spec behavior for a gradient whose
+ // outermost stops aren't at the very ends: the outermost color simply extends flat to the edge.
+ positions[0] = 0f;
+ positions[positions.Length - 1] = 1f;
+
+ brush.InterpolationColors = new ColorBlend
+ {
+ Colors = colors,
+ Positions = positions
+ };
+
+ return new BrushAdapter(brush, true);
}
protected override RImage ConvertImageInt(object image)
diff --git a/Source/HtmlRenderer/Adapters/RAdapter.cs b/Source/HtmlRenderer/Adapters/RAdapter.cs
index 8a5afc3b9..1a19f7a57 100644
--- a/Source/HtmlRenderer/Adapters/RAdapter.cs
+++ b/Source/HtmlRenderer/Adapters/RAdapter.cs
@@ -139,16 +139,15 @@ public RBrush GetSolidBrush(RColor color)
}
///
- /// Get linear gradient color brush from to .
+ /// Get a multi-stop linear gradient brush along the line from to .
///
- /// the rectangle to get the brush for
- /// the start color of the gradient
- /// the end color of the gradient
- /// the angle to move the gradient from start color to end color in the rectangle
+ /// the gradient line's start point
+ /// the gradient line's end point
+ /// color stops, each with a position in [0,1] along the gradient line
/// linear gradient color brush instance
- public RBrush GetLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle)
+ public RBrush GetLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops)
{
- return CreateLinearGradientBrush(rect, color1, color2, angle);
+ return CreateLinearGradientBrush(p1, p2, stops);
}
///
@@ -360,14 +359,13 @@ internal RFont CreateFont(RFontFamily family, double size, RFontStyle style)
protected abstract RBrush CreateSolidBrush(RColor color);
///
- /// Get linear gradient color brush from to .
+ /// Get a multi-stop linear gradient brush along the line from to .
///
- /// the rectangle to get the brush for
- /// the start color of the gradient
- /// the end color of the gradient
- /// the angle to move the gradient from start color to end color in the rectangle
+ /// the gradient line's start point
+ /// the gradient line's end point
+ /// color stops, each with a position in [0,1] along the gradient line
/// linear gradient color brush instance
- protected abstract RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle);
+ protected abstract RBrush CreateLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops);
///
/// Convert image object returned from to .
diff --git a/Source/HtmlRenderer/Adapters/RGraphics.cs b/Source/HtmlRenderer/Adapters/RGraphics.cs
index af54f2ae4..22ba0df77 100644
--- a/Source/HtmlRenderer/Adapters/RGraphics.cs
+++ b/Source/HtmlRenderer/Adapters/RGraphics.cs
@@ -76,16 +76,15 @@ public RBrush GetSolidBrush(RColor color)
}
///
- /// Get linear gradient color brush from to .
+ /// Get a multi-stop linear gradient brush along the line from to .
///
- /// the rectangle to get the brush for
- /// the start color of the gradient
- /// the end color of the gradient
- /// the angle to move the gradient from start color to end color in the rectangle
+ /// the gradient line's start point
+ /// the gradient line's end point
+ /// color stops, each with a position in [0,1] along the gradient line
/// linear gradient color brush instance
- public RBrush GetLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle)
+ public RBrush GetLinearGradientBrush(RPoint p1, RPoint p2, (RColor Color, double Position)[] stops)
{
- return _adapter.GetLinearGradientBrush(rect, color1, color2, angle);
+ return _adapter.GetLinearGradientBrush(p1, p2, stops);
}
///
diff --git a/Source/HtmlRenderer/Adapters/RGraphicsPath.cs b/Source/HtmlRenderer/Adapters/RGraphicsPath.cs
index 21c86bc1a..474781a5e 100644
--- a/Source/HtmlRenderer/Adapters/RGraphicsPath.cs
+++ b/Source/HtmlRenderer/Adapters/RGraphicsPath.cs
@@ -30,10 +30,19 @@ public abstract class RGraphicsPath : IDisposable
public abstract void LineTo(double x, double y);
///
- /// Add circular arc of the given size to the given point from the last point.
+ /// Add an elliptical arc with independent horizontal (X) and vertical (Y) radii to the given
+ /// point from the last point - supports elliptical border-radius corners.
///
- public abstract void ArcTo(double x, double y, double size, Corner corner);
-
+ public abstract void ArcTo(double x, double y, double radiusX, double radiusY, Corner corner);
+
+ ///
+ /// Add a circular arc of the given size to the given point from the last point.
+ ///
+ public void ArcTo(double x, double y, double size, Corner corner)
+ {
+ ArcTo(x, y, size, size, corner);
+ }
+
///
/// Release path resources.
///
diff --git a/Source/HtmlRenderer/Core/CssDefaults.cs b/Source/HtmlRenderer/Core/CssDefaults.cs
index 01e35959a..ec9fce4d9 100644
--- a/Source/HtmlRenderer/Core/CssDefaults.cs
+++ b/Source/HtmlRenderer/Core/CssDefaults.cs
@@ -119,9 +119,8 @@ @media print {
/*This is the background of the HtmlToolTip*/
.htmltooltip {
border:solid 1px #767676;
- background-color:white;
- background-gradient:#E4E5F0;
- padding: 8px;
+ background-color:#E4E5F0;
+ padding: 8px;
Font: 9pt Tahoma;
}";
@@ -139,8 +138,6 @@ @media print {
new System.Collections.Generic.Dictionary(System.StringComparer.OrdinalIgnoreCase)
{
{ "background-color", "transparent" },
- { "background-gradient", "none" },
- { "background-gradient-angle", "90" },
{ "background-image", "none" },
{ "background-position", "0% 0%" },
{ "background-repeat", "repeat" },
@@ -160,11 +157,10 @@ @media print {
{ "border-collapse", "separate" },
{ "color", "black" },
{ "content", "normal" },
- { "corner-nw-radius", "0" },
- { "corner-ne-radius", "0" },
- { "corner-se-radius", "0" },
- { "corner-sw-radius", "0" },
- { "corner-radius", "0" },
+ { "border-top-left-radius", "0 0" },
+ { "border-top-right-radius", "0 0" },
+ { "border-bottom-right-radius", "0 0" },
+ { "border-bottom-left-radius", "0 0" },
{ "empty-cells", "show" },
{ "direction", "ltr" },
{ "display", "inline" },
diff --git a/Source/HtmlRenderer/Core/Dom/CssBox.cs b/Source/HtmlRenderer/Core/Dom/CssBox.cs
index 6a7140a0d..b01a818ff 100644
--- a/Source/HtmlRenderer/Core/Dom/CssBox.cs
+++ b/Source/HtmlRenderer/Core/Dom/CssBox.cs
@@ -729,10 +729,10 @@ internal virtual void MeasureWordsSize(RGraphics g)
{
if (!_wordsSizeMeasured)
{
- if (BackgroundImage != CssConstants.None && _imageLoadHandler == null)
+ if (ActualBackgroundImage is CssImage.Url urlImage && _imageLoadHandler == null)
{
_imageLoadHandler = new ImageLoadHandler(HtmlContainer, OnImageLoadComplete);
- _imageLoadHandler.LoadImage(BackgroundImage, HtmlTag != null ? HtmlTag.Attributes : null);
+ _imageLoadHandler.LoadImage(urlImage.Href, HtmlTag != null ? HtmlTag.Attributes : null);
}
MeasureWordSpacing(g);
@@ -1320,15 +1320,18 @@ protected void PaintBackground(RGraphics g, RRect rect, bool isFirst, bool isLas
{
RBrush brush = null;
- if (BackgroundGradient != CssConstants.None)
+ var backgroundImage = ActualBackgroundImage;
+ if (backgroundImage is CssImage.LinearGradient linearGradient)
{
- brush = g.GetLinearGradientBrush(rect, ActualBackgroundColor, ActualBackgroundGradient, ActualBackgroundGradientAngle);
+ brush = CssImagePainter.GetLinearGradientBrush(g, rect, linearGradient.Gradient);
}
else if (RenderUtils.IsColorVisible(ActualBackgroundColor))
{
brush = g.GetSolidBrush(ActualBackgroundColor);
}
+ var rad = ComputeRadii(rect);
+
if (brush != null)
{
// TODO:a handle it correctly (tables background)
@@ -1336,13 +1339,13 @@ protected void PaintBackground(RGraphics g, RRect rect, bool isFirst, bool isLas
// rectangle.Width -= ActualWordSpacing + CssUtils.GetWordEndWhitespace(ActualFont);
RGraphicsPath roundrect = null;
- if (IsRounded)
+ if (rad.IsRounded)
{
- roundrect = RenderUtils.GetRoundRect(g, rect, ActualCornerNw, ActualCornerNe, ActualCornerSe, ActualCornerSw);
+ roundrect = RenderUtils.GetRoundRect(g, rect, rad.TLX, rad.TLY, rad.TRX, rad.TRY, rad.BRX, rad.BRY, rad.BLX, rad.BLY);
}
Object prevMode = null;
- if (HtmlContainer != null && !HtmlContainer.AvoidGeometryAntialias && IsRounded)
+ if (HtmlContainer != null && !HtmlContainer.AvoidGeometryAntialias && rad.IsRounded)
{
prevMode = g.SetAntiAliasSmoothingMode();
}
@@ -1543,6 +1546,11 @@ protected override RColor GetActualColor(string colorStr)
return HtmlContainer.CssParser.ParseColor(colorStr);
}
+ protected override CssImage GetActualBackgroundImageValue(string value)
+ {
+ return HtmlContainer.CssParser.ParseBackgroundImage(value);
+ }
+
protected override RPoint GetActualLocation(string X, string Y)
{
var left = CssValueParser.ParseLength(X, this.HtmlContainer.PageSize.Width, this, null);
diff --git a/Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs b/Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs
index 6aefc5f2e..d5c9b59d8 100644
--- a/Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs
+++ b/Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs
@@ -16,6 +16,7 @@
using System.Text.RegularExpressions;
using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
using TheArtOfDev.HtmlRenderer.Core.Parse;
using TheArtOfDev.HtmlRenderer.Core.Utils;
@@ -33,8 +34,6 @@ internal abstract class CssBoxProperties
#region CSS Fields
private string _backgroundColor = "transparent";
- private string _backgroundGradient = "none";
- private string _backgroundGradientAngle = "90";
private string _backgroundImage = "none";
private string _backgroundPosition = "0% 0%";
private string _backgroundRepeat = "repeat";
@@ -55,11 +54,10 @@ internal abstract class CssBoxProperties
private string _bottom;
private string _color = "black";
private string _content = "normal";
- private string _cornerNwRadius = "0";
- private string _cornerNeRadius = "0";
- private string _cornerSeRadius = "0";
- private string _cornerSwRadius = "0";
- private string _cornerRadius = "0";
+ private string _borderTopLeftRadius = "0 0";
+ private string _borderTopRightRadius = "0 0";
+ private string _borderBottomRightRadius = "0 0";
+ private string _borderBottomLeftRadius = "0 0";
private string _emptyCells = "show";
private string _direction = "ltr";
private string _display = "inline";
@@ -121,12 +119,15 @@ internal abstract class CssBoxProperties
///
private RSize _size;
- private double _actualCornerNw = double.NaN;
- private double _actualCornerNe = double.NaN;
- private double _actualCornerSw = double.NaN;
- private double _actualCornerSe = double.NaN;
+ private double _actualBorderTopLeftRadiusX = double.NaN;
+ private double _actualBorderTopLeftRadiusY = double.NaN;
+ private double _actualBorderTopRightRadiusX = double.NaN;
+ private double _actualBorderTopRightRadiusY = double.NaN;
+ private double _actualBorderBottomRightRadiusX = double.NaN;
+ private double _actualBorderBottomRightRadiusY = double.NaN;
+ private double _actualBorderBottomLeftRadiusX = double.NaN;
+ private double _actualBorderBottomLeftRadiusY = double.NaN;
private RColor _actualColor = RColor.Empty;
- private double _actualBackgroundGradientAngle = double.NaN;
private double _actualHeight = double.NaN;
private double _actualWidth = double.NaN;
private double _actualPaddingTop = double.NaN;
@@ -152,7 +153,8 @@ internal abstract class CssBoxProperties
private double _actualTextIndent = double.NaN;
private double _actualBorderSpacingHorizontal = double.NaN;
private double _actualBorderSpacingVertical = double.NaN;
- private RColor _actualBackgroundGradient = RColor.Empty;
+ private bool _actualBackgroundImageComputed;
+ private CssImage _actualBackgroundImage;
private RColor _actualBorderTopColor = RColor.Empty;
private RColor _actualBorderLeftColor = RColor.Empty;
private RColor _actualBorderBottomColor = RColor.Empty;
@@ -281,66 +283,96 @@ public string BorderCollapse
set { _borderCollapse = value; }
}
- public string CornerRadius
+ ///
+ /// The "border-radius" shorthand: sets all four corners at once, with an optional "/" separating
+ /// horizontal and vertical radii for elliptical corners (e.g. "10px 5px / 20px 10px"). Each side
+ /// of the "/" independently follows the standard 1/2/3/4-value CSS expansion rule
+ /// (1=all, 2=TL+BR/TR+BL, 3=TL/TR+BL/BR, 4=TL/TR/BR/BL). Write-only, like the other border-*
+ /// shorthands in this class - the four longhand properties below hold the actual state.
+ ///
+ public string BorderRadius
{
- get { return _cornerRadius; }
set
{
- MatchCollection r = RegexParserUtils.Match(RegexParserUtils.CssLength, value);
+ int slash = value.IndexOf('/');
+ string hGroup = (slash >= 0 ? value.Substring(0, slash) : value).Trim();
+ string vGroup = (slash >= 0 ? value.Substring(slash + 1) : hGroup).Trim();
- switch (r.Count)
- {
- case 1:
- CornerNeRadius = r[0].Value;
- CornerNwRadius = r[0].Value;
- CornerSeRadius = r[0].Value;
- CornerSwRadius = r[0].Value;
- break;
- case 2:
- CornerNeRadius = r[0].Value;
- CornerNwRadius = r[0].Value;
- CornerSeRadius = r[1].Value;
- CornerSwRadius = r[1].Value;
- break;
- case 3:
- CornerNeRadius = r[0].Value;
- CornerNwRadius = r[1].Value;
- CornerSeRadius = r[2].Value;
- break;
- case 4:
- CornerNeRadius = r[0].Value;
- CornerNwRadius = r[1].Value;
- CornerSeRadius = r[2].Value;
- CornerSwRadius = r[3].Value;
- break;
- }
+ string[] h = ExpandRadiusShorthand(hGroup);
+ string[] v = ExpandRadiusShorthand(vGroup);
- _cornerRadius = value;
+ BorderTopLeftRadius = h[0] + " " + v[0];
+ BorderTopRightRadius = h[1] + " " + v[1];
+ BorderBottomRightRadius = h[2] + " " + v[2];
+ BorderBottomLeftRadius = h[3] + " " + v[3];
}
}
- public string CornerNwRadius
+ ///
+ /// Expands a 1-4 value length group into [TopLeft, TopRight, BottomRight, BottomLeft] per the
+ /// standard CSS shorthand rule.
+ ///
+ private static string[] ExpandRadiusShorthand(string group)
{
- get { return _cornerNwRadius; }
- set { _cornerNwRadius = value; }
+ MatchCollection r = RegexParserUtils.Match(RegexParserUtils.CssLength, group);
+
+ switch (r.Count)
+ {
+ case 1:
+ return new[] { r[0].Value, r[0].Value, r[0].Value, r[0].Value };
+ case 2:
+ return new[] { r[0].Value, r[1].Value, r[0].Value, r[1].Value };
+ case 3:
+ return new[] { r[0].Value, r[1].Value, r[2].Value, r[1].Value };
+ case 4:
+ return new[] { r[0].Value, r[1].Value, r[2].Value, r[3].Value };
+ default:
+ return new[] { "0", "0", "0", "0" };
+ }
}
- public string CornerNeRadius
+ public string BorderTopLeftRadius
{
- get { return _cornerNeRadius; }
- set { _cornerNeRadius = value; }
+ get { return _borderTopLeftRadius; }
+ set
+ {
+ _borderTopLeftRadius = value;
+ _actualBorderTopLeftRadiusX = double.NaN;
+ _actualBorderTopLeftRadiusY = double.NaN;
+ }
}
- public string CornerSeRadius
+ public string BorderTopRightRadius
{
- get { return _cornerSeRadius; }
- set { _cornerSeRadius = value; }
+ get { return _borderTopRightRadius; }
+ set
+ {
+ _borderTopRightRadius = value;
+ _actualBorderTopRightRadiusX = double.NaN;
+ _actualBorderTopRightRadiusY = double.NaN;
+ }
}
- public string CornerSwRadius
+ public string BorderBottomRightRadius
{
- get { return _cornerSwRadius; }
- set { _cornerSwRadius = value; }
+ get { return _borderBottomRightRadius; }
+ set
+ {
+ _borderBottomRightRadius = value;
+ _actualBorderBottomRightRadiusX = double.NaN;
+ _actualBorderBottomRightRadiusY = double.NaN;
+ }
+ }
+
+ public string BorderBottomLeftRadius
+ {
+ get { return _borderBottomLeftRadius; }
+ set
+ {
+ _borderBottomLeftRadius = value;
+ _actualBorderBottomLeftRadiusX = double.NaN;
+ _actualBorderBottomLeftRadiusY = double.NaN;
+ }
}
public string MarginBottom
@@ -471,7 +503,11 @@ public string BackgroundColor
public string BackgroundImage
{
get { return _backgroundImage; }
- set { _backgroundImage = value; }
+ set
+ {
+ _backgroundImage = value;
+ _actualBackgroundImageComputed = false;
+ }
}
public string BackgroundPosition
@@ -486,18 +522,6 @@ public string BackgroundRepeat
set { _backgroundRepeat = value; }
}
- public string BackgroundGradient
- {
- get { return _backgroundGradient; }
- set { _backgroundGradient = value; }
- }
-
- public string BackgroundGradientAngle
- {
- get { return _backgroundGradientAngle; }
- set { _backgroundGradientAngle = value; }
- }
-
public string Color
{
get { return _color; }
@@ -1066,6 +1090,26 @@ public RColor ActualBorderTopColor
protected abstract RColor GetActualColor(string colorStr);
+ protected abstract CssImage GetActualBackgroundImageValue(string value);
+
+ ///
+ /// Gets the parsed "background-image" value - either a url() reference or a linear-gradient(),
+ /// lazily resolved (needs adapter-dependent named-color lookup for gradient stops, see
+ /// ) and cached until is set again.
+ ///
+ public CssImage ActualBackgroundImage
+ {
+ get
+ {
+ if (!_actualBackgroundImageComputed)
+ {
+ _actualBackgroundImage = GetActualBackgroundImageValue(BackgroundImage);
+ _actualBackgroundImageComputed = true;
+ }
+ return _actualBackgroundImage;
+ }
+ }
+
///
/// Gets the actual Left border Color
///
@@ -1111,72 +1155,147 @@ public RColor ActualBorderRightColor
}
}
- ///
- /// Gets the actual length of the north west corner
- ///
- public double ActualCornerNw
+ public double ActualBorderTopLeftRadiusX
{
get
{
- if (double.IsNaN(_actualCornerNw))
- {
- _actualCornerNw = CssValueParser.ParseLength(CornerNwRadius, 0, this);
- }
- return _actualCornerNw;
+ if (double.IsNaN(_actualBorderTopLeftRadiusX))
+ _actualBorderTopLeftRadiusX = CssValueParser.ParseLength(FirstCssValue(BorderTopLeftRadius), Size.Width, this);
+ return _actualBorderTopLeftRadiusX;
}
}
- ///
- /// Gets the actual length of the north east corner
- ///
- public double ActualCornerNe
+ public double ActualBorderTopLeftRadiusY
{
get
{
- if (double.IsNaN(_actualCornerNe))
- {
- _actualCornerNe = CssValueParser.ParseLength(CornerNeRadius, 0, this);
- }
- return _actualCornerNe;
+ if (double.IsNaN(_actualBorderTopLeftRadiusY))
+ _actualBorderTopLeftRadiusY = CssValueParser.ParseLength(SecondCssValue(BorderTopLeftRadius), Size.Height, this);
+ return _actualBorderTopLeftRadiusY;
}
}
- ///
- /// Gets the actual length of the south east corner
- ///
- public double ActualCornerSe
+ public double ActualBorderTopRightRadiusX
{
get
{
- if (double.IsNaN(_actualCornerSe))
- {
- _actualCornerSe = CssValueParser.ParseLength(CornerSeRadius, 0, this);
- }
- return _actualCornerSe;
+ if (double.IsNaN(_actualBorderTopRightRadiusX))
+ _actualBorderTopRightRadiusX = CssValueParser.ParseLength(FirstCssValue(BorderTopRightRadius), Size.Width, this);
+ return _actualBorderTopRightRadiusX;
}
}
- ///
- /// Gets the actual length of the south west corner
- ///
- public double ActualCornerSw
+ public double ActualBorderTopRightRadiusY
{
get
{
- if (double.IsNaN(_actualCornerSw))
- {
- _actualCornerSw = CssValueParser.ParseLength(CornerSwRadius, 0, this);
- }
- return _actualCornerSw;
+ if (double.IsNaN(_actualBorderTopRightRadiusY))
+ _actualBorderTopRightRadiusY = CssValueParser.ParseLength(SecondCssValue(BorderTopRightRadius), Size.Height, this);
+ return _actualBorderTopRightRadiusY;
+ }
+ }
+
+ public double ActualBorderBottomRightRadiusX
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderBottomRightRadiusX))
+ _actualBorderBottomRightRadiusX = CssValueParser.ParseLength(FirstCssValue(BorderBottomRightRadius), Size.Width, this);
+ return _actualBorderBottomRightRadiusX;
+ }
+ }
+
+ public double ActualBorderBottomRightRadiusY
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderBottomRightRadiusY))
+ _actualBorderBottomRightRadiusY = CssValueParser.ParseLength(SecondCssValue(BorderBottomRightRadius), Size.Height, this);
+ return _actualBorderBottomRightRadiusY;
+ }
+ }
+
+ public double ActualBorderBottomLeftRadiusX
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderBottomLeftRadiusX))
+ _actualBorderBottomLeftRadiusX = CssValueParser.ParseLength(FirstCssValue(BorderBottomLeftRadius), Size.Width, this);
+ return _actualBorderBottomLeftRadiusX;
+ }
+ }
+
+ public double ActualBorderBottomLeftRadiusY
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderBottomLeftRadiusY))
+ _actualBorderBottomLeftRadiusY = CssValueParser.ParseLength(SecondCssValue(BorderBottomLeftRadius), Size.Height, this);
+ return _actualBorderBottomLeftRadiusY;
}
}
///
- /// Gets a value indicating if at least one of the corners of the box is rounded
+ /// Returns the first top-level-whitespace-delimited token in a "border-*-radius" longhand value
+ /// (the horizontal radius).
+ ///
+ private static string FirstCssValue(string value)
+ {
+ foreach (var token in CssValueParser.SplitTopLevelWhitespace(value))
+ return token;
+ return value;
+ }
+
+ ///
+ /// Returns the second top-level-whitespace-delimited token in a "border-*-radius" longhand value
+ /// (the vertical radius), or the first if there is no second (spec: omitted v-radius = h-radius).
+ ///
+ private static string SecondCssValue(string value)
+ {
+ var tokens = new List(CssValueParser.SplitTopLevelWhitespace(value));
+ return tokens.Count > 1 ? tokens[1] : value;
+ }
+
+ ///
+ /// Computes overlap-reduced corner radii for the given rendering rectangle, per CSS Backgrounds
+ /// §4. Horizontal and vertical axes are reduced independently so that, e.g., two large radii on
+ /// the same edge never sum to more than that edge's length.
+ ///
+ public BorderRadii ComputeRadii(RRect rect)
+ {
+ double tlX = ActualBorderTopLeftRadiusX, tlY = ActualBorderTopLeftRadiusY;
+ double trX = ActualBorderTopRightRadiusX, trY = ActualBorderTopRightRadiusY;
+ double brX = ActualBorderBottomRightRadiusX, brY = ActualBorderBottomRightRadiusY;
+ double blX = ActualBorderBottomLeftRadiusX, blY = ActualBorderBottomLeftRadiusY;
+
+ // Horizontal reduction: check top side and bottom side independently.
+ double fTop = tlX + trX > 0 && rect.Width > 0 ? rect.Width / (tlX + trX) : 1.0;
+ double fBot = blX + brX > 0 && rect.Width > 0 ? rect.Width / (blX + brX) : 1.0;
+ double fX = Math.Min(1.0, Math.Min(fTop, fBot));
+
+ // Vertical reduction: check left side and right side independently.
+ double fLeft = tlY + blY > 0 && rect.Height > 0 ? rect.Height / (tlY + blY) : 1.0;
+ double fRight = trY + brY > 0 && rect.Height > 0 ? rect.Height / (trY + brY) : 1.0;
+ double fY = Math.Min(1.0, Math.Min(fLeft, fRight));
+
+ return new BorderRadii(tlX * fX, tlY * fY, trX * fX, trY * fY,
+ brX * fX, brY * fY, blX * fX, blY * fY);
+ }
+
+ ///
+ /// Gets a value indicating if at least one of the corners of the box is rounded. A fast,
+ /// rect-independent pre-check using the raw (not overlap-reduced) actual radii - use
+ /// when the actual per-corner values are needed for painting.
///
public bool IsRounded
{
- get { return ActualCornerNe > 0f || ActualCornerNw > 0f || ActualCornerSe > 0f || ActualCornerSw > 0f; }
+ get
+ {
+ return ActualBorderTopLeftRadiusX > 0 || ActualBorderTopLeftRadiusY > 0 ||
+ ActualBorderTopRightRadiusX > 0 || ActualBorderTopRightRadiusY > 0 ||
+ ActualBorderBottomRightRadiusX > 0 || ActualBorderBottomRightRadiusY > 0 ||
+ ActualBorderBottomLeftRadiusX > 0 || ActualBorderBottomLeftRadiusY > 0;
+ }
}
///
@@ -1220,37 +1339,6 @@ public RColor ActualBackgroundColor
}
}
- ///
- /// Gets the second color that creates a gradient for the background
- ///
- public RColor ActualBackgroundGradient
- {
- get
- {
- if (_actualBackgroundGradient.IsEmpty)
- {
- _actualBackgroundGradient = GetActualColor(BackgroundGradient);
- }
- return _actualBackgroundGradient;
- }
- }
-
- ///
- /// Gets the actual angle specified for the background gradient
- ///
- public double ActualBackgroundGradientAngle
- {
- get
- {
- if (double.IsNaN(_actualBackgroundGradientAngle))
- {
- _actualBackgroundGradientAngle = CssValueParser.ParseNumber(BackgroundGradientAngle, 360f);
- }
-
- return _actualBackgroundGradientAngle;
- }
- }
-
///
/// Gets the actual font of the parent
///
@@ -1528,8 +1616,6 @@ protected void InheritStyle(CssBox p, bool everything)
if (everything)
{
_backgroundColor = p._backgroundColor;
- _backgroundGradient = p._backgroundGradient;
- _backgroundGradientAngle = p._backgroundGradientAngle;
_backgroundImage = p._backgroundImage;
_backgroundPosition = p._backgroundPosition;
_backgroundRepeat = p._backgroundRepeat;
@@ -1546,11 +1632,10 @@ protected void InheritStyle(CssBox p, bool everything)
_borderBottomStyle = p._borderBottomStyle;
_borderLeftStyle = p._borderLeftStyle;
_bottom = p._bottom;
- _cornerNwRadius = p._cornerNwRadius;
- _cornerNeRadius = p._cornerNeRadius;
- _cornerSeRadius = p._cornerSeRadius;
- _cornerSwRadius = p._cornerSwRadius;
- _cornerRadius = p._cornerRadius;
+ _borderTopLeftRadius = p._borderTopLeftRadius;
+ _borderTopRightRadius = p._borderTopRightRadius;
+ _borderBottomRightRadius = p._borderBottomRightRadius;
+ _borderBottomLeftRadius = p._borderBottomLeftRadius;
_display = p._display;
_float = p._float;
_height = p._height;
@@ -1577,4 +1662,27 @@ protected void InheritStyle(CssBox p, bool everything)
}
}
}
+
+ ///
+ /// Holds the eight computed (overlap-reduced) corner radii for a box rectangle - see
+ /// .
+ ///
+ internal struct BorderRadii
+ {
+ public readonly double TLX, TLY, TRX, TRY, BRX, BRY, BLX, BLY;
+
+ public BorderRadii(double tlX, double tlY, double trX, double trY,
+ double brX, double brY, double blX, double blY)
+ {
+ TLX = tlX; TLY = tlY;
+ TRX = trX; TRY = trY;
+ BRX = brX; BRY = brY;
+ BLX = blX; BLY = blY;
+ }
+
+ public bool IsRounded
+ {
+ get { return TLX > 0 || TLY > 0 || TRX > 0 || TRY > 0 || BRX > 0 || BRY > 0 || BLX > 0 || BLY > 0; }
+ }
+ }
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/Entities/CssImage.cs b/Source/HtmlRenderer/Core/Entities/CssImage.cs
new file mode 100644
index 000000000..570f0d275
--- /dev/null
+++ b/Source/HtmlRenderer/Core/Entities/CssImage.cs
@@ -0,0 +1,32 @@
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// The parsed form of a single background-image value: either a url() reference or a
+ /// gradient function. Unlike PeachPDF's CssImage (which this is a trimmed-down port of, same
+ /// name for an easy diff), background-image here is a single value, not a comma-separated
+ /// layered list - this project only backports enough to replace the old non-standard
+ /// "background-gradient" property.
+ ///
+ internal abstract class CssImage
+ {
+ internal sealed class Url : CssImage
+ {
+ public Url(string href)
+ {
+ Href = href;
+ }
+
+ public string Href { get; }
+ }
+
+ internal sealed class LinearGradient : CssImage
+ {
+ public LinearGradient(ParsedLinearGradient gradient)
+ {
+ Gradient = gradient;
+ }
+
+ public ParsedLinearGradient Gradient { get; }
+ }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/Entities/ParsedLinearGradient.cs b/Source/HtmlRenderer/Core/Entities/ParsedLinearGradient.cs
new file mode 100644
index 000000000..cb43e701f
--- /dev/null
+++ b/Source/HtmlRenderer/Core/Entities/ParsedLinearGradient.cs
@@ -0,0 +1,18 @@
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.CssEngine;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// A fully parsed linear-gradient()/repeating-linear-gradient() value - the angle and
+ /// raw (not yet position-resolved) color stop list. Ported from PeachPDF's
+ /// Html/Core/Entities/ParsedLinearGradient.cs, minus CSS Color 4 interpolation-color-space support
+ /// (ColorSpace/HueMethod) - gradients always interpolate in sRGB here.
+ ///
+ internal sealed class ParsedLinearGradient
+ {
+ public double AngleRad { get; set; }
+ public (RColor? Color, Length? Position, bool IsHint)[] Stops { get; set; }
+ public bool IsRepeating { get; set; }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/Handlers/BordersDrawHandler.cs b/Source/HtmlRenderer/Core/Handlers/BordersDrawHandler.cs
index b20c331cb..2cd90b33f 100644
--- a/Source/HtmlRenderer/Core/Handlers/BordersDrawHandler.cs
+++ b/Source/HtmlRenderer/Core/Handlers/BordersDrawHandler.cs
@@ -202,73 +202,76 @@ private static void SetInOutsetRectanglePoints(Border border, CssBox b, RRect r,
/// Beveled border path, null if there is no rounded corners
private static RGraphicsPath GetRoundedBorderPath(RGraphics g, Border border, CssBox b, RRect r)
{
+ var rad = b.ComputeRadii(r);
+ if (!rad.IsRounded) return null;
+
RGraphicsPath path = null;
switch (border)
{
case Border.Top:
- if (b.ActualCornerNw > 0 || b.ActualCornerNe > 0)
+ if (rad.TLX > 0 || rad.TLY > 0 || rad.TRX > 0 || rad.TRY > 0)
{
path = g.GetGraphicsPath();
- path.Start(r.Left + b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + b.ActualCornerNw);
+ path.Start(r.Left + b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + rad.TLY);
- if (b.ActualCornerNw > 0)
- path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2 + b.ActualCornerNw, r.Top + b.ActualBorderTopWidth / 2, b.ActualCornerNw, RGraphicsPath.Corner.TopLeft);
+ if (rad.TLX > 0 || rad.TLY > 0)
+ path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2 + rad.TLX, r.Top + b.ActualBorderTopWidth / 2, rad.TLX, rad.TLY, RGraphicsPath.Corner.TopLeft);
- path.LineTo(r.Right - b.ActualBorderRightWidth / 2 - b.ActualCornerNe, r.Top + b.ActualBorderTopWidth / 2);
+ path.LineTo(r.Right - b.ActualBorderRightWidth / 2 - rad.TRX, r.Top + b.ActualBorderTopWidth / 2);
- if (b.ActualCornerNe > 0)
- path.ArcTo(r.Right - b.ActualBorderRightWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + b.ActualCornerNe, b.ActualCornerNe, RGraphicsPath.Corner.TopRight);
+ if (rad.TRX > 0 || rad.TRY > 0)
+ path.ArcTo(r.Right - b.ActualBorderRightWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + rad.TRY, rad.TRX, rad.TRY, RGraphicsPath.Corner.TopRight);
}
break;
case Border.Bottom:
- if (b.ActualCornerSw > 0 || b.ActualCornerSe > 0)
+ if (rad.BLX > 0 || rad.BLY > 0 || rad.BRX > 0 || rad.BRY > 0)
{
path = g.GetGraphicsPath();
- path.Start(r.Right - b.ActualBorderRightWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - b.ActualCornerSe);
+ path.Start(r.Right - b.ActualBorderRightWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - rad.BRY);
- if (b.ActualCornerSe > 0)
- path.ArcTo(r.Right - b.ActualBorderRightWidth / 2 - b.ActualCornerSe, r.Bottom - b.ActualBorderBottomWidth / 2, b.ActualCornerSe, RGraphicsPath.Corner.BottomRight);
+ if (rad.BRX > 0 || rad.BRY > 0)
+ path.ArcTo(r.Right - b.ActualBorderRightWidth / 2 - rad.BRX, r.Bottom - b.ActualBorderBottomWidth / 2, rad.BRX, rad.BRY, RGraphicsPath.Corner.BottomRight);
- path.LineTo(r.Left + b.ActualBorderLeftWidth / 2 + b.ActualCornerSw, r.Bottom - b.ActualBorderBottomWidth / 2);
+ path.LineTo(r.Left + b.ActualBorderLeftWidth / 2 + rad.BLX, r.Bottom - b.ActualBorderBottomWidth / 2);
- if (b.ActualCornerSw > 0)
- path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - b.ActualCornerSw, b.ActualCornerSw, RGraphicsPath.Corner.BottomLeft);
+ if (rad.BLX > 0 || rad.BLY > 0)
+ path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - rad.BLY, rad.BLX, rad.BLY, RGraphicsPath.Corner.BottomLeft);
}
break;
case Border.Right:
- if (b.ActualCornerNe > 0 || b.ActualCornerSe > 0)
+ if (rad.TRX > 0 || rad.TRY > 0 || rad.BRX > 0 || rad.BRY > 0)
{
path = g.GetGraphicsPath();
bool noTop = b.BorderTopStyle == CssConstants.None || b.BorderTopStyle == CssConstants.Hidden;
bool noBottom = b.BorderBottomStyle == CssConstants.None || b.BorderBottomStyle == CssConstants.Hidden;
- path.Start(r.Right - b.ActualBorderRightWidth / 2 - (noTop ? b.ActualCornerNe : 0), r.Top + b.ActualBorderTopWidth / 2 + (noTop ? 0 : b.ActualCornerNe));
+ path.Start(r.Right - b.ActualBorderRightWidth / 2 - (noTop ? rad.TRX : 0), r.Top + b.ActualBorderTopWidth / 2 + (noTop ? 0 : rad.TRY));
- if (b.ActualCornerNe > 0 && noTop)
- path.ArcTo(r.Right - b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + b.ActualCornerNe, b.ActualCornerNe, RGraphicsPath.Corner.TopRight);
+ if ((rad.TRX > 0 || rad.TRY > 0) && noTop)
+ path.ArcTo(r.Right - b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + rad.TRY, rad.TRX, rad.TRY, RGraphicsPath.Corner.TopRight);
- path.LineTo(r.Right - b.ActualBorderRightWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - b.ActualCornerSe);
+ path.LineTo(r.Right - b.ActualBorderRightWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - rad.BRY);
- if (b.ActualCornerSe > 0 && noBottom)
- path.ArcTo(r.Right - b.ActualBorderRightWidth / 2 - b.ActualCornerSe, r.Bottom - b.ActualBorderBottomWidth / 2, b.ActualCornerSe, RGraphicsPath.Corner.BottomRight);
+ if ((rad.BRX > 0 || rad.BRY > 0) && noBottom)
+ path.ArcTo(r.Right - b.ActualBorderRightWidth / 2 - rad.BRX, r.Bottom - b.ActualBorderBottomWidth / 2, rad.BRX, rad.BRY, RGraphicsPath.Corner.BottomRight);
}
break;
case Border.Left:
- if (b.ActualCornerNw > 0 || b.ActualCornerSw > 0)
+ if (rad.TLX > 0 || rad.TLY > 0 || rad.BLX > 0 || rad.BLY > 0)
{
path = g.GetGraphicsPath();
bool noTop = b.BorderTopStyle == CssConstants.None || b.BorderTopStyle == CssConstants.Hidden;
bool noBottom = b.BorderBottomStyle == CssConstants.None || b.BorderBottomStyle == CssConstants.Hidden;
- path.Start(r.Left + b.ActualBorderLeftWidth / 2 + (noBottom ? b.ActualCornerSw : 0), r.Bottom - b.ActualBorderBottomWidth / 2 - (noBottom ? 0 : b.ActualCornerSw));
+ path.Start(r.Left + b.ActualBorderLeftWidth / 2 + (noBottom ? rad.BLX : 0), r.Bottom - b.ActualBorderBottomWidth / 2 - (noBottom ? 0 : rad.BLY));
- if (b.ActualCornerSw > 0 && noBottom)
- path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - b.ActualCornerSw, b.ActualCornerSw, RGraphicsPath.Corner.BottomLeft);
+ if ((rad.BLX > 0 || rad.BLY > 0) && noBottom)
+ path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - rad.BLY, rad.BLX, rad.BLY, RGraphicsPath.Corner.BottomLeft);
- path.LineTo(r.Left + b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + b.ActualCornerNw);
+ path.LineTo(r.Left + b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + rad.TLY);
- if (b.ActualCornerNw > 0 && noTop)
- path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2 + b.ActualCornerNw, r.Top + b.ActualBorderTopWidth / 2, b.ActualCornerNw, RGraphicsPath.Corner.TopLeft);
+ if ((rad.TLX > 0 || rad.TLY > 0) && noTop)
+ path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2 + rad.TLX, r.Top + b.ActualBorderTopWidth / 2, rad.TLX, rad.TLY, RGraphicsPath.Corner.TopLeft);
}
break;
}
diff --git a/Source/HtmlRenderer/Core/Handlers/CssImagePainter.cs b/Source/HtmlRenderer/Core/Handlers/CssImagePainter.cs
new file mode 100644
index 000000000..e8fe16d7d
--- /dev/null
+++ b/Source/HtmlRenderer/Core/Handlers/CssImagePainter.cs
@@ -0,0 +1,234 @@
+// "Therefore those skilled at the unorthodox
+// are infinite as heaven and earth,
+// inexhaustible as the great rivers.
+// When they come to an end,
+// they begin again,
+// like the days and months;
+// they die and are reborn,
+// like the four seasons."
+//
+// - Sun Tsu,
+// "The Art of War"
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.CssEngine;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Handlers
+{
+ ///
+ /// Turns a parsed linear-gradient() value into a brush ready to paint. Ported from PeachPDF's
+ /// Html/Core/Handlers/CssImagePainter.cs, trimmed to the linear-gradient-only, single (non-layered)
+ /// background-image case this engine supports - see .
+ ///
+ internal static class CssImagePainter
+ {
+ ///
+ /// Builds the brush for a linear-gradient() background, filling the given rectangle.
+ ///
+ public static RBrush GetLinearGradientBrush(RGraphics g, RRect rect, ParsedLinearGradient gradient)
+ {
+ var (p1, p2) = ComputeGradientLine(rect, gradient.AngleRad);
+ double gdx = p2.X - p1.X, gdy = p2.Y - p1.Y;
+ double gradientLength = Math.Sqrt(gdx * gdx + gdy * gdy);
+ var stops = NormalizeGradientStops(gradient.Stops, gradientLength);
+ if (gradient.IsRepeating)
+ stops = ExpandRepeatingStops(stops);
+ return g.GetLinearGradientBrush(p1, p2, stops);
+ }
+
+ ///
+ /// Computes the CSS gradient line endpoints for the given box rect and angle (standard CSS
+ /// "corner-to-corner projection" algorithm - the line is centered on the box and long enough that
+ /// the box's bounding rectangle is fully spanned along the gradient direction).
+ ///
+ private static (RPoint p1, RPoint p2) ComputeGradientLine(RRect rect, double angleRad)
+ {
+ double dx = Math.Sin(angleRad);
+ double dy = -Math.Cos(angleRad);
+ double cx = rect.X + rect.Width / 2;
+ double cy = rect.Y + rect.Height / 2;
+ double halfLen = Math.Abs(dx) * rect.Width / 2 + Math.Abs(dy) * rect.Height / 2;
+ var p1 = new RPoint(cx - dx * halfLen, cy - dy * halfLen);
+ var p2 = new RPoint(cx + dx * halfLen, cy + dy * halfLen);
+ return (p1, p2);
+ }
+
+ private static double? ConvertLength(Length? length, double gradientLength, double emPx = 16.0)
+ {
+ if (!length.HasValue) return null;
+ var len = length.Value;
+ if (len.Type == Length.Unit.Percent)
+ return len.Value / 100.0;
+ if (len.IsAbsolute)
+ return gradientLength > 0 ? len.ToPixel() / gradientLength : 0.0;
+ if (len.Type == Length.Unit.Em)
+ return gradientLength > 0 ? len.Value * emPx / gradientLength : 0.0;
+ return null;
+ }
+
+ private static RColor LerpColor(RColor a, RColor b, double t)
+ {
+ t = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t);
+ return RColor.FromArgb(
+ (int)Math.Round(a.A + t * (b.A - a.A)),
+ (int)Math.Round(a.R + t * (b.R - a.R)),
+ (int)Math.Round(a.G + t * (b.G - a.G)),
+ (int)Math.Round(a.B + t * (b.B - a.B)));
+ }
+
+ ///
+ /// Resolves each stop's position to a [0,1] fraction of the gradient line (per CSS Images §3.5.5:
+ /// the first/last stop default to 0%/100%, middle stops without an explicit position are spaced
+ /// evenly between their neighbors, and bare-position "hints" between two color stops bias the
+ /// interpolation curve rather than being a stop themselves).
+ ///
+ private static (RColor Color, double Position)[] NormalizeGradientStops(
+ (RColor? Color, Length? Position, bool IsHint)[] stops,
+ double gradientLength,
+ double emPx = 16.0)
+ {
+ var colorStops = stops.Where(s => !s.IsHint).ToArray();
+ int n = colorStops.Length;
+ if (n == 0) return new (RColor, double)[0];
+
+ var rawPos = new double?[n];
+ for (int i = 0; i < n; i++)
+ rawPos[i] = ConvertLength(colorStops[i].Position, gradientLength, emPx);
+
+ var resolved = new (RColor Color, double Position)[n];
+ double first = rawPos[0] ?? 0.0;
+ double last = rawPos[n - 1] ?? 1.0;
+ resolved[0] = (colorStops[0].Color.Value, first);
+ resolved[n - 1] = (colorStops[n - 1].Color.Value, last);
+
+ int runStart = -1;
+ for (int i = 1; i < n - 1; i++)
+ {
+ if (rawPos[i].HasValue)
+ {
+ resolved[i] = (colorStops[i].Color.Value, rawPos[i].Value);
+ if (runStart >= 0)
+ {
+ double posA = resolved[runStart - 1].Position;
+ double posB = resolved[i].Position;
+ int count = i - runStart + 1;
+ for (int j = runStart; j < i; j++)
+ {
+ double t = (double)(j - runStart + 1) / count;
+ resolved[j] = (colorStops[j].Color.Value, posA + t * (posB - posA));
+ }
+ runStart = -1;
+ }
+ }
+ else
+ {
+ if (runStart < 0) runStart = i;
+ resolved[i] = (colorStops[i].Color.Value, 0);
+ }
+ }
+ if (runStart >= 0)
+ {
+ double posA = resolved[runStart - 1].Position;
+ double posB = resolved[n - 1].Position;
+ int count = n - 1 - runStart + 1;
+ for (int j = runStart; j < n - 1; j++)
+ {
+ double t = (double)(j - runStart + 1) / count;
+ resolved[j] = (colorStops[j].Color.Value, posA + t * (posB - posA));
+ }
+ }
+
+ if (!stops.Any(s => s.IsHint))
+ return resolved;
+
+ var result = new List<(RColor Color, double Position)>();
+ int colorIdx = 0;
+ for (int i = 0; i < stops.Length; i++)
+ {
+ if (!stops[i].IsHint)
+ {
+ result.Add(resolved[colorIdx++]);
+ }
+ else
+ {
+ if (colorIdx == 0 || colorIdx >= n) continue;
+ var s1 = resolved[colorIdx - 1];
+ var s2 = resolved[colorIdx];
+ double range = s2.Position - s1.Position;
+ double hintPos = ConvertLength(stops[i].Position, gradientLength, emPx) ?? (s1.Position + range * 0.5);
+ double h = range > 1e-9
+ ? Math.Min(Math.Max((hintPos - s1.Position) / range, 1e-9), 1.0 - 1e-9)
+ : 0.5;
+ const int kSteps = 7;
+ double logHalf = Math.Log(0.5);
+ double logH = Math.Log(h);
+ for (int k = 1; k <= kSteps; k++)
+ {
+ double t = (double)k / (kSteps + 1);
+ double curved = Math.Pow(t, logHalf / logH);
+ result.Add((LerpColor(s1.Color, s2.Color, curved), s1.Position + t * range));
+ }
+ }
+ }
+ return result.ToArray();
+ }
+
+ ///
+ /// Flattens a repeating gradient's stop tile into a single non-repeating stop list spanning the
+ /// full [0,1] gradient line, by replicating the tile as many times as needed in both directions.
+ ///
+ private static (RColor Color, double Position)[] ExpandRepeatingStops((RColor Color, double Position)[] stops)
+ {
+ if (stops.Length < 2) return stops;
+ double tileStart = stops[0].Position;
+ double tileEnd = stops[stops.Length - 1].Position;
+ double tileLen = tileEnd - tileStart;
+ if (tileLen < 1e-6 || (tileStart <= 0.0 && tileEnd >= 1.0)) return stops;
+ const double eps = 0.0001;
+ var result = new List<(RColor Color, double Position)>();
+ int kMin = (int)Math.Floor(-tileEnd / tileLen);
+ int kMax = (int)Math.Ceiling((1.0 - tileStart) / tileLen);
+ for (int k = kMin; k <= kMax; k++)
+ {
+ double kOffset = k * tileLen;
+ for (int i = 0; i < stops.Length; i++)
+ {
+ double rawPos = stops[i].Position + kOffset;
+ bool isLastStop = i == stops.Length - 1;
+ double adjPos = isLastStop && k < kMax ? rawPos - eps : rawPos;
+ if (adjPos >= -eps && adjPos <= 1.0 + eps)
+ result.Add((stops[i].Color, Math.Min(Math.Max(adjPos, 0.0), 1.0)));
+ }
+ }
+ result.Sort((a, b) => a.Position.CompareTo(b.Position));
+ if (result.Count == 0) return stops;
+ if (result[0].Position > eps)
+ result.Insert(0, (SampleRepeatingColor(stops, tileStart, tileLen, 0.0), 0.0));
+ if (result[result.Count - 1].Position < 1.0 - eps)
+ result.Add((SampleRepeatingColor(stops, tileStart, tileLen, 1.0), 1.0));
+ return result.ToArray();
+ }
+
+ private static RColor SampleRepeatingColor((RColor Color, double Position)[] stops, double tileStart, double tileLen, double pos)
+ {
+ double relPos = (pos - tileStart) % tileLen;
+ if (relPos < 0) relPos += tileLen;
+ double absWithinTile = tileStart + relPos;
+ for (int i = 0; i < stops.Length - 1; i++)
+ {
+ if (absWithinTile >= stops[i].Position && absWithinTile <= stops[i + 1].Position)
+ {
+ double range = stops[i + 1].Position - stops[i].Position;
+ double t = range > 1e-12 ? (absWithinTile - stops[i].Position) / range : 0.0;
+ return LerpColor(stops[i].Color, stops[i + 1].Color, t);
+ }
+ }
+ return absWithinTile <= stops[0].Position ? stops[0].Color : stops[stops.Length - 1].Color;
+ }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/Parse/CssParser.cs b/Source/HtmlRenderer/Core/Parse/CssParser.cs
index b492f6e68..4c3d7c289 100644
--- a/Source/HtmlRenderer/Core/Parse/CssParser.cs
+++ b/Source/HtmlRenderer/Core/Parse/CssParser.cs
@@ -16,6 +16,7 @@
using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
using TheArtOfDev.HtmlRenderer.Core.CssEngine;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
using TheArtOfDev.HtmlRenderer.Core.Handlers;
using TheArtOfDev.HtmlRenderer.Core.Utils;
@@ -155,6 +156,15 @@ public bool IsColorValid(string colorValue)
return _valueParser.IsColorValid(colorValue);
}
+ ///
+ /// Parses a "background-image" value into a url() reference or a linear-gradient() - see
+ /// .
+ ///
+ public CssImage ParseBackgroundImage(string value)
+ {
+ return _valueParser.ParseImage(value);
+ }
+
///
/// Parse a complex font family css property to check if it contains multiple fonts and if the font exists.
/// returns the font family name to use or 'inherit' if failed.
diff --git a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs
index e1f6d738e..5efeb06c3 100644
--- a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs
+++ b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs
@@ -13,10 +13,13 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
+using System.Text;
using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
using TheArtOfDev.HtmlRenderer.Core.CssEngine;
using TheArtOfDev.HtmlRenderer.Core.Dom;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
using TheArtOfDev.HtmlRenderer.Core.Utils;
namespace TheArtOfDev.HtmlRenderer.Core.Parse
@@ -753,5 +756,217 @@ private static int ParseHexInt(string str, int idx, int length)
}
#endregion
+
+
+ #region Background image / gradient parsing
+
+ ///
+ /// Parses a single background-image value: either a url() reference or a
+ /// linear-gradient()/repeating-linear-gradient() function. Returns null for "none",
+ /// an empty value, or anything else this engine doesn't recognize (matching this engine's existing
+ /// silent-failure behavior for unsupported values).
+ /// Ported from PeachPDF's Html/Core/Parse/CssValueParser.ParseImage, trimmed to the single-value,
+ /// linear-gradient-only case this engine supports (no layered background-image, no radial/conic).
+ ///
+ public CssImage ParseImage(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value) || string.Equals(value, CssConstants.None, StringComparison.OrdinalIgnoreCase))
+ return null;
+
+ var tokens = GetCssTokens(value);
+
+ var urlToken = tokens.OfType().FirstOrDefault();
+ if (urlToken != null)
+ return new CssImage.Url(urlToken.Data);
+
+ var funcToken = tokens.OfType().FirstOrDefault();
+ if (funcToken == null)
+ return null;
+
+ if (string.Equals(funcToken.Data, FunctionNames.LinearGradient, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(funcToken.Data, FunctionNames.RepeatingLinearGradient, StringComparison.OrdinalIgnoreCase))
+ {
+ var gradient = ParseLinearGradient(value);
+ return gradient != null ? new CssImage.LinearGradient(gradient) : null;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Parses a linear-gradient()/repeating-linear-gradient() function value: an
+ /// optional angle or "to <side>" direction (default 180deg, top to bottom), followed by 2+
+ /// comma-separated color stops (each optionally followed by 1-2 length/percent positions), and
+ /// bare-position color hints between stops. Returns null if the value isn't a recognized gradient
+ /// function or doesn't have at least 2 real color stops.
+ /// Ported from PeachPDF's Html/Core/Parse/CssValueParser.ParseLinearGradient, minus CSS Color 4
+ /// interpolation-color-space ("in oklab" etc.) support - see ParsedLinearGradient.
+ ///
+ private ParsedLinearGradient ParseLinearGradient(string value)
+ {
+ var tokens = GetCssTokens(value);
+
+ var funcToken = tokens.OfType().FirstOrDefault(t =>
+ string.Equals(t.Data, FunctionNames.LinearGradient, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(t.Data, FunctionNames.RepeatingLinearGradient, StringComparison.OrdinalIgnoreCase));
+
+ if (funcToken == null)
+ return null;
+
+ bool isRepeating = string.Equals(funcToken.Data, FunctionNames.RepeatingLinearGradient, StringComparison.OrdinalIgnoreCase);
+
+ var args = funcToken.ArgumentTokens.ToList();
+ if (args.Count == 0)
+ return null;
+
+ double angleRad = Math.PI; // default: 180deg = top to bottom
+ int stopOffset = 0;
+
+ var firstGroup = args[0];
+ var firstIdents = firstGroup.Where(t => t.Type == TokenType.Ident).Select(t => t.Data.ToLowerInvariant()).ToList();
+
+ if (firstIdents.Count > 0 && firstIdents[0] == "to")
+ {
+ // keyword direction: "to right", "to bottom left", etc.
+ angleRad = SideKeywordsToAngleRad(firstIdents.Skip(1).ToList());
+ stopOffset = 1;
+ }
+ else
+ {
+ var angle = firstGroup.ToAngle();
+ if (angle.HasValue)
+ {
+ angleRad = angle.Value.ToRadian();
+ stopOffset = 1;
+ }
+ // else no angle token, stopOffset stays 0 - first group is already the first color stop
+ }
+
+ var stopGroups = args.Skip(stopOffset).ToList();
+ if (stopGroups.Count < 2)
+ return null;
+
+ var stops = new List<(RColor? Color, Length? Position, bool IsHint)>();
+
+ foreach (var group in stopGroups)
+ {
+ var items = group.ToItems();
+ if (items.Count == 0)
+ continue;
+
+ Length? position1 = null;
+ Length? position2 = null;
+ int colorItemCount = items.Count;
+
+ // Last item may be a length/percent position.
+ var lastItem = items[items.Count - 1];
+ var pv = lastItem.ToDistance();
+ if (pv.HasValue)
+ {
+ position1 = pv.Value;
+ colorItemCount--;
+
+ // Two-position shorthand (e.g. "red 0 50%").
+ if (colorItemCount > 0)
+ {
+ var pv2 = items[colorItemCount - 1].ToDistance();
+ if (pv2.HasValue)
+ {
+ position2 = position1;
+ position1 = pv2.Value;
+ colorItemCount--;
+ }
+ }
+ }
+
+ if (colorItemCount == 0)
+ {
+ // Bare position with no color - a color hint.
+ if (position1.HasValue && !position2.HasValue)
+ stops.Add((null, position1, true));
+ continue;
+ }
+
+ var colorText = BuildColorText(items.Take(colorItemCount));
+ if (string.IsNullOrWhiteSpace(colorText))
+ continue;
+
+ var color = GetActualColor(colorText);
+ stops.Add((color, position1, false));
+ if (position2.HasValue)
+ stops.Add((color, position2, false));
+ }
+
+ if (stops.Count(s => !s.IsHint) < 2)
+ return null;
+
+ return new ParsedLinearGradient
+ {
+ AngleRad = angleRad,
+ Stops = stops.ToArray(),
+ IsRepeating = isRepeating,
+ };
+ }
+
+ ///
+ /// Converts "to <side> [<side>]" direction keywords (e.g. "right", "bottom left") to
+ /// the equivalent gradient-line angle in radians, per the CSS Images spec's side/corner table.
+ ///
+ private static double SideKeywordsToAngleRad(List sides)
+ {
+ bool hasTop = sides.Contains("top");
+ bool hasBottom = sides.Contains("bottom");
+ bool hasLeft = sides.Contains("left");
+ bool hasRight = sides.Contains("right");
+
+ if (hasTop && hasRight) return Math.PI / 4; // 45deg
+ if (hasBottom && hasRight) return 3 * Math.PI / 4; // 135deg
+ if (hasBottom && hasLeft) return 5 * Math.PI / 4; // 225deg
+ if (hasTop && hasLeft) return 7 * Math.PI / 4; // 315deg
+ if (hasTop) return 0; // 0deg
+ if (hasRight) return Math.PI / 2; // 90deg
+ if (hasBottom) return Math.PI; // 180deg
+ if (hasLeft) return 3 * Math.PI / 2; // 270deg
+
+ return Math.PI; // default
+ }
+
+ ///
+ /// Re-serializes a color stop's token groups (everything before its trailing position token(s))
+ /// back to CSS text so it can be handed to as a normal color string.
+ ///
+ private static string BuildColorText(IEnumerable> itemGroups)
+ {
+ var sb = new StringBuilder();
+ foreach (var group in itemGroups)
+ {
+ sb.Append(group.ToText());
+ }
+ return sb.ToString().Trim();
+ }
+
+ ///
+ /// Splits a CSS value string on top-level whitespace (paren-depth-aware, so a calc()/gradient()
+ /// value's internal spaces aren't mistaken for a delimiter). Used to split a "border-radius"
+ /// corner value like "10px 5%" into its horizontal/vertical components.
+ ///
+ internal static IEnumerable SplitTopLevelWhitespace(string value)
+ {
+ int depth = 0, start = 0;
+ for (int i = 0; i < value.Length; i++)
+ {
+ char c = value[i];
+ if (c == '(') depth++;
+ else if (c == ')') depth--;
+ else if (char.IsWhiteSpace(c) && depth == 0)
+ {
+ if (i > start) yield return value.Substring(start, i - start);
+ start = i + 1;
+ }
+ }
+ if (start < value.Length) yield return value.Substring(start);
+ }
+
+ #endregion
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/Utils/CssUtils.cs b/Source/HtmlRenderer/Core/Utils/CssUtils.cs
index a6b5deebc..79f8402ca 100644
--- a/Source/HtmlRenderer/Core/Utils/CssUtils.cs
+++ b/Source/HtmlRenderer/Core/Utils/CssUtils.cs
@@ -44,12 +44,11 @@ internal static class CssUtils
"border-bottom-style", "border-left-style", "border-right-style", "border-top-style",
"border-bottom-color", "border-left-color", "border-right-color", "border-top-color",
"border-spacing", "border-collapse",
- "corner-radius", "corner-nw-radius", "corner-ne-radius", "corner-se-radius", "corner-sw-radius",
+ "border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius",
"margin-bottom", "margin-left", "margin-right", "margin-top",
"padding-bottom", "padding-left", "padding-right", "padding-top",
"page-break-inside", "left", "top", "width", "max-width", "height",
"background-color", "background-image", "background-position", "background-repeat",
- "background-gradient", "background-gradient-angle",
"content", "color", "display", "direction", "empty-cells", "float", "position",
"line-height", "vertical-align", "text-indent", "text-align", "text-decoration",
"white-space", "word-break", "visibility", "word-spacing",
@@ -124,16 +123,14 @@ public static string GetPropertyValue(CssBox cssBox, string propName)
return cssBox.BorderSpacing;
case "border-collapse":
return cssBox.BorderCollapse;
- case "corner-radius":
- return cssBox.CornerRadius;
- case "corner-nw-radius":
- return cssBox.CornerNwRadius;
- case "corner-ne-radius":
- return cssBox.CornerNeRadius;
- case "corner-se-radius":
- return cssBox.CornerSeRadius;
- case "corner-sw-radius":
- return cssBox.CornerSwRadius;
+ case "border-top-left-radius":
+ return cssBox.BorderTopLeftRadius;
+ case "border-top-right-radius":
+ return cssBox.BorderTopRightRadius;
+ case "border-bottom-right-radius":
+ return cssBox.BorderBottomRightRadius;
+ case "border-bottom-left-radius":
+ return cssBox.BorderBottomLeftRadius;
case "margin-bottom":
return cssBox.MarginBottom;
case "margin-left":
@@ -170,10 +167,6 @@ public static string GetPropertyValue(CssBox cssBox, string propName)
return cssBox.BackgroundPosition;
case "background-repeat":
return cssBox.BackgroundRepeat;
- case "background-gradient":
- return cssBox.BackgroundGradient;
- case "background-gradient-angle":
- return cssBox.BackgroundGradientAngle;
case "content":
return cssBox.Content;
case "color":
@@ -283,20 +276,20 @@ public static void SetPropertyValue(CssBox cssBox, string propName, string value
case "border-collapse":
cssBox.BorderCollapse = value;
break;
- case "corner-radius":
- cssBox.CornerRadius = value;
+ case "border-radius":
+ cssBox.BorderRadius = value;
break;
- case "corner-nw-radius":
- cssBox.CornerNwRadius = value;
+ case "border-top-left-radius":
+ cssBox.BorderTopLeftRadius = value;
break;
- case "corner-ne-radius":
- cssBox.CornerNeRadius = value;
+ case "border-top-right-radius":
+ cssBox.BorderTopRightRadius = value;
break;
- case "corner-se-radius":
- cssBox.CornerSeRadius = value;
+ case "border-bottom-right-radius":
+ cssBox.BorderBottomRightRadius = value;
break;
- case "corner-sw-radius":
- cssBox.CornerSwRadius = value;
+ case "border-bottom-left-radius":
+ cssBox.BorderBottomLeftRadius = value;
break;
case "margin-bottom":
cssBox.MarginBottom = value;
@@ -352,12 +345,6 @@ public static void SetPropertyValue(CssBox cssBox, string propName, string value
case "background-repeat":
cssBox.BackgroundRepeat = value;
break;
- case "background-gradient":
- cssBox.BackgroundGradient = value;
- break;
- case "background-gradient-angle":
- cssBox.BackgroundGradientAngle = value;
- break;
case "color":
cssBox.Color = value;
break;
diff --git a/Source/HtmlRenderer/Core/Utils/RenderUtils.cs b/Source/HtmlRenderer/Core/Utils/RenderUtils.cs
index 2726b5fb2..091c75191 100644
--- a/Source/HtmlRenderer/Core/Utils/RenderUtils.cs
+++ b/Source/HtmlRenderer/Core/Utils/RenderUtils.cs
@@ -95,44 +95,42 @@ public static void DrawImageErrorIcon(RGraphics g, HtmlContainerInt htmlContaine
}
///
- /// Creates a rounded rectangle using the specified corner radius
- /// NW-----NE
+ /// Creates a rounded rectangle path. Each corner has independent horizontal (X) and vertical (Y)
+ /// radii, supporting elliptical corners per the CSS border-radius spec.
+ /// TL-----TR
/// | |
/// | |
- /// SW-----SE
+ /// BL-----BR
///
/// the device to draw into
/// Rectangle to round
- /// Radius of the north east corner
- /// Radius of the north west corner
- /// Radius of the south east corner
- /// Radius of the south west corner
/// GraphicsPath with the lines of the rounded rectangle ready to be painted
- public static RGraphicsPath GetRoundRect(RGraphics g, RRect rect, double nwRadius, double neRadius, double seRadius, double swRadius)
+ public static RGraphicsPath GetRoundRect(RGraphics g, RRect rect,
+ double tlX, double tlY, double trX, double trY,
+ double brX, double brY, double blX, double blY)
{
var path = g.GetGraphicsPath();
- path.Start(rect.Left + nwRadius, rect.Top);
-
- path.LineTo(rect.Right - neRadius, rect.Y);
-
- if (neRadius > 0f)
- path.ArcTo(rect.Right, rect.Top + neRadius, neRadius, RGraphicsPath.Corner.TopRight);
-
- path.LineTo(rect.Right, rect.Bottom - seRadius);
-
- if (seRadius > 0f)
- path.ArcTo(rect.Right - seRadius, rect.Bottom, seRadius, RGraphicsPath.Corner.BottomRight);
-
- path.LineTo(rect.Left + swRadius, rect.Bottom);
-
- if (swRadius > 0f)
- path.ArcTo(rect.Left, rect.Bottom - swRadius, swRadius, RGraphicsPath.Corner.BottomLeft);
-
- path.LineTo(rect.Left, rect.Top + nwRadius);
-
- if (nwRadius > 0f)
- path.ArcTo(rect.Left + nwRadius, rect.Top, nwRadius, RGraphicsPath.Corner.TopLeft);
+ // Top edge: start after TL corner, end before TR corner.
+ path.Start(rect.Left + tlX, rect.Top);
+ path.LineTo(rect.Right - trX, rect.Top);
+ if (trX > 0 || trY > 0)
+ path.ArcTo(rect.Right, rect.Top + trY, trX, trY, RGraphicsPath.Corner.TopRight);
+
+ // Right edge.
+ path.LineTo(rect.Right, rect.Bottom - brY);
+ if (brX > 0 || brY > 0)
+ path.ArcTo(rect.Right - brX, rect.Bottom, brX, brY, RGraphicsPath.Corner.BottomRight);
+
+ // Bottom edge.
+ path.LineTo(rect.Left + blX, rect.Bottom);
+ if (blX > 0 || blY > 0)
+ path.ArcTo(rect.Left, rect.Bottom - blY, blX, blY, RGraphicsPath.Corner.BottomLeft);
+
+ // Left edge.
+ path.LineTo(rect.Left, rect.Top + tlY);
+ if (tlX > 0 || tlY > 0)
+ path.ArcTo(rect.Left + tlX, rect.Top, tlX, tlY, RGraphicsPath.Corner.TopLeft);
return path;
}
From bc3884e42511400b52226b05e124c19b34ad3dae Mon Sep 17 00:00:00 2001
From: Justin Haygood
Date: Thu, 16 Jul 2026 21:39:17 -0400
Subject: [PATCH 04/21] Fixes
Fixes
---
.../HtmlRenderer/Core/Dom/CssContentEngine.cs | 96 +++++++------------
.../HtmlRenderer/Core/Parse/CssValueParser.cs | 2 +-
2 files changed, 33 insertions(+), 65 deletions(-)
diff --git a/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs b/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs
index 9ddb80fe0..e3d4b55e3 100644
--- a/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs
+++ b/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs
@@ -10,7 +10,11 @@
// - Sun Tsu,
// "The Art of War"
+using System;
+using System.Linq;
using System.Text;
+using TheArtOfDev.HtmlRenderer.Core.CssEngine;
+using TheArtOfDev.HtmlRenderer.Core.Parse;
using TheArtOfDev.HtmlRenderer.Core.Utils;
namespace TheArtOfDev.HtmlRenderer.Core.Dom
@@ -23,7 +27,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.Dom
/// attr(), and the four quote keywords are supported; counter()/counters()/
/// string()/content() are not (CSS counters and named strings remain out of scope
/// for this backport - an unrecognized component is simply skipped, the same graceful-ignore
- /// behavior this engine already applies to unsupported CSS elsewhere).
+ /// behavior this engine already applies to unsupported CSS elsewhere).
+ /// Tokenizes via the shared CSS lexer () rather than
+ /// hand-parsing the CSS text, so string literals (e.g. "\A", the standard UA-stylesheet
+ /// line-break trick used by br:before) get the same correct escape decoding the lexer
+ /// already applies when the stylesheet is first parsed - see CalcParser/BackgroundPositionGrammar
+ /// for the same "don't write two independent parsers for the same grammar" precedent.
///
internal static class CssContentEngine
{
@@ -50,82 +59,41 @@ public static void ApplyContent(CssBox box)
private static string Resolve(CssBox box, string content)
{
var sb = new StringBuilder();
- var i = 0;
- while (i < content.Length)
+
+ foreach (var token in CssValueParser.GetCssTokens(content))
{
- if (char.IsWhiteSpace(content[i]))
+ if (token is StringToken stringToken)
{
- i++;
- continue;
+ sb.Append(stringToken.Data);
}
-
- if (content[i] == '"' || content[i] == '\'')
+ else if (token is FunctionToken functionToken &&
+ functionToken.Data.Equals(FunctionNames.Attr, StringComparison.OrdinalIgnoreCase))
{
- var quote = content[i];
- var start = i + 1;
- var j = start;
- while (j < content.Length && content[j] != quote)
+ if (functionToken.ArgumentTokens.FirstOrDefault() is KeywordToken attrNameToken)
{
- if (content[j] == '\\' && j + 1 < content.Length)
- j++;
- j++;
+ var sourceBox = box.IsPseudoElement && box.ParentBox != null ? box.ParentBox : box;
+ var attrValue = sourceBox.GetAttribute(attrNameToken.Data, string.Empty);
+ sb.Append(attrValue);
}
- sb.Append(Unescape(content.Substring(start, j - start)));
- i = j + 1;
- continue;
}
-
- var tokenEnd = i;
- while (tokenEnd < content.Length && !char.IsWhiteSpace(content[tokenEnd]))
- tokenEnd++;
- var token = content.Substring(i, tokenEnd - i);
- i = tokenEnd;
-
- if (token.StartsWith("attr(", System.StringComparison.OrdinalIgnoreCase) && token.EndsWith(")"))
+ else if (token is KeywordToken keywordToken)
{
- var attrName = token.Substring(5, token.Length - 6).Trim();
- var sourceBox = box.IsPseudoElement && box.ParentBox != null ? box.ParentBox : box;
- var attrValue = sourceBox.GetAttribute(attrName, string.Empty);
- sb.Append(attrValue);
- }
- else if (token.Equals("open-quote", System.StringComparison.OrdinalIgnoreCase))
- {
- sb.Append('“');
- }
- else if (token.Equals("close-quote", System.StringComparison.OrdinalIgnoreCase))
- {
- sb.Append('”');
- }
- else if (token.Equals("no-open-quote", System.StringComparison.OrdinalIgnoreCase) ||
- token.Equals("no-close-quote", System.StringComparison.OrdinalIgnoreCase))
- {
- // produces no text, but still counts as a quoting depth change per spec - depth
- // tracking isn't implemented here (no "quotes" property support), so this is a no-op.
+ if (keywordToken.Data.Equals(Keywords.OpenQuote, StringComparison.OrdinalIgnoreCase))
+ {
+ sb.Append('“');
+ }
+ else if (keywordToken.Data.Equals(Keywords.CloseQuote, StringComparison.OrdinalIgnoreCase))
+ {
+ sb.Append('”');
+ }
+ // no-open-quote/no-close-quote produce no text, but still count as a quoting depth
+ // change per spec - depth tracking isn't implemented here (no "quotes" property
+ // support), so they (like any other unrecognized keyword) are a no-op.
}
// counter()/counters()/string()/content() and anything else unrecognized: skip.
}
return sb.ToString();
}
-
- private static string Unescape(string s)
- {
- if (s.IndexOf('\\') < 0) return s;
-
- var sb = new StringBuilder(s.Length);
- for (var i = 0; i < s.Length; i++)
- {
- if (s[i] == '\\' && i + 1 < s.Length)
- {
- i++;
- sb.Append(s[i]);
- }
- else
- {
- sb.Append(s[i]);
- }
- }
- return sb.ToString();
- }
}
}
diff --git a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs
index 5efeb06c3..1a7205f5e 100644
--- a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs
+++ b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs
@@ -126,7 +126,7 @@ public static bool IsValidLength(string value)
/// skipping whitespace/end-of-file tokens - the same "just enough tokenization to recognize a
/// single top-level function call" approach used to detect calc()-family expressions.
///
- private static List GetCssTokens(string value)
+ internal static List GetCssTokens(string value)
{
var lexer = new Lexer(value);
var tokens = new List();
From 75972f23c0295edac8e42761d88ca2edf9b6ed66 Mon Sep 17 00:00:00 2001
From: Justin Haygood
Date: Sat, 25 Jul 2026 00:06:50 -0400
Subject: [PATCH 05/21] CSS engine: sync enumerations, tokens and leaf value
types
Brings the vendored CSS engine's name tables and leaf value types up to
PeachPDF v0.9.6 (669ec1e), from the e5842c3 snapshot the port was taken at.
- PropertyNames: grid, logical box-model, aspect-ratio, font-palette and the
@property/@font-palette-values descriptors
- FunctionNames: basic shapes, grid functions, lab/oklab/lch/oklch/color-mix,
image-set/cross-fade/element
- Keywords, FeatureNames (the prefers-* set), PseudoClassNames (:where),
RuleNames and RuleType (@property, @layer, @font-palette-values)
- Combinators: static readonly -> const so they can be used as constant
patterns in switch statements
- RangeToken: drop the eagerly-expanded SelectedRange in favour of a ToValue()
override that re-emits the "U+" prefix
- Resolution: accept `x` as the alias for `dppx`
- Length: reject arbitrary non-length input parsing as zero in TryParse
Length also gets an HTML-Renderer-specific adaptation rather than the upstream
change. Upstream resolves absolute units against points (its layout unit);
this renderer's layout unit is the CSS pixel, and CssValueParser.ParseLength
already resolves 1in as 96px. The vendored table disagreed - `calc(1in)` gave
72 where `width: 1in` gave 96 - so the absolute-unit arms of ToPixels and the
inverse arms of To(Unit) are now 96dpi (CSS Values & Units 5.2), matching
ParseLength exactly. The fontAdjust px->pt hook is kept, since it mirrors
ParseLength's own font-size output flag.
Co-Authored-By: Claude Opus 5
---
.../CssEngine/Enumerations/Combinators.cs | 31 +++----
.../CssEngine/Enumerations/FeatureNames.cs | 7 ++
.../CssEngine/Enumerations/FunctionNames.cs | 15 ++++
.../Core/CssEngine/Enumerations/Keywords.cs | 5 ++
.../CssEngine/Enumerations/PropertyNames.cs | 81 +++++++++++++++++++
.../Enumerations/PseudoClassNames.cs | 1 +
.../Core/CssEngine/Enumerations/RuleNames.cs | 3 +
.../Core/CssEngine/Enumerations/RuleType.cs | 6 +-
.../Core/CssEngine/Tokens/RangeToken.cs | 38 ++-------
.../Core/CssEngine/Values/Length.cs | 58 ++++++-------
.../Core/CssEngine/Values/Resolution.cs | 2 +
11 files changed, 175 insertions(+), 72 deletions(-)
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs
index 45e491d6e..e023138ad 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs
@@ -2,19 +2,22 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
internal static class Combinators
{
- public static readonly string Exactly = "=";
- public static readonly string Unlike = "!=";
- public static readonly string InList = "~=";
- public static readonly string InToken = "|=";
- public static readonly string Begins = "^=";
- public static readonly string Ends = "$=";
- public static readonly string InText = "*=";
- public static readonly string Column = "||";
- public static readonly string Pipe = "|";
- public static readonly string Adjacent = "+";
- public static readonly string Descendent = " ";
- public static readonly string Deep = ">>>";
- public static readonly string Child = ">";
- public static readonly string Sibling = "~";
+ // const (not static readonly) so these can be used as constant patterns in switch statements
+ // (e.g. AttributeSelectorFactory's reflection-free dispatch). String literals are interned, so
+ // reference/value equality behaviour is unchanged.
+ public const string Exactly = "=";
+ public const string Unlike = "!=";
+ public const string InList = "~=";
+ public const string InToken = "|=";
+ public const string Begins = "^=";
+ public const string Ends = "$=";
+ public const string InText = "*=";
+ public const string Column = "||";
+ public const string Pipe = "|";
+ public const string Adjacent = "+";
+ public const string Descendent = " ";
+ public const string Deep = ">>>";
+ public const string Child = ">";
+ public const string Sibling = "~";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs
index 760776c91..69e71400f 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs
@@ -44,5 +44,12 @@ internal static class FeatureNames
public static readonly string Hover = "hover";
public static readonly string BlockSize = "block-size";
public static readonly string InlineSize = "inline-size";
+ public static readonly string PrefersColorScheme = "prefers-color-scheme";
+ public static readonly string PrefersReducedMotion = "prefers-reduced-motion";
+ public static readonly string PrefersContrast = "prefers-contrast";
+ public static readonly string PrefersReducedTransparency = "prefers-reduced-transparency";
+ public static readonly string Update = "update";
+ public static readonly string AnyHover = "any-hover";
+ public static readonly string AnyPointer = "any-pointer";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs
index 2ed88f666..94c802298 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs
@@ -11,6 +11,13 @@ internal static class FunctionNames
public static readonly string Hsl = "hsl";
public static readonly string Hsla = "hsla";
public static readonly string Rect = "rect";
+ public static readonly string Polygon = "polygon";
+ public static readonly string Circle = "circle";
+ public static readonly string Ellipse = "ellipse";
+ public static readonly string Inset = "inset";
+ public static readonly string Repeat = "repeat";
+ public static readonly string Minmax = "minmax";
+ public static readonly string FitContent = "fit-content";
public static readonly string Attr = "attr";
public static readonly string LinearGradient = "linear-gradient";
public static readonly string RadialGradient = "radial-gradient";
@@ -50,8 +57,16 @@ internal static class FunctionNames
public static readonly string Perspective = "perspective";
public static readonly string Gray = "gray";
public static readonly string Hwb = "hwb";
+ public static readonly string Lab = "lab";
+ public static readonly string Oklab = "oklab";
+ public static readonly string Lch = "lch";
+ public static readonly string Oklch = "oklch";
+ public static readonly string ColorMix = "color-mix";
public static readonly string ConicGradient = "conic-gradient";
public static readonly string RepeatingConicGradient = "repeating-conic-gradient";
public static readonly string Var = "var";
+ public static readonly string ImageSet = "image-set";
+ public static readonly string CrossFade = "cross-fade";
+ public static readonly string Element = "element";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs
index 3bdea8af4..c3635473d 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs
@@ -299,6 +299,11 @@ internal static class Keywords
public static readonly string MaxContent = "max-content";
public static readonly string MinContent = "min-content";
public static readonly string FitContent = "fit-content";
+ public static readonly string AutoFill = "auto-fill";
+ public static readonly string AutoFit = "auto-fit";
+ public static readonly string AutoFlow = "auto-flow";
+ public static readonly string Subgrid = "subgrid";
+ public static readonly string Dense = "dense";
public static readonly string Content = "content";
public static readonly string Revert = "revert";
public static readonly string RevertLayer = "revert-layer";
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs
index e09406269..76fb564ea 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs
@@ -40,6 +40,7 @@ internal static class PropertyNames
public static readonly string BorderBottomRightRadius = "border-bottom-right-radius";
public static readonly string BoxShadow = "box-shadow";
public static readonly string BoxSizing = "box-sizing";
+ public static readonly string AspectRatio = "aspect-ratio";
public static readonly string BoxDecorationBreak = "box-decoration-break";
public static readonly string BreakAfter = "break-after";
public static readonly string BreakBefore = "break-before";
@@ -116,6 +117,7 @@ internal static class PropertyNames
public static readonly string Float = "float";
public static readonly string FontFamily = "font-family";
public static readonly string FontFeatureSettings = "font-feature-settings";
+ public static readonly string FontPalette = "font-palette";
public static readonly string FontSize = "font-size";
public static readonly string FontSizeAdjust = "font-size-adjust";
public static readonly string FontStyle = "font-style";
@@ -124,6 +126,26 @@ internal static class PropertyNames
public static readonly string FontWeight = "font-weight";
public static readonly string Font = "font";
public static readonly string Gap = "gap";
+ public static readonly string Grid = "grid";
+ public static readonly string GridTemplate = "grid-template";
+ public static readonly string GridTemplateColumns = "grid-template-columns";
+ public static readonly string GridTemplateRows = "grid-template-rows";
+ public static readonly string GridTemplateAreas = "grid-template-areas";
+ public static readonly string GridAutoColumns = "grid-auto-columns";
+ public static readonly string GridAutoRows = "grid-auto-rows";
+ public static readonly string GridAutoFlow = "grid-auto-flow";
+ public static readonly string GridColumn = "grid-column";
+ public static readonly string GridColumnStart = "grid-column-start";
+ public static readonly string GridColumnEnd = "grid-column-end";
+ public static readonly string GridRow = "grid-row";
+ public static readonly string GridRowStart = "grid-row-start";
+ public static readonly string GridRowEnd = "grid-row-end";
+ public static readonly string GridArea = "grid-area";
+ public static readonly string JustifyItems = "justify-items";
+ public static readonly string JustifySelf = "justify-self";
+ public static readonly string PlaceItems = "place-items";
+ public static readonly string PlaceContent = "place-content";
+ public static readonly string PlaceSelf = "place-self";
public static readonly string GlyphOrientationHorizontal = "glyph-orientation-horizontal";
public static readonly string GlyphOrientationVertical = "glyph-orientation-vertical";
public static readonly string Height = "height";
@@ -241,8 +263,67 @@ internal static class PropertyNames
public static readonly string Zoom = "zoom";
public static readonly string UnicodeRange = "unicode-range";
public static readonly string Src = "src";
+ // @property descriptors (CSS Properties & Values API Level 1 §3)
+ public static readonly string Syntax = "syntax";
+ public static readonly string InitialValue = "initial-value";
+ public static readonly string Inherits = "inherits";
+ public static readonly string BasePalette = "base-palette";
+ public static readonly string OverrideColors = "override-colors";
public static readonly string ObjectFit = "object-fit";
public static readonly string ObjectPosition = "object-position";
public static readonly string Size = "size";
+
+ // CSS Logical Properties and Values Level 1. Layout here is always LTR / horizontal-tb,
+ // so each logical property maps 1:1 to a physical one (block-start = top,
+ // block-end = bottom, inline-start = left, inline-end = right) and is registered to
+ // produce/export the existing physical longhands.
+ public static readonly string MarginBlock = "margin-block";
+ public static readonly string MarginBlockStart = "margin-block-start";
+ public static readonly string MarginBlockEnd = "margin-block-end";
+ public static readonly string MarginInline = "margin-inline";
+ public static readonly string MarginInlineStart = "margin-inline-start";
+ public static readonly string MarginInlineEnd = "margin-inline-end";
+
+ public static readonly string PaddingBlock = "padding-block";
+ public static readonly string PaddingBlockStart = "padding-block-start";
+ public static readonly string PaddingBlockEnd = "padding-block-end";
+ public static readonly string PaddingInline = "padding-inline";
+ public static readonly string PaddingInlineStart = "padding-inline-start";
+ public static readonly string PaddingInlineEnd = "padding-inline-end";
+
+ public static readonly string Inset = "inset";
+ public static readonly string InsetBlock = "inset-block";
+ public static readonly string InsetBlockStart = "inset-block-start";
+ public static readonly string InsetBlockEnd = "inset-block-end";
+ public static readonly string InsetInline = "inset-inline";
+ public static readonly string InsetInlineStart = "inset-inline-start";
+ public static readonly string InsetInlineEnd = "inset-inline-end";
+
+ public static readonly string BorderBlock = "border-block";
+ public static readonly string BorderBlockStart = "border-block-start";
+ public static readonly string BorderBlockEnd = "border-block-end";
+ public static readonly string BorderInline = "border-inline";
+ public static readonly string BorderInlineStart = "border-inline-start";
+ public static readonly string BorderInlineEnd = "border-inline-end";
+
+ public static readonly string BorderBlockWidth = "border-block-width";
+ public static readonly string BorderBlockStyle = "border-block-style";
+ public static readonly string BorderBlockColor = "border-block-color";
+ public static readonly string BorderInlineWidth = "border-inline-width";
+ public static readonly string BorderInlineStyle = "border-inline-style";
+ public static readonly string BorderInlineColor = "border-inline-color";
+
+ public static readonly string BorderBlockStartWidth = "border-block-start-width";
+ public static readonly string BorderBlockStartStyle = "border-block-start-style";
+ public static readonly string BorderBlockStartColor = "border-block-start-color";
+ public static readonly string BorderBlockEndWidth = "border-block-end-width";
+ public static readonly string BorderBlockEndStyle = "border-block-end-style";
+ public static readonly string BorderBlockEndColor = "border-block-end-color";
+ public static readonly string BorderInlineStartWidth = "border-inline-start-width";
+ public static readonly string BorderInlineStartStyle = "border-inline-start-style";
+ public static readonly string BorderInlineStartColor = "border-inline-start-color";
+ public static readonly string BorderInlineEndWidth = "border-inline-end-width";
+ public static readonly string BorderInlineEndStyle = "border-inline-end-style";
+ public static readonly string BorderInlineEndColor = "border-inline-end-color";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs
index 935caf626..e99dec18c 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs
@@ -40,6 +40,7 @@ internal static class PseudoClassNames
public static readonly string Has = "has";
public static readonly string Matches = "matches";
public static readonly string Is = "is";
+ public static readonly string Where = "where";
public static readonly string NthChild = "nth-child";
public static readonly string NthLastChild = "nth-last-child";
public static readonly string NthOfType = "nth-of-type";
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs
index 12e40213e..6ada85658 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs
@@ -13,5 +13,8 @@ internal static class RuleNames
public static readonly string Namespace = "namespace";
public static readonly string Page = "page";
public static readonly string Container = "container";
+ public static readonly string Property = "property";
+ public static readonly string Layer = "layer";
+ public static readonly string FontPaletteValues = "font-palette-values";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs
index e24f5fa80..f13b491b6 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs
@@ -19,6 +19,10 @@ internal enum RuleType : byte
FontFeatureValues,
Viewport,
RegionStyle,
- Container
+ Container,
+ Property,
+ Layer,
+ LayerStatement,
+ FontPaletteValues
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs b/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs
index 84ce08866..fee373664 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs
@@ -1,38 +1,12 @@
-using System.Collections.Generic;
-using System.Globalization;
-
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
internal sealed class RangeToken : Token
{
- private string[] GetRange()
- {
- var index = int.Parse(Start, NumberStyles.HexNumber);
-
- if (index > Symbols.MaximumCodepoint) return null;
-
- if (End == null) return new[] { index.ConvertFromUtf32() };
-
- var list = new List();
- var f = int.Parse(End, NumberStyles.HexNumber);
-
- if (f > Symbols.MaximumCodepoint) f = Symbols.MaximumCodepoint;
-
- while (index <= f)
- {
- list.Add(index.ConvertFromUtf32());
- index++;
- }
-
- return list.ToArray();
- }
-
public RangeToken(string range, TextPosition position)
: base(TokenType.Range, range, position)
{
Start = range.Replace(Symbols.QuestionMark, '0');
End = range.Replace(Symbols.QuestionMark, 'F');
- SelectedRange = GetRange();
}
public RangeToken(string start, string end, TextPosition position)
@@ -40,12 +14,16 @@ public RangeToken(string start, string end, TextPosition position)
{
Start = start;
End = end;
- SelectedRange = GetRange();
}
- //public bool IsEmpty => (SelectedRange == null) || (SelectedRange.Length == 0);
+ // Data holds the range without its "U+" prefix, so serializing it bare would emit "41-5A" - not a
+ // valid and not what was authored.
+ public override string ToValue()
+ {
+ return string.Concat("U+", Data);
+ }
+
public string Start { get; }
public string End { get; }
- public string[] SelectedRange { get; }
}
-}
\ No newline at end of file
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Values/Length.cs b/Source/HtmlRenderer/Core/CssEngine/Values/Length.cs
index 5183e1464..6b8e05466 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Values/Length.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Values/Length.cs
@@ -176,7 +176,11 @@ public static bool TryParse(string s, out Length result)
return true;
}
- if (value == 0f)
+ // Only a genuinely parsed bare "0" may omit its unit (CSS Values & Units §5.1) -
+ // StylesheetUnit returns an empty unit string for a plain number but null when it
+ // couldn't tokenize a number at all, and both leave value at 0, so the unit string
+ // must be checked to avoid accepting arbitrary non-length input as zero.
+ if (unitString != null && unitString.Length == 0 && value == 0f)
{
result = Zero;
return true;
@@ -229,27 +233,26 @@ public float ToPixel()
/// If the length is in pixels and font related, apply the 72/96 factor.
internal double ToPixels(double emFactor, double remFactor, double hundredPercent, bool fontAdjust = false)
{
- // Physical units (in/cm/mm/pc/pt) resolve directly against points, not the browser's
- // 96dpi CSS-px convention: this engine's native "pixel" unit already equals 1 point at
- // the default PixelsPerInch=72 (matching @page's own mm->pt conversion in DomParser and
- // font-size resolution in FontSizeResolver), so no 96dpi scaling belongs here - applying
- // one inflated every pt/in/cm/mm length by 96/72 relative to the rest of the engine.
+ // The layout unit here is the CSS pixel, so absolute units resolve against 96dpi
+ // (CSS Values & Units §5.2: 1in = 96px, 1pt = 1/72in, 1pc = 12pt). These factors must
+ // stay identical to the ones CssValueParser.ParseLength applies to non-calc() lengths,
+ // otherwise `calc(1in)` and `width: 1in` would disagree.
switch (Type)
{
case Unit.Em: return emFactor * Value;
case Unit.Rem: return remFactor * Value;
case Unit.Ex: return emFactor / 2 * Value;
case Unit.Px: return (fontAdjust ? 72d / 96d : 1d) * Value; //TODO: a check support for hi dpi
- case Unit.In: // 1 in = 72 pt
- return 72d * Value;
- case Unit.Mm: // 1 mm = 72/25.4 pt
- return (72d / 25.4d) * Value;
- case Unit.Pc: // 1 pc = 12 pt
- return 12d * Value;
- case Unit.Pt: // 1 pt = 1 pt
- return Value;
- case Unit.Cm: // 1 cm = 72/2.54 pt
- return (72d / 2.54d) * Value;
+ case Unit.In: // 1 in = 96 px
+ return 96d * Value;
+ case Unit.Mm: // 1 mm = 96/25.4 px
+ return (96d / 25.4d) * Value;
+ case Unit.Pc: // 1 pc = 12 pt = 16 px
+ return 16d * Value;
+ case Unit.Pt: // 1 pt = 1/72 in = 96/72 px
+ return (96d / 72d) * Value;
+ case Unit.Cm: // 1 cm = 96/2.54 px
+ return (96d / 2.54d) * Value;
case Unit.Percent: return hundredPercent / 100d * Value;
default: return 0d;
}
@@ -259,19 +262,20 @@ public float To(Unit unit)
{
var value = ToPixel();
+ // Inverse of ToPixels' absolute-unit factors: `value` is in CSS pixels.
switch (unit)
{
- case Unit.In: // 1 in = 72 pt
- return value / 72f;
- case Unit.Mm: // 1 mm = 72/25.4 pt
- return value * 25.4f / 72f;
- case Unit.Pc: // 1 pc = 12 pt
- return value / 12f;
- case Unit.Pt: // 1 pt = 1 pt
- return value;
- case Unit.Cm: // 1 cm = 72/2.54 pt
- return value * 2.54f / 72f;
- case Unit.Px: // 1 px = 1 pt
+ case Unit.In: // 1 in = 96 px
+ return value / 96f;
+ case Unit.Mm: // 1 mm = 96/25.4 px
+ return value * 25.4f / 96f;
+ case Unit.Pc: // 1 pc = 16 px
+ return value / 16f;
+ case Unit.Pt: // 1 pt = 96/72 px
+ return value * 72f / 96f;
+ case Unit.Cm: // 1 cm = 96/2.54 px
+ return value * 2.54f / 96f;
+ case Unit.Px: // px is the layout unit
return value;
default:
throw new InvalidOperationException("An absolute unit cannot be converted to a relative one.");
diff --git a/Source/HtmlRenderer/Core/CssEngine/Values/Resolution.cs b/Source/HtmlRenderer/Core/CssEngine/Values/Resolution.cs
index c8b91f4c2..9355bac2e 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Values/Resolution.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Values/Resolution.cs
@@ -55,6 +55,8 @@ public static Unit GetUnit(string s)
return Unit.Dpi;
case "dppx":
return Unit.Dppx;
+ case "x": // `x` is the canonical alias for `dppx` (CSS Values 4 §7.4)
+ return Unit.Dppx;
default:
return Unit.None;
}
From 847ab525000139f7908f78ff40eb4946c1ca201d Mon Sep 17 00:00:00 2001
From: Justin Haygood
Date: Sat, 25 Jul 2026 00:11:07 -0400
Subject: [PATCH 06/21] CSS engine: sync CSS Color 4/5 color science
Adds the color-space math and color-function resolution from PeachPDF v0.9.6
(upstream cc1a521, 796bb8c, 7cf1855).
- ColorSpaceMath: forward/reverse transforms between gamma-encoded sRGB and
srgb-linear/display-p3/lab/oklab/xyz-d65/xyz-d50/hsl/hwb/lch/oklch, plus
polar hue interpolation (shorter/longer/increasing/decreasing)
- Color: FromLab/FromOklab/FromLch/FromOklch factories and Mix(), the
premultiplied-alpha color-mix() interpolation
- ColorFunctionExtensions: ToResolvedColor(), which resolves a tokenized value
through every color function - rgb/hsl/hwb/gray plus lab/oklab/lch/oklch and
color-mix() - and MixPaletteColors() for palette-mix()
- ColorInterpolationMethodGrammar: the shared `in [ hue]` prelude
grammar, so the value converter and the render-time parser agree on which
gradient interpolation methods are valid
Downgraded to C# 7.3 for the netstandard2.0 leg: collection expressions, switch
expressions, property patterns, index-from-end and Math.Clamp. Math.Cbrt is
netstandard2.1+, so ColorSpaceMath carries a sign-preserving cube root.
Co-Authored-By: Claude Opus 5
---
.../ColorInterpolationMethodGrammar.cs | 172 +++++++
.../Extensions/ColorFunctionExtensions.cs | 381 +++++++++++++++
.../Core/CssEngine/Values/Color.cs | 77 ++++
.../Core/CssEngine/Values/ColorSpaceMath.cs | 436 ++++++++++++++++++
4 files changed, 1066 insertions(+)
create mode 100644 Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Values/ColorSpaceMath.cs
diff --git a/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs
new file mode 100644
index 000000000..f508d92c9
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Collections.Generic;
+
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ ///
+ /// The single, shared grammar for a gradient's <color-interpolation-method> prelude
+ /// (CSS Images 4 §3.1):
+ /// in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ].
+ /// Both the CSS-OM validator (, which drops a malformed prelude at
+ /// parse time) and the render-time parser classify keywords through this one class, so the two
+ /// layers cannot disagree about which preludes are valid.
+ ///
+ /// Only the color spaces the interpolation math actually implements are recognized; the
+ /// wide-gamut RGB spaces a98-rgb/prophoto-rgb/rec2020 are valid CSS but are
+ /// deliberately rejected here rather than approximated.
+ ///
+ internal static class ColorInterpolationMethodGrammar
+ {
+ private const string In = "in";
+ private const string Hue = "hue";
+
+ private static readonly HashSet ColorSpaces = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "srgb", "srgb-linear", "display-p3", "lab", "oklab", "xyz", "xyz-d65", "xyz-d50",
+ "hsl", "hwb", "lch", "oklch",
+ };
+
+ private static readonly HashSet PolarColorSpaces = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "hsl", "hwb", "lch", "oklch",
+ };
+
+ private static readonly HashSet HueDirections = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "shorter", "longer", "increasing", "decreasing",
+ };
+
+ public static bool IsColorSpace(string name)
+ {
+ return name != null && ColorSpaces.Contains(name);
+ }
+
+ public static bool IsPolarColorSpace(string name)
+ {
+ return name != null && PolarColorSpaces.Contains(name);
+ }
+
+ public static bool IsHueDirection(string name)
+ {
+ return name != null && HueDirections.Contains(name);
+ }
+
+ ///
+ /// If (one gradient comma-group's tokens) contains an in
+ /// interpolation-method prelude, validates it and returns the group with the contiguous
+ /// in … slice removed as (for the caller to validate as the
+ /// gradient's own direction/shape/position, since the two combine with || in either order).
+ /// Returns when an in is present but the method is malformed
+ /// (unknown/unsupported space, a hue-interpolation method on a rectangular space or without the
+ /// trailing hue, or in with nothing after it) — the caller drops the declaration.
+ /// When no in is present, is
+ /// , is the group unchanged, and it returns
+ /// .
+ ///
+ public static bool TryExtractInterpolationMethod(
+ IEnumerable group, out List remainder, out bool hasInterpolationMethod)
+ {
+ var tokens = new List(group);
+
+ int inIndex = IndexOfIdent(tokens, 0, In);
+ if (inIndex < 0)
+ {
+ remainder = tokens;
+ hasInterpolationMethod = false;
+ return true;
+ }
+
+ hasInterpolationMethod = true;
+
+ // in
+ int spaceIndex = NextNonWhitespace(tokens, inIndex + 1);
+ string spaceName;
+ if (spaceIndex < 0 || !IsIdent(tokens[spaceIndex], out spaceName) || !IsColorSpace(spaceName))
+ {
+ remainder = null;
+ return false;
+ }
+
+ int lastMethodIndex = spaceIndex;
+
+ // A hue-interpolation method ( hue) is only valid after a polar space, and only complete
+ // with the trailing "hue" keyword.
+ if (IsPolarColorSpace(spaceName))
+ {
+ int dirIndex = NextNonWhitespace(tokens, spaceIndex + 1);
+ string dir;
+ if (dirIndex >= 0 && IsIdent(tokens[dirIndex], out dir) && IsHueDirection(dir))
+ {
+ int hueIndex = NextNonWhitespace(tokens, dirIndex + 1);
+ string hueWord;
+ if (hueIndex < 0 || !IsIdent(tokens[hueIndex], out hueWord) ||
+ !string.Equals(hueWord, Hue, StringComparison.OrdinalIgnoreCase))
+ {
+ remainder = null;
+ return false;
+ }
+
+ lastMethodIndex = hueIndex;
+ }
+ }
+
+ remainder = BuildRemainder(tokens, inIndex, lastMethodIndex);
+ return true;
+ }
+
+ // The group with tokens [firstMethodIndex..lastMethodIndex] removed, then whitespace-trimmed at the
+ // edges so the leftover direction/shape validates the same as an un-prefixed group would.
+ private static List BuildRemainder(List tokens, int firstMethodIndex, int lastMethodIndex)
+ {
+ var remainder = new List(tokens.Count);
+
+ for (int i = 0; i < tokens.Count; i++)
+ {
+ if (i >= firstMethodIndex && i <= lastMethodIndex) continue;
+ remainder.Add(tokens[i]);
+ }
+
+ while (remainder.Count > 0 && remainder[0].Type == TokenType.Whitespace)
+ remainder.RemoveAt(0);
+ while (remainder.Count > 0 && remainder[remainder.Count - 1].Type == TokenType.Whitespace)
+ remainder.RemoveAt(remainder.Count - 1);
+
+ return remainder;
+ }
+
+ private static int IndexOfIdent(List tokens, int start, string data)
+ {
+ for (int i = start; i < tokens.Count; i++)
+ {
+ string value;
+ if (IsIdent(tokens[i], out value) &&
+ string.Equals(value, data, StringComparison.OrdinalIgnoreCase))
+ return i;
+ }
+
+ return -1;
+ }
+
+ private static int NextNonWhitespace(List tokens, int start)
+ {
+ for (int i = start; i < tokens.Count; i++)
+ {
+ if (tokens[i].Type != TokenType.Whitespace) return i;
+ }
+
+ return -1;
+ }
+
+ private static bool IsIdent(Token token, out string data)
+ {
+ if (token.Type == TokenType.Ident)
+ {
+ data = token.Data;
+ return true;
+ }
+
+ data = null;
+ return false;
+ }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs
new file mode 100644
index 000000000..a92adb739
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs
@@ -0,0 +1,381 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ ///
+ /// Resolves the CSS color function forms from a tokenized value into a concrete
+ /// : the legacy/CSS Color 4 rgb()/rgba()/hsl()/hsla()/hwb()/gray() and the
+ /// CSS Color 4/5 lab()/oklab()/lch()/oklch()/color-mix(). Tokenization is the CSS-OM's job, so
+ /// this consumes the already-parsed stream (nested functions arrive as a
+ /// single token) rather than re-scanning the source text.
+ ///
+ internal static class ColorFunctionExtensions
+ {
+ ///
+ /// Fully resolves a tokenized color value to a concrete - named colors and
+ /// hex via , plus every color function
+ /// (rgb/hsl/hwb/gray/lab/oklab/lch/oklch/color-mix). Unlike ToColor (which the CSS-OM
+ /// cascade uses and which deliberately preserves a function's specified form for serialization),
+ /// this is the render-layer resolver: it always computes the sRGB value.
+ ///
+ public static Color? ToResolvedColor(this IEnumerable value)
+ {
+ var basic = value.ToColor();
+ if (basic.HasValue) return basic;
+
+ var element = value.OnlyOrDefault();
+
+ // A hex color nested inside a function (e.g. a color-mix() operand) tokenizes as a Hash
+ // keyword token rather than a ColorToken, so ToColor misses it - resolve it here.
+ if (element != null && element.Type == TokenType.Hash) return Color.FromHex(element.Data);
+
+ var function = element as FunctionToken;
+ return function != null ? ParseColorFunction(function) : null;
+ }
+
+ public static Color? ParseColorFunction(FunctionToken function)
+ {
+ var name = function.Data;
+ var args = function.ArgumentTokens;
+
+ if (name.Equals(FunctionNames.Rgb, StringComparison.OrdinalIgnoreCase) ||
+ name.Equals(FunctionNames.Rgba, StringComparison.OrdinalIgnoreCase))
+ return ParseRgb(args);
+
+ if (name.Equals(FunctionNames.Hsl, StringComparison.OrdinalIgnoreCase) ||
+ name.Equals(FunctionNames.Hsla, StringComparison.OrdinalIgnoreCase))
+ return ParseHsl(args);
+
+ if (name.Equals(FunctionNames.Hwb, StringComparison.OrdinalIgnoreCase))
+ return ParseHwb(args);
+
+ if (name.Equals(FunctionNames.Gray, StringComparison.OrdinalIgnoreCase))
+ return ParseGray(args);
+
+ if (name.Equals(FunctionNames.Lab, StringComparison.OrdinalIgnoreCase))
+ return ParseRect(args, Color.FromLab, abReference: 125.0, lReference: 100.0);
+ if (name.Equals(FunctionNames.Oklab, StringComparison.OrdinalIgnoreCase))
+ return ParseRect(args, Color.FromOklab, abReference: 0.4, lReference: 1.0);
+ if (name.Equals(FunctionNames.Lch, StringComparison.OrdinalIgnoreCase))
+ return ParsePolar(args, Color.FromLch, chromaReference: 150.0, lReference: 100.0);
+ if (name.Equals(FunctionNames.Oklch, StringComparison.OrdinalIgnoreCase))
+ return ParsePolar(args, Color.FromOklch, chromaReference: 0.4, lReference: 1.0);
+
+ if (name.Equals(FunctionNames.ColorMix, StringComparison.OrdinalIgnoreCase))
+ return ParseColorMix(args);
+
+ return null;
+ }
+
+ // rgb()/rgba(): 3 components (int or % of 255) + optional alpha
+ private static Color? ParseRgb(IEnumerable args)
+ {
+ List comps;
+ Token alphaTok;
+ if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ var r = AsTokens(comps[0]).ToRgbComponent();
+ var g = AsTokens(comps[1]).ToRgbComponent();
+ var b = AsTokens(comps[2]).ToRgbComponent();
+ if (r == null || g == null || b == null) return null;
+ var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
+ return new Color(r.Value, g.Value, b.Value, Clamp01Byte(a));
+ }
+
+ // hsl()/hsla(): hue angle + saturation% + lightness% + optional alpha
+ private static Color? ParseHsl(IEnumerable args)
+ {
+ List comps;
+ Token alphaTok;
+ if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ var hue = HueDegrees(comps[0]);
+ var sat = AsTokens(comps[1]).ToPercent();
+ var light = AsTokens(comps[2]).ToPercent();
+ if (hue == null || sat == null || light == null) return null;
+ var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
+ return Color.FromHsla((float)(hue.Value / 360.0), sat.Value.NormalizedValue, light.Value.NormalizedValue, a);
+ }
+
+ // hwb(): hue angle + whiteness% + blackness% + optional alpha
+ private static Color? ParseHwb(IEnumerable args)
+ {
+ List comps;
+ Token alphaTok;
+ if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ var hue = HueDegrees(comps[0]);
+ var white = AsTokens(comps[1]).ToPercent();
+ var black = AsTokens(comps[2]).ToPercent();
+ if (hue == null || white == null || black == null) return null;
+ var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
+ return Color.FromHwba((float)(hue.Value / 360.0), white.Value.NormalizedValue, black.Value.NormalizedValue, a);
+ }
+
+ // gray(): a single lightness component + optional alpha
+ private static Color? ParseGray(IEnumerable args)
+ {
+ List comps;
+ Token alphaTok;
+ if (!Extract(args, 1, out comps, out alphaTok)) return null;
+ var value = AsTokens(comps[0]).ToRgbComponent();
+ if (value == null) return null;
+ var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
+ return Color.FromGray(value.Value, a);
+ }
+
+ // lab()/oklab(): L + a + b + optional alpha
+ private static Color? ParseRect(IEnumerable args, Func build, double abReference, double lReference)
+ {
+ List comps;
+ Token alphaTok;
+ if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ var l = Component(comps[0], lReference);
+ var a = Component(comps[1], abReference);
+ var b = Component(comps[2], abReference);
+ if (l == null || a == null || b == null) return null;
+ var alpha = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
+ return build(l.Value, a.Value, b.Value, alpha);
+ }
+
+ // lch()/oklch(): L + C + hue angle + optional alpha
+ private static Color? ParsePolar(IEnumerable args, Func build, double chromaReference, double lReference)
+ {
+ List comps;
+ Token alphaTok;
+ if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ var l = Component(comps[0], lReference);
+ var c = Component(comps[1], chromaReference);
+ var h = HueDegrees(comps[2]);
+ if (l == null || c == null || h == null) return null;
+ var alpha = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
+ return build(l.Value, c.Value, h.Value, alpha);
+ }
+
+ // color-mix(in [], c1 [p1], c2 [p2])
+ private static Color? ParseColorMix(IEnumerable args)
+ {
+ var segments = SplitByComma(args);
+ if (segments.Count != 3) return null;
+
+ var config = segments[0].Where(token => token.Type != TokenType.Whitespace)
+ .Select(token => token.Data.ToLowerInvariant()).ToList();
+ if (config.Count < 2 || config[0] != "in") return null;
+ var space = MapSpace(config[1]);
+ var hue = MapHue(config, 2);
+
+ Color c1, c2;
+ double? p1, p2;
+ if (!ParseMixOperand(segments[1], out c1, out p1)) return null;
+ if (!ParseMixOperand(segments[2], out c2, out p2)) return null;
+
+ // CSS Color 5 §3 normalization: a missing percentage is 100% minus the other; percentages
+ // scale to sum 1, and a pre-normalization sum below 100% scales the result alpha down.
+ if (p1 == null && p2 == null) { p1 = 0.5; p2 = 0.5; }
+ else if (p1 == null) p1 = Math.Max(0, 1.0 - p2.Value);
+ else if (p2 == null) p2 = Math.Max(0, 1.0 - p1.Value);
+
+ var sum = p1.Value + p2.Value;
+ if (sum <= 0) return null;
+ var alphaMultiplier = sum < 1.0 ? sum : 1.0;
+ var t = p2.Value / sum;
+
+ return Color.Mix(c1, c2, t, space, hue, alphaMultiplier);
+ }
+
+ private static bool ParseMixOperand(List segment, out Color color, out double? percent)
+ {
+ color = default(Color);
+ percent = null;
+
+ var tokens = new List();
+ foreach (var token in segment)
+ {
+ if (token.Type != TokenType.Whitespace) tokens.Add(token);
+ }
+ if (tokens.Count == 0) return false;
+
+ // The optional percentage may appear before or after the color (CSS Color 5 §3.1), e.g.
+ // both `red 30%` and `30% red` are valid.
+ if (tokens[tokens.Count - 1].Type == TokenType.Percentage)
+ {
+ var value = AsTokens(tokens[tokens.Count - 1]).ToPercent();
+ percent = value.HasValue ? value.Value.NormalizedValue : (double?)null;
+ tokens.RemoveAt(tokens.Count - 1);
+ }
+ else if (tokens[0].Type == TokenType.Percentage)
+ {
+ var value = AsTokens(tokens[0]).ToPercent();
+ percent = value.HasValue ? value.Value.NormalizedValue : (double?)null;
+ tokens.RemoveAt(0);
+ }
+
+ // A color-mix operand may itself be any color form (named/hex/rgb/oklch/...), so resolve it
+ // through the full render-layer resolver rather than the named/hex-only ToColor.
+ var resolved = tokens.ToResolvedColor();
+ if (resolved == null) return false;
+ color = resolved.Value;
+ return true;
+ }
+
+ // Token helpers
+
+ // Partitions a function's argument tokens into `count` component tokens and an optional alpha
+ // token. Whitespace and commas separate; a "/" delimiter (CSS Color 4) marks the alpha that
+ // follows. Without a slash, a trailing extra value (legacy rgba/hsla comma alpha) is the alpha.
+ private static bool Extract(IEnumerable args, int count, out List components, out Token alpha)
+ {
+ components = new List();
+ alpha = null;
+
+ var values = new List();
+ var slashIndex = -1;
+ foreach (var token in args)
+ {
+ switch (token.Type)
+ {
+ case TokenType.Whitespace:
+ case TokenType.Comma:
+ continue;
+ case TokenType.Delim when token.Data == "/":
+ slashIndex = values.Count;
+ continue;
+ default:
+ values.Add(token);
+ break;
+ }
+ }
+
+ if (values.Count < count) return false;
+ for (var i = 0; i < count; i++) components.Add(values[i]);
+
+ var alphaAt = slashIndex >= 0 ? slashIndex : count;
+ if (alphaAt < values.Count) alpha = values[alphaAt];
+ return true;
+ }
+
+ private static List> SplitByComma(IEnumerable tokens)
+ {
+ var result = new List>();
+ var current = new List();
+ foreach (var token in tokens)
+ {
+ if (token.Type == TokenType.Comma)
+ {
+ result.Add(current);
+ current = new List();
+ }
+ else
+ {
+ current.Add(token);
+ }
+ }
+ result.Add(current);
+ return result;
+ }
+
+ private static Token[] AsTokens(Token token)
+ {
+ return new[] { token };
+ }
+
+ // A number (returned as-is) or a percentage resolved against `reference`; "none" resolves to 0.
+ private static double? Component(Token token, double reference)
+ {
+ if (token.Type == TokenType.Ident && token.Data.Equals("none", StringComparison.OrdinalIgnoreCase))
+ return 0.0;
+ var number = AsTokens(token).ToSingle();
+ if (number.HasValue) return number.Value;
+ var percent = AsTokens(token).ToPercent();
+ return percent.HasValue ? percent.Value.NormalizedValue * reference : (double?)null;
+ }
+
+ private static double? HueDegrees(Token token)
+ {
+ if (token.Type == TokenType.Ident && token.Data.Equals("none", StringComparison.OrdinalIgnoreCase))
+ return 0.0;
+ var angle = AsTokens(token).ToAngleNumber();
+ if (!angle.HasValue) return null;
+
+ switch (angle.Value.Type)
+ {
+ case Angle.Unit.Grad: return angle.Value.Value * 0.9;
+ case Angle.Unit.Turn: return angle.Value.Value * 360.0;
+ case Angle.Unit.Rad: return angle.Value.Value * 180.0 / Math.PI;
+ default: return angle.Value.Value;
+ }
+ }
+
+ private static byte Clamp01Byte(float alpha)
+ {
+ return (byte)Math.Round(Math.Min(Math.Max(alpha, 0f), 1f) * 255);
+ }
+
+ private static ColorSpaceMath.Space MapSpace(string name)
+ {
+ switch (name)
+ {
+ case "srgb-linear": return ColorSpaceMath.Space.SrgbLinear;
+ case "display-p3": return ColorSpaceMath.Space.DisplayP3;
+ case "lab": return ColorSpaceMath.Space.Lab;
+ case "oklab": return ColorSpaceMath.Space.Oklab;
+ case "xyz":
+ case "xyz-d65": return ColorSpaceMath.Space.XyzD65;
+ case "xyz-d50": return ColorSpaceMath.Space.XyzD50;
+ case "hsl": return ColorSpaceMath.Space.Hsl;
+ case "hwb": return ColorSpaceMath.Space.Hwb;
+ case "lch": return ColorSpaceMath.Space.Lch;
+ case "oklch": return ColorSpaceMath.Space.Oklch;
+ default: return ColorSpaceMath.Space.Srgb;
+ }
+ }
+
+ private static ColorSpaceMath.HueMethod MapHue(List idents, int startIndex)
+ {
+ for (var i = startIndex; i + 1 < idents.Count; i++)
+ {
+ if (idents[i + 1] != "hue") continue;
+ return MapHueName(idents[i]);
+ }
+ return ColorSpaceMath.HueMethod.Shorter;
+ }
+
+ private static ColorSpaceMath.HueMethod MapHueName(string name)
+ {
+ switch (name)
+ {
+ case "longer": return ColorSpaceMath.HueMethod.Longer;
+ case "increasing": return ColorSpaceMath.HueMethod.Increasing;
+ case "decreasing": return ColorSpaceMath.HueMethod.Decreasing;
+ default: return ColorSpaceMath.HueMethod.Shorter;
+ }
+ }
+
+ ///
+ /// Blends two colors per the CSS palette-mix() rules (CSS Fonts 4), reusing color-mix()'s
+ /// color-space mapping () and CSS Color 5 §3 percentage normalization so the two
+ /// functions interpolate identically. / are 0..100 percentages
+ /// (or null); is a hue-interpolation direction (or null for the default).
+ ///
+ internal static Color MixPaletteColors(Color c1, Color c2, string spaceName, string hueName, double? p1, double? p2)
+ {
+ // CSS keywords are case-insensitive; MapSpace/MapHueName match lowercase literals, so fold first
+ // (a mixed-case space would otherwise silently fall through to the sRGB/Shorter default).
+ var space = MapSpace(spaceName.ToLowerInvariant());
+ var hue = MapHueName(hueName.ToLowerInvariant());
+
+ double? f1 = p1.HasValue ? Math.Min(Math.Max(p1.Value, 0), 100) / 100.0 : (double?)null;
+ double? f2 = p2.HasValue ? Math.Min(Math.Max(p2.Value, 0), 100) / 100.0 : (double?)null;
+
+ if (f1 == null && f2 == null) { f1 = 0.5; f2 = 0.5; }
+ else if (f1 == null) f1 = 1.0 - f2;
+ else if (f2 == null) f2 = 1.0 - f1;
+
+ var sum = f1.Value + f2.Value;
+ if (sum <= 0) return c1;
+
+ var alphaMultiplier = sum < 1.0 ? sum : 1.0;
+ var t = f2.Value / sum;
+ return Color.Mix(c1, c2, t, space, hue, alphaMultiplier);
+ }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Values/Color.cs b/Source/HtmlRenderer/Core/CssEngine/Values/Color.cs
index 5bb4d73dc..06b326524 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Values/Color.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Values/Color.cs
@@ -268,6 +268,83 @@ public static Color FromHwba(float hue, float whiteness, float blackness, float
return FromRgba(red, green, blue, alpha);
}
+ // CSS Color 4/5 spaces (lab/oklab/lch/oklch) and color-mix().
+ // Components are in each space's native units (L: lab/lch 0..100, oklab/oklch 0..1; a/b and C
+ // in native chroma units; H in degrees). The conversion math is shared with gradient
+ // interpolation via ColorSpaceMath so there is a single implementation of the color science.
+
+ public static Color FromLab(double l, double a, double b, float alpha = 1f)
+ {
+ return FromSpaceComponents(new[] { l, a, b }, ColorSpaceMath.Space.Lab, alpha);
+ }
+
+ public static Color FromOklab(double l, double a, double b, float alpha = 1f)
+ {
+ return FromSpaceComponents(new[] { l, a, b }, ColorSpaceMath.Space.Oklab, alpha);
+ }
+
+ public static Color FromLch(double l, double c, double h, float alpha = 1f)
+ {
+ return FromSpaceComponents(new[] { l, c, h }, ColorSpaceMath.Space.Lch, alpha);
+ }
+
+ public static Color FromOklch(double l, double c, double h, float alpha = 1f)
+ {
+ return FromSpaceComponents(new[] { l, c, h }, ColorSpaceMath.Space.Oklch, alpha);
+ }
+
+ private static Color FromSpaceComponents(double[] v, ColorSpaceMath.Space cs, float alpha)
+ {
+ var rgb = ColorSpaceMath.FromSpace(v, cs);
+ return new Color(ToByte(rgb.Item1), ToByte(rgb.Item2), ToByte(rgb.Item3), Normalize(alpha));
+ }
+
+ ///
+ /// CSS Color 5 color-mix():
+ /// premultiplied-alpha interpolation in with weight
+ /// toward , the result alpha then scaled by
+ /// (below 1 only when the two percentages summed to under 100%).
+ ///
+ public static Color Mix(Color c1, Color c2, double t, ColorSpaceMath.Space cs, ColorSpaceMath.HueMethod hue, double alphaMultiplier)
+ {
+ t = Math.Min(Math.Max(t, 0.0), 1.0);
+ double a1 = c1.A / 255.0, a2 = c2.A / 255.0;
+
+ var v1 = ColorSpaceMath.ToSpace(c1.R / 255.0, c1.G / 255.0, c1.B / 255.0, cs);
+ var v2 = ColorSpaceMath.ToSpace(c2.R / 255.0, c2.G / 255.0, c2.B / 255.0, cs);
+ var hueIndex = ColorSpaceMath.HueIndex(cs);
+
+ for (var i = 0; i < 3; i++)
+ {
+ if (i == hueIndex) continue;
+ v1[i] *= a1;
+ v2[i] *= a2;
+ }
+
+ var mixedAlpha = a1 + t * (a2 - a1);
+ var v = new double[3];
+ for (var i = 0; i < 3; i++)
+ {
+ if (i == hueIndex)
+ {
+ v[i] = ColorSpaceMath.InterpolateHue(v1[i], v2[i], t, hue);
+ continue;
+ }
+
+ var mixed = v1[i] + t * (v2[i] - v1[i]);
+ v[i] = mixedAlpha > 1e-10 ? mixed / mixedAlpha : 0;
+ }
+
+ var rgb = ColorSpaceMath.FromSpace(v, cs);
+ var outAlpha = (byte)Math.Round(Math.Min(Math.Max(mixedAlpha * alphaMultiplier, 0), 1) * 255);
+ return new Color(ToByte(rgb.Item1), ToByte(rgb.Item2), ToByte(rgb.Item3), outAlpha);
+ }
+
+ private static byte ToByte(double v)
+ {
+ return (byte)Math.Round(Math.Min(Math.Max(v, 0), 1) * 255);
+ }
+
public int Value => _hashcode;
public byte A => _alpha;
public double Alpha => Math.Round(_alpha / 255.0, 2);
diff --git a/Source/HtmlRenderer/Core/CssEngine/Values/ColorSpaceMath.cs b/Source/HtmlRenderer/Core/CssEngine/Values/ColorSpaceMath.cs
new file mode 100644
index 000000000..452e53ce2
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Values/ColorSpaceMath.cs
@@ -0,0 +1,436 @@
+using System;
+
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ ///
+ /// The single home for CSS color-space conversion math (CSS Color 4/5): forward/reverse transforms
+ /// between gamma-encoded sRGB and every interpolation space, plus polar-hue interpolation. Kept in
+ /// the CSS-OM (rather than the render layer) so that solid-color resolution ('s
+ /// oklch()/lab()/color-mix() factories) and any gradient interpolation that
+ /// needs a non-sRGB space share one implementation.
+ ///
+ internal static class ColorSpaceMath
+ {
+ internal enum Space { Srgb, SrgbLinear, DisplayP3, Lab, Oklab, XyzD65, XyzD50, Hsl, Hwb, Lch, Oklch }
+
+ internal enum HueMethod { Shorter, Longer, Increasing, Decreasing }
+
+ public static bool IsPolar(Space cs)
+ {
+ return cs == Space.Hsl || cs == Space.Hwb || cs == Space.Lch || cs == Space.Oklch;
+ }
+
+ // The index of the hue coordinate in a space's ToSpace/FromSpace layout, or -1 if rectangular.
+ // HSL/HWB carry hue first ([h, s/w, l/bk]); LCH/OKLch carry it last ([L, C, H]).
+ public static int HueIndex(Space cs)
+ {
+ switch (cs)
+ {
+ case Space.Hsl:
+ case Space.Hwb:
+ return 0;
+ case Space.Lch:
+ case Space.Oklch:
+ return 2;
+ default:
+ return -1;
+ }
+ }
+
+ public static double NormalizeHue(double h)
+ {
+ return ((h % 360) + 360) % 360;
+ }
+
+ public static double InterpolateHue(double h1, double h2, double t, HueMethod hue)
+ {
+ h1 = NormalizeHue(h1);
+ h2 = NormalizeHue(h2);
+ var diff = h2 - h1;
+ switch (hue)
+ {
+ case HueMethod.Longer:
+ if (diff > 0 && diff < 180) diff -= 360;
+ else if (diff < 0 && diff > -180) diff += 360;
+ break;
+ case HueMethod.Increasing:
+ if (diff < 0) diff += 360;
+ break;
+ case HueMethod.Decreasing:
+ if (diff > 0) diff -= 360;
+ break;
+ default: // Shorter
+ if (diff > 180) diff -= 360;
+ else if (diff < -180) diff += 360;
+ break;
+ }
+ return h1 + t * diff;
+ }
+
+ private static double Clamp01(double v)
+ {
+ return v < 0 ? 0 : (v > 1 ? 1 : v);
+ }
+
+ // Cube root that accepts negative input, for targets whose Math has no Cbrt.
+ private static double Cbrt(double v)
+ {
+ return v < 0 ? -Math.Pow(-v, 1.0 / 3.0) : Math.Pow(v, 1.0 / 3.0);
+ }
+
+ // sRGB gamma
+
+ private static double Linearize(double c)
+ {
+ c = Clamp01(c);
+ return c <= 0.04045 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4);
+ }
+
+ private static double Delinearize(double c)
+ {
+ c = Clamp01(c);
+ return c <= 0.0031308 ? c * 12.92 : 1.055 * Math.Pow(c, 1.0 / 2.4) - 0.055;
+ }
+
+ // Linear sRGB <-> XYZ-D65 (IEC 61966-2-1)
+
+ private static (double X, double Y, double Z) LinSrgbToXyzD65(double r, double g, double b)
+ {
+ return (0.4124564 * r + 0.3575761 * g + 0.1804375 * b,
+ 0.2126729 * r + 0.7151522 * g + 0.0721750 * b,
+ 0.0193339 * r + 0.1191920 * g + 0.9503041 * b);
+ }
+
+ private static (double R, double G, double B) XyzD65ToLinSrgb(double x, double y, double z)
+ {
+ return ( 3.2404542 * x - 1.5371385 * y - 0.4985314 * z,
+ -0.9692660 * x + 1.8760108 * y + 0.0415560 * z,
+ 0.0556434 * x - 0.2040259 * y + 1.0572252 * z);
+ }
+
+ // XYZ-D65 <-> XYZ-D50 (Bradford)
+
+ private static (double X, double Y, double Z) XyzD65ToXyzD50(double x, double y, double z)
+ {
+ return ( 1.0478112 * x + 0.0228866 * y - 0.0501270 * z,
+ 0.0295424 * x + 0.9904844 * y - 0.0170491 * z,
+ -0.0092345 * x + 0.0150436 * y + 0.7521316 * z);
+ }
+
+ private static (double X, double Y, double Z) XyzD50ToXyzD65(double x, double y, double z)
+ {
+ return ( 0.9554734 * x - 0.0230393 * y + 0.0631636 * z,
+ -0.0282895 * x + 1.0099416 * y + 0.0210077 * z,
+ 0.0122982 * x - 0.0204830 * y + 1.3299098 * z);
+ }
+
+ // Display-P3 <-> XYZ-D65 (P3 uses sRGB gamma)
+
+ private static (double X, double Y, double Z) LinP3ToXyzD65(double r, double g, double b)
+ {
+ return (0.4865709 * r + 0.2656677 * g + 0.1982173 * b,
+ 0.2289746 * r + 0.6917386 * g + 0.0792869 * b,
+ 0.0000000 * r + 0.0451134 * g + 1.0439444 * b);
+ }
+
+ private static (double R, double G, double B) XyzD65ToLinP3(double x, double y, double z)
+ {
+ return ( 2.4934969 * x - 0.9313836 * y - 0.4027108 * z,
+ -0.8294890 * x + 1.7626641 * y + 0.0236247 * z,
+ 0.0358458 * x - 0.0761724 * y + 0.9568845 * z);
+ }
+
+ // CIE L*a*b*
+
+ private const double Xn50 = 0.96422, Yn50 = 1.0, Zn50 = 0.82521;
+
+ private static double LabF(double t)
+ {
+ const double d = 6.0 / 29.0;
+ return t > d * d * d ? Cbrt(t) : t / (3 * d * d) + 4.0 / 29.0;
+ }
+
+ private static double LabFInv(double t)
+ {
+ const double d = 6.0 / 29.0;
+ return t > d ? t * t * t : 3 * d * d * (t - 4.0 / 29.0);
+ }
+
+ private static (double L, double a, double b) XyzD50ToLab(double x, double y, double z)
+ {
+ double fx = LabF(x / Xn50), fy = LabF(y / Yn50), fz = LabF(z / Zn50);
+ return (116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz));
+ }
+
+ private static (double X, double Y, double Z) LabToXyzD50(double L, double a, double b)
+ {
+ var fy = (L + 16) / 116;
+ return (Xn50 * LabFInv(a / 500 + fy), Yn50 * LabFInv(fy), Zn50 * LabFInv(fy - b / 200));
+ }
+
+ // OKLab (Björn Ottosson)
+
+ private static (double L, double a, double b) XyzD65ToOkLab(double x, double y, double z)
+ {
+ var l = Cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z);
+ var m = Cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z);
+ var s = Cbrt(0.0482003018 * x + 0.2643662691 * y + 0.6338517070 * z);
+ return (0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
+ 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
+ 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s);
+ }
+
+ private static (double X, double Y, double Z) OkLabToXyzD65(double L, double a, double b)
+ {
+ double l = L + 0.3963377774 * a + 0.2158037573 * b; l = l * l * l;
+ double m = L - 0.1055613458 * a - 0.0638541728 * b; m = m * m * m;
+ double s = L - 0.0894841775 * a - 1.2914855480 * b; s = s * s * s;
+ return ( 1.2270138511 * l - 0.5577999807 * m + 0.2812561490 * s,
+ -0.0405801784 * l + 1.1122568696 * m - 0.0716766787 * s,
+ -0.0763812845 * l - 0.4214819784 * m + 1.5861632204 * s);
+ }
+
+ // HSL
+
+ private static (double H, double S, double L) SrgbToHsl(double r, double g, double b)
+ {
+ double max = Math.Max(r, Math.Max(g, b));
+ double min = Math.Min(r, Math.Min(g, b));
+ double d = max - min;
+ double l = (max + min) / 2;
+ if (d < 1e-10) return (0, 0, l * 100);
+ double s = d / (1 - Math.Abs(2 * l - 1));
+ double h;
+ if (max == r) h = 60 * (((g - b) / d) % 6);
+ else if (max == g) h = 60 * ((b - r) / d + 2);
+ else h = 60 * ((r - g) / d + 4);
+ if (h < 0) h += 360;
+ return (h, s * 100, l * 100);
+ }
+
+ private static (double R, double G, double B) HslToSrgb(double h, double s, double l)
+ {
+ s /= 100; l /= 100;
+ double c = (1 - Math.Abs(2 * l - 1)) * s;
+ double x = c * (1 - Math.Abs(h / 60 % 2 - 1));
+ double m = l - c / 2;
+ double r = 0, g = 0, b = 0;
+ switch ((int)(h / 60) % 6)
+ {
+ case 0: r = c; g = x; break;
+ case 1: r = x; g = c; break;
+ case 2: g = c; b = x; break;
+ case 3: g = x; b = c; break;
+ case 4: r = x; b = c; break;
+ case 5: r = c; b = x; break;
+ }
+ return (r + m, g + m, b + m);
+ }
+
+ // HWB
+
+ private static (double H, double W, double Bk) SrgbToHwb(double r, double g, double b)
+ {
+ var hsl = SrgbToHsl(r, g, b);
+ return (hsl.H,
+ Math.Min(r, Math.Min(g, b)) * 100,
+ (1 - Math.Max(r, Math.Max(g, b))) * 100);
+ }
+
+ private static (double R, double G, double B) HwbToSrgb(double h, double w, double bk)
+ {
+ w /= 100; bk /= 100;
+ if (w + bk >= 1) { double grey = w / (w + bk); return (grey, grey, grey); }
+ var rgb = HslToSrgb(h, 100, 50);
+ double f = 1 - w - bk;
+ return (rgb.R * f + w, rgb.G * f + w, rgb.B * f + w);
+ }
+
+ // LCH / OKLch (rectangular <-> polar)
+
+ private static (double L, double C, double H) RectToPolar(double L, double a, double b)
+ {
+ double c = Math.Sqrt(a * a + b * b);
+ double h = Math.Atan2(b, a) * (180.0 / Math.PI);
+ if (h < 0) h += 360;
+ return (L, c, h);
+ }
+
+ private static (double L, double a, double b) PolarToRect(double L, double c, double h)
+ {
+ double rad = h * (Math.PI / 180.0);
+ return (L, c * Math.Cos(rad), c * Math.Sin(rad));
+ }
+
+ // Forward: gamma-sRGB (0..1) -> color-space coordinates
+
+ public static double[] ToSpace(double r, double g, double b, Space cs)
+ {
+ switch (cs)
+ {
+ case Space.SrgbLinear:
+ return new[] { Linearize(r), Linearize(g), Linearize(b) };
+
+ case Space.DisplayP3:
+ {
+ var xyz = LinSrgbToXyzD65(Linearize(r), Linearize(g), Linearize(b));
+ var p3 = XyzD65ToLinP3(xyz.X, xyz.Y, xyz.Z);
+ return new[] { Delinearize(p3.R), Delinearize(p3.G), Delinearize(p3.B) };
+ }
+
+ case Space.Lab:
+ {
+ var xyz = LinSrgbToXyzD65(Linearize(r), Linearize(g), Linearize(b));
+ var xyz50 = XyzD65ToXyzD50(xyz.X, xyz.Y, xyz.Z);
+ var lab = XyzD50ToLab(xyz50.X, xyz50.Y, xyz50.Z);
+ return new[] { lab.L, lab.a, lab.b };
+ }
+
+ case Space.Oklab:
+ {
+ var xyz = LinSrgbToXyzD65(Linearize(r), Linearize(g), Linearize(b));
+ var lab = XyzD65ToOkLab(xyz.X, xyz.Y, xyz.Z);
+ return new[] { lab.L, lab.a, lab.b };
+ }
+
+ case Space.XyzD65:
+ {
+ var xyz = LinSrgbToXyzD65(Linearize(r), Linearize(g), Linearize(b));
+ return new[] { xyz.X, xyz.Y, xyz.Z };
+ }
+
+ case Space.XyzD50:
+ {
+ var xyz = LinSrgbToXyzD65(Linearize(r), Linearize(g), Linearize(b));
+ var xyz50 = XyzD65ToXyzD50(xyz.X, xyz.Y, xyz.Z);
+ return new[] { xyz50.X, xyz50.Y, xyz50.Z };
+ }
+
+ case Space.Hsl:
+ {
+ var hsl = SrgbToHsl(r, g, b);
+ return new[] { hsl.H, hsl.S, hsl.L };
+ }
+
+ case Space.Hwb:
+ {
+ var hwb = SrgbToHwb(r, g, b);
+ return new[] { hwb.H, hwb.W, hwb.Bk };
+ }
+
+ case Space.Lch:
+ {
+ var xyz = LinSrgbToXyzD65(Linearize(r), Linearize(g), Linearize(b));
+ var xyz50 = XyzD65ToXyzD50(xyz.X, xyz.Y, xyz.Z);
+ var lab = XyzD50ToLab(xyz50.X, xyz50.Y, xyz50.Z);
+ var lch = RectToPolar(lab.L, lab.a, lab.b);
+ return new[] { lch.L, lch.C, lch.H };
+ }
+
+ case Space.Oklch:
+ {
+ var xyz = LinSrgbToXyzD65(Linearize(r), Linearize(g), Linearize(b));
+ var lab = XyzD65ToOkLab(xyz.X, xyz.Y, xyz.Z);
+ var lch = RectToPolar(lab.L, lab.a, lab.b);
+ return new[] { lch.L, lch.C, lch.H };
+ }
+
+ default: // Srgb
+ return new[] { r, g, b };
+ }
+ }
+
+ // Reverse: color-space coordinates -> gamut-mapped sRGB (0..1)
+
+ public static (double R, double G, double B) FromSpace(double[] v, Space cs)
+ {
+ double r, g, b;
+
+ switch (cs)
+ {
+ case Space.SrgbLinear:
+ r = Delinearize(v[0]); g = Delinearize(v[1]); b = Delinearize(v[2]);
+ break;
+
+ case Space.DisplayP3:
+ {
+ var xyz = LinP3ToXyzD65(Linearize(v[0]), Linearize(v[1]), Linearize(v[2]));
+ var lin = XyzD65ToLinSrgb(xyz.X, xyz.Y, xyz.Z);
+ r = Delinearize(lin.R); g = Delinearize(lin.G); b = Delinearize(lin.B);
+ break;
+ }
+
+ case Space.Lab:
+ {
+ var xyz50 = LabToXyzD50(v[0], v[1], v[2]);
+ var xyz = XyzD50ToXyzD65(xyz50.X, xyz50.Y, xyz50.Z);
+ var lin = XyzD65ToLinSrgb(xyz.X, xyz.Y, xyz.Z);
+ r = Delinearize(lin.R); g = Delinearize(lin.G); b = Delinearize(lin.B);
+ break;
+ }
+
+ case Space.Oklab:
+ {
+ var xyz = OkLabToXyzD65(v[0], v[1], v[2]);
+ var lin = XyzD65ToLinSrgb(xyz.X, xyz.Y, xyz.Z);
+ r = Delinearize(lin.R); g = Delinearize(lin.G); b = Delinearize(lin.B);
+ break;
+ }
+
+ case Space.XyzD65:
+ {
+ var lin = XyzD65ToLinSrgb(v[0], v[1], v[2]);
+ r = Delinearize(lin.R); g = Delinearize(lin.G); b = Delinearize(lin.B);
+ break;
+ }
+
+ case Space.XyzD50:
+ {
+ var xyz = XyzD50ToXyzD65(v[0], v[1], v[2]);
+ var lin = XyzD65ToLinSrgb(xyz.X, xyz.Y, xyz.Z);
+ r = Delinearize(lin.R); g = Delinearize(lin.G); b = Delinearize(lin.B);
+ break;
+ }
+
+ case Space.Hsl:
+ {
+ var rgb = HslToSrgb(v[0], v[1], v[2]);
+ r = rgb.R; g = rgb.G; b = rgb.B;
+ break;
+ }
+
+ case Space.Hwb:
+ {
+ var rgb = HwbToSrgb(v[0], v[1], v[2]);
+ r = rgb.R; g = rgb.G; b = rgb.B;
+ break;
+ }
+
+ case Space.Lch:
+ {
+ var lab = PolarToRect(v[0], v[1], v[2]);
+ var xyz50 = LabToXyzD50(lab.L, lab.a, lab.b);
+ var xyz = XyzD50ToXyzD65(xyz50.X, xyz50.Y, xyz50.Z);
+ var lin = XyzD65ToLinSrgb(xyz.X, xyz.Y, xyz.Z);
+ r = Delinearize(lin.R); g = Delinearize(lin.G); b = Delinearize(lin.B);
+ break;
+ }
+
+ case Space.Oklch:
+ {
+ var lab = PolarToRect(v[0], v[1], v[2]);
+ var xyz = OkLabToXyzD65(lab.L, lab.a, lab.b);
+ var lin = XyzD65ToLinSrgb(xyz.X, xyz.Y, xyz.Z);
+ r = Delinearize(lin.R); g = Delinearize(lin.G); b = Delinearize(lin.B);
+ break;
+ }
+
+ default: // Srgb
+ r = v[0]; g = v[1]; b = v[2];
+ break;
+ }
+
+ return (Clamp01(r), Clamp01(g), Clamp01(b));
+ }
+ }
+}
From 945d60f50fa7a633f06894c89c591263c4d24df1 Mon Sep 17 00:00:00 2001
From: Justin Haygood
Date: Sat, 25 Jul 2026 00:14:09 -0400
Subject: [PATCH 07/21] CSS engine: sync @property and the calc()
time/resolution leaves
Adds the @property at-rule (CSS Properties & Values API Level 1) and the calc
node types its syntax validation needs, from PeachPDF v0.9.6 (upstream 94f8d94,
008b387, d86abc7, 020c23b, dd7a5ae).
- PropertyRule / IPropertyRule, the composer dispatch for `@property --x { }`,
and the syntax/initial-value/inherits descriptor table in PropertyFactory
(stored raw via UnknownProperty, since their grammar depends on the syntax
descriptor itself)
- TimeCalcNode / ResolutionCalcNode leaves, threaded through CalcParser,
CalcTypeChecker (new Time and Resolution categories), CalcEvaluator and
CalcSerializer, plus the previously missing AngleCalcNode evaluation arm
CalcContext keeps its fontAdjust flag and CalcSerializer keeps folding straight
to px, rather than taking upstream's points-based conversion: Length.ToPixels
resolves to CSS pixels here, so no px<->pt round trip belongs in either.
Co-Authored-By: Claude Opus 5
---
.../Core/CssEngine/Calc/CalcCategory.cs | 14 ++++-
.../Core/CssEngine/Calc/CalcEvaluator.cs | 17 ++++++
.../Core/CssEngine/Calc/CalcNode.cs | 24 ++++++++
.../Core/CssEngine/Calc/CalcParser.cs | 14 +++++
.../Core/CssEngine/Calc/CalcSerializer.cs | 11 ++++
.../Core/CssEngine/Calc/CalcTypeChecker.cs | 6 ++
.../CssEngine/Factories/PropertyFactory.cs | 18 ++++++
.../CssEngine/Parser/StylesheetComposer.cs | 24 ++++++++
.../Core/CssEngine/Rules/IPropertyRule.cs | 21 +++++++
.../Core/CssEngine/Rules/PropertyRule.cs | 57 +++++++++++++++++++
10 files changed, 205 insertions(+), 1 deletion(-)
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IPropertyRule.cs
create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
index 87bd4839f..ddf28cfd8 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
@@ -20,6 +20,18 @@ internal enum CalcCategory
/// convert via fixed constants), so a valid Angle-category expression always folds fully at
/// Layer A parse time - see CalcSerializer.
///
- Angle = 8
+ Angle = 8,
+
+ ///
+ /// <time> (s/ms). Like Angle, it resolves without layout context. Currently produced only by
+ /// @property syntax: "<time>" validation - no layout property consumes a time
+ /// calc(), so it never reaches CalcEvaluator during a render.
+ ///
+ Time = 16,
+
+ ///
+ /// <resolution> (dpi/dpcm/dppx). Context-free like Angle/Time; same @property-only provenance.
+ ///
+ Resolution = 32
}
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs
index 9e8ddb885..609602668 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs
@@ -61,6 +61,23 @@ internal static class CalcEvaluator
return new Length((float)percentage.Value, Length.Unit.Percent)
.ToPixels(context.EmFactor, context.RemFactor, context.HundredPercent, context.FontAdjust);
+ case AngleCalcNode angle:
+ // An angle leaf evaluates to radians (the canonical angle unit), so an angle-typed
+ // calc such as `calc(1turn * 0.35)` yields 0.35 turn in radians (a Number leaf stays a
+ // unitless scalar). Length calc()s never contain an angle leaf, so this never perturbs
+ // length evaluation; the only current caller is conic-gradient stop-position parsing.
+ return new Angle((float)angle.Value, angle.Unit).ToRadian();
+
+ case TimeCalcNode time:
+ // Canonical time unit is milliseconds. No layout property evaluates a time calc() today
+ // (these nodes only arise from @property `syntax: "