drawinglayer/source/processor2d/cairopixelprocessor2d.cxx | 489 +++++++++---- include/drawinglayer/processor2d/cairopixelprocessor2d.hxx | 12 2 files changed, 359 insertions(+), 142 deletions(-)
New commits: commit 6c61231e8c4b0a47fd626745b27bfd42777b90db Author: Armin Le Grand (Collabora) <armin.le.gr...@me.com> AuthorDate: Tue Jan 28 21:02:25 2025 +0100 Commit: Xisco Fauli <xiscofa...@libreoffice.org> CommitDate: Fri Feb 14 09:49:24 2025 +0100 tdf#164403 CairoSDPR: adapt hairline width If Cairo Coordinate Limit Workaround is Active do also adapt hairline width to 1.0 since we (have to) fallback to render in view coordinates. Change-Id: I2216c1728355b5e76ee04f699193a896f9c4fd4b Reviewed-on: https://gerrit.libreoffice.org/c/core/+/180860 Tested-by: Jenkins Reviewed-by: Armin Le Grand <armin.le.gr...@me.com> Signed-off-by: Xisco Fauli <xiscofa...@libreoffice.org> Reviewed-on: https://gerrit.libreoffice.org/c/core/+/181550 diff --git a/drawinglayer/source/processor2d/cairopixelprocessor2d.cxx b/drawinglayer/source/processor2d/cairopixelprocessor2d.cxx index a7ae62e85911..36bb4bb1b3b9 100644 --- a/drawinglayer/source/processor2d/cairopixelprocessor2d.cxx +++ b/drawinglayer/source/processor2d/cairopixelprocessor2d.cxx @@ -65,7 +65,8 @@ using namespace com::sun::star; namespace { void impl_cairo_set_hairline(cairo_t* pRT, - const drawinglayer::geometry::ViewInformation2D& rViewInformation) + const drawinglayer::geometry::ViewInformation2D& rViewInformation, + bool bCairoCoordinateLimitWorkaroundActive) { #if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 18, 0) void* addr(dlsym(nullptr, "cairo_set_hairline")); @@ -75,11 +76,19 @@ void impl_cairo_set_hairline(cairo_t* pRT, return; } #endif - // avoid cairo_device_to_user_distance, see note on that below - const double fPx( - (rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(1.0, 0.0)) - .getLength()); - cairo_set_line_width(pRT, fPx); + if (bCairoCoordinateLimitWorkaroundActive) + { + // we have to render in view coordiantes, set line width to 1.0 + cairo_set_line_width(pRT, 1.0); + } + else + { + // avoid cairo_device_to_user_distance, see note on that below + const double fPx( + (rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(1.0, 0.0)) + .getLength()); + cairo_set_line_width(pRT, fPx); + } } void addB2DPolygonToPathGeometry(cairo_t* pRT, const basegfx::B2DPolygon& rPolygon) @@ -1269,7 +1278,8 @@ void CairoPixelProcessor2D::paintBitmapAlpha(const BitmapEx& rBitmapEx, if (bRenderTransformationBounds) { cairo_set_source_rgba(mpRT, 1, 0, 0, 0.8); - impl_cairo_set_hairline(mpRT, getViewInformation2D()); + impl_cairo_set_hairline(mpRT, getViewInformation2D(), + isCairoCoordinateLimitWorkaroundActive()); cairo_rectangle(mpRT, 0, 0, 1, 1); cairo_stroke(mpRT); } @@ -1404,7 +1414,7 @@ void CairoPixelProcessor2D::processPolygonHairlinePrimitive2D( aHairlineColor.getBlue()); // set LineWidth, use Cairo's special cairo_set_hairline - impl_cairo_set_hairline(mpRT, getViewInformation2D()); + impl_cairo_set_hairline(mpRT, getViewInformation2D(), isCairoCoordinateLimitWorkaroundActive()); if (isCairoCoordinateLimitWorkaroundActive()) { @@ -2429,7 +2439,8 @@ void CairoPixelProcessor2D::processFillGraphicPrimitive2D( if (bRenderTransformationBounds) { cairo_set_source_rgba(mpRT, 0, 1, 0, 0.8); - impl_cairo_set_hairline(mpRT, getViewInformation2D()); + impl_cairo_set_hairline(mpRT, getViewInformation2D(), + isCairoCoordinateLimitWorkaroundActive()); // full object cairo_rectangle(mpRT, 0, 0, 1, 1); // outline of pattern root image commit b2aa20a855b4cdd2ff32752c71bd6dd412ddea0b Author: Armin Le Grand (Collabora) <armin.le.gr...@me.com> AuthorDate: Mon Jan 20 19:59:07 2025 +0100 Commit: Xisco Fauli <xiscofa...@libreoffice.org> CommitDate: Fri Feb 14 09:49:17 2025 +0100 tdf#164403 CairoSDPR: Solve Cairo's 24.8 coordinate problem Thus Cairo is offering a full-fledged double precision API but internally uses a 24.8 fixed-float format (see https://gitlab. freedesktop.org/cairo/cairo/-/issues/252, thanks Caolan for finding that). This seems to be the case for linux and windows implementations. Due to the API being on double precision this is an implementation detail, but it has to be taken into account when using cairo. It is not officially documented what is a shame - it should be somewhere in the readmes/ documentation at a prominent place. 24.8 means coordinates are limited to +/- 2^23 (usually minus one) and [0/256 .. 255/256] places after comma. This is unfortunately not limited to the view coordinates - that would be acceptable, discrete/pixel areas of that size should be fine, BUT used with transformations. Thus when having any coordinates beyond that limit - and with double precision and transformations we have that here - cairo just 'cuts' the coordinates to it's min/max 24.8 format. This is not acceptable for e.g. a cad system, and even leads to problems in something like an office suite as we see. It implies that using cairo in a safe way means to basically use linear algebra yourself in your app and use cairo only without transformations and with discrete view coordinates (pixels). AND to know about that limitation in the 1st place ... Well, cairo works well and that problem happens until now in one place in writer below page 490. Let's be pragmatic. Key is to *detect* the situation. Always doing own transformations is possible, but not nice: I *could* always transform the polygon to screen coordinates, but that will kill the PolyPolygon buffering mechanism (in chart we have PolyPolygons with 80.000 entries :-); These are held in cairo_path_t form at the original PolyPolygon to not need to transform them every time, but let Cairo do that. The idea is that when Cairo impl may use HW on the system to do that (HW can do that nowadays) while rendering the PolyPolygon from the object model would have to be converted just once to Cairo form and used multiple times... Cairo *should* be fair, document this and allow you to test/ ask if this limit exists in the version you are using, but it does not. Thus I suppose to detect that situation as 'cheap' as possible and react to it - e.g. in writer when below page 490. But not only - it might be possibe to construct draw/impress pages (free definable) that also reach that limit. With detecting as cheap as possible I mean: Naturally you would start to test your geometry to be painted if it is overlapping or outside that range defined by 24.8, but that would already involve transforming all geometry by world and view transformations. I think it should be okay to just take the target area (starting at 0,0 and having a size in pixels) and transforming that back to logical world coordinates. Then this needs to be checked only if view transformation changes, not object transformation. Usually view transformation is setup and all/many objects are painted to it using object transformations. Detection should then only be needed when view transform changes. Thus I added a test (see checkCoordinateLimitWorkaroundNeededForUsedCairo()). This is esecuted once per office runtime. If it detects that the problem is there it sets a flag. Depending on this the view coordinates are then transformed to logic coordinates and checked if these are completely inside the coordinate area that ciaro *can* correctly handle. This has to be done once per CariroSDPR since (currently) ViewTransformation is not changed (has to be adapted if this should happen one day). Based on that the single helpers on the renderer have to be adapted: Simplest is just to render using ViewCoordinates - and do transform object geometry using basegfx/our own stuff that does that correctly (with ease :-). Done that, checked, works AFAICT. What a bad surprise, but I can work around it... Change-Id: Ic33d6ccfe9854af396d0ac9f14c76eb25bb9e4e2 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/180529 Reviewed-by: Armin Le Grand <armin.le.gr...@me.com> Tested-by: Jenkins Signed-off-by: Xisco Fauli <xiscofa...@libreoffice.org> Reviewed-on: https://gerrit.libreoffice.org/c/core/+/181549 diff --git a/drawinglayer/source/processor2d/cairopixelprocessor2d.cxx b/drawinglayer/source/processor2d/cairopixelprocessor2d.cxx index 588afe4b158f..a7ae62e85911 100644 --- a/drawinglayer/source/processor2d/cairopixelprocessor2d.cxx +++ b/drawinglayer/source/processor2d/cairopixelprocessor2d.cxx @@ -860,6 +860,71 @@ basegfx::B2DRange getDiscreteViewRange(cairo_t* pRT) return basegfx::B2DRange(basegfx::B2DPoint(clip_x1, clip_y1), basegfx::B2DPoint(clip_x2, clip_y2)); } + +bool checkCoordinateLimitWorkaroundNeededForUsedCairo() +{ + // setup surface and render context + cairo_surface_t* pSurface(cairo_image_surface_create(CAIRO_FORMAT_RGB24, 8, 8)); + if (!pSurface) + // got no surface -> be pessimistic + return true; + + cairo_t* pRender(cairo_create(pSurface)); + if (!pRender) + { + // got no render -> be pessimistic + cairo_surface_destroy(pSurface); + return true; + } + + // set basic values + cairo_set_antialias(pRender, CAIRO_ANTIALIAS_NONE); + cairo_set_fill_rule(pRender, CAIRO_FILL_RULE_EVEN_ODD); + cairo_set_operator(pRender, CAIRO_OPERATOR_OVER); + cairo_set_source_rgb(pRender, 1.0, 0.0, 0.0); + + // create a to-be rendered area centered at the fNumCairoMax + // spot and 8x8 discrete units in size + constexpr double fNumCairoMax(1 << 23); + const basegfx::B2DPoint aCenter(fNumCairoMax, fNumCairoMax); + const basegfx::B2DPoint aOffset(4, 4); + const basegfx::B2DRange aObject(aCenter - aOffset, aCenter + aOffset); + + // create transformation to render that to an aerea with + // range(0, 0, 8, 8) and set as transformation + const basegfx::B2DHomMatrix aObjectToView(basegfx::utils::createSourceRangeTargetRangeTransform( + aObject, basegfx::B2DRange(0, 0, 8, 8))); + cairo_matrix_t aMatrix; + cairo_matrix_init(&aMatrix, aObjectToView.a(), aObjectToView.b(), aObjectToView.c(), + aObjectToView.d(), aObjectToView.e(), aObjectToView.f()); + cairo_set_matrix(pRender, &aMatrix); + + // get/create the path for an object exactly filling that area + cairo_new_path(pRender); + basegfx::B2DPolyPolygon aObjectPolygon(basegfx::utils::createPolygonFromRect(aObject)); + CairoPathHelper aPathHelper(aObjectPolygon); + cairo_append_path(pRender, aPathHelper.getCairoPath()); + + // render it and flush since we want to immediately inspect result + cairo_fill(pRender); + cairo_surface_flush(pSurface); + + // get access to pixel data + const sal_uInt32 nStride(cairo_image_surface_get_stride(pSurface)); + sal_uInt8* pStartPixelData(cairo_image_surface_get_data(pSurface)); + + // extract red value for pixels at (1,1) and (7,7) + sal_uInt8 aRedAt_1_1((pStartPixelData + (nStride * 1) + 1)[SVP_CAIRO_RED]); + sal_uInt8 aRedAt_6_6((pStartPixelData + (nStride * 6) + 6)[SVP_CAIRO_RED]); + + // cleanup + cairo_destroy(pRender); + cairo_surface_destroy(pSurface); + + // if cairo works or has no 24.8 internal format all pixels + // have to be red (255), thus workaround is needed if != + return aRedAt_1_1 != aRedAt_6_6; +} } namespace drawinglayer::processor2d @@ -876,6 +941,7 @@ CairoPixelProcessor2D::CairoPixelProcessor2D(const geometry::ViewInformation2D& , mbRenderDecoratedTextDirect( officecfg::Office::Common::Drawinglayer::RenderDecoratedTextDirect::get()) , mnClipRecursionCount(0) + , mbCairoCoordinateLimitWorkaroundActive(false) { if (nWidthPixel <= 0 || nHeightPixel <= 0) // no size, invalid @@ -900,6 +966,9 @@ CairoPixelProcessor2D::CairoPixelProcessor2D(const geometry::ViewInformation2D& : CAIRO_ANTIALIAS_NONE); cairo_set_fill_rule(mpRT, CAIRO_FILL_RULE_EVEN_ODD); cairo_set_operator(mpRT, CAIRO_OPERATOR_OVER); + + // evaluate if CairoCoordinateLimitWorkaround is needed + evaluateCairoCoordinateLimitWorkaround(); } CairoPixelProcessor2D::CairoPixelProcessor2D(const geometry::ViewInformation2D& rViewInformation, @@ -915,59 +984,63 @@ CairoPixelProcessor2D::CairoPixelProcessor2D(const geometry::ViewInformation2D& , mbRenderDecoratedTextDirect( officecfg::Office::Common::Drawinglayer::RenderDecoratedTextDirect::get()) , mnClipRecursionCount(0) + , mbCairoCoordinateLimitWorkaroundActive(false) { - if (pTarget) - { - bool bClipNeeded(false); + // no target, nothing to initialize + if (nullptr == pTarget) + return; - if (0 != nOffsetPixelX || 0 != nOffsetPixelY || 0 != nWidthPixel || 0 != nHeightPixel) + bool bClipNeeded(false); + + if (0 != nOffsetPixelX || 0 != nOffsetPixelY || 0 != nWidthPixel || 0 != nHeightPixel) + { + if (0 != nOffsetPixelX || 0 != nOffsetPixelY) { - if (0 != nOffsetPixelX || 0 != nOffsetPixelY) + // if offset is used we need initial clip + bClipNeeded = true; + } + else + { + // no offset used, compare to real pixel size + const tools::Long nRealPixelWidth(cairo_image_surface_get_width(pTarget)); + const tools::Long nRealPixelHeight(cairo_image_surface_get_height(pTarget)); + + if (nRealPixelWidth != nWidthPixel || nRealPixelHeight != nHeightPixel) { - // if offset is used we need initial clip + // if size differs we need initial clip bClipNeeded = true; } - else - { - // no offset used, compare to real pixel size - const tools::Long nRealPixelWidth(cairo_image_surface_get_width(pTarget)); - const tools::Long nRealPixelHeight(cairo_image_surface_get_height(pTarget)); - - if (nRealPixelWidth != nWidthPixel || nRealPixelHeight != nHeightPixel) - { - // if size differs we need initial clip - bClipNeeded = true; - } - } } + } - if (bClipNeeded) - { - // optional: if the possibility to add an initial clip relative - // to the real pixel dimensions of the target surface is used, - // apply it here using that nice existing method of cairo - mpOwnedSurface = cairo_surface_create_for_rectangle( - pTarget, nOffsetPixelX, nOffsetPixelY, nWidthPixel, nHeightPixel); + if (bClipNeeded) + { + // optional: if the possibility to add an initial clip relative + // to the real pixel dimensions of the target surface is used, + // apply it here using that nice existing method of cairo + mpOwnedSurface = cairo_surface_create_for_rectangle(pTarget, nOffsetPixelX, nOffsetPixelY, + nWidthPixel, nHeightPixel); - if (nullptr != mpOwnedSurface) - mpRT = cairo_create(mpOwnedSurface); - } - else - { - // create RenderTarget for full target - mpRT = cairo_create(pTarget); - } + if (nullptr != mpOwnedSurface) + mpRT = cairo_create(mpOwnedSurface); + } + else + { + // create RenderTarget for full target + mpRT = cairo_create(pTarget); + } - if (nullptr != mpRT) - { - // initialize some basic used values/settings - cairo_set_antialias(mpRT, rViewInformation.getUseAntiAliasing() - ? CAIRO_ANTIALIAS_DEFAULT - : CAIRO_ANTIALIAS_NONE); - cairo_set_fill_rule(mpRT, CAIRO_FILL_RULE_EVEN_ODD); - cairo_set_operator(mpRT, CAIRO_OPERATOR_OVER); - } + if (nullptr != mpRT) + { + // initialize some basic used values/settings + cairo_set_antialias(mpRT, rViewInformation.getUseAntiAliasing() ? CAIRO_ANTIALIAS_DEFAULT + : CAIRO_ANTIALIAS_NONE); + cairo_set_fill_rule(mpRT, CAIRO_FILL_RULE_EVEN_ODD); + cairo_set_operator(mpRT, CAIRO_OPERATOR_OVER); } + + // evaluate if CairoCoordinateLimitWorkaround is needed + evaluateCairoCoordinateLimitWorkaround(); } CairoPixelProcessor2D::~CairoPixelProcessor2D() @@ -1324,16 +1397,6 @@ void CairoPixelProcessor2D::processPolygonHairlinePrimitive2D( cairo_save(mpRT); - // set linear transformation - cairo_matrix_t aMatrix; - const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0); - const basegfx::B2DHomMatrix& rObjectToView( - getViewInformation2D().getObjectToViewTransformation()); - cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), - rObjectToView.d(), rObjectToView.e() + fAAOffset, - rObjectToView.f() + fAAOffset); - cairo_set_matrix(mpRT, &aMatrix); - // determine & set color const basegfx::BColor aHairlineColor( maBColorModifierStack.getModifiedColor(rPolygonHairlinePrimitive2D.getBColor())); @@ -1343,11 +1406,40 @@ void CairoPixelProcessor2D::processPolygonHairlinePrimitive2D( // set LineWidth, use Cairo's special cairo_set_hairline impl_cairo_set_hairline(mpRT, getViewInformation2D()); - // get PathGeometry & paint it - cairo_new_path(mpRT); - getOrCreatePathGeometry(mpRT, rPolygon, getViewInformation2D(), - getViewInformation2D().getUseAntiAliasing()); - cairo_stroke(mpRT); + if (isCairoCoordinateLimitWorkaroundActive()) + { + // need to fallback to paint in view coordinates, unfortunately + // need to transform self (cairo will do it wrong in this coordinate + // space), so no need to try to buffer + cairo_new_path(mpRT); + basegfx::B2DPolygon aAdaptedPolygon(rPolygon); + const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0); + aAdaptedPolygon.transform(basegfx::utils::createTranslateB2DHomMatrix(fAAOffset, fAAOffset) + * getViewInformation2D().getObjectToViewTransformation()); + cairo_identity_matrix(mpRT); + addB2DPolygonToPathGeometry(mpRT, aAdaptedPolygon); + cairo_stroke(mpRT); + } + else + { + // set linear transformation. use own, prepared, re-usable + // ObjectToViewTransformation and PolyPoylgon data and let + // cairo do the transformations + cairo_matrix_t aMatrix; + const basegfx::B2DHomMatrix& rObjectToView( + getViewInformation2D().getObjectToViewTransformation()); + const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0); + cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), + rObjectToView.d(), rObjectToView.e() + fAAOffset, + rObjectToView.f() + fAAOffset); + cairo_set_matrix(mpRT, &aMatrix); + + // get PathGeometry & paint it + cairo_new_path(mpRT); + getOrCreatePathGeometry(mpRT, rPolygon, getViewInformation2D(), + getViewInformation2D().getUseAntiAliasing()); + cairo_stroke(mpRT); + } cairo_restore(mpRT); } @@ -1379,14 +1471,6 @@ void CairoPixelProcessor2D::paintPolyPoylgonRGBA(const basegfx::B2DPolyPolygon& cairo_save(mpRT); - // set linear transformation - cairo_matrix_t aMatrix; - const basegfx::B2DHomMatrix& rObjectToView( - getViewInformation2D().getObjectToViewTransformation()); - cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), - rObjectToView.d(), rObjectToView.e(), rObjectToView.f()); - cairo_set_matrix(mpRT, &aMatrix); - // determine & set color const basegfx::BColor aFillColor(maBColorModifierStack.getModifiedColor(rColor)); @@ -1397,10 +1481,36 @@ void CairoPixelProcessor2D::paintPolyPoylgonRGBA(const basegfx::B2DPolyPolygon& cairo_set_source_rgb(mpRT, aFillColor.getRed(), aFillColor.getGreen(), aFillColor.getBlue()); - // get PathGeometry & paint it - cairo_new_path(mpRT); - getOrCreateFillGeometry(mpRT, rPolyPolygon); - cairo_fill(mpRT); + if (isCairoCoordinateLimitWorkaroundActive()) + { + // need to fallback to paint in view coordinates, unfortunately + // need to transform self (cairo will do it wrong in this coordinate + // space), so no need to try to buffer + cairo_new_path(mpRT); + basegfx::B2DPolyPolygon aAdaptedPolyPolygon(rPolyPolygon); + aAdaptedPolyPolygon.transform(getViewInformation2D().getObjectToViewTransformation()); + cairo_identity_matrix(mpRT); + for (const auto& rPolygon : aAdaptedPolyPolygon) + addB2DPolygonToPathGeometry(mpRT, rPolygon); + cairo_fill(mpRT); + } + else + { + // set linear transformation. use own, prepared, re-usable + // ObjectToViewTransformation and PolyPoylgon data and let + // cairo do the transformations + cairo_matrix_t aMatrix; + const basegfx::B2DHomMatrix& rObjectToView( + getViewInformation2D().getObjectToViewTransformation()); + cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), + rObjectToView.d(), rObjectToView.e(), rObjectToView.f()); + cairo_set_matrix(mpRT, &aMatrix); + + // get PathGeometry & paint it + cairo_new_path(mpRT); + getOrCreateFillGeometry(mpRT, rPolyPolygon); + cairo_fill(mpRT); + } cairo_restore(mpRT); } @@ -1678,24 +1788,41 @@ void CairoPixelProcessor2D::processMaskPrimitive2D( cairo_save(mpRT); - // set linear transformation for applying mask. use no fAAOffset for mask - cairo_matrix_t aMatrix; - const basegfx::B2DHomMatrix& rObjectToView( - getViewInformation2D().getObjectToViewTransformation()); - cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), - rObjectToView.d(), rObjectToView.e(), rObjectToView.f()); - cairo_set_matrix(mpRT, &aMatrix); + if (isCairoCoordinateLimitWorkaroundActive()) + { + // need to fallback to paint in view coordinates, unfortunately + // need to transform self (cairo will do it wrong in this coordinate + // space), so no need to try to buffer + cairo_new_path(mpRT); + basegfx::B2DPolyPolygon aAdaptedPolyPolygon(rMask); + aAdaptedPolyPolygon.transform(getViewInformation2D().getObjectToViewTransformation()); + for (const auto& rPolygon : aAdaptedPolyPolygon) + addB2DPolygonToPathGeometry(mpRT, rPolygon); - // create path geometry and put mask as path - cairo_new_path(mpRT); - getOrCreateFillGeometry(mpRT, rMask); + // clip to this mask + cairo_clip(mpRT); + } + else + { + // set linear transformation for applying mask. use no fAAOffset for mask + cairo_matrix_t aMatrix; + const basegfx::B2DHomMatrix& rObjectToView( + getViewInformation2D().getObjectToViewTransformation()); + cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), + rObjectToView.d(), rObjectToView.e(), rObjectToView.f()); + cairo_set_matrix(mpRT, &aMatrix); - // clip to this mask - cairo_clip(mpRT); + // create path geometry and put mask as path + cairo_new_path(mpRT); + getOrCreateFillGeometry(mpRT, rMask); - // reset transformation to not have it set when processing - // child content below (was only used to set clip path) - cairo_identity_matrix(mpRT); + // clip to this mask + cairo_clip(mpRT); + + // reset transformation to not have it set when processing + // child content below (was only used to set clip path) + cairo_identity_matrix(mpRT); + } // process sub-content (that shall be masked) mnClipRecursionCount++; @@ -1957,14 +2084,6 @@ void CairoPixelProcessor2D::processPolygonStrokePrimitive2D( cairo_save(mpRT); - // set linear transformation - cairo_matrix_t aMatrix; - const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0); - cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), - rObjectToView.d(), rObjectToView.e() + fAAOffset, - rObjectToView.f() + fAAOffset); - cairo_set_matrix(mpRT, &aMatrix); - // setup line attributes cairo_line_join_t eCairoLineJoin = CAIRO_LINE_JOIN_MITER; switch (rLineAttribute.getLineJoin()) @@ -2015,33 +2134,77 @@ void CairoPixelProcessor2D::processPolygonStrokePrimitive2D( aLineColor.setRed(0.5); cairo_set_source_rgb(mpRT, aLineColor.getRed(), aLineColor.getGreen(), aLineColor.getBlue()); - // process/set LineWidth - const double fObjectLineWidth( - bHairline ? (getViewInformation2D().getInverseObjectToViewTransformation() - * basegfx::B2DVector(1.0, 0.0)) - .getLength() - : rLineAttribute.getWidth()); - cairo_set_line_width(mpRT, fObjectLineWidth); - // check stroke const attribute::StrokeAttribute& rStrokeAttribute( rPolygonStrokeCandidate.getStrokeAttribute()); const bool bDashUsed(!rStrokeAttribute.isDefault() && !rStrokeAttribute.getDotDashArray().empty() && 0.0 < rStrokeAttribute.getFullDotDashLen()); - if (bDashUsed) - { - const std::vector<double>& rStroke = rStrokeAttribute.getDotDashArray(); - cairo_set_dash(mpRT, rStroke.data(), rStroke.size(), 0.0); + if (isCairoCoordinateLimitWorkaroundActive()) + { + // need to fallback to paint in view coordinates, unfortunately + // need to transform self (cairo will do it wrong in this coordinate + // space), so no need to try to buffer + cairo_new_path(mpRT); + basegfx::B2DPolygon aAdaptedPolygon(rPolygon); + const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0); + aAdaptedPolygon.transform(basegfx::utils::createTranslateB2DHomMatrix(fAAOffset, fAAOffset) + * getViewInformation2D().getObjectToViewTransformation()); + cairo_identity_matrix(mpRT); + addB2DPolygonToPathGeometry(mpRT, aAdaptedPolygon); + + // process/set LineWidth + const double fObjectLineWidth(bHairline + ? 1.0 + : (getViewInformation2D().getObjectToViewTransformation() + * basegfx::B2DVector(rLineAttribute.getWidth(), 0.0)) + .getLength()); + cairo_set_line_width(mpRT, fObjectLineWidth); + + if (bDashUsed) + { + std::vector<double> aStroke(rStrokeAttribute.getDotDashArray()); + for (auto& rCandidate : aStroke) + rCandidate = (getViewInformation2D().getObjectToViewTransformation() + * basegfx::B2DVector(rCandidate, 0.0)) + .getLength(); + cairo_set_dash(mpRT, aStroke.data(), aStroke.size(), 0.0); + } + + cairo_stroke(mpRT); } + else + { + // set linear transformation + cairo_matrix_t aMatrix; + const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0); + cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), + rObjectToView.d(), rObjectToView.e() + fAAOffset, + rObjectToView.f() + fAAOffset); + cairo_set_matrix(mpRT, &aMatrix); - // create path geometry and put mask as path - cairo_new_path(mpRT); - getOrCreatePathGeometry(mpRT, rPolygon, getViewInformation2D(), - bHairline && getViewInformation2D().getUseAntiAliasing()); + // create path geometry and put mask as path + cairo_new_path(mpRT); + getOrCreatePathGeometry(mpRT, rPolygon, getViewInformation2D(), + bHairline && getViewInformation2D().getUseAntiAliasing()); - // render - cairo_stroke(mpRT); + // process/set LineWidth + const double fObjectLineWidth( + bHairline ? (getViewInformation2D().getInverseObjectToViewTransformation() + * basegfx::B2DVector(1.0, 0.0)) + .getLength() + : rLineAttribute.getWidth()); + cairo_set_line_width(mpRT, fObjectLineWidth); + + if (bDashUsed) + { + const std::vector<double>& rStroke = rStrokeAttribute.getDotDashArray(); + cairo_set_dash(mpRT, rStroke.data(), rStroke.size(), 0.0); + } + + // render + cairo_stroke(mpRT); + } cairo_restore(mpRT); } @@ -2057,16 +2220,11 @@ void CairoPixelProcessor2D::processLineRectanglePrimitive2D( cairo_save(mpRT); - cairo_matrix_t aMatrix; + // work in view coordinates const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0); - const basegfx::B2DHomMatrix& rObjectToView( - getViewInformation2D().getObjectToViewTransformation()); - cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), - rObjectToView.d(), rObjectToView.e() + fAAOffset, - rObjectToView.f() + fAAOffset); - - // set linear transformation - cairo_set_matrix(mpRT, &aMatrix); + basegfx::B2DRange aRange(rLineRectanglePrimitive2D.getB2DRange()); + aRange.transform(getViewInformation2D().getObjectToViewTransformation()); + cairo_identity_matrix(mpRT); const basegfx::BColor aHairlineColor( maBColorModifierStack.getModifiedColor(rLineRectanglePrimitive2D.getBColor())); @@ -2078,9 +2236,8 @@ void CairoPixelProcessor2D::processLineRectanglePrimitive2D( .getLength()); cairo_set_line_width(mpRT, fDiscreteLineWidth); - const basegfx::B2DRange& rRange(rLineRectanglePrimitive2D.getB2DRange()); - cairo_rectangle(mpRT, rRange.getMinX(), rRange.getMinY(), rRange.getWidth(), - rRange.getHeight()); + cairo_rectangle(mpRT, aRange.getMinX() + fAAOffset, aRange.getMinY() + fAAOffset, + aRange.getWidth(), aRange.getHeight()); cairo_stroke(mpRT); cairo_restore(mpRT); @@ -2097,22 +2254,17 @@ void CairoPixelProcessor2D::processFilledRectanglePrimitive2D( cairo_save(mpRT); - cairo_matrix_t aMatrix; - const basegfx::B2DHomMatrix& rObjectToView( - getViewInformation2D().getObjectToViewTransformation()); - cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(), - rObjectToView.d(), rObjectToView.e(), rObjectToView.f()); - - // set linear transformation - cairo_set_matrix(mpRT, &aMatrix); + // work in view coordinates + basegfx::B2DRange aRange(rFilledRectanglePrimitive2D.getB2DRange()); + aRange.transform(getViewInformation2D().getObjectToViewTransformation()); + cairo_identity_matrix(mpRT); const basegfx::BColor aFillColor( maBColorModifierStack.getModifiedColor(rFilledRectanglePrimitive2D.getBColor())); cairo_set_source_rgb(mpRT, aFillColor.getRed(), aFillColor.getGreen(), aFillColor.getBlue()); - const basegfx::B2DRange& rRange(rFilledRectanglePrimitive2D.getB2DRange()); - cairo_rectangle(mpRT, rRange.getMinX(), rRange.getMinY(), rRange.getWidth(), - rRange.getHeight()); + cairo_rectangle(mpRT, aRange.getMinX(), aRange.getMinY(), aRange.getWidth(), + aRange.getHeight()); cairo_fill(mpRT); cairo_restore(mpRT); @@ -2132,6 +2284,7 @@ void CairoPixelProcessor2D::processSingleLinePrimitive2D( getViewInformation2D().getObjectToViewTransformation()); const basegfx::B2DPoint aStart(rObjectToView * rSingleLinePrimitive2D.getStart()); const basegfx::B2DPoint aEnd(rObjectToView * rSingleLinePrimitive2D.getEnd()); + cairo_identity_matrix(mpRT); cairo_set_line_width(mpRT, 1.0f); @@ -3717,6 +3870,47 @@ void CairoPixelProcessor2D::processControlPrimitive2D( process(rControlPrimitive); } +void CairoPixelProcessor2D::evaluateCairoCoordinateLimitWorkaround() +{ + static bool bAlreadyCheckedIfNeeded(false); + static bool bIsNeeded(false); + + if (!bAlreadyCheckedIfNeeded) + { + // check once for office runtime: is workarund needed? + bAlreadyCheckedIfNeeded = true; + bIsNeeded = checkCoordinateLimitWorkaroundNeededForUsedCairo(); + } + + if (!bIsNeeded) + { + // we have a working cairo, so workarund is not needed + // and mbCairoCoordinateLimitWorkaroundActive can stay false + return; + } + + // get discrete size (pixels) + basegfx::B2DRange aLogicViewRange(getDiscreteViewRange(mpRT)); + + // transform to world coordinates -> logic view range + basegfx::B2DHomMatrix aInvViewTrans(getViewInformation2D().getViewTransformation()); + aInvViewTrans.invert(); + aLogicViewRange.transform(aInvViewTrans); + + // create 1<<23 CairoCoordinate limit from 24.8 internal format + // and a range fitting to it (just once, this is static) + constexpr double fNumCairoMax(1 << 23); + static const basegfx::B2DRange aNumericalCairoLimit(-fNumCairoMax, -fNumCairoMax, + fNumCairoMax - 1.0, fNumCairoMax - 1.0); + + if (!aLogicViewRange.isEmpty() && !aNumericalCairoLimit.isInside(aLogicViewRange)) + { + // aLogicViewRange is not completely inside region covered by + // 24.8 cairo format, thus workaround is needed, set flag + mbCairoCoordinateLimitWorkaroundActive = true; + } +} + void CairoPixelProcessor2D::processBasePrimitive2D(const primitive2d::BasePrimitive2D& rCandidate) { const cairo_status_t aStart(cairo_status(mpRT)); diff --git a/include/drawinglayer/processor2d/cairopixelprocessor2d.hxx b/include/drawinglayer/processor2d/cairopixelprocessor2d.hxx index fc67ed78932b..3d20f300a972 100644 --- a/include/drawinglayer/processor2d/cairopixelprocessor2d.hxx +++ b/include/drawinglayer/processor2d/cairopixelprocessor2d.hxx @@ -86,6 +86,9 @@ class UNLESS_MERGELIBS(DRAWINGLAYER_DLLPUBLIC) CairoPixelProcessor2D final : pub // see comment there sal_uInt16 mnClipRecursionCount; + // calculated result of if we are in outsideCairoCoordinateLimits mode + bool mbCairoCoordinateLimitWorkaroundActive; + // helpers for direct paints void paintPolyPoylgonRGBA(const basegfx::B2DPolyPolygon& rPolyPolygon, const basegfx::BColor& rColor, double fTransparency = 0.0); @@ -179,6 +182,9 @@ class UNLESS_MERGELIBS(DRAWINGLAYER_DLLPUBLIC) CairoPixelProcessor2D final : pub void processFillGradientPrimitive2D_square_rect( const primitive2d::FillGradientPrimitive2D& rFillGradientPrimitive2D); + // check if CairoCoordinateLimitWorkaround is needed + void evaluateCairoCoordinateLimitWorkaround(); + protected: bool hasError() const { return cairo_status(mpRT) != CAIRO_STATUS_SUCCESS; } bool hasRenderTarget() const { return nullptr != mpRT; } @@ -186,6 +192,12 @@ protected: public: bool valid() const { return hasRenderTarget() && !hasError(); } + // read access to CairoCoordinateLimitWorkaround mechanism + bool isCairoCoordinateLimitWorkaroundActive() const + { + return mbCairoCoordinateLimitWorkaroundActive; + } + // constructor to create a CairoPixelProcessor2D which // allocates and owns a cairo surface of given size. You // should check the result using valid()