Skip to content
Open
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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,40 @@ PDFs move the selected element directly into the document body and print it usin

Selection works with JavaScript disabled. Missing elements and invalid selectors throw exceptions. Images require an element with nonzero layout bounds; PDFs follow the document's print styles. Each output builder keeps the selector chosen when it was created.

### Customizing a Capture

Use `before()` on a source, image, or PDF builder to customize the prepared driver resource. Each callback receives a `CaptureContext` implementing the `Contracts\CaptureContext` interface. Its `driver()` method returns the actual driver instance:

```php
use DirectoryTree\Screenshot\Contracts\CaptureContext;
use DirectoryTree\Screenshot\Drivers\ChromeDriver;
use DirectoryTree\Screenshot\Drivers\GotenbergDriver;
use DirectoryTree\Screenshot\Facades\Screenshot;

Screenshot::url('https://example.com')
->javascript()
->before(function (CaptureContext $context) {
match (true) {
$context->driver() instanceof ChromeDriver => $context->page()
->evaluate('document.querySelector("#banner")?.remove()')
->getReturnValue(),
$context->driver() instanceof GotenbergDriver => $context->request()
->set('waitForExpression', 'window.ready === true'),
default => null,
};
})
->png()
->capture();
```

Chrome invokes callbacks after the source loads and before image bounds are measured or PDF preparation begins. `page()` returns the live `HeadlessChromium\Page`, allowing JavaScript execution, application-specific readiness waits, and viewport changes. Enable `javascript()` when your callback needs to execute JavaScript.

Gotenberg invokes callbacks immediately before sending its HTTP request. `request()` returns a mutable `DirectoryTree\Screenshot\Drivers\Gotenberg\Request`. Use `set()` to add or replace a multipart field. Its public `url` and `options` properties expose the destination and Guzzle request options, including headers and multipart files. This does not provide access to Gotenberg's remote browser page.

`ChromeCaptureContext` and `GotenbergCaptureContext` implement the shared contract. Only Chrome contexts expose `page()`, and only Gotenberg contexts expose `request()`. Callbacks run in registration order, their return values are ignored, and exceptions propagate through the driver's existing cleanup. A callback added to a source is copied into subsequently created output builders; callbacks added to an output affect only that builder. Callbacks run for every actual capture, including captures triggered by `save()` and `store()`.

The screenshot fake records capture options without executing callbacks. Custom drivers can implement `Contracts\CaptureContext` to expose their own resources and invoke registered callbacks through `$options->prepare($context)`.

### Saving and Reading Captures

Call `save()` or `store()` on a source or output builder, or capture once and reuse the result:
Expand Down
23 changes: 23 additions & 0 deletions src/CaptureOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace DirectoryTree\Screenshot;

use Closure;
use DirectoryTree\Screenshot\Contracts\CaptureContext;
use DirectoryTree\Screenshot\Enums\OutputFormat;

/**
Expand All @@ -18,6 +20,7 @@ public function __construct(
protected BrowserOptions $browser,
protected ImageOptions|PdfOptions $output,
protected ?string $element = null,
protected array $callbacks = [],
) {}

/**
Expand Down Expand Up @@ -63,6 +66,26 @@ public function format(): OutputFormat
return $this->output->getFormat();
}

/**
* Register a callback to customize the driver before capturing.
*/
public function before(Closure $callback): static
{
$this->callbacks[] = $callback;

return $this;
}

/**
* Invoke the callbacks with the prepared driver context.
*/
public function prepare(CaptureContext $context): void
{
foreach ($this->callbacks as $callback) {
$callback($context);
}
}

/**
* Validate the browser and output settings before capturing.
*/
Expand Down
11 changes: 11 additions & 0 deletions src/Contracts/CaptureContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace DirectoryTree\Screenshot\Contracts;

interface CaptureContext
{
/**
* Get the driver performing the capture.
*/
public function driver(): Driver;
}
34 changes: 34 additions & 0 deletions src/Drivers/Chrome/ChromeCaptureContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace DirectoryTree\Screenshot\Drivers\Chrome;

use DirectoryTree\Screenshot\Contracts\CaptureContext;
use DirectoryTree\Screenshot\Drivers\ChromeDriver;
use HeadlessChromium\Page;

class ChromeCaptureContext implements CaptureContext
{
/**
* Create a context for the prepared page.
*/
public function __construct(
protected ChromeDriver $driver,
protected Page $page,
) {}

/**
* Get the driver performing the capture.
*/
public function driver(): ChromeDriver
{
return $this->driver;
}

/**
* Get the prepared page.
*/
public function page(): Page
{
return $this->page;
}
}
4 changes: 4 additions & 0 deletions src/Drivers/Chrome/PageCapture.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace DirectoryTree\Screenshot\Drivers\Chrome;

use DirectoryTree\Screenshot\CaptureOptions;
use DirectoryTree\Screenshot\Drivers\ChromeDriver;
use DirectoryTree\Screenshot\Enums\OutputFormat;
use DirectoryTree\Screenshot\Enums\SourceType;
use DirectoryTree\Screenshot\ImageResult;
Expand All @@ -22,6 +23,7 @@ class PageCapture
* Prepare to capture a page within the given browser.
*/
public function __construct(
protected ChromeDriver $driver,
protected Browser $browser,
protected CaptureOptions $options,
protected bool $shared = false,
Expand All @@ -43,6 +45,8 @@ public function capture(string $source, SourceType $type): ImageResult|PdfResult
$this->configurePage($page);
$this->loadPage($page, $source, $type);

$this->options->prepare(new ChromeCaptureContext($this->driver, $page));

$result = $this->newCapture()->capture($page);
$failed = false;

Expand Down
2 changes: 1 addition & 1 deletion src/Drivers/ChromeDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ protected function capturePage(Browser $browser, string $source, SourceType $typ
$browser->getConnection()->setConnectionHttpHeaders($this->browserOptions['headers']);
}

$result = (new PageCapture($browser, $options, $shared))->capture($source, $type);
$result = (new PageCapture($this, $browser, $options, $shared))->capture($source, $type);

$failed = false;

Expand Down
33 changes: 33 additions & 0 deletions src/Drivers/Gotenberg/GotenbergCaptureContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace DirectoryTree\Screenshot\Drivers\Gotenberg;

use DirectoryTree\Screenshot\Contracts\CaptureContext;
use DirectoryTree\Screenshot\Drivers\GotenbergDriver;

class GotenbergCaptureContext implements CaptureContext
{
/**
* Create a context for the prepared request.
*/
public function __construct(
protected GotenbergDriver $driver,
protected Request $request,
) {}

/**
* Get the driver performing the capture.
*/
public function driver(): GotenbergDriver
{
return $this->driver;
}

/**
* Get the prepared request.
*/
public function request(): Request
{
return $this->request;
}
}
32 changes: 32 additions & 0 deletions src/Drivers/Gotenberg/Request.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace DirectoryTree\Screenshot\Drivers\Gotenberg;

class Request
{
/**
* Create a mutable Guzzle request configuration.
*/
public function __construct(
public string $url,
public array $options,
) {}

/**
* Set a multipart field while preserving other request fields and files.
*/
public function set(string $name, string|int|float|bool $value): static
{
$this->options['multipart'] = array_values(array_filter(
$this->options['multipart'],
fn (array $part) => $part['name'] !== $name,
));

$this->options['multipart'][] = [
'name' => $name,
'contents' => is_bool($value) ? ($value ? 'true' : 'false') : (string) $value,
];

return $this;
}
}
8 changes: 7 additions & 1 deletion src/Drivers/GotenbergDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

use DirectoryTree\Screenshot\CaptureOptions;
use DirectoryTree\Screenshot\Contracts\Driver;
use DirectoryTree\Screenshot\Drivers\Gotenberg\GotenbergCaptureContext;
use DirectoryTree\Screenshot\Drivers\Gotenberg\ImageOutput;
use DirectoryTree\Screenshot\Drivers\Gotenberg\Output;
use DirectoryTree\Screenshot\Drivers\Gotenberg\PdfOutput;
use DirectoryTree\Screenshot\Drivers\Gotenberg\Request;
use DirectoryTree\Screenshot\Enums\OutputFormat;
use DirectoryTree\Screenshot\Enums\SourceType;
use DirectoryTree\Screenshot\ImageResult;
Expand Down Expand Up @@ -62,13 +64,17 @@ protected function capturePage(string $source, SourceType $type, CaptureOptions
{
$route = $output->route();

$response = $this->http->request('POST', rtrim($this->url, '/')."/forms/chromium/{$route}/{$type->value}", [
$request = new Request(rtrim($this->url, '/')."/forms/chromium/{$route}/{$type->value}", [
'multipart' => $this->getMultipart($source, $type, $output->fields()),
'timeout' => $options->browser()->getBrowserTimeout() / 1000,
'http_errors' => false,
'allow_redirects' => false,
]);

$options->prepare(new GotenbergCaptureContext($this, $request));

$response = $this->http->request('POST', $request->url, $request->options);

if ($response->getStatusCode() !== 200) {
throw new RuntimeException(sprintf(
'Gotenberg capture failed (HTTP %d, trace: %s): %s',
Expand Down
11 changes: 11 additions & 0 deletions src/PendingImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace DirectoryTree\Screenshot;

use Closure;
use DirectoryTree\Screenshot\Enums\OutputFormat;

class PendingImage
Expand All @@ -18,6 +19,16 @@ public function __construct(
$this->options = clone $options;
}

/**
* Customize the driver immediately before capturing.
*/
public function before(Closure $callback): static
{
$this->options->before($callback);

return $this;
}

/**
* Set the viewport dimensions after validating both values.
*/
Expand Down
11 changes: 11 additions & 0 deletions src/PendingPdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace DirectoryTree\Screenshot;

use Closure;
use DirectoryTree\Screenshot\Enums\PaperSize;

class PendingPdf
Expand All @@ -18,6 +19,16 @@ public function __construct(
$this->options = clone $options;
}

/**
* Customize the driver immediately before capturing.
*/
public function before(Closure $callback): static
{
$this->options->before($callback);

return $this;
}

/**
* Set the viewport dimensions after validating both values.
*/
Expand Down
21 changes: 20 additions & 1 deletion src/PendingSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@

namespace DirectoryTree\Screenshot;

use Closure;

class PendingSource
{
/**
* The CSS selector to capture from the source.
*/
protected ?string $element = null;

/**
* The callbacks invoked before capturing.
*
* @var array<Closure>
*/
protected array $callbacks = [];

/**
* Prepare a source with independent browser settings and shared output defaults.
*/
Expand All @@ -21,6 +30,16 @@ public function __construct(
$this->browser = clone $browser;
}

/**
* Customize the driver immediately before capturing.
*/
public function before(Closure $callback): static
{
$this->callbacks[] = $callback;

return $this;
}

/**
* Set the viewport dimensions after validating both values.
*/
Expand Down Expand Up @@ -161,6 +180,6 @@ public function capture(): ImageResult
*/
protected function newCaptureOptions(ImageOptions|PdfOptions $output): CaptureOptions
{
return new CaptureOptions($this->browser, $output, $this->element);
return new CaptureOptions($this->browser, $output, $this->element, $this->callbacks);
}
}
16 changes: 16 additions & 0 deletions tests/Browser/CaptureTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use DirectoryTree\Screenshot\BrowserOptions;
use DirectoryTree\Screenshot\Contracts\CaptureContext;
use DirectoryTree\Screenshot\Drivers\ChromeDriver;
use DirectoryTree\Screenshot\Enums\OutputFormat;
use DirectoryTree\Screenshot\ImageOptions;
Expand Down Expand Up @@ -127,3 +128,18 @@ function realScreenshot(): Screenshot
$server->stop();
}
});

test('before callbacks customize a loaded Chrome page before element capture', function () {
$image = realScreenshot()->html('<style>body{margin:0}#card{width:100px;height:100px}</style><div id="card"></div>')
->javascript()
->before(function (CaptureContext $context) {
expect($context->driver())->toBeInstanceOf(ChromeDriver::class);
$context->page()->evaluate('document.querySelector("#card").style.width = "350px"; document.querySelector("#card").style.height = "250px";')->getReturnValue();
})
->element('#card')
->png()
->capture();

expect($image->width())->toBe(350);
expect($image->height())->toBe(250);
});
Loading
Loading