1. Formalizing the Problem

The specification of side-floats is in part taken from the CSS2 recommendation and adapted to XSL-FO. It may be useful to summarize and reformulate the spec to have a precise understanding of how side-floats should be handled.

An fo:float element with the "float" property set to "start" or "end":

To formalize the rules of placement of side-floats, we will set up the following coordinate system:

In such a coordinate-system, a side-float area is characterized by the (x,y) coordinates of its "start-before point", that is, the point of its allocation-rectangle (or border-rectangle, or padding-rectangle, or content-rectangle) which is at the intersection of the start-edge and before-edge.

1.1. Rules for Placing Side-floats

Some definitions:

The nine rules given in the description of the "float" property may then be reformulated like the following:

  1. for a start-float, x ≥ 0

    for an end-float, x + ipd ≤ ipdref-area

  2. ∀ start-float' ≺ start-float, x > x' + ipd' or y > y' + bpd' ∀ end-float' ≺ end-float, x + ipd < x' or y > y' + bpd'

  3. ∀ start-float s, ∀ end-float e, [ys, ys + bpds] ∩ [ye, ye + bpde] ≠ ∅ ⇒ xs + ipds < xe

  4. y ≥ 0
  5. ∀ side-float' ≺ side-float, y ≥ y'

    ∀ block-area ≺ side-float, yside-float ≥ yblock-area, where yblock-area is the y-coordinate of the before-edge of the block-area's border-rectangle minus the block-area's space-before(.optimum?)

  6. ∀ line-area ≺ start-float, y ≥ y[before-edge of the line-area's allocation-rectangle]
  7. for a start-float, ipd ≤ ipdref-area ⇒ x + ipd ≤ ipdref-area for an end-float, ipd ≤ ipdref-area ⇒ x ≥ 0

  8. y should be minimized
  9. x should be minimized
  10. if "clear" = "left", "start", "both", ∀ start-float' ≺ side-float, y > y' + bpd' if "clear" = "right", "end", "both", ∀ end-float' ≺ side-float, y > y' + bpd'

Here's a pdf version for those whose browser has difficulties displaying UTF-8 characters.

1.2. Uncertainties

At the beginning of section 9.5 of the CSS2 recommendation, it is loosely explained how floats should be placed, and it is said that the precise placement rules should be found in the description of the "float" property.

Two hints are given at this place, of which only one may be retrieved in the precise rules:

This last point may occur in XSL-FO if the generated anchor area must be a block-area. AFAICT there is no rule for it in the description of the "float" property. Rule 5 would even let think that the top of the floated box should be aligned with the top of the previous block box. Moreover, in the CSS 2.1 candidate recommendation this sentence doesn't appear anymore.

That said, I think the behavior that most users would expect is to align the float with the bottom of the previous (non-floating) block box. This also is what is implemented by all the browsers I've tested (Konqueror, Safari, Firefox). So I think we may implement it like that too.

Another uncertainty is caused by the following sentence of section 9.5: "If there isn't enough horizontal room on the current line for the float, it is shifted downward, line by line, until a line has room for it." And what if the height of the float is less than the line-height? Should it still be shifted one entire line downward? This sentence is in contradiction with rule 8 (y should be minimized). In CSS 2.1 it has been rephrased such that "line by line" doesn't appear anymore. So I think we may go with CSS 2.1.

Finally, may side-floats be split on several pages? There is nothing about that in the XSL-FO spec. The chapter 13, "Page Media" of the CSS2 recommendation gives a small hint in section 13.3.6: we should "avoid breaking inside a floated element". If one uses side-floats to have margin notes, one can reasonably expect them to be split on several pages. Therefore, if it is simple enough to implement side-float breaking, I'll do it; if it involves too many changes or a too complicated algorithm, I'll leave it for now (note that Xep does not break side-floats, even if they are too big to fit on a page; they are simply discarded).

1.3. Computing Intrusion-adjustments

1.3.1. Definitions

Unless otherwise noted, the coordinates are those of the "start-before" vertex of the allocation-rectangle.

1.3.2. Computation Rules

Let F be a formatting object to which the "intrusion-displace" property applies. Then:

I'll first implement the "line" value, as this is probably the most expected by users. I'll forget "block" and "indent" for now if after a quick look they turn out to be too complicated to implement.

2. An Algorithm for Placing Side-floats

Have you ever played with Legotm? Well, here's an opportunity.

We will use here the relative terms for area placement: before, after, start, end. As a convenience, we will use the words "beforer" (higher in lr-tb writing-mode), "beforest" (highest), "starter" (more on the left than), "startest" (the most on the left), etc., for comparing the placement of areas between each other.

We consider the common ancestor reference-area as a containing box in which bricks have to be placed. There is a jagged line (green, below) which will serve as a guide for placing floats. This jagged line will be made, among others, of three guide lines. The "before-guide" will correspond to:

There will also be one "start-guide", representing the afterest after-edge of start-floats, and one "end-guide", for the afterest after-edge of end-floats.

Now let's suppose we want to place a new start-float; the idea is to place it at the after-end corner of the reference-area. Then we pull it toward the before-edge until it touches one of the guide lines.

While continuing to pull towards the before, we will start pulling also towards the start; the float will progress along the jagged line until it is blocked; it has reached its final placement. The guide lines are updated as should be.

Now we will place an end-float; there is no more place on the current line, so the float will be placed after the last placed start-float:

Note that the before-guide no longer corresponds to the current line-area, as the last float has been placed afterer. Note also that there is a hole below the third end-float from the right: its after-edge no longer serves for the guide as there is an end-float placed to its start (left), whose after-edge is afterer. (This was the same case for the first start-float.)

Let's place a new start-float:

And then a new end-float:

Side-floats are boxes which may allow some shrinking/stretching in the block-progression-dimension. Me may take advantage of this possibility to place floats which otherwise wouldn't fit on the page. On the figure below, the second start-float has the "clear" property set to "start":

2.1. Properties of the Model

By nature, this model should respect all of the rules for side-floats placement:

  1. A start-float starts at the end-edge and is pulled to the start until it touches the start-edge of the ref-area's content-rectangle. So this rule is always followed. Same for end-floats.
  2. The guiding jagged line is designed to follow the afterest and endest edges of start-floats, and the afterest and startest edges of end-floats. So the area into which a start-float may evolve is such that all already placed start-floats are to its start or to its before. (That's why there may be holes above the start-guide or the end-guide, as showed above.)
  3. The model ensures that side-floats don't inter-penetrate each other.
  4. As for 1., the before-edge of the ref-area is the upper limit.
  5. This is ensured by the way the before-guide is computed.
  6. idem
  7. a start-float starts its movement at the end-edge of the ref-area, which acts like a wall, so it may not stick out of it (there will have to be a special handling for floats whose ipds are superior to the ref-area's ipd). If previous start-floats take too much place in the i-p-d, its movement towards the before will be stopped by the start-guide:

  8. the side-float is pulled towards the before as much as possible
  9. the side-float is pulled towards the start/end as much as possible, but after having been pulled towards the before
  10. if "clear" = "start" or "both", we place the before-guide at least at the level of the start-guide. if "clear" = "end" or "both", we place the before-guide at least at the level of the end-guide.

So this model should provide a rather simple way to place side-floats. The most difficult part will be to find a suitable data structure for representing the guide snagged line, in order to easily compute it and update it.

2.2. Side-floats and the Knuth Approach

We saw that the previous algorithm makes it easy to place side-floats while following the placement rules. However, there is one thing which is unknown when lines are typeset: the bottom of the page.

In the current code paragraphs are broken into lines without having even started to think about page breaks. This works well excepted in the case where pages may have differing widths (see PageLayout/InfluenceOfPageSizesOnAlgorithm). Now there is an additional difficulty: how to know if there will be enough room on the page to place a side-float? And if there isn't enough room, what should be done? Split the side-float? Defer it to the next page?

The resulting layout may be quite different then, as may be seen on the following figure:

We may choose to either defer a side-float, or shrink it so that it fits on the page, or split it to put one piece on each page.

We could try to combine in some way the informations about line-breaking as well as page-breaking. For each feasible line-break we would also store informations regarding page breaking: does it also represent a legal page-break? If yes, is it a feasible page-break? And then the following line is computed in two different situations: whether the previous line-break was also chosen as a page-break (in which case the available width is the ipd of the following page), or not (and the available width is the ipd of the current page).

This situation leads to approximately doubling the Knuth nodes for the rest of the paragraph: one set for the case where the line is on the current page, one other set for the case where the line is put on the following page. Of course the nodes don't represent the same feasible breaks, as line widths are different. But there are approximately as many in each case.

If the current page may contain up to two additional lines of the paragraph, this leads to three times as many nodes. If n lines may be put on the current page, there will be (n+1) times as many nodes.

For side-floats, the handling would be similar: when placing a side-float we consider the location of the current line: if it is at the "bottom" of the page (first possibility above), there may be no room for the float which would be placed on the following page. If it is at the "top" of the following page, then there is room for the float. If the float is shrinkable or stretchable, this leads to additional possibilities. If there are more than one float to handle, this may well lead to a combinatorial explosion...

There is another problem in this model regarding orphans: when we have found a feasible break for a paragraph, we don't know yet whether the corresponding line will be the next to last or not. If this is the case then the line-break is not a legal page-break. We would thus have to implement some backtracking mechanism, to disable the page-break possibility if it turns out that the line was the next to last in the paragraph. There may well be similar problems with keep-with-previous settings.

One thing we could do is place all the side-floats without considering page boundaries, and then generate legal page-breaks afterwards, where possible. This is pretty like is currently done, IIC. This approach would have the following limitations:

That said, such situations should be rather uncommon and this method should work in most cases. It might be a good solution for a first implementation.


A huge thanks to Inkscape, who made the drawing of all those figures so easy.

GoogleSummerOfCode2006/FloatsImplementationProgress/ImplementingSideFloats (last edited 2009-09-20 23:52:15 by localhost)