/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace css; bool ImageRequestParameters::convertToDarkTheme() { static bool bIconsForDarkTheme = !!getenv("VCL_ICONS_FOR_DARK_THEME"); bool bConvertToDarkTheme = false; if (!(meFlags & ImageLoadFlags::IgnoreDarkTheme)) bConvertToDarkTheme = bIconsForDarkTheme; return bConvertToDarkTheme; } sal_Int32 ImageRequestParameters::scalePercentage() { sal_Int32 aScalePercentage = 100; OutputDevice* pDevice = Application::GetDefaultDevice(); if (!(meFlags & ImageLoadFlags::IgnoreScalingFactor) && pDevice != nullptr) aScalePercentage = pDevice->GetDPIScalePercentage(); else if (mnScalePercentage > 0) aScalePercentage = mnScalePercentage; return aScalePercentage; } namespace { OUString convertLcTo32Path(std::u16string_view rPath) { OUString aResult; size_t nSlashPos = rPath.rfind('/'); if (nSlashPos != std::u16string_view::npos) { sal_Int32 nCopyFrom = nSlashPos + 1; std::u16string_view sFile = rPath.substr(nCopyFrom); std::u16string_view sDir = rPath.substr(0, nSlashPos); if (!sFile.empty() && o3tl::starts_with(sFile, u"lc_")) { aResult = OUString::Concat(sDir) + "/32/" + sFile.substr(3); } } return aResult; } OUString createPath(std::u16string_view name, sal_Int32 pos, std::u16string_view locale) { return OUString::Concat(name.substr(0, pos + 1)) + locale + name.substr(pos); } OUString getIconCacheUrlImpl() { OUString sDir = u"${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/cache/"_ustr; rtl::Bootstrap::expandMacros(sDir); return sDir; } OUString getIconCacheUrl(std::u16string_view sVariant, ImageRequestParameters const & rParameters) { // the macro expansion can be expensive in bulk, so cache that static OUString CACHE_DIR = getIconCacheUrlImpl(); return CACHE_DIR + rParameters.msStyle + "/" + sVariant + "/" + rParameters.msName; } OUString createIconCacheUrl( std::u16string_view sVariant, ImageRequestParameters const & rParameters) { OUString sUrl(getIconCacheUrl(sVariant, rParameters)); OUString sDir = sUrl.copy(0, sUrl.lastIndexOf('/')); osl::Directory::createPath(sDir); return sUrl; } bool urlExists(OUString const & sUrl) { osl::File aFile(sUrl); osl::FileBase::RC eRC = aFile.open(osl_File_OpenFlag_Read); return osl::FileBase::E_None == eRC; } OUString getNameNoExtension(std::u16string_view sName) { size_t nDotPosition = sName.rfind('.'); return OUString(sName.substr(0, nDotPosition)); } std::shared_ptr wrapStream(uno::Reference const & rInputStream) { // This could use SvInputStream instead if that did not have a broken // SeekPos implementation for an XInputStream that is not also XSeekable // (cf. "@@@" at tags/DEV300_m37/svtools/source/misc1/strmadpt.cxx@264807 // l. 593): OSL_ASSERT(rInputStream.is()); std::shared_ptr aMemoryStream(std::make_shared()); for (;;) { const sal_Int32 nSize(2048); uno::Sequence aData(nSize); sal_Int32 nRead = rInputStream->readBytes(aData, nSize); aMemoryStream->WriteBytes(aData.getConstArray(), nRead); if (nRead < nSize) break; } aMemoryStream->Seek(0); rInputStream->closeInput(); return aMemoryStream; } void loadImageFromStream(std::shared_ptr const & xStream, OUString const & rPath, ImageRequestParameters& rParameters) { bool bConvertToDarkTheme = rParameters.convertToDarkTheme(); sal_Int32 aScalePercentage = rParameters.scalePercentage(); if (rPath.endsWith(".png")) { vcl::PngImageReader aPNGReader(*xStream); aPNGReader.read(rParameters.mrBitmap); } else if (rPath.endsWith(".svg")) { rParameters.mbWriteImageToCache = true; // We always want to cache a SVG image #ifdef _WIN32 // tdf#153421. Do not scale, individual crop handles are created from it using pixel unit. if (rPath.endsWith("cropmarkers.svg")) vcl::bitmap::loadFromSvg(*xStream, rPath, rParameters.mrBitmap, 1.0); else #endif vcl::bitmap::loadFromSvg(*xStream, rPath, rParameters.mrBitmap, aScalePercentage / 100.0); if (bConvertToDarkTheme) BitmapFilter::Filter(rParameters.mrBitmap, BitmapLightenFilter()); return; } else { ReadDIBBitmapEx(rParameters.mrBitmap, *xStream); } if (bConvertToDarkTheme) { rParameters.mbWriteImageToCache = true; // Cache the dark variant BitmapFilter::Filter(rParameters.mrBitmap, BitmapLightenFilter()); } if (aScalePercentage > 100) { rParameters.mbWriteImageToCache = true; // Cache the scaled variant double aScaleFactor(aScalePercentage / 100.0); // when scaling use the full 24bit RGB values rParameters.mrBitmap.Convert(BmpConversion::N24Bit); rParameters.mrBitmap.Scale(aScaleFactor, aScaleFactor, BmpScaleFlag::Fast); } } } // end anonymous namespace ImplImageTree::ImplImageTree() { } ImplImageTree::~ImplImageTree() { } std::vector ImplImageTree::getPaths(OUString const & name, LanguageTag const & rLanguageTag) { std::vector sPaths; sal_Int32 pos = name.lastIndexOf('/'); if (pos != -1) { for (const OUString& rFallback : rLanguageTag.getFallbackStrings(true)) { OUString aFallbackName = getNameNoExtension(getRealImageName(createPath(name, pos, rFallback))); sPaths.emplace_back(aFallbackName + ".png"); sPaths.emplace_back(aFallbackName + ".svg"); } } OUString aRealName = getNameNoExtension(getRealImageName(name)); sPaths.emplace_back(aRealName + ".png"); sPaths.emplace_back(aRealName + ".svg"); return sPaths; } OUString ImplImageTree::getImageUrl(OUString const & rName, OUString const & rStyle, OUString const & rLang) { OUString aStyle(rStyle); while (!aStyle.isEmpty()) { try { setStyle(aStyle); if (checkPathAccess()) { IconSet& rIconSet = getCurrentIconSet(); const uno::Reference & rNameAccess = rIconSet.maNameAccess; LanguageTag aLanguageTag(rLang); for (const OUString& rPath: getPaths(rName, aLanguageTag)) { if (rNameAccess->hasByName(rPath)) { return "vnd.sun.star.zip://" + rtl::Uri::encode(rIconSet.maURL, rtl_UriCharClassRegName, rtl_UriEncodeIgnoreEscapes, RTL_TEXTENCODING_UTF8) + "/" + rPath; } } } } catch (const uno::Exception &) { TOOLS_INFO_EXCEPTION("vcl", ""); } aStyle = fallbackStyle(aStyle); } return OUString(); } uno::Reference ImplImageTree::getImageXInputStream(OUString const & rName, OUString const & rStyle, OUString const & rLang) { OUString aStyle(rStyle); while (!aStyle.isEmpty()) { try { setStyle(aStyle); if (checkPathAccess()) { IconSet& rIconSet = getCurrentIconSet(); const uno::Reference& rNameAccess = rIconSet.maNameAccess; LanguageTag aLanguageTag(rLang); for (const OUString& rPath: getPaths(rName, aLanguageTag)) { if (rNameAccess->hasByName(rPath)) { uno::Reference aStream; bool ok = rNameAccess->getByName(rPath) >>= aStream; assert(ok); (void)ok; // prevent unused warning in release build return aStream; } } } } catch (const uno::Exception &) { TOOLS_INFO_EXCEPTION("vcl", ""); } aStyle = fallbackStyle(aStyle); } return nullptr; } std::shared_ptr ImplImageTree::getImageStream(OUString const & rName, OUString const & rStyle, OUString const & rLang) { uno::Reference xStream = getImageXInputStream(rName, rStyle, rLang); if (xStream) return wrapStream(xStream); return std::shared_ptr(); } OUString ImplImageTree::fallbackStyle(std::u16string_view rsStyle) { OUString sResult; if (rsStyle == u"colibre" || rsStyle == u"helpimg") sResult = ""; else if (rsStyle == u"sifr" || rsStyle == u"breeze_dark") sResult = "breeze"; else if (rsStyle == u"sifr_dark" ) sResult = "breeze_dark"; else sResult = "colibre"; return sResult; } bool ImplImageTree::loadImage(OUString const & rName, OUString const & rStyle, BitmapEx & rBitmap, bool localized, const ImageLoadFlags eFlags, sal_Int32 nScalePercentage) { OUString aCurrentStyle(rStyle); while (!aCurrentStyle.isEmpty()) { try { ImageRequestParameters aParameters(rName, aCurrentStyle, rBitmap, localized, eFlags, nScalePercentage); if (doLoadImage(aParameters)) return true; } catch (uno::RuntimeException &) {} aCurrentStyle = fallbackStyle(aCurrentStyle); } return false; } namespace { OUString createVariant(ImageRequestParameters& rParameters) { bool bConvertToDarkTheme = rParameters.convertToDarkTheme(); sal_Int32 aScalePercentage = rParameters.scalePercentage(); OUString aVariant = OUString::number(aScalePercentage); if (bConvertToDarkTheme) aVariant += "-dark"; return aVariant; } bool loadDiskCachedVersion(std::u16string_view sVariant, ImageRequestParameters& rParameters) { OUString sUrl(getIconCacheUrl(sVariant, rParameters)); if (!urlExists(sUrl)) return false; SvFileStream aFileStream(sUrl, StreamMode::READ); vcl::PngImageReader aPNGReader(aFileStream); aPNGReader.read(rParameters.mrBitmap); return true; } void cacheBitmapToDisk(std::u16string_view sVariant, ImageRequestParameters const & rParameters) { OUString sUrl(createIconCacheUrl(sVariant, rParameters)); try { SvFileStream aStream(sUrl, StreamMode::WRITE); vcl::PngImageWriter aWriter(aStream); aWriter.write(rParameters.mrBitmap); aStream.Close(); } catch (...) {} } } // end anonymous namespace bool ImplImageTree::doLoadImage(ImageRequestParameters& rParameters) { setStyle(rParameters.msStyle); if (iconCacheLookup(rParameters)) return true; OUString aVariant = createVariant(rParameters); if (loadDiskCachedVersion(aVariant, rParameters)) return true; if (!rParameters.mrBitmap.IsEmpty()) rParameters.mrBitmap.SetEmpty(); LanguageTag aLanguageTag = Application::GetSettings().GetUILanguageTag(); std::vector aPaths = getPaths(rParameters.msName, aLanguageTag); bool bFound = false; try { bFound = findImage(aPaths, rParameters); } catch (uno::RuntimeException&) { throw; } catch (const uno::Exception&) { TOOLS_INFO_EXCEPTION("vcl", "ImplImageTree::doLoadImage"); } if (bFound) { if (rParameters.mbWriteImageToCache) { cacheBitmapToDisk(aVariant, rParameters); } getIconCache(rParameters)[rParameters.msName] = std::make_pair(rParameters.mbLocalized, rParameters.mrBitmap); } return bFound; } void ImplImageTree::shutdown() { maCurrentStyle.clear(); maIconSets.clear(); } void ImplImageTree::setStyle(OUString const & style) { assert(!style.isEmpty()); if (style != maCurrentStyle) { maCurrentStyle = style; createStyle(); } } /** * The vcldemo app doesn't set up all the config stuff that the main app does, so we need another * way of finding the cursor images. */ static bool isVclDemo() { static const bool bVclDemoOverride = std::getenv("LIBO_VCL_DEMO") != nullptr; return bVclDemoOverride; } void ImplImageTree::createStyle() { if (maIconSets.find(maCurrentStyle) != maIconSets.end()) return; OUString sThemeUrl; if (isVclDemo()) { if (maCurrentStyle == "default") sThemeUrl = "file://" SRC_ROOT "/icon-themes/colibre-svg"; else sThemeUrl = "file://" SRC_ROOT "/icon-themes/" + maCurrentStyle; } else if (maCurrentStyle != "default") { OUString paths = vcl::IconThemeScanner::GetStandardIconThemePath(); std::deque aPaths; sal_Int32 nIndex = 0; do { aPaths.push_front(paths.getToken(0, ';', nIndex)); } while (nIndex >= 0); for (const auto& path : aPaths) { INetURLObject aUrl(path); OSL_ASSERT(!aUrl.HasError()); bool ok = aUrl.Append(Concat2View("images_" + maCurrentStyle), INetURLObject::EncodeMechanism::All); OSL_ASSERT(ok); sThemeUrl = aUrl.GetMainURL(INetURLObject::DecodeMechanism::NONE) + ".zip"; if (urlExists(sThemeUrl)) break; sThemeUrl.clear(); } if (sThemeUrl.isEmpty()) return; } else { sThemeUrl += "images"; if (!urlExists(sThemeUrl)) return; } maIconSets[maCurrentStyle] = IconSet(sThemeUrl); loadImageLinks(); } /// Find an icon cache for the right scale factor ImplImageTree::IconCache &ImplImageTree::getIconCache(const ImageRequestParameters& rParameters) { IconSet &rSet = getCurrentIconSet(); auto it = rSet.maScaledIconCaches.find(rParameters.mnScalePercentage); if ( it != rSet.maScaledIconCaches.end() ) return it->second; rSet.maScaledIconCaches.emplace(rParameters.mnScalePercentage, IconCache()); return rSet.maScaledIconCaches[rParameters.mnScalePercentage]; } bool ImplImageTree::iconCacheLookup(ImageRequestParameters& rParameters) { IconCache& rIconCache = getIconCache(rParameters); IconCache::iterator i(rIconCache.find(getRealImageName(rParameters.msName))); if (i != rIconCache.end() && i->second.first == rParameters.mbLocalized) { rParameters.mrBitmap = i->second.second; return true; } return false; } bool ImplImageTree::findImage(std::vector const & rPaths, ImageRequestParameters& rParameters) { if (!checkPathAccess()) return false; uno::Reference const & rNameAccess = getCurrentIconSet().maNameAccess; for (OUString const & rPath : rPaths) { if (rNameAccess->hasByName(rPath)) { uno::Reference aStream; bool ok = rNameAccess->getByName(rPath) >>= aStream; assert(ok); (void)ok; // prevent unused warning in release build loadImageFromStream(wrapStream(aStream), rPath, rParameters); return true; } } return false; } void ImplImageTree::loadImageLinks() { static constexpr OUString aLinkFilename(u"links.txt"_ustr); if (!checkPathAccess()) return; const uno::Reference &rNameAccess = getCurrentIconSet().maNameAccess; if (rNameAccess->hasByName(aLinkFilename)) { uno::Reference xStream; bool ok = rNameAccess->getByName(aLinkFilename) >>= xStream; assert(ok); (void)ok; // prevent unused warning in release build parseLinkFile(wrapStream(xStream)); return; } } void ImplImageTree::parseLinkFile(std::shared_ptr const & xStream) { OStringBuffer aLine; OUString aLink, aOriginal; int nLineNo = 0; while (xStream->ReadLine(aLine)) { ++nLineNo; if (aLine.isEmpty()) continue; sal_Int32 nIndex = 0; aLink = OStringToOUString(o3tl::getToken(aLine, 0, ' ', nIndex), RTL_TEXTENCODING_UTF8); aOriginal = OStringToOUString(o3tl::getToken(aLine, 0, ' ', nIndex), RTL_TEXTENCODING_UTF8); // skip comments, or incomplete entries if (aLink.isEmpty() || aLink[0] == '#' || aOriginal.isEmpty()) { if (aLink.isEmpty() || aOriginal.isEmpty()) SAL_WARN("vcl", "ImplImageTree::parseLinkFile: icon links.txt parse error, incomplete link at line " << nLineNo); continue; } getCurrentIconSet().maLinkHash[aLink] = aOriginal; OUString aOriginal32 = convertLcTo32Path(aOriginal); OUString aLink32 = convertLcTo32Path(aLink); if (!aOriginal32.isEmpty() && !aLink32.isEmpty()) getCurrentIconSet().maLinkHash[aLink32] = aOriginal32; } } OUString const & ImplImageTree::getRealImageName(OUString const & rIconName) { IconLinkHash & rLinkHash = maIconSets[maCurrentStyle].maLinkHash; OUString sNameWithNoExtension = getNameNoExtension(rIconName); // PNG is priority auto it = rLinkHash.find(sNameWithNoExtension + ".png"); if (it != rLinkHash.end()) return it->second; // also check SVG name it = rLinkHash.find(sNameWithNoExtension + ".svg"); if (it != rLinkHash.end()) return it->second; // neither was found so just return the original name return rIconName; } namespace { class FolderFileAccess : public ::cppu::WeakImplHelper { public: uno::Reference< uno::XComponentContext > mxContext; OUString maURL; FolderFileAccess(uno::Reference< uno::XComponentContext > context, OUString url) : mxContext(std::move(context)), maURL(std::move(url)) {} // XElementAccess virtual css::uno::Type SAL_CALL getElementType() override { return cppu::UnoType::get(); } virtual sal_Bool SAL_CALL hasElements() override { return true; } // XNameAccess virtual css::uno::Any SAL_CALL getByName( const OUString& aName ) override { uno::Reference< io::XInputStream > xInputStream = ucb::SimpleFileAccess::create(mxContext)->openFileRead( maURL + "/" + aName ); return css::uno::Any(xInputStream); } virtual css::uno::Sequence< OUString > SAL_CALL getElementNames() override { return {}; } virtual sal_Bool SAL_CALL hasByName( const OUString& aName ) override { osl::File aBaseFile(maURL + "/" + aName); return osl::File::E_None == aBaseFile.open(osl_File_OpenFlag_Read); } }; } bool ImplImageTree::checkPathAccess() { IconSet& rIconSet = getCurrentIconSet(); uno::Reference & rNameAccess = rIconSet.maNameAccess; if (rNameAccess.is()) return true; try { if (isVclDemo()) rNameAccess = new FolderFileAccess(comphelper::getProcessComponentContext(), rIconSet.maURL); else rNameAccess = packages::zip::ZipFileAccess::createWithURL(comphelper::getProcessComponentContext(), rIconSet.maURL); } catch (const uno::RuntimeException &) { throw; } catch (const uno::Exception &) { TOOLS_INFO_EXCEPTION("vcl", "ImplImageTree::zip file location " << rIconSet.maURL); return false; } return rNameAccess.is(); } uno::Reference const & ImplImageTree::getNameAccess() { (void)checkPathAccess(); return getCurrentIconSet().maNameAccess; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */