diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
commit | 940b4d1848e8c70ab7642901a68594e8016caffc (patch) | |
tree | eb72f344ee6c3d9b80a7ecc079ea79e9fba8676d /vcl/win/gdi/winlayout.cxx | |
parent | Initial commit. (diff) | |
download | libreoffice-940b4d1848e8c70ab7642901a68594e8016caffc.tar.xz libreoffice-940b4d1848e8c70ab7642901a68594e8016caffc.zip |
Adding upstream version 1:7.0.4.upstream/1%7.0.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vcl/win/gdi/winlayout.cxx')
-rw-r--r-- | vcl/win/gdi/winlayout.cxx | 635 |
1 files changed, 635 insertions, 0 deletions
diff --git a/vcl/win/gdi/winlayout.cxx b/vcl/win/gdi/winlayout.cxx new file mode 100644 index 000000000..056540921 --- /dev/null +++ b/vcl/win/gdi/winlayout.cxx @@ -0,0 +1,635 @@ + +/* -*- 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_features.h> + +#include <memory> +#include <osl/module.h> +#include <osl/file.h> +#include <sal/log.hxx> + +#include <comphelper/windowserrorstring.hxx> +#include <comphelper/scopeguard.hxx> + +#include <opengl/win/gdiimpl.hxx> +#include <opengl/win/winlayout.hxx> + +#include <vcl/opengl/OpenGLHelper.hxx> +#include <win/salgdi.h> +#include <win/saldata.hxx> +#include <win/wingdiimpl.hxx> +#include <outdev.h> + +#include <win/DWriteTextRenderer.hxx> +#include <win/scoped_gdi.hxx> + +#include <sft.hxx> +#include <sallayout.hxx> + +#include <cstdio> +#include <cstdlib> + +#include <rtl/character.hxx> + +#include <boost/functional/hash.hpp> +#include <algorithm> + +#include <shlwapi.h> +#include <winver.h> + +GlobalWinGlyphCache * GlobalWinGlyphCache::get() +{ + SalData *data = GetSalData(); + if (!data->m_pGlobalWinGlyphCache) + { + if (OpenGLHelper::isVCLOpenGLEnabled()) + data->m_pGlobalWinGlyphCache.reset(new OpenGLGlobalWinGlyphCache); + } + return data->m_pGlobalWinGlyphCache.get(); +} + +bool WinFontInstance::CacheGlyphToAtlas(HDC hDC, HFONT hFont, int nGlyphIndex, + SalGraphics& rGraphics, const GenericSalLayout& rLayout) +{ + WinGlyphDrawElement aElement; + + ScopedHDC aHDC(CreateCompatibleDC(hDC)); + + if (!aHDC) + { + SAL_WARN("vcl.gdi", "CreateCompatibleDC failed: " << WindowsErrorString(GetLastError())); + return false; + } + + const HFONT hOrigFont = static_cast<HFONT>(SelectObject(aHDC.get(), hFont)); + if (hOrigFont == nullptr) + { + SAL_WARN("vcl.gdi", "SelectObject failed: " << WindowsErrorString(GetLastError())); + return false; + } + const ::comphelper::ScopeGuard aHFONTrestoreScopeGuard( + [&aHDC,hOrigFont]() { SelectFont(aHDC.get(), hOrigFont); }); + + // For now we assume DWrite is present and we won't bother with fallback paths. + D2DWriteTextOutRenderer * pTxt = dynamic_cast<D2DWriteTextOutRenderer *>(&TextOutRenderer::get(true)); + if (!pTxt) + return false; + + pTxt->changeTextAntiAliasMode(D2DTextAntiAliasMode::AntiAliased); + + if (!pTxt->BindFont(aHDC.get())) + { + SAL_WARN("vcl.gdi", "Binding of font failed. The font might not be supported by DirectWrite."); + return false; + } + const ::comphelper::ScopeGuard aFontReleaseScopeGuard([&pTxt]() { pTxt->ReleaseFont(); }); + + std::vector<WORD> aGlyphIndices(1); + aGlyphIndices[0] = nGlyphIndex; + // Fetch the ink boxes and calculate the size of the atlas. + tools::Rectangle bounds(0, 0, 0, 0); + auto aInkBoxes = pTxt->GetGlyphInkBoxes(aGlyphIndices.data(), aGlyphIndices.data() + 1); + if (aInkBoxes.empty()) + return false; + + for (auto &box : aInkBoxes) + bounds.Union(box + Point(bounds.Right(), 0)); + + // bounds.Top() is the offset from the baseline at (0,0) to the top of the + // inkbox. + aElement.mnBaselineOffset = -bounds.Top(); + aElement.mnHeight = bounds.getHeight(); + aElement.mbVertical = false; + + // Try hard to avoid overlap as we want to be able to use + // individual rectangles for each glyph. The ABC widths don't + // take anti-aliasing into consideration. Let's hope that leaving + // "extra" space between glyphs will help. + std::vector<float> aGlyphAdv(1); // offsets between glyphs + std::vector<DWRITE_GLYPH_OFFSET> aGlyphOffset(1, {0.0f, 0.0f}); + std::vector<int> aEnds(1); // end of each glyph box + float fHScale = getHScale(); + float totWidth = 0; + { + int overhang = aInkBoxes[0].Left(); + int blackWidth = aInkBoxes[0].getWidth() * fHScale; // width of non-AA pixels + aElement.maLeftOverhangs = overhang; + + aGlyphAdv[0] = blackWidth + aElement.getExtraSpace(); + aGlyphOffset[0].advanceOffset = -overhang; + + totWidth += aGlyphAdv[0]; + aEnds[0] = totWidth; + } + // Leave extra space also at top and bottom + int nBitmapWidth = totWidth; + int nBitmapHeight = bounds.getHeight() + aElement.getExtraSpace(); + + UINT nPos = 0; + + aElement.maLocation.SetLeft(nPos); + aElement.maLocation.SetRight(aEnds[0]); + aElement.maLocation.SetTop(0); + aElement.maLocation.SetBottom(bounds.getHeight() + aElement.getExtraSpace()); + nPos = aEnds[0]; + + std::unique_ptr<CompatibleDC> aDC(CompatibleDC::create(rGraphics, 0, 0, nBitmapWidth, nBitmapHeight)); + + SetTextColor(aDC->getCompatibleHDC(), RGB(0, 0, 0)); + SetBkColor(aDC->getCompatibleHDC(), RGB(255, 255, 255)); + + aDC->fill(RGB(0xff, 0xff, 0xff)); + + pTxt->BindDC(aDC->getCompatibleHDC(), tools::Rectangle(0, 0, nBitmapWidth, nBitmapHeight)); + auto pRT = pTxt->GetRenderTarget(); + + ID2D1SolidColorBrush* pBrush = nullptr; + if (!SUCCEEDED(pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBrush))) + return false; + + D2D1_POINT_2F baseline = { + static_cast<FLOAT>(aElement.getExtraOffset()), + static_cast<FLOAT>(aElement.getExtraOffset() + aElement.mnBaselineOffset) + }; + + DWRITE_GLYPH_RUN glyphs = { + pTxt->GetFontFace(), + pTxt->GetEmHeight(), + 1, + aGlyphIndices.data(), + aGlyphAdv.data(), + aGlyphOffset.data(), + false, + 0 + }; + + WinFontTransformGuard aTransformGuard(pRT, fHScale, rLayout, baseline); + pRT->BeginDraw(); + pRT->DrawGlyphRun(baseline, &glyphs, pBrush); + HRESULT hResult = pRT->EndDraw(); + + pBrush->Release(); + + switch (hResult) + { + case S_OK: + break; + case D2DERR_RECREATE_TARGET: + pTxt->CreateRenderTarget(); + break; + default: + SAL_WARN("vcl.gdi", "DrawGlyphRun-EndDraw failed: " << WindowsErrorString(GetLastError())); + return false; + } + + if (!GlobalWinGlyphCache::get()->AllocateTexture(aElement, aDC.get())) + return false; + + maWinGlyphCache.PutDrawElementInCache(std::move(aElement), nGlyphIndex); + + return true; +} + +TextOutRenderer & TextOutRenderer::get(bool bUseDWrite) +{ + SalData *const pSalData = GetSalData(); + + if (!pSalData) + { // don't call this after DeInitVCL() + fprintf(stderr, "TextOutRenderer fatal error: no SalData"); + abort(); + } + + if (bUseDWrite) + { + if (!pSalData->m_pD2DWriteTextOutRenderer) + { + pSalData->m_pD2DWriteTextOutRenderer.reset(new D2DWriteTextOutRenderer()); + } + return *pSalData->m_pD2DWriteTextOutRenderer; + } + if (!pSalData->m_pExTextOutRenderer) + { + pSalData->m_pExTextOutRenderer.reset(new ExTextOutRenderer); + } + return *pSalData->m_pExTextOutRenderer; +} + + +bool ExTextOutRenderer::operator ()(GenericSalLayout const &rLayout, + SalGraphics & /*rGraphics*/, + HDC hDC) +{ + HFONT hFont = static_cast<HFONT>(GetCurrentObject( hDC, OBJ_FONT )); + ScopedHFONT hAltFont; + bool bUseAltFont = false; + bool bShift = false; + if (rLayout.GetFont().GetFontSelectPattern().mbVertical) + { + LOGFONTW aLogFont; + GetObjectW(hFont, sizeof(aLogFont), &aLogFont); + if (aLogFont.lfFaceName[0] == '@') + { + memmove(&aLogFont.lfFaceName[0], &aLogFont.lfFaceName[1], + sizeof(aLogFont.lfFaceName)-sizeof(aLogFont.lfFaceName[0])); + hAltFont.reset(CreateFontIndirectW(&aLogFont)); + } + else + { + bShift = true; + aLogFont.lfEscapement += 2700; + aLogFont.lfOrientation = aLogFont.lfEscapement; + hAltFont.reset(CreateFontIndirectW(&aLogFont)); + } + } + + UINT nTextAlign = GetTextAlign ( hDC ); + int nStart = 0; + Point aPos(0, 0); + const GlyphItem* pGlyph; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nStart)) + { + WORD glyphWStr[] = { pGlyph->glyphId() }; + if (hAltFont && pGlyph->IsVertical() == bUseAltFont) + { + bUseAltFont = !bUseAltFont; + SelectFont(hDC, bUseAltFont ? hAltFont.get() : hFont); + } + if (bShift && pGlyph->IsVertical()) + SetTextAlign(hDC, TA_TOP|TA_LEFT); + + ExtTextOutW(hDC, aPos.X(), aPos.Y(), ETO_GLYPH_INDEX, nullptr, LPCWSTR(&glyphWStr), 1, nullptr); + + if (bShift && pGlyph->IsVertical()) + SetTextAlign(hDC, nTextAlign); + } + if (hAltFont) + { + if (bUseAltFont) + SelectFont(hDC, hFont); + } + + return true; +} + +std::unique_ptr<GenericSalLayout> WinSalGraphics::GetTextLayout(int nFallbackLevel) +{ + assert(mpWinFontEntry[nFallbackLevel]); + if (!mpWinFontEntry[nFallbackLevel]) + return nullptr; + + assert(mpWinFontEntry[nFallbackLevel]->GetFontFace()); + + mpWinFontEntry[nFallbackLevel]->SetGraphics(this); + return std::make_unique<GenericSalLayout>(*mpWinFontEntry[nFallbackLevel]); +} + +WinFontInstance::WinFontInstance(const WinFontFace& rPFF, const FontSelectPattern& rFSP) + : LogicalFontInstance(rPFF, rFSP) + , m_pGraphics(nullptr) + , m_hFont(nullptr) + , m_fScale(1.0f) +{ +} + +WinFontInstance::~WinFontInstance() +{ + if (m_hFont) + ::DeleteFont(m_hFont); +} + +bool WinFontInstance::hasHScale() const +{ + const FontSelectPattern &rPattern = GetFontSelectPattern(); + int nHeight(rPattern.mnHeight); + int nWidth(rPattern.mnWidth ? rPattern.mnWidth * GetAverageWidthFactor() : nHeight); + return nWidth != nHeight; +} + +float WinFontInstance::getHScale() const +{ + const FontSelectPattern& rPattern = GetFontSelectPattern(); + int nHeight(rPattern.mnHeight); + if (!nHeight) + return 1.0; + float nWidth(rPattern.mnWidth ? rPattern.mnWidth * GetAverageWidthFactor() : nHeight); + return nWidth / nHeight; +} + +namespace { + +struct BlobReference +{ + hb_blob_t* mpBlob; + BlobReference(hb_blob_t* pBlob) : mpBlob(pBlob) + { + hb_blob_reference(mpBlob); + } + BlobReference(BlobReference const & other) + : mpBlob(other.mpBlob) + { + hb_blob_reference(mpBlob); + } + ~BlobReference() { hb_blob_destroy(mpBlob); } +}; + +} + +using BlobCacheKey = std::pair<rtl::Reference<PhysicalFontFace>, hb_tag_t>; + +namespace { + +struct BlobCacheKeyHash +{ + std::size_t operator()(BlobCacheKey const& rKey) const + { + std::size_t seed = 0; + boost::hash_combine(seed, rKey.first.get()); + boost::hash_combine(seed, rKey.second); + return seed; + } +}; + +} + +static hb_blob_t* getFontTable(hb_face_t* /*face*/, hb_tag_t nTableTag, void* pUserData) +{ + static o3tl::lru_map<BlobCacheKey, BlobReference, BlobCacheKeyHash> gCache(50); + + WinFontInstance* pFont = static_cast<WinFontInstance*>(pUserData); + HDC hDC = pFont->GetGraphics()->getHDC(); + HFONT hFont = pFont->GetHFONT(); + assert(hDC); + assert(hFont); + + BlobCacheKey cacheKey { rtl::Reference<PhysicalFontFace>(pFont->GetFontFace()), nTableTag }; + auto it = gCache.find(cacheKey); + if (it != gCache.end()) + { + hb_blob_reference(it->second.mpBlob); + return it->second.mpBlob; + } + + sal_uLong nLength = 0; + unsigned char* pBuffer = nullptr; + + HGDIOBJ hOrigFont = SelectObject(hDC, hFont); + nLength = ::GetFontData(hDC, OSL_NETDWORD(nTableTag), 0, nullptr, 0); + if (nLength > 0 && nLength != GDI_ERROR) + { + pBuffer = new unsigned char[nLength]; + ::GetFontData(hDC, OSL_NETDWORD(nTableTag), 0, pBuffer, nLength); + } + SelectObject(hDC, hOrigFont); + + if (!pBuffer) + return nullptr; + + hb_blob_t* pBlob = hb_blob_create(reinterpret_cast<const char*>(pBuffer), nLength, HB_MEMORY_MODE_READONLY, + pBuffer, [](void* data){ delete[] static_cast<unsigned char*>(data); }); + if (!pBlob) + return pBlob; + gCache.insert({cacheKey, BlobReference(pBlob)}); + return pBlob; +} + +hb_font_t* WinFontInstance::ImplInitHbFont() +{ + assert(m_pGraphics); + hb_font_t* pHbFont = InitHbFont(hb_face_create_for_tables(getFontTable, this, nullptr)); + + // Calculate the AverageWidthFactor, see LogicalFontInstance::GetScale(). + if (GetFontSelectPattern().mnWidth) + { + double nUPEM = hb_face_get_upem(hb_font_get_face(pHbFont)); + + LOGFONTW aLogFont; + GetObjectW(m_hFont, sizeof(LOGFONTW), &aLogFont); + + // Set the height (font size) to EM to minimize rounding errors. + aLogFont.lfHeight = -nUPEM; + // Set width to the default to get the original value in the metrics. + aLogFont.lfWidth = 0; + + TEXTMETRICW aFontMetric; + { + // Get the font metrics. + HDC hDC = m_pGraphics->getHDC(); + ScopedSelectedHFONT hFont(hDC, CreateFontIndirectW(&aLogFont)); + GetTextMetricsW(hDC, &aFontMetric); + } + + SetAverageWidthFactor(nUPEM / aFontMetric.tmAveCharWidth); + } + + return pHbFont; +} + +void WinFontInstance::SetGraphics(WinSalGraphics *pGraphics) +{ + m_pGraphics = pGraphics; + if (m_hFont) + return; + HFONT hOrigFont; + m_hFont = m_pGraphics->ImplDoSetFont(GetFontSelectPattern(), GetFontFace(), hOrigFont); + SelectObject(m_pGraphics->getHDC(), hOrigFont); +} + +bool WinSalGraphics::CacheGlyphs(const GenericSalLayout& rLayout) +{ + static bool bDoGlyphCaching = (std::getenv("SAL_DISABLE_GLYPH_CACHING") == nullptr); + if (!bDoGlyphCaching) + return false; + + if (rLayout.GetOrientation()) + // Our caching is incomplete, skip it for non-horizontal text. + return false; + + HDC hDC = getHDC(); + WinFontInstance& rFont = *static_cast<WinFontInstance*>(&rLayout.GetFont()); + HFONT hFONT = rFont.GetHFONT(); + + int nStart = 0; + Point aPos(0, 0); + const GlyphItem* pGlyph; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nStart)) + { + if (!rFont.GetWinGlyphCache().IsGlyphCached(pGlyph->glyphId())) + { + if (!rFont.CacheGlyphToAtlas(hDC, hFONT, pGlyph->glyphId(), *this, rLayout)) + return false; + } + } + + return true; +} + +bool WinSalGraphics::DrawCachedGlyphs(const GenericSalLayout& rLayout) +{ + HDC hDC = getHDC(); + + tools::Rectangle aRect; + rLayout.GetBoundRect(aRect); + + COLORREF color = GetTextColor(hDC); + Color salColor(GetRValue(color), GetGValue(color), GetBValue(color)); + + WinSalGraphicsImplBase *pImpl = dynamic_cast<WinSalGraphicsImplBase*>(mpImpl.get()); + if (!pImpl->UseTextDraw()) + return false; + + WinFontInstance& rFont = *static_cast<WinFontInstance*>(&rLayout.GetFont()); + + int nStart = 0; + Point aPos(0, 0); + const GlyphItem* pGlyph; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nStart)) + { + WinGlyphDrawElement& rElement(rFont.GetWinGlyphCache().GetDrawElement(pGlyph->glyphId())); + const CompatibleDC::Texture* texture = rElement.maTexture.get(); + + if (!texture || !texture->isValid()) + return false; + + SalTwoRect a2Rects(0, 0, + texture->GetWidth(), texture->GetHeight(), + aPos.X() - rElement.getExtraOffset() + rElement.maLeftOverhangs, + aPos.Y() - rElement.mnBaselineOffset - rElement.getExtraOffset(), + texture->GetWidth(), texture->GetHeight()); + + pImpl->DeferredTextDraw(texture, salColor, a2Rects); + } + + return true; +} + +static void PruneGlyphCache() +{ + GlobalWinGlyphCache::get()->Prune(); +} + +void WinSalGraphics::DrawTextLayout(const GenericSalLayout& rLayout, HDC hDC, bool bUseDWrite) +{ + TextOutRenderer &render = TextOutRenderer::get(bUseDWrite); + render(rLayout, *this, hDC); +} + +void WinSalGraphics::DrawTextLayout(const GenericSalLayout& rLayout) +{ + WinSalGraphicsImplBase* pImpl = dynamic_cast<WinSalGraphicsImplBase*>(mpImpl.get()); + if( !mbPrinter && pImpl->DrawTextLayout(rLayout)) + return; // handled by pImpl + + HDC hDC = getHDC(); + const WinFontInstance* pWinFont = static_cast<const WinFontInstance*>(&rLayout.GetFont()); + const HFONT hLayoutFont = pWinFont->GetHFONT(); + bool bUseClassic = !pImpl->UseTextDraw() || mbPrinter; + + // Our DirectWrite renderer is incomplete, skip it for vertical text where glyphs are not + // rotated. + bool bForceGDI = rLayout.GetFont().GetFontSelectPattern().mbVertical; + + if (bUseClassic) + { + // no OpenGL, just classic rendering + const HFONT hOrigFont = ::SelectFont(hDC, hLayoutFont); + DrawTextLayout(rLayout, hDC, false); + ::SelectFont(hDC, hOrigFont); + } + // if we can't draw the cached OpenGL glyphs, try to draw a full OpenGL layout + else if (!bForceGDI && CacheGlyphs(rLayout) && DrawCachedGlyphs(rLayout)) + { + PruneGlyphCache(); + } + else + { + PruneGlyphCache(); // prune the cache from the failed calls above + + // We have to render the text to a hidden texture, and draw it. + // + // Note that Windows GDI does not really support the alpha correctly + // when drawing - ie. it draws nothing to the alpha channel when + // rendering the text, even the antialiasing is done as 'real' pixels, + // not alpha... + // + // Luckily, this does not really limit us: + // + // To blend properly, we draw the texture, but then use it as an alpha + // channel for solid color (that will define the text color). This + // destroys the subpixel antialiasing - turns it into 'classic' + // antialiasing - but that is the best we can do, because the subpixel + // antialiasing needs to know what is in the background: When the + // background is white, or white-ish, it does the subpixel, but when + // there is a color, it just darkens the color (and does this even + // when part of the character is on a colored background, and part on + // white). It has to work this way, the results would look strange + // otherwise. + // + // For the GL rendering to work even with the subpixel antialiasing, + // we would need to get the current texture from the screen, let GDI + // draw the text to it (so that it can decide well where to use the + // subpixel and where not), and draw the result - but in that case we + // don't need alpha anyway. + // + // TODO: check the performance of this 2nd approach at some stage and + // switch to that if it performs well. + + tools::Rectangle aRect; + rLayout.GetBoundRect(aRect); + if( aRect.IsEmpty()) + return; + + pImpl->PreDrawText(); + + std::unique_ptr<CompatibleDC> aDC(CompatibleDC::create(*this, aRect.Left(), aRect.Top(), aRect.GetWidth(), aRect.GetHeight())); + + // we are making changes to the DC, make sure we got a new one + assert(aDC->getCompatibleHDC() != hDC); + + RECT aWinRect = { aRect.Left(), aRect.Top(), aRect.Left() + aRect.GetWidth(), aRect.Top() + aRect.GetHeight() }; + ::FillRect(aDC->getCompatibleHDC(), &aWinRect, static_cast<HBRUSH>(::GetStockObject(WHITE_BRUSH))); + + // setup the hidden DC with black color and white background, we will + // use the result of the text drawing later as a mask only + const HFONT hOrigFont = ::SelectFont(aDC->getCompatibleHDC(), hLayoutFont); + + ::SetTextColor(aDC->getCompatibleHDC(), RGB(0, 0, 0)); + ::SetBkColor(aDC->getCompatibleHDC(), RGB(255, 255, 255)); + + UINT nTextAlign = ::GetTextAlign(hDC); + ::SetTextAlign(aDC->getCompatibleHDC(), nTextAlign); + + COLORREF color = ::GetTextColor(hDC); + Color salColor(GetRValue(color), GetGValue(color), GetBValue(color)); + + // the actual drawing + DrawTextLayout(rLayout, aDC->getCompatibleHDC(), !bForceGDI); + + std::unique_ptr<CompatibleDC::Texture> xTexture(aDC->getAsMaskTexture()); + if (xTexture) + pImpl->DrawTextMask(xTexture.get(), salColor, aDC->getTwoRect()); + + ::SelectFont(aDC->getCompatibleHDC(), hOrigFont); + + pImpl->PostDrawText(); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |