@@ -188,6 +188,25 @@ public bool IsBlock
188188 get { return Display == CssConstants . Block ; }
189189 }
190190
191+ /// <summary>
192+ /// Is this box floated (float: left or float: right).
193+ /// </summary>
194+ public bool IsFloated
195+ {
196+ get { return Float == CssConstants . Left || Float == CssConstants . Right ; }
197+ }
198+
199+ /// <summary>
200+ /// True for a box removed from normal flow: floated, or absolutely/fixed positioned. Ported
201+ /// from PeachPDF's CssBox.IsOutOfFlow - used to find a container's last IN-FLOW child (e.g.
202+ /// <see cref="MarginBottomCollapse"/>), since an out-of-flow box doesn't contribute to a
203+ /// non-BFC-establishing container's own auto-height (CSS 2.1 10.6.3/10.6.7).
204+ /// </summary>
205+ public bool IsOutOfFlow
206+ {
207+ get { return IsFloated || Position == CssConstants . Absolute || Position == CssConstants . Fixed ; }
208+ }
209+
191210 /// <summary>
192211 /// Is the css box clickable (by default only "a" elements that are actual hyperlinks - i.e.
193212 /// have an "href" - are clickable; an "a" used only as a named anchor/target has no href and
@@ -633,6 +652,90 @@ public virtual void Dispose()
633652
634653 #region Private Methods
635654
655+ /// <summary>
656+ /// Computes this box's used height, ported from PeachPDF's CssLayoutEngine.GetBoxHeight -
657+ /// scoped to the height features this engine supports (no aspect-ratio, no absolutely-
658+ /// positioned top+bottom fill, no per-page height: PeachPDF features this engine never had).
659+ /// PeachPDF's Size.Height is content-only, so its GetBoxHeight starts from
660+ /// ActualBoxSizingHeight (Size.Height + padding/border) as the auto-height baseline and can
661+ /// return null for an indeterminate percentage height, leaving ApplyHeight's Math.Max to
662+ /// preserve whatever content-driven value is already there. This engine's Size.Height is
663+ /// already the border-box measure (see <see cref="ActualRight"/>), so the equivalent baseline
664+ /// is Size.Height itself, and there is no null case to thread through - an indeterminate
665+ /// percentage height simply falls back to that same baseline directly.
666+ /// </summary>
667+ private double GetBoxHeight ( )
668+ {
669+ double height = Size . Height ;
670+
671+ if ( CssValueParser . IsValidLength ( Height ) && ! ( Height . EndsWith ( "%" ) && ! ContainingBlock . IsHeightCalculated ) )
672+ {
673+ height = CssValueParser . ParseLength ( Height , ContainingBlock . Size . Height , this ) + ActualBoxSizeIncludedHeight ;
674+ }
675+
676+ if ( CssValueParser . IsValidLength ( MinHeight ) && ( ContainingBlock . IsHeightCalculated || ! MinHeight . EndsWith ( "%" ) ) )
677+ {
678+ var minHeight = CssValueParser . ParseLength ( MinHeight , ContainingBlock . Size . Height , this ) + ActualBoxSizeIncludedHeight ;
679+ if ( minHeight > height )
680+ {
681+ height = minHeight ;
682+ }
683+ }
684+
685+ return height ;
686+ }
687+
688+ /// <summary>
689+ /// Applies this box's used height (CSS 2.1 10.5/10.6.3/10.7) to <see cref="CssBoxProperties.ActualBottom"/>,
690+ /// and computes whether this box counts as height-calculated for a descendant's percentage
691+ /// height resolution. Ported from PeachPDF's CssLayoutEngine.ApplyHeight.
692+ /// <para>
693+ /// The assignment below is unconditional (not the plain <c>Math.Max(ActualBottom, ...)</c>
694+ /// this replaced) - a no-op for auto height, since <see cref="GetBoxHeight"/>'s baseline is
695+ /// already ActualBottom's own current value, but a real correction for a definite height,
696+ /// able to shrink ActualBottom back down, which Math.Max alone never could. That correction is
697+ /// what fixes a real bug this exposed: <see cref="CssLayoutEngine.FlowBox"/>'s own "handle
698+ /// height setting" step (for a nested atomic inline-block) pushes a block's own maxBottom out
699+ /// to the block's full ActualHeight measured from its content top, and CreateLineBoxes then
700+ /// adds that same block's bottom padding/border again on top - inflating ActualBottom by one
701+ /// extra padding+border box for any floated or block-level box with both an explicit height
702+ /// and non-zero padding/border (e.g. every floated box in the W3C ACID1 test). A definite
703+ /// height here authoritatively overwrites that inflated value instead of merely preserving it.
704+ /// </para>
705+ /// </summary>
706+ private void ApplyHeight ( )
707+ {
708+ var height = GetBoxHeight ( ) ;
709+ ActualBottom = Location . Y + height ;
710+
711+ var isRootWithPageHeight = ParentBox == null && HtmlContainer != null ;
712+ var isDefiniteHeight = CssValueParser . IsValidLength ( Height ) && ( ContainingBlock . IsHeightCalculated || ! Height . EndsWith ( "%" ) ) ;
713+ IsHeightCalculated = isRootWithPageHeight || isDefiniteHeight ;
714+
715+ if ( CssValueParser . IsValidLength ( MaxHeight ) && MaxHeight != CssConstants . None
716+ && ( ContainingBlock . IsHeightCalculated || ! MaxHeight . EndsWith ( "%" ) ) )
717+ {
718+ var maxHeight = CssValueParser . ParseLength ( MaxHeight , ContainingBlock . Size . Height , this ) + ActualBoxSizeIncludedHeight ;
719+ var maxBottom = Location . Y + maxHeight ;
720+
721+ if ( ActualBottom > maxBottom )
722+ {
723+ ActualBottom = maxBottom ;
724+
725+ if ( CssValueParser . IsValidLength ( MinHeight ) && ( ContainingBlock . IsHeightCalculated || ! MinHeight . EndsWith ( "%" ) ) )
726+ {
727+ var minHeight = CssValueParser . ParseLength ( MinHeight , ContainingBlock . Size . Height , this ) + ActualBoxSizeIncludedHeight ;
728+ var minBottom = Location . Y + minHeight ;
729+
730+ if ( ActualBottom < minBottom )
731+ {
732+ ActualBottom = minBottom ;
733+ }
734+ }
735+ }
736+ }
737+ }
738+
636739 /// <summary>
637740 /// Measures the bounds of box and children, recursively.<br/>
638741 /// Performs layout of the DOM structure creating lines by set bounds restrictions.<br/>
@@ -651,19 +754,44 @@ protected virtual void PerformLayoutImp(RGraphics g)
651754 // Because their width and height are set by CssTable
652755 if ( Display != CssConstants . TableCell && Display != CssConstants . Table )
653756 {
654- double width = ContainingBlock . Size . Width
757+ double availableWidth = ContainingBlock . Size . Width
655758 - ContainingBlock . ActualPaddingLeft - ContainingBlock . ActualPaddingRight
656759 - ContainingBlock . ActualBorderLeftWidth - ContainingBlock . ActualBorderRightWidth ;
760+ double width = availableWidth ;
761+ bool explicitWidth = Width != CssConstants . Auto && ! string . IsNullOrEmpty ( Width ) ;
657762
658- if ( Width != CssConstants . Auto && ! string . IsNullOrEmpty ( Width ) )
763+ if ( explicitWidth )
659764 {
660- width = CssValueParser . ParseLength ( Width , width , this ) ;
765+ // CSS 2.1 10.2: a percentage/length `width` resolves against the containing block's
766+ // content width. Per CSS Box Sizing 3, that resolved value is this box's CONTENT
767+ // width under the default `box-sizing: content-box` - its border-box width (what
768+ // Size.Width holds throughout this engine) additionally includes this box's own
769+ // padding+border (ActualBoxSizeIncludedWidth is 0 for `box-sizing: border-box`,
770+ // where the resolved value already IS the border-box width).
771+ width = CssValueParser . ParseLength ( Width , availableWidth , this ) + ActualBoxSizeIncludedWidth ;
772+ }
773+ else if ( IsFloated )
774+ {
775+ // CSS 2.1 10.3.5: a floated box with width:auto shrinks to fit its content
776+ // instead of taking the full containing-block width like an ordinary block.
777+ // GetMinMaxWidth already returns border-box-inclusive bounds (its own padding/
778+ // border baked in), so no box-sizing adjustment is needed here.
779+ double minWidth , maxWidth ;
780+ GetMinMaxWidth ( out minWidth , out maxWidth ) ;
781+ width = Math . Min ( Math . Max ( minWidth , width ) , maxWidth ) ;
661782 }
662783
663784 Size = new RSize ( width , Size . Height ) ;
664785
665- // must be separate because the margin can be calculated by percentage of the width
666- Size = new RSize ( width - ActualMarginLeft - ActualMarginRight , Size . Height ) ;
786+ if ( ! explicitWidth && ! IsFloated )
787+ {
788+ // CSS 2.1 10.3.3: only a plain (non-floated) width:auto block absorbs its own
789+ // margin into the available fill width to become its used (border-box) width - an
790+ // explicit width, or a floated shrink-to-fit width, is the used width as-is; margin
791+ // is separate space outside the border box (already accounted for via this box's
792+ // Location.X), not a further reduction of Size.Width on top of that.
793+ Size = new RSize ( width - ActualMarginLeft - ActualMarginRight , Size . Height ) ;
794+ }
667795 }
668796
669797 if ( Display != CssConstants . TableCell )
@@ -683,6 +811,10 @@ protected virtual void PerformLayoutImp(RGraphics g)
683811 top = ( prevSibling == null && ParentBox != null ? ParentBox . ClientTop : ParentBox == null ? Location . Y : 0 ) + MarginTopCollapse ( prevSibling ) + ( prevSibling != null ? prevSibling . ActualBottom + prevSibling . ActualBorderBottomWidth : 0 ) ;
684812 Location = new RPoint ( left , top ) ;
685813 ActualBottom = top ;
814+
815+ // static position committed above; float/clear now overwrite Location using that
816+ // static position as input. No-op for boxes that are neither floated nor clearing.
817+ CssLayoutEngine . FloatBox ( this ) ;
686818 }
687819 }
688820
@@ -706,7 +838,19 @@ protected virtual void PerformLayoutImp(RGraphics g)
706838 childBox . PerformLayout ( g ) ;
707839 }
708840 ActualRight = CalculateActualRight ( ) ;
709- ActualBottom = MarginBottomCollapse ( ) ;
841+
842+ // CSS 2.1 10.6.3/10.6.7: a plain (non-BFC-establishing) box's own auto-height
843+ // doesn't contribute from out-of-flow (floated/absolutely-positioned) children
844+ // at all - if EVERY child is out-of-flow (e.g. a <ul> whose only children are
845+ // floated <li>s), there is no in-flow content to measure, so ActualBottom is
846+ // left exactly as the static-position commit above already set it (its own
847+ // top, i.e. zero content height) rather than being pulled down to match a
848+ // floated child's position. Ported from PeachPDF's CssBox.PerformLayoutImp
849+ // (`if (Boxes.Any(b => !b.IsOutOfFlow)) { ActualBottom = MarginBottomCollapse(); }`).
850+ if ( Boxes . Exists ( b => ! b . IsOutOfFlow ) )
851+ {
852+ ActualBottom = MarginBottomCollapse ( ) ;
853+ }
710854 }
711855 }
712856 }
@@ -720,7 +864,7 @@ protected virtual void PerformLayoutImp(RGraphics g)
720864 ActualBottom = prevSibling . ActualBottom ;
721865 }
722866 }
723- ActualBottom = Math . Max ( ActualBottom , Location . Y + ActualHeight ) ;
867+ ApplyHeight ( ) ;
724868
725869 CreateListItemBox ( g ) ;
726870
@@ -1193,13 +1337,20 @@ private double CalculateActualRight()
11931337 /// <returns>Resulting bottom margin</returns>
11941338 private double MarginBottomCollapse ( )
11951339 {
1340+ // Only called when at least one in-flow child exists (see the caller's IsOutOfFlow gate),
1341+ // so this is guaranteed to find one - an out-of-flow (floated/absolutely-positioned) child
1342+ // doesn't contribute to this box's own auto-height (CSS 2.1 10.6.3/10.6.7) and must not be
1343+ // used as "the last child" here, matching PeachPDF's own MarginBottomCollapse
1344+ // (`Boxes.Last(b => !b.IsOutOfFlow)`).
1345+ var lastInFlowBox = _boxes . FindLast ( b => ! b . IsOutOfFlow ) ;
1346+
11961347 double margin = 0 ;
11971348 if ( ParentBox != null && ParentBox . Boxes . IndexOf ( this ) == ParentBox . Boxes . Count - 1 && _parentBox . ActualMarginBottom < 0.1 )
11981349 {
1199- var lastChildBottomMargin = _boxes [ _boxes . Count - 1 ] . ActualMarginBottom ;
1350+ var lastChildBottomMargin = lastInFlowBox . ActualMarginBottom ;
12001351 margin = Height == "auto" ? Math . Max ( ActualMarginBottom , lastChildBottomMargin ) : lastChildBottomMargin ;
12011352 }
1202- return Math . Max ( ActualBottom , _boxes [ _boxes . Count - 1 ] . ActualBottom + margin + ActualPaddingBottom + ActualBorderBottomWidth ) ;
1353+ return Math . Max ( ActualBottom , lastInFlowBox . ActualBottom + margin + ActualPaddingBottom + ActualBorderBottomWidth ) ;
12031354 }
12041355
12051356 /// <summary>
@@ -1468,7 +1619,7 @@ protected void PaintDecoration(RGraphics g, RRect rectangle, bool isFirst, bool
14681619
14691620 // text-decoration-line may list several space-separated line keywords (e.g. "underline
14701621 // overline") - draw each one present rather than only the first/only value.
1471- foreach ( var line in TextDecoration . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries ) )
1622+ foreach ( var line in TextDecoration . Split ( new [ ] { ' ' } , StringSplitOptions . RemoveEmptyEntries ) )
14721623 {
14731624 double y ;
14741625 if ( line == CssConstants . Underline )
0 commit comments