Martin Leitner created FOP-3334:
-----------------------------------

             Summary: min-width/min-height on fo:external-graphic is used as 
the fixed viewport size, so min-width="0" collapses the viewport and graphics 
overlap
                 Key: FOP-3334
                 URL: https://issues.apache.org/jira/browse/FOP-3334
             Project: FOP
          Issue Type: Bug
          Components: layout/inline
    Affects Versions: main, 2.11
         Environment: Apache FOP 2.11 official binary distribution (SHA-512 
verified), OpenJDK 21.0.2, Windows 11
            Reporter: Martin Leitner


h3. Summary

An explicit {{min-width}} / {{min-height}} (or the equivalent 
{{inline-progression-dimension.minimum}} / 
{{block-progression-dimension.minimum}}) on {{fo:external-graphic}} is used as 
the *fixed* viewport size instead of as a lower bound. With {{min-width="0"}} 
the viewport becomes 0 wide while the image content keeps its full width, so 
consecutive graphics are all painted at the same position and overlap. 
{{min-height="0"}} does the same in the block-progression direction.

Since {{0}} is the initial value of {{min-width}} in XSL-FO 1.1, any tool that 
writes the property out explicitly triggers this.

h3. Steps to reproduce

Attached {{minwidth-zero-viewport.fo}} is fully self-contained (the three 
images are RFC 2397 {{data:}} URIs, so no external resources are needed).

{code}
fop -fo minwidth-zero-viewport.fo -pdf out.pdf
fop -fo minwidth-zero-viewport.fo -at application/pdf out-at.xml
{code}

Block *A* draws three graphics (120x60, 90x60, 60x60 px) with no {{min-width}}; 
block *B* draws the same three with {{min-width="0"}}.

h3. Actual result (Apache FOP 2.11, official binary distribution)

Viewport extents from the area tree, in millipoints:

|| id || min-* set || viewport ipd || viewport bpd || content pos ||
| a-red | - | 120000 | 60000 | 0 0 120000 60000 |
| a-green | - | 90000 | 60000 | 0 0 90000 60000 |
| a-blue | - | 60000 | 60000 | 0 0 60000 60000 |
| b-red | min-width="0" | *0* | 60000 | 0 0 120000 60000 |
| b-green | min-width="0" | *0* | 60000 | 0 0 90000 60000 |
| b-blue | min-width="0" | *0* | 60000 | 0 0 60000 60000 |
| c-red | min-width="3in" | 216000 | 60000 | 0 0 120000 60000 |
| d-red | min-height="0" | 120000 | *0* | 0 0 120000 60000 |
| d-green | min-height="0" | 90000 | *0* | 0 0 90000 60000 |
| d-blue | min-height="0" | 60000 | *0* | 0 0 60000 60000 |

The {{b-*}} rows are the defect: the viewport is reported as {{ipd="0"}} while 
its content placement is the full {{120000}} / {{90000}} / {{60000}} wide image:

{code:xml}
<viewport ipd="0" bpd="60000" prod-id="b-red" pos="0 0 120000 60000">
  <image prod-id="b-red" url="data:image/png;base64,..."/>
</viewport>
{code}

Because each viewport advances the inline-progression position by 0, all three 
images are painted on top of each other in the PDF. The {{c-red}} row shows 
that a *non-zero* minimum does widen the viewport as expected, so only the zero 
case is visibly broken - but the underlying cause is the same for every 
explicit minimum.

h3. Expected result

A minimum is a lower bound, not the size. The viewport should be sized from the 
content and then constrained by the minimum/maximum, i.e. {{max(content extent, 
minimum)}} - which for {{min-width="0"}} yields the content width and 
reproduces the same output as if the property were absent.

h3. Cause

{{min-width}} / {{min-height}} are not read directly. They are _extra 
corresponding_ properties of {{inline-progression-dimension}} / 
{{block-progression-dimension}} ({{FOPropertyMapping}}, the 
{{setExtraCorresponding()}} calls for both dimension properties), and 
{{DimensionPropertyMaker.compute()}} copies an explicit value into the 
{{CP_MINIMUM}} subproperty of the length-range.

