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)