Skip to content

Commit

Permalink
feat: implementation of chromium headless
Browse files Browse the repository at this point in the history
  • Loading branch information
Arthurlbc committed Nov 6, 2024
1 parent 5c859d0 commit 412d207
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 169 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"psr-4": {
"KNPLabs\\Snappy\\Backend\\Dompdf\\": "src/Backend/Dompdf/",
"KNPLabs\\Snappy\\Backend\\WkHtmlToPdf\\": "src/Backend/WkHtmlToPdf/",
"KNPLabs\\Snappy\\Backend\\HeadlessChromium\\": "src/Backend/HeadlessChromium/",
"KNPLabs\\Snappy\\Core\\": "src/Core/",
"KNPLabs\\Snappy\\Framework\\Symfony\\": "src/Framework/Symfony/"
}
Expand Down
13 changes: 13 additions & 0 deletions src/Backend/HeadlessChromium/ExtraOption.php
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 src/Backend/HeadlessChromium/ExtraOption/DisableFeatures.php
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)];
}
}
20 changes: 20 additions & 0 deletions src/Backend/HeadlessChromium/ExtraOption/DisableGpu.php
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'];
}
}
20 changes: 20 additions & 0 deletions src/Backend/HeadlessChromium/ExtraOption/Headless.php
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'];
}
}
20 changes: 20 additions & 0 deletions src/Backend/HeadlessChromium/ExtraOption/NoSandbox.php
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'];
}
}
30 changes: 30 additions & 0 deletions src/Backend/HeadlessChromium/ExtraOption/PrintToPdf.php
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;
}
}
24 changes: 24 additions & 0 deletions src/Backend/HeadlessChromium/ExtraOption/WindowSize.php
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 src/Backend/HeadlessChromium/HeadlessChromiumAdapter.php
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(),
],
[],
);
}
}
26 changes: 21 additions & 5 deletions src/Backend/HeadlessChromium/HeadlessChromiumFactory.php
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,
);
}
}
Loading

0 comments on commit 412d207

Please sign in to comment.