Files
loongoffice/vcl/source/image/ImplImageTree.cxx
Regina Henschel fcccd6bf9f tdf#153421 On Windows exclude cropmarkers.svg from scaling
The file cropmarkers.svg is used to create individual crop markers.
That methods expects a certain, fixed size in unit pixel. The number of
pixels was wrong on Windows, if display scale is set so large that
resulting dpi is larger than 120.
The patch excludes this special file from scaling.

For testing you need to set display scale to larger than 125% in Windows
(125%*96dpi=120dpi). And remove any existing cropmarkers.png in
cache/themename_svg/DPI/svx/res/. 'DPI' therein is not your actual dpi
but the scaling factor from CountDPIScaleFactor.

Change-Id: I6b508e28f2f3782441ad1283cfe85c4bd1588383
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/172019
Tested-by: Jenkins
Reviewed-by: Patrick Luby <guibomacdev@gmail.com>
Reviewed-by: Regina Henschel <rb.henschel@t-online.de>
2024-08-22 18:12:00 +02:00

725 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: */