{{ImageLayout.doLayout()}} then takes that minimum as the viewport extent (2.11 
lines 89-93, and 80-83 for the BPD):

{code:java}
len = 
props.getInlineProgressionDimension().getMinimum(percentBaseContext).getLength();
if (ipd == -1 && len.getEnum() != EN_AUTO) {
    //Establish minimum viewport size
    ipd = len.getValue(percentBaseContext);
}
{code}

Because {{ipd}} is no longer {{-1}}, the content-driven sizing at lines 163-171 
is skipped:

{code:java}
//Adjust viewport if not explicit
if (ipd == -1) {
    ipd = constrainExtent(cwidth,
            props.getInlineProgressionDimension(), props.getContentWidth());
}
{code}

{{constrainExtent()}} is where the correct semantics already live - it applies 
the maximum as a ceiling and the minimum as a floor ({{extent = 
Math.max(extent, min)}}) - but it is only reached when no minimum was specified.

h3. Suggested fix

Keep the provisional minimum-derived extent (the 
{{content-width}}/{{content-height}} {{scale-*-to-fit}} branches rely on it) 
but re-derive the final viewport extent through {{constrainExtent()}}:

{code:java}
int bpd = -1;
int ipd = -1;
boolean bpdFromMinimum = false;
boolean ipdFromMinimum = false;
...
// where the minimum is currently assigned, also set the corresponding flag
...
//Adjust viewport if not explicit
if (ipd == -1 || ipdFromMinimum) {
    ipd = constrainExtent(cwidth,
            props.getInlineProgressionDimension(), props.getContentWidth());
}
if (bpd == -1 || bpdFromMinimum) {
    bpd = constrainExtent(cheight,
            props.getBlockProgressionDimension(), props.getContentHeight());
}
{code}

No change is needed in {{DimensionPropertyMaker}} or {{FOPropertyMapping}}; the 
property-to-subproperty mapping is correct, only the layout consumption is 
wrong.

*Verified against the official 2.11 binary distribution* by compiling only this 
patched class and putting it ahead of {{fop-core-2.11.jar}} on the classpath. 
Attached {{areatree-fop-2.11-patched.xml}} is the result:

* {{b-red}} / {{b-green}} / {{b-blue}} viewport ipd: {{0}} -> {{120000}} / 
{{90000}} / {{60000}} (equal to the {{a-*}} reference, no overlap)
* {{d-*}} viewport bpd: {{0}} -> {{60000}}
* {{c-red}} (min-width="3in") unchanged at {{216000}} - the floor still applies
* {{a-*}} (no minimum) unchanged

A side-case matrix produced identical area trees before and after the patch 
for: {{inline-progression-dimension.minimum}} at 1in and 3in combined with 
{{content-width="scale-to-fit"}}, {{scale-down-to-fit}} and 
{{scale-up-to-fit}}; {{block-progression-dimension.minimum}} with 
{{content-height="scale-to-fit"}}; an explicit {{.optimum}}; and a conflicting 
{{min-width="4in" max-width="2in"}} pair. In particular the 
{{external-graphic_size_1}} layout-engine testcase pattern 
({{inline-progression-dimension.minimum="1in"}} + 
{{content-width="scale-to-fit"}}) is unaffected.

h3. Related observation (not addressed by the patch above)

{{min-width="0"}} combined with {{content-width="scale-to-fit"}} makes the 
graphic disappear completely - no viewport or image area is generated at all - 
both before and after the patch. The content extent is scaled to the 
provisional zero viewport before the viewport is re-derived. This may deserve 
separate treatment; it is not the overlap defect reported here.

h3. Versions checked

* 2.11 (latest release): reproduced by execution; {{ImageLayout}} lines 89-93 
and 163-171 as quoted above.
* 2.4, 2.8, 2.10: the same block is present in the published sources jars.
* trunk HEAD (gitbox): the same block is present (shifted to lines ~95-97 by 
the unrelated {{useParentIPDImageScaling}} addition).

h3. Environment

Apache FOP 2.11 binary distribution (SHA-512 verified against the published 
checksum), OpenJDK 21.0.2, Windows 11.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to