-
Notifications
You must be signed in to change notification settings - Fork 437
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implementation of chromium headless
- Loading branch information
Showing
12 changed files
with
291 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium; | ||
|
||
interface ExtraOption | ||
{ | ||
public function isRepeatable(): bool; | ||
|
||
/** @return array<float|int|string> */ | ||
public function compile(): array; | ||
} |
24 changes: 24 additions & 0 deletions
24
src/Backend/HeadlessChromium/ExtraOption/DisableFeatures.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
class DisableFeatures implements ExtraOption | ||
{ | ||
public function __construct(private readonly array $features) | ||
{ | ||
} | ||
|
||
public function isRepeatable(): bool | ||
{ | ||
return false; | ||
} | ||
|
||
public function compile(): array | ||
{ | ||
return ['--disable-features=' . \implode(',', $this->features)]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
class DisableGpu implements ExtraOption | ||
{ | ||
public function isRepeatable(): bool | ||
{ | ||
return false; | ||
} | ||
|
||
public function compile(): array | ||
{ | ||
return ['--disable-gpu']; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
class Headless implements ExtraOption | ||
{ | ||
public function isRepeatable(): bool | ||
{ | ||
return false; | ||
} | ||
|
||
public function compile(): array | ||
{ | ||
return ['--headless']; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
class NoSandbox implements ExtraOption | ||
{ | ||
public function isRepeatable(): bool | ||
{ | ||
return false; | ||
} | ||
|
||
public function compile(): array | ||
{ | ||
return ['--no-sandbox']; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
use SplFileInfo; | ||
|
||
class PrintToPdf implements ExtraOption | ||
{ | ||
public function __construct(private readonly SplFileInfo $file) | ||
{ | ||
} | ||
|
||
public function isRepeatable(): bool | ||
{ | ||
return false; | ||
} | ||
|
||
public function compile(): array | ||
{ | ||
return ['--print-to-pdf=' . $this->file]; | ||
} | ||
|
||
public function getFile(): SplFileInfo | ||
{ | ||
return $this->file; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption; | ||
|
||
class WindowSize implements ExtraOption | ||
{ | ||
public function __construct(private readonly int $width, private readonly int $height) | ||
{ | ||
} | ||
|
||
public function isRepeatable(): bool | ||
{ | ||
return false; | ||
} | ||
|
||
public function compile(): array | ||
{ | ||
return ['--window-size', "{$this->width}x{$this->height}"]; | ||
} | ||
} |
130 changes: 62 additions & 68 deletions
130
src/Backend/HeadlessChromium/HeadlessChromiumAdapter.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,112 +1,106 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium; | ||
|
||
use KNPLabs\Snappy\Core\Backend\Adapter\UriToPdf; | ||
use KNPLabs\Snappy\Core\Backend\Adapter\HtmlFileToPdf; | ||
use KNPLabs\Snappy\Core\Backend\Adapter\HtmlToPdf; | ||
use KNPLabs\Snappy\Core\Backend\Adapter\Reconfigurable; | ||
use KNPLabs\Snappy\Core\Backend\Options; | ||
use Psr\Http\Message\StreamFactoryInterface; | ||
use Psr\Http\Message\StreamInterface; | ||
use Psr\Http\Message\UriFactoryInterface; | ||
use Psr\Http\Message\UriInterface; | ||
use SplFileInfo; | ||
use Symfony\Component\Process\Exception\ProcessFailedException; | ||
use Symfony\Component\Process\Process; | ||
use InvalidArgumentException; | ||
use RuntimeException; | ||
|
||
final class HeadlessChromiumAdapter implements UriToPdf, HtmlFileToPdf, HtmlToPdf | ||
final class HeadlessChromiumAdapter implements UriToPdf | ||
{ | ||
private string $tempDir; | ||
|
||
/** | ||
* @use Reconfigurable<self> | ||
*/ | ||
use Reconfigurable; | ||
|
||
private string $tempDir; | ||
|
||
public function __construct( | ||
private Options $options, | ||
private StreamFactoryInterface $streamFactory | ||
) {} | ||
private string $binary, | ||
private int $timeout, | ||
HeadlessChromiumFactory $factory, | ||
Options $options, | ||
private readonly StreamFactoryInterface $streamFactory, | ||
private readonly UriFactoryInterface $uriFactory, | ||
) { | ||
$this->tempDir = __DIR__; | ||
self::validateOptions($options); | ||
|
||
$this->factory = $factory; | ||
$this->options = $options; | ||
} | ||
|
||
public function generateFromUri(UriInterface $url): StreamInterface | ||
{ | ||
$this->tempDir = sys_get_temp_dir(); | ||
$process = new Process( | ||
command: [ | ||
$this->binary, | ||
...$this->compileOptions(), | ||
(string) $url, | ||
], | ||
timeout: $this->timeout | ||
); | ||
|
||
$command = $this->buildChromiumCommand((string) $url, $this->tempDir); | ||
$this->runProcess($command); | ||
$process->run(); | ||
|
||
return $this->createStreamFromFile($this->tempDir); | ||
return $this->streamFactory->createStream($this->getPrintToPdfFilePath()); | ||
} | ||
|
||
public function generateFromHtmlFile(SplFileInfo $file): StreamInterface | ||
public function getPrintToPdfFilePath(): string | ||
{ | ||
$htmlContent = file_get_contents($file->getPathname()); | ||
return $this->generateFromHtml($htmlContent); | ||
} | ||
$printToPdfOption = \array_filter( | ||
$this->options->extraOptions, | ||
fn ($option) => $option instanceof ExtraOption\PrintToPdf | ||
); | ||
|
||
public function generateFromHtml(string $html): StreamInterface | ||
{ | ||
$outputFile = $this->tempDir . '/pdf_output_'; | ||
$htmlFile = $this->tempDir . '/html_input_'; | ||
file_put_contents($htmlFile, $html); | ||
if (!empty($printToPdfOption)) { | ||
$printToPdfOption = \array_values($printToPdfOption)[0]; | ||
|
||
$command = $this->buildChromiumCommand("file://$htmlFile", $outputFile); | ||
$this->runProcess($command); | ||
return $printToPdfOption->getFile()->getPathname(); | ||
} | ||
|
||
unlink($htmlFile); | ||
return $this->createStreamFromFile($outputFile); | ||
throw new RuntimeException('Missing option print to pdf.'); | ||
} | ||
|
||
/** | ||
* @return array<string> | ||
*/ | ||
private function buildChromiumCommand(string $inputUri, string $outputPath): array | ||
private static function validateOptions(Options $options): void | ||
{ | ||
$options = $this->compileConstructOptions(); | ||
|
||
return array_merge([ | ||
'chromium', | ||
'--headless', | ||
'--disable-gpu', | ||
'--no-sandbox', | ||
'--print-to-pdf=' . $outputPath, | ||
], $options, [$inputUri]); | ||
} | ||
$optionTypes = []; | ||
|
||
/** | ||
* @return array<string> | ||
*/ | ||
private function compileConstructOptions(): array | ||
{ | ||
$constructOptions = $this->options->extraOptions['construct'] ?? []; | ||
|
||
$compiledOptions = []; | ||
if (is_array($constructOptions)) { | ||
foreach ($constructOptions as $key => $value) { | ||
$compiledOptions[] = "--$key=$value"; | ||
foreach ($options->extraOptions as $option) { | ||
if (!$option instanceof ExtraOption) { | ||
throw new InvalidArgumentException(\sprintf('Invalid option type provided. Expected "%s", received "%s".', ExtraOption::class, \gettype($option) === 'object' ? \get_class($option) : \gettype($option), )); | ||
} | ||
} | ||
|
||
return $compiledOptions; | ||
} | ||
|
||
private function runProcess(array $command): void | ||
{ | ||
$process = new Process($command); | ||
$process->run(); | ||
if (\in_array($option::class, $optionTypes, true) && !$option->isRepeatable()) { | ||
throw new InvalidArgumentException(\sprintf('Duplicate option type provided: "%s".', $option::class, )); | ||
} | ||
|
||
if (!$process->isSuccessful()) { | ||
throw new ProcessFailedException($process); | ||
$optionTypes[] = $option::class; | ||
} | ||
} | ||
|
||
private function createStreamFromFile(string $filePath): StreamInterface | ||
/** | ||
* @return array<float|int|string> | ||
*/ | ||
private function compileOptions(): array | ||
{ | ||
$output = file_get_contents($filePath); | ||
unlink($filePath); | ||
|
||
return $this->streamFactory->createStream($output ?: ''); | ||
return \array_reduce( | ||
$this->options->extraOptions, | ||
fn (array $carry, ExtraOption $extraOption) => $this->options->pageOrientation !== null | ||
?: [ | ||
...$carry, | ||
...$extraOption->compile(), | ||
], | ||
[], | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,36 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
declare(strict_types = 1); | ||
|
||
namespace KNPLabs\Snappy\Backend\HeadlessChromium; | ||
|
||
use KNPLabs\Snappy\Core\Backend\Factory; | ||
use KNPLabs\Snappy\Core\Backend\Options; | ||
use Psr\Http\Message\StreamFactoryInterface; | ||
use Psr\Http\Message\UriFactoryInterface; | ||
|
||
final class HeadlessChromiumFactory | ||
/** | ||
* @implements Factory<HeadlessChromiumAdapter> | ||
*/ | ||
final class HeadlessChromiumFactory implements Factory | ||
{ | ||
public function __construct( | ||
private StreamFactoryInterface $streamFactory | ||
) {} | ||
private readonly string $binary, | ||
private readonly int $timeout, | ||
private readonly StreamFactoryInterface $streamFactory, | ||
private readonly UriFactoryInterface $uriFactory, | ||
) { | ||
} | ||
|
||
public function create(Options $options): HeadlessChromiumAdapter | ||
{ | ||
return new HeadlessChromiumAdapter($options, $this->streamFactory); | ||
return new HeadlessChromiumAdapter( | ||
$this->binary, | ||
$this->timeout, | ||
$this, | ||
$options, | ||
$this->streamFactory, | ||
$this->uriFactory, | ||
); | ||
} | ||
} |
Oops, something went wrong.