diff --git a/Tests/Unit/classes/Picture/BuildPictureTagTest.php b/Tests/Unit/classes/Picture/BuildPictureTagTest.php
new file mode 100644
index 000000000..6598e8bdf
--- /dev/null
+++ b/Tests/Unit/classes/Picture/BuildPictureTagTest.php
@@ -0,0 +1,189 @@
+returnArg();
+ }
+
+ /**
+ * Get an instance of Display without invoking the constructor (its only
+ * dependency, Imagify_Filesystem, is not needed by build_picture_tag()).
+ *
+ * @return Display
+ */
+ private function get_display_instance(): Display {
+ $reflection = new ReflectionClass( Display::class );
+
+ return $reflection->newInstanceWithoutConstructor();
+ }
+
+ /**
+ * Build a minimal $image array as expected by build_picture_tag(), build_source_tag()
+ * and build_img_tag(). $attributes is the raw
attributes array.
+ *
+ * @param array $attributes The
attributes.
+ *
+ * @return array
+ */
+ private function get_image_data( array $attributes ): array {
+ return [
+ 'attributes' => $attributes,
+ 'src_attribute' => 'src',
+ 'srcset_attribute' => false,
+ 'srcset' => [],
+ 'src' => [],
+ ];
+ }
+
+ /**
+ * Extract the opening tag's attribute string from the built output.
+ *
+ * @param string $output The output of build_picture_tag().
+ *
+ * @return string
+ */
+ private function get_picture_opening_tag( string $output ): string {
+ preg_match( '/^]*)>/', $output, $matches );
+
+ return $matches[1] ?? '';
+ }
+
+ /**
+ * Extract the
tag's attribute string from the built output.
+ *
+ * @param string $output The output of build_picture_tag().
+ *
+ * @return string
+ */
+ private function get_img_tag( string $output ): string {
+ preg_match( '/
]*)\/>/', $output, $matches );
+
+ return $matches[1] ?? '';
+ }
+
+ /**
+ * `data-wp-*` directive attributes must be filtered out of the tag,
+ * but kept on the inner
tag.
+ */
+ public function testDataWpAttributesAreExcludedFromPictureTagButKeptOnImgTag(): void {
+ $attributes = [
+ 'data-object-fit' => 'cover',
+ 'data-wp-interactive' => 'core/image',
+ 'data-wp-context' => '{"isOpen":false}',
+ 'data-wp-on--click' => 'actions.showLightbox',
+ ];
+
+ $image = $this->get_image_data( $attributes );
+
+ $method = $this->get_reflective_method( 'build_picture_tag', Display::class );
+ $output = $method->invoke( $this->get_display_instance(), $image );
+
+ $picture_tag = $this->get_picture_opening_tag( $output );
+ $img_tag = $this->get_img_tag( $output );
+
+ $this->assertStringNotContainsString( 'data-wp-interactive', $picture_tag );
+ $this->assertStringNotContainsString( 'data-wp-context', $picture_tag );
+ $this->assertStringNotContainsString( 'data-wp-on--click', $picture_tag );
+
+ $this->assertStringContainsString( 'data-wp-interactive="core/image"', $img_tag );
+ $this->assertStringContainsString( 'data-wp-context="{"isOpen":false}"', $img_tag );
+ $this->assertStringContainsString( 'data-wp-on--click="actions.showLightbox"', $img_tag );
+ }
+
+ /**
+ * Non `data-wp` attributes are unaffected by the new filter and keep behaving
+ * as they did before: they remain on the tag, and follow the
+ * pre-existing removal rules on the
tag.
+ */
+ public function testNonDataWpAttributesAreUnaffected(): void {
+ $attributes = [
+ 'data-object-fit' => 'cover',
+ 'class' => 'my-image',
+ 'id' => 'img-1',
+ ];
+
+ $image = $this->get_image_data( $attributes );
+
+ $method = $this->get_reflective_method( 'build_picture_tag', Display::class );
+ $output = $method->invoke( $this->get_display_instance(), $image );
+
+ $picture_tag = $this->get_picture_opening_tag( $output );
+ $img_tag = $this->get_img_tag( $output );
+
+ // All 3 attributes remain on the tag, since none of them contain 'data-wp'.
+ $this->assertStringContainsString( 'data-object-fit="cover"', $picture_tag );
+ $this->assertStringContainsString( 'class="my-image"', $picture_tag );
+ $this->assertStringContainsString( 'id="img-1"', $picture_tag );
+
+ // Pre-existing logic (unrelated to this fix) strips class/id from the
tag,
+ // but keeps data-object-fit.
+ $this->assertStringContainsString( 'data-object-fit="cover"', $img_tag );
+ $this->assertStringNotContainsString( 'class=', $img_tag );
+ $this->assertStringNotContainsString( 'id=', $img_tag );
+ }
+
+ /**
+ * The new `data-wp` filter composes correctly with the pre-existing
+ * 'wp-block-cover__image-background' exclusion logic: both sets of
+ * attributes are removed from the tag, while the
tag
+ * (which is treated as a Gutenberg cover background image) keeps
+ * everything, including the `data-wp` attributes.
+ */
+ public function testDataWpFilterComposesWithGutenbergCoverExclusion(): void {
+ $attributes = [
+ 'class' => 'wp-block-cover__image-background',
+ 'style' => 'object-fit:cover',
+ 'data-object-fit' => 'cover',
+ 'data-object-position' => '50% 50%',
+ 'data-wp-interactive' => 'core/image',
+ 'data-wp-context' => '{}',
+ ];
+
+ $image = $this->get_image_data( $attributes );
+
+ $method = $this->get_reflective_method( 'build_picture_tag', Display::class );
+ $output = $method->invoke( $this->get_display_instance(), $image );
+
+ $picture_tag = $this->get_picture_opening_tag( $output );
+ $img_tag = $this->get_img_tag( $output );
+
+ // None of the Gutenberg-specific attributes, nor the data-wp attributes, remain on .
+ $this->assertStringNotContainsString( 'class=', $picture_tag );
+ $this->assertStringNotContainsString( 'style=', $picture_tag );
+ $this->assertStringNotContainsString( 'data-object-fit', $picture_tag );
+ $this->assertStringNotContainsString( 'data-object-position', $picture_tag );
+ $this->assertStringNotContainsString( 'data-wp-interactive', $picture_tag );
+ $this->assertStringNotContainsString( 'data-wp-context', $picture_tag );
+ $this->assertSame( '', trim( $picture_tag ) );
+
+ // The
tag, being the Gutenberg cover background image, keeps everything
+ // (only 'id' and 'title' are ever stripped from it in that branch).
+ $this->assertStringContainsString( 'class="wp-block-cover__image-background"', $img_tag );
+ $this->assertStringContainsString( 'style="object-fit:cover"', $img_tag );
+ $this->assertStringContainsString( 'data-object-fit="cover"', $img_tag );
+ $this->assertStringContainsString( 'data-object-position="50% 50%"', $img_tag );
+ $this->assertStringContainsString( 'data-wp-interactive="core/image"', $img_tag );
+ $this->assertStringContainsString( 'data-wp-context="{}"', $img_tag );
+ }
+}
diff --git a/classes/Picture/Display.php b/classes/Picture/Display.php
index 38848e874..c2bc2ff49 100644
--- a/classes/Picture/Display.php
+++ b/classes/Picture/Display.php
@@ -218,7 +218,15 @@ protected function build_picture_tag( $image ) {
unset( $attributes['data-object-position'] );
}
- $output = 'build_attributes( $attributes ) . ">\n";
+ $picture_attributes = array_filter(
+ $attributes,
+ function ( $attribute ) {
+ return strpos( $attribute, 'data-wp' ) === false;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+
+ $output = 'build_attributes( $picture_attributes ) . ">\n";
/**
* Allow to add more tags to the tag.
*