diff --git a/php-transformer/src/HtmlToBlocks/Style/AuthorStylesheetProjector.php b/php-transformer/src/HtmlToBlocks/Style/AuthorStylesheetProjector.php
index c26aeb86a..8183321ca 100644
--- a/php-transformer/src/HtmlToBlocks/Style/AuthorStylesheetProjector.php
+++ b/php-transformer/src/HtmlToBlocks/Style/AuthorStylesheetProjector.php
@@ -425,7 +425,7 @@ private function emitProjectedStyleRule(string $prelude, string $body, AuthorSty
$svgImagePrelude = $this->projectAuthorImageSelectorPrelude($prelude, $context, 'svg', $declarations);
$imageRule = '' === $imagePrelude
? ''
- : $imagePrelude . '{' . $this->imageProjectionBridgeDeclarations($declarations) . '}';
+ : $this->imageProjectionBridgeRule($prelude, $imagePrelude, $declarations, $context);
if ('' !== $imagePrelude) {
// The generic Image bridge introduces block display and responsive
// shrinkage. An inline source image in an auto table must instead
@@ -3335,8 +3335,44 @@ private function lineImageFigureRule(string $imagePrelude, array $declarations,
. ( isset($declarations['vertical-align']) ? ';vertical-align:' . $declarations['vertical-align'] : '' ) . '}';
}
+ /**
+ * The bridge lets the native image follow the figure that now carries the
+ * source rule. A selector whose subject is a bare `img` has nothing that
+ * moves to the figure, so the source rule still paints the image itself:
+ * inheriting the figure's corners there would replace the image's own
+ * radius at the same specificity, later in the cascade.
+ *
+ * @param array $declarations
+ */
+ private function imageProjectionBridgeRule(string $prelude, string $imagePrelude, array $declarations, AuthorStylesheetProjectionContext $context): string
+ {
+ $selectors = CssStylesheetTransformer::splitSelectorList($prelude) ?? array();
+ $moved = array_values(array_filter($selectors, fn (string $selector): bool => ! $this->subjectStaysOnImage($selector, $context)));
+ if ( count($moved) === count($selectors) ) {
+ return $imagePrelude . '{' . $this->imageProjectionBridgeDeclarations($declarations) . '}';
+ }
+ $rule = $imagePrelude . '{' . $this->imageProjectionBridgeDeclarations($declarations, false, false) . '}';
+ $movedPrelude = array() === $moved ? '' : $this->projectAuthorImageSelectorPrelude(implode(',', $moved), $context);
+
+ return '' === $movedPrelude ? $rule : $rule . $movedPrelude . '{border-radius:inherit}';
+ }
+
+ private function subjectStaysOnImage(string $selector, AuthorStylesheetProjectionContext $context): bool
+ {
+ $parsed = $context->sourceStyles->parsedSelector($selector);
+ if ( ! $parsed['supported'] ) {
+ return false;
+ }
+ $subject = CssSelectorCompoundInspector::rightmost($parsed['compounds']);
+
+ return 'img' === strtolower((string) ($subject['type'] ?? ''))
+ && array() === ($subject['classes'] ?? array())
+ && array() === ($subject['ids'] ?? array())
+ && array() === ($subject['attributes'] ?? array());
+ }
+
/** @param array $declarations */
- private function imageProjectionBridgeDeclarations(array $declarations, bool $preserveObjectFit = false): string
+ private function imageProjectionBridgeDeclarations(array $declarations, bool $preserveObjectFit = false, bool $inheritRadius = true): string
{
$bridge = array( 'display:block' );
$position = strtolower(trim((string) ($declarations['position'] ?? '')));
@@ -3362,7 +3398,9 @@ private function imageProjectionBridgeDeclarations(array $declarations, bool $pr
$objectFit = $this->explicitObjectFit($declarations);
$bridge[] = 'object-fit:' . ($preserveObjectFit && '' !== $objectFit ? $objectFit : 'inherit');
$bridge[] = 'object-position:inherit';
- $bridge[] = 'border-radius:inherit';
+ if ( $inheritRadius ) {
+ $bridge[] = 'border-radius:inherit';
+ }
return implode(';', $bridge);
}
diff --git a/php-transformer/tests/unit/artifact-author-stylesheet-projection.php b/php-transformer/tests/unit/artifact-author-stylesheet-projection.php
index def8b1ce7..4993e3e24 100644
--- a/php-transformer/tests/unit/artifact-author-stylesheet-projection.php
+++ b/php-transformer/tests/unit/artifact-author-stylesheet-projection.php
@@ -553,6 +553,35 @@
$assert(str_contains($positionedImageCss, 'object-fit:fill !important;object-position:-197.702px -102.702px !important'), 'image presentation carrier preserves source object fit and position for the nested image bridge');
$assert(str_contains($positionedImageCss, '.map.wp-block-image > img,.map.wp-block-image > a > img{display:block;width:100%;height:100%;max-width:100%;object-fit:inherit;object-position:inherit;border-radius:inherit}'), 'nested image fills a wrapper with explicitly owned width and height, linked or not');
+// Corner radius follows whichever box the source rule paints after projection:
+// the figure when the image's class moved onto it, the image itself when the
+// rule's subject is a bare `img`.
+foreach (array(
+ 'class moved to figure' => array('
', '.photo{width:120px;height:120px;border-radius:50%}'),
+ 'descendant img subject' => array('', '.avatar img{width:120px;height:120px;border-radius:50%}'),
+ 'later radius-less breakpoint rule' => array('', '.avatar img{width:120px;height:120px;border-radius:50%}@media (min-width:1px){.avatar img{width:100px;height:100px}}'),
+ 'mixed selector list' => array('
', '.photo,.avatar img{width:120px;height:120px;border-radius:50%}'),
+) as $case => [$radiusHtml, $radiusCss]) {
+ $radius = ( new ArtifactCompiler() )->compile(array(
+ 'files' => array(
+ array( 'path' => 'index.html', 'kind' => 'html', 'content' => '' . $radiusHtml ),
+ array( 'path' => 'r.css', 'kind' => 'css', 'content_base64' => base64_encode($radiusCss) ),
+ array( 'path' => 'p.jpg', 'kind' => 'image', 'content' => 'image-bytes' ),
+ ),
+ ) )->toArray();
+ $radiusDocument = new DOMDocument();
+ @$radiusDocument->loadHTML('' . preg_replace('//s', '', (string) ($radius['serialized_blocks'] ?? '')) . '');
+ $radiusCascade = new StaticCssCascade($radiusDocument, implode("\n", array_column($radius['assets'] ?? array(), 'content')));
+ $radii = array();
+ foreach ( $radiusDocument->getElementsByTagName('img') as $radiusImage ) {
+ $value = $radiusCascade->resolve($radiusImage, array( 'border-radius' ), array())['border-radius'] ?? '';
+ $radii[] = 'inherit' === $value && $radiusImage->parentNode instanceof DOMElement
+ ? ($radiusCascade->resolve($radiusImage->parentNode, array( 'border-radius' ), array())['border-radius'] ?? '')
+ : $value;
+ }
+ $assert(array() !== $radii && array( '50%' ) === array_values(array_unique($radii)), 'projected image keeps its source corner radius: ' . $case . ' (got ' . implode(', ', $radii) . ')');
+}
+
$multiPage = ( new ArtifactCompiler() )->compile(array(
'entrypoint' => 'index.html',
'files' => array(