diff --git a/.gitattributes b/.gitattributes index 3a67686..ee2fb8a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,7 @@ /phpunit.xml.dist export-ignore /phpstan.neon.dist export-ignore /docs export-ignore +/examples export-ignore /tests export-ignore /include export-ignore /rust export-ignore diff --git a/examples/assets/images/golden-star-disco-ball-2.png b/examples/assets/images/golden-star-disco-ball-2.png new file mode 100644 index 0000000..0a949e3 Binary files /dev/null and b/examples/assets/images/golden-star-disco-ball-2.png differ diff --git a/examples/assets/images/golden-star-disco-ball.png b/examples/assets/images/golden-star-disco-ball.png new file mode 100644 index 0000000..70aec9d Binary files /dev/null and b/examples/assets/images/golden-star-disco-ball.png differ diff --git a/examples/assets/images/htmlshot.png b/examples/assets/images/htmlshot.png new file mode 100644 index 0000000..ba713a1 Binary files /dev/null and b/examples/assets/images/htmlshot.png differ diff --git a/examples/assets/images/luma.svg b/examples/assets/images/luma.svg new file mode 100644 index 0000000..4af137d --- /dev/null +++ b/examples/assets/images/luma.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/examples/assets/images/martin-martz-W0NRebXbsjM-unsplash.jpg b/examples/assets/images/martin-martz-W0NRebXbsjM-unsplash.jpg new file mode 100644 index 0000000..db0e910 Binary files /dev/null and b/examples/assets/images/martin-martz-W0NRebXbsjM-unsplash.jpg differ diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 548f8f1..1a8dc9d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "takumi-php" -version = "0.1.1" +version = "0.1.2" edition = "2021" authors = ["Alessio Vertemati"] license = "MIT" diff --git a/rust/src/internal/html.rs b/rust/src/internal/html.rs index 88f62be..c8a2459 100644 --- a/rust/src/internal/html.rs +++ b/rust/src/internal/html.rs @@ -8,9 +8,12 @@ //! - Everything else becomes `Node::container` with class/id/tag metadata //! - ``, `
`, and `` wrappers are treated as transparent +use std::str::FromStr; + use scraper::node::Node as HtmlNode; use scraper::Html; use takumi::layout::node::Node; +use takumi::layout::style::tw::TailwindValues; /// Parse `html` and return a root `Node` plus any extra CSS strings. /// @@ -158,7 +161,7 @@ fn convert_node( } } -/// Apply class, id, and inline-style metadata to a node. +/// Apply class, id, tailwind (`tw`), and inline-style metadata to a node. fn apply_metadata( mut node: Node, element: &scraper::node::Element, @@ -186,6 +189,15 @@ fn apply_metadata( if let Some(id) = element.attr("id") { node = node.with_id(id); } + + // Promote the `tw` attribute (Satori/Takumi inline Tailwind) to the node's + // Tailwind-derived style. Mirrors `takumi-helpers`' `tailwindClassesProperty`. + // Unparseable utilities are dropped rather than failing the whole render. + if let Some(tw) = element.attr("tw") { + if let Ok(values) = TailwindValues::from_str(tw) { + node = node.with_tw(values); + } + } node } @@ -246,6 +258,24 @@ mod tests { ); } + #[test] + fn tw_attribute_background_renders_opaque() { + // The `tw` attribute (Satori/Takumi inline Tailwind) must be resolved + // into actual styles, not silently dropped. + let image = render_html( + r#""#, + ); + let p = image.get_pixel(50, 50); + assert!( + p[3] > 0, + "expected opaque pixel at (50,50) from tw attribute, got transparent; RGBA={p:?}" + ); + assert!( + p[0] > 200, + "expected red pixel at (50,50) from tw attribute; RGBA={p:?}" + ); + } + #[test] fn parse_html_inline_style_produces_css_class() { let (_node, css) = parse_html(r#""#); diff --git a/tests/Integration/BackgroundImageTest.php b/tests/Integration/BackgroundImageTest.php new file mode 100644 index 0000000..415b48f --- /dev/null +++ b/tests/Integration/BackgroundImageTest.php @@ -0,0 +1,108 @@ + rendering. + * Requires image fixture files from examples/assets/images/. + */ +class BackgroundImageTest extends TestCase +{ + /** Photographic JPEG fixture: full-bleed, every pixel opaque and colored. */ + private const JPEG = 'martin-martz-W0NRebXbsjM-unsplash.jpg'; + + /** Fill colour for the live-generated SVG fixture (#3b82f6). */ + private const SVG_FILL = [59, 130, 246]; + + private static string $imagesDir; + + public static function setUpBeforeClass(): void + { + self::$imagesDir = dirname(__DIR__, 2).'/examples/assets/images'; + } + + public function test_background_image_jpeg(): void + { + $jpg = self::$imagesDir.'/'.self::JPEG; + + if (! file_exists($jpg)) { + $this->markTestSkipped(self::JPEG.' not found in examples/assets/images'); + } + + $ctx = new Context; + $bytes = (new Renderer($ctx))->render( + '', + 200, 200 + ); + + $img = imagecreatefromstring($bytes); + $this->assertNotFalse($img); + + $rgba = imagecolorsforindex($img, imagecolorat($img, 100, 100)); + $this->assertSame(0, $rgba['alpha'], 'Pixel should be fully opaque'); + $this->assertGreaterThan(10, $rgba['red'] + $rgba['green'] + $rgba['blue'], 'Pixel should be non-black'); + } + + public function test_background_image_svg(): void + { + // Generate a solid-fill SVG live and feed it in as a data URI, so the + // test owns its fixture and can assert the exact colour that renders. + $dataUri = $this->solidSvgDataUri(...self::SVG_FILL); + + $ctx = new Context; + $bytes = (new Renderer($ctx))->render( + '', + 200, 200 + ); + + $img = imagecreatefromstring($bytes); + $this->assertNotFalse($img); + + [$red, $green, $blue] = self::SVG_FILL; + $rgba = imagecolorsforindex($img, imagecolorat($img, 100, 100)); + $this->assertSame(0, $rgba['alpha'], 'Pixel should be fully opaque'); + $this->assertEqualsWithDelta($red, $rgba['red'], 4, 'Red channel should match the SVG fill'); + $this->assertEqualsWithDelta($green, $rgba['green'], 4, 'Green channel should match the SVG fill'); + $this->assertEqualsWithDelta($blue, $rgba['blue'], 4, 'Blue channel should match the SVG fill'); + } + + public function test_img_src_jpeg(): void + { + $jpg = self::$imagesDir.'/'.self::JPEG; + + if (! file_exists($jpg)) { + $this->markTestSkipped(self::JPEG.' not found in examples/assets/images'); + } + + $ctx = new Context; + $bytes = (new Renderer($ctx))->render( + '