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": "*",