Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/assets/images/golden-star-disco-ball.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/assets/images/htmlshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions examples/assets/images/luma.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "takumi-php"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
authors = ["Alessio Vertemati"]
license = "MIT"
Expand Down
32 changes: 31 additions & 1 deletion rust/src/internal/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
//! - Everything else becomes `Node::container` with class/id/tag metadata
//! - `<html>`, `<head>`, and `<body>` 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.
///
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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#"<div tw="w-[100px] h-[100px] bg-[#ff0000]"></div>"#,
);
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#"<div style="color: black"></div>"#);
Expand Down
108 changes: 108 additions & 0 deletions tests/Integration/BackgroundImageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

declare(strict_types=1);

namespace HtmlShot\Tests\Integration;

use HtmlShot\Context;
use HtmlShot\Renderer;
use PHPUnit\Framework\TestCase;

/**
* Integration tests for background-image and <img src> 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(
'<div style="background-image:url('.$jpg.');background-size:cover;width:200px;height:200px"></div>',
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(
'<div style="background-image:url('.$dataUri.');background-size:cover;width:200px;height:200px"></div>',
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(
'<img src="'.$jpg.'" width="200" height="200" />',
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');
}

/**
* Build a 200x200 solid-fill SVG and return it as a base64 data URI.
*/
private function solidSvgDataUri(int $red, int $green, int $blue): string
{
$hex = sprintf('#%02x%02x%02x', $red, $green, $blue);
$svg = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">'
.'<rect width="200" height="200" fill="'.$hex.'"/></svg>';

return 'data:image/svg+xml;base64,'.base64_encode($svg);
}
}
20 changes: 20 additions & 0 deletions tests/Integration/HtmlShotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ public function test_render_with_stylesheet_option(): void
$this->assertSame(0, $rgba['alpha'], 'Pixel should be fully opaque');
}

public function test_render_resolves_tw_attribute(): void
{
// The `tw` attribute (Satori/Takumi inline Tailwind) must be resolved into
// real styles rather than silently dropped during HTML parsing.
$bytes = HtmlShot::render('<div tw="w-[100px] h-[100px] bg-[#ff0000]"></div>', [
'width' => 200,
'height' => 200,
]);

$this->assertNotEmpty($bytes);
$img = imagecreatefromstring($bytes);
$this->assertNotFalse($img);

$rgba = imagecolorsforindex($img, imagecolorat($img, 50, 50));
$this->assertGreaterThan(200, $rgba['red'], 'Pixel should be red from tw background');
$this->assertLessThan(50, $rgba['green']);
$this->assertLessThan(50, $rgba['blue']);
$this->assertSame(0, $rgba['alpha'], 'Pixel should be fully opaque');
}

public function test_render_with_device_pixel_ratio(): void
{
$bytes1x = HtmlShot::render('<div style="background:red;width:100%;height:100%"></div>', [
Expand Down
Loading