1
0
Fork 0
libreoffice/vcl/source/image/ImplImageTree.cxx
Daniel Baumann 8e63e14cf6
Adding upstream version 4:25.2.3.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 16:20:04 +02:00

724 lines
22 KiB
C++

/* -*- 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 <config_folders.h>
#include <sal/config.h>
#include <sal/log.hxx>
#include <deque>
#include <string_view>
#include <com/sun/star/container/XNameAccess.hpp>
#include <com/sun/star/io/XInputStream.hpp>
#include <com/sun/star/packages/zip/ZipFileAccess.hpp>
#include <com/sun/star/ucb/SimpleFileAccess.hpp>
#include <com/sun/star/uno/Exception.hpp>
#include <com/sun/star/uno/RuntimeException.hpp>
#include <com/sun/star/uno/Sequence.hxx>
#include <comphelper/processfactory.hxx>
#include <cppuhelper/implbase.hxx>
#include <osl/file.hxx>
#include <osl/diagnose.h>
#include <osl/process.h>
#include <rtl/bootstrap.hxx>
#include <rtl/uri.hxx>
#include <rtl/strbuf.hxx>
#include <comphelper/diagnose_ex.hxx>
#include <tools/stream.hxx>
#include <tools/urlobj.hxx>
#include <implimagetree.hxx>
#include <utility>
#include <vcl/bitmapex.hxx>
#include <vcl/dibtools.hxx>
#include <vcl/settings.hxx>
#include <vcl/svapp.hxx>
#include <vcl/BitmapTools.hxx>
#include <IconThemeScanner.hxx>
#include <vcl/filter/PngImageReader.hxx>
#include <vcl/outdev.hxx>
#include <vcl/filter/PngImageWriter.hxx>
#include <o3tl/string_view.hxx>
#include <bitmap/BitmapLightenFilter.hxx>
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<SvMemoryStream> wrapStream(uno::Reference<io::XInputStream> 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<SvMemoryStream> aMemoryStream(std::make_shared<SvMemoryStream>());
for (;;)
{
const sal_Int32 nSize(2048);
uno::Sequence<sal_Int8> 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<SvStream> 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<OUString> ImplImageTree::getPaths(OUString const & name, LanguageTag const & rLanguageTag)
{
std::vector<OUString> 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<container::XNameAccess> & 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<io::XInputStream> 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<container::XNameAccess>& rNameAccess = rIconSet.maNameAccess;
LanguageTag aLanguageTag(rLang);
for (const OUString& rPath: getPaths(rName, aLanguageTag))
{
if (rNameAccess->hasByName(rPath))
{
uno::Reference<io::XInputStream> 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<SvMemoryStream> ImplImageTree::getImageStream(OUString const & rName, OUString const & rStyle, OUString const & rLang)
{
uno::Reference<io::XInputStream> xStream = getImageXInputStream(rName, rStyle, rLang);
if (xStream)
return wrapStream(xStream);
return std::shared_ptr<SvMemoryStream>();
}
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<OUString> 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<OUString> 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<OUString> const & rPaths, ImageRequestParameters& rParameters)
{
if (!checkPathAccess())
return false;
uno::Reference<container::XNameAccess> const & rNameAccess = getCurrentIconSet().maNameAccess;
for (OUString const & rPath : rPaths)
{
if (rNameAccess->hasByName(rPath))
{
uno::Reference<io::XInputStream> 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<container::XNameAccess> &rNameAccess = getCurrentIconSet().maNameAccess;
if (rNameAccess->hasByName(aLinkFilename))
{
uno::Reference<io::XInputStream> 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<SvStream> 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<css::container::XNameAccess>
{
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<io::XInputStream>::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<container::XNameAccess> & 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<container::XNameAccess> const & ImplImageTree::getNameAccess()
{
(void)checkPathAccess();
return getCurrentIconSet().maNameAccess;
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */