diff --git a/cli/Commands/StockImages/Process.php b/cli/Commands/StockImages/Process.php new file mode 100644 index 0000000000000000000000000000000000000000..b6217c2ecfe900b6234e33cf21c24dfe531130ba --- /dev/null +++ b/cli/Commands/StockImages/Process.php @@ -0,0 +1,40 @@ +<?php + +namespace Studip\Cli\Commands\StockImages; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use CronjobSchedule; +use CronjobScheduler; +use Studip\StockImages\Jobs\ProcessImage; + +class Process extends Command +{ + protected static $defaultName = 'stock-images:process'; + + protected function configure(): void + { + $this->setDescription('Process images.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $verbose = $input->getOption('verbose'); + + $cron = CronjobScheduler::getInstance(); + + $taskId = $cron->registerTask(new ProcessImage()); + $output->writeln("Registered Task: '{$taskId}'"); + $parameters = [ + 'image-id' => time(), + ]; + $schedule = $cron->scheduleOnce($taskId, time(), CronjobSchedule::PRIORITY_HIGH, $parameters); + $schedule->active = 1; + $schedule->store(); + + + return Command::SUCCESS; + } +} diff --git a/cli/Commands/StockImages/Sizes.php b/cli/Commands/StockImages/Sizes.php new file mode 100644 index 0000000000000000000000000000000000000000..da2f8d66ffef4c4ed05f9b4b91bae3df148f3fdd --- /dev/null +++ b/cli/Commands/StockImages/Sizes.php @@ -0,0 +1,31 @@ +<?php + +namespace Studip\Cli\Commands\StockImages; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Studip\StockImages\Scaler; + +class Sizes extends Command +{ + protected static $defaultName = 'stock-images:sizes'; + + protected function configure(): void + { + $this->setDescription('Creates smaller sizes a stock image.'); + $this->addArgument('id', InputArgument::REQUIRED, 'ID of the stock image'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $verbose = $input->getOption('verbose'); + $imageId = (int) $input->getArgument('id'); + + $scaler = new Scaler(); + $scaler(['id' => $imageId]); + + return Command::SUCCESS; + } +} diff --git a/cli/Commands/StockImages/Unsplash.php b/cli/Commands/StockImages/Unsplash.php new file mode 100644 index 0000000000000000000000000000000000000000..532f59155b5607d46e875f44f6052a058402db02 --- /dev/null +++ b/cli/Commands/StockImages/Unsplash.php @@ -0,0 +1,126 @@ +<?php + +namespace Studip\Cli\Commands\StockImages; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Studip\StockImages\PaletteCreator; +use Studip\StockImages\Scaler; +use GuzzleHttp\Client; + +class Unsplash extends Command +{ + protected static $defaultName = 'stock-images:unsplash'; + + protected function configure(): void + { + $this->setDescription('Import image from unsplash.'); + $this->addArgument('url', InputArgument::REQUIRED, 'URL of the unsplash image'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $url = $input->getArgument('url'); + $verbose = $input->getOption('verbose'); + + $matched = preg_match('/^https:\/\/unsplash.com\/photos\/([-_A-Za-z0-9]+)$/', $url, $matches); + if (!$matched) { + $output->writeln('<error>Invalid URL.</error>'); + + return Command::FAILURE; + } + $imageId = $matches[1]; + + if ($verbose) { + $output->writeln("Importing image from unsplash: '{$imageId}'"); + } + + $document = $this->fetchURL($imageId); + $ldJson = $this->findLdJson($document); + + \DBManager::get()->exec('SET CHARACTER SET utf8mb4'); + + $stockImage = new \StockImage(); + $stockImage->author = $ldJson['author']['name']; + $stockImage->license = 'Unsplash-Lizenz: https://unsplash.com/license'; + $stockImage->mkdate = (new \DateTime($ldJson['datePublished']))->getTimestamp(); + $stockImage->description = $ldJson['description'] . "\n\nImported from: " . $url; + $stockImage->title = $ldJson['caption']; + $stockImage->width = sscanf($ldJson['width'], '%d'); + $stockImage->height = sscanf($ldJson['height'], '%d'); + $stockImage->tags = '["unsplash"]'; + + $stockImage->mime_type = ''; + $stockImage->size = 0; + $stockImage->store(); + + $this->fetchImage($stockImage, $ldJson['contentUrl']); + $this->processImage($stockImage); + + return Command::SUCCESS; + } + + private function fetchURL($imageId): \DOMDocument + { + $client = new Client(); + $response = $client->request('GET', 'https://unsplash.com/photos/' . $imageId); + $body = $response->getBody(); + + $document = new \DOMDocument(); + $document->loadHTML($body); + + return $document; + } + + private function fetchImage(\StockImage $image, $url): void + { + $client = new Client(); + $response = $client->request('GET', $url); + if (200 !== $response->getStatusCode()) { + throw new \RuntimeException(); + } + $mimeType = current($response->getHeader('Content-Type')); + if ('image/' !== substr($mimeType, 0, 6)) { + throw new \RuntimeException(); + } + + // TODO: Accept list of valid mime_types + + $image->mime_type = $mimeType; + $this->writeImage($image, $response->getBody()); + + $image->store(); + } + + private function writeImage(\StockImage $image, string $body): void + { + $filepath = $image->getPath(); + $file = fopen($filepath, 'w'); + fwrite($file, $body); + $image->size = fstat($file)['size']; + fclose($file); + } + + private function findLdJson(\DOMDocument $document): iterable + { + foreach ($document->getElementsByTagName('script') as $script) { + $type = $script->getAttribute('type'); + if ('application/ld+json' === $type) { + return json_decode($script->textContent, true); + } + } + + throw new \RuntimeException(); + } + + private function processImage(\StockImage $stockImage): void + { + $processors = [Scaler::class, PaletteCreator::class]; + foreach ($processors as $class) { + $processor = new $class(); + $processor($stockImage); + } + } +} diff --git a/cli/studip b/cli/studip index 7c47fcb6bcdb09f6ff8a9204f384f67eafadffe6..ee37794fb2e2ec3c09db30b7b327476bebde15d7 100755 --- a/cli/studip +++ b/cli/studip @@ -57,6 +57,9 @@ $commands = [ Commands\Twillo\PrivateKeys::class, Commands\Users\UserDelete::class, Commands\Users\UserDelete::class, + Commands\StockImages\Unsplash::class, + Commands\StockImages\Process::class, + Commands\StockImages\Sizes::class, ]; $creator = function ($command) { return app($command); diff --git a/composer.json b/composer.json index 413e23c38f44f996445b14f82bef5ef39bc3db90..3afd6088f88e0d8d6dcf1eed4364f69a5016bc34 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,9 @@ "vendor-dir": "composer", "platform": { "php": "7.2.5" + }, + "allow-plugins": { + "php-http/discovery": true } }, "require-dev": { @@ -60,7 +63,9 @@ "symfony/polyfill-php80": "^1.27", "symfony/polyfill-php81": "^1.27", "phpowermove/docblock": "^2.0", - "ksubileau/color-thief-php": "^2.0" + "ksubileau/color-thief-php": "^2.0", + "php-http/curl-client": "^2.2", + "guzzlehttp/guzzle": "^7.0" }, "replace": { "symfony/polyfill-php54": "*",