Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/implement headless chromium #512

Draft
wants to merge 3 commits into
base: v2-unstable
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ build:
.PHONY: test
test: build
$(MAKE) -C src/Bundle test IMAGE_TAG="${IMAGE_TAG}" ARGS="${ARGS}"

.PHONY: phpstan
phpstan:
php vendor/bin/phpstan analyse --level max src/

.PHONY: php-cs-fixer
php-cs-fixer:
tools/php-cs-fixer/vendor/bin/php-cs-fixer fix ./src
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;
}
27 changes: 27 additions & 0 deletions src/Backend/HeadlessChromium/ExtraOption/DisableFeatures.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types = 1);

namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;

use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;

class DisableFeatures implements ExtraOption
{
/**
* @param array<string> $features
*/
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'];
}
}
29 changes: 29 additions & 0 deletions src/Backend/HeadlessChromium/ExtraOption/PrintToPdf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types = 1);

namespace KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;

use KNPLabs\Snappy\Backend\HeadlessChromium\ExtraOption;

class PrintToPdf implements ExtraOption
{
public function __construct(private readonly string $filePath)
{
}

public function isRepeatable(): bool
{
return false;
}

public function compile(): array
{
return ['--print-to-pdf=' . $this->filePath];
}

public function getFilePath(): string
{
return $this->filePath;
}
}
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}"];
}
}
112 changes: 112 additions & 0 deletions src/Backend/HeadlessChromium/HeadlessChromiumAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types = 1);

namespace KNPLabs\Snappy\Backend\HeadlessChromium;

use KNPLabs\Snappy\Core\Backend\Adapter\UriToPdf;
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\UriInterface;
use Symfony\Component\Process\Process;
use InvalidArgumentException;
use RuntimeException;

final class HeadlessChromiumAdapter implements UriToPdf
{
/**
* @use Reconfigurable<self>
*/
use Reconfigurable;

public function __construct(
private string $binary,
private int $timeout,
HeadlessChromiumFactory $factory,
Options $options,
private readonly StreamFactoryInterface $streamFactory,
) {
self::validateOptions($options);

$this->factory = $factory;
$this->options = $options;
}

public function generateFromUri(UriInterface $url): StreamInterface
{
$process = new Process(
command: [
$this->binary,
...$this->compileOptions(),
(string) $url,
],
timeout: $this->timeout
);

$process->run();

return $this->streamFactory->createStream($this->getPrintToPdfFilePath());
}

public function getPrintToPdfFilePath(): string
{
$printToPdfOption = \array_filter(
$this->options->extraOptions,
fn ($option) => $option instanceof ExtraOption\PrintToPdf
);

if (!empty($printToPdfOption)) {
$printToPdfOption = \array_values($printToPdfOption)[0];

return $printToPdfOption->getFilePath();
}

throw new RuntimeException('Missing option print to pdf.');
}

private static function validateOptions(Options $options): void
{
$optionTypes = [];

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), ));
}

if (\in_array($option::class, $optionTypes, true) && !$option->isRepeatable()) {
throw new InvalidArgumentException(\sprintf('Duplicate option type provided: "%s".', $option::class, ));
}

$optionTypes[] = $option::class;
}
}

/**
* @return array<mixed>
*/
private function compileOptions(): array
{
return \array_reduce(
$this->options->extraOptions,
/**
* @param array<mixed> $carry
* @param ExtraOption $extraOption
*
* @return array<mixed>
*/
function (array $carry, $extraOption) {
if ($extraOption instanceof ExtraOption) {
return [
...$carry,
...$extraOption->compile(),
];
}

return $carry;
},
[]
);
}
}
33 changes: 33 additions & 0 deletions src/Backend/HeadlessChromium/HeadlessChromiumFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

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;

/**
* @implements Factory<HeadlessChromiumAdapter>
*/
final class HeadlessChromiumFactory implements Factory
{
public function __construct(
private readonly string $binary,
private readonly int $timeout,
private readonly StreamFactoryInterface $streamFactory,
) {
}

public function create(Options $options): HeadlessChromiumAdapter
{
return new HeadlessChromiumAdapter(
$this->binary,
$this->timeout,
$this,
$options,
$this->streamFactory,
);
}
}
Loading
Loading