From dd656390c3b6cbdfc6e88c8ff3e6a011238b6f6e Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Sat, 25 Feb 2023 23:08:38 +0200 Subject: SL-19108 Only permit 64 to 256 px images for thumbnails --- indra/newview/llviewertexturelist.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'indra/newview/llviewertexturelist.cpp') diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 93ae1670c8..901747ce57 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -1286,7 +1286,8 @@ void LLViewerTextureList::decodeAllImages(F32 max_time) BOOL LLViewerTextureList::createUploadFile(const std::string& filename, const std::string& out_filename, const U8 codec, - const S32 max_image_dimentions) + const S32 max_image_dimentions, + const S32 min_image_dimentions) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; // Load the image @@ -1314,6 +1315,16 @@ BOOL LLViewerTextureList::createUploadFile(const std::string& filename, image->setLastError("Image files with less than 3 or more than 4 components are not supported."); return FALSE; } + if (image->getWidth() < min_image_dimentions || image->getHeight() < min_image_dimentions) + { + std::string reason = llformat("Images below %d x %d pixels are not allowed. Actual size: %d x %dpx", + min_image_dimentions, + min_image_dimentions, + image->getWidth(), + image->getHeight()); + image->setLastError(reason); + return FALSE; + } // Convert to j2c (JPEG2000) and save the file locally LLPointer compressedImage = convertToUploadFile(raw_image, max_image_dimentions); if (compressedImage.isNull()) -- cgit v1.3 From 4800e3e4164640bc98f92f9c50d2ab7b882f97a1 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Thu, 17 Aug 2023 21:51:05 +0300 Subject: SL-20099 Thumbnail upload scale textures to be square thumbnail copy&paste only allows square textures and wasn't allowing none square ones despite those already being used as thumbnails --- indra/newview/llfloatersimplesnapshot.cpp | 2 +- indra/newview/llviewertexturelist.cpp | 19 +++++++++++++++---- indra/newview/llviewertexturelist.h | 7 +++++-- 3 files changed, 21 insertions(+), 7 deletions(-) (limited to 'indra/newview/llviewertexturelist.cpp') diff --git a/indra/newview/llfloatersimplesnapshot.cpp b/indra/newview/llfloatersimplesnapshot.cpp index cf596fd82f..757ac605e3 100644 --- a/indra/newview/llfloatersimplesnapshot.cpp +++ b/indra/newview/llfloatersimplesnapshot.cpp @@ -386,7 +386,7 @@ void LLFloaterSimpleSnapshot::uploadThumbnail(const std::string &file_path, cons // generate a temp texture file for coroutine std::string temp_file = gDirUtilp->getTempFilename(); U32 codec = LLImageBase::getCodecFromExtension(gDirUtilp->getExtension(file_path)); - if (!LLViewerTextureList::createUploadFile(file_path, temp_file, codec, THUMBNAIL_SNAPSHOT_DIM_MAX, THUMBNAIL_SNAPSHOT_DIM_MIN)) + if (!LLViewerTextureList::createUploadFile(file_path, temp_file, codec, THUMBNAIL_SNAPSHOT_DIM_MAX, THUMBNAIL_SNAPSHOT_DIM_MIN, true)) { LLSD notif_args; notif_args["REASON"] = LLImage::getLastError().c_str(); diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index 60c7fb6b8c..d10e1ea8c9 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -1290,7 +1290,8 @@ BOOL LLViewerTextureList::createUploadFile(const std::string& filename, const std::string& out_filename, const U8 codec, const S32 max_image_dimentions, - const S32 min_image_dimentions) + const S32 min_image_dimentions, + bool force_square) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; // Load the image @@ -1329,7 +1330,7 @@ BOOL LLViewerTextureList::createUploadFile(const std::string& filename, return FALSE; } // Convert to j2c (JPEG2000) and save the file locally - LLPointer compressedImage = convertToUploadFile(raw_image, max_image_dimentions); + LLPointer compressedImage = convertToUploadFile(raw_image, max_image_dimentions, force_square); if (compressedImage.isNull()) { image->setLastError("Couldn't convert the image to jpeg2000."); @@ -1354,10 +1355,20 @@ BOOL LLViewerTextureList::createUploadFile(const std::string& filename, } // note: modifies the argument raw_image!!!! -LLPointer LLViewerTextureList::convertToUploadFile(LLPointer raw_image, const S32 max_image_dimentions) +LLPointer LLViewerTextureList::convertToUploadFile(LLPointer raw_image, const S32 max_image_dimentions, bool force_square) { LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; - raw_image->biasedScaleToPowerOfTwo(max_image_dimentions); + if (force_square) + { + S32 biggest_side = llmax(raw_image->getWidth(), raw_image->getHeight()); + S32 square_size = raw_image->biasedDimToPowerOfTwo(biggest_side, max_image_dimentions); + + raw_image->scale(square_size, square_size); + } + else + { + raw_image->biasedScaleToPowerOfTwo(max_image_dimentions); + } LLPointer compressedImage = new LLImageJ2C(); if (gSavedSettings.getBOOL("LosslessJ2CUpload") && diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index eac26ab0eb..8fc65fc9ce 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -96,8 +96,11 @@ public: const std::string& out_filename, const U8 codec, const S32 max_image_dimentions = LLViewerFetchedTexture::MAX_IMAGE_SIZE_DEFAULT, - const S32 min_image_dimentions = 0); - static LLPointer convertToUploadFile(LLPointer raw_image, const S32 max_image_dimentions = LLViewerFetchedTexture::MAX_IMAGE_SIZE_DEFAULT); + const S32 min_image_dimentions = 0, + bool force_square = false); + static LLPointer convertToUploadFile(LLPointer raw_image, + const S32 max_image_dimentions = LLViewerFetchedTexture::MAX_IMAGE_SIZE_DEFAULT, + bool force_square = false); static void processImageNotInDatabase( LLMessageSystem *msg, void **user_data ); public: -- cgit v1.3 From cfb69846f1e8309ed86d4a18eb26a889f6dbaccc Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Sat, 9 Sep 2023 00:00:22 +0300 Subject: SL-20261 Allow and resize existing textures as necessary for thumbnails --- indra/newview/llfloaterchangeitemthumbnail.cpp | 132 ++++++++++++++++++++- indra/newview/llfloaterchangeitemthumbnail.h | 9 +- indra/newview/llfloatersimplesnapshot.cpp | 15 +++ indra/newview/llfloatersimplesnapshot.h | 1 + indra/newview/llsnapshotlivepreview.cpp | 32 +---- indra/newview/lltexturectrl.cpp | 5 +- indra/newview/lltexturectrl.h | 2 +- indra/newview/llviewertexturelist.cpp | 39 ++++++ indra/newview/llviewertexturelist.h | 4 + .../skins/default/xui/en/floater_texture_ctrl.xml | 2 +- 10 files changed, 197 insertions(+), 44 deletions(-) (limited to 'indra/newview/llviewertexturelist.cpp') diff --git a/indra/newview/llfloaterchangeitemthumbnail.cpp b/indra/newview/llfloaterchangeitemthumbnail.cpp index 77212507ab..780130039b 100644 --- a/indra/newview/llfloaterchangeitemthumbnail.cpp +++ b/indra/newview/llfloaterchangeitemthumbnail.cpp @@ -512,6 +512,13 @@ void LLFloaterChangeItemThumbnail::onPasteFromClipboard(void *userdata) asset_id = potential_uuid; } } + + LLInventoryObject* obj = self->getInventoryObject(); + if (obj && obj->getThumbnailUUID() == asset_id) + { + // nothing to do + return; + } if (asset_id.notNull()) { self->assignAndValidateAsset(asset_id); @@ -547,6 +554,8 @@ struct ImageLoadedData LLUUID mObjectId; LLHandle mFloaterHandle; bool mSilent; + // Keep image reference to prevent deletion on timeout + LLPointer mTexturep; }; void LLFloaterChangeItemThumbnail::assignAndValidateAsset(const LLUUID &asset_id, bool silent) @@ -576,8 +585,9 @@ void LLFloaterChangeItemThumbnail::assignAndValidateAsset(const LLUUID &asset_id data->mThumbnailId = asset_id; data->mFloaterHandle = getHandle(); data->mSilent = silent; + data->mTexturep = texturep; - texturep->setLoadedCallback(onImageLoaded, + texturep->setLoadedCallback(onImageDataLoaded, MAX_DISCARD_LEVEL, // Don't need full image, just size data FALSE, FALSE, @@ -636,7 +646,7 @@ bool LLFloaterChangeItemThumbnail::validateAsset(const LLUUID &asset_id) } //static -void LLFloaterChangeItemThumbnail::onImageLoaded( +void LLFloaterChangeItemThumbnail::onImageDataLoaded( BOOL success, LLViewerFetchedTexture *src_vi, LLImageRaw* src, @@ -678,6 +688,45 @@ void LLFloaterChangeItemThumbnail::onImageLoaded( delete data; } +//static +void LLFloaterChangeItemThumbnail::onFullImageLoaded( + BOOL success, + LLViewerFetchedTexture* src_vi, + LLImageRaw* src, + LLImageRaw* aux_src, + S32 discard_level, + BOOL final, + void* userdata) +{ + if (!userdata) return; + + if (!final && success) return; //not done yet + + ImageLoadedData* data = (ImageLoadedData*)userdata; + + if (success) + { + if (src_vi->getFullWidth() != src_vi->getFullHeight() + || src_vi->getFullWidth() < LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN) + { + if (!data->mSilent) + { + LLNotificationsUtil::add("ThumbnailDimentionsLimit"); + } + } + else if (src_vi->getFullWidth() > LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MAX) + { + LLFloaterSimpleSnapshot::uploadThumbnail(src, data->mObjectId, LLUUID::null); + } + else + { + setThumbnailId(data->mThumbnailId, data->mObjectId); + } + } + + delete data; +} + void LLFloaterChangeItemThumbnail::showTexturePicker(const LLUUID &thumbnail_id) { // show hourglass cursor when loading inventory window @@ -725,8 +774,7 @@ void LLFloaterChangeItemThumbnail::showTexturePicker(const LLUUID &thumbnail_id) texture_floaterp->setBakeTextureEnabled(FALSE); texture_floaterp->setCanApplyImmediately(false); texture_floaterp->setCanApply(false, true, false /*Hide 'preview disabled'*/); - texture_floaterp->setDimentionsLimits(LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MAX, - LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN); + texture_floaterp->setMinDimentionsLimits(LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN); addDependentFloater(texture_floaterp); } @@ -743,14 +791,86 @@ void LLFloaterChangeItemThumbnail::onTexturePickerCommit() if (floaterp) { LLUUID asset_id = floaterp->getAssetID(); - if (validateAsset(asset_id)) + + if (asset_id.isNull()) { setThumbnailId(asset_id); + return; } - else + + LLInventoryObject* obj = getInventoryObject(); + if (obj && obj->getThumbnailUUID() == asset_id) + { + // nothing to do + return; + } + + LLPointer texturep = LLViewerTextureManager::findFetchedTexture(asset_id, TEX_LIST_STANDARD); + if (!texturep) + { + LL_WARNS() << "Image " << asset_id << " doesn't exist" << LL_ENDL; + return; + } + + if (texturep->isMissingAsset()) + { + LL_WARNS() << "Image " << asset_id << " is missing" << LL_ENDL; + return; + } + + if (texturep->getFullWidth() != texturep->getFullHeight()) + { + LLNotificationsUtil::add("ThumbnailDimentionsLimit"); + return; + } + + if (texturep->getFullWidth() < LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN + && texturep->getFullWidth() > 0) { LLNotificationsUtil::add("ThumbnailDimentionsLimit"); + return; + } + + if (texturep->getFullWidth() > LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MAX + || texturep->getFullWidth() == 0) + { + if (texturep->isFullyLoaded() + && (texturep->getCachedRawImageLevel() == 0 || texturep->getRawImageLevel() == 0) + && (texturep->isCachedRawImageReady() || texturep->isRawImageValid())) + { + if (texturep->isRawImageValid()) + { + LLFloaterSimpleSnapshot::uploadThumbnail(texturep->getRawImage(), mItemId, mTaskId); + } + else + { + LLFloaterSimpleSnapshot::uploadThumbnail(texturep->getCachedRawImage(), mItemId, mTaskId); + } + } + else + { + ImageLoadedData* data = new ImageLoadedData(); + data->mObjectId = mItemId; + data->mThumbnailId = asset_id; + data->mFloaterHandle = getHandle(); + data->mSilent = false; + data->mTexturep = texturep; + + texturep->setBoostLevel(LLGLTexture::BOOST_PREVIEW); + texturep->setMinDiscardLevel(0); + texturep->setLoadedCallback(onFullImageLoaded, + 0, // Need best quality + TRUE, + FALSE, + (void*)data, + NULL, + FALSE); + texturep->forceToSaveRawImage(0); + } + return; } + + setThumbnailId(asset_id); } } diff --git a/indra/newview/llfloaterchangeitemthumbnail.h b/indra/newview/llfloaterchangeitemthumbnail.h index 02f934337b..a91e9b8ee9 100644 --- a/indra/newview/llfloaterchangeitemthumbnail.h +++ b/indra/newview/llfloaterchangeitemthumbnail.h @@ -83,13 +83,20 @@ private: static void onRemovalConfirmation(const LLSD& notification, const LLSD& response, LLHandle handle); void assignAndValidateAsset(const LLUUID &asset_id, bool silent = false); - static void onImageLoaded(BOOL success, + static void onImageDataLoaded(BOOL success, LLViewerFetchedTexture *src_vi, LLImageRaw* src, LLImageRaw* aux_src, S32 discard_level, BOOL final, void* userdata); + static void onFullImageLoaded(BOOL success, + LLViewerFetchedTexture* src_vi, + LLImageRaw* src, + LLImageRaw* aux_src, + S32 discard_level, + BOOL final, + void* userdata); void showTexturePicker(const LLUUID &thumbnail_id); void onTexturePickerCommit(); diff --git a/indra/newview/llfloatersimplesnapshot.cpp b/indra/newview/llfloatersimplesnapshot.cpp index 757ac605e3..58604c5628 100644 --- a/indra/newview/llfloatersimplesnapshot.cpp +++ b/indra/newview/llfloatersimplesnapshot.cpp @@ -397,6 +397,21 @@ void LLFloaterSimpleSnapshot::uploadThumbnail(const std::string &file_path, cons uploadImageUploadFile(temp_file, inventory_id, task_id); } +// static +void LLFloaterSimpleSnapshot::uploadThumbnail(LLPointer raw_image, const LLUUID& inventory_id, const LLUUID& task_id) +{ + std::string temp_file = gDirUtilp->getTempFilename(); + if (!LLViewerTextureList::createUploadFile(raw_image, temp_file, THUMBNAIL_SNAPSHOT_DIM_MAX, THUMBNAIL_SNAPSHOT_DIM_MIN)) + { + LLSD notif_args; + notif_args["REASON"] = LLImage::getLastError().c_str(); + LLNotificationsUtil::add("CannotUploadTexture", notif_args); + LL_WARNS("Thumbnail") << "Failed to upload thumbnail for " << inventory_id << " " << task_id << ", reason: " << notif_args["REASON"].asString() << LL_ENDL; + return; + } + uploadImageUploadFile(temp_file, inventory_id, task_id); +} + // static void LLFloaterSimpleSnapshot::uploadImageUploadFile(const std::string &temp_file, const LLUUID &inventory_id, const LLUUID &task_id) { diff --git a/indra/newview/llfloatersimplesnapshot.h b/indra/newview/llfloatersimplesnapshot.h index a2bf2946d4..91a81ee5c3 100644 --- a/indra/newview/llfloatersimplesnapshot.h +++ b/indra/newview/llfloatersimplesnapshot.h @@ -63,6 +63,7 @@ public: void postSave(); static void uploadThumbnail(const std::string &file_path, const LLUUID &inventory_id, const LLUUID &task_id); + static void uploadThumbnail(LLPointer raw_image, const LLUUID& inventory_id, const LLUUID& task_id); class Impl; friend class Impl; diff --git a/indra/newview/llsnapshotlivepreview.cpp b/indra/newview/llsnapshotlivepreview.cpp index 37ebcd1266..b7a1832b17 100644 --- a/indra/newview/llsnapshotlivepreview.cpp +++ b/indra/newview/llsnapshotlivepreview.cpp @@ -876,37 +876,7 @@ LLPointer LLSnapshotLivePreview::getEncodedImage() bool LLSnapshotLivePreview::createUploadFile(const std::string &out_filename, const S32 max_image_dimentions, const S32 min_image_dimentions) { - // make a copy, since convertToUploadFile modifies raw image - LLPointer raw_image = new LLImageRaw( - mPreviewImage->getData(), - mPreviewImage->getWidth(), - mPreviewImage->getHeight(), - mPreviewImage->getComponents()); - - LLPointer compressedImage = LLViewerTextureList::convertToUploadFile(raw_image, max_image_dimentions); - if (compressedImage->getWidth() < min_image_dimentions || compressedImage->getHeight() < min_image_dimentions) - { - std::string reason = llformat("Images below %d x %d pixels are not allowed. Actual size: %d x %dpx", - min_image_dimentions, - min_image_dimentions, - compressedImage->getWidth(), - compressedImage->getHeight()); - compressedImage->setLastError(reason); - return FALSE; - } - if (compressedImage.isNull()) - { - compressedImage->setLastError("Couldn't convert the image to jpeg2000."); - LL_INFOS() << "Couldn't convert to j2c, file : " << out_filename << LL_ENDL; - return false; - } - if (!compressedImage->save(out_filename)) - { - compressedImage->setLastError("Couldn't create the jpeg2000 image for upload."); - LL_INFOS() << "Couldn't create output file : " << out_filename << LL_ENDL; - return false; - } - return true; + return LLViewerTextureList::createUploadFile(mPreviewImage, out_filename, max_image_dimentions, min_image_dimentions); } // We actually estimate the data size so that we do not require actual compression when showing the preview diff --git a/indra/newview/lltexturectrl.cpp b/indra/newview/lltexturectrl.cpp index a746744e53..0c3730d084 100644 --- a/indra/newview/lltexturectrl.cpp +++ b/indra/newview/lltexturectrl.cpp @@ -1197,15 +1197,12 @@ void LLFloaterTexturePicker::setCanApply(bool can_preview, bool can_apply, bool mPreviewSettingChanged = true; } -void LLFloaterTexturePicker::setDimentionsLimits(S32 max_dim, S32 min_dim) +void LLFloaterTexturePicker::setMinDimentionsLimits(S32 min_dim) { - mMaxDim = max_dim; mMinDim = min_dim; std::string formatted_dims = llformat("%dx%d", mMinDim, mMinDim); mResolutionWarning->setTextArg("[MINTEXDIM]", formatted_dims); - formatted_dims = llformat("%dx%d", mMaxDim, mMaxDim); - mResolutionWarning->setTextArg("[MAXTEXDIM]", formatted_dims); } void LLFloaterTexturePicker::onFilterEdit(const std::string& search_string ) diff --git a/indra/newview/lltexturectrl.h b/indra/newview/lltexturectrl.h index 60543191b6..7239b97552 100644 --- a/indra/newview/lltexturectrl.h +++ b/indra/newview/lltexturectrl.h @@ -323,7 +323,7 @@ public: void onFilterEdit(const std::string& search_string); void setCanApply(bool can_preview, bool can_apply, bool inworld_image = true); - void setDimentionsLimits(S32 max_dim, S32 min_dim); + void setMinDimentionsLimits(S32 min_dim); void setTextureSelectedCallback(const texture_selected_callback& cb) { mTextureSelectedCallback = cb; } void setOnFloaterCloseCallback(const floater_close_callback& cb) { mOnFloaterCloseCallback = cb; } void setOnFloaterCommitCallback(const floater_commit_callback& cb) { mOnFloaterCommitCallback = cb; } diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp index d10e1ea8c9..f9fe8054a4 100644 --- a/indra/newview/llviewertexturelist.cpp +++ b/indra/newview/llviewertexturelist.cpp @@ -1285,6 +1285,45 @@ void LLViewerTextureList::decodeAllImages(F32 max_time) << LL_ENDL; } +bool LLViewerTextureList::createUploadFile(LLPointer raw_image, + const std::string& out_filename, + const S32 max_image_dimentions, + const S32 min_image_dimentions) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + + // make a copy, since convertToUploadFile scales raw image + LLPointer scale_image = new LLImageRaw( + raw_image->getData(), + raw_image->getWidth(), + raw_image->getHeight(), + raw_image->getComponents()); + + LLPointer compressedImage = LLViewerTextureList::convertToUploadFile(scale_image, max_image_dimentions); + if (compressedImage->getWidth() < min_image_dimentions || compressedImage->getHeight() < min_image_dimentions) + { + std::string reason = llformat("Images below %d x %d pixels are not allowed. Actual size: %d x %dpx", + min_image_dimentions, + min_image_dimentions, + compressedImage->getWidth(), + compressedImage->getHeight()); + compressedImage->setLastError(reason); + return false; + } + if (compressedImage.isNull()) + { + compressedImage->setLastError("Couldn't convert the image to jpeg2000."); + LL_INFOS() << "Couldn't convert to j2c, file : " << out_filename << LL_ENDL; + return false; + } + if (!compressedImage->save(out_filename)) + { + compressedImage->setLastError("Couldn't create the jpeg2000 image for upload."); + LL_INFOS() << "Couldn't create output file : " << out_filename << LL_ENDL; + return false; + } + return true; +} BOOL LLViewerTextureList::createUploadFile(const std::string& filename, const std::string& out_filename, diff --git a/indra/newview/llviewertexturelist.h b/indra/newview/llviewertexturelist.h index 8fc65fc9ce..82dec6b329 100644 --- a/indra/newview/llviewertexturelist.h +++ b/indra/newview/llviewertexturelist.h @@ -92,6 +92,10 @@ class LLViewerTextureList friend class LLLocalBitmap; public: + static bool createUploadFile(LLPointer raw_image, + const std::string& out_filename, + const S32 max_image_dimentions = LLViewerFetchedTexture::MAX_IMAGE_SIZE_DEFAULT, + const S32 min_image_dimentions = 0); static BOOL createUploadFile(const std::string& filename, const std::string& out_filename, const U8 codec, diff --git a/indra/newview/skins/default/xui/en/floater_texture_ctrl.xml b/indra/newview/skins/default/xui/en/floater_texture_ctrl.xml index 771113d989..8081af6673 100644 --- a/indra/newview/skins/default/xui/en/floater_texture_ctrl.xml +++ b/indra/newview/skins/default/xui/en/floater_texture_ctrl.xml @@ -96,7 +96,7 @@ name="over_limit_lbl" visible="false" top_delta="0"> - Selected texture is [TEXDIM]. Inventory image must be square, between [MINTEXDIM] and [MAXTEXDIM]. + Selected texture is [TEXDIM]. Inventory image must be square, no less than [MINTEXDIM]. -- cgit v1.3