diff --git a/apps/dav/lib/Avatars/AvatarNode.php b/apps/dav/lib/Avatars/AvatarNode.php index ade523561f2f9..93a1242bb61ef 100644 --- a/apps/dav/lib/Avatars/AvatarNode.php +++ b/apps/dav/lib/Avatars/AvatarNode.php @@ -61,6 +61,8 @@ public function get() { ob_start(); if ($this->ext === 'png') { imagepng($res); + } elseif ($this->ext === 'jxl') { + imagejxl($res); } else { imagejpeg($res); } @@ -78,8 +80,10 @@ public function get() { public function getContentType() { if ($this->ext === 'png') { return 'image/png'; + } elseif ($this->ext === 'jxl') { + return 'image/jxl'; } - return 'image/jpeg'; + return 'image/jpeg'; } public function getETag() { diff --git a/apps/dav/lib/CardDAV/PhotoCache.php b/apps/dav/lib/CardDAV/PhotoCache.php index 9f05ec2354aaf..3e46b35001924 100644 --- a/apps/dav/lib/CardDAV/PhotoCache.php +++ b/apps/dav/lib/CardDAV/PhotoCache.php @@ -47,6 +47,7 @@ class PhotoCache { public const ALLOWED_CONTENT_TYPES = [ 'image/png' => 'png', 'image/jpeg' => 'jpg', + 'image/jxl' => 'jxl', 'image/gif' => 'gif', 'image/vnd.microsoft.icon' => 'ico', ]; diff --git a/apps/theming/lib/ImageManager.php b/apps/theming/lib/ImageManager.php index f536bae0421de..0e9c274c3b7d7 100644 --- a/apps/theming/lib/ImageManager.php +++ b/apps/theming/lib/ImageManager.php @@ -254,6 +254,10 @@ public function updateImage(string $key, string $tmpFile): string { if (!imagejpeg($outputImage, $newTmpFile, 90)) { throw new \Exception('Could not recompress background image as JPEG'); } + } else if (str_contains($detectedMimeType, 'image/jxl')) { + if (!imagejxl($outputImage, $newTmpFile, 90)) { + throw new \Exception('Could not recompress background image as JPEG XL'); + } } else { if (!imagepng($outputImage, $newTmpFile, 8)) { throw new \Exception('Could not recompress background image as PNG'); diff --git a/apps/theming/src/components/admin/FileInputField.vue b/apps/theming/src/components/admin/FileInputField.vue index 3d6fda9ec70b8..72a1869863fa9 100644 --- a/apps/theming/src/components/admin/FileInputField.vue +++ b/apps/theming/src/components/admin/FileInputField.vue @@ -142,7 +142,7 @@ export default { return { showLoading: false, acceptMime: (allowedMimeTypes[this.name] - || ['image/jpeg', 'image/png', 'image/gif', 'image/webp']).join(','), + || ['image/jpeg', 'image/jxl', 'image/png', 'image/gif', 'image/webp']).join(','), } }, diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 4212f56ce2b44..1d17219886ef9 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -466,28 +466,35 @@ public function searchFile(string $user, ?string $properties = null, ?string $sc image/png - + image/jpeg - + + + + + + image/jxl + + image/heic - + video/mp4 - + diff --git a/config/config.sample.php b/config/config.sample.php index 27b99636a2207..b1540ad36462b 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -1297,6 +1297,7 @@ 'OC\Preview\BMP', 'OC\Preview\GIF', 'OC\Preview\JPEG', + 'OC\Preview\JXL', 'OC\Preview\Krita', 'OC\Preview\MarkDown', 'OC\Preview\MP3', diff --git a/core/Controller/AvatarController.php b/core/Controller/AvatarController.php index 32858b526122b..193a9bbc25271 100644 --- a/core/Controller/AvatarController.php +++ b/core/Controller/AvatarController.php @@ -243,7 +243,7 @@ public function postAvatar(?string $path = null): JSONResponse { if ($image->valid()) { $mimeType = $image->mimeType(); - if ($mimeType !== 'image/jpeg' && $mimeType !== 'image/png') { + if ($mimeType !== 'image/jpeg' && $mimeType !== 'image/jxl' && $mimeType !== 'image/png') { return new JSONResponse( ['data' => ['message' => $this->l10n->t('Unknown filetype')]], Http::STATUS_OK diff --git a/core/js/mimetypelist.js b/core/js/mimetypelist.js index 2e87ce756ba50..685ddf0ab6afc 100644 --- a/core/js/mimetypelist.js +++ b/core/js/mimetypelist.js @@ -13,6 +13,7 @@ OC.MimeTypeList={ "application/font-sfnt": "font", "application/font-woff": "font", "application/gpx+xml": "location", + "application/gzip": "package/x-generic", "application/illustrator": "image", "application/javascript": "text/code", "application/json": "text/code", @@ -80,7 +81,7 @@ OC.MimeTypeList={ "application/x-fictionbook+xml": "text", "application/x-font": "font", "application/x-gimp": "image", - "application/x-gzip": "package/x-generic", + "application/x-gzip": "application/gzip", "application/x-iwork-keynote-sffkey": "x-office/presentation", "application/x-iwork-numbers-sffnumbers": "x-office/spreadsheet", "application/x-iwork-pages-sffpages": "x-office/document", @@ -111,6 +112,7 @@ OC.MimeTypeList={ "application/km": "mindmap", "application/x-freemind": "mindmap", "application/vnd.xmind.workbook": "mindmap", + "image/jxl": "image/jxl", "image/targa": "image/tga", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform": "x-office/form", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf": "x-office/form-template", diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b90e2866bc6f0..376d8209ae631 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1552,6 +1552,7 @@ 'OC\\Preview\\Image' => $baseDir . '/lib/private/Preview/Image.php', 'OC\\Preview\\Imaginary' => $baseDir . '/lib/private/Preview/Imaginary.php', 'OC\\Preview\\JPEG' => $baseDir . '/lib/private/Preview/JPEG.php', + 'OC\\Preview\\JXL' => $baseDir . '/lib/private/Preview/JXL.php', 'OC\\Preview\\Krita' => $baseDir . '/lib/private/Preview/Krita.php', 'OC\\Preview\\MP3' => $baseDir . '/lib/private/Preview/MP3.php', 'OC\\Preview\\MSOffice2003' => $baseDir . '/lib/private/Preview/MSOffice2003.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index c1c3bc25869a5..060c06296e84e 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -11,7 +11,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 ); public static $prefixLengthsPsr4 = array ( - 'O' => + 'O' => array ( 'OC\\Core\\' => 8, 'OC\\' => 3, @@ -20,15 +20,15 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 ); public static $prefixDirsPsr4 = array ( - 'OC\\Core\\' => + 'OC\\Core\\' => array ( 0 => __DIR__ . '/../../..' . '/core', ), - 'OC\\' => + 'OC\\' => array ( 0 => __DIR__ . '/../../..' . '/lib/private', ), - 'OCP\\' => + 'OCP\\' => array ( 0 => __DIR__ . '/../../..' . '/lib/public', ), @@ -1585,6 +1585,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Preview\\Image' => __DIR__ . '/../../..' . '/lib/private/Preview/Image.php', 'OC\\Preview\\Imaginary' => __DIR__ . '/../../..' . '/lib/private/Preview/Imaginary.php', 'OC\\Preview\\JPEG' => __DIR__ . '/../../..' . '/lib/private/Preview/JPEG.php', + 'OC\\Preview\\JXL' => __DIR__ . '/../../..' . '/lib/private/Preview/JXL.php', 'OC\\Preview\\Krita' => __DIR__ . '/../../..' . '/lib/private/Preview/Krita.php', 'OC\\Preview\\MP3' => __DIR__ . '/../../..' . '/lib/private/Preview/MP3.php', 'OC\\Preview\\MSOffice2003' => __DIR__ . '/../../..' . '/lib/private/Preview/MSOffice2003.php', diff --git a/lib/private/Collaboration/Reference/LinkReferenceProvider.php b/lib/private/Collaboration/Reference/LinkReferenceProvider.php index df6c6cc9da9d6..f7dd60421f8eb 100644 --- a/lib/private/Collaboration/Reference/LinkReferenceProvider.php +++ b/lib/private/Collaboration/Reference/LinkReferenceProvider.php @@ -49,6 +49,7 @@ class LinkReferenceProvider implements IReferenceProvider { 'image/png', 'image/jpg', 'image/jpeg', + 'image/jxl', 'image/gif', 'image/svg+xml', 'image/webp' diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 695d4a3357fa8..8ddc3f23e3fc6 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -621,6 +621,8 @@ private function getExtension($mimeType) { return 'png'; case 'image/jpeg': return 'jpg'; + case 'image/jxl': + return 'jxl'; case 'image/webp': return 'webp'; case 'image/gif': diff --git a/lib/private/Preview/Imaginary.php b/lib/private/Preview/Imaginary.php index faf84696e17b6..a50a2bc7d3baa 100644 --- a/lib/private/Preview/Imaginary.php +++ b/lib/private/Preview/Imaginary.php @@ -57,7 +57,7 @@ public function getMimeType(): string { } public static function supportedMimeTypes(): string { - return '/(image\/(bmp|x-bitmap|png|jpeg|gif|heic|heif|svg\+xml|tiff|webp)|application\/(pdf|illustrator))/'; + return '/(image\/(bmp|x-bitmap|png|jpeg|jxl|gif|heic|heif|svg\+xml|tiff|webp)|application\/(pdf|illustrator))/'; } public function getCroppedThumbnail(File $file, int $maxX, int $maxY, bool $crop): ?IImage { @@ -93,6 +93,9 @@ public function getCroppedThumbnail(File $file, int $maxX, int $maxY, bool $crop $autorotate = false; $mimeType = 'jpeg'; break; + case 'image/jxl': + $mimeType = 'jxl'; + break; case 'image/gif': case 'image/png': $mimeType = 'png'; diff --git a/lib/private/Preview/JXL.php b/lib/private/Preview/JXL.php new file mode 100644 index 0000000000000..dbc766b06d099 --- /dev/null +++ b/lib/private/Preview/JXL.php @@ -0,0 +1,155 @@ + + * @author Robin Appelman + * @author Peter Kovář + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OC\Preview; + +use OCP\Files\File; +use OCP\Files\FileInfo; +use OCP\IImage; +use Psr\Log\LoggerInterface; + +/** + * Creates a JXL preview using ImageMagick via the PECL extension + * + * @package OC\Preview + */ +class JXL extends ProviderV2 { + /** + * {@inheritDoc} + */ + public function getMimeType(): string { + return '/image\/jxl/'; + } + + /** + * {@inheritDoc} + */ + public function isAvailable(FileInfo $file): bool { + return in_array('JXL', \Imagick::queryFormats("JXL")); + } + + /** + * {@inheritDoc} + */ + public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage { + if (!$this->isAvailable($file)) { + return null; + } + + $tmpPath = $this->getLocalFile($file); + if ($tmpPath === false) { + \OC::$server->get(LoggerInterface::class)->error( + 'Failed to get thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } + + // Creates \Imagick object from the heic file + try { + $bp = $this->getResizedPreview($tmpPath, $maxX, $maxY); + $bp->setFormat('jxl'); + } catch (\Exception $e) { + \OC::$server->get(LoggerInterface::class)->error( + 'File: ' . $file->getPath() . ' Imagick says:', + [ + 'exception' => $e, + 'app' => 'core', + ] + ); + return null; + } + + $this->cleanTmpFiles(); + + //new bitmap image object + $image = new \OCP\Image(); + $image->loadFromData((string) $bp); + //check if image object is valid + return $image->valid() ? $image : null; + } + + /** + * Returns a preview of maxX times maxY dimensions in JPG format + * + * * The default resolution is already 72dpi, no need to change it for a bitmap output + * * It's possible to have proper colour conversion using profileimage(). + * ICC profiles are here: http://www.color.org/srgbprofiles.xalter + * * It's possible to Gamma-correct an image via gammaImage() + * + * @param string $tmpPath the location of the file to convert + * @param int $maxX + * @param int $maxY + * + * @return \Imagick + */ + private function getResizedPreview($tmpPath, $maxX, $maxY) { + $bp = new \Imagick(); + + // Layer 0 contains either the bitmap or a flat representation of all vector layers + $bp->readImage($tmpPath . '[0]'); + + // Fix orientation from EXIF + $bp->autoOrient(); + + $bp->setImageFormat('jxl'); + + $bp = $this->resize($bp, $maxX, $maxY); + + return $bp; + } + + /** + * Returns a resized \Imagick object + * + * If you want to know more on the various methods available to resize an + * image, check out this link : @link https://stackoverflow.com/questions/8517304/what-the-difference-of-sample-resample-scale-resize-adaptive-resize-thumbnail-im + * + * @param \Imagick $bp + * @param int $maxX + * @param int $maxY + * + * @return \Imagick + */ + private function resize($bp, $maxX, $maxY) { + [$previewWidth, $previewHeight] = array_values($bp->getImageGeometry()); + + // We only need to resize a preview which doesn't fit in the maximum dimensions + if ($previewWidth > $maxX || $previewHeight > $maxY) { + // If we want a small image (thumbnail) let's be most space- and time-efficient + if ($maxX <= 500 && $maxY <= 500) { + $bp->thumbnailImage($maxY, $maxX, true); + $bp->stripImage(); + } else { + // A bigger image calls for some better resizing algorithm + // According to http://www.imagemagick.org/Usage/filter/#lanczos + // the catrom filter is almost identical to Lanczos2, but according + // to https://www.php.net/manual/en/imagick.resizeimage.php it is + // significantly faster + $bp->resizeImage($maxX, $maxY, \Imagick::FILTER_CATROM, 1, true); + } + } + + return $bp; + } +} diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php index aedcbbce335fc..3d60d324f74ec 100644 --- a/lib/private/PreviewManager.php +++ b/lib/private/PreviewManager.php @@ -276,6 +276,7 @@ public function isAvailable(\OCP\Files\FileInfo $file): bool { * The following providers are enabled by default: * - OC\Preview\PNG * - OC\Preview\JPEG + * - OC\Preview\JXL * - OC\Preview\GIF * - OC\Preview\BMP * - OC\Preview\XBitmap @@ -309,6 +310,7 @@ protected function getEnabledDefaultProvider() { $imageProviders = [ Preview\PNG::class, Preview\JPEG::class, + Preview\JXL::class, Preview\GIF::class, Preview\BMP::class, Preview\XBitmap::class, @@ -357,6 +359,7 @@ protected function registerCoreProviders() { $this->registerCoreProvider(Preview\MarkDown::class, '/text\/(x-)?markdown/'); $this->registerCoreProvider(Preview\PNG::class, '/image\/png/'); $this->registerCoreProvider(Preview\JPEG::class, '/image\/jpeg/'); + $this->registerCoreProvider(Preview\JXL::class, '/image\/jxl/'); $this->registerCoreProvider(Preview\GIF::class, '/image\/gif/'); $this->registerCoreProvider(Preview\BMP::class, '/image\/bmp/'); $this->registerCoreProvider(Preview\XBitmap::class, '/image\/x-xbitmap/'); @@ -379,6 +382,7 @@ protected function registerCoreProviders() { 'HEIC' => ['mimetype' => '/image\/hei(f|c)/', 'class' => Preview\HEIC::class], 'TGA' => ['mimetype' => '/image\/t(ar)?ga/', 'class' => Preview\TGA::class], 'SGI' => ['mimetype' => '/image\/sgi/', 'class' => Preview\SGI::class], + 'JXL' => ['mimetype' => '/image\/jxl/', 'class' => Preview\JXL::class], ]; foreach ($imagickProviders as $queryFormat => $provider) { diff --git a/lib/private/Repair/RepairMimeTypes.php b/lib/private/Repair/RepairMimeTypes.php index f951c3b916df6..8e7ea99a40536 100644 --- a/lib/private/Repair/RepairMimeTypes.php +++ b/lib/private/Repair/RepairMimeTypes.php @@ -118,6 +118,7 @@ private function introduceAsciidocType() { private function introduceImageTypes() { $updatedMimetypes = [ 'jp2' => 'image/jp2', + 'jxl' => 'image/jxl', 'webp' => 'image/webp', ]; diff --git a/lib/private/legacy/OC_Image.php b/lib/private/legacy/OC_Image.php index aac0c49673400..6317807e1a7c4 100644 --- a/lib/private/legacy/OC_Image.php +++ b/lib/private/legacy/OC_Image.php @@ -273,6 +273,9 @@ private function _output(?string $filePath = null, ?string $mimeType = null): bo case 'image/jpeg': $imageType = IMAGETYPE_JPEG; break; + case 'image/jxl': + $imageType = IMAGETYPE_JXL; + break; case 'image/png': $imageType = IMAGETYPE_PNG; break; @@ -297,6 +300,9 @@ private function _output(?string $filePath = null, ?string $mimeType = null): bo imageinterlace($this->resource, (PHP_VERSION_ID >= 80000 ? true : 1)); $retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality()); break; + case IMAGETYPE_JXL: + $retVal = imagejxl($this->resource, $filePath, $this->getJxlQuality()); + break; case IMAGETYPE_PNG: $retVal = imagepng($this->resource, $filePath); break; @@ -363,6 +369,7 @@ public function dataMimeType(): ?string { switch ($this->mimeType) { case 'image/png': case 'image/jpeg': + case 'image/jxl': case 'image/gif': return $this->mimeType; default: @@ -388,6 +395,10 @@ public function data(): ?string { $quality = $this->getJpegQuality(); $res = imagejpeg($this->resource, null, $quality); break; + case "image/jxl": + $quality = $this->getJxlQuality(); + $res = imagejxl($this->resource, null, $quality); + break; case "image/gif": $res = imagegif($this->resource); break; diff --git a/resources/config/mimetypemapping.dist.json b/resources/config/mimetypemapping.dist.json index 47b207d6bccda..1b639f65003d3 100644 --- a/resources/config/mimetypemapping.dist.json +++ b/resources/config/mimetypemapping.dist.json @@ -87,6 +87,7 @@ "jpeg": ["image/jpeg"], "jpg": ["image/jpeg"], "jps": ["image/jpeg"], + "jxl": ["image/jxl"], "js": ["application/javascript", "text/plain"], "json": ["application/json", "text/plain"], "k25": ["image/x-dcraw"], diff --git a/tests/lib/Repair/RepairMimeTypesTest.php b/tests/lib/Repair/RepairMimeTypesTest.php index 61cf58582414e..efed782678998 100644 --- a/tests/lib/Repair/RepairMimeTypesTest.php +++ b/tests/lib/Repair/RepairMimeTypesTest.php @@ -116,11 +116,13 @@ private function renameMimeTypes($currentMimeTypes, $fixedMimeTypes) { public function testRenameImageTypes() { $currentMimeTypes = [ ['test.jp2', 'application/octet-stream'], + ['test.jxl', 'application/octet-stream'], ['test.webp', 'application/octet-stream'], ]; $fixedMimeTypes = [ ['test.jp2', 'image/jp2'], + ['test.jxl', 'image/jxl'], ['test.webp', 'image/webp'], ]; @@ -187,6 +189,7 @@ public function testDoNothingWhenOnlyNewFiles() { ['test.jp2', 'image/jp2'], ['test.jps', 'image/jpeg'], ['test.MPO', 'image/jpeg'], + ['test.jxl', 'image/jxl'], ['test.webp', 'image/webp'], ['test.conf', 'text/plain'], ['test.cnf', 'text/plain'], @@ -241,6 +244,7 @@ public function testDoNothingWhenOnlyNewFiles() { ['test.jp2', 'image/jp2'], ['test.jps', 'image/jpeg'], ['test.MPO', 'image/jpeg'], + ['test.jxl', 'image/jxl'], ['test.webp', 'image/webp'], ['test.conf', 'text/plain'], ['test.cnf', 'text/plain'],