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
31 changes: 31 additions & 0 deletions bin/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ContextHandler extends BaseHandler {
cookies: async () => ({ cookies: await context.cookies(command.urls) }),
storageState: async () => ({ storageState: await context.storageState(command.options) }),
clipboardText: () => this.getClipboardText(context),
close: () => this.closeContext(context, command.contextId),
newPage: () => this.createNewPage(context, command)
});

Expand Down Expand Up @@ -77,6 +78,35 @@ class ContextHandler extends BaseHandler {
return { pageId };
}

async closeContext(context, contextId) {
try {
await context.close();
} catch (error) {
logger.error('Failed to close context', { contextId, error: error?.message });
throw error;
} finally {
this.cleanupContextResources(contextId);
}
}

cleanupContextResources(contextId) {
this.contexts.delete(contextId);
this.contextThrottling?.delete?.(contextId);

for (const [pageId, mappedContextId] of this.pageContexts.entries()) {
if (mappedContextId === contextId) {
this.pageContexts.delete(pageId);
this.pages.delete(pageId);
}
}

for (const [routeId, info] of this.routes.entries()) {
if (info?.contextId === contextId) {
this.routes.delete(routeId);
}
}
}

async waitForPopup(context, command) {
const timeout = command.timeout || 30000;
const requestId = command.requestId || this.generateId('popup_req');
Expand Down Expand Up @@ -155,6 +185,7 @@ class PageHandler extends BaseHandler {
waitForURL: () => page.waitForURL(command.url, command.options),
waitForSelector: () => page.waitForSelector(command.selector, command.options),
screenshot: () => PromiseUtils.wrapBinary(page.screenshot(command.options)),
pdf: () => PromiseUtils.wrapBinary(page.pdf(command.options || {})),
evaluateHandle: () => this.evaluateHandle(page, command),
addScriptTag: () => page.addScriptTag(command.options),
addStyleTag: () => page.addStyleTag(command.options).then(() => ({ success: true })),
Expand Down
3 changes: 2 additions & 1 deletion bin/playwright-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class PlaywrightServer extends BaseHandler {
pageContexts: this.pageContexts, dialogs: this.dialogs, elementHandles: this.elementHandles,
responses: this.responses, routes: this.routes, generateId: this.generateId.bind(this),
extractRequestData: this.extractRequestData.bind(this), serializeResponse: this.serializeResponse.bind(this),
sendFramedResponse, routeCounter: { value: this.counters.route },
sendFramedResponse,
routeCounter: { value: this.counters.route },
setupPageEventListeners: this.setupPageEventListeners.bind(this)
};
this.contextHandler = new ContextHandler(deps);
Expand Down
32 changes: 32 additions & 0 deletions docs/examples/pdf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/*
* This file is part of the community-maintained Playwright PHP project.
* It is not affiliated with or endorsed by Microsoft.
*
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

require_once __DIR__.'/../../vendor/autoload.php';

use Playwright\Playwright;

$browser = Playwright::chromium();
$page = $browser->newPage();

$page->goto('https://example.com');

$pdfPath = __DIR__.'/example.pdf';
$page->pdf($pdfPath, ['format' => 'A4']);
echo 'PDF saved to: '.$pdfPath."\n";

$pdfBytes = $page->pdfContent();
echo 'Inline PDF bytes: '.strlen($pdfBytes)."\n";

$page->close();
$browser->close();
7 changes: 7 additions & 0 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ echo $page->title() . PHP_EOL; // Outputs: "Example Domain"
// Take a screenshot and save it as 'screenshot.png'.
$page->screenshot('screenshot.png');

// Export the page to PDF on disk.
$page->pdf('invoice.pdf', ['format' => 'A4']);

// Or grab the PDF bytes directly without keeping files around.
$pdfBytes = $page->pdfContent();
file_put_contents('inline-invoice.pdf', $pdfBytes);

// Close the browser context and all its pages.
$context->close();
```
Expand Down
273 changes: 273 additions & 0 deletions src/Page/Options/PdfOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<?php

declare(strict_types=1);

/*
* This file is part of the community-maintained Playwright PHP project.
* It is not affiliated with or endorsed by Microsoft.
*
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Playwright\Page\Options;

use Playwright\Exception\RuntimeException;

/**
* @phpstan-type PdfMargins array{top?: string, right?: string, bottom?: string, left?: string}
* @phpstan-type PdfOptionsArray array{
* path?: string,
* format?: string,
* landscape?: bool,
* scale?: float,
* printBackground?: bool,
* width?: string,
* height?: string,
* margin?: PdfMargins,
* displayHeaderFooter?: bool,
* footerTemplate?: string,
* headerTemplate?: string,
* outline?: bool,
* pageRanges?: string,
* preferCSSPageSize?: bool,
* tagged?: bool
* }
*/
final class PdfOptions
{
private const SCALE_MIN = 0.1;
private const SCALE_MAX = 2.0;

private ?string $path;
private ?string $format;
private ?bool $landscape;
private ?float $scale;
private ?bool $printBackground;
private ?string $width;
private ?string $height;
/** @var PdfMargins|null */
private ?array $margin;
private ?bool $displayHeaderFooter;
private ?string $footerTemplate;
private ?string $headerTemplate;
private ?bool $outline;
private ?string $pageRanges;
private ?bool $preferCSSPageSize;
private ?bool $tagged;

public function __construct(
?string $path = null,
?string $format = null,
?bool $landscape = null,
?float $scale = null,
?bool $printBackground = null,
?string $width = null,
?string $height = null,
mixed $margin = null,
?bool $displayHeaderFooter = null,
?string $footerTemplate = null,
?string $headerTemplate = null,
?bool $outline = null,
?string $pageRanges = null,
?bool $preferCSSPageSize = null,
?bool $tagged = null,
) {
$this->path = self::normalizeNullableString($path);
$this->format = self::normalizeNullableString($format);
$this->landscape = $landscape;
$this->scale = null;
if (null !== $scale) {
if ($scale < self::SCALE_MIN || $scale > self::SCALE_MAX) {
throw new RuntimeException(sprintf('PDF scale must be between %.1f and %.1f.', self::SCALE_MIN, self::SCALE_MAX));
}

$this->scale = round($scale, 2);
}

$this->printBackground = $printBackground;
$this->width = self::normalizeNullableString($width);
$this->height = self::normalizeNullableString($height);
$this->margin = self::normalizeMargin($margin);
$this->displayHeaderFooter = $displayHeaderFooter;
$this->footerTemplate = self::normalizeNullableString($footerTemplate);
$this->headerTemplate = self::normalizeNullableString($headerTemplate);
$this->outline = $outline;
$this->pageRanges = self::normalizeNullableString($pageRanges);
$this->preferCSSPageSize = $preferCSSPageSize;
$this->tagged = $tagged;
}

/**
* @param array<string, mixed>|self $options
*/
public static function from(array|self $options): self
{
if ($options instanceof self) {
return $options;
}

return self::fromArray($options);
}

/**
* @param array<string, mixed> $options
*/
public static function fromArray(array $options): self
{
$scale = null;
if (array_key_exists('scale', $options)) {
if (!is_numeric($options['scale'])) {
throw new RuntimeException('PDF option "scale" must be numeric.');
}

$scale = (float) $options['scale'];
}

return new self(
path: isset($options['path']) ? (string) $options['path'] : null,

Check failure on line 130 in src/Page/Options/PdfOptions.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Cannot cast mixed to string.
format: isset($options['format']) ? (string) $options['format'] : null,

Check failure on line 131 in src/Page/Options/PdfOptions.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Cannot cast mixed to string.
landscape: isset($options['landscape']) ? (bool) $options['landscape'] : null,
scale: $scale,
printBackground: isset($options['printBackground']) ? (bool) $options['printBackground'] : null,
width: isset($options['width']) ? (string) $options['width'] : null,

Check failure on line 135 in src/Page/Options/PdfOptions.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Cannot cast mixed to string.
height: isset($options['height']) ? (string) $options['height'] : null,

Check failure on line 136 in src/Page/Options/PdfOptions.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Cannot cast mixed to string.
margin: $options['margin'] ?? null,
displayHeaderFooter: isset($options['displayHeaderFooter']) ? (bool) $options['displayHeaderFooter'] : null,
footerTemplate: isset($options['footerTemplate']) ? (string) $options['footerTemplate'] : null,

Check failure on line 139 in src/Page/Options/PdfOptions.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Cannot cast mixed to string.
headerTemplate: isset($options['headerTemplate']) ? (string) $options['headerTemplate'] : null,

Check failure on line 140 in src/Page/Options/PdfOptions.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Cannot cast mixed to string.
outline: isset($options['outline']) ? (bool) $options['outline'] : null,
pageRanges: isset($options['pageRanges']) ? (string) $options['pageRanges'] : null,

Check failure on line 142 in src/Page/Options/PdfOptions.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Cannot cast mixed to string.
preferCSSPageSize: isset($options['preferCSSPageSize']) ? (bool) $options['preferCSSPageSize'] : null,
tagged: isset($options['tagged']) ? (bool) $options['tagged'] : null,
);
}

public function path(): ?string
{
return $this->path;
}

public function withPath(?string $path): self
{
return new self(
path: $path,
format: $this->format,
landscape: $this->landscape,
scale: $this->scale,
printBackground: $this->printBackground,
width: $this->width,
height: $this->height,
margin: $this->margin,
displayHeaderFooter: $this->displayHeaderFooter,
footerTemplate: $this->footerTemplate,
headerTemplate: $this->headerTemplate,
outline: $this->outline,
pageRanges: $this->pageRanges,
preferCSSPageSize: $this->preferCSSPageSize,
tagged: $this->tagged,
);
}

/**
* @return PdfOptionsArray
*/
public function toArray(): array
{
$options = [];

if (null !== $this->path) {
$options['path'] = $this->path;
}
if (null !== $this->format) {
$options['format'] = $this->format;
}
if (null !== $this->landscape) {
$options['landscape'] = $this->landscape;
}
if (null !== $this->scale) {
$options['scale'] = $this->scale;
}
if (null !== $this->printBackground) {
$options['printBackground'] = $this->printBackground;
}
if (null !== $this->width) {
$options['width'] = $this->width;
}
if (null !== $this->height) {
$options['height'] = $this->height;
}
if (null !== $this->margin) {
$options['margin'] = $this->margin;
}
if (null !== $this->displayHeaderFooter) {
$options['displayHeaderFooter'] = $this->displayHeaderFooter;
}
if (null !== $this->footerTemplate) {
$options['footerTemplate'] = $this->footerTemplate;
}
if (null !== $this->headerTemplate) {
$options['headerTemplate'] = $this->headerTemplate;
}
if (null !== $this->outline) {
$options['outline'] = $this->outline;
}
if (null !== $this->pageRanges) {
$options['pageRanges'] = $this->pageRanges;
}
if (null !== $this->preferCSSPageSize) {
$options['preferCSSPageSize'] = $this->preferCSSPageSize;
}
if (null !== $this->tagged) {
$options['tagged'] = $this->tagged;
}

return $options;
}

private static function normalizeNullableString(?string $value): ?string
{
if (null === $value) {
return null;
}

$trimmed = trim($value);

return '' === $trimmed ? null : $trimmed;
}

/**
* @return PdfMargins|null
*/
private static function normalizeMargin(mixed $margin): ?array
{
if (null === $margin) {
return null;
}

if (!is_array($margin)) {
throw new RuntimeException('PDF option "margin" must be an array of edge => size.');
}

$normalized = [];
foreach (['top', 'right', 'bottom', 'left'] as $edge) {
if (!array_key_exists($edge, $margin)) {
continue;
}

$value = $margin[$edge];
if (null === $value) {
continue;
}

$normalizedValue = self::normalizeNullableString((string) $value);

Check failure on line 265 in src/Page/Options/PdfOptions.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Cannot cast mixed to string.
if (null !== $normalizedValue) {
$normalized[$edge] = $normalizedValue;
}
}

return [] === $normalized ? null : $normalized;
}
}
Loading
Loading