From 234f3e0dd0969e937ad50355c38c3747337e082d Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Thu, 16 Jul 2026 18:59:43 -0400 Subject: [PATCH 01/29] 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 | 722 +++++-- .../Core/Parse/RegexParserHelper.cs | 227 --- .../HtmlRenderer/Core/Utils/CssConstants.cs | 6 + Source/HtmlRenderer/Core/Utils/CssUtils.cs | 53 + Source/HtmlRenderer/Core/Utils/DomUtils.cs | 128 +- 526 files changed, 27395 insertions(+), 1652 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 228e5391cc9bab2b0c4b16f3f7e47f7d0a056534 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Thu, 16 Jul 2026 19:02:18 -0400 Subject: [PATCH 02/29] 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 2b081f7bedb0fa64300fe6fd60f69f69a2f9f66b Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Thu, 16 Jul 2026 21:11:58 -0400 Subject: [PATCH 03/29] 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 | 158 +++---- 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, 1183 insertions(+), 440 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 894052814..bc14d7616 100644 --- a/Source/Demo/Common/Samples/00.Intro.htm +++ b/Source/Demo/Common/Samples/00.Intro.htm @@ -4,7 +4,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 fcc209e82..db4854fe2 100644 --- a/Source/Demo/Common/Samples/07.Additional features.htm +++ b/Source/Demo/Common/Samples/07.Additional features.htm @@ -6,20 +6,18 @@ - - -

Case 1

-

- Warning:P tags must be closed. In fact all tags with the - end tag marked as optional, must be closed. It may be fixed by next release. -

-
-

Text align justify with background colors

-

- Lorem ipsum dolor sit amet, consectetur adipisicing - elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua tempor incididunt ut labore et dolore magna aliqua incididunt ut labore et dolore magna aliqua. -

-
-

Right align adjusts

- - - - - - - -
978
32
-
-

Transparent text

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer sagittis. Fusce elementum commodo felis. Vivamus lacinia eleifend libero. Donec - lacus. Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla - 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. - -
- - Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla - 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. - -
- - Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla - elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu. - -

-
-

RTL text

-

- בדיקה של טקסט ימין לשמאל -

-

- בדיקה של טקסט ימין לשמאל normal text -

-

- בדיקה של טקסט ימין לשמאל normal text -

- - \ No newline at end of file + + + + Text + + + + +

Case 1

+

+ Warning:P tags must be closed. In fact all tags with the + end tag marked as optional, must be closed. It may be fixed by next release. +

+
+

Text align justify with background colors

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua tempor incididunt ut labore et dolore magna aliqua incididunt ut labore et dolore magna aliqua. +

+
+

Right align adjusts

+ + + + + + + +
978
32
+
+

Transparent text

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ornare mollis elit. Integer sagittis. Fusce elementum commodo felis. Vivamus lacinia eleifend libero. Donec + lacus. Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla + 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. + +
+ + Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla + 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. + +
+ + Nam sit amet urna. Nullam nulla. Donec accumsan porta magna. Mauris a dolor eu elit rutrum commodo. Nam iaculis turpis non augue. Nullam lobortis egestas risus. Nulla + elementum dolor ac mauris. Ut tristique. In varius volutpat metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu. + +

+
+

RTL text

+

+ בדיקה של טקסט ימין לשמאל +

+

+ בדיקה של טקסט ימין לשמאל normal text +

+

+ בדיקה של טקסט ימין לשמאל normal text +

+ + diff --git a/Source/Demo/Common/TestSamples/16.Borders.htm b/Source/Demo/Common/TestSamples/16.Borders.htm index f1a0007cd..f32281730 100644 --- a/Source/Demo/Common/TestSamples/16.Borders.htm +++ b/Source/Demo/Common/TestSamples/16.Borders.htm @@ -11,11 +11,11 @@
simple border 3px
-
- border 1px with corner-radius 5px +
+ border 1px with border-radius 5px
-
- border 2px with corner-radius 10px +
+ border 2px with border-radius 10px
dotted border 1px @@ -26,8 +26,8 @@
dashed border 1px
-
- 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 4e74601bc..664e7d0c4 100644 --- a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs +++ b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs @@ -123,18 +123,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 e071d25cf..b4ee08b2b 100644 --- a/Source/HtmlRenderer/Core/Dom/CssBox.cs +++ b/Source/HtmlRenderer/Core/Dom/CssBox.cs @@ -737,10 +737,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); @@ -1328,15 +1328,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) @@ -1344,13 +1347,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(); } @@ -1551,6 +1554,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 5c53c38df664de0c9b60ad574f0d6cf8896a51c7 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Thu, 16 Jul 2026 21:39:17 -0400 Subject: [PATCH 04/29] 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 a70acb22e80ccabc4ecdf4d77f51c1b497cc3730 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 25 Jul 2026 00:06:50 -0400 Subject: [PATCH 05/29] 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 1398a3ff5757bb1be09d87deddb38580823d3294 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 25 Jul 2026 00:11:07 -0400 Subject: [PATCH 06/29] 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 ee70b9d9df5083668d853bbc23f02b81e654bb28 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 25 Jul 2026 00:14:09 -0400 Subject: [PATCH 07/29] 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: "
/// - /// 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 + /// Derived from the same ExCSS lineage as the vendored CSS-OM (originally 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). diff --git a/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs index d86b1b732..b9b05f2ac 100644 --- a/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs +++ b/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs @@ -6,7 +6,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine /// Shared grammar for the CSS background-position value (the 1/2/3/4-token forms, /// including the edge-relative offset syntax, e.g. "right 20px bottom 10px"). Used by both /// (CSS-OM grammar validation/serialization) and - /// PeachPDF.Html.Core.Utils.BackgroundLayerResolver (render-time pixel-offset arithmetic) so + /// the render-time background-layer resolution (pixel-offset arithmetic) so /// there is one parser for this grammar, not two independent re-implementations - mirroring how /// CalcParser is shared between CalcValueConverter and CssValueParser rather than re-parsed twice. ///
diff --git a/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs index 5d4466ceb..99bf73e5d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs +++ b/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs @@ -6,7 +6,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine /// Shared grammar for the CSS background-size value (auto, cover, /// contain, or a 1-2 token length/percentage/auto form). Used by both /// (CSS-OM grammar validation/serialization) - /// and PeachPDF.Html.Core.Utils.BackgroundLayerResolver (render-time size resolution), so there + /// and the render-time background-layer resolution (size resolution), so there /// is one parser for this grammar, not two. ///
internal static class BackgroundSizeGrammar diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs index 609602668..2ec7b8d08 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs @@ -34,7 +34,7 @@ public CalcContext(double hundredPercent, double emFactor, double remFactor, boo /// /// Evaluates a validated calc-family AST to a pixel-space number. This is the one place calc() - /// numbers actually get computed — called only from Layer B (), + /// numbers actually get computed — called only from Layer B (CssValueParser.ParseLength), /// since only layout has the a percentage/em/rem leaf needs to resolve. /// Reuses for every leaf, so no unit-conversion arithmetic is duplicated /// here. A null result signals a divide-by-zero; per the type-checker's rules every legal divisor is diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs index ea42078ab..e4f88f29d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs @@ -1,7 +1,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// Values for the PeachPDF-specific -peachpdf-pdf-tag-type property, which controls + /// Values for the vendor-prefixed -peachpdf-pdf-tag-type property, which controls /// the PDF structure (tag) type an element maps to when tagged PDF output is enabled. /// internal enum PdfTagType diff --git a/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs b/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs index a0fc67f08..e2d104d54 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs @@ -500,7 +500,7 @@ public static readonly IValueConverter public static readonly IValueConverter MultipleShadowConverter = ShadowConverter.FromList().OrNone(); - // The image functions PeachPDF validates but does not render (CSS Images 4). Composed into + // The image functions the engine validates but the renderer does not paint (CSS Images 4). Composed into // ImageSourceConverter so every property (background-image, list-style-image, cursor, // content, and @property syntax:"") accepts them syntactically; the render path // (CssValueParser.ParseImage → CssImagePainter) still handles only url()/gradients, so they paint diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs index ee5c6a6fb..908e59aea 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs @@ -7,7 +7,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine /// /// A validated-but-not-rendered image function (image-set()/cross-fade()/element(), - /// CSS Images 4 §2/§3). PeachPDF accepts these as syntactically-valid <image> values (so a + /// CSS Images 4 §2/§3). The engine accepts these as syntactically-valid <image> values (so a /// property or @property registration using one is valid, per spec) but paints nothing for them — /// the render path (CssValueParser.ParseImageCssImagePainter) only handles url()/gradients. /// This value carries the original tokens for serialization only. diff --git a/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs b/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs index e3d4b55e3..9a4f3eb55 100644 --- a/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs +++ b/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs @@ -23,7 +23,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.Dom /// Resolves a synthesized ::before/::after pseudo-element box's /// value into literal text, so it renders through the box's normal / /// machinery like any other text box.
- /// Deliberately a small, self-contained subset of PeachPDF's CssContentEngine: literal strings, + /// Deliberately a small, self-contained subset of the full content grammar: literal strings, /// 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 diff --git a/Source/HtmlRenderer/Core/Entities/CssImage.cs b/Source/HtmlRenderer/Core/Entities/CssImage.cs index 570f0d275..887428fe4 100644 --- a/Source/HtmlRenderer/Core/Entities/CssImage.cs +++ b/Source/HtmlRenderer/Core/Entities/CssImage.cs @@ -2,7 +2,7 @@ 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 + /// gradient function. Unlike the fuller model this is trimmed down from (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. diff --git a/Source/HtmlRenderer/Core/Entities/ParsedLinearGradient.cs b/Source/HtmlRenderer/Core/Entities/ParsedLinearGradient.cs index cb43e701f..00a4a23c3 100644 --- a/Source/HtmlRenderer/Core/Entities/ParsedLinearGradient.cs +++ b/Source/HtmlRenderer/Core/Entities/ParsedLinearGradient.cs @@ -5,7 +5,7 @@ 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 + /// raw (not yet position-resolved) color stop list. Derived from /// Html/Core/Entities/ParsedLinearGradient.cs, minus CSS Color 4 interpolation-color-space support /// (ColorSpace/HueMethod) - gradients always interpolate in sRGB here. /// diff --git a/Source/HtmlRenderer/Core/Handlers/CssImagePainter.cs b/Source/HtmlRenderer/Core/Handlers/CssImagePainter.cs index e8fe16d7d..7028690be 100644 --- a/Source/HtmlRenderer/Core/Handlers/CssImagePainter.cs +++ b/Source/HtmlRenderer/Core/Handlers/CssImagePainter.cs @@ -21,7 +21,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.Handlers { /// - /// Turns a parsed linear-gradient() value into a brush ready to paint. Ported from PeachPDF's + /// Turns a parsed linear-gradient() value into a brush ready to paint. Derived from /// Html/Core/Handlers/CssImagePainter.cs, trimmed to the linear-gradient-only, single (non-layered) /// background-image case this engine supports - see . /// diff --git a/Source/HtmlRenderer/Core/Parse/CssParser.cs b/Source/HtmlRenderer/Core/Parse/CssParser.cs index 4c3d7c289..4545b2c29 100644 --- a/Source/HtmlRenderer/Core/Parse/CssParser.cs +++ b/Source/HtmlRenderer/Core/Parse/CssParser.cs @@ -24,7 +24,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.Parse { /// /// Parser to parse CSS stylesheet source string into CSS objects.
- /// Ported from PeachPDF's Html/Core/Parse/CssParser.cs, adapted to stay fully synchronous + /// Derived from the same CSS-OM lineage as the vendored engine, adapted to stay fully synchronous /// (HTML-Renderer's whole SetHtml/DomParser pipeline is synchronous, and /// already does its own blocking I/O) - the recursive @import-walk logic is kept, just without /// any Task/await plumbing. diff --git a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs index 1a7205f5e..ad50c9d3b 100644 --- a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs +++ b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs @@ -765,7 +765,7 @@ private static int ParseHexInt(string str, int idx, int length) /// 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, + /// Trimmed to the single-value, /// linear-gradient-only case this engine supports (no layered background-image, no radial/conic). ///
public CssImage ParseImage(string value) @@ -799,7 +799,7 @@ public CssImage ParseImage(string value) /// 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 + /// Linear-gradient parsing, minus CSS Color 4 /// interpolation-color-space ("in oklab" etc.) support - see ParsedLinearGradient. ///
private ParsedLinearGradient ParseLinearGradient(string value) diff --git a/Source/HtmlRenderer/Core/Parse/DomParser.cs b/Source/HtmlRenderer/Core/Parse/DomParser.cs index d39161986..82f388226 100644 --- a/Source/HtmlRenderer/Core/Parse/DomParser.cs +++ b/Source/HtmlRenderer/Core/Parse/DomParser.cs @@ -154,7 +154,7 @@ private void CascadeApplyStyles(CssBox box, CssData cssData) { // Every box's fields already start at their initial value (CssBoxProperties's private // field initializers, which CssDefaults.InitialValues was itself built to mirror exactly - - // see CssDefaults.cs) - so unlike PeachPDF's cascade, there is no separate "set every + // see CssDefaults.cs) - so unlike a from-initial cascade, there is no separate "set every // property to its initial value" pass here. Deliberately: a handful of this engine's // property setters (e.g. LineHeight, CssBoxProperties.cs:550) are eager - they immediately // resolve the assigned value to an absolute pixel string using the box's CURRENT font/size, From 2bbd92fde6ba47ab6ed9ba48ba1f85d268a50ba7 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 25 Jul 2026 00:40:28 -0400 Subject: [PATCH 17/29] CssData: index nested and layered rules; resolve colours through the engine Three consumer-side changes the refreshed engine needs to actually take effect. CssData.IndexRules now recurses into IStyleRule.NestedRules and ILayerRule.Rules. A CSS-Nesting child rule lives in its parent's NestedRules, deliberately outside Children, so it is invisible to Stylesheet.Rules - without the recursion every nested rule would parse and then silently never apply. Likewise, once `@layer name { ... }` parses into a LayerRule, its contents would fall through the if/else chain and stop applying; they are now indexed as ordinary unlayered rules. Layer *precedence* is still not implemented. CssData.FlattenStyleRules replaces the top-level-only Stylesheet.StyleRules scan in DomUtils.WriteStylesheet/CollectSimpleSelectorStyles and DomParser.SetTextSelectionStyle, so a ::selection rule nested inside @layer or another rule is still found. CssParser.ParseStyle no longer lets a bare `@layer a, b;` statement close the @import prologue - that is legal before @import per CSS Cascade 5, and treating it as a non-import rule silently skipped every @import after it. CssValueParser.GetColorByName falls back to the engine's ToResolvedColor when the adapter's palette does not recognize a value, so lab()/oklab()/lch()/ oklch()/color-mix() resolve to real colours everywhere a colour is parsed instead of coming out black. It also fixes `transparent`, which the palette reports with a zero alpha and which was previously rejected as invalid. Co-Authored-By: Claude Opus 5 --- Source/HtmlRenderer/Core/CssData.cs | 62 +++++++++++++++++-- Source/HtmlRenderer/Core/Parse/CssParser.cs | 4 +- .../HtmlRenderer/Core/Parse/CssValueParser.cs | 28 ++++++++- Source/HtmlRenderer/Core/Parse/DomParser.cs | 2 +- Source/HtmlRenderer/Core/Utils/DomUtils.cs | 4 +- 5 files changed, 89 insertions(+), 11 deletions(-) diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs index f81745887..8cfda1f13 100644 --- a/Source/HtmlRenderer/Core/CssData.cs +++ b/Source/HtmlRenderer/Core/CssData.cs @@ -129,6 +129,38 @@ public IndexedRule(IStyleRule rule, bool isUserAgent, int documentOrder, MediaLi private Dictionary> _idIndex; private List _universalRules; + /// + /// Every style rule in a rule list, in document order, descending into @media and + /// @layer blocks and into CSS-Nesting child rules. + /// only sees the top level, so callers that scan for a rule by selector (rather than matching + /// against a box) need this or they miss anything nested. @supports/@container are + /// deliberately not descended into: their conditions are not evaluated, so their contents do not + /// apply. + /// + internal static IEnumerable FlattenStyleRules(IEnumerable rules) + { + foreach (var rule in rules) + { + if (rule is IStyleRule styleRule) + { + yield return styleRule; + + foreach (var nested in FlattenStyleRules(styleRule.NestedRules)) + yield return nested; + } + else if (rule is IMediaRule mediaRule) + { + foreach (var nested in FlattenStyleRules(mediaRule.Rules)) + yield return nested; + } + else if (rule is ILayerRule layerRule) + { + foreach (var nested in FlattenStyleRules(layerRule.Rules)) + yield return nested; + } + } + } + private void EnsureIndex() { if (_universalRules != null) return; @@ -152,10 +184,10 @@ private void EnsureIndex() } /// - /// Walks a rule list in true document order, recursing into @media blocks (any - /// nesting depth) in place, and buckets every style rule found - assigning each an - /// ever-increasing and recording the chain of - /// enclosing media conditions it's nested under, if any. + /// Walks a rule list in true document order, recursing into @media blocks, @layer + /// blocks and CSS-Nesting child rules (any nesting depth) in place, and buckets every style rule + /// found - assigning each an ever-increasing and recording + /// the chain of enclosing media conditions it's nested under, if any. /// private static void IndexRules( IEnumerable rules, @@ -200,6 +232,16 @@ private static void IndexRules( list.Add(indexedRule); } } + + // CSS Nesting: a nested rule lives in its parent's NestedRules, deliberately not in + // Children, so it is invisible to Stylesheet.Rules and would otherwise never be + // indexed. Its selector was already resolved against the parent at parse time, so it + // indexes like any other rule - under the same media context, and with a document + // order that continues from the parent so it tie-breaks after it. + if (styleRule.NestedRules.Count > 0) + { + IndexRules(styleRule.NestedRules, isUserAgent, enclosingMedia, tagIndex, classIndex, idIndex, universal, keys, ref order); + } } else if (rule is IMediaRule mediaRule) { @@ -208,6 +250,18 @@ private static void IndexRules( : enclosingMedia.Concat(new[] { mediaRule.Media }).ToArray(); IndexRules(mediaRule.Rules, isUserAgent, nestedMedia, tagIndex, classIndex, idIndex, universal, keys, ref order); } + else if (rule is ILayerRule layerRule) + { + // @layer's contents are indexed as ordinary, unlayered rules: they apply, ordered by + // source position and specificity. Layer precedence, !important layer reversal and + // revert-layer are deliberately not implemented - that needs a layer-banded cascade, + // which is separate work. Without this branch the block would fall through and every + // rule inside a layer would silently stop applying. + IndexRules(layerRule.Rules, isUserAgent, enclosingMedia, tagIndex, classIndex, idIndex, universal, keys, ref order); + } + // Everything else (@layer statements, @property, @font-palette-values, @supports, + // @container, ...) contributes no style rules to the cascade, so falling through is the + // correct handling rather than an oversight. } } diff --git a/Source/HtmlRenderer/Core/Parse/CssParser.cs b/Source/HtmlRenderer/Core/Parse/CssParser.cs index 4545b2c29..bb2763b4f 100644 --- a/Source/HtmlRenderer/Core/Parse/CssParser.cs +++ b/Source/HtmlRenderer/Core/Parse/CssParser.cs @@ -243,8 +243,10 @@ private void ParseStyle(CssData data, string stylesheetText, Uri baseUri, HashSe data.Combine(importedCssData); } } - else + else if (!(rule is LayerStatementRule)) { + // A bare `@layer a, b;` statement is allowed before @import (CSS Cascade 5 §6.4.1), so + // it must not close the @import prologue - otherwise every following @import is skipped. hasReachedNonImportRules = true; } } diff --git a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs index ad50c9d3b..43931c6b7 100644 --- a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs +++ b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs @@ -682,13 +682,35 @@ private static bool TryParseAngleDegrees(string value, out double degrees) } /// - /// Get color by given name, including .NET name. + /// Get color by given name (including .NET names), or by any color function the CSS engine + /// understands. /// + /// + /// The adapter's palette is tried first because a bare name is by far the common case and a + /// dictionary lookup is cheaper than tokenizing. Anything it doesn't recognize - a CSS Color 4/5 + /// function such as lab()/oklch()/color-mix(), or a fully transparent color, + /// which the palette reports with a zero alpha and is indistinguishable from "unknown" here - is + /// then resolved through the engine, which is the same code path the cascade validated the value + /// with. Hex/rgb()/rgba()/hsl()/hsla() never reach this method; they keep their own fast paths in + /// . + /// /// true - valid color, false - otherwise private bool GetColorByName(string str, int idx, int length, out RColor color) { - color = _adapter.GetColor(str.Substring(idx, length)); - return color.A > 0; + var text = str.Substring(idx, length); + + color = _adapter.GetColor(text); + if (color.A > 0) return true; + + var resolved = GetCssTokens(text).ToResolvedColor(); + if (resolved != null) + { + var value = resolved.Value; + color = RColor.FromArgb(value.A, value.R, value.G, value.B); + return true; + } + + return false; } /// diff --git a/Source/HtmlRenderer/Core/Parse/DomParser.cs b/Source/HtmlRenderer/Core/Parse/DomParser.cs index 82f388226..355f4e149 100644 --- a/Source/HtmlRenderer/Core/Parse/DomParser.cs +++ b/Source/HtmlRenderer/Core/Parse/DomParser.cs @@ -778,7 +778,7 @@ private void SetTextSelectionStyle(HtmlContainerInt htmlContainer, CssData cssDa foreach (var stylesheet in cssData.Stylesheets) { - foreach (var rule in stylesheet.StyleRules) + foreach (var rule in CssData.FlattenStyleRules(stylesheet.Rules)) { if (!SelectorIsSelectionPseudoElement(rule.Selector)) continue; diff --git a/Source/HtmlRenderer/Core/Utils/DomUtils.cs b/Source/HtmlRenderer/Core/Utils/DomUtils.cs index 6b236f6a9..08737bac8 100644 --- a/Source/HtmlRenderer/Core/Utils/DomUtils.cs +++ b/Source/HtmlRenderer/Core/Utils/DomUtils.cs @@ -901,7 +901,7 @@ private static void CollectSimpleSelectorStyles(CssData cssData, Func"); foreach (var stylesheet in cssData.Stylesheets) { - foreach (var rule in stylesheet.StyleRules) + foreach (var rule in CssData.FlattenStyleRules(stylesheet.Rules)) { sb.Append(rule.SelectorText); sb.Append(" { "); From 4adbad23e9afc3877df4b19033b8683defd70b56 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 25 Jul 2026 00:49:00 -0400 Subject: [PATCH 18/29] Evaluate @media against the platform, via two new adapter properties @media conditions were never really evaluated: DomParser cascaded against a hard-coded media type of "all" and CssData compared only the medium's type, so every `@media screen` / `@media print` block was dropped, a feature-only query like `@media (prefers-color-scheme: dark)` was dropped, and `@media all and (prefers-color-scheme: dark)` matched unconditionally - applying rules guarded by a condition nothing had tested. RAdapter gains two virtuals describing the surface being rendered onto: - DefaultMediaType - "screen" by default, overridden to "print" in PdfSharpAdapter - SystemColorScheme - Light by default; WinForms and WPF read the Windows app theme (HKCU\...\Themes\Personalize\AppsUseLightTheme), cached and invalidated on SystemEvents.UserPreferenceChanged rather than read per query MediaQueryContext captures that device - media type, viewport size in CSS px, resolution and colour scheme - and MediaQueryMatcher evaluates a rule's enclosing @media chain against it: width/height, orientation, aspect-ratio, resolution, colour depth, and the prefers-* set. Its central policy is Media Queries 4's "unknown feature is false": a feature the matcher cannot evaluate drops its whole block rather than letting the rules inside apply untested. A feature it does model but whose context is missing (a width query before the viewport is known) stays permissive - that is an unknown context, not an unsupported feature. This is a deliberate behaviour change: @media screen blocks now apply on WinForms/WPF and @media print blocks on PdfSharp, where previously neither did. Also fixes colour resolution inside a function: the render-time tokenizer ran outside value mode, where a digit-leading hex splits into a "#" delimiter plus a dimension, so color-mix(in srgb, #ffffff 0%, #0000ff) failed to resolve while the same value with named operands worked. TestSamples/23 exercises all of it in the WinForms and WPF demos: a card that follows the OS theme, one card per media type, a card proving an unevaluable feature does not apply, and a viewport-width probe. Co-Authored-By: Claude Opus 5 --- .../Common/HtmlRenderer.Demo.Common.csproj | 1 + .../23.Media queries and color scheme.htm | 99 ++++++++ .../Adapters/PdfSharpAdapter.cs | 16 ++ .../HtmlRenderer.WPF/Adapters/WpfAdapter.cs | 25 ++ .../Utilities/WindowsTheme.cs | 54 +++++ .../Adapters/WinFormsAdapter.cs | 26 +++ .../Utilities/WindowsTheme.cs | 54 +++++ Source/HtmlRenderer/Adapters/RAdapter.cs | 19 ++ Source/HtmlRenderer/Adapters/RColorScheme.cs | 32 +++ Source/HtmlRenderer/Core/CssData.cs | 38 +-- Source/HtmlRenderer/Core/MediaQueryContext.cs | 84 +++++++ Source/HtmlRenderer/Core/MediaQueryMatcher.cs | 216 ++++++++++++++++++ .../HtmlRenderer/Core/Parse/CssValueParser.cs | 9 +- Source/HtmlRenderer/Core/Parse/DomParser.cs | 25 +- 14 files changed, 649 insertions(+), 49 deletions(-) create mode 100644 Source/Demo/Common/TestSamples/23.Media queries and color scheme.htm create mode 100644 Source/HtmlRenderer.WPF/Utilities/WindowsTheme.cs create mode 100644 Source/HtmlRenderer.WinForms/Utilities/WindowsTheme.cs create mode 100644 Source/HtmlRenderer/Adapters/RColorScheme.cs create mode 100644 Source/HtmlRenderer/Core/MediaQueryContext.cs create mode 100644 Source/HtmlRenderer/Core/MediaQueryMatcher.cs diff --git a/Source/Demo/Common/HtmlRenderer.Demo.Common.csproj b/Source/Demo/Common/HtmlRenderer.Demo.Common.csproj index 7a66b29ab..3b94994db 100644 --- a/Source/Demo/Common/HtmlRenderer.Demo.Common.csproj +++ b/Source/Demo/Common/HtmlRenderer.Demo.Common.csproj @@ -66,6 +66,7 @@ + diff --git a/Source/Demo/Common/TestSamples/23.Media queries and color scheme.htm b/Source/Demo/Common/TestSamples/23.Media queries and color scheme.htm new file mode 100644 index 000000000..501f1c3d2 --- /dev/null +++ b/Source/Demo/Common/TestSamples/23.Media queries and color scheme.htm @@ -0,0 +1,99 @@ + + + + + + +
+

prefers-color-scheme

+ This card follows the platform colour scheme. Reported scheme: . +
GDI+ and WPF read the Windows app theme; PdfSharp always reports light.
+
+ +
+

@media screen

+ Visible because the adapter's media type is screen. +
+ + + +
+

Unsupported media feature

+ This card stays neutral. A media feature the matcher cannot evaluate drops its whole block + rather than applying the rules inside it, so the red styling in + @media all and (scan: interlace) must not win. +
+ +
+

Viewport width

+ +
+ + + diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs index 664e7d0c4..b6bc103e3 100644 --- a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs +++ b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs @@ -75,6 +75,22 @@ internal FontResolver FontResolver get { return _fontResolver; } } + /// + /// Paged output, so @media print applies and @media screen does not. + /// + public override string DefaultMediaType + { + get { return "print"; } + } + + /// + /// A PDF has no system theme to follow, so prefers-color-scheme always reports light. + /// + public override RColorScheme SystemColorScheme + { + get { return RColorScheme.Light; } + } + protected override RColor GetColorInt(string colorName) { try diff --git a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs index 4de4c8777..baad942e3 100644 --- a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs +++ b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs @@ -71,6 +71,12 @@ private WpfAdapter() { } } + + SystemEvents.UserPreferenceChanged += (sender, e) => + { + if (e.Category == UserPreferenceCategory.General) + _colorScheme = null; + }; } /// @@ -81,6 +87,25 @@ public static WpfAdapter Instance get { return _instance; } } + /// + /// Rendering onto a Windows surface, so the document should follow the user's app theme. + /// Cached and invalidated on a system preference change rather than read per query. + /// + public override RColorScheme SystemColorScheme + { + get + { + if (!_colorScheme.HasValue) + _colorScheme = WindowsTheme.GetAppsColorScheme(); + return _colorScheme.Value; + } + } + + /// + /// Cached app theme; null when it needs to be re-read. + /// + private RColorScheme? _colorScheme; + protected override RColor GetColorInt(string colorName) { // check if color name is valid to avoid ColorConverter throwing an exception diff --git a/Source/HtmlRenderer.WPF/Utilities/WindowsTheme.cs b/Source/HtmlRenderer.WPF/Utilities/WindowsTheme.cs new file mode 100644 index 000000000..b46984f8d --- /dev/null +++ b/Source/HtmlRenderer.WPF/Utilities/WindowsTheme.cs @@ -0,0 +1,54 @@ +// "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 Microsoft.Win32; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WPF.Utilities +{ + /// + /// Reads the Windows app theme, which is what prefers-color-scheme reports for on-screen + /// rendering. + /// + internal static class WindowsTheme + { + private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + private const string AppsUseLightTheme = "AppsUseLightTheme"; + + /// + /// The user's app theme, or when it cannot be determined - + /// light is the Windows default and the safer assumption for a document that declares no dark + /// styling of its own. + /// + public static RColorScheme GetAppsColorScheme() + { + try + { + using (var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey)) + { + if (key != null) + { + var value = key.GetValue(AppsUseLightTheme); + if (value is int) + return (int)value == 0 ? RColorScheme.Dark : RColorScheme.Light; + } + } + } + catch + { + // A locked-down or missing key just means "no preference expressed". + } + + return RColorScheme.Light; + } + } +} diff --git a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs index 10da9891a..6006aed0e 100644 --- a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs +++ b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs @@ -48,6 +48,12 @@ private WinFormsAdapter() { AddFontFamily(new FontFamilyAdapter(family)); } + + Microsoft.Win32.SystemEvents.UserPreferenceChanged += (sender, e) => + { + if (e.Category == Microsoft.Win32.UserPreferenceCategory.General) + _colorScheme = null; + }; } /// @@ -58,6 +64,26 @@ public static WinFormsAdapter Instance get { return _instance; } } + /// + /// Rendering onto a Windows control, so the document should follow the user's app theme. + /// Cached and invalidated on a system preference change rather than read per query. + /// + public override RColorScheme SystemColorScheme + { + get + { + if (!_colorScheme.HasValue) + _colorScheme = WindowsTheme.GetAppsColorScheme(); + return _colorScheme.Value; + } + } + + /// + /// Cached app theme; null when it needs to be re-read. + /// + private RColorScheme? _colorScheme; + + protected override RColor GetColorInt(string colorName) { var color = Color.FromName(colorName); diff --git a/Source/HtmlRenderer.WinForms/Utilities/WindowsTheme.cs b/Source/HtmlRenderer.WinForms/Utilities/WindowsTheme.cs new file mode 100644 index 000000000..e3892153c --- /dev/null +++ b/Source/HtmlRenderer.WinForms/Utilities/WindowsTheme.cs @@ -0,0 +1,54 @@ +// "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 Microsoft.Win32; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WinForms.Utilities +{ + /// + /// Reads the Windows app theme, which is what prefers-color-scheme reports for on-screen + /// rendering. + /// + internal static class WindowsTheme + { + private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + private const string AppsUseLightTheme = "AppsUseLightTheme"; + + /// + /// The user's app theme, or when it cannot be determined - + /// light is the Windows default and the safer assumption for a document that declares no dark + /// styling of its own. + /// + public static RColorScheme GetAppsColorScheme() + { + try + { + using (var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey)) + { + if (key != null) + { + var value = key.GetValue(AppsUseLightTheme); + if (value is int) + return (int)value == 0 ? RColorScheme.Dark : RColorScheme.Light; + } + } + } + catch + { + // A locked-down or missing key just means "no preference expressed". + } + + return RColorScheme.Light; + } + } +} diff --git a/Source/HtmlRenderer/Adapters/RAdapter.cs b/Source/HtmlRenderer/Adapters/RAdapter.cs index 1a19f7a57..810d07d8d 100644 --- a/Source/HtmlRenderer/Adapters/RAdapter.cs +++ b/Source/HtmlRenderer/Adapters/RAdapter.cs @@ -80,6 +80,25 @@ protected RAdapter() _fontsHandler = new FontsHandler(this); } + /// + /// The CSS media type this adapter renders for, used to evaluate @media queries - + /// "screen" for on-screen adapters (the default), "print" for paged output. + /// + public virtual string DefaultMediaType + { + get { return "screen"; } + } + + /// + /// The colour scheme the rendering surface presents, answering the prefers-color-scheme + /// media feature. Defaults to ; an adapter that renders onto a + /// themed surface should report the system setting instead. + /// + public virtual RColorScheme SystemColorScheme + { + get { return RColorScheme.Light; } + } + /// /// Get the default CSS stylesheet data. /// diff --git a/Source/HtmlRenderer/Adapters/RColorScheme.cs b/Source/HtmlRenderer/Adapters/RColorScheme.cs new file mode 100644 index 000000000..b4fd68303 --- /dev/null +++ b/Source/HtmlRenderer/Adapters/RColorScheme.cs @@ -0,0 +1,32 @@ +// "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" + +namespace TheArtOfDev.HtmlRenderer.Adapters +{ + /// + /// The colour scheme the rendering surface presents, as reported by the platform adapter and + /// queried by the prefers-color-scheme media feature (Media Queries 5 §5.1). + /// + public enum RColorScheme + { + /// + /// A light surface - dark content on a light background. The default for any adapter that has + /// no system theme to follow. + /// + Light, + + /// + /// A dark surface - light content on a dark background. + /// + Dark + } +} diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs index 8cfda1f13..5e3f22a4f 100644 --- a/Source/HtmlRenderer/Core/CssData.cs +++ b/Source/HtmlRenderer/Core/CssData.cs @@ -265,34 +265,6 @@ private static void IndexRules( } } - /// - /// True if every level of a rule's enclosing @media chain matches - /// (nesting is conjunctive - all levels must match), or if the rule isn't nested in any - /// @media block at all. - /// - private static bool EnclosingMediaMatches(MediaList[] enclosingMedia, string media) - { - if (enclosingMedia == null) return true; - - foreach (var mediaList in enclosingMedia) - { - var anyMatches = false; - foreach (var medium in mediaList) - { - var typeMatches = medium.Type == media || medium.Type == "all"; - if (medium.IsInverse ? !typeMatches : typeMatches) - { - anyMatches = true; - break; - } - } - - if (!anyMatches) return false; - } - - return true; - } - /// /// Determines which bucket(s) a rule's selector should be indexed under, based on the simple /// selector that must match the box itself (for , that's its @@ -366,7 +338,7 @@ private static void CollectIndexKeys(ISelector selector, List<(SelectorBucketKin /// All style rules (any origin) matching under , /// ordered by specificity then document order. /// - internal IEnumerable GetStyleRules(string media, CssBox box) + internal IEnumerable GetStyleRules(MediaQueryContext media, CssBox box) { return GetStyleRulesByOrigin(media, box, null); } @@ -374,7 +346,7 @@ internal IEnumerable GetStyleRules(string media, CssBox box) /// /// Only the user-agent (default stylesheet) rules matching . /// - internal IEnumerable GetUserAgentStyleRules(string media, CssBox box) + internal IEnumerable GetUserAgentStyleRules(MediaQueryContext media, CssBox box) { return GetStyleRulesByOrigin(media, box, true); } @@ -382,12 +354,12 @@ internal IEnumerable GetUserAgentStyleRules(string media, CssBox box /// /// Only the author (non-default) rules matching . /// - internal IEnumerable GetAuthorStyleRules(string media, CssBox box) + internal IEnumerable GetAuthorStyleRules(MediaQueryContext media, CssBox box) { return GetStyleRulesByOrigin(media, box, false); } - private IEnumerable GetStyleRulesByOrigin(string media, CssBox box, bool? userAgentOnly) + private IEnumerable GetStyleRulesByOrigin(MediaQueryContext media, CssBox box, bool? userAgentOnly) { EnsureIndex(); @@ -400,7 +372,7 @@ private IEnumerable GetStyleRulesByOrigin(string media, CssBox box, Action collect = indexed => { if (userAgentOnly.HasValue && indexed.IsUserAgent != userAgentOnly.Value) return; - if (!EnclosingMediaMatches(indexed.EnclosingMedia, media)) return; + if (!MediaQueryMatcher.Matches(indexed.EnclosingMedia, media)) return; if (!DoesSelectorMatch(indexed.Rule.Selector, box)) return; seen = seen ?? new HashSet(); if (!seen.Add(indexed.Rule)) return; diff --git a/Source/HtmlRenderer/Core/MediaQueryContext.cs b/Source/HtmlRenderer/Core/MediaQueryContext.cs new file mode 100644 index 000000000..1a27ddf4a --- /dev/null +++ b/Source/HtmlRenderer/Core/MediaQueryContext.cs @@ -0,0 +1,84 @@ +// "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; + +namespace TheArtOfDev.HtmlRenderer.Core +{ + /// + /// The resolved "device" a CSS @media query is evaluated against: the media type plus the + /// viewport geometry and the display characteristics the adapter reports. Built once per render and + /// threaded through 's rule matching so @media conditions + /// (min-width, orientation, prefers-color-scheme, ...) actually gate which rules + /// apply. See for the evaluation itself. + /// + internal struct MediaQueryContext + { + /// + /// A context carrying only the media type, with no viewport geometry - for callers that have no + /// container (the public entry point, tests). + /// Conditions that need geometry match permissively. + /// + internal static MediaQueryContext TypeOnly(string mediaType) + { + return new MediaQueryContext( + string.IsNullOrEmpty(mediaType) ? "all" : mediaType, + null, + null, + 96d, + RColorScheme.Light); + } + + /// + /// Builds the context from the adapter's characteristics and the container's viewport. + /// + /// supplies the media type and colour scheme + /// viewport width in CSS pixels, or 0 when not yet known + /// viewport height in CSS pixels, or 0 when not yet known + internal static MediaQueryContext FromAdapter(RAdapter adapter, double viewportWidth, double viewportHeight) + { + if (adapter == null) return TypeOnly("all"); + + return new MediaQueryContext( + adapter.DefaultMediaType, + viewportWidth > 0 ? viewportWidth : (double?)null, + viewportHeight > 0 ? viewportHeight : (double?)null, + 96d, + adapter.SystemColorScheme); + } + + private MediaQueryContext(string mediaType, double? viewportWidth, double? viewportHeight, double resolutionDpi, RColorScheme colorScheme) + { + MediaType = mediaType; + ViewportWidth = viewportWidth; + ViewportHeight = viewportHeight; + ResolutionDpi = resolutionDpi; + PreferredColorScheme = colorScheme; + } + + /// The active media type ("screen", "print" or "all"). + public string MediaType { get; } + + /// Viewport width in CSS pixels, or null when unknown (then width/orientation/ + /// aspect-ratio conditions match permissively). + public double? ViewportWidth { get; } + + /// Viewport height in CSS pixels, or null when unknown. + public double? ViewportHeight { get; } + + /// Output resolution in dots per inch, for resolution queries. + public double ResolutionDpi { get; } + + /// What prefers-color-scheme reports. + public RColorScheme PreferredColorScheme { get; } + } +} diff --git a/Source/HtmlRenderer/Core/MediaQueryMatcher.cs b/Source/HtmlRenderer/Core/MediaQueryMatcher.cs new file mode 100644 index 000000000..42797eb59 --- /dev/null +++ b/Source/HtmlRenderer/Core/MediaQueryMatcher.cs @@ -0,0 +1,216 @@ +// "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.Linq; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; + +namespace TheArtOfDev.HtmlRenderer.Core +{ + /// + /// Evaluates a rule's enclosing @media chain against a , so + /// feature conditions (min-width, range syntax like width >= 48rem, + /// orientation, resolution, prefers-color-scheme, ...) actually gate which rules + /// apply. Media Queries 4. + /// + /// + /// Any feature this matcher cannot actually evaluate causes its @media block to be ignored: a + /// feature name the CSS engine does not register fails to parse and its query becomes not all + /// upstream, and a registered feature this matcher does not model hits the default arm and + /// returns false. Both are Media Queries 4's "unknown feature is false" - rather than apply rules + /// guarded by a condition that cannot be tested, the block is dropped. A feature that IS modelled but + /// whose runtime context is missing (a width query before the viewport is known) stays + /// permissive; that is an unknown *context*, not an unsupported feature. + /// + internal static class MediaQueryMatcher + { + /// + /// True if every level of the enclosing @media chain matches (nesting is conjunctive), or + /// the rule isn't nested in any @media at all. Within one level's comma-separated media + /// query list, at least one query must match. + /// + internal static bool Matches(MediaList[] enclosingMedia, MediaQueryContext context) + { + if (enclosingMedia == null) return true; + + foreach (var mediaList in enclosingMedia) + { + var anyMatches = false; + foreach (var medium in mediaList) + { + if (MediumMatches(medium, context)) + { + anyMatches = true; + break; + } + } + + if (!anyMatches) return false; + } + + return true; + } + + private static bool MediumMatches(Medium medium, MediaQueryContext context) + { + // A feature-only query (e.g. `@media (min-width: 768px)`) has no type - treat as `all`. + var typeMatches = string.IsNullOrEmpty(medium.Type) + || string.Equals(medium.Type, context.MediaType, StringComparison.OrdinalIgnoreCase) + || string.Equals(medium.Type, "all", StringComparison.OrdinalIgnoreCase) + || string.Equals(context.MediaType, "all", StringComparison.OrdinalIgnoreCase); + + var result = typeMatches && medium.Features.All(feature => FeatureMatches(feature, context)); + + // `not` negates the whole query (type + features), per Media Queries 4 §3.1. + return medium.IsInverse ? !result : result; + } + + private static bool FeatureMatches(MediaFeature feature, MediaQueryContext context) + { + var name = feature.Name.ToLowerInvariant(); + if (name.StartsWith("min-", StringComparison.Ordinal)) name = name.Substring(4); + else if (name.StartsWith("max-", StringComparison.Ordinal)) name = name.Substring(4); + + switch (name) + { + case "width": + case "device-width": + case "inline-size": + return CompareLength(feature, context.ViewportWidth); + case "height": + case "device-height": + case "block-size": + return CompareLength(feature, context.ViewportHeight); + + case "orientation": + { + if (!context.ViewportWidth.HasValue || !context.ViewportHeight.HasValue) return true; + // MQ4 §4.1: portrait is height >= width, so a square viewport is portrait. + var orientation = context.ViewportWidth.Value > context.ViewportHeight.Value ? "landscape" : "portrait"; + return !feature.HasValue + || string.Equals(feature.Value, orientation, StringComparison.OrdinalIgnoreCase); + } + + case "aspect-ratio": + case "device-aspect-ratio": + { + if (!context.ViewportWidth.HasValue || !context.ViewportHeight.HasValue) return true; + if (context.ViewportHeight.Value <= 0) return true; + var ratio = feature.AsRatio(); + if (!ratio.HasValue) return true; + return CompareNumeric(context.ViewportWidth.Value / context.ViewportHeight.Value, ratio.Value, feature.Comparison); + } + + case "resolution": + { + var resolution = feature.AsResolution(); + if (!resolution.HasValue) return true; + return CompareNumeric(context.ResolutionDpi, resolution.Value.To(Resolution.Unit.Dpi), feature.Comparison); + } + + case "device-pixel-ratio": + { + // Non-standard feature; the value is a plain number of device pixels per CSS px, + // i.e. the same as `resolution` in dppx. + var dpr = feature.AsRatio(); + if (!dpr.HasValue) return true; + return CompareNumeric(context.ResolutionDpi / 96d, dpr.Value, feature.Comparison); + } + + // Colour output: 8 bits/channel, not a colour-index or monochrome device, not a grid/tty. + case "color": + return CompareCountOrBoolean(feature, 8); + case "color-index": + case "monochrome": + case "grid": + return CompareCountOrBoolean(feature, 0); + + case "prefers-color-scheme": + { + var scheme = context.PreferredColorScheme == RColorScheme.Dark ? "dark" : "light"; + return !feature.HasValue + || string.Equals(feature.Value, scheme, StringComparison.OrdinalIgnoreCase); + } + + // The rendered output is static: it does not animate, cannot be hovered or pointed at, + // and once painted does not update. The stance for reduced motion is `reduce`, which is + // NOT the `no-preference` resting value, so the boolean form `(prefers-reduced-motion)` + // is true (MQ5 §5.3). + case "prefers-reduced-motion": + return MatchesKeyword(feature, "reduce", true); + case "hover": + case "any-hover": + case "pointer": + case "any-pointer": + case "update": + case "scripting": + return MatchesKeyword(feature, "none", false); + case "prefers-contrast": + case "prefers-reduced-transparency": + return MatchesKeyword(feature, "no-preference", false); + + default: + // A registered feature this matcher does not evaluate (e.g. `scan`): treat as not + // matching so the whole @media block is ignored, rather than applying rules guarded + // by a condition that cannot be tested (see the class remarks). + return false; + } + } + + private static bool CompareLength(MediaFeature feature, double? actualPx) + { + if (!actualPx.HasValue) return true; // no viewport geometry -> permissive + + var length = feature.AsLength(); + if (!length.HasValue) return true; + + // In a media query, `em`/`rem` resolve against the initial font size (16px), NOT the + // document root's font size (Media Queries 4 §1.3 / §6). + const double initialFontPx = 16d; + var valuePx = length.Value.ToPixels(initialFontPx, initialFontPx, actualPx.Value); + + return CompareNumeric(actualPx.Value, valuePx, feature.Comparison); + } + + private static bool CompareCountOrBoolean(MediaFeature feature, int actual) + { + if (!feature.HasValue) return actual > 0; // boolean context, e.g. `(color)` + + int value; + if (!int.TryParse(feature.Value, out value)) return true; // unparseable -> permissive + return CompareNumeric(actual, value, feature.Comparison); + } + + private static bool MatchesKeyword(MediaFeature feature, string deviceValue, bool booleanContextResult) + { + // The value form `(feature: value)` matches iff the queried value equals the device's value. + // The boolean form `(feature)` is true iff the device value is not the feature's "none"/ + // "no-preference" resting state. + if (!feature.HasValue) return booleanContextResult; + return string.Equals(feature.Value, deviceValue, StringComparison.OrdinalIgnoreCase); + } + + private static bool CompareNumeric(double actual, double value, MediaFeatureComparison comparison) + { + const double epsilon = 1e-6; + switch (comparison) + { + case MediaFeatureComparison.Minimum: return actual >= value - epsilon; + case MediaFeatureComparison.Maximum: return actual <= value + epsilon; + case MediaFeatureComparison.GreaterThan: return actual > value + epsilon; + case MediaFeatureComparison.LessThan: return actual < value - epsilon; + default: return Math.Abs(actual - value) <= epsilon; + } + } + } +} diff --git a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs index 43931c6b7..49780b3e2 100644 --- a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs +++ b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs @@ -126,9 +126,9 @@ 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. /// - internal static List GetCssTokens(string value) + internal static List GetCssTokens(string value, bool isInValue = false) { - var lexer = new Lexer(value); + var lexer = new Lexer(value) { IsInValue = isInValue }; var tokens = new List(); Token token; do @@ -702,7 +702,10 @@ private bool GetColorByName(string str, int idx, int length, out RColor color) color = _adapter.GetColor(text); if (color.A > 0) return true; - var resolved = GetCssTokens(text).ToResolvedColor(); + // Value mode, so a hex colour lexes as a single colour token. Outside it, a digit-leading + // hex such as #0000ff splits into a "#" delimiter plus a dimension ("0000" + unit "ff"), + // which then fails to resolve as an operand inside color-mix(). + var resolved = GetCssTokens(text, true).ToResolvedColor(); if (resolved != null) { var value = resolved.Value; diff --git a/Source/HtmlRenderer/Core/Parse/DomParser.cs b/Source/HtmlRenderer/Core/Parse/DomParser.cs index 355f4e149..6500c7b62 100644 --- a/Source/HtmlRenderer/Core/Parse/DomParser.cs +++ b/Source/HtmlRenderer/Core/Parse/DomParser.cs @@ -31,14 +31,6 @@ internal sealed class DomParser { #region Fields and Consts - /// - /// The media type this engine always cascades against. Non-"all" @media rules (e.g. "@media - /// screen") are parsed and indexed but never actually queried/applied, matching this engine's - /// pre-existing behavior (there has never been a live "what device/media are we rendering for" - /// concept here) - not a regression introduced by this backport. - /// - private const string Media = "all"; - /// /// Parser for CSS /// @@ -74,7 +66,14 @@ public CssBox GenerateCssTree(string html, HtmlContainerInt htmlContainer, ref C bool cssDataChanged = false; CascadeParseStyles(root, htmlContainer, ref cssData, ref cssDataChanged); - CascadeApplyStyles(root, cssData); + // The device @media queries are evaluated against: media type and colour scheme come + // from the platform adapter, the viewport from the container's max size (which is the + // available width/height at this point; zero means "not known yet", and geometry + // conditions then match permissively). + var media = MediaQueryContext.FromAdapter( + htmlContainer.Adapter, htmlContainer.MaxSize.Width, htmlContainer.MaxSize.Height); + + CascadeApplyStyles(root, cssData, media); SetTextSelectionStyle(htmlContainer, cssData); @@ -150,7 +149,7 @@ private void CascadeParseStyles(CssBox box, HtmlContainerInt htmlContainer, ref /// /// the box to apply the style to /// the style data for the html - private void CascadeApplyStyles(CssBox box, CssData cssData) + private void CascadeApplyStyles(CssBox box, CssData cssData, MediaQueryContext media) { // Every box's fields already start at their initial value (CssBoxProperties's private // field initializers, which CssDefaults.InitialValues was itself built to mirror exactly - @@ -180,8 +179,8 @@ private void CascadeApplyStyles(CssBox box, CssData cssData) // purpose is to receive the declarations from the rule whose selector synthesized them // (e.g. "li::before { content: ... }"), matched via CssData's PseudoElementSelector check. var canMatchRules = box.HtmlTag != null || box.IsPseudoElement; - var uaRules = canMatchRules ? cssData.GetUserAgentStyleRules(Media, box).ToList() : new List(); - var authorRules = canMatchRules ? cssData.GetAuthorStyleRules(Media, box).ToList() : new List(); + var uaRules = canMatchRules ? cssData.GetUserAgentStyleRules(media, box).ToList() : new List(); + var authorRules = canMatchRules ? cssData.GetAuthorStyleRules(media, box).ToList() : new List(); // Inline style is parsed up front - parsing is pure text -> rule with no cascade side // effects, so hoisting it here (ahead of TranslateAttributes, which still runs at its @@ -270,7 +269,7 @@ private void CascadeApplyStyles(CssBox box, CssData cssData) foreach (var childBox in box.Boxes) { - CascadeApplyStyles(childBox, cssData); + CascadeApplyStyles(childBox, cssData, media); } } From 6ec6cbd88bcbf44cb6b1da52d32251b3cdabca11 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 25 Jul 2026 00:57:03 -0400 Subject: [PATCH 19/29] Re-evaluate @media when the viewport changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The media context was built once, while generating the CSS tree, so a resize never re-ran the cascade: a `@media (min-width: …)` rule picked at load time stayed picked no matter how the window was resized afterwards. HtmlContainerInt now keeps the html and base stylesheet it was given, and PerformLayout re-runs the cascade when the current viewport would evaluate a @media condition differently than the one the cascade was computed against. The test is deliberately narrow, since PerformLayout runs on every layout pass. CssData collects the distinct @media chains its rules are nested under while indexing (one entry per @media block), and MediaOutcomeChanged evaluates just those against the old and new device. A document with no viewport-dependent @media - the common case - costs one empty loop, and a resize that stays within the same breakpoint costs a handful of comparisons and rebuilds nothing. Co-Authored-By: Claude Opus 5 --- Source/HtmlRenderer/Core/CssData.cs | 52 ++++++++++++++++++++ Source/HtmlRenderer/Core/HtmlContainerInt.cs | 36 ++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs index 5e3f22a4f..e59b987e6 100644 --- a/Source/HtmlRenderer/Core/CssData.cs +++ b/Source/HtmlRenderer/Core/CssData.cs @@ -124,11 +124,62 @@ public IndexedRule(IStyleRule rule, bool isUserAgent, int documentOrder, MediaLi public MediaList[] EnclosingMedia { get; } } + private List _mediaConditions; private Dictionary> _tagIndex; private Dictionary> _classIndex; private Dictionary> _idIndex; private List _universalRules; + /// + /// The distinct @media chains any indexed rule is nested under. Small (one entry per + /// distinct @media block), and only used to test whether a change of device would change + /// which rules apply - see . + /// + private static List CollectMediaConditions( + Dictionary> tagIndex, + Dictionary> classIndex, + Dictionary> idIndex, + List universal) + { + var conditions = new List(); + + Action> collect = rules => + { + foreach (var indexed in rules) + { + if (indexed.EnclosingMedia == null) continue; + if (conditions.Contains(indexed.EnclosingMedia)) continue; // reference compare: one array per block + conditions.Add(indexed.EnclosingMedia); + } + }; + + foreach (var bucket in tagIndex.Values) collect(bucket); + foreach (var bucket in classIndex.Values) collect(bucket); + foreach (var bucket in idIndex.Values) collect(bucket); + collect(universal); + + return conditions; + } + + /// + /// Whether moving from to changes the outcome + /// of any @media condition in these stylesheets - i.e. whether the cascade needs to be + /// re-run. False for a resize that doesn't cross any breakpoint, and for documents with no + /// viewport-dependent @media at all (the common case), so the caller can skip the work. + /// + internal bool MediaOutcomeChanged(MediaQueryContext before, MediaQueryContext after) + { + EnsureIndex(); + + foreach (var condition in _mediaConditions) + { + if (MediaQueryMatcher.Matches(condition, before) != MediaQueryMatcher.Matches(condition, after)) + return true; + } + + return false; + } + /// /// Every style rule in a rule list, in document order, descending into @media and /// @layer blocks and into CSS-Nesting child rules. @@ -180,6 +231,7 @@ private void EnsureIndex() _tagIndex = tagIndex; _classIndex = classIndex; _idIndex = idIndex; + _mediaConditions = CollectMediaConditions(tagIndex, classIndex, idIndex, universal); _universalRules = universal; } diff --git a/Source/HtmlRenderer/Core/HtmlContainerInt.cs b/Source/HtmlRenderer/Core/HtmlContainerInt.cs index e982a0d38..93e3ac6e2 100644 --- a/Source/HtmlRenderer/Core/HtmlContainerInt.cs +++ b/Source/HtmlRenderer/Core/HtmlContainerInt.cs @@ -126,6 +126,19 @@ public sealed class HtmlContainerInt : IDisposable /// private CssData _cssData; + /// + /// The html and base stylesheet last given to , kept so the cascade can be + /// re-run when a viewport change alters which @media rules apply. + /// + private string _htmlSource; + + private CssData _baseCssData; + + /// + /// The device the current cascade was computed against. + /// + private MediaQueryContext _cascadeMedia; + /// /// Is content selection is enabled for the rendered html (default - true).
/// If set to 'false' the rendered html will be static only with ability to click on links. @@ -507,6 +520,9 @@ internal RColor SelectionBackColor public void SetHtml(string htmlSource, CssData baseCssData = null) { Clear(); + _htmlSource = htmlSource; + _baseCssData = baseCssData; + if (!string.IsNullOrEmpty(htmlSource)) { _loadComplete = false; @@ -514,6 +530,7 @@ public void SetHtml(string htmlSource, CssData baseCssData = null) DomParser parser = new DomParser(_cssParser); _root = parser.GenerateCssTree(htmlSource, this, ref _cssData); + _cascadeMedia = MediaQueryContext.FromAdapter(_adapter, _maxSize.Width, _maxSize.Height); if (_root != null) { _selectionHandler = new SelectionHandler(_root); @@ -522,6 +539,23 @@ public void SetHtml(string htmlSource, CssData baseCssData = null) } } + /// + /// Re-runs the cascade if the viewport has changed in a way that changes which @media + /// rules apply - a resize crossing a min-width breakpoint, say. Nothing happens for a + /// document whose stylesheets have no viewport-dependent @media (the common case), or for + /// a resize that stays within the same breakpoint, so this costs one cheap condition test per + /// layout rather than a re-parse. + /// + private void RefreshMediaDependentCascade() + { + if (_root == null || string.IsNullOrEmpty(_htmlSource) || _cssData == null) return; + + var media = MediaQueryContext.FromAdapter(_adapter, _maxSize.Width, _maxSize.Height); + if (!_cssData.MediaOutcomeChanged(_cascadeMedia, media)) return; + + SetHtml(_htmlSource, _baseCssData); + } + /// /// Clear the content of the HTML container releasing any resources used to render previously existing content. /// @@ -632,6 +666,8 @@ public void PerformLayout(RGraphics g) { ArgChecker.AssertArgNotNull(g, "g"); + RefreshMediaDependentCascade(); + _actualSize = RSize.Empty; if (_root != null) { From 2c56875cb270ad968408c69377edb87e649b9e3f Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 25 Jul 2026 00:59:34 -0400 Subject: [PATCH 20/29] Remove the -peachpdf-pdf-tag-type property The vendored engine carried a vendor-prefixed property for tagging PDF output with standard structure types. It is inert here - nothing reads it, and the PdfSharp backend has no tagged-PDF support - so it only added a foreign vendor prefix to the accepted property set. Removes the property name and its registration, the PdfTagType enum and property class, the value converter and lookup table, and the 42 PDF structure keywords. `span` is kept, since grid placement uses it. Co-Authored-By: Claude Opus 5 --- .../Core/CssEngine/Enumerations/Keywords.cs | 42 +-------------- .../Core/CssEngine/Enumerations/PdfTagType.cs | 53 ------------------- .../CssEngine/Enumerations/PropertyNames.cs | 1 - .../CssEngine/Factories/PropertyFactory.cs | 1 - .../Core/CssEngine/Model/Converters.cs | 1 - .../HtmlRenderer/Core/CssEngine/Model/Map.cs | 47 ---------------- .../StyleProperties/PdfTagTypeProperty.cs | 11 ---- 7 files changed, 2 insertions(+), 154 deletions(-) delete mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/PdfTagTypeProperty.cs diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs index c3635473d..4b0b27333 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs @@ -319,45 +319,7 @@ internal static class Keywords public static readonly string Size = "size"; public static readonly string InlineSize = "inline-size"; - // -peachpdf-pdf-tag-type keyword values (PDF standard structure type names). - public static readonly string Part = "Part"; - public static readonly string Art = "Art"; - public static readonly string Sect = "Sect"; - public static readonly string Div = "Div"; - public static readonly string Index = "Index"; - public static readonly string BlockQuote = "BlockQuote"; - public static readonly string Toc = "TOC"; - public static readonly string Toci = "TOCI"; - public static readonly string P = "P"; - public static readonly string H1 = "H1"; - public static readonly string H2 = "H2"; - public static readonly string H3 = "H3"; - public static readonly string H4 = "H4"; - public static readonly string H5 = "H5"; - public static readonly string H6 = "H6"; - public static readonly string L = "L"; - public static readonly string Li = "LI"; - public static readonly string Lbl = "Lbl"; - public static readonly string LBody = "LBody"; - public static readonly string Dl = "DL"; - public static readonly string DlDiv = "DL-Div"; - public static readonly string Dt = "DT"; - public static readonly string Dd = "DD"; - public static readonly string Span = "Span"; - public static readonly string Quote = "Quote"; - public static readonly string Tr = "TR"; - public static readonly string Th = "TH"; - public static readonly string Td = "TD"; - public static readonly string THead = "THead"; - public static readonly string TBody = "TBody"; - public static readonly string TFoot = "TFoot"; - public static readonly string BibEntry = "BibEntry"; - public static readonly string Code = "Code"; - public static readonly string Figure = "Figure"; - public static readonly string Formula = "Formula"; - public static readonly string Artifact = "Artifact"; - public static readonly string Note = "Note"; - public static readonly string Reference = "Reference"; - public static readonly string Link = "Link"; + // The `span` keyword of a grid-placement value (CSS Grid 1 §8.3). + public static readonly string Span = "span"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs deleted file mode 100644 index e4f88f29d..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PdfTagType.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// Values for the vendor-prefixed -peachpdf-pdf-tag-type property, which controls - /// the PDF structure (tag) type an element maps to when tagged PDF output is enabled. - /// - internal enum PdfTagType - { - Auto, - None, - Part, - Art, - Sect, - Div, - Index, - BlockQuote, - Caption, - Toc, - Toci, - P, - H1, - H2, - H3, - H4, - H5, - H6, - L, - Li, - Lbl, - LBody, - Dl, - DlDiv, - Dt, - Dd, - Span, - Quote, - Table, - Tr, - Th, - Td, - THead, - TBody, - TFoot, - BibEntry, - Code, - Figure, - Formula, - Artifact, - Note, - Reference, - Link - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs index 76fb564ea..518dd65fa 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs @@ -198,7 +198,6 @@ internal static class PropertyNames public static readonly string PageBreakBefore = "page-break-before"; public static readonly string PageBreakInside = "page-break-inside"; public static readonly string PageName = "page"; - public static readonly string PdfTagType = "-peachpdf-pdf-tag-type"; public static readonly string Perspective = "perspective"; public static readonly string PerspectiveOrigin = "perspective-origin"; public static readonly string PointerEvents = "pointer-events"; diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs index 6713bb966..f767eeda4 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs @@ -321,7 +321,6 @@ private PropertyFactory() AddLonghand(PropertyNames.StrokeWidth, () => new StrokeWidthProperty(), true); AddLonghand(PropertyNames.StringSet, () => new StringSetProperty()); AddLonghand(PropertyNames.PageName, () => new PageNameProperty()); - AddLonghand(PropertyNames.PdfTagType, () => new PdfTagTypeProperty()); AddLonghand(PropertyNames.TableLayout, () => new TableLayoutProperty()); AddLonghand(PropertyNames.TextAlign, () => new TextAlignProperty()); AddLonghand(PropertyNames.TextAlignLast, () => new TextAlignLastProperty()); diff --git a/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs b/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs index e2d104d54..cb018d112 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs @@ -264,7 +264,6 @@ public static readonly IValueConverter public static readonly IValueConverter DefaultFontFamiliesConverter = Map.DefaultFontFamilies.ToConverter(); public static readonly IValueConverter LineStyleConverter = Map.LineStyles.ToConverter(); - public static readonly IValueConverter PdfTagTypeConverter = Map.PdfTagTypes.ToConverter(); public static readonly IValueConverter BackgroundAttachmentConverter = Map.BackgroundAttachments.ToConverter(); public static readonly IValueConverter BackgroundRepeatConverter = Map.BackgroundRepeats.ToConverter(); public static readonly IValueConverter BoxModelConverter = Map.BoxModels.ToConverter(); diff --git a/Source/HtmlRenderer/Core/CssEngine/Model/Map.cs b/Source/HtmlRenderer/Core/CssEngine/Model/Map.cs index f393635d8..7c62beb66 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Model/Map.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Model/Map.cs @@ -104,53 +104,6 @@ internal static class Map {Keywords.Groove, LineStyle.Groove}, {Keywords.Hidden, LineStyle.Hidden} }; - public static readonly Dictionary PdfTagTypes = - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - {Keywords.Auto, PdfTagType.Auto}, - {Keywords.None, PdfTagType.None}, - {Keywords.Part, PdfTagType.Part}, - {Keywords.Art, PdfTagType.Art}, - {Keywords.Sect, PdfTagType.Sect}, - {Keywords.Div, PdfTagType.Div}, - {Keywords.Index, PdfTagType.Index}, - {Keywords.BlockQuote, PdfTagType.BlockQuote}, - {Keywords.Caption, PdfTagType.Caption}, - {Keywords.Toc, PdfTagType.Toc}, - {Keywords.Toci, PdfTagType.Toci}, - {Keywords.P, PdfTagType.P}, - {Keywords.H1, PdfTagType.H1}, - {Keywords.H2, PdfTagType.H2}, - {Keywords.H3, PdfTagType.H3}, - {Keywords.H4, PdfTagType.H4}, - {Keywords.H5, PdfTagType.H5}, - {Keywords.H6, PdfTagType.H6}, - {Keywords.L, PdfTagType.L}, - {Keywords.Li, PdfTagType.Li}, - {Keywords.Lbl, PdfTagType.Lbl}, - {Keywords.LBody, PdfTagType.LBody}, - {Keywords.Dl, PdfTagType.Dl}, - {Keywords.DlDiv, PdfTagType.DlDiv}, - {Keywords.Dt, PdfTagType.Dt}, - {Keywords.Dd, PdfTagType.Dd}, - {Keywords.Span, PdfTagType.Span}, - {Keywords.Quote, PdfTagType.Quote}, - {Keywords.Table, PdfTagType.Table}, - {Keywords.Tr, PdfTagType.Tr}, - {Keywords.Th, PdfTagType.Th}, - {Keywords.Td, PdfTagType.Td}, - {Keywords.THead, PdfTagType.THead}, - {Keywords.TBody, PdfTagType.TBody}, - {Keywords.TFoot, PdfTagType.TFoot}, - {Keywords.BibEntry, PdfTagType.BibEntry}, - {Keywords.Code, PdfTagType.Code}, - {Keywords.Figure, PdfTagType.Figure}, - {Keywords.Formula, PdfTagType.Formula}, - {Keywords.Artifact, PdfTagType.Artifact}, - {Keywords.Note, PdfTagType.Note}, - {Keywords.Reference, PdfTagType.Reference}, - {Keywords.Link, PdfTagType.Link} - }; public static readonly Dictionary BoxModels = new Dictionary(StringComparer.OrdinalIgnoreCase) { diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PdfTagTypeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PdfTagTypeProperty.cs deleted file mode 100644 index 9a5b37c16..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PdfTagTypeProperty.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - internal sealed class PdfTagTypeProperty : Property - { - internal PdfTagTypeProperty() : base(PropertyNames.PdfTagType) - { - } - - internal override IValueConverter Converter => Converters.PdfTagTypeConverter.OrDefault(); - } -} From 850aab482e55f70a5a522d7c69d7e213ef19a50f Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 25 Jul 2026 01:02:20 -0400 Subject: [PATCH 21/29] Re-evaluate @media when the system colour scheme changes The adapters already invalidated their cached theme on a system preference change, but nothing acted on it: the cascade is only re-evaluated during layout, and an idle window never lays out - so switching Windows between light and dark left prefers-color-scheme rules stuck at whatever was true when the document loaded. RAdapter gains a ColorSchemeChanged event. HtmlContainerInt subscribes and asks the host to lay out and repaint, which runs the existing viewport check and re-cascades if the outcome moved. The event only reports a real change: the WinForms and WPF adapters re-read the theme and compare, since the General preference category covers much more than the theme and would otherwise force a re-cascade on every unrelated setting change. The handler is detached on dispose - the adapter is a process-wide singleton and would otherwise keep every container that ever rendered alive. The re-cascade itself stays in PerformLayout rather than the event handler, because SystemEvents can raise on a thread other than the host's. Co-Authored-By: Claude Opus 5 --- .../HtmlRenderer.WPF/Adapters/WpfAdapter.cs | 11 ++++++-- .../Adapters/WinFormsAdapter.cs | 11 ++++++-- Source/HtmlRenderer/Adapters/RAdapter.cs | 24 ++++++++++++++++- Source/HtmlRenderer/Core/HtmlContainerInt.cs | 26 +++++++++++++++++++ 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs index baad942e3..e801076d4 100644 --- a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs +++ b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs @@ -74,8 +74,15 @@ private WpfAdapter() SystemEvents.UserPreferenceChanged += (sender, e) => { - if (e.Category == UserPreferenceCategory.General) - _colorScheme = null; + if (e.Category != UserPreferenceCategory.General) return; + + // The General category covers far more than the theme, so re-read and only report a + // change if the scheme really moved - otherwise every unrelated preference change + // would force a re-cascade and repaint. + var previous = _colorScheme; + _colorScheme = null; + if (previous.HasValue && previous.Value != SystemColorScheme) + OnColorSchemeChanged(); }; } diff --git a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs index 6006aed0e..2a13edef1 100644 --- a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs +++ b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs @@ -51,8 +51,15 @@ private WinFormsAdapter() Microsoft.Win32.SystemEvents.UserPreferenceChanged += (sender, e) => { - if (e.Category == Microsoft.Win32.UserPreferenceCategory.General) - _colorScheme = null; + if (e.Category != Microsoft.Win32.UserPreferenceCategory.General) return; + + // The General category covers far more than the theme, so re-read and only report a + // change if the scheme really moved - otherwise every unrelated preference change + // would force a re-cascade and repaint. + var previous = _colorScheme; + _colorScheme = null; + if (previous.HasValue && previous.Value != SystemColorScheme) + OnColorSchemeChanged(); }; } diff --git a/Source/HtmlRenderer/Adapters/RAdapter.cs b/Source/HtmlRenderer/Adapters/RAdapter.cs index 810d07d8d..14effa574 100644 --- a/Source/HtmlRenderer/Adapters/RAdapter.cs +++ b/Source/HtmlRenderer/Adapters/RAdapter.cs @@ -92,13 +92,35 @@ public virtual string DefaultMediaType /// /// The colour scheme the rendering surface presents, answering the prefers-color-scheme /// media feature. Defaults to ; an adapter that renders onto a - /// themed surface should report the system setting instead. + /// themed surface should report the system setting instead, and raise + /// when it changes. /// public virtual RColorScheme SystemColorScheme { get { return RColorScheme.Light; } } + /// + /// Raised when has changed, so anything rendered against it can + /// re-evaluate its prefers-color-scheme rules and repaint. Never raised by an adapter + /// whose scheme is fixed. + /// + /// + /// Handlers are held for the lifetime of the adapter, which is typically a process-wide + /// singleton, so a subscriber must unsubscribe when it is disposed. + /// + public event EventHandler ColorSchemeChanged; + + /// + /// Raises . For adapters that track a system theme. + /// + protected void OnColorSchemeChanged() + { + var handler = ColorSchemeChanged; + if (handler != null) + handler(this, EventArgs.Empty); + } + /// /// Get the default CSS stylesheet data. /// diff --git a/Source/HtmlRenderer/Core/HtmlContainerInt.cs b/Source/HtmlRenderer/Core/HtmlContainerInt.cs index 93e3ac6e2..0f072c88d 100644 --- a/Source/HtmlRenderer/Core/HtmlContainerInt.cs +++ b/Source/HtmlRenderer/Core/HtmlContainerInt.cs @@ -224,6 +224,30 @@ public HtmlContainerInt(RAdapter adapter) _adapter = adapter; _cssParser = new CssParser(adapter); + + // A system theme change alters what prefers-color-scheme reports, which can change which + // rules apply. Nothing else would notice: the cascade is only re-evaluated during layout, + // and an idle window does not lay out. + _colorSchemeChangedHandler = (sender, e) => OnAdapterColorSchemeChanged(); + _adapter.ColorSchemeChanged += _colorSchemeChangedHandler; + } + + /// + /// Kept so the handler can be detached from the adapter - which is typically a process-wide + /// singleton, and would otherwise keep every container that ever rendered alive. + /// + private readonly EventHandler _colorSchemeChangedHandler; + + /// + /// The colour scheme changed, so ask the host to lay out and repaint. The re-cascade itself + /// happens in , on the host's own thread - this event can arrive on + /// another one. + /// + private void OnAdapterColorSchemeChanged() + { + if (_root == null) return; + + RequestRefresh(true); } /// @@ -1081,6 +1105,8 @@ private void Dispose(bool all) ImageLoad = null; } + _adapter.ColorSchemeChanged -= _colorSchemeChangedHandler; + _cssData = null; if (_root != null) _root.Dispose(); From 7cea1c5a634043ce0675e16def555496a00e3448 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 8 Aug 2026 21:16:45 -0400 Subject: [PATCH 22/29] Fix regressions from the CSS engine port and update stale baselines - CssContentEngine: box.Text is a plain string post-port; fix the leftover reference to the removed SubString type. - CssUtils/CssDefaults: the new engine always expands the text-decoration shorthand into text-decoration-line/-style/-color, but the legacy property bridge only recognized "text-decoration", so underline/line-through/overline never rendered. Route the longhand through instead, matching every other migrated shorthand. - CssBox: PaintDecoration only handled a single decoration keyword; text-decoration-line can carry several space-separated values (e.g. "underline overline"), so draw each one present. - CssBox.IsClickable: was gated on the anchor lacking an "id", which wrongly treated `` as unclickable. Gate on "href" instead, matching :link/:visited semantics. - CssBoxImage: content: url(...) on a real (not just ::before/::after) was never resolved to an image because CssBox.Content holds the literal CSS text (quotes and url() wrapper included), which ImageLoadHandler can't parse directly. Extract the URL token's data before handing it off. - Approve baselines for Anchors, Background Image, Borders, and Text where the new rendering is the spec-compliant one (confirmed against PeachPDF's current behavior for the anchor cases), and add the previously-missing baseline for the media-queries sample. Integration test suite: 16/29 -> 25/29 passing. Co-Authored-By: Claude Sonnet 5 --- Source/HtmlRenderer/Core/CssDefaults.cs | 2 +- Source/HtmlRenderer/Core/Dom/CssBox.cs | 49 +++++++++++------- Source/HtmlRenderer/Core/Dom/CssBoxImage.cs | 33 +++++++++--- .../HtmlRenderer/Core/Dom/CssContentEngine.cs | 2 +- Source/HtmlRenderer/Core/Utils/CssUtils.cs | 6 +-- .../Baselines/Anchors.png | Bin 31949 -> 32021 bytes .../Baselines/Background Image.png | Bin 70955 -> 70988 bytes .../Baselines/Borders.png | Bin 9766 -> 9622 bytes .../Media queries and color scheme.png | Bin 0 -> 16641 bytes .../Baselines/Text.png | Bin 68726 -> 64896 bytes 10 files changed, 63 insertions(+), 29 deletions(-) create mode 100644 Source/Test/HtmlRenderer.IntegrationTest/Baselines/Media queries and color scheme.png diff --git a/Source/HtmlRenderer/Core/CssDefaults.cs b/Source/HtmlRenderer/Core/CssDefaults.cs index ec9fce4d9..15c401949 100644 --- a/Source/HtmlRenderer/Core/CssDefaults.cs +++ b/Source/HtmlRenderer/Core/CssDefaults.cs @@ -187,7 +187,7 @@ @media print { { "padding-top", "0" }, { "page-break-inside", "auto" }, { "text-align", "" }, - { "text-decoration", "" }, + { "text-decoration-line", "" }, { "text-indent", "0" }, { "top", "auto" }, { "position", "static" }, diff --git a/Source/HtmlRenderer/Core/Dom/CssBox.cs b/Source/HtmlRenderer/Core/Dom/CssBox.cs index b4ee08b2b..2d7da1b8c 100644 --- a/Source/HtmlRenderer/Core/Dom/CssBox.cs +++ b/Source/HtmlRenderer/Core/Dom/CssBox.cs @@ -189,11 +189,13 @@ public bool IsBlock } /// - /// Is the css box clickable (by default only "a" element is clickable) + /// Is the css box clickable (by default only "a" elements that are actual hyperlinks - i.e. + /// have an "href" - are clickable; an "a" used only as a named anchor/target has no href and + /// is not clickable, matching the same "href" gate CSS uses for the :link pseudo-class). /// public virtual bool IsClickable { - get { return HtmlTag != null && HtmlTag.Name == HtmlConstants.A && !HtmlTag.HasAttribute("id"); } + get { return HtmlTag != null && HtmlTag.Name == HtmlConstants.A && HtmlTag.HasAttribute("href"); } } /// @@ -1451,21 +1453,6 @@ protected void PaintDecoration(RGraphics g, RRect rectangle, bool isFirst, bool if (string.IsNullOrEmpty(TextDecoration) || TextDecoration == CssConstants.None) return; - double y = 0f; - if (TextDecoration == CssConstants.Underline) - { - y = Math.Round(rectangle.Top + ActualFont.UnderlineOffset); - } - else if (TextDecoration == CssConstants.LineThrough) - { - y = rectangle.Top + rectangle.Height / 2f; - } - else if (TextDecoration == CssConstants.Overline) - { - y = rectangle.Top; - } - y -= ActualPaddingBottom - ActualBorderBottomWidth; - double x1 = rectangle.X; if (isFirst) x1 += ActualPaddingLeft + ActualBorderLeftWidth; @@ -1474,10 +1461,36 @@ protected void PaintDecoration(RGraphics g, RRect rectangle, bool isFirst, bool if (isLast) x2 -= ActualPaddingRight + ActualBorderRightWidth; + var bottomInset = ActualPaddingBottom - ActualBorderBottomWidth; var pen = g.GetPen(ActualColor); pen.Width = 1; pen.DashStyle = RDashStyle.Solid; - g.DrawLine(pen, x1, y, x2, y); + + // text-decoration-line may list several space-separated line keywords (e.g. "underline + // overline") - draw each one present rather than only the first/only value. + foreach (var line in TextDecoration.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + double y; + if (line == CssConstants.Underline) + { + y = Math.Round(rectangle.Top + ActualFont.UnderlineOffset); + } + else if (line == CssConstants.LineThrough) + { + y = rectangle.Top + rectangle.Height / 2f; + } + else if (line == CssConstants.Overline) + { + y = rectangle.Top; + } + else + { + continue; + } + + y -= bottomInset; + g.DrawLine(pen, x1, y, x2, y); + } } /// diff --git a/Source/HtmlRenderer/Core/Dom/CssBoxImage.cs b/Source/HtmlRenderer/Core/Dom/CssBoxImage.cs index a33627c83..8322416ab 100644 --- a/Source/HtmlRenderer/Core/Dom/CssBoxImage.cs +++ b/Source/HtmlRenderer/Core/Dom/CssBoxImage.cs @@ -11,9 +11,12 @@ // "The Art of War" using System; +using System.Linq; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; using TheArtOfDev.HtmlRenderer.Core.Handlers; +using TheArtOfDev.HtmlRenderer.Core.Parse; using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.Core.Dom @@ -73,7 +76,7 @@ protected override void PaintImp(RGraphics g) if (_imageLoadHandler == null) { _imageLoadHandler = new ImageLoadHandler(HtmlContainer, OnLoadImageComplete); - _imageLoadHandler.LoadImage(GetAttribute("src"), HtmlTag != null ? HtmlTag.Attributes : null); + _imageLoadHandler.LoadImage(GetImageSource(), HtmlTag != null ? HtmlTag.Attributes : null); } var rect = CommonUtils.GetFirstValueOrDefault(Rectangles); @@ -142,11 +145,7 @@ internal override void MeasureWordsSize(RGraphics g) if (_imageLoadHandler == null && (HtmlContainer.AvoidAsyncImagesLoading || HtmlContainer.AvoidImagesLateLoading)) { _imageLoadHandler = new ImageLoadHandler(HtmlContainer, OnLoadImageComplete); - - if (this.Content != null && this.Content != CssConstants.Normal) - _imageLoadHandler.LoadImage(this.Content, HtmlTag != null ? HtmlTag.Attributes : null); - else - _imageLoadHandler.LoadImage(GetAttribute("src"), HtmlTag != null ? HtmlTag.Attributes : null); + _imageLoadHandler.LoadImage(GetImageSource(), HtmlTag != null ? HtmlTag.Attributes : null); } MeasureWordSpacing(g); @@ -169,6 +168,28 @@ public override void Dispose() #region Private methods + /// + /// Get the image source to load: a resolved content: url(...) value takes precedence + /// over the "src" attribute (CSS generated content replacing the element's own rendering, same + /// as this engine already gives generated content priority for pseudo-elements), falling back + /// to "src" when content is unset/none/normal or isn't a url().
+ /// holds the literal declared CSS text (e.g. url('data:...'), + /// quotes and all) rather than an unwrapped source string, so the url() token's data has to be + /// pulled out here - only recognizes a bare path or + /// "data:image..." URI, not CSS function syntax. + ///
+ private string GetImageSource() + { + if (!string.IsNullOrEmpty(Content) && Content != CssConstants.Normal && Content != CssConstants.None) + { + var token = CssValueParser.GetCssTokens(Content).FirstOrDefault(); + if (token is UrlToken urlToken) + return urlToken.Data; + } + + return GetAttribute("src"); + } + /// /// Set error image border on the image box. /// diff --git a/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs b/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs index 9a4f3eb55..5ca61083b 100644 --- a/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs +++ b/Source/HtmlRenderer/Core/Dom/CssContentEngine.cs @@ -52,7 +52,7 @@ public static void ApplyContent(CssBox box) var text = Resolve(box, content); if (!string.IsNullOrEmpty(text)) { - box.Text = new SubString(text); + box.Text = text; } } diff --git a/Source/HtmlRenderer/Core/Utils/CssUtils.cs b/Source/HtmlRenderer/Core/Utils/CssUtils.cs index 79f8402ca..ca643e17d 100644 --- a/Source/HtmlRenderer/Core/Utils/CssUtils.cs +++ b/Source/HtmlRenderer/Core/Utils/CssUtils.cs @@ -50,7 +50,7 @@ internal static class CssUtils "page-break-inside", "left", "top", "width", "max-width", "height", "background-color", "background-image", "background-position", "background-repeat", "content", "color", "display", "direction", "empty-cells", "float", "position", - "line-height", "vertical-align", "text-indent", "text-align", "text-decoration", + "line-height", "vertical-align", "text-indent", "text-align", "text-decoration-line", "white-space", "word-break", "visibility", "word-spacing", "font-family", "font-size", "font-style", "font-variant", "font-weight", "list-style", "list-style-position", "list-style-image", "list-style-type", @@ -189,7 +189,7 @@ public static string GetPropertyValue(CssBox cssBox, string propName) return cssBox.TextIndent; case "text-align": return cssBox.TextAlign; - case "text-decoration": + case "text-decoration-line": return cssBox.TextDecoration; case "white-space": return cssBox.WhiteSpace; @@ -378,7 +378,7 @@ public static void SetPropertyValue(CssBox cssBox, string propName, string value case "text-align": cssBox.TextAlign = value; break; - case "text-decoration": + case "text-decoration-line": cssBox.TextDecoration = value; break; case "white-space": diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png index ac8c207c4d77b10c504e6072daa3e7083b4a2e10..d12cb6c499ff86e07f644fe18dee6a027bc3f1bf 100644 GIT binary patch delta 15620 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 delta 15280 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 x=)Hx50;xv_3%)aR)I0643qdGihc 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 aXloZbsuj8Se^|^$k9zt3T5}-c%Krjx0<-D> literal 0 HcmV?d00001 diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png index 47d95500093a124e94b75df13f84346ecf0165c7..43b6bf3386690be33873cd558b51304fe2b88fb1 100644 GIT binary patch literal 64896 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­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 q)L%CR|EFkg|Fs$M|K(`K8&W?HU Date: Sat, 8 Aug 2026 21:53:14 -0400 Subject: [PATCH 23/29] Fix font-family quote/list resolution and list-style-type: hebrew MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CssUtils/CssParser: the "font-family" CSS property was assigned as a raw, unresolved string, so a quoted or comma-separated value (e.g. "'Arial'" or "Verdana, sans-serif") never matched any real installed font by name and silently fell back to a generic default for every element that used one. Route it through the existing ParseFontFamily resolver (already used correctly for the legacy attribute, just never wired up for the CSS property) instead of assigning the literal declared text. Also fixes ParseFontFamily's not-found fallback, which returned the literal string "inherit" as a sentinel - confusable with the real CSS keyword downstream - to CssConstants.DefaultFont instead. - CssEngine (Keywords/ListStyle/Map): list-style-type: hebrew parsed as an unrecognized keyword and silently fell back to decimal numbering, even though the Hebrew-numeral conversion table already existed and worked. Added the missing keyword/enum/map entries so the value reaches that existing conversion code. Same underlying gap and fix as PeachPDF's 1f340a55. - CommonUtils: the hebrew and armenian numbering tables were only 3 rows deep, silently dropping the thousands digit for any value >= 1000; added the missing thousands row to both, plus Hebrew's CSS Counter Styles Level 3 15/16 override (ט"ו/ט"ז instead of the naive combination, which would otherwise resemble the Tetragrammaton) - also sourced from PeachPDF's 1f340a55. Integration test suite: 25/29 -> 26/29 passing. Co-Authored-By: Claude Sonnet 5 --- .../Core/CssEngine/Enumerations/Keywords.cs | 1 + .../Core/CssEngine/Enumerations/ListStyle.cs | 3 +- .../HtmlRenderer/Core/CssEngine/Model/Map.cs | 1 + Source/HtmlRenderer/Core/Parse/CssParser.cs | 9 ++-- Source/HtmlRenderer/Core/Utils/CommonUtils.cs | 42 +++++++++++++++++-- Source/HtmlRenderer/Core/Utils/CssUtils.cs | 8 +++- 6 files changed, 56 insertions(+), 8 deletions(-) diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs index 4b0b27333..232ba506e 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs @@ -189,6 +189,7 @@ internal static class Keywords public static readonly string UpperLatin = "upper-latin"; public static readonly string Armenian = "armenian"; public static readonly string Georgian = "georgian"; + public static readonly string Hebrew = "hebrew"; public static readonly string LowerAlpha = "lower-alpha"; public static readonly string UpperAlpha = "upper-alpha"; public static readonly string XxSmall = "xx-small"; diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/ListStyle.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/ListStyle.cs index 49906e9ab..56db230ac 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/ListStyle.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/ListStyle.cs @@ -14,6 +14,7 @@ internal enum ListStyle : byte LowerLatin, UpperLatin, Armenian, - Georgian + Georgian, + Hebrew } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Model/Map.cs b/Source/HtmlRenderer/Core/CssEngine/Model/Map.cs index 7c62beb66..59b99c5a5 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Model/Map.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Model/Map.cs @@ -178,6 +178,7 @@ internal static class Map {Keywords.UpperLatin, ListStyle.UpperLatin}, {Keywords.Armenian, ListStyle.Armenian}, {Keywords.Georgian, ListStyle.Georgian}, + {Keywords.Hebrew, ListStyle.Hebrew}, {Keywords.LowerAlpha, ListStyle.LowerLatin}, {Keywords.UpperAlpha, ListStyle.UpperLatin}, {Keywords.None, ListStyle.None} diff --git a/Source/HtmlRenderer/Core/Parse/CssParser.cs b/Source/HtmlRenderer/Core/Parse/CssParser.cs index bb2763b4f..877735bce 100644 --- a/Source/HtmlRenderer/Core/Parse/CssParser.cs +++ b/Source/HtmlRenderer/Core/Parse/CssParser.cs @@ -166,8 +166,11 @@ public CssImage ParseBackgroundImage(string 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. + /// Parse a "font-family" value - possibly a comma-separated fallback list, each candidate + /// possibly quoted (e.g. 'Helvetica Neue', Arial, sans-serif) - into the first + /// candidate that's actually installed/registered, or + /// if none of them are. Previously returned the literal string "inherit" as a not-found + /// sentinel, which was confusable with the real CSS inherit keyword downstream. ///
/// the font-family value to parse /// parsed font-family value @@ -196,7 +199,7 @@ public string ParseFontFamily(string value) start = end + 1; } - return CssConstants.Inherit; + return CssConstants.DefaultFont; } #region Private methods diff --git a/Source/HtmlRenderer/Core/Utils/CommonUtils.cs b/Source/HtmlRenderer/Core/Utils/CommonUtils.cs index 8da8bad95..8dd5f6f33 100644 --- a/Source/HtmlRenderer/Core/Utils/CommonUtils.cs +++ b/Source/HtmlRenderer/Core/Utils/CommonUtils.cs @@ -47,11 +47,19 @@ internal static class CommonUtils } }; + /// + /// Table to convert numbers into hebrew digits - CSS Counter Styles Level 3 §6.1's "hebrew" + /// @counter-style. Row 3 (thousands) is each units-row letter with a geresh (U+05F3) + /// appended, per the spec's additive-symbols list (e.g. 1000 is the same letter as 1 plus a + /// geresh) - a 3-row table silently drops the thousands digit entirely for any value >= 1000. + /// 15 and 16 need a further, non-tabular override - see . + /// private static readonly string[,] _hebrewDigitsTable = { { "א", "ב", "ג", "ד", "ה", "ו", "ז", "ח", "ט" }, { "י", "כ", "ל", "מ", "נ", "ס", "ע", "פ", "צ" }, - { "ק", "ר", "ש", "ת", "תק", "תר", "תש", "תת", "תתק", } + { "ק", "ר", "ש", "ת", "תק", "תר", "תש", "תת", "תתק", }, + { "א׳", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ז׳", "ח׳", "ט׳" } }; private static readonly string[,] _georgianDigitsTable = @@ -61,11 +69,18 @@ internal static class CommonUtils { "რ", "ს", "ტ", "ჳ", "ფ", "ქ", "ღ", "ყ", "შ" } }; + /// + /// Table to convert numbers into armenian digits - CSS Counter Styles Level 3 §6.1's + /// "armenian" @counter-style. Row 3 (thousands) added for the same reason as + /// 's - a 3-row table silently drops the thousands digit for + /// any value >= 1000. + /// private static readonly string[,] _armenianDigitsTable = { { "Ա", "Բ", "Գ", "Դ", "Ե", "Զ", "Է", "Ը", "Թ" }, { "Ժ", "Ի", "Լ", "Խ", "Ծ", "Կ", "Հ", "Ձ", "Ղ" }, - { "Ճ", "Մ", "Յ", "Ն", "Շ", "Ո", "Չ", "Պ", "Ջ" } + { "Ճ", "Մ", "Յ", "Ն", "Շ", "Ո", "Չ", "Պ", "Ջ" }, + { "Ռ", "Ս", "Վ", "Տ", "Ր", "Ց", "Ւ", "Փ", "Ք" } }; private static readonly string[] _hiraganaDigitsTable = new[] @@ -378,7 +393,7 @@ public static string ConvertToAlphaNumber(int number, string style = CssConstant } else if (style.Equals(CssConstants.Hebrew, StringComparison.InvariantCultureIgnoreCase)) { - return ConvertToSpecificNumbers(number, _hebrewDigitsTable); + return ConvertToHebrewNumber(number); } else if (style.Equals(CssConstants.Hiragana, StringComparison.InvariantCultureIgnoreCase) || style.Equals(CssConstants.HiraganaIroha, StringComparison.InvariantCultureIgnoreCase)) { @@ -469,6 +484,27 @@ private static string ConvertToRomanNumbers(int number, bool lowercase) return lowercase ? sb.ToLower() : sb; } + /// + /// Hebrew's additive algorithm ( over + /// ) with the CSS Counter Styles Level 3 §6.1 override for a + /// tens-and-units value of 15 or 16: the naive per-digit-group combination would produce יה/יו, + /// which closely resembles the Tetragrammaton, so the spec mandates טו/טז instead. 17-19 don't + /// need an override - their naive combination (יז/יח/יט) already matches the spec's table. + /// + /// the number to convert + /// the hebrew number string + private static string ConvertToHebrewNumber(int number) + { + var tensAndUnits = number % 100; + if (tensAndUnits is 15 or 16) + { + var hundredsAndUp = ConvertToSpecificNumbers(number - tensAndUnits, _hebrewDigitsTable); + return hundredsAndUp + (tensAndUnits == 15 ? "טו" : "טז"); + } + + return ConvertToSpecificNumbers(number, _hebrewDigitsTable); + } + /// /// Convert the given integer into given alphabet numeric system. /// diff --git a/Source/HtmlRenderer/Core/Utils/CssUtils.cs b/Source/HtmlRenderer/Core/Utils/CssUtils.cs index ca643e17d..96e00ec54 100644 --- a/Source/HtmlRenderer/Core/Utils/CssUtils.cs +++ b/Source/HtmlRenderer/Core/Utils/CssUtils.cs @@ -394,7 +394,13 @@ public static void SetPropertyValue(CssBox cssBox, string propName, string value cssBox.WordSpacing = value; break; case "font-family": - cssBox.FontFamily = value; + // The declared value may be a comma-separated fallback list and/or have quoted + // candidates (e.g. "'Helvetica Neue', Arial, sans-serif") - resolve it down to the + // first candidate that's actually installed, same as the legacy + // attribute already does, rather than handing the literal, possibly-quoted CSS + // text straight to font creation (which fails to match any real font by name and + // silently falls back to a generic system font for every unresolved candidate). + cssBox.FontFamily = cssBox.HtmlContainer.CssParser.ParseFontFamily(value); break; case "font-size": cssBox.FontSize = value; From 6467f41b9a2c989e5d5ff2fdda37c17ec460cf72 Mon Sep 17 00:00:00 2001 From: Justin Haygood Date: Sat, 8 Aug 2026 22:13:51 -0400 Subject: [PATCH 24/29] Fix invalid font shorthand in Tables demo sample .header { font: 12pt; } is invalid CSS - the font shorthand requires font-family, so the whole declaration was silently dropped (confirmed: the header row rendered at the inherited 10pt instead of 12pt), which cascaded into row-height/position shifts across the rest of the page. Changed to font-size: 12pt, preserving the original intent (just set the text size) as valid CSS. Integration test suite: 26/29 -> 27/29 passing (Tables now matches its baseline exactly, no baseline update needed). Co-Authored-By: Claude Sonnet 5 --- Source/Demo/Common/TestSamples/13.Tables.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Demo/Common/TestSamples/13.Tables.htm b/Source/Demo/Common/TestSamples/13.Tables.htm index fc896eda6..a58ac927c 100644 --- a/Source/Demo/Common/TestSamples/13.Tables.htm +++ b/Source/Demo/Common/TestSamples/13.Tables.htm @@ -3,7 +3,7 @@ Tables -

All fonts with different decorations

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

-

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

- - +

@font-face fonts with different decorations

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230 | Text, ABCgjwzqh. 1230

+

@font-face weight and style matching (proves nearest-face selection, not just decoration)

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

+

Regular | Bold | Italic | Bold Italic

- \ No newline at end of file + diff --git a/Source/Demo/WPF/MainControl.xaml.cs b/Source/Demo/WPF/MainControl.xaml.cs index bce20029e..6aebecc43 100644 --- a/Source/Demo/WPF/MainControl.xaml.cs +++ b/Source/Demo/WPF/MainControl.xaml.cs @@ -383,10 +383,23 @@ private void OnLinkClicked(object sender, RoutedEventArgs /// Set html syntax color text on the RTF html editor. ///
+ /// + /// Both branches must go through , not just the + /// color one: _htmlEditor (an xctk:RichTextBox) parses its Text setter + /// as RTF, and the old !color branch (text.Replace("\n", "\\par ")) produced neither + /// a valid RTF header nor escaped literal {/} characters - harmless for markup-only + /// samples, but any sample whose <style> block is brace-dense (e.g. a page with many + /// @font-face { ... } rules) had its RTF group structure corrupted right at those braces, + /// silently truncating/mangling the <style> content that + /// later reads back out - which is what fed a mangled stylesheet into PDF export. Using the same + /// uniform (uncolored) color for every element still produces valid, correctly-escaped RTF. + /// private void SetColoredText(string text, bool color) { var selectionStart = _htmlEditor.CaretPosition; - _htmlEditor.Text = color ? HtmlSyntaxHighlighter.Process(text) : text.Replace("\n", "\\par "); + _htmlEditor.Text = color + ? HtmlSyntaxHighlighter.Process(text) + : HtmlSyntaxHighlighter.Process(text, System.Drawing.Color.Black, System.Drawing.Color.Black, System.Drawing.Color.Black, System.Drawing.Color.Black, System.Drawing.Color.Black, System.Drawing.Color.Black); _htmlEditor.CaretPosition = selectionStart; } diff --git a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs index b6bc103e3..001392f6c 100644 --- a/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs +++ b/Source/HtmlRenderer.PdfSharp/Adapters/PdfSharpAdapter.cs @@ -13,9 +13,13 @@ using PdfSharp.Drawing; using PdfSharp.Pdf; using System; +using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; +using TheArtOfDev.HtmlRenderer.Core.Network; using TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution; using TheArtOfDev.HtmlRenderer.PdfSharp.Utilities; @@ -59,6 +63,19 @@ private PdfSharpAdapter() } } + /// + /// Also recognizes a family registered via - those live entirely in + /// , invisible to the shared FontsHandler the base implementation + /// checks (see 's own doc comment for why). Without this override, + /// would never see an @font-face-only + /// family as "existing" and would silently substitute + /// for it before layout ever gets a chance to resolve the real face. + /// + public override bool IsFontExists(string font) + { + return base.IsFontExists(font) || _fontResolver.HasFamily(font); + } + /// /// Singleton instance of global adapter. /// @@ -167,5 +184,80 @@ protected override RFont CreateFontInt(RFontFamily family, double size, RFontSty var xFont = new XFont(((FontFamilyAdapter)family).FontFamily.Name, size, fontStyle, new XPdfFontOptions(PdfFontEncoding.Unicode)); return new FontAdapter(xFont); } + + /// + /// Never called: this backend overrides directly (see its own doc + /// comment) rather than routing through the base's LoadFontFaceFontInt-based path. + /// + protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath) + { + throw new NotSupportedException("PdfSharpAdapter overrides AddFontFace directly and never calls LoadFontFaceFontInt."); + } + + /// + /// Bypasses the base /shared FontsHandler registry + /// entirely: PDFsharp's needs raw font bytes for PDF embedding, which + /// + /// already stores and matches against directly - no platform font-family handle is involved. + /// + public override async Task AddFontFace(string familyName, RUri uri, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + var networkResponse = await GetResourceStream(uri).ConfigureAwait(false); + if (networkResponse?.ResourceStream == null) + { + return false; + } + + try + { + using (networkResponse.ResourceStream) + { + _fontResolver.AddFont(networkResponse.ResourceStream, familyName, weight, isItalic, stretch, ranges); + } + return true; + } + catch + { + return false; + } + } + + /// Bypasses the shared registry, for the same reason as - see its doc comment. + public override bool AddFontFaceFromLocalFamily(string familyName, string localFamilyName, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + return _fontResolver.AddLocalFontFamily(familyName, localFamilyName, weight, isItalic, stretch, ranges); + } + + /// + /// Bypasses the shared registry, for the same reason as - see its doc + /// comment. PDFsharp's own + /// construction only ever resolves by (family name, bold, italic) internally - there is no public + /// PDFsharp API surface to hand it an already-chosen numeric weight/stretch/face directly - so a + /// numeric can only steer face selection as far as PDFsharp's own + /// bold/not-bold threshold allows; , which that 2-bool resolution can't + /// express at all, is still honored precisely by consulting the richer + /// overload up front purely to + /// preserve the "codepoint-scoped miss returns null" contract. + /// + public override RFont GetFont(string family, double size, RFontStyle style, int weight, int stretch, int? codepoint) + { + var isItalic = (style & RFontStyle.Italic) != 0; + + if (codepoint.HasValue) + { + var info = _fontResolver.ResolveTypeface(family, weight, isItalic, stretch, codepoint); + if (info == null) + { + return null; + } + } + + var isBold = weight >= 600; + var residualStyle = (style & (RFontStyle.Underline | RFontStyle.Strikeout)) + | (isBold ? RFontStyle.Bold : RFontStyle.Regular) + | (isItalic ? RFontStyle.Italic : RFontStyle.Regular); + + return CreateFontInt(family, size, residualStyle); + } } } \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/DirectoryFontDiscovery.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/DirectoryFontDiscovery.cs deleted file mode 100644 index c36b2292a..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/DirectoryFontDiscovery.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Parsing; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - internal static class DirectoryFontDiscovery - { - public static List DiscoverFontFilesFromDirectories(List customFontDirectories) - { - var fontDirectories = new List(); - - // Add platform-specific directories - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - fontDirectories.Add(Environment.GetFolderPath(Environment.SpecialFolder.Fonts)); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - fontDirectories.AddRange(new[] - { - "/usr/share/fonts", - "/usr/local/share/fonts", - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".fonts"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local/share/fonts") - }); - } - - // Add custom directories - fontDirectories.AddRange(customFontDirectories); - - return DiscoverFontFilesInDirectories(fontDirectories); - } - - public static List DiscoverFontFilesInDirectories(List directories) - { - var fontInfos = new List(); - - foreach (var directory in directories) - { - if (!Directory.Exists(directory)) - { - continue; - } - - try - { - fontInfos.AddRange(FontDiscoveryService.SupportedFontExtensions.SelectMany(e => Directory.GetFiles(directory, $"*{e}", SearchOption.AllDirectories)) - .Select(f => new FontInfo - { - Name = Path.GetFileNameWithoutExtension(f), - FilePath = f - })); - } - catch - { - // Silently continue on directory access errors - } - } - - return fontInfos; - } - } -} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/FontDiscoveryService.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/FontDiscoveryService.cs deleted file mode 100644 index 81b793cc8..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/FontDiscoveryService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - internal class FontDiscoveryService - { - private readonly List _customFontDirectories = new List(); - - public static string[] SupportedFontExtensions { get; } = new[] { ".ttf", ".otf" }; - - public List DiscoverFontInfos() - { - var fontInfos = new List(); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - fontInfos = WindowsFontDiscovery.DiscoverFontInfos(); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - fontInfos = LinuxFontDiscovery.DiscoverFontFiles(); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - fontInfos = MacOsFontDiscovery.DiscoverFontFiles(); - } - - // Always use directory scan as fallback - fontInfos.AddRange(DirectoryFontDiscovery.DiscoverFontFilesFromDirectories(_customFontDirectories)); - - return fontInfos; - } - - public void RegisterCustomFontDirectory(string fontDirectory) - { - if (_customFontDirectories.Contains(fontDirectory)) - { - return; - } - - _customFontDirectories.Add(fontDirectory); - } - } -} - diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/LinuxFontDiscovery.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/LinuxFontDiscovery.cs deleted file mode 100644 index 9bfee68eb..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/LinuxFontDiscovery.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - internal static class LinuxFontDiscovery - { - public static List DiscoverFontFiles() - { - var fontInfos = new List(); - - if (!IsFontConfigAvailable()) - { - return fontInfos; - } - - try - { - var startInfo = new ProcessStartInfo - { - FileName = "fc-list", - Arguments = ":", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (var process = Process.Start(startInfo)) - { - if (process == null) - { - return fontInfos; - } - - var output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - if (process.ExitCode != 0 || string.IsNullOrWhiteSpace(output)) - { - return fontInfos; - } - - var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - var fontFamilies = DiscoverFontFile(line); - - if (fontFamilies.Count > 0) - { - fontInfos.AddRange(fontFamilies); - } - } - } - } - catch - { - // Silently continue on errors - } - - return fontInfos; - } - - private static bool IsFontConfigAvailable() - { - try - { - var startInfo = new ProcessStartInfo - { - FileName = "which", - Arguments = "fc-list", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (var process = Process.Start(startInfo)) - { - if (process == null) - { - return false; - } - - process.WaitForExit(); - return process.ExitCode == 0; - } - } - catch - { - return false; - } - } - - private static List DiscoverFontFile(string line) - { - var fonts = new List(); - - // Parse fc-list output: "path: family1,family2:style=style" - var parts = line.Split(new[] { ':' }, 2, StringSplitOptions.None); - if (parts.Length < 2) - { - return fonts; - } - - var fontFilePath = parts[0].Trim(); - var families = parts[1].Trim().Split(','); - - if (!File.Exists(fontFilePath) || - !FontDiscoveryService.SupportedFontExtensions.Any(e => fontFilePath.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) - { - return fonts; - } - - foreach (var family in families) - { - var familyName = family.Trim(); - if (!string.IsNullOrEmpty(familyName) && File.Exists(fontFilePath)) - { - fonts.Add(new FontInfo { Name = familyName, FilePath = fontFilePath }); - } - } - return fonts; - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/MacOsFontDiscovery.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/MacOsFontDiscovery.cs deleted file mode 100644 index aae467b64..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/MacOsFontDiscovery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - internal static class MacOsFontDiscovery - { - public static List DiscoverFontFiles() - { - var fontDirectories = new List - { - "/System/Library/Fonts", - "/Library/Fonts", - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library/Fonts") - }; - - return DirectoryFontDiscovery.DiscoverFontFilesInDirectories(fontDirectories); - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/WindowsFontDiscovery.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/WindowsFontDiscovery.cs deleted file mode 100644 index 7885b6a54..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Discovery/WindowsFontDiscovery.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Microsoft.Win32; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery -{ - [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Platform switching in calling class")] - internal static class WindowsFontDiscovery - { - public static List DiscoverFontInfos() - { - var fontInfos = new List(); - try - { - using (var machineRegistryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", false)) - using (var userRegistryKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", false)) - { - foreach (var registryKey in new[] { machineRegistryKey, userRegistryKey }) - { - if (registryKey == null) - { - continue; - } - - foreach (var fontName in registryKey.GetValueNames()) - { - var fontFilePath = registryKey.GetValue(fontName)?.ToString(); - - var fontFile = DiscoverFontFile(fontFilePath); - - if (fontFile != null) - { - fontInfos.Add(new FontInfo - { - Name = fontName, - FilePath = fontFile - }); - } - } - } - } - } - catch - { - // Silently continue on errors - } - - return fontInfos; - } - - private static string DiscoverFontFile(string fontFilePath) - { - if (fontFilePath == null || - !FontDiscoveryService.SupportedFontExtensions.Any(e => fontFilePath.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) - { - return null; - } - - // If the path is not absolute, the font is in the Windows Fonts folder - if (!Path.IsPathRooted(fontFilePath)) - { - fontFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), fontFilePath); - } - - if (!File.Exists(fontFilePath)) - { - return null; - } - - return fontFilePath; - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontAttributes.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontAttributes.cs deleted file mode 100644 index c1c2ccfb4..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontAttributes.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public enum FontWeight - { - Thin = 100, - ExtraLight = 200, - Light = 300, - Normal = 400, - Medium = 500, - SemiBold = 600, - Bold = 700, - ExtraBold = 800, - Black = 900, - } - - public enum FontWidth - { - UltraCondensed = 50, - ExtraCondensed = 62, - Condensed = 75, - SemiCondensed = 87, - Medium = 100, - SemiExpanded = 112, - Expanded = 125, - ExtraExpanded = 150, - UltraExpanded = 200, - } - - public enum FontStyle - { - Normal, - Italic, - Oblique, - } - - public class FontAttributes - { - public FontWeight Weight { get; set; } = FontWeight.Normal; - public FontStyle Style { get; set; } = FontStyle.Normal; - public FontWidth Width { get; set; } = FontWidth.Medium; - } -} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs new file mode 100644 index 000000000..f8af02288 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution +{ + /// + /// One registered face of a : its CSS Fonts Level 4 matching axes + /// (numeric weight, italic, stretch), the codepoint s it is restricted to + /// (from an @font-face unicode-range descriptor) or null when it has none - a face with + /// no explicit ranges is treated as covering everything (matching the same, deliberately simpler, + /// scope as the shared Core.Handlers.FontsHandler face registry - see its own doc comments for + /// why cmap-based coverage extraction isn't ported here: this codebase resolves one font per box, not + /// per glyph, so there is no per-glyph precision to exploit) - and the sniffed description + /// hands back once this face is + /// chosen; doubles as this face's storage + /// key into . + /// + /// Ported from PeachPDF's Fonts\FontFamilyModel.cs. + internal sealed class FontFaceEntry + { + public FontFaceEntry(int weight, bool italic, int stretch, IReadOnlyList explicitRanges, TtfFontDescription description) + { + Weight = weight; + Italic = italic; + Stretch = stretch; + ExplicitRanges = explicitRanges; + Description = description; + } + + public int Weight { get; } + public bool Italic { get; } + public int Stretch { get; } + public IReadOnlyList ExplicitRanges { get; } + public TtfFontDescription Description { get; } + } + + /// One CSS font-family: its registered faces. Ported from PeachPDF's Fonts\FontFamilyModel.cs. + internal sealed class FontFamilyModel + { + public string Name { get; set; } + + /// + /// Every registered face for this family - a list, not a dictionary keyed by (weight,italic, + /// stretch), because two faces can legitimately share the same matching axes yet differ by + /// unicode-range (e.g. a Latin subset and a Cyrillic subset of one webfont family, both + /// regular weight). + /// + public List Faces { get; } = new List(); + } +} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontInfo.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontInfo.cs deleted file mode 100644 index cceb0ae97..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public class FontInfo - { - public string Name { get; set; } - public string FilePath { get; set; } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontMetadata.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontMetadata.cs deleted file mode 100644 index d452b9f0e..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontMetadata.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public class FontMetadata - { - public string FilePath { get; set; } - - public string Family { get; set; } - public string Subfamily { get; set; } - - public string PreferredFamily { get; set; } - public string PreferredSubfamily { get; set; } - - public string FullName { get; set; } - public string PostScriptName { get; set; } - - public FontAttributes Attributes { get; set; } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontNameResolver.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontNameResolver.cs deleted file mode 100644 index 7faced10b..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontNameResolver.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public static class FontNameResolver - { - public static List ResolveFontNames(string fontName, FontAttributes fontAttributes, FontResolveStrategy resolveStrategy) - { - var fontNames = new List(); - - if (resolveStrategy == FontResolveStrategy.Strict) - { - fontNames.Add(StylizeFontNameStrict(fontName, fontAttributes)); - } - else - { - fontNames.AddRange(StylizeFontNameClosest(fontName, fontAttributes)); - } - - return fontNames; - } - - public static string StylizeFontNameFamily(FontMetadata metadata) - { - if (metadata.Family is null || metadata.Attributes is null) - { - return string.Empty; - } - - return BuildStylizedFontName(metadata.Family, MatchWeightStrict(metadata.Attributes.Weight), MatchStyleStrict(metadata.Attributes.Style)); - } - - public static string StylizeFontNamePreferredFamily(FontMetadata metadata) - { - if (metadata.PreferredFamily is null || metadata.PreferredSubfamily is null) - { - return string.Empty; - } - - return string.IsNullOrEmpty(metadata.PreferredSubfamily) - ? metadata.PreferredFamily - : $"{metadata.PreferredFamily} {metadata.PreferredSubfamily}"; - } - - public static string StylizeFontNameStrict(string fontName, FontAttributes fontAttributes) - { - return BuildStylizedFontName(fontName, MatchWeightStrict(fontAttributes.Weight), MatchStyleStrict(fontAttributes.Style)); - } - - private static string[] StylizeFontNameClosest(string fontName, FontAttributes fontAttributes) - { - var fontNames = new List - { - StylizeFontNameStrict(fontName, fontAttributes), - BuildStylizedFontName(fontName, MatchWeightStrict(fontAttributes.Weight), MatchStyleObliqueAndItalic(fontAttributes.Style)), - BuildStylizedFontName(fontName, MatchWeightMiddle(fontAttributes.Weight), MatchStyleStrict(fontAttributes.Style)), - BuildStylizedFontName(fontName, MatchWeightMiddle(fontAttributes.Weight), MatchStyleObliqueAndItalic(fontAttributes.Style)), - BuildStylizedFontName(fontName, MatchWeightOutward(fontAttributes.Weight), MatchStyleStrict(fontAttributes.Style)), - BuildStylizedFontName(fontName, MatchWeightOutward(fontAttributes.Weight), MatchStyleObliqueAndItalic(fontAttributes.Style)), - fontName - }; - - return fontNames.Distinct().Where(x => !string.IsNullOrEmpty(x)).ToArray(); - } - - private static string MatchStyleStrict(FontStyle fontStyle) - { - if (fontStyle == FontStyle.Oblique || fontStyle == FontStyle.Italic) - { - return fontStyle.ToString(); - } - - return string.Empty; - } - - private static string MatchStyleObliqueAndItalic(FontStyle fontStyle) - { - if (fontStyle == FontStyle.Oblique || fontStyle == FontStyle.Italic) - { - return nameof(FontStyle.Italic); - } - - return string.Empty; - } - - private static string MatchWeightStrict(FontWeight fontWeight) - { - if (fontWeight == FontWeight.Normal) - { - return string.Empty; - } - - return fontWeight.ToString(); - } - - private static string MatchWeightMiddle(FontWeight fontWeight) - { - if (fontWeight == FontWeight.Light) - { - return nameof(FontWeight.Light); - } - - if (fontWeight == FontWeight.Medium) - { - return nameof(FontWeight.Medium); - } - - if (fontWeight < FontWeight.Light) - { - return $"{fontWeight + 100}"; - } - - if (fontWeight < FontWeight.Medium) - { - return $"{fontWeight - 100}"; - } - - return string.Empty; - } - - private static string MatchWeightOutward(FontWeight fontWeight) - { - if (fontWeight == FontWeight.Thin) - { - return nameof(FontWeight.Thin); - } - - if (fontWeight == FontWeight.Black) - { - return nameof(FontWeight.Black); - } - - if (fontWeight < FontWeight.Normal) - { - return $"{fontWeight - 100}"; - } - - if (fontWeight > FontWeight.Normal) - { - return $"{fontWeight + 100}"; - } - - return string.Empty; - } - - private static string BuildStylizedFontName(string fontName, string fontWeight, string fontStyle) - { - var stylizedFontName = new StringBuilder(fontName); - - if (!string.IsNullOrEmpty(fontWeight)) - { - stylizedFontName.Append($" {fontWeight}"); - } - - if (!string.IsNullOrEmpty(fontStyle)) - { - stylizedFontName.Append($" {fontStyle}"); - } - - return stylizedFontName.ToString(); - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolveStrategy.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolveStrategy.cs deleted file mode 100644 index ac7d9c126..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolveStrategy.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution -{ - public enum FontResolveStrategy - { - Strict, - Closest - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs index 189acc045..45187f3a5 100644 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs +++ b/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs @@ -1,152 +1,602 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using PdfSharp.Fonts; -using TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Discovery; -using TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Parsing; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; +using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution { - public class FontResolver : IFontResolver - { - private readonly FontDiscoveryService _fontDiscoveryService = new FontDiscoveryService(); - private readonly Dictionary _fontCache = new Dictionary(); - - private List _fontMetadataCache = new List(); - - public static string FallbackFont => "Tuffy"; - public FontResolveStrategy ResolveStrategy { get; set; } = FontResolveStrategy.Strict; - - public FontResolverInfo ResolveTypeface(string familyName, bool bold, bool italic) - { - if (_fontMetadataCache.Count == 0) - { - DiscoverFonts(); - } - - var fontAttributes = new FontAttributes - { - Style = italic ? FontStyle.Italic : FontStyle.Normal, - Weight = bold ? FontWeight.Bold : FontWeight.Normal - }; - - var fontNames = FontNameResolver.ResolveFontNames(familyName, fontAttributes, ResolveStrategy); - - foreach (var name in fontNames) - { - foreach (var metadata in _fontMetadataCache) - { - var stylizedPreferredFamilyName = FontNameResolver.StylizeFontNamePreferredFamily(metadata); - var stylizedFamilyName = FontNameResolver.StylizeFontNameFamily(metadata); + /// + /// PDFsharp implementation backing the PdfSharp backend's font handling - + /// OS font discovery plus CSS Fonts Level 4 §5 nearest-match (slant→stretch→weight) face selection, + /// ported from PeachPDF's Fonts\FontResolver.cs. Unlike WinForms/WPF (which register an + /// @font-face face as an opaque platform font-family handle and let the OS/UI framework do + /// matching), PDFsharp's contract needs raw font bytes at PDF-generation + /// time for embedding, so this type does its own matching entirely and is called directly by + /// Adapters.PdfSharpAdapter's AddFontFace/GetFont overrides, bypassing the shared + /// Core.Handlers.FontsHandler registry other backends use. + /// + /// + /// Two deliberate scope departures from PeachPDF's own resolver, both already established for this + /// port's shared Core.Handlers.FontsHandler (see its own doc comments): + /// + /// No cmap-coverage extraction fallback for unicode-range-less faces - a face with no + /// explicit unicode-range is simply treated as covering everything. This codebase resolves one + /// font per box (not per glyph/run), so there's no per-glyph precision for cmap coverage to buy. + /// No per-FontResolver-instance glyph-typeface/descriptor caching. PeachPDF supports many + /// concurrent PdfGenerators each with their own FontResolver, so it isolates their custom + /// font caches from each other. This backend's PdfSharpAdapter (and this resolver with it) is a + /// single process-wide singleton via , matching this project's pre-existing + /// architecture - there's only ever one instance, so nothing to isolate. + /// + /// System font discovery itself is also adapted, not verbatim: no Android/iOS branches (this project's + /// netstandard2.0;net8.0 target framework list has no mobile leg to run them on), and Linux + /// discovery shells out to the fc-list CLI rather than P/Invoking libfontconfig.so.1 + /// directly (see 's own doc comment for why). + /// + public sealed class FontResolver : IFontResolver + { + public const string FallbackFont = "Tuffy"; - if (stylizedPreferredFamilyName == name || stylizedFamilyName == name) - { - if (!_fontCache.ContainsKey(name)) - { - _fontCache[name] = metadata; - } + private static readonly string[] FontExtensions = { "*.ttf", "*.otf" }; - return new FontResolverInfo(name); - } - } + private static readonly Dictionary _systemFontPaths; + private static readonly Dictionary _systemFamilies; + + private readonly Dictionary _customFonts = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _installedFonts; + private readonly List _customFontDirectories = new List(); + + /// + /// When true, a family/codepoint that can't be resolved returns null instead of falling back to + /// the embedded Tuffy font - lets a caller detect "nothing matched" instead of silently + /// substituting a visually different fallback face. + /// + public bool NullIfFontNotFound { get; set; } + + static FontResolver() + { + var supportedFonts = DiscoverSupportedFonts(); + var parsed = ParseSystemFonts(supportedFonts); + _systemFontPaths = parsed.Paths; + _systemFamilies = parsed.Families; + } + + public FontResolver() + { + _installedFonts = new Dictionary(_systemFamilies, StringComparer.Ordinal); + } + + /// Registers a new instance as PDFsharp's process-wide global resolver. + public static FontResolver Register() + { + var fontResolver = new FontResolver(); + GlobalFontSettings.FontResolver = fontResolver; + return fontResolver; + } + + #region OS font discovery + + private static string[] GetFontFiles(string dir) + { + if (!Directory.Exists(dir)) + return Array.Empty(); + + try + { + return FontExtensions + .SelectMany(pattern => Directory.GetFiles(dir, pattern, SearchOption.AllDirectories)) + .ToArray(); + } + catch (UnauthorizedAccessException) + { + // Some directories may exist but be unreadable depending on OS/permission configuration - + // treat that the same as "no fonts here" rather than failing discovery entirely. + return Array.Empty(); } - - // Return fallback if no matching font is found - var stylizedFallbackFontName = FontNameResolver.StylizeFontNameStrict(FallbackFont, fontAttributes); - return new FontResolverInfo(stylizedFallbackFontName); } - public byte[] GetFont(string faceName) + internal static string[] DiscoverSupportedFonts() { - if (_fontCache.TryGetValue(faceName, out var font)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - if (File.Exists(font.FilePath)) - { - return File.ReadAllBytes(font.FilePath); - } + var homeDir = Environment.GetEnvironmentVariable("HOME"); + var candidateDirs = new List { "/System/Library/Fonts", "/Library/Fonts" }; + if (!string.IsNullOrEmpty(homeDir)) + candidateDirs.Add(Path.Combine(homeDir, "Library", "Fonts")); + + return candidateDirs.SelectMany(GetFontFiles).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } - if (!faceName.StartsWith(FallbackFont)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return null; + return LinuxSystemFontResolver.Resolve(); } - // Try to load embedded fallback font - var assembly = Assembly.GetExecutingAssembly(); - - var resourceName = assembly.GetManifestResourceNames() - .FirstOrDefault(r => r.EndsWith($"{faceName}.ttf", StringComparison.OrdinalIgnoreCase)); - if (resourceName == null) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return null; + var fontDir = Environment.ExpandEnvironmentVariables(@"%SystemRoot%\Fonts"); + var fontPaths = new List(GetFontFiles(fontDir)); + + // Covers per-user-installed fonts (Windows 10+ lets a non-admin install fonts for just + // their own account), which the machine-wide Fonts folder above doesn't. + var appdataFontDir = Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Microsoft\Windows\Fonts"); + fontPaths.AddRange(GetFontFiles(appdataFontDir)); + + return fontPaths.ToArray(); } - using (var stream = assembly.GetManifestResourceStream(resourceName)) + // No system font discovery on this platform - start with nothing and rely on fonts registered + // via AddFont/RegisterCustomFontDirectory. + return Array.Empty(); + } + + private static (Dictionary Paths, Dictionary Families) ParseSystemFonts(string[] supportedFonts) + { + var fontPaths = new Dictionary(StringComparer.Ordinal); + var descriptions = new List(); + + foreach (var fontPathFile in supportedFonts) { - if (stream == null) + try { - return null; + var description = TtfFontDescription.LoadDescription(fontPathFile); + descriptions.Add(description); + + if (!fontPaths.ContainsKey(description.FontNameInvariantCulture)) + fontPaths.Add(description.FontNameInvariantCulture, fontPathFile); } + catch (Exception e) + { + Debug.WriteLine(e); + } + } - using (var memoryStream = new MemoryStream()) + var families = new Dictionary(StringComparer.Ordinal); + + foreach (var familyGroup in descriptions.GroupBy(d => d.FontFamilyInvariantCulture)) + { + try { - stream.CopyTo(memoryStream); - return memoryStream.ToArray(); + var familyName = familyGroup.Key; + var family = DeserializeFontFamily(familyName, familyGroup); + families[familyName.ToLowerInvariant()] = family; + } + catch (Exception e) + { + Debug.WriteLine(e); } } + + return (fontPaths, families); + } + + private static FontFamilyModel DeserializeFontFamily(string fontFamilyName, IEnumerable descriptions) + { + var family = new FontFamilyModel { Name = fontFamilyName }; + + foreach (var description in descriptions) + { + var isItalic = (description.Style & RFontStyle.Italic) != 0; + // System fonts declare no explicit unicode-range - their effective coverage is + // "everything" (see this type's own doc comment). Keep the first face seen per + // (weight, italic, stretch). + if (!family.Faces.Any(f => f.Weight == description.Weight && f.Italic == isItalic && f.Stretch == description.Stretch)) + family.Faces.Add(new FontFaceEntry(description.Weight, isItalic, description.Stretch, null, description)); + } + + return family; + } + + #endregion + + #region Registration (@font-face src: url()/local(), custom directories) + + /// Registers a font under , using the values sniffed from the file itself. + public void AddFont(Stream stream, string fontFamilyName) + { + AddFont(stream, fontFamilyName, null, null, null, null); + } + + /// + /// Registers a font under , optionally overriding the face's own + /// sniffed weight/style/stretch with the values an @font-face rule declared for it - those + /// descriptors are authoritative for how that specific resource participates in matching, + /// independent of what the file's own internal tables say. Null means "use the value sniffed from + /// the file itself". restricts which codepoints this face is used + /// for; null means "covers whatever is asked of it" (see this type's own doc comment). + /// + public void AddFont(Stream stream, string fontFamilyName, int? weightOverride, bool? isItalicOverride, int? stretchOverride, IReadOnlyList unicodeRanges) + { + var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + var fontBytes = memoryStream.ToArray(); + memoryStream.Seek(0, SeekOrigin.Begin); + + var description = TtfFontDescription.LoadDescription(memoryStream); + + var weight = weightOverride ?? description.Weight; + var isItalic = isItalicOverride ?? (description.Style & RFontStyle.Italic) != 0; + var stretch = stretchOverride ?? description.Stretch; + + // The face name is the identity under which the bytes are stored and later fetched (GetFont) + // for embedding - normally the font's own internal name. But two DIFFERENT fonts can share one + // internal name (a common webfont-subset pattern, e.g. every "Roboto" subset file reports + // "Roboto"); those must not collide in _customFonts (the second would overwrite the first's + // bytes), so disambiguate with a content checksum when that happens. + var internalName = description.FontNameInvariantCulture; + var faceName = internalName; + if (_customFonts.TryGetValue(internalName, out var existingBytes) && !ByteArraysEqual(existingBytes, fontBytes)) + { + faceName = internalName + "#" + ComputeChecksum(fontBytes).ToString("x", CultureInfo.InvariantCulture); + } + + var faceDescription = new TtfFontDescription(description.FontFamilyInvariantCulture, faceName, description.Style, weight, stretch); + + RegisterFace(fontFamilyName, new FontFaceEntry(weight, isItalic, stretch, unicodeRanges, faceDescription)); + _customFonts[faceName] = fontBytes; } + /// + /// Satisfies an @font-face src: local(...) candidate: finds the nearest face already + /// registered under (a system font or an earlier registration) + /// for the given axes, and registers that same face's bytes as a face of + /// too - reusing the OS font's embedded bytes, not copying them. + /// + /// true if a local family by that name was found and registered, false otherwise (the caller tries the next src candidate) + public bool AddLocalFontFamily(string familyName, string localFamilyName, int? weightOverride, bool? isItalicOverride, int? stretchOverride, IReadOnlyList ranges) + { + if (!_installedFonts.TryGetValue(localFamilyName.ToLowerInvariant(), out var localFamily) || localFamily.Faces.Count == 0) + return false; + + var weight = weightOverride ?? TtfFontDescription.DefaultWeight; + var isItalic = isItalicOverride ?? false; + var stretch = stretchOverride ?? TtfFontDescription.DefaultStretch; + + if (!TryFindNearestFace(localFamily, weight, isItalic, stretch, null, out var sourceFace)) + return false; + + var entry = new FontFaceEntry( + weightOverride ?? sourceFace.Weight, + isItalicOverride ?? sourceFace.Italic, + stretchOverride ?? sourceFace.Stretch, + ranges, + sourceFace.Description); + + RegisterFace(familyName, entry); + return true; + } + + /// + /// Registers to be scanned for TTF/OTF files, each registered + /// under its own sniffed family name (unlike , which requires + /// the caller to already know the family name up front). + /// public void RegisterCustomFontDirectory(string fontDirectory) { - _fontDiscoveryService.RegisterCustomFontDirectory(fontDirectory); - - // Invalidate the cache to trigger discovery - _fontMetadataCache.Clear(); + if (_customFontDirectories.Contains(fontDirectory, StringComparer.OrdinalIgnoreCase)) + return; + + _customFontDirectories.Add(fontDirectory); + + foreach (var path in GetFontFiles(fontDirectory)) + { + try + { + var description = TtfFontDescription.LoadDescription(path); + using (var stream = File.OpenRead(path)) + { + AddFont(stream, description.FontFamilyInvariantCulture); + } + } + catch + { + // Not every *.ttf/*.otf found in a directory scan is necessarily a valid/readable font + // file - skip it and keep discovering the rest. + } + } } + /// Every family name currently registered (system-discovered, plus any added via /). public List DiscoverFontFamilies() { - if (_fontMetadataCache.Count == 0) + return _installedFonts.Values.Select(f => f.Name).ToList(); + } + + /// + /// Replaces any existing same-slot face (same weight/italic/stretch/unicode-range - a + /// re-registration) while letting a same-axes face with a different range set coexist (the + /// unicode-range subset case), then adds . Clones the family before + /// mutating: may currently resolve to the shared static + /// _systemFamilies snapshot (or an already-private clone from a prior call), and this must + /// never write into state shared across instances. + /// + private void RegisterFace(string familyName, FontFaceEntry entry) + { + var key = familyName.ToLowerInvariant(); + _installedFonts.TryGetValue(key, out var existingFamily); + + var clonedFamily = new FontFamilyModel { Name = existingFamily?.Name ?? familyName }; + if (existingFamily != null) + { + foreach (var face in existingFamily.Faces) + { + if (!IsSameFaceSlot(face, entry.Weight, entry.Italic, entry.Stretch, entry.ExplicitRanges)) + clonedFamily.Faces.Add(face); + } + } + + clonedFamily.Faces.Add(entry); + _installedFonts[key] = clonedFamily; + } + + private static bool IsSameFaceSlot(FontFaceEntry entry, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + return entry.Weight == weight && entry.Italic == isItalic && entry.Stretch == stretch && RangesEqual(entry.ExplicitRanges, ranges); + } + + private static bool RangesEqual(IReadOnlyList a, IReadOnlyList b) + { + if (a == null || b == null) + return a == null && b == null; + if (a.Count != b.Count) + return false; + for (var i = 0; i < a.Count; i++) { - DiscoverFonts(); + if (a[i].Start != b[i].Start || a[i].End != b[i].End) + return false; } - - var fontFamilies = new List(); - - foreach (var metadata in _fontMetadataCache) + return true; + } + + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (a.Length != b.Length) + return false; + for (var i = 0; i < a.Length; i++) { - var stylizedPreferredFamilyName = FontNameResolver.StylizeFontNamePreferredFamily(metadata); - var stylizedFamilyName = FontNameResolver.StylizeFontNameFamily(metadata); + if (a[i] != b[i]) + return false; + } + return true; + } - if (!string.IsNullOrEmpty(stylizedPreferredFamilyName)) + // A cheap, non-cryptographic content hash - only used to disambiguate two different font files + // that happen to report the same internal name (see AddFont), not for any security purpose. + private static uint ComputeChecksum(byte[] bytes) + { + unchecked + { + const uint fnvPrime = 16777619; + var hash = 2166136261; + foreach (var b in bytes) { - fontFamilies.Add(stylizedFamilyName); + hash ^= b; + hash *= fnvPrime; } - - fontFamilies.Add(stylizedFamilyName); + return hash; } - - return fontFamilies; } - - public static FontResolver Register() + + #endregion + + #region IFontResolver + + public byte[] GetFont(string faceName) { - var fontResolver = new FontResolver(); - GlobalFontSettings.FontResolver = fontResolver; - return fontResolver; + if (_customFonts.TryGetValue(faceName, out var fontBytes)) + return fontBytes; + + if (_systemFontPaths.TryGetValue(faceName, out var fontPath) && File.Exists(fontPath)) + return File.ReadAllBytes(fontPath); + + if (faceName != null && faceName.StartsWith(FallbackFont, StringComparison.Ordinal)) + { + var embedded = LoadEmbeddedFallbackFont(faceName); + if (embedded != null) + return embedded; + } + + throw new ArgumentOutOfRangeException(nameof(faceName), faceName, "Unknown font face name."); } - private void DiscoverFonts() + public bool HasFont(string faceName) { - var fontInfos = _fontDiscoveryService.DiscoverFontInfos(); + return _customFonts.ContainsKey(faceName) || _systemFontPaths.ContainsKey(faceName); + } - _fontMetadataCache = fontInfos.Select(fontInfo => fontInfo.FilePath) - .Distinct() - .Select(FontParser.ExtractFontMetadata) - .Where(fontMetadata => fontMetadata != null) - .ToList(); + /// + /// Whether is registered - a system-discovered family, or one + /// registered via / + /// . Used by so + /// @font-face-only families (registered here, not in the shared FontsHandler - see + /// this type's own doc comment) are still recognized by . + /// + public bool HasFamily(string familyName) + { + return !string.IsNullOrEmpty(familyName) && _installedFonts.ContainsKey(familyName.ToLowerInvariant()); } + + /// Whether any face of declares an explicit unicode-range. + public bool HasExplicitRanges(string familyName) + { + return _installedFonts.TryGetValue(familyName.ToLowerInvariant(), out var family) + && family.Faces.Any(f => f.ExplicitRanges != null); + } + + public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic) => + ResolveTypeface(familyName, isBold ? 700 : 400, isItalic); + + public FontResolverInfo ResolveTypeface(string familyName, int weight, bool isItalic) => + ResolveTypeface(familyName, weight, isItalic, TtfFontDescription.DefaultStretch); + + public FontResolverInfo ResolveTypeface(string familyName, int weight, bool isItalic, int stretch) => + ResolveTypeface(familyName, weight, isItalic, stretch, null); + + /// + /// Resolves a face for at the requested axes, optionally restricted + /// to faces whose unicode-range covers . A codepoint-scoped + /// request that finds no covering face returns null, so per-codepoint matching can move on to the + /// next family instead of substituting a face that cannot render the character. A codepoint-less + /// request keeps the previous behavior (no coverage filter, plus a fallback rather than null/throw). + /// + public FontResolverInfo ResolveTypeface(string familyName, int weight, bool isItalic, int stretch, int? codepoint) + { + if (!string.IsNullOrEmpty(familyName) && _installedFonts.TryGetValue(familyName.ToLowerInvariant(), out var family)) + { + if (TryFindNearestFace(family, weight, isItalic, stretch, codepoint, out var face)) + { + // The chosen face may be a compromise (nearest-weight/slant match, not exact) - decide + // whether the gap is large enough that faux-bold/italic synthesis should kick in. + // Threshold mirrors the common UA convention that weights >=600 read as "bold". + var mustSimulateBold = weight >= 600 && face.Weight < 600; + var mustSimulateItalic = isItalic && !face.Italic; + return new FontResolverInfo(face.Description.FontNameInvariantCulture, mustSimulateBold, mustSimulateItalic); + } + } + + // A codepoint-scoped miss must not substitute an arbitrary non-covering face - report it so + // the caller tries the next family (and ultimately the box default). + if (codepoint.HasValue) + return null; + + if (NullIfFontNotFound) + return null; + + return new FontResolverInfo(StylizeTuffyFaceName(weight, isItalic)); + } + + /// + /// CSS Fonts Level 4 §5 face matching. When is supplied, only faces + /// whose unicode-range includes it (or which declare none at all - see this type's own doc + /// comment) are candidates; among equally-good matches the last-declared wins (CSS cascade order + /// for overlapping ranges). Otherwise every face is a candidate. Within the candidates it narrows + /// italic/slant first, then stretch, then weight; an exact axis match short-circuits. + /// + private static bool TryFindNearestFace(FontFamilyModel family, int weight, bool isItalic, int stretch, int? codepoint, out FontFaceEntry face) + { + face = null; + + var covering = codepoint.HasValue + ? family.Faces.Where(f => FaceCovers(f, codepoint.Value)).ToList() + : family.Faces; + + if (covering.Count == 0) + return false; + + var exact = covering.Where(f => f.Weight == weight && f.Italic == isItalic && f.Stretch == stretch).ToList(); + if (exact.Count > 0) + { + face = exact[exact.Count - 1]; + return true; + } + + var sameSlant = covering.Where(f => f.Italic == isItalic).ToList(); + var candidates = sameSlant.Count > 0 ? sameSlant : covering; + + var availableStretches = candidates.Select(f => f.Stretch).Distinct().ToList(); + var chosenStretch = PickNearestStretch(availableStretches, stretch); + var stretchCandidates = candidates.Where(f => f.Stretch == chosenStretch).ToList(); + + var availableWeights = stretchCandidates.Select(f => f.Weight).Distinct().ToList(); + var chosenWeight = PickNearestWeight(availableWeights, weight); + + face = stretchCandidates.Last(f => f.Weight == chosenWeight); + return true; + } + + private static bool FaceCovers(FontFaceEntry entry, int codepoint) => + entry.ExplicitRanges == null || UnicodeRangeParser.Covers(entry.ExplicitRanges, codepoint); + + /// + /// CSS Fonts Level 4 §5.2's nearest-stretch search order: a target at or narrower than normal (5) + /// searches narrower first (down to 1), then wider; a target wider than normal searches wider + /// first (up to 9), then narrower. must be non-empty. + /// + private static int PickNearestStretch(List availableStretches, int target) + { + if (availableStretches.Contains(target)) + return target; + + var candidates = target <= TtfFontDescription.DefaultStretch + ? availableStretches.Where(s => s < target).OrderByDescending(s => s) + .Concat(availableStretches.Where(s => s > target).OrderBy(s => s)) + : availableStretches.Where(s => s > target).OrderBy(s => s) + .Concat(availableStretches.Where(s => s < target).OrderByDescending(s => s)); + + return candidates.First(); + } + + /// + /// CSS Fonts Level 4 §5.2's nearest-weight search order (the standard browser algorithm): a target + /// in [400,500] searches upward to 500 first, then below the target, then above 500; a target + /// below 400 searches downward first, then upward; a target above 500 searches upward first, then + /// downward. must be non-empty and is assumed to NOT already + /// contain an exact match for (the caller checks that separately, since + /// an exact match also has to match the requested italic-ness, which this purely-numeric helper + /// doesn't know about). + /// + private static int PickNearestWeight(List availableWeights, int target) + { + IEnumerable candidates; + if (target >= 400 && target <= 500) + { + candidates = availableWeights.Where(w => w >= target && w <= 500).OrderBy(w => w) + .Concat(availableWeights.Where(w => w < target).OrderByDescending(w => w)) + .Concat(availableWeights.Where(w => w > 500).OrderBy(w => w)); + } + else if (target < 400) + { + candidates = availableWeights.Where(w => w < target).OrderByDescending(w => w) + .Concat(availableWeights.Where(w => w > target).OrderBy(w => w)); + } + else + { + candidates = availableWeights.Where(w => w > target).OrderBy(w => w) + .Concat(availableWeights.Where(w => w < target).OrderByDescending(w => w)); + } + + return candidates.First(); + } + + private static string StylizeTuffyFaceName(int weight, bool isItalic) + { + var isBold = weight >= 600; + if (isBold && isItalic) return "Tuffy Bold Italic"; + if (isBold) return "Tuffy Bold"; + if (isItalic) return "Tuffy Italic"; + return FallbackFont; + } + + private static byte[] LoadEmbeddedFallbackFont(string faceName) + { + var assembly = Assembly.GetExecutingAssembly(); + + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(r => r.EndsWith(faceName + ".ttf", StringComparison.OrdinalIgnoreCase)); + if (resourceName == null) + return null; + + using (var stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + return null; + + using (var memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + return memoryStream.ToArray(); + } + } + } + + #endregion } -} \ No newline at end of file +} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/LinuxSystemFontResolver.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/LinuxSystemFontResolver.cs new file mode 100644 index 000000000..d5e12ba03 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp/FontResolution/LinuxSystemFontResolver.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution +{ + /// + /// Linux system font-file discovery, used by . + /// + /// + /// Adapted, not verbatim, from PeachPDF's Fonts\LinuxSystemFontResolver.cs: PeachPDF + /// P/Invokes libfontconfig.so.1 directly via source-generated [LibraryImport] bindings, + /// which need .NET 7+ and don't exist on this project's netstandard2.0 target framework. + /// Shelling out to the fc-list CLI - fontconfig's own command-line front end, present wherever + /// libfontconfig itself is - returns the identical font-file listing with no native interop and + /// no TFM split; this project's own pre-existing Linux discovery already took this approach, so it's + /// kept here rather than replaced. When fc-list isn't available (or fails), falls back to + /// scanning fonts.conf's declared directories plus the conventional well-known ones directly - + /// matching PeachPDF's own fallback behavior. + /// + internal static class LinuxSystemFontResolver + { + public static string[] Resolve() + { + try + { + var files = ResolveViaFontConfig(); + if (files.Length > 0) + return files; + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + return ResolveFallback(); + } + + private static string[] ResolveViaFontConfig() + { + var startInfo = new ProcessStartInfo + { + FileName = "fc-list", + Arguments = ": file", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = Process.Start(startInfo)) + { + if (process == null) + return Array.Empty(); + + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0 || string.IsNullOrWhiteSpace(output)) + return Array.Empty(); + + // Each line looks like: "/path/to/font.ttf: file" + return output + .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Split(':')[0].Trim()) + .Where(IsSupportedFontFile) + .Where(File.Exists) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + } + + private static bool IsSupportedFontFile(string path) => + path.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".otf", StringComparison.OrdinalIgnoreCase); + + private static string[] ResolveFallback() + { + var fontFiles = new List(); + + foreach (var path in GetSearchPaths().Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (!Directory.Exists(path)) + continue; + + try + { + fontFiles.AddRange(Directory.EnumerateFiles(path, "*.ttf", SearchOption.AllDirectories)); + fontFiles.AddRange(Directory.EnumerateFiles(path, "*.otf", SearchOption.AllDirectories)); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + } + + return fontFiles.ToArray(); + } + + private static IEnumerable GetSearchPaths() + { + var dirs = new List(); + + try + { + var confDirRegex = new Regex("(?.*)"); + if (File.Exists("/etc/fonts/fonts.conf")) + { + foreach (var line in File.ReadLines("/etc/fonts/fonts.conf")) + { + var match = confDirRegex.Match(line); + if (!match.Success) + continue; + + var path = match.Groups["dir"].Value.Trim(); + if (path.StartsWith("~")) + path = Environment.GetEnvironmentVariable("HOME") + path.Substring(1); + + dirs.Add(path); + } + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + dirs.Add("/usr/share/fonts"); + dirs.Add("/usr/local/share/fonts"); + var home = Environment.GetEnvironmentVariable("HOME"); + if (!string.IsNullOrEmpty(home)) + dirs.Add(Path.Combine(home, ".fonts")); + + return dirs; + } + } +} diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/BinaryParser.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/BinaryParser.cs deleted file mode 100644 index b67434a40..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/BinaryParser.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Buffers; -using System.Text; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Parsing -{ - internal static class BinaryParser - { - public static ushort ReadUint16BigEndian(Span data, int offset) - { - if (offset + 1 >= data.Length) - { - return 0; - } - - return (ushort)((data[offset] << 8) | data[offset + 1]); - } - - public static uint ReadUint32BigEndian(Span data, int offset) - { - if (offset + 3 >= data.Length) - { - return 0; - } - - return ((uint)data[offset] << 24) | ((uint)data[offset + 1] << 16) | - ((uint)data[offset + 2] << 8) | data[offset + 3]; - } - - public static string ReadAsciiString(Span data, int offset, int length) - { - return Encoding.ASCII.GetString(data.Slice(offset, length).ToArray()); - } - - public static string ReadAsciiString(Span data) - { - return ReadAsciiString(data, 0, data.Length); - } - - public static string ReadUtf16StringBigEndian(Span data) - { - var chars = ArrayPool.Shared.Rent(data.Length); - - try - { - var index = 0; - - for (var i = 0; i < data.Length - 1; i += 2) - { - var c = (char)((data[i] << 8) | data[i + 1]); - - if (c == '\0') - { - continue; - } - - chars[index] = c; - ++index; - } - - return new string(chars, 0, index); - } - finally - { - ArrayPool.Shared.Return(chars); - } - } - } -} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/FontParser.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/FontParser.cs deleted file mode 100644 index db01ae555..000000000 --- a/Source/HtmlRenderer.PdfSharp/FontResolution/Parsing/FontParser.cs +++ /dev/null @@ -1,467 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; - -namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution.Parsing -{ - internal static class FontParser - { - public static FontMetadata ExtractFontMetadata(string fontFilePath) - { - try - { - using (var fs = new FileStream(fontFilePath, FileMode.Open, FileAccess.Read)) - { - if (fs.Length > int.MaxValue) - { - return null; - } - - var pooledBytes = ArrayPool.Shared.Rent((int)fs.Length); - - try - { - var bytesRead = fs.Read(pooledBytes, 0, (int)fs.Length); - var bytes = pooledBytes.AsSpan(0, bytesRead); // Fixed size span view - - var metadata = ParseFontMetadata(bytes); - if (metadata != null) - { - metadata.FilePath = fontFilePath; - } - return metadata; - } - finally - { - ArrayPool.Shared.Return(pooledBytes); - } - } - } - catch - { - return null; - } - } - - private static FontMetadata ParseFontMetadata(Span bytes) - { - // Read the font header (big-endian) - var scalarType = BinaryParser.ReadUint32BigEndian(bytes, 0); - - // Verify it's a valid TTF/OTF file - if (scalarType != 0x00010000 && // TrueType version 1.0 - scalarType != 0x74727565 && // "true" - TrueType - scalarType != 0x4F54544F) // "OTTO" - OpenType with CFF outline - { - return null; - } - - var tableCount = BinaryParser.ReadUint16BigEndian(bytes, 4); - - // Find the "name" and "os/2" tables - long nameTableOffset = -1; - uint? os2TableOffset = null; - var tableRecordOffset = 12; - - for (var i = 0; i < tableCount; i++) - { - var tag = BinaryParser.ReadAsciiString(bytes, tableRecordOffset, 4); - var offset = BinaryParser.ReadUint32BigEndian(bytes, tableRecordOffset + 8); - - if (tag == "name") - { - nameTableOffset = offset; - } - else if (tag == "OS/2") - { - os2TableOffset = offset; - } - - tableRecordOffset += 16; - } - - if (nameTableOffset == -1) - { - return null; - } - - var metadata = new FontMetadata(); - - ParseNameTable(bytes, nameTableOffset, metadata); - - // Extract OS/2 table data if available for more accurate weight/width/style info - var hasOs2Data = ParseOs2Table(bytes, os2TableOffset, metadata); - - // Parse style from subfamily if not already set from OS/2 - if (!hasOs2Data) - { - ParseStyleFromSubfamily(metadata, metadata.Subfamily); - } - - // Return metadata if we at least have a family name - return !string.IsNullOrEmpty(metadata.Family) ? metadata : null; - } - - private static void ParseNameTable(Span bytes, long nameTableOffset, FontMetadata metadata) - { - var nameTablePos = (int)nameTableOffset; - var count = BinaryParser.ReadUint16BigEndian(bytes, nameTablePos + 2); - var stringDataOffset = BinaryParser.ReadUint16BigEndian(bytes, nameTablePos + 4); - - // Track which values have been set with English entries - var hasEnglishValue = new HashSet(); - - // First pass: Look for English entries - var nameRecordOffset = nameTablePos + 6; - for (var i = 0; i < count; i++, nameRecordOffset += 12) - { - var platformId = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset); - var encodingId = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 2); - var languageId = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 4); - var nameId = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 6); - var length = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 8); - var offset = BinaryParser.ReadUint16BigEndian(bytes, nameRecordOffset + 10); - - // Filter for English language entries: - // Platform 3 (Windows): languageID 0x0409 (1033) = US English - // Platform 1 (Macintosh): languageID 0 = English - var isEnglish = (platformId == 3 && languageId == 0x0409) || - (platformId == 1 && languageId == 0); - - if ((platformId != 3 && platformId != 1) || !IsRelevantNameId(nameId)) - { - continue; - } - - var stringPos = (int)nameTableOffset + stringDataOffset + offset; - - if (stringPos + length > bytes.Length) - { - continue; - } - - var nameBytes = bytes.Slice(stringPos, length); - - var decodedString = DecodeNameTableString(nameBytes, platformId, encodingId); - - if (string.IsNullOrEmpty(decodedString) || decodedString == null) - { - continue; - } - - // If English, set the value and mark as having English - if (isEnglish) - { - if (SetMetadataValue(metadata, nameId, decodedString)) - { - hasEnglishValue.Add(nameId); - } - } - // If not English but we haven't found an English entry yet, use it as fallback - else if (!hasEnglishValue.Contains(nameId)) - { - SetMetadataValue(metadata, nameId, decodedString); - } - } - } - - private static bool IsRelevantNameId(ushort nameId) - { - return nameId is 1 || nameId is 2 || nameId is 4 || nameId is 6 || nameId is 16 || nameId is 17; - } - - private static bool SetMetadataValue(FontMetadata metadata, ushort nameId, string value) - { - switch (nameId) - { - case 1: // Legacy Family name - if (metadata.Family == null) - { - metadata.Family = value; - return true; - } - return false; - case 2: // Legacy Subfamily - if (metadata.Subfamily == null) - { - metadata.Subfamily = value; - ParseStyleFromSubfamily(metadata, value); - return true; - } - return false; - case 4: // Full font name - if (metadata.FullName == null) - { - metadata.FullName = value; - return true; - } - return false; - case 6: // PostScript name - if (metadata.PostScriptName == null) - { - metadata.PostScriptName = value; - return true; - } - return false; - case 16: // Preferred Family (Typographic Family) - if (metadata.PreferredFamily == null) - { - metadata.PreferredFamily = value; - return true; - } - return false; - case 17: // Preferred Subfamily (Typographic Subfamily) - if (metadata.PreferredSubfamily == null) - { - metadata.PreferredSubfamily = value; - return true; - } - return false; - default: - return false; - } - } - - private static void ParseStyleFromSubfamily(FontMetadata metadata, string subfamily) - { - if (subfamily is null) - { - return; - } - - var lower = subfamily.ToLowerInvariant(); - - // Parse weight, width, and style - var weight = ParseWeight(lower); - var width = ParseWidth(lower); - var style = ParseStyle(lower); - - // Update metadata with parsed attributes (note: FontAttributes order is Weight, Style, Width) - metadata.Attributes = new FontAttributes - { - Weight = weight, - Style = style, - Width = width - }; - } - - private static FontStyle ParseStyle(string lowerSubfamily) - { - if (lowerSubfamily.Contains("oblique")) - { - return FontStyle.Oblique; - } - - if (lowerSubfamily.Contains("italic")) - { - return FontStyle.Italic; - } - - return FontStyle.Normal; - } - - private static FontWeight ParseWeight(string lowerSubfamily) - { - // Map weight keywords to enum values - if (lowerSubfamily.Contains("thin") || lowerSubfamily.Contains("hairline")) - { - return FontWeight.Thin; - } - - if (lowerSubfamily.Contains("extralight") || lowerSubfamily.Contains("ultra light")) - { - return FontWeight.ExtraLight; - } - - if (lowerSubfamily.Contains("light")) - { - return FontWeight.Light; - } - - if (lowerSubfamily.Contains("medium")) - { - return FontWeight.Medium; - } - - if (lowerSubfamily.Contains("semibold") || lowerSubfamily.Contains("demibold")) - { - return FontWeight.SemiBold; - } - - if (lowerSubfamily.Contains("extrabold") || lowerSubfamily.Contains("ultra bold")) - { - return FontWeight.ExtraBold; - } - - if (lowerSubfamily.Contains("bold")) - { - return FontWeight.Bold; - } - - if (lowerSubfamily.Contains("black") || lowerSubfamily.Contains("heavy")) - { - return FontWeight.Black; - } - - return FontWeight.Normal; - } - - private static FontWidth ParseWidth(string lowerSubfamily) - { - if (lowerSubfamily.Contains("ultracondensed") || lowerSubfamily.Contains("ultra condensed")) - { - return FontWidth.UltraCondensed; - } - - if (lowerSubfamily.Contains("extracondensed") || lowerSubfamily.Contains("extra condensed")) - { - return FontWidth.ExtraCondensed; - } - - if (lowerSubfamily.Contains("condensed")) - { - return FontWidth.Condensed; - } - - if (lowerSubfamily.Contains("semicondensed")) - { - return FontWidth.SemiCondensed; - } - - if (lowerSubfamily.Contains("semiexpanded")) - { - return FontWidth.SemiExpanded; - } - - if (lowerSubfamily.Contains("extraexpanded") || lowerSubfamily.Contains("extra expanded")) - { - return FontWidth.ExtraExpanded; - } - - if (lowerSubfamily.Contains("ultraexpanded") || lowerSubfamily.Contains("ultra expanded")) - { - return FontWidth.UltraExpanded; - } - - if (lowerSubfamily.Contains("expanded")) - { - return FontWidth.Expanded; - } - - return FontWidth.Medium; - } - - private static bool ParseOs2Table(Span bytes, uint? os2TableOffset, FontMetadata metadata) - { - if (!os2TableOffset.HasValue) - { - return false; - } - - var offset = (int)os2TableOffset.Value; - - try - { - // OS/2 table structure (v0+): - // Offset 4: usWeightClass (USHORT) - font weight (100-900) - // Offset 6: usWidthClass (USHORT) - font width (1-9) - // Offset 8: fsType (USHORT) - embedding permissions - // Offset 62: fsSelection (USHORT) - contains style bits - - // Check if we have enough bytes for OS/2 table - if (offset + 64 > bytes.Length) - { - return false; - } - - // Read usWeightClass (offset 4 in OS/2) - var usWeightClass = BinaryParser.ReadUint16BigEndian(bytes, offset + 4); - var weight = FontWeight.Normal; - if (usWeightClass >= 100 && usWeightClass <= 900) - { - weight = (FontWeight)usWeightClass; - } - - // Read usWidthClass (offset 6 in OS/2) - var usWidthClass = BinaryParser.ReadUint16BigEndian(bytes, offset + 6); - var width = FontWidth.Medium; - if (usWidthClass >= 1 && usWidthClass <= 9) - { - width = WidthClassToEnum(usWidthClass); - } - - // Read fsSelection for style bits (offset 62 in OS/2) - var fsSelection = BinaryParser.ReadUint16BigEndian(bytes, offset + 62); - - // Bit 9: Oblique, Bit 6: Regular, Bit 0: Italic - var style = ((fsSelection & 0x0200) != 0) ? FontStyle.Oblique : - ((fsSelection & 0x0001) != 0) ? FontStyle.Italic : - FontStyle.Normal; - - // Update attributes with extracted OS/2 data (note: FontAttributes order is Weight, Style, Width) - metadata.Attributes = new FontAttributes - { - Weight = weight, - Style = style, - Width = width - }; - return true; - } - catch - { - // Silently continue if OS/2 parsing fails - return false; - } - } - - private static FontWidth WidthClassToEnum(int widthClass) - { - switch (widthClass) - { - case 1: return FontWidth.UltraCondensed; - case 2: return FontWidth.ExtraCondensed; - case 3: return FontWidth.Condensed; - case 4: return FontWidth.SemiCondensed; - case 5: return FontWidth.Medium; - case 6: return FontWidth.SemiExpanded; - case 7: return FontWidth.Expanded; - case 8: return FontWidth.ExtraExpanded; - case 9: return FontWidth.UltraExpanded; - default: return FontWidth.Medium; - } - } - - private static string DecodeNameTableString(Span data, ushort platformId, ushort encodingId) - { - try - { - // Windows platform - if (platformId == 3) - { - if (encodingId == 1) - { - return BinaryParser.ReadUtf16StringBigEndian(data); - } - - return null; - } - - // Macintosh platform - if (platformId == 1) - { - if (encodingId == 0) - { - return BinaryParser.ReadAsciiString(data).TrimEnd('\0'); - } - } - - return null; - } - catch - { - return null; - } - } - } -} diff --git a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs index 15287ca15..d679be8a7 100644 --- a/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs +++ b/Source/HtmlRenderer.WPF/Adapters/WpfAdapter.cs @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Reflection; using System.Windows; @@ -33,6 +34,20 @@ internal sealed class WpfAdapter : RAdapter { #region Fields and Consts + // One HttpClient shared for the adapter's (process) lifetime, not one per request - `new + // HttpClient()` per call is a well-documented anti-pattern that exhausts sockets under load and + // never observes DNS changes. + // + // Declared BEFORE _instance deliberately: C# runs static field initializers in textual + // declaration order, and _instance's own initializer (`new WpfAdapter()`) runs the instance + // constructor immediately, which reads _sharedHttpClient on its very first line. If this field + // were declared after _instance, that read would observe _sharedHttpClient's still-default value + // (null - its own initializer hasn't run yet) and permanently capture a null HttpClient into + // NetworkLoader, since HttpClientNetworkLoader takes it as a constructor parameter, not a live + // reference to this field. (Confirmed by a real crash with this exact ordering, in the sibling + // WinFormsAdapter.) + private static readonly HttpClient _sharedHttpClient = new HttpClient(); + /// /// Singleton instance of global adapter. /// @@ -43,10 +58,18 @@ internal sealed class WpfAdapter : RAdapter ///
private static readonly List ValidColorNamesLc; - // One HttpClient shared for the adapter's (process) lifetime, not one per request - `new - // HttpClient()` per call is a well-documented anti-pattern that exhausts sockets under load and - // never observes DNS changes. - private static readonly HttpClient _sharedHttpClient = new HttpClient(); + // Backs LoadFontFaceFontInt's temp-file registration (see its own doc comment for why a real file + // is necessary - WPF's font-loading APIs are documented as file-URI-only, with no supported + // memory-only path) - one directory per process, cleaned up by the OS's normal temp-file + // housekeeping, not by this process. + private static readonly string _fontFaceTempDirectory = CreateFontFaceTempDirectory(); + + private static string CreateFontFaceTempDirectory() + { + var dir = Path.Combine(Path.GetTempPath(), "HtmlRenderer.FontFace." + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + return dir; + } #endregion @@ -192,6 +215,47 @@ protected override RFont CreateFontInt(RFontFamily family, double size, RFontSty return new FontAdapter(new Typeface(((FontFamilyAdapter)family).FontFamily, GetFontStyle(style), GetFontWidth(style), FontStretches.Normal), size); } + /// + /// Loads one @font-face face's bytes as a WPF + /// via a temp file and - WPF's own documented way to load + /// a font from an arbitrary location. + /// + /// + /// Two earlier approaches were tried and rejected first: the Win32 AddFontMemResourceEx API + /// registers with GDI, but WPF's text stack (DirectWrite-based) never consults GDI's per-process + /// font table, so registered faces were silently invisible to WPF. A fully in-memory + /// -scheme trick (serving the bytes from a + /// for a synthetic URI, the same mechanism that historically let WPF/XBAP + /// apps reference fonts over plain http://) was also tried and confirmed NOT to work: WPF's + /// own source documents Fonts.GetFontFamilies's location parameter as "must be an absolute + /// file URI or path" - empirically, both the eager folder-scan API and the lazy + /// new FontFamily(baseUri, "./file#Name") reference came back empty against the synthetic + /// scheme even though the handler correctly served the bytes. There is no supported WPF API for + /// loading a font from memory alone, so - like WinForms' own PrivateFontCollection.AddFontFile + /// path, for its own different reason (GDI+'s AddMemoryFont being unreliable, not a + /// fundamental API gap) - this writes to a real, never-deleted temp file. + /// + /// + /// Each face gets its OWN, never-reused temp subdirectory - not one shared directory for every + /// face. WPF's font-family folder enumeration caches its scan per directory and does not notice + /// files added to that directory after the first scan (confirmed empirically: with a single shared + /// directory, every face after the first one silently resolved back to the first face's glyphs, + /// because 's first call had already cached "what's in this + /// folder" before the later faces' files existed). A fresh, single-file directory per face sidesteps + /// that cache entirely. + /// + protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath) + { + var faceDirectory = Path.Combine(_fontFaceTempDirectory, Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(faceDirectory); + var tempFilePath = Path.Combine(faceDirectory, "face.ttf"); + File.WriteAllBytes(tempFilePath, fontBytes); + + var family = Fonts.GetFontFamilies(new Uri(tempFilePath)).FirstOrDefault(); + + return family != null ? new FontFamilyAdapter(family) : null; + } + protected override object GetClipboardDataObjectInt(string html, string plainText) { return ClipboardHelper.CreateDataObject(html, plainText); diff --git a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs index 8fc339c4a..7fe659f10 100644 --- a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs +++ b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs @@ -13,12 +13,14 @@ using System; using System.Drawing; using System.Drawing.Drawing2D; +using System.Drawing.Text; using System.IO; using System.Net.Http; using System.Windows.Forms; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Core.Network; +using TheArtOfDev.HtmlRenderer.Core.Utils; using TheArtOfDev.HtmlRenderer.WinForms.Utilities; namespace TheArtOfDev.HtmlRenderer.WinForms.Adapters @@ -30,15 +32,39 @@ internal sealed class WinFormsAdapter : RAdapter { #region Fields and Consts + // One HttpClient shared for the adapter's (process) lifetime, not one per request - `new + // HttpClient()` per call is a well-documented anti-pattern that exhausts sockets under load and + // never observes DNS changes. + // + // Declared BEFORE _instance deliberately: C# runs static field initializers in textual + // declaration order, and _instance's own initializer (`new WinFormsAdapter()`) runs the instance + // constructor immediately, which reads _sharedHttpClient on its very first line. If this field + // were declared after _instance, that read would observe _sharedHttpClient's still-default value + // (null - its own initializer hasn't run yet) and permanently capture a null HttpClient into + // NetworkLoader, since HttpClientNetworkLoader takes it as a constructor parameter, not a live + // reference to this field. (Confirmed by a real crash with this exact ordering.) + private static readonly HttpClient _sharedHttpClient = new HttpClient(); + /// /// Singleton instance of global adapter. /// private static readonly WinFormsAdapter _instance = new WinFormsAdapter(); - // One HttpClient shared for the adapter's (process) lifetime, not one per request - `new - // HttpClient()` per call is a well-documented anti-pattern that exhausts sockets under load and - // never observes DNS changes. - private static readonly HttpClient _sharedHttpClient = new HttpClient(); + // Adapter-level PrivateFontCollection for @font-face-loaded faces - one collection shared for the + // adapter's (process) lifetime, growing by one family per LoadFontFaceFontInt call. + private readonly PrivateFontCollection _fontFaceCollection = new PrivateFontCollection(); + + // Backs LoadFontFaceFontInt's temp-file registration (see its own doc comment for why a temp file + // is used instead of PrivateFontCollection.AddMemoryFont) - one directory per process, cleaned up + // by the OS's normal temp-file housekeeping, not by this process. + private static readonly string _fontFaceTempDirectory = CreateFontFaceTempDirectory(); + + private static string CreateFontFaceTempDirectory() + { + var dir = Path.Combine(Path.GetTempPath(), "HtmlRenderer.FontFace." + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + return dir; + } #endregion @@ -182,6 +208,67 @@ protected override RFont CreateFontInt(RFontFamily family, double size, RFontSty return new FontAdapter(new Font(((FontFamilyAdapter)family).FontFamily, (float)size, fontStyle)); } + /// + /// Loads one @font-face face's bytes into the adapter's + /// via - through a temp file, matching this + /// adapter's own pre-existing DemoForm.LoadCustomFonts pattern (that path is untouched, + /// this is a new, separate mechanism specific to @font-face). + /// + /// + /// Deliberately NOT , despite it needing no temp + /// file: empirically (a throwaway repro project registering a dozen distinct families into one + /// ), AddMemoryFont is unreliable on this target + /// framework - permanently fails to reflect several of + /// the added families (not a timing race: polling for up to 300ms after the call never finds them + /// either), while the identical sequence of files through AddFontFile succeeds 100% of the + /// time across repeated runs. This is a known-flaky area of GDI+'s AddMemoryFont P/Invoke + /// path, not a bug in this port. The temp file is deliberately never deleted: GDI+ keeps it + /// memory-mapped for as long as this process-lifetime singleton's + /// references it, and the OS's own temp-directory housekeeping reclaims it afterward - the same + /// "small, bounded, process-lifetime" rationale the removed AddMemoryFont/AllocHGlobal + /// approach relied on. + /// + /// The returned is found by sniffing the font's own internal + /// family name via and matching it against + /// - not by comparing Families.Length before + /// and after, nor by taking the array's last entry. Two bugs made that approach unreliable: (1) + /// Families groups every face by family name (the whole point of + /// - it lets pick the right face via + /// automatically), so registering a second face of an *already-registered* + /// family (e.g. this face set's own Bold after its Regular) never changes the count at all; and + /// (2) even when the count does change, Families is returned in a GDI-defined (effectively + /// alphabetical) order, not insertion order, so "the last entry" is often a completely unrelated, + /// alphabetically-later family, not the one just added. + /// + /// + protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath) + { + string familyName; + using (var stream = new MemoryStream(fontBytes)) + { + familyName = TtfFontDescription.LoadDescription(stream).FontFamilyInvariantCulture; + } + + if (string.IsNullOrEmpty(familyName)) + { + return null; + } + + var tempFilePath = Path.Combine(_fontFaceTempDirectory, Guid.NewGuid().ToString("N") + ".ttf"); + File.WriteAllBytes(tempFilePath, fontBytes); + _fontFaceCollection.AddFontFile(tempFilePath); + + foreach (var family in _fontFaceCollection.Families) + { + if (string.Equals(family.Name, familyName, StringComparison.OrdinalIgnoreCase)) + { + return new FontFamilyAdapter(family); + } + } + + return null; + } + protected override object GetClipboardDataObjectInt(string html, string plainText) { return ClipboardHelper.CreateDataObject(html, plainText); diff --git a/Source/HtmlRenderer/Adapters/RAdapter.cs b/Source/HtmlRenderer/Adapters/RAdapter.cs index 7661582ef..7de21d306 100644 --- a/Source/HtmlRenderer/Adapters/RAdapter.cs +++ b/Source/HtmlRenderer/Adapters/RAdapter.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.CssEngine; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Core.Handlers; using TheArtOfDev.HtmlRenderer.Core.Network; @@ -56,6 +57,16 @@ public abstract class RAdapter ///
private readonly FontsHandler _fontsHandler; + /// + /// Dedup cache for , keyed by the resolved resource's absolute URI - since + /// the orchestrator re-runs @font-face registration on every SetHtml and this adapter + /// is a process-wide singleton, re-registering the same face's bytes with the platform text engine + /// on every call would leak native font handles (WinForms' PrivateFontCollection.AddMemoryFont/ + /// WPF's AddFontMemResourceEx both register a *new* resource each call, even for identical + /// bytes - neither is idempotent). + /// + private readonly Dictionary _fontFaceCache = new Dictionary(); + /// /// default CSS parsed data singleton /// @@ -147,6 +158,11 @@ protected void OnColorSchemeChanged() private FileUriNetworkLoader _internalFileLoader; private FileUriNetworkLoader InternalFileLoader => _internalFileLoader ?? (_internalFileLoader = new FileUriNetworkLoader()); + // Serves embedded: URIs unconditionally, the same way data:/file: are always handled internally + // regardless of which loader is configured - stateless (the target assembly is named in the URI + // itself, see EmbeddedResourceNetworkLoader), so a single shared instance needs no lazy init. + private static readonly EmbeddedResourceNetworkLoader InternalEmbeddedResourceLoader = new EmbeddedResourceNetworkLoader(); + /// /// The document's base URL, used to resolve relative href/src/CSS url() /// references that have no closer <base href> element to resolve against. Sourced from @@ -158,9 +174,9 @@ protected void OnColorSchemeChanged() /// /// Resolve an external resource (a stylesheet, image, or @font-face font) referenced by the - /// document, dispatching by URI scheme: data: and file: always resolve internally - /// (the latter refused outright when is false), every - /// other scheme goes to the configured . + /// document, dispatching by URI scheme: data:, file:, and embedded: always + /// resolve internally (file: refused outright when is + /// false), every other scheme goes to the configured . /// /// the resource URI, already resolved to absolute against or a <base href> element /// the resolved resource, or null if it could not be resolved @@ -195,6 +211,12 @@ public Task GetResourceStream(RUri uri) return fileLoader.GetResourceStream(uri); } + if (uri.Scheme == EmbeddedResourceNetworkLoader.Scheme) + { + var embeddedLoader = NetworkLoader as EmbeddedResourceNetworkLoader ?? InternalEmbeddedResourceLoader; + return embeddedLoader.GetResourceStream(uri); + } + return NetworkLoader.GetResourceStream(uri); } @@ -290,11 +312,20 @@ public RImage ImageFromStream(Stream memoryStream) } /// - /// Check if the given font exists in the system by font family name. + /// Check if the given font exists in the system by font family name. Consulted by + /// to decide whether a font-family + /// candidate should be kept as-is or the next fallback (ultimately ) + /// tried instead - so this must recognize a family the moment it's usable, including one + /// registered via , not just /system fonts. /// /// the font name to check /// true - font exists by given family name, false - otherwise - public bool IsFontExists(string font) + /// + /// Virtual so the PdfSharp backend can override it - its @font-face registrations live + /// entirely in its own FontResolver (see 's doc comment for why), + /// invisible to the shared FontsHandler this base implementation checks. + /// + public virtual bool IsFontExists(string font) { return _fontsHandler.IsFontExists(font); } @@ -320,6 +351,91 @@ public void AddFontFamilyMapping(string fromFamily, string toFamily) _fontsHandler.AddFontFamilyMapping(fromFamily, toFamily); } + /// + /// Loads one @font-face src: url(...) candidate and registers it as a face of + /// for CSS Fonts Level 4 matching. Resolves the resource through + /// - the same funnel used for images/stylesheets, so this supports + /// file:/data:/http(s): uniformly - loads the platform-specific face via + /// , and registers it with . + /// + /// the CSS family name declared by the @font-face rule + /// the resolved, absolute src URI to fetch (already resolved against the document base/a stylesheet's own location by the caller) + /// CSS Fonts Level 4 numeric weight (1-1000) this face matches for + /// whether this face matches an italic/oblique request + /// CSS Fonts Level 3 stretch (1-9) this face matches for + /// the face's unicode-range restriction, or null for "covers whatever is asked of it" + /// true if the face was loaded and registered, false if the resource could not be resolved/loaded (the caller tries the next src candidate) + /// + /// Virtual so the PdfSharp backend can override it to bypass 's shared + /// registry entirely and register directly with its own FontResolver instead - PDFsharp + /// needs raw font bytes at PDF-generation time for embedding (via its own richer + /// IFontResolver-based CSS-Fonts-L4 matching), unlike WinForms/WPF, which just need an + /// opaque platform font-family handle. See the base implementation's own doc comment for the + /// shared-registry path this overrides. + /// + public virtual async Task AddFontFace(string familyName, RUri uri, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + RFontFamily fontFamily; + if (!_fontFaceCache.TryGetValue(uri.AbsoluteUri, out fontFamily)) + { + var networkResponse = await GetResourceStream(uri).ConfigureAwait(false); + if (networkResponse == null || networkResponse.ResourceStream == null) + { + return false; + } + + byte[] fontBytes; + using (var memoryStream = new MemoryStream()) + { + using (networkResponse.ResourceStream) + { + networkResponse.ResourceStream.CopyTo(memoryStream); + } + fontBytes = memoryStream.ToArray(); + } + + try + { + fontFamily = LoadFontFaceFontInt(fontBytes, uri.AbsoluteUri); + } + catch + { + return false; + } + + if (fontFamily == null) + { + return false; + } + + _fontFaceCache[uri.AbsoluteUri] = fontFamily; + } + + _fontsHandler.AddFontFace(familyName, fontFamily, weight, isItalic, stretch, ranges); + return true; + } + + /// + /// Satisfies an @font-face src: local(...) candidate: rather than fetching a resource + /// at all, this looks for a family already registered under (a + /// system font, or an earlier / registration) + /// and, if found, registers *that same* as a face of + /// too. + /// + /// true if a local family by that name was found and registered, false otherwise (the caller tries the next src candidate) + /// Virtual for the same reason as - see its doc comment. + public virtual bool AddFontFaceFromLocalFamily(string familyName, string localFamilyName, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + var localFamily = _fontsHandler.TryGetExistingFamily(localFamilyName); + if (localFamily == null) + { + return false; + } + + _fontsHandler.AddFontFace(familyName, localFamily, weight, isItalic, stretch, ranges); + return true; + } + /// /// Get font instance by given font family name, size and style. /// @@ -332,6 +448,28 @@ public RFont GetFont(string family, double size, RFontStyle style) return _fontsHandler.GetCachedFont(family, size, style); } + /// + /// Get font instance by given font family name, size, style, and CSS Fonts Level 4 numeric + /// weight/stretch - matches against any @font-face faces registered for + /// (see ), falling + /// back to the legacy family-name lookup when none are registered. + /// + /// the font family name + /// font size + /// font style (Italic/Underline/Strikeout are honored regardless of which face is chosen; Bold is superseded by ) + /// CSS Fonts Level 4 numeric weight (1-1000) + /// CSS Fonts Level 3 stretch (1-9) + /// the box's first non-whitespace character's codepoint, for unicode-range face disambiguation, or null to skip it + /// font instance, or null when is given and no registered face covers it + /// + /// Virtual so the PdfSharp backend can override it to bypass 's shared + /// registry entirely, for the same reason as - see its doc comment. + /// + public virtual RFont GetFont(string family, double size, RFontStyle style, int weight, int stretch, int? codepoint) + { + return _fontsHandler.GetCachedFont(family, size, style, weight, stretch, codepoint); + } + /// /// Get image to be used while HTML image is loading. /// @@ -518,6 +656,19 @@ internal RFont CreateFont(RFontFamily family, double size, RFontStyle style) /// font instance protected abstract RFont CreateFontInt(RFontFamily family, double size, RFontStyle style); + /// + /// Registers (a loaded @font-face src candidate's raw + /// font file bytes) with the platform text engine and returns an opaque + /// handle for it - one file's bytes in, one face's family handle out (a WinForms/WPF + /// implementation registers with the OS/GDI text engine and returns a handle to that one face; + /// this is not called for backends - like PdfSharp - that override to + /// bypass this entirely). + /// + /// the font file's raw bytes (TTF/OTF) + /// the resolved source this was loaded from, for diagnostics/error messages only + /// the registered face's family handle, or null if the bytes could not be loaded as a font (the caller tries the next src candidate) + protected abstract RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath); + /// /// Get data object for the given html and plain text data.
/// The data object can be used for clipboard or drag-drop operation. diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs index e59b987e6..d7ae1ca52 100644 --- a/Source/HtmlRenderer/Core/CssData.cs +++ b/Source/HtmlRenderer/Core/CssData.cs @@ -181,34 +181,50 @@ internal bool MediaOutcomeChanged(MediaQueryContext before, MediaQueryContext af } /// - /// Every style rule in a rule list, in document order, descending into @media and - /// @layer blocks and into CSS-Nesting child rules. - /// only sees the top level, so callers that scan for a rule by selector (rather than matching - /// against a box) need this or they miss anything nested. @supports/@container are - /// deliberately not descended into: their conditions are not evaluated, so their contents do not - /// apply. + /// Every rule in a rule list, in document order, descending into @layer, @media, + /// @container, and (when its condition evaluates true) @supports blocks - matching + /// PeachPDF's own CssData.FlattenRules exactly (full descent, not the narrower + /// style-rule-only/no-@supports-or-@container predecessor this replaces). Callers that only want + /// style rules (the common case - e.g. scanning for a rule by selector rather than matching + /// against a box, since only sees the top level) filter with + /// .OfType<IStyleRule>(), matching PeachPDF's own call-site pattern of filtering the + /// general enumerator per use rather than a dedicated pre-filtered helper. CSS-Nesting child rules + /// are reached because is itself yielded before recursing. /// - internal static IEnumerable FlattenStyleRules(IEnumerable rules) + internal static IEnumerable FlattenRules(IEnumerable rules) { foreach (var rule in rules) { + yield return rule; + if (rule is IStyleRule styleRule) { - yield return styleRule; - - foreach (var nested in FlattenStyleRules(styleRule.NestedRules)) + foreach (var nested in FlattenRules(styleRule.NestedRules)) + yield return nested; + } + else if (rule is ILayerRule layerRule) + { + foreach (var nested in FlattenRules(layerRule.Rules)) yield return nested; } else if (rule is IMediaRule mediaRule) { - foreach (var nested in FlattenStyleRules(mediaRule.Rules)) + foreach (var nested in FlattenRules(mediaRule.Rules)) yield return nested; } - else if (rule is ILayerRule layerRule) + else if (rule is IContainerRule containerRule) { - foreach (var nested in FlattenStyleRules(layerRule.Rules)) + foreach (var nested in FlattenRules(containerRule.Rules)) yield return nested; } + else if (rule is ISupportsRule supportsRule) + { + if (supportsRule.Condition.Check()) + { + foreach (var nested in FlattenRules(supportsRule.Rules)) + yield return nested; + } + } } } diff --git a/Source/HtmlRenderer/Core/CssDefaults.cs b/Source/HtmlRenderer/Core/CssDefaults.cs index 489bb7617..fa143788a 100644 --- a/Source/HtmlRenderer/Core/CssDefaults.cs +++ b/Source/HtmlRenderer/Core/CssDefaults.cs @@ -168,6 +168,7 @@ @media print { { "font-style", "normal" }, { "font-variant", "normal" }, { "font-weight", "normal" }, + { "font-stretch", "normal" }, { "float", "none" }, { "clear", "none" }, { "box-sizing", "content-box" }, @@ -219,7 +220,7 @@ @media print { "white-space", "visibility", "text-indent", "text-align", - "font-family", "font-size", "font-style", "font-variant", "font-weight", + "font-family", "font-size", "font-style", "font-variant", "font-weight", "font-stretch", "list-style-image", "list-style-position", "list-style-type", "list-style", "line-height", "word-break", diff --git a/Source/HtmlRenderer/Core/CssEngine/Model/CodepointRange.cs b/Source/HtmlRenderer/Core/CssEngine/Model/CodepointRange.cs new file mode 100644 index 000000000..5cd5d7de3 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Model/CodepointRange.cs @@ -0,0 +1,31 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// An inclusive range of Unicode scalar values (codepoints), e.g. the U+0000-00FF of a CSS + /// @font-face unicode-range descriptor. Both ends are inclusive - unlike + /// , which is half-open and index based. int-based (matching this + /// codebase's existing / + /// int-codepoint convention) rather than PeachPDF's Rune-based RuneRange - see + /// for why: System.Text.Rune only exists on + /// net8.0, not this project's netstandard2.0 leg. Public: it appears in + /// 's public signature, for a custom adapter implementation + /// outside this assembly. + /// + public readonly struct CodepointRange + { + public CodepointRange(int start, int end) + { + Start = start; + End = end; + } + + /// The first codepoint covered by this range (inclusive). + public int Start { get; } + + /// The last codepoint covered by this range (inclusive). + public int End { get; } + + /// Whether lies within this range (both ends inclusive). + public bool Contains(int codepoint) => codepoint >= Start && codepoint <= End; + } +} diff --git a/Source/HtmlRenderer/Core/Dom/CssBox.cs b/Source/HtmlRenderer/Core/Dom/CssBox.cs index 4879bfeb1..4b26a8fd4 100644 --- a/Source/HtmlRenderer/Core/Dom/CssBox.cs +++ b/Source/HtmlRenderer/Core/Dom/CssBox.cs @@ -1708,9 +1708,29 @@ protected RBrush GetSelectionBackBrush(RGraphics g, bool forceAlpha) } } - protected override RFont GetCachedFont(string fontFamily, double fsize, RFontStyle st) + protected override RFont GetCachedFont(string fontFamily, double fsize, RFontStyle st, int weight, int stretch, int? codepoint) { - return HtmlContainer.Adapter.GetFont(fontFamily, fsize, st); + return HtmlContainer.Adapter.GetFont(fontFamily, fsize, st, weight, stretch, codepoint); + } + + /// See . + protected override int? GetFirstNonWhitespaceCodepoint() + { + if (string.IsNullOrEmpty(_text)) + return null; + + for (var i = 0; i < _text.Length; i++) + { + if (char.IsWhiteSpace(_text[i])) + continue; + + if (char.IsHighSurrogate(_text[i]) && i + 1 < _text.Length && char.IsLowSurrogate(_text[i + 1])) + return char.ConvertToUtf32(_text[i], _text[i + 1]); + + return _text[i]; + } + + return null; } protected override RColor GetActualColor(string colorStr) diff --git a/Source/HtmlRenderer/Core/Dom/CssBoxFrame.cs b/Source/HtmlRenderer/Core/Dom/CssBoxFrame.cs index 7cf607cf7..ecebb9223 100644 --- a/Source/HtmlRenderer/Core/Dom/CssBoxFrame.cs +++ b/Source/HtmlRenderer/Core/Dom/CssBoxFrame.cs @@ -65,6 +65,22 @@ internal sealed class CssBoxFrame : CssBox ///
private bool _imageLoadingComplete; + /// + /// the resolved YouTube/Vimeo oEmbed API URI to fetch, set in the constructor but not fetched + /// until - see its own doc comment for why the fetch can't start + /// in the constructor. + /// + private RUri _videoApiUri; + + /// "YouTube" or "Vimeo", for 's error reporting. + private string _videoApiSource; + + /// or . + private Action _videoApiParseResult; + + /// Whether has already been kicked off - guards so it only starts once. + private bool _videoDataRequested; + #endregion @@ -85,12 +101,16 @@ public CssBoxFrame(CssBox parent, HtmlTag tag) if (uri.Host.IndexOf("youtube.com", StringComparison.InvariantCultureIgnoreCase) > -1) { _isVideo = true; - LoadYoutubeDataAsync(uri); + _videoApiUri = new RUri(string.Format("https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v={0}&format=json", uri.Segments[2])); + _videoApiSource = "YouTube"; + _videoApiParseResult = OnDownloadYoutubeApiCompleted; } else if (uri.Host.IndexOf("vimeo.com", StringComparison.InvariantCultureIgnoreCase) > -1) { _isVideo = true; - LoadVimeoDataAsync(uri); + _videoApiUri = new RUri(string.Format("https://vimeo.com/api/v2/video/{0}.json", uri.Segments[2])); + _videoApiSource = "Vimeo"; + _videoApiParseResult = OnDownloadVimeoApiCompleted; } } @@ -137,28 +157,6 @@ public override void Dispose() #region Private methods - /// - /// Load YouTube video data (title, image, link) by calling YouTube API through the configured - /// - the same funnel used for images/stylesheets/fonts, so a - /// consumer's custom loader (auth headers, proxy, timeouts) applies here too, not just a raw - /// process-wide WebClient bypassing it. - /// - private void LoadYoutubeDataAsync(Uri uri) - { - var apiUri = new RUri(string.Format("https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v={0}&format=json", uri.Segments[2])); - _ = FetchVideoApiDataAsync(apiUri, "YouTube", OnDownloadYoutubeApiCompleted); - } - - /// - /// Load Vimeo video data (title, image, link) by calling Vimeo API through the configured - /// . - /// - private void LoadVimeoDataAsync(Uri uri) - { - var apiUri = new RUri(string.Format("https://vimeo.com/api/v2/video/{0}.json", uri.Segments[2])); - _ = FetchVideoApiDataAsync(apiUri, "Vimeo", OnDownloadVimeoApiCompleted); - } - /// /// Fetch and read as text, fire-and-forget (matching /// 's pattern - nothing here blocks layout/paint), and hand the @@ -527,6 +525,21 @@ private void DrawPlay(RGraphics g, RRect rect) /// the device to use internal override void MeasureWordsSize(RGraphics g) { + // Kicks off the YouTube/Vimeo oEmbed API fetch (through the configured + // RAdapter.NetworkLoader - the same funnel used for images/stylesheets/fonts, so a consumer's + // custom loader applies here too) here rather than from the constructor: HtmlContainer walks + // up to the root box's own HtmlContainer, which DomParser.GenerateCssTree only assigns AFTER + // the whole box tree (including this one) has already been constructed - so at construction + // time it's always null. FetchVideoApiDataAsync dereferences it immediately (before its first + // genuine await), so starting the fetch from the constructor throws a NullReferenceException + // synchronously out of box-tree construction itself. MeasureWordsSize runs during layout, well + // after HtmlContainer is assigned, so it's a safe place to start this instead. + if (_isVideo && !_videoDataRequested) + { + _videoDataRequested = true; + _ = FetchVideoApiDataAsync(_videoApiUri, _videoApiSource, _videoApiParseResult); + } + if (!_wordsSizeMeasured) { MeasureWordSpacing(g); diff --git a/Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs b/Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs index 0f609769b..79c1e6987 100644 --- a/Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs +++ b/Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs @@ -66,6 +66,7 @@ internal abstract class CssBoxProperties private string _fontStyle = "normal"; private string _fontVariant = "normal"; private string _fontWeight = "normal"; + private string _fontStretch = "normal"; private string _float = "none"; private string _clear = "none"; private string _boxSizing = CssConstants.ContentBox; @@ -166,6 +167,7 @@ internal abstract class CssBoxProperties private RColor _actualBorderRightColor = RColor.Empty; private RColor _actualBackgroundColor = RColor.Empty; private RFont _actualFont; + private int? _actualFontWeight; #endregion @@ -725,6 +727,12 @@ public string FontWeight set { _fontWeight = value; } } + public string FontStretch + { + get { return _fontStretch; } + set { _fontStretch = value; } + } + public string ListStyle { get { return _listStyle; } @@ -1438,6 +1446,35 @@ public RFont ActualParentFont get { return GetParent() == null ? ActualFont : GetParent().ActualFont; } } + /// + /// Gets the box's own resolved CSS Fonts Level 4 numeric font-weight (1-1000) - cached and + /// computed lazily like , since bolder/lighter resolve + /// relative to the parent's own resolved weight (see ). + /// + internal int ActualFontWeight + { + get + { + if (_actualFontWeight == null) + { + var parentWeight = GetParent() != null ? GetParent().ActualFontWeight : CssFontWeightResolver.Normal; + _actualFontWeight = CssFontWeightResolver.Resolve(FontWeight, parentWeight); + } + return _actualFontWeight.Value; + } + } + + /// + /// The codepoint of this box's own first non-whitespace character, for unicode-range face + /// disambiguation (see ) - null when this box has no text of its own (e.g. + /// a container box whose text lives on its children) or only whitespace. + /// itself holds no text (that's ), hence the hook. + /// + protected virtual int? GetFirstNonWhitespaceCodepoint() + { + return null; + } + /// /// Gets the font that should be actually used to paint the text of the box /// @@ -1463,11 +1500,14 @@ public RFont ActualFont st |= RFontStyle.Italic; } - if (FontWeight != CssConstants.Normal && FontWeight != CssConstants.Lighter && !string.IsNullOrEmpty(FontWeight) && FontWeight != CssConstants.Inherit) + var weight = ActualFontWeight; + if (weight >= 600) { st |= RFontStyle.Bold; } + var stretch = FontStretchResolver.Resolve(FontStretch); + double fsize; double parentSize = CssConstants.FontSize; @@ -1513,13 +1553,22 @@ public RFont ActualFont fsize = CssConstants.FontSize; } - _actualFont = GetCachedFont(FontFamily, fsize, st); + var codepoint = GetFirstNonWhitespaceCodepoint(); + _actualFont = GetCachedFont(FontFamily, fsize, st, weight, stretch, codepoint); + if (_actualFont == null) + { + // A codepoint-scoped miss (the family has @font-face faces registered, but none + // of them cover this box's first character) - fall back to the default font family + // rather than propagate null, matching the pre-existing "nothing matched" behavior + // for any other unresolvable font-family value. + _actualFont = GetCachedFont(CssConstants.DefaultFont, fsize, st, weight, stretch, null); + } } return _actualFont; } } - protected abstract RFont GetCachedFont(string fontFamily, double fsize, RFontStyle st); + protected abstract RFont GetCachedFont(string fontFamily, double fsize, RFontStyle st, int weight, int stretch, int? codepoint); /// /// Gets the line height @@ -1702,6 +1751,7 @@ protected void InheritStyle(CssBox p, bool everything) _fontStyle = p._fontStyle; _fontVariant = p._fontVariant; _fontWeight = p._fontWeight; + _fontStretch = p._fontStretch; _listStyleImage = p._listStyleImage; _listStylePosition = p._listStylePosition; _listStyleType = p._listStyleType; diff --git a/Source/HtmlRenderer/Core/Entities/CssFontFace.cs b/Source/HtmlRenderer/Core/Entities/CssFontFace.cs new file mode 100644 index 000000000..c9ccd9f3a --- /dev/null +++ b/Source/HtmlRenderer/Core/Entities/CssFontFace.cs @@ -0,0 +1,10 @@ +#nullable enable + +namespace TheArtOfDev.HtmlRenderer.Core.Entities +{ + /// + /// One comma-separated candidate of an @font-face src descriptor, e.g. the + /// url(font.woff2) format("woff2") in src: url(font.woff2) format("woff2"), local("My Font"). + /// + internal record CssFontFace(string? Url, string? Format, string? Tech, string? Local); +} diff --git a/Source/HtmlRenderer/Core/Handlers/FontsHandler.cs b/Source/HtmlRenderer/Core/Handlers/FontsHandler.cs index 83c037eac..7f0bebc0c 100644 --- a/Source/HtmlRenderer/Core/Handlers/FontsHandler.cs +++ b/Source/HtmlRenderer/Core/Handlers/FontsHandler.cs @@ -6,18 +6,61 @@ // 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.Utils; namespace TheArtOfDev.HtmlRenderer.Core.Handlers { + /// + /// One registered @font-face face of a : its CSS Fonts Level 4 + /// matching axes (numeric weight, italic, stretch), the codepoint s it is + /// restricted to (an explicit unicode-range descriptor) or null when it has none (covers + /// whatever a box asks for), and the wrapper backends use to actually create + /// an from it. + /// + internal sealed class FontFaceEntry + { + public FontFaceEntry(int weight, bool italic, int stretch, IReadOnlyList explicitRanges, RFontFamily fontFamily) + { + Weight = weight; + Italic = italic; + Stretch = stretch; + ExplicitRanges = explicitRanges; + FontFamily = fontFamily; + } + + public int Weight { get; } + public bool Italic { get; } + public int Stretch { get; } + public IReadOnlyList ExplicitRanges { get; } + public RFontFamily FontFamily { get; } + } + + /// + /// One registered family: either a legacy simple (the pre-existing + /// path - e.g. a WinForms demo's LoadCustomFonts, + /// unchanged behavior), or a list of @font-face-registered faces (new path), or both at once. + /// Named to match PeachPDF's Fonts\FontFamilyModel.cs, since it's the same "one family, its + /// registered faces" concept - extended here with the escape hatch + /// PeachPDF's PDF-only resolver has no equivalent of, since this registry is shared across backends + /// that still need the legacy single-family API to keep working unchanged. + /// + internal sealed class FontFamilyModel + { + public string Name { get; set; } + public RFontFamily SimpleFamily { get; set; } + public List Faces { get; } = new List(); + } + /// /// Utilities for fonts and fonts families handling. /// @@ -26,7 +69,7 @@ internal sealed class FontsHandler #region Fields and Consts /// - /// + /// /// private readonly RAdapter _adapter; @@ -38,13 +81,22 @@ internal sealed class FontsHandler /// /// collection of all installed and added font families to check if font exists /// - private readonly Dictionary _existingFontFamilies = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly Dictionary _existingFontFamilies = new Dictionary(StringComparer.InvariantCultureIgnoreCase); /// - /// cache of all the font used not to create same font again and again + /// cache of all the font used not to create same font again and again (legacy family-string path) /// private readonly Dictionary>> _fontsCache = new Dictionary>>(StringComparer.InvariantCultureIgnoreCase); + /// + /// cache of fonts resolved through the @font-face matching path, keyed by everything that + /// can change which face gets chosen (family, size, weight, italic, stretch, and the codepoint used + /// for unicode-range disambiguation, -1 when none) - a wider key than + /// needs, since two requests with the same can still resolve to different + /// faces (e.g. weight 300 vs 600, both non-bold). + /// + private readonly Dictionary> _faceFontsCache = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + #endregion @@ -77,6 +129,25 @@ public bool IsFontExists(string family) return exists; } + /// + /// Finds an already-registered family's representative by name - a + /// system font, an registration, or an earlier + /// registration (its first face, arbitrarily - this is used to satisfy an @font-face + /// src: local(...) candidate, which only needs *a* face to exist under that name, not a + /// specific one). Returns null if no family is registered under that exact name (mapping via + /// is deliberately not consulted here - local() refers to + /// an actual installed/registered font by its own name, not this renderer's not-found fallback). + /// + public RFontFamily TryGetExistingFamily(string family) + { + FontFamilyModel model; + if (!_existingFontFamilies.TryGetValue(family, out model)) + { + return null; + } + return model.SimpleFamily ?? (model.Faces.Count > 0 ? model.Faces[0].FontFamily : null); + } + /// /// Adds a font family to be used. /// @@ -85,12 +156,18 @@ public void AddFontFamily(RFontFamily fontFamily) { ArgChecker.AssertArgNotNull(fontFamily, "family"); - _existingFontFamilies[fontFamily.Name] = fontFamily; + FontFamilyModel model; + if (!_existingFontFamilies.TryGetValue(fontFamily.Name, out model)) + { + model = new FontFamilyModel { Name = fontFamily.Name }; + _existingFontFamilies[fontFamily.Name] = model; + } + model.SimpleFamily = fontFamily; } /// /// Adds a font mapping from to iff the is not found.
- /// When the font is used in rendered html and is not found in existing + /// When the font is used in rendered html and is not found in existing /// fonts (installed or added) it will be replaced by .
///
/// the font family to replace @@ -103,6 +180,47 @@ public void AddFontFamilyMapping(string fromFamily, string toFamily) _fontsMapping[fromFamily] = toFamily; } + /// + /// Registers one @font-face face under . Re-registering the + /// exact same axes+ranges combo (the orchestrator re-runs on every SetHtml) replaces the + /// prior entry rather than duplicating it, so repeated loads of the same document don't grow the + /// family's face list unbounded. + /// + /// the CSS family name declared by the @font-face rule + /// the loaded face, already registered with the platform text engine + /// CSS Fonts Level 4 numeric weight (1-1000) this face matches for + /// whether this face matches an italic/oblique request + /// CSS Fonts Level 3 stretch (1-9, matching OS/2 usWidthClass) this face matches for + /// the face's unicode-range restriction, or null for "covers whatever is asked of it" + public void AddFontFace(string familyName, RFontFamily fontFamily, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + ArgChecker.AssertArgNotNullOrEmpty(familyName, "familyName"); + ArgChecker.AssertArgNotNull(fontFamily, "fontFamily"); + + FontFamilyModel model; + if (!_existingFontFamilies.TryGetValue(familyName, out model)) + { + model = new FontFamilyModel { Name = familyName }; + _existingFontFamilies[familyName] = model; + } + + for (var i = model.Faces.Count - 1; i >= 0; i--) + { + if (IsSameFaceSlot(model.Faces[i], weight, isItalic, stretch, ranges)) + { + model.Faces.RemoveAt(i); + } + } + + model.Faces.Add(new FontFaceEntry(weight, isItalic, stretch, ranges, fontFamily)); + + // Faces changed under this family name - any font already resolved/cached against the old face + // set (legacy or @font-face path alike) could now be stale (a re-registration can change which + // face is nearest for a given request). + _fontsCache.Remove(familyName); + _faceFontsCache.Remove(familyName); + } + /// /// Get cached font instance for the given font properties.
/// Improve performance not to create same font multiple times. @@ -137,9 +255,231 @@ public RFont GetCachedFont(string family, double size, RFontStyle style) return font; } + /// + /// Get cached font instance for the given font properties, matching against any @font-face + /// faces registered for via using the CSS + /// Fonts Level 4 §5 nearest-match algorithm (). Falls through to + /// the legacy path when the family has no + /// registered faces (the common case - no @font-face for this family), so existing + /// system-font/ behavior is unaffected. + /// + /// the css font-family name + /// the font size + /// the box's cascaded style (its Italic/Underline/Strikeout bits are honored regardless of which face is chosen; its Bold bit is superseded by ) + /// CSS Fonts Level 4 numeric weight (1-1000) resolved from font-weight + /// CSS Fonts Level 3 stretch (1-9) resolved from font-stretch + /// the box's first non-whitespace character's codepoint, for unicode-range face disambiguation, or null to skip it + /// + /// the resolved font, or null when is given and no registered face + /// covers it (a real "this family can't render this character" outcome - see the unicode-range + /// scope note on - the caller falls back to the box's next + /// candidate/default font, matching today's existing "nothing matched" behavior) + /// + public RFont GetCachedFont(string family, double size, RFontStyle style, int weight, int stretch, int? codepoint) + { + FontFamilyModel model; + if (_existingFontFamilies.TryGetValue(family, out model) && model.Faces.Count > 0) + { + var requestedItalic = (style & RFontStyle.Italic) != 0; + + FontFaceEntry face; + if (TryFindNearestFace(model, weight, requestedItalic, stretch, codepoint, out face)) + { + return GetCachedFaceFont(family, face, size, style, weight, requestedItalic, stretch, codepoint ?? -1); + } + + if (codepoint != null) + { + // A codepoint-scoped miss must not substitute an arbitrary non-covering face - report + // it so the caller (CssBoxProperties.ActualFont) falls back to the box's next candidate + // family / CssConstants.DefaultFont, matching today's existing "nothing matched" behavior. + return null; + } + + // Codepoint-less miss shouldn't happen (AddFontFace always leaves >=1 face registered), but + // degrade to the legacy path rather than throw if it ever does. + } + + return GetCachedFont(family, size, style); + } + #region Private methods + /// + /// Resolves the chosen 's actual , computing the + /// residual - only the gap between what was requested and what was + /// actually found (e.g. faux-bold when no true bold face exists), plus the box's own + /// underline/strikeout bits, which apply regardless of which face was chosen - and caching by + /// every axis that could change the result. + /// + private RFont GetCachedFaceFont(string family, FontFaceEntry face, double size, RFontStyle requestedStyle, int requestedWeight, bool requestedItalic, int requestedStretch, int codepointKey) + { + Dictionary<(double, int, bool, int, int), RFont> familyCache; + if (!_faceFontsCache.TryGetValue(family, out familyCache)) + { + familyCache = new Dictionary<(double, int, bool, int, int), RFont>(); + _faceFontsCache[family] = familyCache; + } + + var key = (size, requestedWeight, requestedItalic, requestedStretch, codepointKey); + + RFont font; + if (familyCache.TryGetValue(key, out font)) + { + return font; + } + + // Every backend's face.FontFamily is a FAMILY handle, not distinct per-face content - WinForms' + // PrivateFontCollection and WPF's AddFontMemResourceEx-registered FontFamily both group every + // face of one family (Regular/Bold/Italic/BoldItalic) under one platform family object, and + // rely on the RFontStyle passed to CreateFont to pick the actual glyphs at that point (exactly + // like a plain installed font). So the requested style must always carry the CHOSEN face's own + // weight/italic bits (faceIsBold/face.Italic below), not just a synthesis gap - dropping them + // on an exact match would ask the platform for the wrong (regular) face despite having found + // and registered the real bold/italic one. + // + // Synthesis on top of that: the chosen face may still be a compromise (nearest-weight/slant + // match, not exact) - decide whether the gap is large enough that faux-bold/italic should also + // kick in, so e.g. "font-weight: bold" against a Regular-only family doesn't render with zero + // visual distinction. Threshold mirrors the common UA convention that weights >=600 read as + // "bold" and <600 don't. + var faceIsBold = face.Weight >= 600; + var mustSimulateBold = requestedWeight >= 600 && !faceIsBold; + var mustSimulateItalic = requestedItalic && !face.Italic; + + var residualStyle = requestedStyle & (RFontStyle.Underline | RFontStyle.Strikeout); + if (faceIsBold || mustSimulateBold) residualStyle |= RFontStyle.Bold; + if (face.Italic || mustSimulateItalic) residualStyle |= RFontStyle.Italic; + + font = _adapter.CreateFont(face.FontFamily, size, residualStyle); + familyCache[key] = font; + return font; + } + + /// + /// CSS Fonts Level 4 §5 face matching, direct port of PeachPDF's FontResolver.TryFindNearestFace. + /// When is supplied, only faces whose explicit unicode-range + /// covers it are candidates (a face with no declared range is treated as covering everything - see + /// the class-level unicode-range scope note; unlike PeachPDF, there is no cmap-coverage fallback + /// for an unlabeled face, since per-box single-font resolution has no per-glyph text-shaping layer + /// to exploit that precision). Otherwise every face is a candidate. Within the candidates it + /// narrows italic/slant first, then stretch, then weight; an exact axis match short-circuits. + /// Returns false when no candidate face qualifies. + /// + private static bool TryFindNearestFace(FontFamilyModel family, int weight, bool isItalic, int stretch, int? codepoint, out FontFaceEntry face) + { + face = null; + + List covering = codepoint is int cp + ? family.Faces.Where(f => FaceCovers(f, cp)).ToList() + : family.Faces; + + if (covering.Count == 0) + return false; + + var exact = covering.Where(f => f.Weight == weight && f.Italic == isItalic && f.Stretch == stretch).ToList(); + if (exact.Count > 0) + { + face = exact[exact.Count - 1]; + return true; + } + + var sameSlant = covering.Where(f => f.Italic == isItalic).ToList(); + var candidates = sameSlant.Count > 0 ? sameSlant : covering; + + var availableStretches = candidates.Select(f => f.Stretch).Distinct().ToList(); + var chosenStretch = PickNearestStretch(availableStretches, stretch); + var stretchCandidates = candidates.Where(f => f.Stretch == chosenStretch).ToList(); + + var availableWeights = stretchCandidates.Select(f => f.Weight).Distinct().ToList(); + var chosenWeight = PickNearestWeight(availableWeights, weight); + + face = stretchCandidates.Last(f => f.Weight == chosenWeight); + return true; + } + + /// + /// Whether is used for : inside its declared + /// unicode-range if it has one, else it covers everything (see the unicode-range scope note + /// on ). + /// + private static bool FaceCovers(FontFaceEntry entry, int codepoint) + { + return entry.ExplicitRanges == null || UnicodeRangeParser.Covers(entry.ExplicitRanges, codepoint); + } + + /// + /// CSS Fonts Level 4 §5.2's nearest-stretch search order: a target at or narrower than normal (5) + /// searches narrower first (down to 1), then wider; a target wider than normal searches wider + /// first (up to 9), then narrower. must be non-empty. + /// + private static int PickNearestStretch(List availableStretches, int target) + { + if (availableStretches.Contains(target)) + return target; + + IEnumerable candidates = target <= FontStretchResolver.Normal + ? availableStretches.Where(s => s < target).OrderByDescending(s => s) + .Concat(availableStretches.Where(s => s > target).OrderBy(s => s)) + : availableStretches.Where(s => s > target).OrderBy(s => s) + .Concat(availableStretches.Where(s => s < target).OrderByDescending(s => s)); + + return candidates.First(); + } + + /// + /// CSS Fonts Level 4 §5.2's nearest-weight search order (the standard browser algorithm): a target + /// in [400,500] searches upward to 500 first, then below the target, then above 500; a target + /// below 400 searches downward first, then upward; a target above 500 searches upward first, then + /// downward. must be non-empty and is assumed to NOT already + /// contain an exact match for (the caller checks that separately, since + /// an exact match also has to match the requested italic-ness, which this purely-numeric helper + /// doesn't know about). + /// + private static int PickNearestWeight(List availableWeights, int target) + { + IEnumerable candidates; + if (target >= 400 && target <= 500) + { + candidates = availableWeights.Where(w => w >= target && w <= 500).OrderBy(w => w) + .Concat(availableWeights.Where(w => w < target).OrderByDescending(w => w)) + .Concat(availableWeights.Where(w => w > 500).OrderBy(w => w)); + } + else if (target < 400) + { + candidates = availableWeights.Where(w => w < target).OrderByDescending(w => w) + .Concat(availableWeights.Where(w => w > target).OrderBy(w => w)); + } + else + { + candidates = availableWeights.Where(w => w > target).OrderBy(w => w) + .Concat(availableWeights.Where(w => w < target).OrderByDescending(w => w)); + } + + return candidates.First(); + } + + private static bool IsSameFaceSlot(FontFaceEntry entry, int weight, bool isItalic, int stretch, IReadOnlyList ranges) + { + return entry.Weight == weight && entry.Italic == isItalic && entry.Stretch == stretch + && RangesEqual(entry.ExplicitRanges, ranges); + } + + private static bool RangesEqual(IReadOnlyList a, IReadOnlyList b) + { + if (a == null || b == null) + return a == null && b == null; + if (a.Count != b.Count) + return false; + for (var i = 0; i < a.Count; i++) + { + if (a[i].Start != b[i].Start || a[i].End != b[i].End) + return false; + } + return true; + } + /// /// Get cached font if it exists in cache or null if it is not. /// @@ -175,22 +515,22 @@ private RFont TryGetFont(string family, double size, RFontStyle style) ///
private RFont CreateFont(string family, double size, RFontStyle style) { - RFontFamily fontFamily; + FontFamilyModel model; try { - return _existingFontFamilies.TryGetValue(family, out fontFamily) - ? _adapter.CreateFont(fontFamily, size, style) + return _existingFontFamilies.TryGetValue(family, out model) && model.SimpleFamily != null + ? _adapter.CreateFont(model.SimpleFamily, size, style) : _adapter.CreateFont(family, size, style); } catch { // handle possibility of no requested style exists for the font, use regular then - return _existingFontFamilies.TryGetValue(family, out fontFamily) - ? _adapter.CreateFont(fontFamily, size, RFontStyle.Regular) + return _existingFontFamilies.TryGetValue(family, out model) && model.SimpleFamily != null + ? _adapter.CreateFont(model.SimpleFamily, size, RFontStyle.Regular) : _adapter.CreateFont(family, size, RFontStyle.Regular); } } #endregion } -} \ No newline at end of file +} diff --git a/Source/HtmlRenderer/Core/Network/EmbeddedResourceNetworkLoader.cs b/Source/HtmlRenderer/Core/Network/EmbeddedResourceNetworkLoader.cs new file mode 100644 index 000000000..980345888 --- /dev/null +++ b/Source/HtmlRenderer/Core/Network/EmbeddedResourceNetworkLoader.cs @@ -0,0 +1,125 @@ +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace TheArtOfDev.HtmlRenderer.Core.Network +{ + /// + /// Resolves embedded://<assemblySimpleName>/<manifestResourceName> URIs against a + /// loaded .NET assembly's own embedded resources (). + /// The assembly to search is named directly in the URI's authority rather than configured on the + /// loader instance, so - like data:/file: - this always resolves the same way regardless + /// of which is configured on : + /// intercepts this scheme unconditionally, the same + /// way it already does for data:/file:. + /// + /// + /// Useful for a library/app that ships its own bundled resources (fonts, images, stylesheets) as + /// embedded resources and wants @font-face src: url(...)/<img src>/etc. to + /// reference them directly, without a custom of its own. + /// + public sealed class EmbeddedResourceNetworkLoader : RNetworkLoader + { + public const string Scheme = "embedded"; + + private readonly RUri? _primaryContentsUri; + + /// + /// Creates a loader with no primary document - it only resolves embedded: URIs it's asked + /// for via (which is how 's internal, + /// always-on instance of this loader is used). is not supported on + /// an instance created this way — pass the HTML directly instead, or use the other constructor. + /// + public EmbeddedResourceNetworkLoader() + { + } + + /// + /// Creates a loader whose root document is the embedded resource named + /// in , and whose base URI is + /// that resource's own embedded: URI (so relative references resolve against it). Pass + /// null as the HTML argument when rendering to render this resource. + /// + public EmbeddedResourceNetworkLoader(Assembly assembly, string manifestResourceName) + { + _primaryContentsUri = BuildUri(assembly, manifestResourceName); + } + + /// Builds an embedded: URI for in . + public static RUri BuildUri(Assembly assembly, string manifestResourceName) + { + if (assembly is null) throw new ArgumentNullException(nameof(assembly)); + if (manifestResourceName is null) throw new ArgumentNullException(nameof(manifestResourceName)); + + return new RUri($"{Scheme}://{assembly.GetName().Name}/{manifestResourceName}"); + } + + /// + public override RUri? BaseUri => _primaryContentsUri; + + /// + public override async Task GetPrimaryContents() + { + if (_primaryContentsUri is null) + { + throw new InvalidOperationException( + "This EmbeddedResourceNetworkLoader was created without a primary resource; construct it with an assembly and manifest resource name to load the root document, or pass the HTML directly."); + } + + var networkResponse = await GetResourceStream(_primaryContentsUri).ConfigureAwait(false); + + if (networkResponse?.ResourceStream is null) + { + throw new InvalidOperationException("Primary contents stream is null."); + } + + using (var reader = new StreamReader(networkResponse.ResourceStream)) + { + return await reader.ReadToEndAsync().ConfigureAwait(false); + } + } + + /// + public override Task GetResourceStream(RUri uri) + { + var assemblyName = uri.Uri.Host; + var resourceName = uri.Uri.AbsolutePath.TrimStart('/'); + + var assembly = FindOrLoadAssembly(assemblyName); + var stream = assembly?.GetManifestResourceStream(resourceName); + + return Task.FromResult(stream != null ? new RNetworkResponse(stream, null) : null); + } + + /// + /// Finds among already-loaded assemblies first (cheap, and the + /// common case - the assembly that built this URI via is, by construction, + /// already loaded), falling back to - which actively + /// resolves and loads the assembly from the app's own probing paths - for the case this URI came + /// from parsed CSS/HTML text (e.g. an @font-face src: url(embedded://...) written directly + /// into a bundled sample) rather than , where nothing else in the calling + /// process may have referenced a type from that assembly yet to trigger .NET's normal lazy load. + /// + private static Assembly? FindOrLoadAssembly(string assemblyName) + { + var loaded = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)); + if (loaded != null) + { + return loaded; + } + + try + { + return Assembly.Load(new AssemblyName(assemblyName)); + } + catch + { + return null; + } + } + } +} diff --git a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs index 49780b3e2..46e3d8e6c 100644 --- a/Source/HtmlRenderer/Core/Parse/CssValueParser.cs +++ b/Source/HtmlRenderer/Core/Parse/CssValueParser.cs @@ -143,6 +143,75 @@ internal static List GetCssTokens(string value, bool isInValue = false) return tokens; } + /// + /// Resolves an @font-face rule's font-family descriptor value to the plain family + /// name: a single quoted string token unwraps to its literal text, anything else (an unquoted + /// identifier, or a value this tokenizer doesn't recognize as exactly one string) is returned as-is. + /// + internal static string GetFontFaceFamilyName(string propValue) + { + var tokens = GetCssTokens(propValue); + + if (tokens.Count == 1 && tokens[0] is StringToken stringToken) + { + return stringToken.Data; + } + + return propValue; + } + + /// + /// Parses an @font-face src descriptor into its comma-separated candidates, each with + /// whatever url()/local()/format()/tech() pieces it declared - order + /// matters here: the top-level commas are split first, then each candidate segment is inspected on + /// its own, so a value with multiple url() alternatives (src: url(a.woff2) format("woff2"), + /// url(a.woff) format("woff")) doesn't have its tokens cross-matched between candidates. + /// + internal static List GetFontFacePropertyValue(string propValue) + { + var tokens = GetCssTokens(propValue); + var result = new List(); + var segment = new List(); + + foreach (var token in tokens) + { + if (token.Type == TokenType.Comma) + { + AppendFontFaceCandidate(result, segment); + segment = new List(); + } + else + { + segment.Add(token); + } + } + AppendFontFaceCandidate(result, segment); + + return result; + } + + /// + /// Builds one candidate from a comma-delimited segment of an src + /// descriptor's tokens and appends it to , or does nothing for an empty + /// segment (a trailing/doubled comma). + /// + private static void AppendFontFaceCandidate(List result, List segment) + { + if (segment.Count == 0) + return; + + var urlToken = segment.OfType().SingleOrDefault(); + var formatToken = segment.OfType().SingleOrDefault(t => t.Data == "format"); + var techToken = segment.OfType().SingleOrDefault(t => t.Data == "tech"); + var localToken = segment.OfType().SingleOrDefault(t => t.Data == "local"); + + result.Add(new CssFontFace( + urlToken?.Data, + formatToken?.ArgumentTokens?.FirstOrDefault()?.Data, + techToken?.ArgumentTokens?.FirstOrDefault()?.Data, + localToken?.ArgumentTokens?.FirstOrDefault()?.Data)); + } + /// /// Recognizes a length string that is a single calc-family (calc/min/max/clamp) function call. /// Real grammar/type validation happens in the vendored CSS-OM's CalcValueConverter at parse diff --git a/Source/HtmlRenderer/Core/Parse/DomParser.cs b/Source/HtmlRenderer/Core/Parse/DomParser.cs index fc6664124..66287d5a1 100644 --- a/Source/HtmlRenderer/Core/Parse/DomParser.cs +++ b/Source/HtmlRenderer/Core/Parse/DomParser.cs @@ -21,6 +21,7 @@ using TheArtOfDev.HtmlRenderer.Core.Dom; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Core.Handlers; +using TheArtOfDev.HtmlRenderer.Core.Network; using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.Core.Parse @@ -66,6 +67,8 @@ public DomParser(CssParser cssParser) (cssData, _) = await CascadeParseStyles(root, htmlContainer, cssData, false).ConfigureAwait(false); + await RegisterFontFaces(cssData, htmlContainer).ConfigureAwait(false); + // The device @media queries are evaluated against: media type and colour scheme come // from the platform adapter, the viewport from the container's max size (which is the // available width/height at this point; zero means "not known yet", and geometry @@ -141,6 +144,60 @@ public DomParser(CssParser cssParser) return (cssData, cssDataChanged); } + /// + /// Walks every parsed stylesheet's @font-face rules (including ones nested inside + /// @media/@supports/@layer/@container - see ) + /// and registers each one's src candidates with / + /// , in source order, stopping at the + /// first candidate that loads successfully - matching the CSS spec's own "try each src in turn" + /// behavior. Called once per , after stylesheets are collected and + /// before the cascade applies, so every box's can + /// already see the registered faces. + /// + private static async Task RegisterFontFaces(CssData cssData, HtmlContainerInt htmlContainer) + { + var adapter = htmlContainer.Adapter; + + foreach (var stylesheet in cssData.Stylesheets) + { + var stylesheetBaseUri = stylesheet.BaseUri != null ? new RUri(stylesheet.BaseUri) : null; + + foreach (var rule in CssData.FlattenRules(stylesheet.Rules).OfType()) + { + var familyName = CssValueParser.GetFontFaceFamilyName(rule.Family ?? string.Empty); + if (string.IsNullOrWhiteSpace(familyName)) + continue; + + var weight = FontFaceDescriptorResolver.ResolveWeight(rule.Weight) ?? CssFontWeightResolver.Normal; + var isItalic = FontFaceDescriptorResolver.ResolveIsItalic(rule.Style) ?? false; + var stretch = FontFaceDescriptorResolver.ResolveStretch(rule.Stretch) ?? FontStretchResolver.Normal; + var ranges = UnicodeRangeParser.Parse(rule.Range); + + foreach (var candidate in CssValueParser.GetFontFacePropertyValue(rule.Source ?? string.Empty)) + { + bool loaded; + if (!string.IsNullOrEmpty(candidate.Local)) + { + var localFamilyName = CssValueParser.GetFontFaceFamilyName(candidate.Local); + loaded = adapter.AddFontFaceFromLocalFamily(familyName, localFamilyName, weight, isItalic, stretch, ranges); + } + else if (!string.IsNullOrEmpty(candidate.Url)) + { + var uri = CommonUtils.ResolveAgainstDocumentBase(htmlContainer, candidate.Url, stylesheetBaseUri); + loaded = uri != null && await adapter.AddFontFace(familyName, uri, weight, isItalic, stretch, ranges).ConfigureAwait(false); + } + else + { + continue; + } + + if (loaded) + break; + } + } + } + } + /// /// Applies style to all boxes in the tree via a spec-correct, origin-aware six-phase cascade: @@ -778,7 +835,7 @@ private void SetTextSelectionStyle(HtmlContainerInt htmlContainer, CssData cssDa foreach (var stylesheet in cssData.Stylesheets) { - foreach (var rule in CssData.FlattenStyleRules(stylesheet.Rules)) + foreach (var rule in CssData.FlattenRules(stylesheet.Rules).OfType()) { if (!SelectorIsSelectionPseudoElement(rule.Selector)) continue; diff --git a/Source/HtmlRenderer/Core/Utils/CssConstants.cs b/Source/HtmlRenderer/Core/Utils/CssConstants.cs index 17f0720bf..06fe0aa02 100644 --- a/Source/HtmlRenderer/Core/Utils/CssConstants.cs +++ b/Source/HtmlRenderer/Core/Utils/CssConstants.cs @@ -34,6 +34,14 @@ internal static class CssConstants public const string KeepAll = "keep-all"; public const string Center = "center"; public const string Collapse = "collapse"; + public const string UltraCondensed = "ultra-condensed"; + public const string ExtraCondensed = "extra-condensed"; + public const string Condensed = "condensed"; + public const string SemiCondensed = "semi-condensed"; + public const string SemiExpanded = "semi-expanded"; + public const string Expanded = "expanded"; + public const string ExtraExpanded = "extra-expanded"; + public const string UltraExpanded = "ultra-expanded"; public const string ContentBox = "content-box"; public const string BorderBox = "border-box"; public const string Cursive = "cursive"; diff --git a/Source/HtmlRenderer/Core/Utils/CssFontWeightResolver.cs b/Source/HtmlRenderer/Core/Utils/CssFontWeightResolver.cs new file mode 100644 index 000000000..dfc799a6a --- /dev/null +++ b/Source/HtmlRenderer/Core/Utils/CssFontWeightResolver.cs @@ -0,0 +1,48 @@ +using System; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Resolves a box's cascaded font-weight value (normal/bold/bolder/ + /// lighter/a number) to a concrete CSS Fonts Level 4 numeric weight (1-1000), for + /// 's face-matching call into + /// . + /// + internal static class CssFontWeightResolver + { + internal const int Normal = 400; + internal const int Bold = 700; + + /// + /// bolder/lighter are relative to the parent's resolved weight; true CSS2.1 defines + /// this via a lookup table over 9 named weight steps, not a fixed offset. A flat +/-300 clamped to + /// [1,1000] is a real, explicitly-simplified approximation of that table - close enough to move a + /// normal-weight (400) parent to bold (700) and back, without reproducing the full step table for + /// a CSS feature real-world content rarely nests more than one level deep. + /// + private const int RelativeStep = 300; + + internal static int Resolve(string fontWeightValue, int parentWeight) + { + if (string.IsNullOrWhiteSpace(fontWeightValue)) + return Normal; + + switch (fontWeightValue) + { + case CssConstants.Normal: + case CssConstants.Inherit: + return Normal; + case CssConstants.Bold: + return Bold; + case CssConstants.Bolder: + return Clamp(parentWeight + RelativeStep); + case CssConstants.Lighter: + return Clamp(parentWeight - RelativeStep); + default: + return int.TryParse(fontWeightValue, out var numeric) ? Clamp(numeric) : Normal; + } + } + + private static int Clamp(int weight) => Math.Max(1, Math.Min(1000, weight)); + } +} diff --git a/Source/HtmlRenderer/Core/Utils/CssUtils.cs b/Source/HtmlRenderer/Core/Utils/CssUtils.cs index ba216d514..df9fe7dc7 100644 --- a/Source/HtmlRenderer/Core/Utils/CssUtils.cs +++ b/Source/HtmlRenderer/Core/Utils/CssUtils.cs @@ -217,6 +217,8 @@ public static string GetPropertyValue(CssBox cssBox, string propName) return cssBox.FontVariant; case "font-weight": return cssBox.FontWeight; + case "font-stretch": + return cssBox.FontStretch; case "list-style": return cssBox.ListStyle; case "list-style-position": @@ -434,6 +436,9 @@ public static void SetPropertyValue(CssBox cssBox, string propName, string value case "font-weight": cssBox.FontWeight = value; break; + case "font-stretch": + cssBox.FontStretch = value; + break; case "list-style": cssBox.ListStyle = value; break; diff --git a/Source/HtmlRenderer/Core/Utils/DomUtils.cs b/Source/HtmlRenderer/Core/Utils/DomUtils.cs index 454ecdb3f..5fa75ca74 100644 --- a/Source/HtmlRenderer/Core/Utils/DomUtils.cs +++ b/Source/HtmlRenderer/Core/Utils/DomUtils.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core.CssEngine; @@ -1143,7 +1144,7 @@ private static void CollectSimpleSelectorStyles(CssData cssData, Func()) { if (!selectorMatches(rule.Selector)) continue; foreach (var prop in rule.Style) @@ -1182,7 +1183,7 @@ private static void WriteStylesheet(StringBuilder sb, CssData cssData) sb.AppendLine("