93350f4694
Introduced in commit a1b18eba35
(Implement
Custom Font Collections on pre-Windows 10 systems, 2024-08-20).
Change-Id: I2aa16ae560d4dad8489e13289ad79d8be349b6b1
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/178031
Tested-by: Jenkins
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
437 lines
17 KiB
C++
437 lines
17 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/.
|
|
*/
|
|
|
|
#include <sal/config.h>
|
|
|
|
#include <skia/win/gdiimpl.hxx>
|
|
|
|
#include <win/saldata.hxx>
|
|
#include <vcl/skia/SkiaHelper.hxx>
|
|
#include <skia/utils.hxx>
|
|
#include <skia/zone.hxx>
|
|
#include <skia/win/font.hxx>
|
|
#include <comphelper/scopeguard.hxx>
|
|
#include <comphelper/windowserrorstring.hxx>
|
|
#include <sal/log.hxx>
|
|
|
|
#include <SkBitmap.h>
|
|
#include <SkCanvas.h>
|
|
#include <SkPaint.h>
|
|
#include <SkPixelRef.h>
|
|
#include <SkTypeface_win.h>
|
|
#include <SkFont.h>
|
|
#include <SkFontMgr.h>
|
|
#include <tools/window/win/WindowContextFactory_win.h>
|
|
#include <tools/window/WindowContext.h>
|
|
|
|
#include <windows.h>
|
|
|
|
#include <type_traits>
|
|
|
|
namespace
|
|
{
|
|
sal::systools::COMReference<IDWriteFontCollection>
|
|
getDWritePrivateFontCollection(IDWriteFontFace* fontFace)
|
|
{
|
|
UINT32 numberOfFiles;
|
|
sal::systools::ThrowIfFailed(fontFace->GetFiles(&numberOfFiles, nullptr), SAL_WHERE);
|
|
if (numberOfFiles != 1)
|
|
return {};
|
|
|
|
sal::systools::COMReference<IDWriteFontFile> fontFile;
|
|
sal::systools::ThrowIfFailed(fontFace->GetFiles(&numberOfFiles, &fontFile), SAL_WHERE);
|
|
|
|
static sal::systools::COMReference<IDWriteFactory3> dwriteFactory3 = [] {
|
|
IDWriteFactory* dwriteFactory = WinSalGraphics::getDWriteFactory();
|
|
sal::systools::COMReference<IDWriteFactory3> factory3;
|
|
sal::systools::ThrowIfFailed(dwriteFactory->QueryInterface(&factory3), SAL_WHERE);
|
|
return factory3;
|
|
}();
|
|
|
|
static sal::systools::COMReference<IDWriteFontSetBuilder> dwriteFontSetBuilder = [] {
|
|
sal::systools::COMReference<IDWriteFontSetBuilder> builder;
|
|
sal::systools::ThrowIfFailed(dwriteFactory3->CreateFontSetBuilder(&dwriteFontSetBuilder),
|
|
SAL_WHERE);
|
|
return builder;
|
|
}();
|
|
|
|
BOOL isSupported;
|
|
DWRITE_FONT_FILE_TYPE fileType;
|
|
UINT32 numberOfFonts;
|
|
sal::systools::ThrowIfFailed(
|
|
fontFile->Analyze(&isSupported, &fileType, nullptr, &numberOfFonts), SAL_WHERE);
|
|
if (!isSupported)
|
|
return {};
|
|
|
|
// For each font within the font file, get a font face reference and add to the builder.
|
|
for (UINT32 fontIndex = 0; fontIndex < numberOfFonts; ++fontIndex)
|
|
{
|
|
sal::systools::COMReference<IDWriteFontFaceReference> fontFaceReference;
|
|
if (FAILED(dwriteFactory3->CreateFontFaceReference(
|
|
fontFile, fontIndex, DWRITE_FONT_SIMULATIONS_NONE, &fontFaceReference)))
|
|
continue;
|
|
|
|
// Leave it to DirectWrite to read properties directly out of the font files
|
|
dwriteFontSetBuilder->AddFontFaceReference(fontFaceReference);
|
|
}
|
|
|
|
sal::systools::COMReference<IDWriteFontSet> fontSet;
|
|
sal::systools::ThrowIfFailed(dwriteFontSetBuilder->CreateFontSet(&fontSet), SAL_WHERE);
|
|
|
|
sal::systools::COMReference<IDWriteFontCollection1> fc1;
|
|
sal::systools::ThrowIfFailed(dwriteFactory3->CreateFontCollectionFromFontSet(fontSet, &fc1),
|
|
SAL_WHERE);
|
|
return { fc1.get() };
|
|
}
|
|
}
|
|
|
|
using namespace SkiaHelper;
|
|
|
|
WinSkiaSalGraphicsImpl::WinSkiaSalGraphicsImpl(WinSalGraphics& rGraphics,
|
|
SalGeometryProvider* mpProvider)
|
|
: SkiaSalGraphicsImpl(rGraphics, mpProvider)
|
|
, mWinParent(rGraphics)
|
|
{
|
|
}
|
|
|
|
void WinSkiaSalGraphicsImpl::createWindowSurfaceInternal(bool forceRaster)
|
|
{
|
|
assert(!mWindowContext);
|
|
assert(!mSurface);
|
|
SkiaZone zone;
|
|
skwindow::DisplayParams displayParams;
|
|
assert(GetWidth() > 0 && GetHeight() > 0);
|
|
displayParams.fSurfaceProps = *surfaceProps();
|
|
switch (forceRaster ? RenderRaster : renderMethodToUse())
|
|
{
|
|
case RenderRaster:
|
|
mWindowContext = skwindow::MakeRasterForWin(mWinParent.gethWnd(), displayParams);
|
|
if (mWindowContext)
|
|
mSurface = mWindowContext->getBackbufferSurface();
|
|
break;
|
|
case RenderVulkan:
|
|
mWindowContext = skwindow::MakeVulkanForWin(mWinParent.gethWnd(), displayParams);
|
|
// See flushSurfaceToWindowContext().
|
|
if (mWindowContext)
|
|
mSurface = createSkSurface(GetWidth(), GetHeight());
|
|
break;
|
|
case RenderMetal:
|
|
abort();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void WinSkiaSalGraphicsImpl::Flush() { performFlush(); }
|
|
|
|
bool WinSkiaSalGraphicsImpl::TryRenderCachedNativeControl(ControlCacheKey const& rControlCacheKey,
|
|
int nX, int nY)
|
|
{
|
|
static bool gbCacheEnabled = !getenv("SAL_WITHOUT_WIDGET_CACHE");
|
|
if (!gbCacheEnabled)
|
|
return false;
|
|
|
|
auto& controlsCache = SkiaControlsCache::get();
|
|
SkiaControlCacheType::const_iterator iterator = controlsCache.find(rControlCacheKey);
|
|
if (iterator == controlsCache.end())
|
|
return false;
|
|
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "tryrendercachednativecontrol("
|
|
<< this << "): "
|
|
<< SkIRect::MakeXYWH(nX, nY, iterator->second->width(),
|
|
iterator->second->height()));
|
|
addUpdateRegion(
|
|
SkRect::MakeXYWH(nX, nY, iterator->second->width(), iterator->second->height()));
|
|
mSurface->getCanvas()->drawImage(iterator->second, nX, nY);
|
|
postDraw();
|
|
return true;
|
|
}
|
|
|
|
bool WinSkiaSalGraphicsImpl::RenderAndCacheNativeControl(CompatibleDC& rWhite, CompatibleDC& rBlack,
|
|
int nX, int nY,
|
|
ControlCacheKey& aControlCacheKey)
|
|
{
|
|
assert(dynamic_cast<SkiaCompatibleDC*>(&rWhite));
|
|
assert(dynamic_cast<SkiaCompatibleDC*>(&rBlack));
|
|
|
|
sk_sp<SkImage> image = static_cast<SkiaCompatibleDC&>(rBlack).getAsImageDiff(
|
|
static_cast<SkiaCompatibleDC&>(rWhite));
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace",
|
|
"renderandcachednativecontrol("
|
|
<< this << "): " << SkIRect::MakeXYWH(nX, nY, image->width(), image->height()));
|
|
addUpdateRegion(SkRect::MakeXYWH(nX, nY, image->width(), image->height()));
|
|
mSurface->getCanvas()->drawImage(image, nX, nY);
|
|
postDraw();
|
|
|
|
if (!aControlCacheKey.canCacheControl())
|
|
return true;
|
|
SkiaControlCachePair pair(aControlCacheKey, std::move(image));
|
|
SkiaControlsCache::get().insert(std::move(pair));
|
|
return true;
|
|
}
|
|
|
|
sk_sp<SkTypeface>
|
|
WinSkiaSalGraphicsImpl::createDirectWriteTypeface(const WinFontInstance* pWinFont) try
|
|
{
|
|
using sal::systools::ThrowIfFailed;
|
|
IDWriteFactory* dwriteFactory = WinSalGraphics::getDWriteFactory();
|
|
if (!dwriteDone)
|
|
{
|
|
dwriteFontMgr = SkFontMgr_New_DirectWrite(dwriteFactory);
|
|
dwriteDone = true;
|
|
}
|
|
if (!dwriteFontMgr)
|
|
return nullptr;
|
|
|
|
IDWriteFontFace* fontFace = pWinFont->GetDWFontFace();
|
|
if (!fontFace)
|
|
return nullptr;
|
|
|
|
sal::systools::COMReference<IDWriteFontCollection> collection;
|
|
ThrowIfFailed(dwriteFactory->GetSystemFontCollection(&collection), SAL_WHERE);
|
|
sal::systools::COMReference<IDWriteFont> font;
|
|
// As said above, this fails for our fonts.
|
|
if (FAILED(collection->GetFontFromFontFace(fontFace, &font)))
|
|
{
|
|
// If not found in system collection, try our private font collection.
|
|
// If that's not possible we'll fall back to Skia's GDI-based font rendering.
|
|
if (!dwritePrivateCollection
|
|
|| FAILED(dwritePrivateCollection->GetFontFromFontFace(fontFace, &font)))
|
|
{
|
|
// Our private fonts are installed using AddFontResourceExW( FR_PRIVATE )
|
|
// and that does not make them available to the DWrite system font
|
|
// collection. For such cases attempt to update a collection of
|
|
// private fonts with this newly used font.
|
|
|
|
dwritePrivateCollection = getDWritePrivateFontCollection(fontFace);
|
|
if (!dwritePrivateCollection) // Not one file? Unsupported font?
|
|
return nullptr;
|
|
ThrowIfFailed(dwritePrivateCollection->GetFontFromFontFace(fontFace, &font), SAL_WHERE);
|
|
}
|
|
}
|
|
sal::systools::COMReference<IDWriteFontFamily> fontFamily;
|
|
ThrowIfFailed(font->GetFontFamily(&fontFamily), SAL_WHERE);
|
|
return sk_sp<SkTypeface>(
|
|
SkCreateTypefaceDirectWrite(dwriteFontMgr, fontFace, font.get(), fontFamily.get()));
|
|
}
|
|
catch (const sal::systools::ComError& e)
|
|
{
|
|
SAL_DETAIL_LOG_STREAM(SAL_DETAIL_ENABLE_LOG_INFO, ::SAL_DETAIL_LOG_LEVEL_INFO, "vcl.skia",
|
|
e.what(),
|
|
"HRESULT 0x" << OUString::number(e.GetHresult(), 16) << ": "
|
|
<< WindowsErrorStringFromHRESULT(e.GetHresult()));
|
|
return nullptr;
|
|
}
|
|
|
|
bool WinSkiaSalGraphicsImpl::DrawTextLayout(const GenericSalLayout& rLayout)
|
|
{
|
|
assert(dynamic_cast<SkiaWinFontInstance*>(&rLayout.GetFont()));
|
|
SkiaWinFontInstance& rWinFont = static_cast<SkiaWinFontInstance&>(rLayout.GetFont());
|
|
const vcl::font::FontSelectPattern& rFSD = rWinFont.GetFontSelectPattern();
|
|
if (rFSD.mnHeight == 0)
|
|
return false;
|
|
const HFONT hLayoutFont = rWinFont.GetHFONT();
|
|
LOGFONTW logFont;
|
|
if (GetObjectW(hLayoutFont, sizeof(logFont), &logFont) == 0)
|
|
{
|
|
assert(false);
|
|
return false;
|
|
}
|
|
sk_sp<SkTypeface> typeface = rWinFont.GetSkiaTypeface();
|
|
if (!typeface)
|
|
{
|
|
typeface = createDirectWriteTypeface(&rWinFont);
|
|
bool dwrite = true;
|
|
if (!typeface) // fall back to GDI text rendering
|
|
{
|
|
// If lfWidth is kept, then with hScale != 1 characters get too wide, presumably
|
|
// because the horizontal scaling gets applied twice if GDI is used for drawing (tdf#141715).
|
|
// Using lfWidth /= hScale gives slightly incorrect sizes, for a reason I don't understand.
|
|
// LOGFONT docs say that 0 means GDI will find out the right value on its own somehow,
|
|
// and it apparently works.
|
|
logFont.lfWidth = 0;
|
|
// Reset LOGFONT orientation, the proper orientation is applied by drawGenericLayout(),
|
|
// and keeping this would make it get applied once more when doing the actual GDI drawing.
|
|
// Resetting it here does not seem to cause any problem.
|
|
logFont.lfOrientation = 0;
|
|
logFont.lfEscapement = 0;
|
|
typeface = SkCreateTypefaceFromLOGFONT(logFont);
|
|
dwrite = false;
|
|
if (!typeface)
|
|
return false;
|
|
}
|
|
// Cache the typeface.
|
|
rWinFont.SetSkiaTypeface(typeface, dwrite);
|
|
}
|
|
|
|
SkFont font(typeface);
|
|
|
|
bool bSubpixelPositioning = rLayout.GetSubpixelPositioning();
|
|
SkFont::Edging ePreferredAliasing
|
|
= bSubpixelPositioning ? SkFont::Edging::kSubpixelAntiAlias : fontEdging;
|
|
if (bSubpixelPositioning)
|
|
{
|
|
// note that SkFont defaults to a BaselineSnap of true, so I think really only
|
|
// subpixel in text direction
|
|
font.setSubpixel(true);
|
|
}
|
|
font.setEdging(logFont.lfQuality == NONANTIALIASED_QUALITY ? SkFont::Edging::kAlias
|
|
: ePreferredAliasing);
|
|
|
|
double nHeight = rFSD.mnHeight;
|
|
double nWidth = rFSD.mnWidth ? rFSD.mnWidth * rWinFont.GetAverageWidthFactor() : nHeight;
|
|
font.setSize(nHeight);
|
|
font.setScaleX(nWidth / nHeight);
|
|
|
|
SkFont verticalFont(font);
|
|
verticalFont.setSize(nWidth);
|
|
verticalFont.setScaleX(nHeight / nWidth);
|
|
|
|
assert(dynamic_cast<SkiaSalGraphicsImpl*>(mWinParent.GetImpl()));
|
|
SkiaSalGraphicsImpl* impl = static_cast<SkiaSalGraphicsImpl*>(mWinParent.GetImpl());
|
|
COLORREF color = ::GetTextColor(mWinParent.getHDC());
|
|
Color salColor(GetRValue(color), GetGValue(color), GetBValue(color));
|
|
impl->drawGenericLayout(rLayout, salColor, font, verticalFont);
|
|
return true;
|
|
}
|
|
|
|
SkFont::Edging WinSkiaSalGraphicsImpl::fontEdging;
|
|
|
|
void WinSkiaSalGraphicsImpl::initFontInfo()
|
|
{
|
|
// Skia needs to be explicitly told what kind of antialiasing should be used,
|
|
// get it from system settings. This does not actually matter for the text
|
|
// rendering itself, since Skia has been patched to simply use the setting
|
|
// from the LOGFONT, which gets set by VCL's ImplGetLogFontFromFontSelect()
|
|
// and that one normally uses DEFAULT_QUALITY, so Windows will select
|
|
// the appropriate AA setting. But Skia internally chooses the format to which
|
|
// the glyphs will be rendered based on this setting (subpixel AA requires colors,
|
|
// others do not).
|
|
fontEdging = SkFont::Edging::kAlias;
|
|
SkPixelGeometry pixelGeometry = kUnknown_SkPixelGeometry;
|
|
BOOL set;
|
|
if (SystemParametersInfo(SPI_GETFONTSMOOTHING, 0, &set, 0) && set)
|
|
{
|
|
UINT set2;
|
|
if (SystemParametersInfo(SPI_GETFONTSMOOTHINGTYPE, 0, &set2, 0)
|
|
&& set2 == FE_FONTSMOOTHINGCLEARTYPE)
|
|
{
|
|
fontEdging = SkFont::Edging::kSubpixelAntiAlias;
|
|
if (SystemParametersInfo(SPI_GETFONTSMOOTHINGORIENTATION, 0, &set2, 0)
|
|
&& set2 == FE_FONTSMOOTHINGORIENTATIONBGR)
|
|
// No idea how to tell if it's horizontal or vertical.
|
|
pixelGeometry = kBGR_H_SkPixelGeometry;
|
|
else
|
|
pixelGeometry = kRGB_H_SkPixelGeometry; // default
|
|
}
|
|
else
|
|
fontEdging = SkFont::Edging::kAntiAlias;
|
|
}
|
|
setPixelGeometry(pixelGeometry);
|
|
}
|
|
|
|
void WinSkiaSalGraphicsImpl::ClearDevFontCache()
|
|
{
|
|
dwriteFontMgr.reset();
|
|
dwritePrivateCollection.clear();
|
|
dwriteDone = false;
|
|
initFontInfo(); // get font info again, just in case
|
|
}
|
|
|
|
SkiaCompatibleDC::SkiaCompatibleDC(SalGraphics& rGraphics, int x, int y, int width, int height)
|
|
: CompatibleDC(rGraphics, x, y, width, height, false)
|
|
{
|
|
}
|
|
|
|
sk_sp<SkImage> SkiaCompatibleDC::getAsImageDiff(const SkiaCompatibleDC& white) const
|
|
{
|
|
SkiaZone zone;
|
|
assert(maRects.mnSrcWidth == white.maRects.mnSrcWidth
|
|
|| maRects.mnSrcHeight == white.maRects.mnSrcHeight);
|
|
SkBitmap tmpBitmap;
|
|
if (!tmpBitmap.tryAllocPixels(SkImageInfo::Make(maRects.mnSrcWidth, maRects.mnSrcHeight,
|
|
kBGRA_8888_SkColorType, kPremul_SkAlphaType),
|
|
maRects.mnSrcWidth * 4))
|
|
abort();
|
|
// Native widgets are drawn twice on black/white background to synthetize alpha
|
|
// (commit c6b66646870cb2bffaa73565affcf80bf74e0b5c). The problem is that
|
|
// most widgets when drawn on transparent background are drawn properly (and the result
|
|
// is in premultiplied alpha format), some such as "Edit" (used by ControlType::Editbox)
|
|
// keep the alpha channel as transparent. Therefore the alpha is actually computed
|
|
// from the difference in the premultiplied red channels when drawn one black and on white.
|
|
// Alpha is computed as "alpha = 1.0 - abs(black.red - white.red)".
|
|
// I doubt this can be done using Skia, so do it manually here. Fortunately
|
|
// the bitmaps should be fairly small and are cached.
|
|
uint32_t* dest = tmpBitmap.getAddr32(0, 0);
|
|
assert(dest == tmpBitmap.getPixels());
|
|
const sal_uInt32* src = mpData;
|
|
const sal_uInt32* whiteSrc = white.mpData;
|
|
uint32_t* end = dest + tmpBitmap.width() * tmpBitmap.height();
|
|
while (dest < end)
|
|
{
|
|
uint32_t alpha = 255 - abs(int(*src & 0xff) - int(*whiteSrc & 0xff));
|
|
*dest = (*src & 0x00ffffff) | (alpha << 24);
|
|
++dest;
|
|
++src;
|
|
++whiteSrc;
|
|
}
|
|
tmpBitmap.notifyPixelsChanged();
|
|
tmpBitmap.setImmutable();
|
|
sk_sp<SkSurface> surface = createSkSurface(tmpBitmap.width(), tmpBitmap.height());
|
|
SkPaint paint;
|
|
paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
|
|
SkCanvas* canvas = surface->getCanvas();
|
|
canvas->save();
|
|
// The data we got is upside-down.
|
|
SkMatrix matrix;
|
|
matrix.preTranslate(0, tmpBitmap.height());
|
|
matrix.setConcat(matrix, SkMatrix::Scale(1, -1));
|
|
canvas->concat(matrix);
|
|
canvas->drawImage(tmpBitmap.asImage(), 0, 0, SkSamplingOptions(), &paint);
|
|
canvas->restore();
|
|
return makeCheckedImageSnapshot(surface);
|
|
}
|
|
|
|
SkiaControlsCache::SkiaControlsCache()
|
|
: cache(200)
|
|
{
|
|
}
|
|
|
|
SkiaControlCacheType& SkiaControlsCache::get()
|
|
{
|
|
SalData* data = GetSalData();
|
|
if (!data->m_pSkiaControlsCache)
|
|
data->m_pSkiaControlsCache.reset(new SkiaControlsCache);
|
|
return data->m_pSkiaControlsCache->cache;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
std::unique_ptr<skwindow::WindowContext> createVulkanWindowContext(bool /*temporary*/)
|
|
{
|
|
SkiaZone zone;
|
|
skwindow::DisplayParams displayParams;
|
|
return skwindow::MakeVulkanForWin(nullptr, displayParams);
|
|
}
|
|
}
|
|
|
|
void WinSkiaSalGraphicsImpl::prepareSkia()
|
|
{
|
|
initFontInfo();
|
|
SkiaHelper::prepareSkia(createVulkanWindowContext);
|
|
}
|
|
|
|
void WinSkiaSalGraphicsImpl::ClearNativeControlCache()
|
|
{
|
|
SalData* data = GetSalData();
|
|
data->m_pSkiaControlsCache.reset();
|
|
}
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|