Skip to content

Commit

Permalink
Merge pull request #18 from SynergiTech/screenshot
Browse files Browse the repository at this point in the history
feat: Add screenshot support
  • Loading branch information
willpower232 authored Apr 10, 2024
2 parents b6ca88f + 8a1b00a commit 1de1e96
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 138 deletions.
12 changes: 1 addition & 11 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,9 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [7.3, 7.4, "8.0", 8.1, 8.2, 8.3]
php: [8.1, 8.2, 8.3]
symfony_process: [4, 5, 6, 7]
exclude:
- php: 7.3
symfony_process: 6
- php: 7.4
symfony_process: 6
- php: 7.3
symfony_process: 7
- php: 7.4
symfony_process: 7
- php: "8.0"
symfony_process: 7
- php: 8.1
symfony_process: 7

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG PHP_VERSION=8.0
ARG PHP_VERSION=8.1
FROM php:$PHP_VERSION-cli-alpine

RUN apk add git zip unzip autoconf make g++
Expand Down
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# PHP ChromePDF Renderer
![Unit tests](https://github.com/SynergiTech/chrome-pdf-php/workflows/Unit%20tests/badge.svg)
[![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)

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

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

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:
* [SynergiTech/chrome-pdf](https://github.com/SynergiTech/chrome-pdf)
* [browserless](https://www.browserless.io/)
* [Browserless](https://www.browserless.io/)

## Installation
```
Expand All @@ -15,8 +16,8 @@ composer require synergitech/chrome-pdf-php
### chrome-pdf
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.

### browserless
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.
### Browserless
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.

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

### Advanced Browserless Usage

You can optionally use specific endpoints when you create a client.

```php
use SynergiTech\ChromePDF\Browserless;

$pdf = new Browserless('your-api-key', Browserless\EndpointsEnum::London);
```

As this library essentially functions as an API client for Browserless, we have also implemented the screenshot API.

```php
use SynergiTech\ChromePDF\Browserless;

// For information on options, see https://www.browserless.io/docs/screenshot#custom-options.
// `render()` defaults to using png and fullPage set to false
// but jpegs will get a quality of 75 set if you don't set one
$file = new Browserless\Screenshot('your-api-key');
$file->render('https://example.com');
$file->render('https://example.com', [
'fullPage' => true,
'type' => 'jpeg',
]);
```

## Examples
Some examples can be found in the `examples` folder.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
}
],
"require": {
"php": "^7.3|^8.0|^8.1",
"php": "^8.1",
"symfony/process": "~4.0 || ~5.0 || ~6.0 || ~7.0",
"guzzlehttp/guzzle": "^6.3 || ^7.0"
},
Expand Down
94 changes: 5 additions & 89 deletions src/Browserless.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,20 @@

namespace SynergiTech\ChromePDF;

use GuzzleHttp\Psr7\StreamWrapper;
use SynergiTech\ChromePDF\Browserless\Client;

/**
* Driver to render PDFs remotely using browserless.io
*/
class Browserless extends AbstractPDF
{
/**
* @var string|null
*/
private $apiKey;
/**
* @var string
*/
private $apiUrl = 'https://chrome.browserless.io';
use Client;

/**
* @var string
*/
private $pdfEndpoint = '/pdf';
/**
* @var \GuzzleHttp\Client
*/
private $client;

/**
* @var bool
*/
Expand All @@ -38,25 +29,6 @@ class Browserless extends AbstractPDF
*/
private $timeout;

/**
* @param string $apiKey api key from browserless.io
* @param \GuzzleHttp\Client $client custom Guzzle client
*/
public function __construct(string $apiKey = null, $client = null)
{
if ($client === null) {
// @codeCoverageIgnoreStart
$client = new \GuzzleHttp\Client([
'base_uri' => $this->apiUrl,
]);
// @codeCoverageIgnoreEnd
}
$this->client = $client;
if ($apiKey !== null) {
$this->setApiKey($apiKey);
}
}

/**
* Sets the PDF documents rotation
*
Expand All @@ -69,18 +41,6 @@ public function setRotation(int $rotation = null): self
return $this;
}

/**
* Sets the browserless API key
*
* @param string $apiKey
* @return self
*/
public function setApiKey(string $apiKey): self
{
$this->apiKey = $apiKey;
return $this;
}

/**
* Sets whether or not to ask Browserless to attempt to render the document in safe mode
*
Expand Down Expand Up @@ -116,16 +76,6 @@ public function getTimeout(): ?int
return $this->timeout;
}

/**
* Retrieves the browserless.io API key
*
* @return string|null
*/
public function getApiKey(): ?string
{
return $this->apiKey;
}

/**
* Whether the document will be rendered in safe mode or not
*
Expand Down Expand Up @@ -232,41 +182,7 @@ public function getFormattedOptions(): array
*/
private function render(array $options)
{
try {
$response = $this->client->post($this->pdfEndpoint, [
'query' => [
'token' => $this->getApiKey(),
],
'json' => $options,
]);
} catch (\GuzzleHttp\Exception\ClientException $e) {
$message = 'No response';

$response = $e->getResponse();

/**
* You could use $e->hasResponse() but that is not accurate enough,
* as phpstan will be analysing against method signatures from guzzle 6 & 7
*/
if ($response !== null) {
$message = $response->getBody();

$json = json_decode($message);
if (json_last_error() === JSON_ERROR_NONE) {
$messages = [];
foreach ($json as $error) {
$messages[] = $error->message;
}
$message = implode(', ', $messages);
}
}

throw new Browserless\APIException("Failed to render PDF: {$message}", $e->getCode(), $e);
} catch (\Exception $e) {
throw new Browserless\APIException("Failed to render PDF: {$e->getMessage()}", $e->getCode(), $e);
}

return StreamWrapper::getResource($response->getBody());
return $this->request($this->pdfEndpoint, $options);
}

/**
Expand Down
105 changes: 105 additions & 0 deletions src/Browserless/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace SynergiTech\ChromePDF\Browserless;

use GuzzleHttp\Psr7\StreamWrapper;

trait Client
{
/**
* @var \GuzzleHttp\Client
*/
private $client;

/**
* @var string|null
*/
private $apiKey;

/**
* @param \GuzzleHttp\Client $client custom Guzzle client
*/
public function __construct(
string $apiKey = null,
EndpointsEnum|string $endpoint = EndpointsEnum::Default,
$client = null
) {
if ($client === null) {
// @codeCoverageIgnoreStart
$client = new \GuzzleHttp\Client([
'base_uri' => ($endpoint instanceof EndpointsEnum) ? $endpoint->value : $endpoint,
]);
// @codeCoverageIgnoreEnd
}
$this->client = $client;
if ($apiKey !== null) {
$this->setApiKey($apiKey);
}
}

/**
* Retrieves the browserless.io API key
*
* @return string|null
*/
public function getApiKey(): ?string
{
return $this->apiKey;
}

/**
* @param array<mixed> $json
*
* @return resource
*/
protected function request(string $endpoint, array $json)
{
try {
$response = $this->client->post($endpoint, [
'query' => [
'token' => $this->getApiKey(),
],
'json' => $json,
]);
} catch (\GuzzleHttp\Exception\ClientException $e) {
$message = 'No response';

$response = $e->getResponse();

/**
* You could use $e->hasResponse() but that is not accurate enough,
* as phpstan will be analysing against method signatures from guzzle 6 & 7
*/
if ($response !== null) {
$message = $response->getBody();

$json = json_decode($message);
if (json_last_error() === JSON_ERROR_NONE) {
$messages = [];
foreach ($json as $error) {
$messages[] = $error->message;
}
$message = implode(', ', $messages);
}
}

throw new APIException("Failed to render from Browserless: {$message}", $e->getCode(), $e);
} catch (\Exception $e) {
throw new APIException("Failed to render from Browserless: {$e->getMessage()}", $e->getCode(), $e);
}

return StreamWrapper::getResource($response->getBody());
}

/**
* Sets the browserless API key
*
* @param string $apiKey
* @return self
*/
private function setApiKey(string $apiKey): self
{
$this->apiKey = $apiKey;
return $this;
}
}
10 changes: 10 additions & 0 deletions src/Browserless/EndpointsEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace SynergiTech\ChromePDF\Browserless;

enum EndpointsEnum: string
{
case Default = 'https://chrome.browserless.io';
case London = 'https://production-lon.browserless.io';
case SanFrancisco = 'https://production-sfo.browserless.io';
}
33 changes: 33 additions & 0 deletions src/Browserless/Screenshot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace SynergiTech\ChromePDF\Browserless;

class Screenshot
{
use Client;

/**
* @param array<string,mixed> $options see https://www.browserless.io/docs/screenshot#custom-options
*
* @return resource
*/
public function render(string $url, array $options = [])
{
$options = array_merge([
'type' => 'png',
'fullPage' => false,
], $options);

if ($options['type'] === 'jpeg' && ! isset($options['quality'])) {
$options['quality'] = 75;
}

return $this->request(
endpoint: '/screenshot',
json: [
'url' => $url,
'options' => $options,
],
);
}
}
Loading

0 comments on commit 1de1e96

Please sign in to comment.