/* -*- 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 #if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 12, 0) #error "require at least cairo 1.12.0" #endif void dl_cairo_surface_set_device_scale(cairo_surface_t* surface, double x_scale, double y_scale) { #if !HAVE_DLAPI cairo_surface_set_device_scale(surface, x_scale, y_scale); #else static auto func = reinterpret_cast( osl_getAsciiFunctionSymbol(nullptr, "cairo_surface_set_device_scale")); if (func) func(surface, x_scale, y_scale); #endif } void dl_cairo_surface_get_device_scale(cairo_surface_t* surface, double* x_scale, double* y_scale) { #if !HAVE_DLAPI cairo_surface_get_device_scale(surface, x_scale, y_scale); #else static auto func = reinterpret_cast( osl_getAsciiFunctionSymbol(nullptr, "cairo_surface_get_device_scale")); if (func) func(surface, x_scale, y_scale); else { if (x_scale) *x_scale = 1.0; if (y_scale) *y_scale = 1.0; } #endif } basegfx::B2DRange getFillDamage(cairo_t* cr) { double x1, y1, x2, y2; // this is faster than cairo_fill_extents, at the cost of some overdraw cairo_path_extents(cr, &x1, &y1, &x2, &y2); // support B2DRange::isEmpty() if (0.0 != x1 || 0.0 != y1 || 0.0 != x2 || 0.0 != y2) { return basegfx::B2DRange(x1, y1, x2, y2); } return basegfx::B2DRange(); } basegfx::B2DRange getClipBox(cairo_t* cr) { double x1, y1, x2, y2; cairo_clip_extents(cr, &x1, &y1, &x2, &y2); // support B2DRange::isEmpty() if (0.0 != x1 || 0.0 != y1 || 0.0 != x2 || 0.0 != y2) { return basegfx::B2DRange(x1, y1, x2, y2); } return basegfx::B2DRange(); } basegfx::B2DRange getClippedFillDamage(cairo_t* cr) { basegfx::B2DRange aDamageRect(getFillDamage(cr)); aDamageRect.intersect(getClipBox(cr)); return aDamageRect; } basegfx::B2DRange getStrokeDamage(cairo_t* cr) { double x1, y1, x2, y2; // less accurate, but much faster cairo_path_extents(cr, &x1, &y1, &x2, &y2); // support B2DRange::isEmpty() if (0.0 != x1 || 0.0 != y1 || 0.0 != x2 || 0.0 != y2) { return basegfx::B2DRange(x1, y1, x2, y2); } return basegfx::B2DRange(); } basegfx::B2DRange getClippedStrokeDamage(cairo_t* cr) { basegfx::B2DRange aDamageRect(getStrokeDamage(cr)); aDamageRect.intersect(getClipBox(cr)); return aDamageRect; } // Remove bClosePath: Checked that the already used mechanism for Win using // Gdiplus already relies on rPolygon.isClosed(), so should be safe to replace // this. // For PixelSnap we need the ObjectToDevice transformation here now. This is a // special case relative to the also executed LineDraw-Offset of (0.5, 0.5) in // DeviceCoordinates: The LineDraw-Offset is applied *after* the snap, so we // need the ObjectToDevice transformation *without* that offset here to do the // same. The LineDraw-Offset will be applied by the callers using a linear // transformation for Cairo now // For support of PixelSnapHairline we also need the ObjectToDevice transformation // and a method (same as in gdiimpl.cxx for Win and Gdiplus). This is needed e.g. // for Chart-content visualization. CAUTION: It's not the same as PixelSnap (!) // tdf#129845 add reply value to allow counting a point/byte/size measurement to // be included static size_t AddPolygonToPath(cairo_t* cr, const basegfx::B2DPolygon& rPolygon, const basegfx::B2DHomMatrix& rObjectToDevice, bool bPixelSnap, bool bPixelSnapHairline) { // short circuit if there is nothing to do const sal_uInt32 nPointCount(rPolygon.count()); size_t nSizeMeasure(0); if (0 == nPointCount) { return nSizeMeasure; } const bool bHasCurves(rPolygon.areControlPointsUsed()); const bool bClosePath(rPolygon.isClosed()); const bool bObjectToDeviceUsed(!rObjectToDevice.isIdentity()); basegfx::B2DHomMatrix aObjectToDeviceInv; basegfx::B2DPoint aLast; PixelSnapper aSnapper; for (sal_uInt32 nPointIdx = 0, nPrevIdx = 0;; nPrevIdx = nPointIdx++) { int nClosedIdx = nPointIdx; if (nPointIdx >= nPointCount) { // prepare to close last curve segment if needed if (bClosePath && (nPointIdx == nPointCount)) { nClosedIdx = 0; } else { break; } } basegfx::B2DPoint aPoint(rPolygon.getB2DPoint(nClosedIdx)); if (bPixelSnap) { // snap device coordinates to full pixels if (bObjectToDeviceUsed) { // go to DeviceCoordinates aPoint *= rObjectToDevice; } // snap by rounding aPoint.setX(basegfx::fround(aPoint.getX())); aPoint.setY(basegfx::fround(aPoint.getY())); if (bObjectToDeviceUsed) { if (aObjectToDeviceInv.isIdentity()) { aObjectToDeviceInv = rObjectToDevice; aObjectToDeviceInv.invert(); } // go back to ObjectCoordinates aPoint *= aObjectToDeviceInv; } } if (bPixelSnapHairline) { // snap horizontal and vertical lines (mainly used in Chart for // 'nicer' AAing) aPoint = aSnapper.snap(rPolygon, rObjectToDevice, aObjectToDeviceInv, nClosedIdx); } if (!nPointIdx) { // first point => just move there cairo_move_to(cr, aPoint.getX(), aPoint.getY()); aLast = aPoint; continue; } bool bPendingCurve(false); if (bHasCurves) { bPendingCurve = rPolygon.isNextControlPointUsed(nPrevIdx); bPendingCurve |= rPolygon.isPrevControlPointUsed(nClosedIdx); } if (!bPendingCurve) // line segment { cairo_line_to(cr, aPoint.getX(), aPoint.getY()); nSizeMeasure++; } else // cubic bezier segment { basegfx::B2DPoint aCP1 = rPolygon.getNextControlPoint(nPrevIdx); basegfx::B2DPoint aCP2 = rPolygon.getPrevControlPoint(nClosedIdx); // tdf#99165 if the control points are 'empty', create the mathematical // correct replacement ones to avoid problems with the graphical sub-system // tdf#101026 The 1st attempt to create a mathematically correct replacement control // vector was wrong. Best alternative is one as close as possible which means short. if (aCP1.equal(aLast)) { aCP1 = aLast + ((aCP2 - aLast) * 0.0005); } if (aCP2.equal(aPoint)) { aCP2 = aPoint + ((aCP1 - aPoint) * 0.0005); } cairo_curve_to(cr, aCP1.getX(), aCP1.getY(), aCP2.getX(), aCP2.getY(), aPoint.getX(), aPoint.getY()); // take some bigger measure for curve segments - too expensive to subdivide // here and that precision not needed, but four (2 points, 2 control-points) // would be a too low weight nSizeMeasure += 10; } aLast = aPoint; } if (bClosePath) { cairo_close_path(cr); } return nSizeMeasure; } basegfx::B2DPoint PixelSnapper::snap(const basegfx::B2DPolygon& rPolygon, const basegfx::B2DHomMatrix& rObjectToDevice, basegfx::B2DHomMatrix& rObjectToDeviceInv, sal_uInt32 nIndex) { const sal_uInt32 nCount(rPolygon.count()); // get the data if (nIndex == 0) { // if it's the first time, we need to calculate everything maPrevPoint = rObjectToDevice * rPolygon.getB2DPoint((nIndex + nCount - 1) % nCount); maCurrPoint = rObjectToDevice * rPolygon.getB2DPoint(nIndex); maPrevTuple = basegfx::fround(maPrevPoint); maCurrTuple = basegfx::fround(maCurrPoint); } else { // but for all other times, we can re-use the previous iteration computations maPrevPoint = maCurrPoint; maPrevTuple = maCurrTuple; maCurrPoint = maNextPoint; maCurrTuple = maNextTuple; } maNextPoint = rObjectToDevice * rPolygon.getB2DPoint((nIndex + 1) % nCount); maNextTuple = basegfx::fround(maNextPoint); // get the states const bool bPrevVertical(maPrevTuple.getX() == maCurrTuple.getX()); const bool bNextVertical(maNextTuple.getX() == maCurrTuple.getX()); const bool bPrevHorizontal(maPrevTuple.getY() == maCurrTuple.getY()); const bool bNextHorizontal(maNextTuple.getY() == maCurrTuple.getY()); const bool bSnapX(bPrevVertical || bNextVertical); const bool bSnapY(bPrevHorizontal || bNextHorizontal); if (bSnapX || bSnapY) { basegfx::B2DPoint aSnappedPoint(bSnapX ? maCurrTuple.getX() : maCurrPoint.getX(), bSnapY ? maCurrTuple.getY() : maCurrPoint.getY()); if (rObjectToDeviceInv.isIdentity()) { rObjectToDeviceInv = rObjectToDevice; rObjectToDeviceInv.invert(); } aSnappedPoint *= rObjectToDeviceInv; return aSnappedPoint; } return rPolygon.getB2DPoint(nIndex); } SystemDependentData_CairoPath::SystemDependentData_CairoPath(size_t nSizeMeasure, cairo_t* cr, bool bNoJoin, bool bAntiAlias, const std::vector* pStroke) : basegfx::SystemDependentData(Application::GetSystemDependentDataManager()) , mpCairoPath(nullptr) , mbNoJoin(bNoJoin) , mbAntiAlias(bAntiAlias) { static const bool bFuzzing = comphelper::IsFuzzing(); // tdf#129845 only create a copy of the path when nSizeMeasure is // bigger than some decent threshold if (!bFuzzing && nSizeMeasure > 50) { mpCairoPath = cairo_copy_path(cr); if (nullptr != pStroke) { maStroke = *pStroke; } } } SystemDependentData_CairoPath::~SystemDependentData_CairoPath() { if (nullptr != mpCairoPath) { cairo_path_destroy(mpCairoPath); mpCairoPath = nullptr; } } sal_Int64 SystemDependentData_CairoPath::estimateUsageInBytes() const { // tdf#129845 by using the default return value of zero when no path // was created, SystemDependentData::calculateCombinedHoldCyclesInSeconds // will do the right thing and not buffer this entry at all sal_Int64 nRetval(0); if (nullptr != mpCairoPath) { // per node // - num_data incarnations of // - sizeof(cairo_path_data_t) which is a union of defines and point data // thus may 2 x sizeof(double) nRetval = mpCairoPath->num_data * sizeof(cairo_path_data_t); } return nRetval; } void add_polygon_path(cairo_t* cr, const basegfx::B2DPolyPolygon& rPolyPolygon, const basegfx::B2DHomMatrix& rObjectToDevice, bool bPixelSnap) { // try to access buffered data std::shared_ptr pSystemDependentData_CairoPath( rPolyPolygon.getSystemDependentData()); if (pSystemDependentData_CairoPath) { // re-use data cairo_append_path(cr, pSystemDependentData_CairoPath->getCairoPath()); } else { // create data size_t nSizeMeasure(0); for (const auto& rPoly : rPolyPolygon) { // PixelOffset used: Was dependent of 'm_aLineColor != SALCOLOR_NONE' // Adapt setupPolyPolygon-users to set a linear transformation to achieve PixelOffset nSizeMeasure += AddPolygonToPath(cr, rPoly, rObjectToDevice, bPixelSnap, false); } // copy and add to buffering mechanism // for decisions how/what to buffer, see Note in WinSalGraphicsImpl::drawPolyPolygon pSystemDependentData_CairoPath = rPolyPolygon.addOrReplaceSystemDependentData( nSizeMeasure, cr, false, false, nullptr); } } cairo_user_data_key_t* CairoCommon::getDamageKey() { static cairo_user_data_key_t aDamageKey; return &aDamageKey; } sal_uInt16 CairoCommon::GetBitCount() const { if (cairo_surface_get_content(m_pSurface) == CAIRO_CONTENT_ALPHA) return 1; return 32; } cairo_t* CairoCommon::getCairoContext(bool bXorModeAllowed, bool bAntiAlias) const { cairo_t* cr; if (m_ePaintMode == PaintMode::Xor && bXorModeAllowed) cr = createTmpCompatibleCairoContext(); else cr = cairo_create(m_pSurface); cairo_set_line_width(cr, 1); cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); cairo_set_antialias(cr, bAntiAlias ? CAIRO_ANTIALIAS_DEFAULT : CAIRO_ANTIALIAS_NONE); cairo_set_operator(cr, CAIRO_OPERATOR_OVER); // ensure no linear transformation and no PathInfo in local cairo_path_t cairo_identity_matrix(cr); cairo_new_path(cr); return cr; } void CairoCommon::releaseCairoContext(cairo_t* cr, bool bXorModeAllowed, const basegfx::B2DRange& rExtents) const { const bool bXoring = (m_ePaintMode == PaintMode::Xor && bXorModeAllowed); if (rExtents.isEmpty()) { //nothing changed, return early if (bXoring) { cairo_surface_t* surface = cairo_get_target(cr); cairo_surface_destroy(surface); } cairo_destroy(cr); return; } basegfx::B2IRange aIntExtents(basegfx::unotools::b2ISurroundingRangeFromB2DRange(rExtents)); sal_Int32 nExtentsLeft(aIntExtents.getMinX()), nExtentsTop(aIntExtents.getMinY()); sal_Int32 nExtentsRight(aIntExtents.getMaxX()), nExtentsBottom(aIntExtents.getMaxY()); sal_Int32 nWidth = m_aFrameSize.getX(); sal_Int32 nHeight = m_aFrameSize.getY(); nExtentsLeft = std::max(nExtentsLeft, 0); nExtentsTop = std::max(nExtentsTop, 0); nExtentsRight = std::min(nExtentsRight, nWidth); nExtentsBottom = std::min(nExtentsBottom, nHeight); cairo_surface_t* surface = cairo_get_target(cr); cairo_surface_flush(surface); //For the most part we avoid the use of XOR these days, but there //are some edge cases where legacy stuff still supports it, so //emulate it (slowly) here. if (bXoring) doXorOnRelease(nExtentsLeft, nExtentsTop, nExtentsRight, nExtentsBottom, surface, nWidth); cairo_destroy(cr); // unref DamageHandler* pDamage = static_cast(cairo_surface_get_user_data(m_pSurface, getDamageKey())); if (pDamage) { pDamage->damaged(pDamage->handle, nExtentsLeft, nExtentsTop, nExtentsRight - nExtentsLeft, nExtentsBottom - nExtentsTop); } } void CairoCommon::doXorOnRelease(sal_Int32 nExtentsLeft, sal_Int32 nExtentsTop, sal_Int32 nExtentsRight, sal_Int32 nExtentsBottom, cairo_surface_t* const surface, sal_Int32 nWidth) const { //For the most part we avoid the use of XOR these days, but there //are some edge cases where legacy stuff still supports it, so //emulate it (slowly) here. cairo_surface_t* target_surface = m_pSurface; if (cairo_surface_get_type(target_surface) != CAIRO_SURFACE_TYPE_IMAGE) { //in the unlikely case we can't use m_pSurface directly, copy contents //to another temp image surface if (cairo_surface_get_content(m_pSurface) == CAIRO_CONTENT_COLOR_ALPHA) target_surface = cairo_surface_map_to_image(target_surface, nullptr); else { // for gen, which is CAIRO_FORMAT_RGB24/CAIRO_CONTENT_COLOR I'm getting // visual corruption in vcldemo with cairo_surface_map_to_image cairo_t* copycr = createTmpCompatibleCairoContext(); cairo_rectangle(copycr, nExtentsLeft, nExtentsTop, nExtentsRight - nExtentsLeft, nExtentsBottom - nExtentsTop); cairo_set_source_surface(copycr, m_pSurface, 0, 0); cairo_fill(copycr); target_surface = cairo_get_target(copycr); cairo_destroy(copycr); } } cairo_surface_flush(target_surface); unsigned char* target_surface_data = cairo_image_surface_get_data(target_surface); unsigned char* xor_surface_data = cairo_image_surface_get_data(surface); cairo_format_t nFormat = cairo_image_surface_get_format(target_surface); assert(nFormat == CAIRO_FORMAT_ARGB32 && "need to implement CAIRO_FORMAT_A1 after all here"); sal_Int32 nStride = cairo_format_stride_for_width(nFormat, nWidth * m_fScale); sal_Int32 nUnscaledExtentsLeft = nExtentsLeft * m_fScale; sal_Int32 nUnscaledExtentsRight = nExtentsRight * m_fScale; sal_Int32 nUnscaledExtentsTop = nExtentsTop * m_fScale; sal_Int32 nUnscaledExtentsBottom = nExtentsBottom * m_fScale; // Handle headless size forced to (1,1) by SvpSalFrame::GetSurfaceFrameSize(). int target_surface_width = cairo_image_surface_get_width(target_surface); if (nUnscaledExtentsLeft > target_surface_width) nUnscaledExtentsLeft = target_surface_width; if (nUnscaledExtentsRight > target_surface_width) nUnscaledExtentsRight = target_surface_width; int target_surface_height = cairo_image_surface_get_height(target_surface); if (nUnscaledExtentsTop > target_surface_height) nUnscaledExtentsTop = target_surface_height; if (nUnscaledExtentsBottom > target_surface_height) nUnscaledExtentsBottom = target_surface_height; #if !ENABLE_WASM_STRIP_PREMULTIPLY vcl::bitmap::lookup_table const& unpremultiply_table = vcl::bitmap::get_unpremultiply_table(); vcl::bitmap::lookup_table const& premultiply_table = vcl::bitmap::get_premultiply_table(); #endif for (sal_Int32 y = nUnscaledExtentsTop; y < nUnscaledExtentsBottom; ++y) { unsigned char* true_row = target_surface_data + (nStride * y); unsigned char* xor_row = xor_surface_data + (nStride * y); unsigned char* true_data = true_row + (nUnscaledExtentsLeft * 4); unsigned char* xor_data = xor_row + (nUnscaledExtentsLeft * 4); for (sal_Int32 x = nUnscaledExtentsLeft; x < nUnscaledExtentsRight; ++x) { sal_uInt8 a = true_data[SVP_CAIRO_ALPHA]; sal_uInt8 xor_a = xor_data[SVP_CAIRO_ALPHA]; #if ENABLE_WASM_STRIP_PREMULTIPLY sal_uInt8 b = vcl::bitmap::unpremultiply(a, true_data[SVP_CAIRO_BLUE]) ^ vcl::bitmap::unpremultiply(xor_a, xor_data[SVP_CAIRO_BLUE]); sal_uInt8 g = vcl::bitmap::unpremultiply(a, true_data[SVP_CAIRO_GREEN]) ^ vcl::bitmap::unpremultiply(xor_a, xor_data[SVP_CAIRO_GREEN]); sal_uInt8 r = vcl::bitmap::unpremultiply(a, true_data[SVP_CAIRO_RED]) ^ vcl::bitmap::unpremultiply(xor_a, xor_data[SVP_CAIRO_RED]); true_data[SVP_CAIRO_BLUE] = vcl::bitmap::premultiply(a, b); true_data[SVP_CAIRO_GREEN] = vcl::bitmap::premultiply(a, g); true_data[SVP_CAIRO_RED] = vcl::bitmap::premultiply(a, r); #else sal_uInt8 b = unpremultiply_table[a][true_data[SVP_CAIRO_BLUE]] ^ unpremultiply_table[xor_a][xor_data[SVP_CAIRO_BLUE]]; sal_uInt8 g = unpremultiply_table[a][true_data[SVP_CAIRO_GREEN]] ^ unpremultiply_table[xor_a][xor_data[SVP_CAIRO_GREEN]]; sal_uInt8 r = unpremultiply_table[a][true_data[SVP_CAIRO_RED]] ^ unpremultiply_table[xor_a][xor_data[SVP_CAIRO_RED]]; true_data[SVP_CAIRO_BLUE] = premultiply_table[a][b]; true_data[SVP_CAIRO_GREEN] = premultiply_table[a][g]; true_data[SVP_CAIRO_RED] = premultiply_table[a][r]; #endif true_data += 4; xor_data += 4; } } cairo_surface_mark_dirty(target_surface); if (target_surface != m_pSurface) { if (cairo_surface_get_content(m_pSurface) == CAIRO_CONTENT_COLOR_ALPHA) cairo_surface_unmap_image(m_pSurface, target_surface); else { cairo_t* copycr = cairo_create(m_pSurface); //copy contents back from image surface cairo_rectangle(copycr, nExtentsLeft, nExtentsTop, nExtentsRight - nExtentsLeft, nExtentsBottom - nExtentsTop); cairo_set_source_surface(copycr, target_surface, 0, 0); cairo_fill(copycr); cairo_destroy(copycr); cairo_surface_destroy(target_surface); } } cairo_surface_destroy(surface); } cairo_t* CairoCommon::createTmpCompatibleCairoContext() const { cairo_surface_t* target = cairo_surface_create_similar_image(m_pSurface, CAIRO_FORMAT_ARGB32, m_aFrameSize.getX() * m_fScale, m_aFrameSize.getY() * m_fScale); dl_cairo_surface_set_device_scale(target, m_fScale, m_fScale); return cairo_create(target); } void CairoCommon::applyColor(cairo_t* cr, Color aColor, double fTransparency) { if (cairo_surface_get_content(cairo_get_target(cr)) != CAIRO_CONTENT_ALPHA) { cairo_set_source_rgba(cr, aColor.GetRed() / 255.0, aColor.GetGreen() / 255.0, aColor.GetBlue() / 255.0, 1.0 - fTransparency); } else { double fSet = aColor == COL_BLACK ? 1.0 : 0.0; cairo_set_source_rgba(cr, 1, 1, 1, fSet); cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); } } void CairoCommon::clipRegion(cairo_t* cr, const vcl::Region& rClipRegion) { RectangleVector aRectangles; if (!rClipRegion.IsEmpty()) { rClipRegion.GetRegionRectangles(aRectangles); } if (!aRectangles.empty()) { bool bEmpty = true; for (auto const& rectangle : aRectangles) { if (rectangle.GetWidth() <= 0 || rectangle.GetHeight() <= 0) { SAL_WARN("vcl.gdi", "bad clip rect of: " << rectangle); continue; } cairo_rectangle(cr, rectangle.Left(), rectangle.Top(), rectangle.GetWidth(), rectangle.GetHeight()); bEmpty = false; } if (!bEmpty) cairo_clip(cr); } } void CairoCommon::clipRegion(cairo_t* cr) { CairoCommon::clipRegion(cr, m_aClipRegion); } void CairoCommon::SetXORMode(bool bSet, bool /*bInvertOnly*/) { m_ePaintMode = bSet ? PaintMode::Xor : PaintMode::Over; } void CairoCommon::SetROPLineColor(SalROPColor nROPColor) { switch (nROPColor) { case SalROPColor::N0: m_oLineColor = Color(0, 0, 0); break; case SalROPColor::N1: m_oLineColor = Color(0xff, 0xff, 0xff); break; case SalROPColor::Invert: m_oLineColor = Color(0xff, 0xff, 0xff); break; } } void CairoCommon::SetROPFillColor(SalROPColor nROPColor) { switch (nROPColor) { case SalROPColor::N0: m_oFillColor = Color(0, 0, 0); break; case SalROPColor::N1: m_oFillColor = Color(0xff, 0xff, 0xff); break; case SalROPColor::Invert: m_oFillColor = Color(0xff, 0xff, 0xff); break; } } void CairoCommon::drawPixel(const std::optional& rLineColor, tools::Long nX, tools::Long nY, bool bAntiAlias) { if (!rLineColor) return; cairo_t* cr = getCairoContext(true, bAntiAlias); clipRegion(cr); cairo_rectangle(cr, nX, nY, 1, 1); CairoCommon::applyColor(cr, *rLineColor, 0.0); cairo_fill(cr); basegfx::B2DRange extents = getClippedFillDamage(cr); releaseCairoContext(cr, true, extents); } Color CairoCommon::getPixel(cairo_surface_t* pSurface, tools::Long nX, tools::Long nY) { cairo_surface_t* target = cairo_surface_create_similar_image(pSurface, CAIRO_FORMAT_ARGB32, 1, 1); cairo_t* cr = cairo_create(target); cairo_rectangle(cr, 0, 0, 1, 1); cairo_set_source_surface(cr, pSurface, -nX, -nY); cairo_paint(cr); cairo_destroy(cr); cairo_surface_flush(target); #if !ENABLE_WASM_STRIP_PREMULTIPLY vcl::bitmap::lookup_table const& unpremultiply_table = vcl::bitmap::get_unpremultiply_table(); #endif unsigned char* data = cairo_image_surface_get_data(target); sal_uInt8 a = data[SVP_CAIRO_ALPHA]; #if ENABLE_WASM_STRIP_PREMULTIPLY sal_uInt8 b = vcl::bitmap::unpremultiply(a, data[SVP_CAIRO_BLUE]); sal_uInt8 g = vcl::bitmap::unpremultiply(a, data[SVP_CAIRO_GREEN]); sal_uInt8 r = vcl::bitmap::unpremultiply(a, data[SVP_CAIRO_RED]); #else sal_uInt8 b = unpremultiply_table[a][data[SVP_CAIRO_BLUE]]; sal_uInt8 g = unpremultiply_table[a][data[SVP_CAIRO_GREEN]]; sal_uInt8 r = unpremultiply_table[a][data[SVP_CAIRO_RED]]; #endif Color aColor(ColorAlpha, a, r, g, b); cairo_surface_destroy(target); return aColor; } void CairoCommon::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2, tools::Long nY2, bool bAntiAlias) { cairo_t* cr = getCairoContext(false, bAntiAlias); clipRegion(cr); basegfx::B2DPolygon aPoly; // PixelOffset used: To not mix with possible PixelSnap, cannot do // directly on coordinates as tried before - despite being already 'snapped' // due to being integer. If it would be directly added here, it would be // 'snapped' again when !getAntiAlias(), losing the (0.5, 0.5) offset aPoly.append(basegfx::B2DPoint(nX1, nY1)); aPoly.append(basegfx::B2DPoint(nX2, nY2)); // PixelOffset used: Set PixelOffset as linear transformation cairo_matrix_t aMatrix; cairo_matrix_init_translate(&aMatrix, 0.5, 0.5); cairo_set_matrix(cr, &aMatrix); AddPolygonToPath(cr, aPoly, basegfx::B2DHomMatrix(), !bAntiAlias, false); CairoCommon::applyColor(cr, *m_oLineColor); basegfx::B2DRange extents = getClippedStrokeDamage(cr); extents.transform(basegfx::utils::createTranslateB2DHomMatrix(0.5, 0.5)); cairo_stroke(cr); releaseCairoContext(cr, false, extents); } // true if we have a fill color and the line color is the same or non-existent static bool onlyFillRect(const std::optional& rFillColor, const std::optional& rLineColor) { if (!rFillColor) return false; if (!rLineColor) return true; return *rFillColor == *rLineColor; } void CairoCommon::drawRect(double nX, double nY, double nWidth, double nHeight, bool bAntiAlias) { // fast path for the common case of simply creating a solid block of color if (onlyFillRect(m_oFillColor, m_oLineColor)) { double fTransparency = 0; // don't bother trying to draw stuff which is effectively invisible if (nWidth < 0.1 || nHeight < 0.1) return; cairo_t* cr = getCairoContext(true, bAntiAlias); clipRegion(cr); bool bPixelSnap = !bAntiAlias; if (bPixelSnap) { // snap by rounding nX = basegfx::fround(nX); nY = basegfx::fround(nY); nWidth = basegfx::fround(nWidth); nHeight = basegfx::fround(nHeight); } cairo_rectangle(cr, nX, nY, nWidth, nHeight); CairoCommon::applyColor(cr, *m_oFillColor, fTransparency); // Get FillDamage basegfx::B2DRange extents = getClippedFillDamage(cr); cairo_fill(cr); releaseCairoContext(cr, true, extents); return; } // because of the -1 hack we have to do fill and draw separately std::optional aOrigFillColor = m_oFillColor; std::optional aOrigLineColor = m_oLineColor; m_oFillColor = std::nullopt; m_oLineColor = std::nullopt; if (aOrigFillColor) { basegfx::B2DPolygon aRect = basegfx::utils::createPolygonFromRect( basegfx::B2DRectangle(nX, nY, nX + nWidth, nY + nHeight)); m_oFillColor = aOrigFillColor; drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aRect), 0.0, bAntiAlias); m_oFillColor = std::nullopt; } if (aOrigLineColor) { // need -1 hack to exclude the bottom and right edges to act like wingdi "Rectangle" // function which is what was probably the ultimate origin of this behavior basegfx::B2DPolygon aRect = basegfx::utils::createPolygonFromRect( basegfx::B2DRectangle(nX, nY, nX + nWidth - 1, nY + nHeight - 1)); m_oLineColor = aOrigLineColor; drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aRect), 0.0, bAntiAlias); m_oLineColor = std::nullopt; } m_oFillColor = aOrigFillColor; m_oLineColor = aOrigLineColor; } void CairoCommon::drawPolygon(sal_uInt32 nPoints, const Point* pPtAry, bool bAntiAlias) { basegfx::B2DPolygon aPoly; aPoly.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints); for (sal_uInt32 i = 1; i < nPoints; ++i) aPoly.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY())); drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aPoly), 0.0, bAntiAlias); } void CairoCommon::drawPolyPolygon(sal_uInt32 nPoly, const sal_uInt32* pPointCounts, const Point** pPtAry, bool bAntiAlias) { basegfx::B2DPolyPolygon aPolyPoly; for (sal_uInt32 nPolygon = 0; nPolygon < nPoly; ++nPolygon) { sal_uInt32 nPoints = pPointCounts[nPolygon]; if (nPoints) { const Point* pPoints = pPtAry[nPolygon]; basegfx::B2DPolygon aPoly; aPoly.append(basegfx::B2DPoint(pPoints->getX(), pPoints->getY()), nPoints); for (sal_uInt32 i = 1; i < nPoints; ++i) aPoly.setB2DPoint(i, basegfx::B2DPoint(pPoints[i].getX(), pPoints[i].getY())); aPolyPoly.append(aPoly); } } drawPolyPolygon(basegfx::B2DHomMatrix(), aPolyPoly, 0.0, bAntiAlias); } void CairoCommon::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice, const basegfx::B2DPolyPolygon& rPolyPolygon, double fTransparency, bool bAntiAlias) { const bool bHasFill(m_oFillColor.has_value()); const bool bHasLine(m_oLineColor.has_value()); if (0 == rPolyPolygon.count() || !(bHasFill || bHasLine) || fTransparency < 0.0 || fTransparency >= 1.0) { return; } if (!bHasLine) { // don't bother trying to draw stuff which is effectively invisible, speeds up // drawing some complex drawings. This optimisation is not valid when we do // the pixel offset thing (i.e. bHasLine) basegfx::B2DRange aPolygonRange = rPolyPolygon.getB2DRange(); aPolygonRange.transform(rObjectToDevice); if (aPolygonRange.getWidth() < 0.1 || aPolygonRange.getHeight() < 0.1) return; } cairo_t* cr = getCairoContext(true, bAntiAlias); if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) { SAL_WARN("vcl.gdi", "cannot render to surface: " << cairo_status_to_string(cairo_status(cr))); releaseCairoContext(cr, true, basegfx::B2DRange()); return; } clipRegion(cr); // Set full (Object-to-Device) transformation - if used if (!rObjectToDevice.isIdentity()) { cairo_matrix_t aMatrix; cairo_matrix_init(&aMatrix, rObjectToDevice.get(0, 0), rObjectToDevice.get(1, 0), rObjectToDevice.get(0, 1), rObjectToDevice.get(1, 1), rObjectToDevice.get(0, 2), rObjectToDevice.get(1, 2)); cairo_set_matrix(cr, &aMatrix); } // To make releaseCairoContext work, use empty extents basegfx::B2DRange extents; if (bHasFill) { add_polygon_path(cr, rPolyPolygon, rObjectToDevice, !bAntiAlias); CairoCommon::applyColor(cr, *m_oFillColor, fTransparency); // Get FillDamage (will be extended for LineDamage below) extents = getClippedFillDamage(cr); cairo_fill(cr); } if (bHasLine) { // PixelOffset used: Set PixelOffset as linear transformation cairo_matrix_t aMatrix; cairo_matrix_init_translate(&aMatrix, 0.5, 0.5); cairo_set_matrix(cr, &aMatrix); add_polygon_path(cr, rPolyPolygon, rObjectToDevice, !bAntiAlias); CairoCommon::applyColor(cr, *m_oLineColor, fTransparency); // expand with possible StrokeDamage basegfx::B2DRange stroke_extents = getClippedStrokeDamage(cr); stroke_extents.transform(basegfx::utils::createTranslateB2DHomMatrix(0.5, 0.5)); extents.expand(stroke_extents); cairo_stroke(cr); } // if transformation has been applied, transform also extents (ranges) // of damage so they can be correctly redrawn extents.transform(rObjectToDevice); releaseCairoContext(cr, true, extents); } void CairoCommon::drawPolyLine(sal_uInt32 nPoints, const Point* pPtAry, bool bAntiAlias) { basegfx::B2DPolygon aPoly; aPoly.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints); for (sal_uInt32 i = 1; i < nPoints; ++i) aPoly.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY())); aPoly.setClosed(false); drawPolyLine(basegfx::B2DHomMatrix(), aPoly, 0.0, 1.0, nullptr, basegfx::B2DLineJoin::Miter, css::drawing::LineCap_BUTT, basegfx::deg2rad(15.0) /*default*/, false, bAntiAlias); } bool CairoCommon::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice, const basegfx::B2DPolygon& rPolyLine, double fTransparency, double fLineWidth, const std::vector* pStroke, basegfx::B2DLineJoin eLineJoin, css::drawing::LineCap eLineCap, double fMiterMinimumAngle, bool bPixelSnapHairline, bool bAntiAlias) { // short circuit if there is nothing to do if (0 == rPolyLine.count() || fTransparency < 0.0 || fTransparency >= 1.0) { return true; } static const bool bFuzzing = comphelper::IsFuzzing(); if (bFuzzing) { const basegfx::B2DRange aRange(basegfx::utils::getRange(rPolyLine)); if (aRange.getMaxX() - aRange.getMinX() > 0x10000000 || aRange.getMaxY() - aRange.getMinY() > 0x10000000) { SAL_WARN("vcl.gdi", "drawPolyLine, skipping suspicious range of: " << aRange << " for fuzzing performance"); return true; } } cairo_t* cr = getCairoContext(false, bAntiAlias); clipRegion(cr); // need to check/handle LineWidth when ObjectToDevice transformation is used const bool bObjectToDeviceIsIdentity(rObjectToDevice.isIdentity()); // tdf#124848 calculate-back logical LineWidth for a hairline // since this implementation hands over the transformation to // the graphic sub-system if (fLineWidth == 0) { fLineWidth = 1.0; if (!bObjectToDeviceIsIdentity) { basegfx::B2DHomMatrix aObjectToDeviceInv(rObjectToDevice); aObjectToDeviceInv.invert(); fLineWidth = (aObjectToDeviceInv * basegfx::B2DVector(fLineWidth, 0)).getLength(); } } // PixelOffset used: Need to reflect in linear transformation cairo_matrix_t aMatrix; basegfx::B2DHomMatrix aDamageMatrix(basegfx::utils::createTranslateB2DHomMatrix(0.5, 0.5)); if (bObjectToDeviceIsIdentity) { // Set PixelOffset as requested cairo_matrix_init_translate(&aMatrix, 0.5, 0.5); } else { // Prepare ObjectToDevice transformation. Take PixelOffset for Lines into // account: Multiply from left to act in DeviceCoordinates aDamageMatrix = aDamageMatrix * rObjectToDevice; cairo_matrix_init(&aMatrix, aDamageMatrix.get(0, 0), aDamageMatrix.get(1, 0), aDamageMatrix.get(0, 1), aDamageMatrix.get(1, 1), aDamageMatrix.get(0, 2), aDamageMatrix.get(1, 2)); } // set linear transformation cairo_set_matrix(cr, &aMatrix); // setup line attributes cairo_line_join_t eCairoLineJoin = CAIRO_LINE_JOIN_MITER; switch (eLineJoin) { case basegfx::B2DLineJoin::Bevel: eCairoLineJoin = CAIRO_LINE_JOIN_BEVEL; break; case basegfx::B2DLineJoin::Round: eCairoLineJoin = CAIRO_LINE_JOIN_ROUND; break; case basegfx::B2DLineJoin::NONE: case basegfx::B2DLineJoin::Miter: eCairoLineJoin = CAIRO_LINE_JOIN_MITER; break; } // convert miter minimum angle to miter limit double fMiterLimit = 1.0 / sin(std::max(fMiterMinimumAngle, 0.01 * M_PI) / 2.0); // setup cap attribute cairo_line_cap_t eCairoLineCap(CAIRO_LINE_CAP_BUTT); switch (eLineCap) { default: // css::drawing::LineCap_BUTT: { eCairoLineCap = CAIRO_LINE_CAP_BUTT; break; } case css::drawing::LineCap_ROUND: { eCairoLineCap = CAIRO_LINE_CAP_ROUND; break; } case css::drawing::LineCap_SQUARE: { eCairoLineCap = CAIRO_LINE_CAP_SQUARE; break; } } cairo_set_source_rgba(cr, m_oLineColor->GetRed() / 255.0, m_oLineColor->GetGreen() / 255.0, m_oLineColor->GetBlue() / 255.0, 1.0 - fTransparency); cairo_set_line_join(cr, eCairoLineJoin); cairo_set_line_cap(cr, eCairoLineCap); constexpr int MaxNormalLineWidthPx = 64; if (fLineWidth > MaxNormalLineWidthPx) { const double fLineWidthPixel = bObjectToDeviceIsIdentity ? fLineWidth : (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength(); constexpr double MaxLineWidth = 0x20000000; // if the width is pixels is excessive, or if the actual number is huge, then // when fuzzing drop it to something small if (fLineWidthPixel > MaxNormalLineWidthPx || fLineWidth > MaxLineWidth) { SAL_WARN("vcl.gdi", "drawPolyLine, suspicious input line width of: " << fLineWidth << ", will be " << fLineWidthPixel << " pixels thick"); if (bFuzzing) { basegfx::B2DHomMatrix aObjectToDeviceInv(rObjectToDevice); aObjectToDeviceInv.invert(); fLineWidth = (aObjectToDeviceInv * basegfx::B2DVector(MaxNormalLineWidthPx, 0)) .getLength(); fLineWidth = std::min(fLineWidth, 2048.0); } } } cairo_set_line_width(cr, fLineWidth); cairo_set_miter_limit(cr, fMiterLimit); // try to access buffered data std::shared_ptr pSystemDependentData_CairoPath( rPolyLine.getSystemDependentData()); // MM01 need to do line dashing as fallback stuff here now const double fDotDashLength( nullptr != pStroke ? std::accumulate(pStroke->begin(), pStroke->end(), 0.0) : 0.0); const bool bStrokeUsed(0.0 != fDotDashLength); assert(!bStrokeUsed || (bStrokeUsed && pStroke)); // MM01 decide if to stroke directly static const bool bDoDirectCairoStroke(true); // MM01 activate to stroke directly if (bDoDirectCairoStroke && bStrokeUsed) { cairo_set_dash(cr, pStroke->data(), pStroke->size(), 0.0); } if (!bDoDirectCairoStroke && pSystemDependentData_CairoPath) { // MM01 - check on stroke change. Used against not used, or if both used, // equal or different? const bool bStrokeWasUsed(!pSystemDependentData_CairoPath->getStroke().empty()); if (bStrokeWasUsed != bStrokeUsed || (bStrokeUsed && *pStroke != pSystemDependentData_CairoPath->getStroke())) { // data invalid, forget pSystemDependentData_CairoPath.reset(); } } // check for basegfx::B2DLineJoin::NONE to react accordingly const bool bNoJoin( (basegfx::B2DLineJoin::NONE == eLineJoin && basegfx::fTools::more(fLineWidth, 0.0))); if (pSystemDependentData_CairoPath) { // check data validity if (nullptr == pSystemDependentData_CairoPath->getCairoPath() || pSystemDependentData_CairoPath->getNoJoin() != bNoJoin || pSystemDependentData_CairoPath->getAntiAlias() != bAntiAlias || bPixelSnapHairline /*tdf#124700*/) { // data invalid, forget pSystemDependentData_CairoPath.reset(); } } if (pSystemDependentData_CairoPath) { // re-use data cairo_append_path(cr, pSystemDependentData_CairoPath->getCairoPath()); } else { // create data size_t nSizeMeasure(0); // MM01 need to do line dashing as fallback stuff here now basegfx::B2DPolyPolygon aPolyPolygonLine; if (!bDoDirectCairoStroke && bStrokeUsed) { // apply LineStyle basegfx::utils::applyLineDashing(rPolyLine, // source *pStroke, // pattern &aPolyPolygonLine, // target for lines nullptr, // target for gaps fDotDashLength); // full length if available } else { // no line dashing or direct stroke, just copy aPolyPolygonLine.append(rPolyLine); } // MM01 checked/verified for Cairo for (sal_uInt32 a(0); a < aPolyPolygonLine.count(); a++) { const basegfx::B2DPolygon aPolyLine(aPolyPolygonLine.getB2DPolygon(a)); if (!bNoJoin) { // PixelOffset now reflected in linear transformation used nSizeMeasure += AddPolygonToPath(cr, aPolyLine, rObjectToDevice, // ObjectToDevice *without* LineDraw-Offset !bAntiAlias, bPixelSnapHairline); } else { const sal_uInt32 nPointCount(aPolyLine.count()); const sal_uInt32 nEdgeCount(aPolyLine.isClosed() ? nPointCount : nPointCount - 1); basegfx::B2DPolygon aEdge; aEdge.append(aPolyLine.getB2DPoint(0)); aEdge.append(basegfx::B2DPoint(0.0, 0.0)); for (sal_uInt32 i(0); i < nEdgeCount; i++) { const sal_uInt32 nNextIndex((i + 1) % nPointCount); aEdge.setB2DPoint(1, aPolyLine.getB2DPoint(nNextIndex)); aEdge.setNextControlPoint(0, aPolyLine.getNextControlPoint(i)); aEdge.setPrevControlPoint(1, aPolyLine.getPrevControlPoint(nNextIndex)); // PixelOffset now reflected in linear transformation used nSizeMeasure += AddPolygonToPath( cr, aEdge, rObjectToDevice, // ObjectToDevice *without* LineDraw-Offset !bAntiAlias, bPixelSnapHairline); // prepare next step aEdge.setB2DPoint(0, aEdge.getB2DPoint(1)); } } } // copy and add to buffering mechanism if (!bPixelSnapHairline /*tdf#124700*/) { pSystemDependentData_CairoPath = rPolyLine.addOrReplaceSystemDependentData( nSizeMeasure, cr, bNoJoin, bAntiAlias, pStroke); } } // extract extents basegfx::B2DRange extents = getClippedStrokeDamage(cr); // transform also extents (ranges) of damage so they can be correctly redrawn extents.transform(aDamageMatrix); // draw and consume cairo_stroke(cr); releaseCairoContext(cr, false, extents); return true; } bool CairoCommon::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, sal_uInt8 nTransparency, bool bAntiAlias) { const bool bHasFill(m_oFillColor.has_value()); const bool bHasLine(m_oLineColor.has_value()); if (!bHasFill && !bHasLine) return true; cairo_t* cr = getCairoContext(false, bAntiAlias); clipRegion(cr); const double fTransparency = nTransparency * (1.0 / 100); // To make releaseCairoContext work, use empty extents basegfx::B2DRange extents; if (bHasFill) { cairo_rectangle(cr, nX, nY, nWidth, nHeight); applyColor(cr, *m_oFillColor, fTransparency); // set FillDamage extents = getClippedFillDamage(cr); cairo_fill(cr); } if (bHasLine) { // PixelOffset used: Set PixelOffset as linear transformation // Note: Was missing here - probably not by purpose (?) cairo_matrix_t aMatrix; cairo_matrix_init_translate(&aMatrix, 0.5, 0.5); cairo_set_matrix(cr, &aMatrix); cairo_rectangle(cr, nX, nY, nWidth, nHeight); applyColor(cr, *m_oLineColor, fTransparency); // expand with possible StrokeDamage basegfx::B2DRange stroke_extents = getClippedStrokeDamage(cr); stroke_extents.transform(basegfx::utils::createTranslateB2DHomMatrix(0.5, 0.5)); extents.expand(stroke_extents); cairo_stroke(cr); } releaseCairoContext(cr, false, extents); return true; } bool CairoCommon::drawGradient(const tools::PolyPolygon& rPolyPolygon, const Gradient& rGradient, bool bAntiAlias) { if (rGradient.GetStyle() != css::awt::GradientStyle_LINEAR && rGradient.GetStyle() != css::awt::GradientStyle_RADIAL) return false; // unsupported if (rGradient.GetSteps() != 0) return false; // We can't tell cairo how many colors to use in the gradient. cairo_t* cr = getCairoContext(true, bAntiAlias); clipRegion(cr); tools::Rectangle aInputRect(rPolyPolygon.GetBoundRect()); if (rPolyPolygon.IsRect()) { // Rect->Polygon conversion loses the right and bottom edge, fix that. aInputRect.AdjustRight(1); aInputRect.AdjustBottom(1); basegfx::B2DHomMatrix rObjectToDevice; AddPolygonToPath(cr, tools::Polygon(aInputRect).getB2DPolygon(), rObjectToDevice, !bAntiAlias, false); } else { basegfx::B2DPolyPolygon aB2DPolyPolygon(rPolyPolygon.getB2DPolyPolygon()); for (auto const& rPolygon : std::as_const(aB2DPolyPolygon)) { basegfx::B2DHomMatrix rObjectToDevice; AddPolygonToPath(cr, rPolygon, rObjectToDevice, !bAntiAlias, false); } } Gradient aGradient(rGradient); tools::Rectangle aBoundRect; Point aCenter; aGradient.SetAngle(aGradient.GetAngle() + 2700_deg10); aGradient.GetBoundRect(aInputRect, aBoundRect, aCenter); Color aStartColor = aGradient.GetStartColor(); Color aEndColor = aGradient.GetEndColor(); cairo_pattern_t* pattern; if (rGradient.GetStyle() == css::awt::GradientStyle_LINEAR) { tools::Polygon aPoly(aBoundRect); aPoly.Rotate(aCenter, aGradient.GetAngle() % 3600_deg10); pattern = cairo_pattern_create_linear(aPoly[0].X(), aPoly[0].Y(), aPoly[1].X(), aPoly[1].Y()); } else { double radius = std::max(aBoundRect.GetWidth() / 2.0, aBoundRect.GetHeight() / 2.0); // Move the center a bit to the top-left (the default VCL algorithm is a bit off-center that way, // cairo is the opposite way). pattern = cairo_pattern_create_radial(aCenter.X() - 0.5, aCenter.Y() - 0.5, 0, aCenter.X() - 0.5, aCenter.Y() - 0.5, radius); std::swap(aStartColor, aEndColor); } cairo_pattern_add_color_stop_rgba( pattern, aGradient.GetBorder() / 100.0, aStartColor.GetRed() * aGradient.GetStartIntensity() / 25500.0, aStartColor.GetGreen() * aGradient.GetStartIntensity() / 25500.0, aStartColor.GetBlue() * aGradient.GetStartIntensity() / 25500.0, 1.0); cairo_pattern_add_color_stop_rgba( pattern, 1.0, aEndColor.GetRed() * aGradient.GetEndIntensity() / 25500.0, aEndColor.GetGreen() * aGradient.GetEndIntensity() / 25500.0, aEndColor.GetBlue() * aGradient.GetEndIntensity() / 25500.0, 1.0); cairo_set_source(cr, pattern); cairo_pattern_destroy(pattern); basegfx::B2DRange extents = getClippedFillDamage(cr); cairo_fill_preserve(cr); releaseCairoContext(cr, true, extents); return true; } bool CairoCommon::implDrawGradient(basegfx::B2DPolyPolygon const& rPolyPolygon, SalGradient const& rGradient, bool bAntiAlias) { cairo_t* cr = getCairoContext(true, bAntiAlias); basegfx::B2DHomMatrix rObjectToDevice; for (auto const& rPolygon : rPolyPolygon) AddPolygonToPath(cr, rPolygon, rObjectToDevice, !bAntiAlias, false); cairo_pattern_t* pattern = cairo_pattern_create_linear(rGradient.maPoint1.getX(), rGradient.maPoint1.getY(), rGradient.maPoint2.getX(), rGradient.maPoint2.getY()); for (SalGradientStop const& rStop : rGradient.maStops) { double r = rStop.maColor.GetRed() / 255.0; double g = rStop.maColor.GetGreen() / 255.0; double b = rStop.maColor.GetBlue() / 255.0; double a = rStop.maColor.GetAlpha() / 255.0; double offset = rStop.mfOffset; cairo_pattern_add_color_stop_rgba(pattern, offset, r, g, b, a); } cairo_set_source(cr, pattern); cairo_pattern_destroy(pattern); basegfx::B2DRange extents = getClippedFillDamage(cr); cairo_fill_preserve(cr); releaseCairoContext(cr, true, extents); return true; } namespace { basegfx::B2DRange renderWithOperator(cairo_t* cr, const SalTwoRect& rTR, cairo_surface_t* source, cairo_operator_t eOperator = CAIRO_OPERATOR_SOURCE) { cairo_rectangle(cr, rTR.mnDestX, rTR.mnDestY, rTR.mnDestWidth, rTR.mnDestHeight); basegfx::B2DRange extents = getClippedFillDamage(cr); cairo_clip(cr); cairo_translate(cr, rTR.mnDestX, rTR.mnDestY); if (rTR.mnSrcWidth != 0 && rTR.mnSrcHeight != 0) { double fXScale = static_cast(rTR.mnDestWidth) / rTR.mnSrcWidth; double fYScale = static_cast(rTR.mnDestHeight) / rTR.mnSrcHeight; cairo_scale(cr, fXScale, fYScale); } cairo_save(cr); cairo_set_source_surface(cr, source, -rTR.mnSrcX, -rTR.mnSrcY); if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) { //tdf#133716 borders of upscaled images should not be blurred cairo_pattern_t* sourcepattern = cairo_get_source(cr); cairo_pattern_set_extend(sourcepattern, CAIRO_EXTEND_PAD); } cairo_set_operator(cr, eOperator); cairo_paint(cr); cairo_restore(cr); return extents; } } // end anonymous ns basegfx::B2DRange CairoCommon::renderSource(cairo_t* cr, const SalTwoRect& rTR, cairo_surface_t* source) { return renderWithOperator(cr, rTR, source, CAIRO_OPERATOR_SOURCE); } void CairoCommon::copyWithOperator(const SalTwoRect& rTR, cairo_surface_t* source, cairo_operator_t eOp, bool bAntiAlias) { cairo_t* cr = getCairoContext(false, bAntiAlias); clipRegion(cr); basegfx::B2DRange extents = renderWithOperator(cr, rTR, source, eOp); releaseCairoContext(cr, false, extents); } void CairoCommon::copySource(const SalTwoRect& rTR, cairo_surface_t* source, bool bAntiAlias) { copyWithOperator(rTR, source, CAIRO_OPERATOR_SOURCE, bAntiAlias); } void CairoCommon::copyBitsCairo(const SalTwoRect& rTR, cairo_surface_t* pSourceSurface, bool bAntiAlias) { SalTwoRect aTR(rTR); cairo_surface_t* pCopy = nullptr; if (pSourceSurface == getSurface()) { //self copy is a problem, so dup source in that case pCopy = cairo_surface_create_similar(pSourceSurface, cairo_surface_get_content(getSurface()), aTR.mnSrcWidth * m_fScale, aTR.mnSrcHeight * m_fScale); dl_cairo_surface_set_device_scale(pCopy, m_fScale, m_fScale); cairo_t* cr = cairo_create(pCopy); cairo_set_source_surface(cr, pSourceSurface, -aTR.mnSrcX, -aTR.mnSrcY); cairo_rectangle(cr, 0, 0, aTR.mnSrcWidth, aTR.mnSrcHeight); cairo_fill(cr); cairo_destroy(cr); pSourceSurface = pCopy; aTR.mnSrcX = 0; aTR.mnSrcY = 0; } copySource(aTR, pSourceSurface, bAntiAlias); if (pCopy) cairo_surface_destroy(pCopy); } namespace { cairo_pattern_t* create_stipple() { static unsigned char data[16] = { 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF }; cairo_surface_t* surface = cairo_image_surface_create_for_data(data, CAIRO_FORMAT_A8, 4, 4, 4); cairo_pattern_t* pattern = cairo_pattern_create_for_surface(surface); cairo_surface_destroy(surface); cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT); cairo_pattern_set_filter(pattern, CAIRO_FILTER_NEAREST); return pattern; } } // end anonymous ns void CairoCommon::invert(const basegfx::B2DPolygon& rPoly, SalInvert nFlags, bool bAntiAlias) { cairo_t* cr = getCairoContext(false, bAntiAlias); clipRegion(cr); // To make releaseCairoContext work, use empty extents basegfx::B2DRange extents; AddPolygonToPath(cr, rPoly, basegfx::B2DHomMatrix(), !bAntiAlias, false); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE); if (nFlags & SalInvert::TrackFrame) { cairo_set_line_width(cr, 2.0); const double dashLengths[2] = { 4.0, 4.0 }; cairo_set_dash(cr, dashLengths, 2, 0); extents = getClippedStrokeDamage(cr); //see tdf#106577 under wayland, some pixel droppings seen, maybe we're //out by one somewhere, or cairo_stroke_extents is confused by //dashes/line width if (!extents.isEmpty()) { extents.grow(1); } cairo_stroke(cr); } else { extents = getClippedFillDamage(cr); cairo_clip(cr); if (nFlags & SalInvert::N50) { cairo_pattern_t* pattern = create_stipple(); cairo_surface_t* surface = cairo_surface_create_similar( m_pSurface, cairo_surface_get_content(m_pSurface), extents.getWidth() * m_fScale, extents.getHeight() * m_fScale); dl_cairo_surface_set_device_scale(surface, m_fScale, m_fScale); cairo_t* stipple_cr = cairo_create(surface); cairo_set_source_rgb(stipple_cr, 1.0, 1.0, 1.0); cairo_mask(stipple_cr, pattern); cairo_pattern_destroy(pattern); cairo_destroy(stipple_cr); cairo_mask_surface(cr, surface, extents.getMinX(), extents.getMinY()); cairo_surface_destroy(surface); } else { cairo_paint(cr); } } releaseCairoContext(cr, false, extents); } void CairoCommon::invert(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, SalInvert nFlags, bool bAntiAlias) { basegfx::B2DPolygon aRect = basegfx::utils::createPolygonFromRect( basegfx::B2DRectangle(nX, nY, nX + nWidth, nY + nHeight)); invert(aRect, nFlags, bAntiAlias); } void CairoCommon::invert(sal_uInt32 nPoints, const Point* pPtAry, SalInvert nFlags, bool bAntiAlias) { basegfx::B2DPolygon aPoly; aPoly.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints); for (sal_uInt32 i = 1; i < nPoints; ++i) aPoly.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY())); aPoly.setClosed(true); invert(aPoly, nFlags, bAntiAlias); } void CairoCommon::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, bool bAntiAlias) { // MM02 try to access buffered BitmapHelper std::shared_ptr aSurface; tryToUseSourceBuffer(rSalBitmap, aSurface); cairo_surface_t* source = aSurface->getSurface(rPosAry.mnDestWidth, rPosAry.mnDestHeight); if (!source) { SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawAlphaBitmap case"); return; } #if 0 // LO code is not yet bitmap32-ready. // if m_bSupportsBitmap32 becomes true for Svp revisit this copyWithOperator(rPosAry, source, CAIRO_OPERATOR_OVER, bAntiAlias); #else copyWithOperator(rPosAry, source, CAIRO_OPERATOR_SOURCE, bAntiAlias); #endif } bool CairoCommon::drawAlphaBitmap(const SalTwoRect& rTR, const SalBitmap& rSourceBitmap, const SalBitmap& rAlphaBitmap, bool bAntiAlias) { if (rAlphaBitmap.GetBitCount() != 8 && rAlphaBitmap.GetBitCount() != 1) { SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawAlphaBitmap alpha depth case: " << rAlphaBitmap.GetBitCount()); return false; } if (!rTR.mnSrcWidth || !rTR.mnSrcHeight) { SAL_WARN("vcl.gdi", "not possible to stretch nothing"); return true; } // MM02 try to access buffered BitmapHelper std::shared_ptr aSurface; tryToUseSourceBuffer(rSourceBitmap, aSurface); cairo_surface_t* source = aSurface->getSurface(rTR.mnDestWidth, rTR.mnDestHeight); if (!source) { SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawAlphaBitmap case"); return false; } // MM02 try to access buffered MaskHelper std::shared_ptr aMask; tryToUseMaskBuffer(rAlphaBitmap, aMask); cairo_surface_t* mask = aMask->getSurface(rTR.mnDestWidth, rTR.mnDestHeight); if (!mask) { SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawAlphaBitmap case"); return false; } cairo_t* cr = getCairoContext(false, bAntiAlias); if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) { SAL_WARN("vcl.gdi", "cannot render to surface: " << cairo_status_to_string(cairo_status(cr))); releaseCairoContext(cr, false, basegfx::B2DRange()); return true; } clipRegion(cr); cairo_rectangle(cr, rTR.mnDestX, rTR.mnDestY, rTR.mnDestWidth, rTR.mnDestHeight); basegfx::B2DRange extents = getClippedFillDamage(cr); cairo_clip(cr); cairo_pattern_t* maskpattern = cairo_pattern_create_for_surface(mask); cairo_translate(cr, rTR.mnDestX, rTR.mnDestY); double fXScale = static_cast(rTR.mnDestWidth) / rTR.mnSrcWidth; double fYScale = static_cast(rTR.mnDestHeight) / rTR.mnSrcHeight; cairo_scale(cr, fXScale, fYScale); cairo_set_source_surface(cr, source, -rTR.mnSrcX, -rTR.mnSrcY); cairo_pattern_t* sourcepattern = cairo_get_source(cr); //tdf#133716 borders of upscaled images should not be blurred //tdf#114117 when stretching a single or multi pixel width/height source to fit an area //the image will be extended into that size. cairo_pattern_set_extend(sourcepattern, CAIRO_EXTEND_PAD); cairo_pattern_set_extend(maskpattern, CAIRO_EXTEND_PAD); //this block is just "cairo_mask_surface", but we have to make it explicit //because of the cairo_pattern_set_filter etc we may want applied cairo_matrix_t matrix; cairo_matrix_init_translate(&matrix, rTR.mnSrcX, rTR.mnSrcY); cairo_pattern_set_matrix(maskpattern, &matrix); cairo_mask(cr, maskpattern); cairo_pattern_destroy(maskpattern); releaseCairoContext(cr, false, extents); return true; } bool CairoCommon::drawTransformedBitmap(const basegfx::B2DPoint& rNull, const basegfx::B2DPoint& rX, const basegfx::B2DPoint& rY, const SalBitmap& rSourceBitmap, const SalBitmap* pAlphaBitmap, double fAlpha, bool bAntiAlias) { if (pAlphaBitmap && pAlphaBitmap->GetBitCount() != 8 && pAlphaBitmap->GetBitCount() != 1) { SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawTransformedBitmap alpha depth case: " << pAlphaBitmap->GetBitCount()); return false; } if (fAlpha != 1.0) return false; // MM02 try to access buffered BitmapHelper std::shared_ptr aSurface; tryToUseSourceBuffer(rSourceBitmap, aSurface); const tools::Long nDestWidth(basegfx::fround(basegfx::B2DVector(rX - rNull).getLength())); const tools::Long nDestHeight(basegfx::fround(basegfx::B2DVector(rY - rNull).getLength())); cairo_surface_t* source(aSurface->getSurface(nDestWidth, nDestHeight)); if (!source) { SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawTransformedBitmap case"); return false; } // MM02 try to access buffered MaskHelper std::shared_ptr aMask; if (nullptr != pAlphaBitmap) { tryToUseMaskBuffer(*pAlphaBitmap, aMask); } // access cairo_surface_t from MaskHelper cairo_surface_t* mask(nullptr); if (aMask) { mask = aMask->getSurface(nDestWidth, nDestHeight); } if (nullptr != pAlphaBitmap && nullptr == mask) { SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawTransformedBitmap case"); return false; } const Size aSize = rSourceBitmap.GetSize(); cairo_t* cr = getCairoContext(false, bAntiAlias); clipRegion(cr); // setup the image transformation // using the rNull,rX,rY points as destinations for the (0,0),(0,Width),(Height,0) source points const basegfx::B2DVector aXRel = rX - rNull; const basegfx::B2DVector aYRel = rY - rNull; cairo_matrix_t matrix; cairo_matrix_init(&matrix, aXRel.getX() / aSize.Width(), aXRel.getY() / aSize.Width(), aYRel.getX() / aSize.Height(), aYRel.getY() / aSize.Height(), rNull.getX(), rNull.getY()); cairo_transform(cr, &matrix); cairo_rectangle(cr, 0, 0, aSize.Width(), aSize.Height()); basegfx::B2DRange extents = getClippedFillDamage(cr); cairo_clip(cr); cairo_set_source_surface(cr, source, 0, 0); if (mask) cairo_mask_surface(cr, mask, 0, 0); else cairo_paint(cr); releaseCairoContext(cr, false, extents); return true; } void CairoCommon::drawMask(const SalTwoRect& rTR, const SalBitmap& rSalBitmap, Color nMaskColor, bool bAntiAlias) { /** creates an image from the given rectangle, replacing all black pixels * with nMaskColor and make all other full transparent */ // MM02 here decided *against* using buffered BitmapHelper // because the data gets somehow 'unmuliplied'. This may also be // done just once, but I am not sure if this is safe to do. // So for now dispense re-using data here. BitmapHelper aSurface(rSalBitmap, true); // The mask is argb32 if (!aSurface.getSurface()) { SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawMask case"); return; } sal_Int32 nStride; unsigned char* mask_data = aSurface.getBits(nStride); #if !ENABLE_WASM_STRIP_PREMULTIPLY vcl::bitmap::lookup_table const& unpremultiply_table = vcl::bitmap::get_unpremultiply_table(); #endif for (tools::Long y = rTR.mnSrcY; y < rTR.mnSrcY + rTR.mnSrcHeight; ++y) { unsigned char* row = mask_data + (nStride * y); unsigned char* data = row + (rTR.mnSrcX * 4); for (tools::Long x = rTR.mnSrcX; x < rTR.mnSrcX + rTR.mnSrcWidth; ++x) { sal_uInt8 a = data[SVP_CAIRO_ALPHA]; #if ENABLE_WASM_STRIP_PREMULTIPLY sal_uInt8 b = vcl::bitmap::unpremultiply(a, data[SVP_CAIRO_BLUE]); sal_uInt8 g = vcl::bitmap::unpremultiply(a, data[SVP_CAIRO_GREEN]); sal_uInt8 r = vcl::bitmap::unpremultiply(a, data[SVP_CAIRO_RED]); #else sal_uInt8 b = unpremultiply_table[a][data[SVP_CAIRO_BLUE]]; sal_uInt8 g = unpremultiply_table[a][data[SVP_CAIRO_GREEN]]; sal_uInt8 r = unpremultiply_table[a][data[SVP_CAIRO_RED]]; #endif if (r == 0 && g == 0 && b == 0) { data[0] = nMaskColor.GetBlue(); data[1] = nMaskColor.GetGreen(); data[2] = nMaskColor.GetRed(); data[3] = 0xff; } else { data[0] = 0; data[1] = 0; data[2] = 0; data[3] = 0; } data += 4; } } aSurface.mark_dirty(); cairo_t* cr = getCairoContext(false, bAntiAlias); clipRegion(cr); cairo_rectangle(cr, rTR.mnDestX, rTR.mnDestY, rTR.mnDestWidth, rTR.mnDestHeight); basegfx::B2DRange extents = getClippedFillDamage(cr); cairo_clip(cr); cairo_translate(cr, rTR.mnDestX, rTR.mnDestY); double fXScale = static_cast(rTR.mnDestWidth) / rTR.mnSrcWidth; double fYScale = static_cast(rTR.mnDestHeight) / rTR.mnSrcHeight; cairo_scale(cr, fXScale, fYScale); cairo_set_source_surface(cr, aSurface.getSurface(), -rTR.mnSrcX, -rTR.mnSrcY); if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) { //tdf#133716 borders of upscaled images should not be blurred cairo_pattern_t* sourcepattern = cairo_get_source(cr); cairo_pattern_set_extend(sourcepattern, CAIRO_EXTEND_PAD); } cairo_paint(cr); releaseCairoContext(cr, false, extents); } std::shared_ptr CairoCommon::getBitmap(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight) { std::shared_ptr pBitmap = std::make_shared(); BitmapPalette aPal; assert(GetBitCount() != 1 && "not supported anymore"); vcl::PixelFormat ePixelFormat = vcl::PixelFormat::N32_BPP; if (!pBitmap->ImplCreate(Size(nWidth, nHeight), ePixelFormat, aPal, false)) { SAL_WARN("vcl.gdi", "SvpSalGraphics::getBitmap, cannot create bitmap"); return nullptr; } cairo_surface_t* target = CairoCommon::createCairoSurface(pBitmap->GetBuffer()); if (!target) { SAL_WARN("vcl.gdi", "SvpSalGraphics::getBitmap, cannot create cairo surface"); return nullptr; } cairo_t* cr = cairo_create(target); SalTwoRect aTR(nX, nY, nWidth, nHeight, 0, 0, nWidth, nHeight); CairoCommon::renderSource(cr, aTR, m_pSurface); cairo_destroy(cr); cairo_surface_destroy(target); return pBitmap; } cairo_format_t getCairoFormat(const BitmapBuffer& rBuffer) { cairo_format_t nFormat; #ifdef HAVE_CAIRO_FORMAT_RGB24_888 assert(rBuffer.mnBitCount == 32 || rBuffer.mnBitCount == 24 || rBuffer.mnBitCount == 1); #else assert(rBuffer.mnBitCount == 32 || rBuffer.mnBitCount == 1); #endif if (rBuffer.mnBitCount == 32) nFormat = CAIRO_FORMAT_ARGB32; #ifdef HAVE_CAIRO_FORMAT_RGB24_888 else if (rBuffer.mnBitCount == 24) nFormat = CAIRO_FORMAT_RGB24_888; #endif else nFormat = CAIRO_FORMAT_A1; return nFormat; } namespace { bool isCairoCompatible(const BitmapBuffer* pBuffer) { if (!pBuffer) return false; // We use Cairo that supports 24-bit RGB. #ifdef HAVE_CAIRO_FORMAT_RGB24_888 if (pBuffer->mnBitCount != 32 && pBuffer->mnBitCount != 24 && pBuffer->mnBitCount != 1) #else if (pBuffer->mnBitCount != 32 && pBuffer->mnBitCount != 1) #endif return false; cairo_format_t nFormat = getCairoFormat(*pBuffer); return (cairo_format_stride_for_width(nFormat, pBuffer->mnWidth) == pBuffer->mnScanlineSize); } } cairo_surface_t* CairoCommon::createCairoSurface(const BitmapBuffer* pBuffer) { if (!isCairoCompatible(pBuffer)) return nullptr; cairo_format_t nFormat = getCairoFormat(*pBuffer); cairo_surface_t* target = cairo_image_surface_create_for_data( pBuffer->mpBits, nFormat, pBuffer->mnWidth, pBuffer->mnHeight, pBuffer->mnScanlineSize); if (cairo_surface_status(target) != CAIRO_STATUS_SUCCESS) { cairo_surface_destroy(target); return nullptr; } return target; } bool CairoCommon::hasFastDrawTransformedBitmap() { return false; } bool CairoCommon::supportsOperation(OutDevSupportType eType) { switch (eType) { case OutDevSupportType::TransparentRect: case OutDevSupportType::TransparentText: return true; } return false; } std::optional FastConvert24BitRgbTo32BitCairo(const BitmapBuffer* pSrc) { if (pSrc == nullptr) return std::nullopt; assert(pSrc->meFormat == SVP_24BIT_FORMAT); const tools::Long nWidth = pSrc->mnWidth; const tools::Long nHeight = pSrc->mnHeight; std::optional pDst(std::in_place); pDst->meFormat = ScanlineFormat::N32BitTcArgb; pDst->meDirection = ScanlineDirection::TopDown; pDst->mnWidth = nWidth; pDst->mnHeight = nHeight; pDst->mnBitCount = 32; pDst->maColorMask = pSrc->maColorMask; pDst->maPalette = pSrc->maPalette; tools::Long nScanlineBase; const bool bFail = o3tl::checked_multiply(pDst->mnBitCount, nWidth, nScanlineBase); if (bFail) { SAL_WARN("vcl.gdi", "checked multiply failed"); pDst->mpBits = nullptr; return std::nullopt; } pDst->mnScanlineSize = AlignedWidth4Bytes(nScanlineBase); if (pDst->mnScanlineSize < nScanlineBase / 8) { SAL_WARN("vcl.gdi", "scanline calculation wraparound"); pDst->mpBits = nullptr; return std::nullopt; } try { pDst->mpBits = new sal_uInt8[pDst->mnScanlineSize * nHeight]; } catch (const std::bad_alloc&) { // memory exception, clean up pDst->mpBits = nullptr; return std::nullopt; } for (tools::Long y = 0; y < nHeight; ++y) { sal_uInt8* pS = pSrc->mpBits + y * pSrc->mnScanlineSize; sal_uInt8* pD = pDst->mpBits + y * pDst->mnScanlineSize; for (tools::Long x = 0; x < nWidth; ++x) { #if ENABLE_CAIRO_RGBA static_assert(SVP_CAIRO_FORMAT == ScanlineFormat::N32BitTcRgba, "Expected SVP_CAIRO_FORMAT set to N32BitTcBgra"); static_assert(SVP_24BIT_FORMAT == ScanlineFormat::N24BitTcRgb, "Expected SVP_24BIT_FORMAT set to N24BitTcRgb"); pD[0] = pS[0]; pD[1] = pS[1]; pD[2] = pS[2]; pD[3] = 0xff; // Alpha #elif defined OSL_BIGENDIAN static_assert(SVP_CAIRO_FORMAT == ScanlineFormat::N32BitTcArgb, "Expected SVP_CAIRO_FORMAT set to N32BitTcBgra"); static_assert(SVP_24BIT_FORMAT == ScanlineFormat::N24BitTcRgb, "Expected SVP_24BIT_FORMAT set to N24BitTcRgb"); pD[0] = 0xff; // Alpha pD[1] = pS[0]; pD[2] = pS[1]; pD[3] = pS[2]; #else static_assert(SVP_CAIRO_FORMAT == ScanlineFormat::N32BitTcBgra, "Expected SVP_CAIRO_FORMAT set to N32BitTcBgra"); static_assert(SVP_24BIT_FORMAT == ScanlineFormat::N24BitTcBgr, "Expected SVP_24BIT_FORMAT set to N24BitTcBgr"); pD[0] = pS[0]; pD[1] = pS[1]; pD[2] = pS[2]; pD[3] = 0xff; // Alpha #endif pS += 3; pD += 4; } } return pDst; } namespace { // check for env var that decides for using downscale pattern const char* pDisableDownScale(getenv("SAL_DISABLE_CAIRO_DOWNSCALE")); bool bDisableDownScale(nullptr != pDisableDownScale); } cairo_surface_t* SurfaceHelper::implCreateOrReuseDownscale(unsigned long nTargetWidth, unsigned long nTargetHeight) { const unsigned long nSourceWidth(cairo_image_surface_get_width(pSurface)); const unsigned long nSourceHeight(cairo_image_surface_get_height(pSurface)); // zoomed in, need to stretch at paint, no pre-scale useful if (nTargetWidth >= nSourceWidth || nTargetHeight >= nSourceHeight) { return pSurface; } // calculate downscale factor unsigned long nWFactor(1); unsigned long nW((nSourceWidth + 1) / 2); unsigned long nHFactor(1); unsigned long nH((nSourceHeight + 1) / 2); while (nW > nTargetWidth && nW > 1) { nW = (nW + 1) / 2; nWFactor *= 2; } while (nH > nTargetHeight && nH > 1) { nH = (nH + 1) / 2; nHFactor *= 2; } if (1 == nWFactor && 1 == nHFactor) { // original size *is* best binary size, use it return pSurface; } // go up one scale again - look for no change nW = (1 == nWFactor) ? nTargetWidth : nW * 2; nH = (1 == nHFactor) ? nTargetHeight : nH * 2; // check if we have a downscaled version of required size // bail out if the multiplication for the key would overflow if (nW >= SAL_MAX_UINT32 || nH >= SAL_MAX_UINT32) return pSurface; const sal_uInt64 key((nW * static_cast(SAL_MAX_UINT32)) + nH); auto isHit(maDownscaled.find(key)); if (isHit != maDownscaled.end()) { return isHit->second; } // create new surface in the targeted size cairo_surface_t* pSurfaceTarget = cairo_surface_create_similar(pSurface, cairo_surface_get_content(pSurface), nW, nH); // made a version to scale self first that worked well, but would've // been hard to support CAIRO_FORMAT_A1 including bit shifting, so // I decided to go with cairo itself - use CAIRO_FILTER_FAST or // CAIRO_FILTER_GOOD though. Please modify as needed for // performance/quality cairo_t* cr = cairo_create(pSurfaceTarget); const double fScaleX(static_cast(nW) / static_cast(nSourceWidth)); const double fScaleY(static_cast(nH) / static_cast(nSourceHeight)); cairo_scale(cr, fScaleX, fScaleY); cairo_set_source_surface(cr, pSurface, 0.0, 0.0); cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_GOOD); cairo_paint(cr); cairo_destroy(cr); // need to set device_scale for downscale surfaces to get // them handled correctly cairo_surface_set_device_scale(pSurfaceTarget, fScaleX, fScaleY); // add entry to cached entries maDownscaled[key] = pSurfaceTarget; return pSurfaceTarget; } bool SurfaceHelper::isTrivial() const { constexpr unsigned long nMinimalSquareSizeToBuffer(64 * 64); const unsigned long nSourceWidth(cairo_image_surface_get_width(pSurface)); const unsigned long nSourceHeight(cairo_image_surface_get_height(pSurface)); return nSourceWidth * nSourceHeight < nMinimalSquareSizeToBuffer; } SurfaceHelper::SurfaceHelper() : pSurface(nullptr) { } SurfaceHelper::~SurfaceHelper() { cairo_surface_destroy(pSurface); for (auto& candidate : maDownscaled) { cairo_surface_destroy(candidate.second); } } cairo_surface_t* SurfaceHelper::getSurface(unsigned long nTargetWidth, unsigned long nTargetHeight) const { if (bDisableDownScale || 0 == nTargetWidth || 0 == nTargetHeight || !pSurface || isTrivial()) { // caller asks for original or disabled or trivial (smaller then a minimal square size) // also excludes zero cases for width/height after this point if need to prescale return pSurface; } return const_cast(this)->implCreateOrReuseDownscale(nTargetWidth, nTargetHeight); } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */