When an SVG <foreignObject> is rendered indirectly through
an feImage filter primitive that references an ancestor group
containing it, the foreignObject HTML content is scaled by the page/SVG zoom
twice. The rest of the referenced group (here a gradient-filled
<rect>) is rendered at the correct size.
body has zoom: 1.5 applied to the
<svg>. A group #g contains a gradient
<rect> and a <foreignObject> with text
"hello world". A second <rect> at (100, 100)
uses filter="url(#filter)" where the filter is a single
<feImage href="#g">.
Expected: two equally sized blue boxes with "hello world" - one at
top-left (direct paint of #g) and one at bottom-right (the
filtered copy via feImage).
Actual: the foreignObject HTML content in the bottom-right (filtered)
copy renders at the wrong scale and ends up clipped to a tiny mark in the
top-left of the filter region. The gradient <rect> in
the same group renders at the correct size, so the bug is specific to
<foreignObject> when reached through
feImage.
The feImage code path that renders a referenced layout
subtree lives in
third_party/blink/renderer/core/svg/graphics/filters/svg_fe_image.cc
in FEImage::CreateImageFilterForLayoutObject:
const AffineTransform transform =
SourceToDestinationTransform(layout_object, dst_rect);
...
PaintRecordBuilder builder;
SVGObjectPainter(layout_object, nullptr)
.PaintResourceSubtree(builder.Context());
builder.EndRecording(*canvas);
SourceToDestinationTransform applies a scale of
GetFilter()->Scale() (which equals the effective device-scale,
i.e. the page zoom for this test) to the recorded canvas, on the assumption
that PaintResourceSubtree draws unscaled SVG content.
PaintResourceSubtree walks the referenced subtree via
SVGContainerPainter. For most SVG children that assumption holds.
But <foreignObject> is special:
LayoutSVGForeignObject lays its HTML children out at the
zoomed size and compensates in
LayoutSVGForeignObject::LocalToSVGParentTransform() by
scaling by 1 / EffectiveZoom(). When the foreignObject is
reached through PaintResourceSubtree + an outer canvas-level
SourceToDestinationTransform scale, the zoom factor in the
foreignObject's paint-property chain is no longer cancelled in the same way
it is during a normal SVG paint pass. The recorded chunks for the
foreignObject HTML content end up in a different effective scale than the
sibling SVG shapes - in practice the content collapses to a tiny clipped
mark inside the filter region. The issue was filed as "zoomed twice" from
the symmetric case where the foreignObject content overflows.
feImage.setAttribute('href', '#g') in a timeout to work around
issue 450817280.