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'],