Skip to content

Commit 1de1e96

Browse files
authored
Merge pull request #18 from SynergiTech/screenshot
feat: Add screenshot support
2 parents b6ca88f + 8a1b00a commit 1de1e96

File tree

11 files changed

+244
-138
lines changed

11 files changed

+244
-138
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,9 @@ jobs:
88
strategy:
99
fail-fast: false
1010
matrix:
11-
php: [7.3, 7.4, "8.0", 8.1, 8.2, 8.3]
11+
php: [8.1, 8.2, 8.3]
1212
symfony_process: [4, 5, 6, 7]
1313
exclude:
14-
- php: 7.3
15-
symfony_process: 6
16-
- php: 7.4
17-
symfony_process: 6
18-
- php: 7.3
19-
symfony_process: 7
20-
- php: 7.4
21-
symfony_process: 7
22-
- php: "8.0"
23-
symfony_process: 7
2414
- php: 8.1
2515
symfony_process: 7
2616

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG PHP_VERSION=8.0
1+
ARG PHP_VERSION=8.1
22
FROM php:$PHP_VERSION-cli-alpine
33

44
RUN apk add git zip unzip autoconf make g++

README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# PHP ChromePDF Renderer
2-
![Unit tests](https://github.com/SynergiTech/chrome-pdf-php/workflows/Unit%20tests/badge.svg)
32
[![Tests](https://github.com/SynergiTech/chrome-pdf-php/actions/workflows/main.yml/badge.svg)](https://github.com/SynergiTech/chrome-pdf-php/actions/workflows/main.yml)
43

54
_For pre-V1 documentation [click here](https://github.com/SynergiTech/chrome-pdf-php/blob/v0/README.md)_
65

6+
_For pre-V3 documentation [click here](https://github.com/SynergiTech/chrome-pdf-php/blob/v2/README.md)_
7+
78
This is a library for creating PDFs from HTML rendered with the SkPDF backend via Chrome. In order to do this, you can opt to use one of the supported drivers:
89
* [SynergiTech/chrome-pdf](https://github.com/SynergiTech/chrome-pdf)
9-
* [browserless](https://www.browserless.io/)
10+
* [Browserless](https://www.browserless.io/)
1011

1112
## Installation
1213
```
@@ -15,8 +16,8 @@ composer require synergitech/chrome-pdf-php
1516
### chrome-pdf
1617
If you are planning to use the [`chrome-pdf`](https://github.com/SynergiTech/chrome-pdf) driver to render PDFs locally, you should also make sure to install this from npm.
1718

18-
### browserless
19-
If you are planning to use the [browserless](https://www.browserless.io/) driver to render PDFs remotely, you should register for an API key. Remember that local assets cannot be rendered by browserless.
19+
### Browserless
20+
If you are planning to use the [Browserless](https://www.browserless.io/) driver to render PDFs remotely or take screenshots, you should register for an API key. Remember that local assets cannot be rendered by Browserless.
2021

2122
## Usage
2223
A common interface is provided via AbstractPDF. The options presented via this class will be available from all drivers.
@@ -33,5 +34,31 @@ $pdf = new Browserless('your-api-key');
3334
$pdf->renderContent('<h1>test</h1>');
3435
```
3536

37+
### Advanced Browserless Usage
38+
39+
You can optionally use specific endpoints when you create a client.
40+
41+
```php
42+
use SynergiTech\ChromePDF\Browserless;
43+
44+
$pdf = new Browserless('your-api-key', Browserless\EndpointsEnum::London);
45+
```
46+
47+
As this library essentially functions as an API client for Browserless, we have also implemented the screenshot API.
48+
49+
```php
50+
use SynergiTech\ChromePDF\Browserless;
51+
52+
// For information on options, see https://www.browserless.io/docs/screenshot#custom-options.
53+
// `render()` defaults to using png and fullPage set to false
54+
// but jpegs will get a quality of 75 set if you don't set one
55+
$file = new Browserless\Screenshot('your-api-key');
56+
$file->render('https://example.com');
57+
$file->render('https://example.com', [
58+
'fullPage' => true,
59+
'type' => 'jpeg',
60+
]);
61+
```
62+
3663
## Examples
3764
Some examples can be found in the `examples` folder.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
}
1111
],
1212
"require": {
13-
"php": "^7.3|^8.0|^8.1",
13+
"php": "^8.1",
1414
"symfony/process": "~4.0 || ~5.0 || ~6.0 || ~7.0",
1515
"guzzlehttp/guzzle": "^6.3 || ^7.0"
1616
},

src/Browserless.php

Lines changed: 5 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,20 @@
22

33
namespace SynergiTech\ChromePDF;
44

5-
use GuzzleHttp\Psr7\StreamWrapper;
5+
use SynergiTech\ChromePDF\Browserless\Client;
66

77
/**
88
* Driver to render PDFs remotely using browserless.io
99
*/
1010
class Browserless extends AbstractPDF
1111
{
12-
/**
13-
* @var string|null
14-
*/
15-
private $apiKey;
16-
/**
17-
* @var string
18-
*/
19-
private $apiUrl = 'https://chrome.browserless.io';
12+
use Client;
13+
2014
/**
2115
* @var string
2216
*/
2317
private $pdfEndpoint = '/pdf';
24-
/**
25-
* @var \GuzzleHttp\Client
26-
*/
27-
private $client;
18+
2819
/**
2920
* @var bool
3021
*/
@@ -38,25 +29,6 @@ class Browserless extends AbstractPDF
3829
*/
3930
private $timeout;
4031

41-
/**
42-
* @param string $apiKey api key from browserless.io
43-
* @param \GuzzleHttp\Client $client custom Guzzle client
44-
*/
45-
public function __construct(string $apiKey = null, $client = null)
46-
{
47-
if ($client === null) {
48-
// @codeCoverageIgnoreStart
49-
$client = new \GuzzleHttp\Client([
50-
'base_uri' => $this->apiUrl,
51-
]);
52-
// @codeCoverageIgnoreEnd
53-
}
54-
$this->client = $client;
55-
if ($apiKey !== null) {
56-
$this->setApiKey($apiKey);
57-
}
58-
}
59-
6032
/**
6133
* Sets the PDF documents rotation
6234
*
@@ -69,18 +41,6 @@ public function setRotation(int $rotation = null): self
6941
return $this;
7042
}
7143

72-
/**
73-
* Sets the browserless API key
74-
*
75-
* @param string $apiKey
76-
* @return self
77-
*/
78-
public function setApiKey(string $apiKey): self
79-
{
80-
$this->apiKey = $apiKey;
81-
return $this;
82-
}
83-
8444
/**
8545
* Sets whether or not to ask Browserless to attempt to render the document in safe mode
8646
*
@@ -116,16 +76,6 @@ public function getTimeout(): ?int
11676
return $this->timeout;
11777
}
11878

119-
/**
120-
* Retrieves the browserless.io API key
121-
*
122-
* @return string|null
123-
*/
124-
public function getApiKey(): ?string
125-
{
126-
return $this->apiKey;
127-
}
128-
12979
/**
13080
* Whether the document will be rendered in safe mode or not
13181
*
@@ -232,41 +182,7 @@ public function getFormattedOptions(): array
232182
*/
233183
private function render(array $options)
234184
{
235-
try {
236-
$response = $this->client->post($this->pdfEndpoint, [
237-
'query' => [
238-
'token' => $this->getApiKey(),
239-
],
240-
'json' => $options,
241-
]);
242-
} catch (\GuzzleHttp\Exception\ClientException $e) {
243-
$message = 'No response';
244-
245-
$response = $e->getResponse();
246-
247-
/**
248-
* You could use $e->hasResponse() but that is not accurate enough,
249-
* as phpstan will be analysing against method signatures from guzzle 6 & 7
250-
*/
251-
if ($response !== null) {
252-
$message = $response->getBody();
253-
254-
$json = json_decode($message);
255-
if (json_last_error() === JSON_ERROR_NONE) {
256-
$messages = [];
257-
foreach ($json as $error) {
258-
$messages[] = $error->message;
259-
}
260-
$message = implode(', ', $messages);
261-
}
262-
}
263-
264-
throw new Browserless\APIException("Failed to render PDF: {$message}", $e->getCode(), $e);
265-
} catch (\Exception $e) {
266-
throw new Browserless\APIException("Failed to render PDF: {$e->getMessage()}", $e->getCode(), $e);
267-
}
268-
269-
return StreamWrapper::getResource($response->getBody());
185+
return $this->request($this->pdfEndpoint, $options);
270186
}
271187

272188
/**

src/Browserless/Client.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace SynergiTech\ChromePDF\Browserless;
4+
5+
use GuzzleHttp\Psr7\StreamWrapper;
6+
7+
trait Client
8+
{
9+
/**
10+
* @var \GuzzleHttp\Client
11+
*/
12+
private $client;
13+
14+
/**
15+
* @var string|null
16+
*/
17+
private $apiKey;
18+
19+
/**
20+
* @param \GuzzleHttp\Client $client custom Guzzle client
21+
*/
22+
public function __construct(
23+
string $apiKey = null,
24+
EndpointsEnum|string $endpoint = EndpointsEnum::Default,
25+
$client = null
26+
) {
27+
if ($client === null) {
28+
// @codeCoverageIgnoreStart
29+
$client = new \GuzzleHttp\Client([
30+
'base_uri' => ($endpoint instanceof EndpointsEnum) ? $endpoint->value : $endpoint,
31+
]);
32+
// @codeCoverageIgnoreEnd
33+
}
34+
$this->client = $client;
35+
if ($apiKey !== null) {
36+
$this->setApiKey($apiKey);
37+
}
38+
}
39+
40+
/**
41+
* Retrieves the browserless.io API key
42+
*
43+
* @return string|null
44+
*/
45+
public function getApiKey(): ?string
46+
{
47+
return $this->apiKey;
48+
}
49+
50+
/**
51+
* @param array<mixed> $json
52+
*
53+
* @return resource
54+
*/
55+
protected function request(string $endpoint, array $json)
56+
{
57+
try {
58+
$response = $this->client->post($endpoint, [
59+
'query' => [
60+
'token' => $this->getApiKey(),
61+
],
62+
'json' => $json,
63+
]);
64+
} catch (\GuzzleHttp\Exception\ClientException $e) {
65+
$message = 'No response';
66+
67+
$response = $e->getResponse();
68+
69+
/**
70+
* You could use $e->hasResponse() but that is not accurate enough,
71+
* as phpstan will be analysing against method signatures from guzzle 6 & 7
72+
*/
73+
if ($response !== null) {
74+
$message = $response->getBody();
75+
76+
$json = json_decode($message);
77+
if (json_last_error() === JSON_ERROR_NONE) {
78+
$messages = [];
79+
foreach ($json as $error) {
80+
$messages[] = $error->message;
81+
}
82+
$message = implode(', ', $messages);
83+
}
84+
}
85+
86+
throw new APIException("Failed to render from Browserless: {$message}", $e->getCode(), $e);
87+
} catch (\Exception $e) {
88+
throw new APIException("Failed to render from Browserless: {$e->getMessage()}", $e->getCode(), $e);
89+
}
90+
91+
return StreamWrapper::getResource($response->getBody());
92+
}
93+
94+
/**
95+
* Sets the browserless API key
96+
*
97+
* @param string $apiKey
98+
* @return self
99+
*/
100+
private function setApiKey(string $apiKey): self
101+
{
102+
$this->apiKey = $apiKey;
103+
return $this;
104+
}
105+
}

src/Browserless/EndpointsEnum.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace SynergiTech\ChromePDF\Browserless;
4+
5+
enum EndpointsEnum: string
6+
{
7+
case Default = 'https://chrome.browserless.io';
8+
case London = 'https://production-lon.browserless.io';
9+
case SanFrancisco = 'https://production-sfo.browserless.io';
10+
}

src/Browserless/Screenshot.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace SynergiTech\ChromePDF\Browserless;
4+
5+
class Screenshot
6+
{
7+
use Client;
8+
9+
/**
10+
* @param array<string,mixed> $options see https://www.browserless.io/docs/screenshot#custom-options
11+
*
12+
* @return resource
13+
*/
14+
public function render(string $url, array $options = [])
15+
{
16+
$options = array_merge([
17+
'type' => 'png',
18+
'fullPage' => false,
19+
], $options);
20+
21+
if ($options['type'] === 'jpeg' && ! isset($options['quality'])) {
22+
$options['quality'] = 75;
23+
}
24+
25+
return $this->request(
26+
endpoint: '/screenshot',
27+
json: [
28+
'url' => $url,
29+
'options' => $options,
30+
],
31+
);
32+
}
33+
}

0 commit comments

Comments
 (0)