* * @author Jan C. Borchardt * @author Julius Härtl * @author Christopher Ng * @author Ferdinand Thiessen * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ namespace OCA\Theming\Service; use InvalidArgumentException; use OC\User\NoUserException; use OCA\Theming\AppInfo\Application; use OCP\Files\File; use OCP\Files\IAppData; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; use OCP\IConfig; use OCP\Lock\LockedException; use OCP\PreConditionNotMetException; use RuntimeException; class BackgroundService { public const DEFAULT_COLOR = '#00679e'; public const DEFAULT_BACKGROUND_COLOR = '#00679e'; /** * One of our shipped background images is used */ public const BACKGROUND_SHIPPED = 'shipped'; /** * A custom background image is used */ public const BACKGROUND_CUSTOM = 'custom'; /** * The default background image is used */ public const BACKGROUND_DEFAULT = 'default'; /** * Just a background color is used */ public const BACKGROUND_COLOR = 'color'; public const DEFAULT_BACKGROUND_IMAGE = 'kamil-porembinski-clouds.jpg'; /** * 'attribution': Name, artist and license * 'description': Alternative text * 'attribution_url': URL for attribution * 'background_color': Cached mean color of the top part to calculate app menu colors and use as fallback * 'primary_color': Recommended primary color for this theme / image */ public const SHIPPED_BACKGROUNDS = [ 'hannah-maclean-soft-floral.jpg' => [ 'attribution' => 'Soft floral (Hannah MacLean, CC0)', 'description' => 'Abstract background picture in yellow and white color whith a flower on it', 'attribution_url' => 'https://stocksnap.io/photo/soft-floral-XOYWCCW5PA', 'background_color' => '#e4d2c1', 'primary_color' => '#9f652f', ], 'ted-moravec-morning-fog.jpg' => [ 'attribution' => 'Morning fog (Ted Moravec, Public Domain)', 'description' => 'Background picture of a forest shrouded in fog', 'attribution_url' => 'https://flickr.com/photos/tmoravec/52392410261', 'background_color' => '#f6f7f6', 'primary_color' => '#114c3b', ], 'stefanus-martanto-setyo-husodo-underwater-ocean.jpg' => [ 'attribution' => 'Underwater ocean (Stefanus Martanto Setyo Husodo, CC0)', 'description' => 'Background picture of an underwater ocean', 'attribution_url' => 'https://stocksnap.io/photo/underwater-ocean-TJA9LBH4WS', 'background_color' => '#003351', 'primary_color' => '#04577e', ], 'zoltan-voros-rhythm-and-blues.jpg' => [ 'attribution' => 'Rhythm and blues (Zoltán Vörös, CC BY)', 'description' => 'Abstract background picture of sand dunes during night', 'attribution_url' => 'https://flickr.com/photos/v923z/51634409289/', 'background_color' => '#1c2437', 'primary_color' => '#1c243c', ], 'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [ 'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)', 'description' => 'Background picture of a red-ish butterfly wing under microscope', 'attribution_url' => 'https://commons.wikimedia.org/wiki/File:%D0%A7%D0%B5%D1%88%D1%83%D0%B9%D0%BA%D0%B8_%D0%BA%D1%80%D1%8B%D0%BB%D0%B0_%D0%B1%D0%B0%D0%B1%D0%BE%D1%87%D0%BA%D0%B8.jpg', 'background_color' => '#652e11', 'primary_color' => '#a53c17', ], 'bernie-cetonia-aurata-take-off-composition.jpg' => [ 'attribution' => 'Cetonia aurata take off composition (Bernie, Public Domain)', 'description' => 'Montage of a cetonia aurata bug that takes off with white background', 'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Cetonia_aurata_take_off_composition_05172009.jpg', 'background_color' => '#dee0d3', 'primary_color' => '#56633d', ], 'dejan-krsmanovic-ribbed-red-metal.jpg' => [ 'attribution' => 'Ribbed red metal (Dejan Krsmanovic, CC BY)', 'description' => 'Abstract background picture of red ribbed metal with two horizontal white elements on top of it', 'attribution_url' => 'https://www.flickr.com/photos/dejankrsmanovic/42971456774/', 'background_color' => '#9b171c', 'primary_color' => '#9c4236', ], 'eduardo-neves-pedra-azul.jpg' => [ 'attribution' => 'Pedra azul milky way (Eduardo Neves, CC BY-SA)', 'description' => 'Background picture of the milky way during night with a mountain in front of it', 'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Pedra_Azul_Milky_Way.jpg', 'background_color' => '#1d242d', 'primary_color' => '#4f6071', ], 'european-space-agency-barents-bloom.jpg' => [ 'attribution' => 'Barents bloom (European Space Agency, CC BY-SA)', 'description' => 'Abstract background picture of blooming barents in blue and green colors', 'attribution_url' => 'https://www.esa.int/ESA_Multimedia/Images/2016/08/Barents_bloom', 'background_color' => '#1c383d', 'primary_color' => '#396475', ], 'hannes-fritz-flippity-floppity.jpg' => [ 'attribution' => 'Flippity floppity (Hannes Fritz, CC BY-SA)', 'description' => 'Abstract background picture of many pairs of flip flops hanging on a wall in multiple colors', 'attribution_url' => 'http://hannes.photos/flippity-floppity', 'background_color' => '#5b2d53', 'primary_color' => '#98415a', ], 'hannes-fritz-roulette.jpg' => [ 'attribution' => 'Roulette (Hannes Fritz, CC BY-SA)', 'description' => 'Background picture of a rotating giant wheel during night', 'attribution_url' => 'http://hannes.photos/roulette', 'background_color' => '#000000', 'primary_color' => '#845334', ], 'hannes-fritz-sea-spray.jpg' => [ 'attribution' => 'Sea spray (Hannes Fritz, CC BY-SA)', 'description' => 'Background picture of a stone coast with fog and sea behind it', 'attribution_url' => 'http://hannes.photos/sea-spray', 'background_color' => '#333f47', 'primary_color' => '#4f6071', ], 'kamil-porembinski-clouds.jpg' => [ 'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)', 'description' => 'Background picture of white clouds on in front of a blue sky', 'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/', 'background_color' => self::DEFAULT_BACKGROUND_COLOR, 'primary_color' => self::DEFAULT_COLOR, ], 'bernard-spragg-new-zealand-fern.jpg' => [ 'attribution' => 'New zealand fern (Bernard Spragg, CC0)', 'description' => 'Abstract background picture of fern leafes', 'attribution_url' => 'https://commons.wikimedia.org/wiki/File:NZ_Fern.(Blechnum_chambersii)_(11263534936).jpg', 'background_color' => '#0c3c03', 'primary_color' => '#316b26', ], 'rawpixel-pink-tapioca-bubbles.jpg' => [ 'attribution' => 'Pink tapioca bubbles (Rawpixel, CC BY)', 'description' => 'Abstract background picture of pink tapioca bubbles', 'attribution_url' => 'https://www.flickr.com/photos/byrawpixel/27665140298/in/photostream/', 'background_color' => '#c56e95', 'primary_color' => '#7b4e7e', ], 'nasa-waxing-crescent-moon.jpg' => [ 'attribution' => 'Waxing crescent moon (NASA, Public Domain)', 'description' => 'Background picture of glowing earth in foreground and moon in the background', 'attribution_url' => 'https://www.nasa.gov/image-feature/a-waxing-crescent-moon', 'background_color' => '#000002', 'primary_color' => '#005ac1', ], 'tommy-chau-already.jpg' => [ 'attribution' => 'Cityscape (Tommy Chau, CC BY)', 'description' => 'Background picture of a skyscraper city during night', 'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/16910999368', 'background_color' => '#35229f', 'primary_color' => '#6a2af4', ], 'tommy-chau-lion-rock-hill.jpg' => [ 'attribution' => 'Lion rock hill (Tommy Chau, CC BY)', 'description' => 'Background picture of mountains during sunset or sunrise', 'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/17136440246', 'background_color' => '#cb92b7', 'primary_color' => '#7f4f70', ], 'lali-masriera-yellow-bricks.jpg' => [ 'attribution' => 'Yellow bricks (Lali Masriera, CC BY)', 'description' => 'Background picture of yellow bricks with some yellow tubes', 'attribution_url' => 'https://www.flickr.com/photos/visualpanic/3982464447', 'background_color' => '#c78a19', 'primary_color' => '#7f5700', ], ]; public function __construct( private IRootFolder $rootFolder, private IAppData $appData, private IConfig $config, private ?string $userId, ) { } public function setDefaultBackground(): void { $this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image'); $this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_color'); $this->config->deleteUserValue($this->userId, Application::APP_ID, 'primary_color'); } /** * @param $path * @throws NotFoundException * @throws NotPermittedException * @throws LockedException * @throws PreConditionNotMetException * @throws NoUserException */ public function setFileBackground($path): void { if ($this->userId === null) { throw new RuntimeException('No currently logged-in user'); } $userFolder = $this->rootFolder->getUserFolder($this->userId); /** @var File $file */ $file = $userFolder->get($path); $image = new \OCP\Image(); if ($image->loadFromFileHandle($file->fopen('r')) === false) { throw new InvalidArgumentException('Invalid image file'); } $meanColor = $this->calculateMeanColor($image); if ($meanColor !== false) { $this->setColorBackground($meanColor); } $this->getAppDataFolder()->newFile('background.jpg', $file->fopen('r')); $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM); } public function setShippedBackground($fileName): void { if ($this->userId === null) { throw new RuntimeException('No currently logged-in user'); } if (!array_key_exists($fileName, self::SHIPPED_BACKGROUNDS)) { throw new InvalidArgumentException('The given file name is invalid'); } $this->setColorBackground(self::SHIPPED_BACKGROUNDS[$fileName]['background_color']); $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', $fileName); $this->config->setUserValue($this->userId, Application::APP_ID, 'primary_color', self::SHIPPED_BACKGROUNDS[$fileName]['primary_color']); } /** * Set the background to color only */ public function setColorBackground(string $color): void { if ($this->userId === null) { throw new RuntimeException('No currently logged-in user'); } if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) { throw new InvalidArgumentException('The given color is invalid'); } $this->config->setUserValue($this->userId, Application::APP_ID, 'background_color', $color); $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR); } public function deleteBackgroundImage(): void { if ($this->userId === null) { throw new RuntimeException('No currently logged-in user'); } $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR); } public function getBackground(): ?ISimpleFile { $background = $this->config->getUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DEFAULT); if ($background === self::BACKGROUND_CUSTOM) { try { return $this->getAppDataFolder()->getFile('background.jpg'); } catch (NotFoundException | NotPermittedException $e) { return null; } } return null; } /** * Called when a new global background (backgroundMime) is uploaded (admin setting) * This sets all necessary app config values * @param resource|string $path * @return string|null The fallback background color - if any */ public function setGlobalBackground($path): string|null { $image = new \OCP\Image(); $handle = is_resource($path) ? $path : fopen($path, 'rb'); if ($handle && $image->loadFromFileHandle($handle) !== false) { $meanColor = $this->calculateMeanColor($image); if ($meanColor !== false) { $this->config->setAppValue(Application::APP_ID, 'background_color', $meanColor); return $meanColor; } } return null; } /** * Calculate mean color of an given image * It only takes the upper part into account so that a matching text color can be derived for the app menu */ private function calculateMeanColor(\OCP\Image $image): false|string { /** * Small helper to ensure one channel is returned as 8byte hex */ function toHex(int $channel): string { $hex = dechex($channel); return match (strlen($hex)) { 0 => '00', 1 => '0'.$hex, 2 => $hex, default => 'ff', }; } $tempImage = new \OCP\Image(); // Crop to only analyze top bar $resource = $image->cropNew(0, 0, $image->width(), min(max(50, (int)($image->height() * 0.125)), $image->height())); if ($resource === false) { return false; } $tempImage->setResource($resource); if (!$tempImage->preciseResize(100, 7)) { return false; } $resource = $tempImage->resource(); if ($resource === false) { return false; } $reds = []; $greens = []; $blues = []; for ($y = 0; $y < 7; $y++) { for ($x = 0; $x < 100; $x++) { $value = imagecolorat($resource, $x, $y); if ($value === false) { continue; } $reds[] = ($value >> 16) & 0xFF; $greens[] = ($value >> 8) & 0xFF; $blues[] = $value & 0xFF; } } $meanColor = '#' . toHex((int)(array_sum($reds) / count($reds))); $meanColor .= toHex((int)(array_sum($greens) / count($greens))); $meanColor .= toHex((int)(array_sum($blues) / count($blues))); return $meanColor; } /** * Storing the data in appdata/theming/users/USERID * * @return ISimpleFolder * @throws NotPermittedException */ private function getAppDataFolder(): ISimpleFolder { try { $rootFolder = $this->appData->getFolder('users'); } catch (NotFoundException $e) { $rootFolder = $this->appData->newFolder('users'); } try { return $rootFolder->getFolder($this->userId); } catch (NotFoundException $e) { return $rootFolder->newFolder($this->userId); } } }