Skip to content

Commit 412d207

Browse files
committed
feat: implementation of chromium headless
1 parent 5c859d0 commit 412d207

File tree

12 files changed

+291
-169
lines changed

12 files changed

+291
-169
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"psr-4": {
5252
"KNPLabs\\Snappy\\Backend\\Dompdf\\": "src/Backend/Dompdf/",
5353
"KNPLabs\\Snappy\\Backend\\WkHtmlToPdf\\": "src/Backend/WkHtmlToPdf/",
54+
"KNPLabs\\Snappy\\Backend\\HeadlessChromium\\": "src/Backend/HeadlessChromium/",
5455
"KNPLabs\\Snappy\\Core\\": "src/Core/",
5556
"KNPLabs\\Snappy\\Framework\\Symfony\\": "src/Framework/Symfony/"
5657
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace KNPLabs\Snappy\Backend\HeadlessChromium;
6+
7+
interface ExtraOption
8+
{
9+
public function isRepeatable(): bool;
10+
11+
/** @return array<float|int|string> */
12+
public function compile(): array;
13+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
6+
7+
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
8+
9+
class DisableFeatures implements ExtraOption
10+
{
11+
public function __construct(private readonly array $features)
12+
{
13+
}
14+
15+
public function isRepeatable(): bool
16+
{
17+
return false;
18+
}
19+
20+
public function compile(): array
21+
{
22+
return ['--disable-features=' . \implode(',', $this->features)];
23+
}
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
6+
7+
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
8+
9+
class DisableGpu implements ExtraOption
10+
{
11+
public function isRepeatable(): bool
12+
{
13+
return false;
14+
}
15+
16+
public function compile(): array
17+
{
18+
return ['--disable-gpu'];
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
6+
7+
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
8+
9+
class Headless implements ExtraOption
10+
{
11+
public function isRepeatable(): bool
12+
{
13+
return false;
14+
}
15+
16+
public function compile(): array
17+
{
18+
return ['--headless'];
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
6+
7+
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
8+
9+
class NoSandbox implements ExtraOption
10+
{
11+
public function isRepeatable(): bool
12+
{
13+
return false;
14+
}
15+
16+
public function compile(): array
17+
{
18+
return ['--no-sandbox'];
19+
}
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
6+
7+
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
8+
use SplFileInfo;
9+
10+
class PrintToPdf implements ExtraOption
11+
{
12+
public function __construct(private readonly SplFileInfo $file)
13+
{
14+
}
15+
16+
public function isRepeatable(): bool
17+
{
18+
return false;
19+
}
20+
21+
public function compile(): array
22+
{
23+
return ['--print-to-pdf=' . $this->file];
24+
}
25+
26+
public function getFile(): SplFileInfo
27+
{
28+
return $this->file;
29+
}
30+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
6+
7+
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;
8+
9+
class WindowSize implements ExtraOption
10+
{
11+
public function __construct(private readonly int $width, private readonly int $height)
12+
{
13+
}
14+
15+
public function isRepeatable(): bool
16+
{
17+
return false;
18+
}
19+
20+
public function compile(): array
21+
{
22+
return ['--window-size', "{$this->width}x{$this->height}"];
23+
}
24+
}
Lines changed: 62 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,106 @@
11
<?php
22

3-
declare(strict_types=1);
3+
declare(strict_types = 1);
44

55
namespace KNPLabs\Snappy\Backend\HeadlessChromium;
66

77
use KNPLabs\Snappy\Core\Backend\Adapter\UriToPdf;
8-
use KNPLabs\Snappy\Core\Backend\Adapter\HtmlFileToPdf;
9-
use KNPLabs\Snappy\Core\Backend\Adapter\HtmlToPdf;
108
use KNPLabs\Snappy\Core\Backend\Adapter\Reconfigurable;
119
use KNPLabs\Snappy\Core\Backend\Options;
1210
use Psr\Http\Message\StreamFactoryInterface;
1311
use Psr\Http\Message\StreamInterface;
12+
use Psr\Http\Message\UriFactoryInterface;
1413
use Psr\Http\Message\UriInterface;
15-
use SplFileInfo;
16-
use Symfony\Component\Process\Exception\ProcessFailedException;
1714
use Symfony\Component\Process\Process;
15+
use InvalidArgumentException;
16+
use RuntimeException;
1817

19-
final class HeadlessChromiumAdapter implements UriToPdf, HtmlFileToPdf, HtmlToPdf
18+
final class HeadlessChromiumAdapter implements UriToPdf
2019
{
21-
private string $tempDir;
22-
2320
/**
2421
* @use Reconfigurable<self>
2522
*/
2623
use Reconfigurable;
2724

25+
private string $tempDir;
26+
2827
public function __construct(
29-
private Options $options,
30-
private StreamFactoryInterface $streamFactory
31-
) {}
28+
private string $binary,
29+
private int $timeout,
30+
HeadlessChromiumFactory $factory,
31+
Options $options,
32+
private readonly StreamFactoryInterface $streamFactory,
33+
private readonly UriFactoryInterface $uriFactory,
34+
) {
35+
$this->tempDir = __DIR__;
36+
self::validateOptions($options);
37+
38+
$this->factory = $factory;
39+
$this->options = $options;
40+
}
3241

3342
public function generateFromUri(UriInterface $url): StreamInterface
3443
{
35-
$this->tempDir = sys_get_temp_dir();
44+
$process = new Process(
45+
command: [
46+
$this->binary,
47+
...$this->compileOptions(),
48+
(string) $url,
49+
],
50+
timeout: $this->timeout
51+
);
3652

37-
$command = $this->buildChromiumCommand((string) $url, $this->tempDir);
38-
$this->runProcess($command);
53+
$process->run();
3954

40-
return $this->createStreamFromFile($this->tempDir);
55+
return $this->streamFactory->createStream($this->getPrintToPdfFilePath());
4156
}
4257

43-
public function generateFromHtmlFile(SplFileInfo $file): StreamInterface
58+
public function getPrintToPdfFilePath(): string
4459
{
45-
$htmlContent = file_get_contents($file->getPathname());
46-
return $this->generateFromHtml($htmlContent);
47-
}
60+
$printToPdfOption = \array_filter(
61+
$this->options->extraOptions,
62+
fn ($option) => $option instanceof ExtraOption\PrintToPdf
63+
);
4864

49-
public function generateFromHtml(string $html): StreamInterface
50-
{
51-
$outputFile = $this->tempDir . '/pdf_output_';
52-
$htmlFile = $this->tempDir . '/html_input_';
53-
file_put_contents($htmlFile, $html);
65+
if (!empty($printToPdfOption)) {
66+
$printToPdfOption = \array_values($printToPdfOption)[0];
5467

55-
$command = $this->buildChromiumCommand("file://$htmlFile", $outputFile);
56-
$this->runProcess($command);
68+
return $printToPdfOption->getFile()->getPathname();
69+
}
5770

58-
unlink($htmlFile);
59-
return $this->createStreamFromFile($outputFile);
71+
throw new RuntimeException('Missing option print to pdf.');
6072
}
6173

62-
/**
63-
* @return array<string>
64-
*/
65-
private function buildChromiumCommand(string $inputUri, string $outputPath): array
74+
private static function validateOptions(Options $options): void
6675
{
67-
$options = $this->compileConstructOptions();
68-
69-
return array_merge([
70-
'chromium',
71-
'--headless',
72-
'--disable-gpu',
73-
'--no-sandbox',
74-
'--print-to-pdf=' . $outputPath,
75-
], $options, [$inputUri]);
76-
}
76+
$optionTypes = [];
7777

78-
/**
79-
* @return array<string>
80-
*/
81-
private function compileConstructOptions(): array
82-
{
83-
$constructOptions = $this->options->extraOptions['construct'] ?? [];
84-
85-
$compiledOptions = [];
86-
if (is_array($constructOptions)) {
87-
foreach ($constructOptions as $key => $value) {
88-
$compiledOptions[] = "--$key=$value";
78+
foreach ($options->extraOptions as $option) {
79+
if (!$option instanceof ExtraOption) {
80+
throw new InvalidArgumentException(\sprintf('Invalid option type provided. Expected "%s", received "%s".', ExtraOption::class, \gettype($option) === 'object' ? \get_class($option) : \gettype($option), ));
8981
}
90-
}
91-
92-
return $compiledOptions;
93-
}
9482

95-
private function runProcess(array $command): void
96-
{
97-
$process = new Process($command);
98-
$process->run();
83+
if (\in_array($option::class, $optionTypes, true) && !$option->isRepeatable()) {
84+
throw new InvalidArgumentException(\sprintf('Duplicate option type provided: "%s".', $option::class, ));
85+
}
9986

100-
if (!$process->isSuccessful()) {
101-
throw new ProcessFailedException($process);
87+
$optionTypes[] = $option::class;
10288
}
10389
}
10490

105-
private function createStreamFromFile(string $filePath): StreamInterface
91+
/**
92+
* @return array<float|int|string>
93+
*/
94+
private function compileOptions(): array
10695
{
107-
$output = file_get_contents($filePath);
108-
unlink($filePath);
109-
110-
return $this->streamFactory->createStream($output ?: '');
96+
return \array_reduce(
97+
$this->options->extraOptions,
98+
fn (array $carry, ExtraOption $extraOption) => $this->options->pageOrientation !== null
99+
?: [
100+
...$carry,
101+
...$extraOption->compile(),
102+
],
103+
[],
104+
);
111105
}
112106
}
Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
<?php
22

3-
declare(strict_types=1);
3+
declare(strict_types = 1);
44

55
namespace KNPLabs\Snappy\Backend\HeadlessChromium;
66

7+
use KNPLabs\Snappy\Core\Backend\Factory;
78
use KNPLabs\Snappy\Core\Backend\Options;
89
use Psr\Http\Message\StreamFactoryInterface;
10+
use Psr\Http\Message\UriFactoryInterface;
911

10-
final class HeadlessChromiumFactory
12+
/**
13+
* @implements Factory<HeadlessChromiumAdapter>
14+
*/
15+
final class HeadlessChromiumFactory implements Factory
1116
{
1217
public function __construct(
13-
private StreamFactoryInterface $streamFactory
14-
) {}
18+
private readonly string $binary,
19+
private readonly int $timeout,
20+
private readonly StreamFactoryInterface $streamFactory,
21+
private readonly UriFactoryInterface $uriFactory,
22+
) {
23+
}
1524

1625
public function create(Options $options): HeadlessChromiumAdapter
1726
{
18-
return new HeadlessChromiumAdapter($options, $this->streamFactory);
27+
return new HeadlessChromiumAdapter(
28+
$this->binary,
29+
$this->timeout,
30+
$this,
31+
$options,
32+
$this->streamFactory,
33+
$this->uriFactory,
34+
);
1935
}
2036
}

0 commit comments

Comments
 (0)