diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
commit | 267c6f2ac71f92999e969232431ba04678e7437e (patch) | |
tree | 358c9467650e1d0a1d7227a21dac2e3d08b622b2 /basegfx/source/tools | |
parent | Initial commit. (diff) | |
download | libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip |
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'basegfx/source/tools')
-rw-r--r-- | basegfx/source/tools/b2dclipstate.cxx | 476 | ||||
-rw-r--r-- | basegfx/source/tools/bgradient.cxx | 997 | ||||
-rw-r--r-- | basegfx/source/tools/canvastools.cxx | 474 | ||||
-rw-r--r-- | basegfx/source/tools/gradienttools.cxx | 756 | ||||
-rw-r--r-- | basegfx/source/tools/keystoplerp.cxx | 90 | ||||
-rw-r--r-- | basegfx/source/tools/numbertools.cxx | 58 | ||||
-rw-r--r-- | basegfx/source/tools/stringconversiontools.cxx | 158 | ||||
-rw-r--r-- | basegfx/source/tools/systemdependentdata.cxx | 166 | ||||
-rw-r--r-- | basegfx/source/tools/tools.cxx | 111 | ||||
-rw-r--r-- | basegfx/source/tools/unopolypolygon.cxx | 446 | ||||
-rw-r--r-- | basegfx/source/tools/zoomtools.cxx | 113 |
11 files changed, 3845 insertions, 0 deletions
diff --git a/basegfx/source/tools/b2dclipstate.cxx b/basegfx/source/tools/b2dclipstate.cxx new file mode 100644 index 0000000000..126235699b --- /dev/null +++ b/basegfx/source/tools/b2dclipstate.cxx @@ -0,0 +1,476 @@ +/* -*- 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 <basegfx/utils/b2dclipstate.hxx> + +#include <basegfx/range/b2drange.hxx> +#include <basegfx/range/b2dpolyrange.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> +#include <utility> + + +namespace basegfx::utils +{ + class ImplB2DClipState + { + public: + enum Operation {UNION, INTERSECT, XOR, SUBTRACT}; + + ImplB2DClipState() : + mePendingOps(UNION) + {} + + explicit ImplB2DClipState( B2DPolyPolygon aPoly ) : + maClipPoly(std::move(aPoly)), + mePendingOps(UNION) + {} + + bool isCleared() const + { + return !maClipPoly.count() + && !maPendingPolygons.count() + && !maPendingRanges.count(); + } + + bool isNullClipPoly() const + { + return maClipPoly.count() == 1 + && !maClipPoly.getB2DPolygon(0).count(); + } + + bool isNull() const + { + return !maPendingPolygons.count() + && !maPendingRanges.count() + && isNullClipPoly(); + } + + void makeNull() + { + maPendingPolygons.clear(); + maPendingRanges.clear(); + maClipPoly.clear(); + maClipPoly.append(B2DPolygon()); + mePendingOps = UNION; + } + + bool operator==(const ImplB2DClipState& rRHS) const + { + return maPendingPolygons == rRHS.maPendingPolygons + && maPendingRanges == rRHS.maPendingRanges + && maClipPoly == rRHS.maClipPoly + && mePendingOps == rRHS.mePendingOps; + } + + void addRange(const B2DRange& rRange, Operation eOp) + { + if( rRange.isEmpty() ) + return; + + commitPendingPolygons(); + if( mePendingOps != eOp ) + commitPendingRanges(); + + mePendingOps = eOp; + maPendingRanges.appendElement( + rRange, + B2VectorOrientation::Positive); + } + + void addPolyPolygon(const B2DPolyPolygon& aPoly, Operation eOp) + { + commitPendingRanges(); + if( mePendingOps != eOp ) + commitPendingPolygons(); + + mePendingOps = eOp; + maPendingPolygons.append(aPoly); + } + + void unionRange(const B2DRange& rRange) + { + if( isCleared() ) + return; + + addRange(rRange,UNION); + } + + void unionPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + if( isCleared() ) + return; + + addPolyPolygon(rPolyPoly,UNION); + } + + void intersectRange(const B2DRange& rRange) + { + if( isNull() ) + return; + + addRange(rRange,INTERSECT); + } + + void intersectPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + if( isNull() ) + return; + + addPolyPolygon(rPolyPoly,INTERSECT); + } + + void subtractRange(const B2DRange& rRange ) + { + if( isNull() ) + return; + + addRange(rRange,SUBTRACT); + } + + void subtractPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + if( isNull() ) + return; + + addPolyPolygon(rPolyPoly,SUBTRACT); + } + + void xorRange(const B2DRange& rRange) + { + addRange(rRange,XOR); + } + + void xorPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + addPolyPolygon(rPolyPoly,XOR); + } + + void transform(const basegfx::B2DHomMatrix& rTranslate) + { + maPendingRanges.transform(rTranslate); + maPendingPolygons.transform(rTranslate); + maClipPoly.transform(rTranslate); + } + + B2DPolyPolygon const & getClipPoly() const + { + commitPendingRanges(); + commitPendingPolygons(); + + return maClipPoly; + } + + private: + void commitPendingPolygons() const + { + if( !maPendingPolygons.count() ) + return; + + // assumption: maClipPoly has kept polygons prepared for + // clipping; i.e. no neutral polygons & correct + // orientation + maPendingPolygons = utils::prepareForPolygonOperation(maPendingPolygons); + const bool bIsEmpty=isNullClipPoly(); + const bool bIsCleared=!maClipPoly.count(); + switch(mePendingOps) + { + case UNION: + assert( !bIsCleared ); + + if( bIsEmpty ) + maClipPoly = maPendingPolygons; + else + maClipPoly = utils::solvePolygonOperationOr( + maClipPoly, + maPendingPolygons); + break; + case INTERSECT: + assert( !bIsEmpty ); + + if( bIsCleared ) + maClipPoly = maPendingPolygons; + else + maClipPoly = utils::solvePolygonOperationAnd( + maClipPoly, + maPendingPolygons); + break; + case XOR: + if( bIsEmpty ) + maClipPoly = maPendingPolygons; + else if( bIsCleared ) + { + // not representable, strictly speaking, + // using polygons with the common even/odd + // or nonzero winding number fill rule. If + // we'd want to represent it, fill rule + // would need to be "non-negative winding + // number" (and we then would return + // 'holes' here) + + // going for an ugly hack meanwhile + maClipPoly = utils::solvePolygonOperationXor( + B2DPolyPolygon( + utils::createPolygonFromRect(B2DRange(-1E20,-1E20,1E20,1E20))), + maPendingPolygons); + } + else + maClipPoly = utils::solvePolygonOperationXor( + maClipPoly, + maPendingPolygons); + break; + case SUBTRACT: + assert( !bIsEmpty ); + + // first union all pending ones, subtract en bloc then + maPendingPolygons = solveCrossovers(maPendingPolygons); + maPendingPolygons = stripNeutralPolygons(maPendingPolygons); + maPendingPolygons = stripDispensablePolygons(maPendingPolygons); + + if( bIsCleared ) + { + // not representable, strictly speaking, + // using polygons with the common even/odd + // or nonzero winding number fill rule. If + // we'd want to represent it, fill rule + // would need to be "non-negative winding + // number" (and we then would return + // 'holes' here) + + // going for an ugly hack meanwhile + maClipPoly = utils::solvePolygonOperationDiff( + B2DPolyPolygon( + utils::createPolygonFromRect(B2DRange(-1E20,-1E20,1E20,1E20))), + maPendingPolygons); + } + else + maClipPoly = utils::solvePolygonOperationDiff( + maClipPoly, + maPendingPolygons); + break; + } + + maPendingPolygons.clear(); + mePendingOps = UNION; + } + + void commitPendingRanges() const + { + if( !maPendingRanges.count() ) + return; + + // use the specialized range clipper for the win + B2DPolyPolygon aCollectedRanges; + const bool bIsEmpty=isNullClipPoly(); + const bool bIsCleared=!maClipPoly.count(); + switch(mePendingOps) + { + case UNION: + assert( !bIsCleared ); + + aCollectedRanges = maPendingRanges.solveCrossovers(); + aCollectedRanges = stripNeutralPolygons(aCollectedRanges); + aCollectedRanges = stripDispensablePolygons(aCollectedRanges); + if( bIsEmpty ) + maClipPoly = aCollectedRanges; + else + maClipPoly = utils::solvePolygonOperationOr( + maClipPoly, + aCollectedRanges); + break; + case INTERSECT: + assert( !bIsEmpty ); + + aCollectedRanges = maPendingRanges.solveCrossovers(); + aCollectedRanges = stripNeutralPolygons(aCollectedRanges); + if( maPendingRanges.count() > 1 ) + aCollectedRanges = stripDispensablePolygons(aCollectedRanges, true); + + if( bIsCleared ) + maClipPoly = aCollectedRanges; + else + maClipPoly = utils::solvePolygonOperationAnd( + maClipPoly, + aCollectedRanges); + break; + case XOR: + aCollectedRanges = maPendingRanges.solveCrossovers(); + aCollectedRanges = stripNeutralPolygons(aCollectedRanges); + aCollectedRanges = correctOrientations(aCollectedRanges); + + if( bIsEmpty ) + maClipPoly = aCollectedRanges; + else if( bIsCleared ) + { + // not representable, strictly speaking, + // using polygons with the common even/odd + // or nonzero winding number fill rule. If + // we'd want to represent it, fill rule + // would need to be "non-negative winding + // number" (and we then would return + // 'holes' here) + + // going for an ugly hack meanwhile + maClipPoly = utils::solvePolygonOperationXor( + B2DPolyPolygon( + utils::createPolygonFromRect(B2DRange(-1E20,-1E20,1E20,1E20))), + aCollectedRanges); + } + else + maClipPoly = utils::solvePolygonOperationXor( + maClipPoly, + aCollectedRanges); + break; + case SUBTRACT: + assert( !bIsEmpty ); + + // first union all pending ranges, subtract en bloc then + aCollectedRanges = maPendingRanges.solveCrossovers(); + aCollectedRanges = stripNeutralPolygons(aCollectedRanges); + aCollectedRanges = stripDispensablePolygons(aCollectedRanges); + + if( bIsCleared ) + { + // not representable, strictly speaking, + // using polygons with the common even/odd + // or nonzero winding number fill rule. If + // we'd want to represent it, fill rule + // would need to be "non-negative winding + // number" (and we then would return + // 'holes' here) + + // going for an ugly hack meanwhile + maClipPoly = utils::solvePolygonOperationDiff( + B2DPolyPolygon( + utils::createPolygonFromRect(B2DRange(-1E20,-1E20,1E20,1E20))), + aCollectedRanges); + } + else + maClipPoly = utils::solvePolygonOperationDiff( + maClipPoly, + aCollectedRanges); + break; + } + + maPendingRanges.clear(); + mePendingOps = UNION; + } + + mutable B2DPolyPolygon maPendingPolygons; + mutable B2DPolyRange maPendingRanges; + mutable B2DPolyPolygon maClipPoly; + mutable Operation mePendingOps; + }; + + B2DClipState::B2DClipState() = default; + + B2DClipState::~B2DClipState() = default; + + B2DClipState::B2DClipState( const B2DClipState& ) = default; + + B2DClipState::B2DClipState( B2DClipState&& ) = default; + + B2DClipState::B2DClipState( const B2DPolyPolygon& rPolyPoly ) : + mpImpl( ImplB2DClipState(rPolyPoly) ) + {} + + B2DClipState& B2DClipState::operator=( const B2DClipState& ) = default; + + B2DClipState& B2DClipState::operator=( B2DClipState&& ) = default; + + void B2DClipState::makeNull() + { + mpImpl->makeNull(); + } + + bool B2DClipState::isCleared() const + { + return mpImpl->isCleared(); + } + + bool B2DClipState::operator==(const B2DClipState& rRHS) const + { + if(mpImpl.same_object(rRHS.mpImpl)) + return true; + + return ((*mpImpl) == (*rRHS.mpImpl)); + } + + bool B2DClipState::operator!=(const B2DClipState& rRHS) const + { + return !(*this == rRHS); + } + + void B2DClipState::unionRange(const B2DRange& rRange) + { + mpImpl->unionRange(rRange); + } + + void B2DClipState::unionPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + mpImpl->unionPolyPolygon(rPolyPoly); + } + + void B2DClipState::intersectRange(const B2DRange& rRange) + { + mpImpl->intersectRange(rRange); + } + + void B2DClipState::intersectPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + mpImpl->intersectPolyPolygon(rPolyPoly); + } + + void B2DClipState::subtractRange(const B2DRange& rRange) + { + mpImpl->subtractRange(rRange); + } + + void B2DClipState::subtractPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + mpImpl->subtractPolyPolygon(rPolyPoly); + } + + void B2DClipState::xorRange(const B2DRange& rRange) + { + mpImpl->xorRange(rRange); + } + + void B2DClipState::xorPolyPolygon(const B2DPolyPolygon& rPolyPoly) + { + mpImpl->xorPolyPolygon(rPolyPoly); + } + + B2DPolyPolygon const & B2DClipState::getClipPoly() const + { + return mpImpl->getClipPoly(); + } + + void B2DClipState::transform(const basegfx::B2DHomMatrix& rTranslate) + { + return mpImpl->transform(rTranslate); + } + + +} // end of namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/bgradient.cxx b/basegfx/source/tools/bgradient.cxx new file mode 100644 index 0000000000..86e1812d21 --- /dev/null +++ b/basegfx/source/tools/bgradient.cxx @@ -0,0 +1,997 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <basegfx/utils/bgradient.hxx> +#include <basegfx/utils/gradienttools.hxx> +#include <com/sun/star/awt/Gradient2.hpp> +#include <boost/property_tree/json_parser.hpp> +#include <map> + +typedef std::map<OUString, OUString> StringMap; + +namespace +{ +css::awt::GradientStyle lcl_getStyleFromString(std::u16string_view rStyle) +{ + if (rStyle == u"LINEAR") + return css::awt::GradientStyle_LINEAR; + else if (rStyle == u"AXIAL") + return css::awt::GradientStyle_AXIAL; + else if (rStyle == u"RADIAL") + return css::awt::GradientStyle_RADIAL; + else if (rStyle == u"ELLIPTICAL") + return css::awt::GradientStyle_ELLIPTICAL; + else if (rStyle == u"SQUARE") + return css::awt::GradientStyle_SQUARE; + else if (rStyle == u"RECT") + return css::awt::GradientStyle_RECT; + + return css::awt::GradientStyle_LINEAR; +} + +StringMap lcl_jsonToStringMap(std::u16string_view rJSON) +{ + StringMap aArgs; + if (rJSON.size() && rJSON[0] != '\0') + { + std::stringstream aStream(std::string(OUStringToOString(rJSON, RTL_TEXTENCODING_ASCII_US))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + for (const auto& rPair : aTree) + { + aArgs[OUString::fromUtf8(rPair.first)] + = OUString::fromUtf8(rPair.second.get_value<std::string>(".")); + } + } + return aArgs; +} + +basegfx::BGradient lcl_buildGradientFromStringMap(StringMap& rMap) +{ + basegfx::BGradient aGradient( + basegfx::BColorStops(ColorToBColorConverter(rMap["startcolor"].toInt32(16)).getBColor(), + ColorToBColorConverter(rMap["endcolor"].toInt32(16)).getBColor())); + + aGradient.SetGradientStyle(lcl_getStyleFromString(rMap["style"])); + aGradient.SetAngle(Degree10(rMap["angle"].toInt32())); + + return aGradient; +} +} + +namespace basegfx +{ +// constructor with two colors to explicitly create a +// BColorStops for a single StartColor @0.0 & EndColor @1.0 +BColorStops::BColorStops(const BColor& rStart, const BColor& rEnd) +{ + emplace_back(0.0, rStart); + emplace_back(1.0, rEnd); +} + +/* Helper to grep the correct ColorStop out of + ColorStops and interpolate as needed for given + relative value in fPosition in the range of [0.0 .. 1.0]. + It also takes care of evtl. given RequestedSteps. + */ +BColor BColorStops::getInterpolatedBColor(double fPosition, sal_uInt32 nRequestedSteps, + BColorStopRange& rLastColorStopRange) const +{ + // no color at all, done + if (empty()) + return BColor(); + + // outside range -> at start + const double fMin(front().getStopOffset()); + if (fPosition < fMin) + return front().getStopColor(); + + // outside range -> at end + const double fMax(back().getStopOffset()); + if (fPosition > fMax) + return back().getStopColor(); + + // special case for the 'classic' case with just two colors: + // we can optimize that and keep the speed/resources low + // by avoiding some calculations and an O(log(N)) array access + if (2 == size()) + { + // if same StopOffset use front color + if (fTools::equal(fMin, fMax)) + return front().getStopColor(); + + const basegfx::BColor aCStart(front().getStopColor()); + const basegfx::BColor aCEnd(back().getStopColor()); + + // if colors are equal just return one + if (aCStart == aCEnd) + return aCStart; + + // calculate Steps + const sal_uInt32 nSteps( + basegfx::utils::calculateNumberOfSteps(nRequestedSteps, aCStart, aCEnd)); + + // we need to extend the interpolation to the local + // range of ColorStops. Despite having two ColorStops + // these are not necessarily at 0.0 and 1.0, so may be + // not the classical Start/EndColor (what is allowed) + fPosition = (fPosition - fMin) / (fMax - fMin); + return basegfx::interpolate(aCStart, aCEnd, + nSteps > 1 ? floor(fPosition * nSteps) / double(nSteps - 1) + : fPosition); + } + + // check if we need to newly populate the needed interpolation data + // or if we can re-use from last time. + // If this scope is not entered, we do not need the binary search. It's + // only a single buffered entry, and only used when more than three + // ColorStops exist, but makes a huge difference compared with accessing + // the sorted ColorStop vector each time. + // NOTE: with this simple change I get very high hit rates, e.g. rotating + // a donut with gradient test '1' hit rate is at 0.99909440357755486 + if (rLastColorStopRange.mfOffsetStart == rLastColorStopRange.mfOffsetEnd + || fPosition < rLastColorStopRange.mfOffsetStart + || fPosition > rLastColorStopRange.mfOffsetEnd) + { + // access needed spot in sorted array using binary search + // NOTE: This *seems* slow(er) when developing compared to just + // looping/accessing, but that's just due to the extensive + // debug test code created by the stl. In a pro version, + // all is good/fast as expected + const auto upperBound(std::upper_bound(begin(), end(), BColorStop(fPosition), + [](const BColorStop& x, const BColorStop& y) { + return x.getStopOffset() < y.getStopOffset(); + })); + + // no upper bound, done + if (end() == upperBound) + return back().getStopColor(); + + // lower bound is one entry back, access that + const auto lowerBound(upperBound - 1); + + // no lower bound, done + if (end() == lowerBound) + return back().getStopColor(); + + // we have lower and upper bound, get colors and offsets + rLastColorStopRange.maColorStart = lowerBound->getStopColor(); + rLastColorStopRange.maColorEnd = upperBound->getStopColor(); + rLastColorStopRange.mfOffsetStart = lowerBound->getStopOffset(); + rLastColorStopRange.mfOffsetEnd = upperBound->getStopOffset(); + } + + // when there are just two color steps this cannot happen, but when using + // a range of colors this *may* be used inside the range to represent + // single-colored regions inside a ColorRange. Use that color & done + if (rLastColorStopRange.maColorStart == rLastColorStopRange.maColorEnd) + return rLastColorStopRange.maColorStart; + + // calculate number of steps and adapted proportional + // range for scaler in [0.0 .. 1.0] + const double fAdaptedScaler( + (fPosition - rLastColorStopRange.mfOffsetStart) + / (rLastColorStopRange.mfOffsetEnd - rLastColorStopRange.mfOffsetStart)); + const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps( + nRequestedSteps, rLastColorStopRange.maColorStart, rLastColorStopRange.maColorEnd)); + + // interpolate & evtl. apply steps + return interpolate(rLastColorStopRange.maColorStart, rLastColorStopRange.maColorEnd, + nSteps > 1 ? floor(fAdaptedScaler * nSteps) / double(nSteps - 1) + : fAdaptedScaler); +} + +/* Tooling method that allows to replace the StartColor in a + vector of ColorStops. A vector in 'ordered state' is expected, + so you may use/have used sortAndCorrect. + This method is for convenience & backwards compatibility, please + think about handling multi-colored gradients directly. + */ +void BColorStops::replaceStartColor(const BColor& rStart) +{ + BColorStops::iterator a1stNonStartColor(begin()); + + // search for highest existing non-StartColor - CAUTION, + // there might be none, one or multiple with StopOffset 0.0 + while (a1stNonStartColor != end() + && basegfx::fTools::lessOrEqual(a1stNonStartColor->getStopOffset(), 0.0)) + a1stNonStartColor++; + + // create new ColorStops by 1st adding new one and then all + // non-StartColor entries + BColorStops aNewColorStops; + + aNewColorStops.reserve(size() + 1); + aNewColorStops.emplace_back(0.0, rStart); + aNewColorStops.insert(aNewColorStops.end(), a1stNonStartColor, end()); + + // assign & done + *this = aNewColorStops; +} + +/* Tooling method that allows to replace the EndColor in a + vector of ColorStops. A vector in 'ordered state' is expected, + so you may use/have used sortAndCorrectColorStops. + This method is for convenience & backwards compatibility, please + think about handling multi-colored gradients directly. + */ +void BColorStops::replaceEndColor(const BColor& rEnd) +{ + // erase all evtl. existing EndColor(s) + while (!empty() && basegfx::fTools::moreOrEqual(back().getStopOffset(), 1.0)) + pop_back(); + + // add at the end of existing ColorStops + emplace_back(1.0, rEnd); +} + +/* Tooling method to linearly blend the Colors contained in + a given ColorStop vector against a given Color using the + given intensity values. + The intensity values fStartIntensity, fEndIntensity are + in the range of [0.0 .. 1.0] and describe how much the + blend is supposed to be done at the start color position + and the end color position respectively, where 0.0 means + to fully use the given BlendColor, 1.0 means to not change + the existing color in the ColorStop. + Every color entry in the given ColorStop is blended + relative to it's StopPosition, interpolating the + given intensities with the range [0.0 .. 1.0] to do so. + */ +void BColorStops::blendToIntensity(double fStartIntensity, double fEndIntensity, + const BColor& rBlendColor) +{ + // no entries, done + if (empty()) + return; + + // correct intensities (maybe assert when input was wrong) + fStartIntensity = std::max(std::min(1.0, fStartIntensity), 0.0); + fEndIntensity = std::max(std::min(1.0, fEndIntensity), 0.0); + + // all 100%, no real blend, done + if (basegfx::fTools::equal(fStartIntensity, 1.0) && basegfx::fTools::equal(fEndIntensity, 1.0)) + return; + + // blend relative to StopOffset position + for (auto& candidate : *this) + { + const double fOffset(candidate.getStopOffset()); + const double fIntensity((fStartIntensity * (1.0 - fOffset)) + (fEndIntensity * fOffset)); + candidate = basegfx::BColorStop( + fOffset, basegfx::interpolate(rBlendColor, candidate.getStopColor(), fIntensity)); + } +} + +/* Tooling method to guarantee sort and correctness for + the given ColorStops vector. + A vector fulfilling these conditions is called to be + in 'ordered state'. + + At return, the following conditions are guaranteed: + - contains no ColorStops with offset < 0.0 (will + be removed) + - contains no ColorStops with offset > 1.0 (will + be removed) + - ColorStops with identical offsets are now allowed + - will be sorted from lowest offset to highest + + Some more notes: + - It can happen that the result is empty + - It is allowed to have consecutive entries with + the same color, this represents single-color + regions inside the gradient + - A entry with 0.0 is not required or forced, so + no 'StartColor' is technically required + - A entry with 1.0 is not required or forced, so + no 'EndColor' is technically required + + All this is done in one run (sort + O(N)) without + creating a copy of the data in any form + */ +void BColorStops::sortAndCorrect() +{ + // no content, we are done + if (empty()) + return; + + if (1 == size()) + { + // no gradient at all, but preserve given color + // evtl. correct offset to be in valid range [0.0 .. 1.0] + // NOTE: This does not move it to 0.0 or 1.0, it *can* still + // be somewhere in-between what is allowed + const BColorStop aEntry(front()); + clear(); + emplace_back(std::max(0.0, std::min(1.0, aEntry.getStopOffset())), aEntry.getStopColor()); + + // done + return; + } + + // start with sorting the input data. Remember that + // this preserves the order of equal entries, where + // equal is defined here by offset (see use operator==) + std::sort(begin(), end()); + + // prepare status values + size_t write(0); + + // use the paradigm of a band machine with two heads, read + // and write with write <= read all the time. Step over the + // data using read and check for valid entry. If valid, decide + // how to keep it + for (size_t read(0); read < size(); read++) + { + // get offset of entry at read position + double fOff((*this)[read].getStopOffset()); + + if (basegfx::fTools::less(fOff, 0.0) && read + 1 < size()) + { + // value < 0.0 and we have a next entry. check for gradient snippet + // containing 0.0 resp. StartColor + const double fOff2((*this)[read + 1].getStopOffset()); + + if (basegfx::fTools::more(fOff2, 0.0)) + { + // read is the start of a gradient snippet containing 0.0. Correct + // entry to StartColor, interpolate to correct StartColor + (*this)[read] + = BColorStop(0.0, basegfx::interpolate((*this)[read].getStopColor(), + (*this)[read + 1].getStopColor(), + (0.0 - fOff) / (fOff2 - fOff))); + + // adapt fOff + fOff = 0.0; + } + } + + // step over < 0 values, these are outside and will be removed + if (basegfx::fTools::less(fOff, 0.0)) + { + continue; + } + + if (basegfx::fTools::less(fOff, 1.0) && read + 1 < size()) + { + // value < 1.0 and we have a next entry. check for gradient snippet + // containing 1.0 resp. EndColor + const double fOff2((*this)[read + 1].getStopOffset()); + + if (basegfx::fTools::more(fOff2, 1.0)) + { + // read is the start of a gradient snippet containing 1.0. Correct + // next entry to EndColor, interpolate to correct EndColor + (*this)[read + 1] + = BColorStop(1.0, basegfx::interpolate((*this)[read].getStopColor(), + (*this)[read + 1].getStopColor(), + (1.0 - fOff) / (fOff2 - fOff))); + + // adapt fOff + fOff = 1.0; + } + } + + // step over > 1 values; even break, since all following + // entries will also be bigger due to being sorted, so done + if (basegfx::fTools::more(fOff, 1.0)) + { + break; + } + + // entry is valid value at read position + // copy if write target is empty (write at start) or when + // write target is different to read in color or offset + if (0 == write || !((*this)[read] == (*this)[write - 1])) + { + if (write != read) + { + // copy read to write backwards to close gaps + (*this)[write] = (*this)[read]; + } + + // always forward write position + write++; + } + } + + // correct size when length is reduced. write is always at + // last used position + 1 + if (size() > write) + { + if (0 == write) + { + // no valid entries at all, but not empty. This can only happen + // when all entries are below 0.0 or above 1.0 (else a gradient + // snippet spawning over both would have been detected) + if (basegfx::fTools::less(back().getStopOffset(), 0.0)) + { + // all outside too low, rescue last due to being closest to content + const BColor aBackColor(back().getStopColor()); + clear(); + emplace_back(0.0, aBackColor); + } + else // if (basegfx::fTools::more(front().getStopOffset(), 1.0)) + { + // all outside too high, rescue first due to being closest to content + const BColor aFrontColor(front().getStopColor()); + clear(); + emplace_back(1.0, aFrontColor); + } + } + else + { + resize(write); + } + } +} + +bool BColorStops::checkPenultimate() const +{ + // not needed when no ColorStops + if (empty()) + return false; + + // not needed when last ColorStop at the end or outside + if (basegfx::fTools::moreOrEqual(back().getStopOffset(), 1.0)) + return false; + + // get penultimate entry + const auto penultimate(rbegin() + 1); + + // if there is none, we need no correction and are done + if (penultimate == rend()) + return false; + + // not needed when the last two ColorStops have different offset, then + // a visible range will be processed already + if (!basegfx::fTools::equal(back().getStopOffset(), penultimate->getStopOffset())) + return false; + + // not needed when the last two ColorStops have the same Color, then the + // range before solves the problem + if (back().getStopColor() == penultimate->getStopColor()) + return false; + + return true; +} + +/* Tooling method to check if a ColorStop vector is defined + by a single color. It returns true if this is the case. + If true is returned, rSingleColor contains that single + color for convenience. + NOTE: If no ColorStop is defined, a fallback to BColor-default + (which is black) and true will be returned + */ +bool BColorStops::isSingleColor(BColor& rSingleColor) const +{ + if (empty()) + { + rSingleColor = BColor(); + return true; + } + + if (1 == size()) + { + rSingleColor = front().getStopColor(); + return true; + } + + rSingleColor = front().getStopColor(); + + for (auto const& rCandidate : *this) + { + if (rCandidate.getStopColor() != rSingleColor) + return false; + } + + return true; +} + +/* Tooling method to reverse ColorStops, including offsets. + When also mirroring offsets a valid sort keeps valid. + */ +void BColorStops::reverseColorStops() +{ + // can use std::reverse, but also need to adapt offset(s) + std::reverse(begin(), end()); + for (auto& candidate : *this) + candidate = BColorStop(1.0 - candidate.getStopOffset(), candidate.getStopColor()); +} + +// createSpaceAtStart creates fOffset space at start by +// translating/scaling all entries to the right +void BColorStops::createSpaceAtStart(double fOffset) +{ + // nothing to do if empty + if (empty()) + return; + + // correct offset to [0.0 .. 1.0] + fOffset = std::max(std::min(1.0, fOffset), 0.0); + + // nothing to do if 0.0 == offset + if (basegfx::fTools::equalZero(fOffset)) + return; + + BColorStops aNewStops; + + for (const auto& candidate : *this) + { + aNewStops.emplace_back(fOffset + (candidate.getStopOffset() * (1.0 - fOffset)), + candidate.getStopColor()); + } + + *this = aNewStops; +} + +// removeSpaceAtStart removes fOffset space from start by +// translating/scaling entries more or equal to fOffset +// to the left. Entries less than fOffset will be removed +void BColorStops::removeSpaceAtStart(double fOffset) +{ + // nothing to do if empty + if (empty()) + return; + + // correct factor to [0.0 .. 1.0] + fOffset = std::max(std::min(1.0, fOffset), 0.0); + + // nothing to do if fOffset == 0.0 + if (basegfx::fTools::equalZero(fOffset)) + return; + + BColorStops aNewStops; + const double fMul(basegfx::fTools::equal(fOffset, 1.0) ? 1.0 : 1.0 / (1.0 - fOffset)); + + for (const auto& candidate : *this) + { + if (basegfx::fTools::moreOrEqual(candidate.getStopOffset(), fOffset)) + { + aNewStops.emplace_back((candidate.getStopOffset() - fOffset) * fMul, + candidate.getStopColor()); + } + } + + *this = aNewStops; +} + +// try to detect if an empty/no-color-change area exists +// at the start and return offset to it. Returns 0.0 if not. +double BColorStops::detectPossibleOffsetAtStart() const +{ + BColor aSingleColor; + const bool bSingleColor(isSingleColor(aSingleColor)); + + // no useful offset for single color + if (bSingleColor) + return 0.0; + + // here we know that we have at least two colors, so we have a + // color change. Find colors left and right of that first color change + BColorStops::const_iterator aColorR(begin()); + BColorStops::const_iterator aColorL(aColorR++); + + // aColorR would 1st get equal to end(), so no need to also check aColorL + // for end(). Loop as long as same color. Since we *have* a color change + // not even aColorR can get equal to end() before color inequality, but + // keep for safety + while (aColorR != end() && aColorL->getStopColor() == aColorR->getStopColor()) + { + aColorL++; + aColorR++; + } + + // also for safety: access values at aColorL below *only* + // if not equal to end(), but can theoretically not happen + if (aColorL == end()) + { + return 0.0; + } + + // return offset (maybe 0.0 what is OK) + return aColorL->getStopOffset(); +} + +// checks whether the color stops are symmetrical in color and offset. +bool BColorStops::isSymmetrical() const +{ + if (empty()) + return false; + if (1 == size()) + return basegfx::fTools::equal(0.5, front().getStopOffset()); + + BColorStops::const_iterator aIter(begin()); // for going forward + BColorStops::const_iterator aRIter(end()); // for going backward + --aRIter; + // We have at least two elements, so aIter <= aRIter fails before iterators no longer point to + // an element. + while (aIter <= aRIter && aIter->getStopColor().equal(aRIter->getStopColor()) + && basegfx::fTools::equal(aIter->getStopOffset(), 1.0 - aRIter->getStopOffset())) + { + ++aIter; + --aRIter; + } + return aIter > aRIter; +} + +void BColorStops::doApplyAxial() +{ + // prepare new ColorStops + basegfx::BColorStops aNewColorStops; + + // add gradient stops in reverse order, scaled to [0.0 .. 0.5] + basegfx::BColorStops::const_reverse_iterator aRevCurrColor(rbegin()); + + while (aRevCurrColor != rend()) + { + aNewColorStops.emplace_back((1.0 - aRevCurrColor->getStopOffset()) * 0.5, + aRevCurrColor->getStopColor()); + aRevCurrColor++; + } + + // prepare non-reverse run + basegfx::BColorStops::const_iterator aCurrColor(begin()); + + if (basegfx::fTools::equalZero(aCurrColor->getStopOffset())) + { + // Caution: do not add 1st entry again, that would be double since it was + // already added as last element of the inverse run above. But only if + // the gradient has a start entry for 0.0 aka StartColor, else it is correct. + aCurrColor++; + } + + // add gradient stops in non-reverse order, translated and scaled to [0.5 .. 1.0] + while (aCurrColor != end()) + { + aNewColorStops.emplace_back((aCurrColor->getStopOffset() * 0.5) + 0.5, + aCurrColor->getStopColor()); + aCurrColor++; + } + + // apply color stops + *this = aNewColorStops; +} + +void BColorStops::doApplySteps(sal_uInt16 nStepCount) +{ + // check for zero or invalid steps setting -> done + if (0 == nStepCount || nStepCount > 100) + return; + + // no change needed if single color + BColor aSingleColor; + if (isSingleColor(aSingleColor)) + return; + + // prepare new color stops, get L/R iterators for segments + basegfx::BColorStops aNewColorStops; + basegfx::BColorStops::const_iterator aColorR(begin()); + basegfx::BColorStops::const_iterator aColorL(aColorR++); + + while (aColorR != end()) + { + // get start/end color for segment + const double fStart(aColorL->getStopOffset()); + const double fDelta(aColorR->getStopOffset() - fStart); + + if (aNewColorStops.empty() || aNewColorStops.back() != *aColorL) + { + // add start color, but check if it is already there - which is the + // case from the 2nd segment on due to a new segment starting with + // the same color as the previous one ended + aNewColorStops.push_back(*aColorL); + } + if (!basegfx::fTools::equalZero(fDelta)) + { + // create in-between steps, always two at the same position to + // define a 'hard' color stop. Get start/end color for the segment + const basegfx::BColor& rStartColor(aColorL->getStopColor()); + const basegfx::BColor& rEndColor(aColorR->getStopColor()); + + if (rStartColor != rEndColor) + { + // get relative single-step width + // tdf155852 Use same method for the color as in rendering. + const double fSingleStep(1.0 / static_cast<double>(nStepCount - 1)); + const double fOffsetStep(fDelta / static_cast<double>(nStepCount)); + + for (sal_uInt16 a(1); a < nStepCount; a++) + { + // calculate stop position since being used twice + const double fPosition(fStart + fOffsetStep * static_cast<double>(a)); + + // add end color of previous sub-segment + aNewColorStops.emplace_back( + fPosition, basegfx::interpolate(rStartColor, rEndColor, + static_cast<double>(a - 1) * fSingleStep)); + + // add start color of current sub-segment + aNewColorStops.emplace_back( + fPosition, basegfx::interpolate(rStartColor, rEndColor, + static_cast<double>(a) * fSingleStep)); + } + } + } + + // always add end color of segment + aNewColorStops.push_back(*aColorR); + + // next segment + aColorL++; + aColorR++; + } + + // apply the change to color stops + *this = aNewColorStops; +} + +std::string BGradient::GradientStyleToString(css::awt::GradientStyle eStyle) +{ + switch (eStyle) + { + case css::awt::GradientStyle::GradientStyle_LINEAR: + return "LINEAR"; + + case css::awt::GradientStyle::GradientStyle_AXIAL: + return "AXIAL"; + + case css::awt::GradientStyle::GradientStyle_RADIAL: + return "RADIAL"; + + case css::awt::GradientStyle::GradientStyle_ELLIPTICAL: + return "ELLIPTICAL"; + + case css::awt::GradientStyle::GradientStyle_SQUARE: + return "SQUARE"; + + case css::awt::GradientStyle::GradientStyle_RECT: + return "RECT"; + + case css::awt::GradientStyle::GradientStyle_MAKE_FIXED_SIZE: + return "MAKE_FIXED_SIZE"; + } + + return ""; +} + +BGradient BGradient::fromJSON(std::u16string_view rJSON) +{ + StringMap aMap(lcl_jsonToStringMap(rJSON)); + return lcl_buildGradientFromStringMap(aMap); +} + +BGradient::BGradient() + : eStyle(css::awt::GradientStyle_LINEAR) + , aColorStops() + , nAngle(0) + , nBorder(0) + , nOfsX(50) + , nOfsY(50) + , nIntensStart(100) + , nIntensEnd(100) + , nStepCount(0) +{ + aColorStops.emplace_back(0.0, BColor(0.0, 0.0, 0.0)); // COL_BLACK + aColorStops.emplace_back(1.0, BColor(1.0, 1.0, 1.0)); // COL_WHITE +} + +BGradient::BGradient(const basegfx::BColorStops& rColorStops, css::awt::GradientStyle eTheStyle, + Degree10 nTheAngle, sal_uInt16 nXOfs, sal_uInt16 nYOfs, sal_uInt16 nTheBorder, + sal_uInt16 nStartIntens, sal_uInt16 nEndIntens, sal_uInt16 nSteps) + : eStyle(eTheStyle) + , aColorStops(rColorStops) + , nAngle(nTheAngle) + , nBorder(nTheBorder) + , nOfsX(nXOfs) + , nOfsY(nYOfs) + , nIntensStart(nStartIntens) + , nIntensEnd(nEndIntens) + , nStepCount(nSteps) +{ + SetColorStops(aColorStops); +} + +bool BGradient::operator==(const BGradient& rGradient) const +{ + return (eStyle == rGradient.eStyle && aColorStops == rGradient.aColorStops + && nAngle == rGradient.nAngle && nBorder == rGradient.nBorder + && nOfsX == rGradient.nOfsX && nOfsY == rGradient.nOfsY + && nIntensStart == rGradient.nIntensStart && nIntensEnd == rGradient.nIntensEnd + && nStepCount == rGradient.nStepCount); +} + +void BGradient::SetColorStops(const basegfx::BColorStops& rSteps) +{ + aColorStops = rSteps; + aColorStops.sortAndCorrect(); + if (aColorStops.empty()) + aColorStops.emplace_back(0.0, basegfx::BColor()); +} + +namespace +{ +OUString AsRGBHexString(const ColorToBColorConverter& rVal) +{ + std::stringstream ss; + ss << std::hex << std::setfill('0') << std::setw(6) << sal_uInt32(rVal); + return OUString::createFromAscii(ss.str()); +} +} + +boost::property_tree::ptree BGradient::dumpAsJSON() const +{ + boost::property_tree::ptree aTree; + + aTree.put("style", BGradient::GradientStyleToString(eStyle)); + const ColorToBColorConverter aStart(GetColorStops().front().getStopColor()); + aTree.put("startcolor", AsRGBHexString(aStart.GetRGBColor())); + const ColorToBColorConverter aEnd(GetColorStops().back().getStopColor()); + aTree.put("endcolor", AsRGBHexString(aEnd.GetRGBColor())); + aTree.put("angle", std::to_string(nAngle.get())); + aTree.put("border", std::to_string(nBorder)); + aTree.put("x", std::to_string(nOfsX)); + aTree.put("y", std::to_string(nOfsY)); + aTree.put("intensstart", std::to_string(nIntensStart)); + aTree.put("intensend", std::to_string(nIntensEnd)); + aTree.put("stepcount", std::to_string(nStepCount)); + + return aTree; +} + +void BGradient::tryToRecreateBorder(basegfx::BColorStops* pAssociatedTransparencyStops) +{ + // border already set, do not try to recreate + if (0 != GetBorder()) + return; + + BColor aSingleColor; + const bool bSingleColor(GetColorStops().isSingleColor(aSingleColor)); + + // no need to recreate with single color + if (bSingleColor) + return; + + const bool bIsAxial(css::awt::GradientStyle_AXIAL == GetGradientStyle()); + + if (bIsAxial) + { + // for axial due to reverse used gradient work reversed + aColorStops.reverseColorStops(); + if (nullptr != pAssociatedTransparencyStops) + pAssociatedTransparencyStops->reverseColorStops(); + } + + // check if we have space at start of range [0.0 .. 1.0] that + // may be interpreted as 'border' -> same color. That may involve + // different scenarios, e.g. 1st index > 0.0, but also a non-zero + // number of same color entries, or a combination of both + const double fOffset(aColorStops.detectPossibleOffsetAtStart()); + + if (!basegfx::fTools::equalZero(fOffset)) + { + // we have a border area, indeed re-create + aColorStops.removeSpaceAtStart(fOffset); + if (nullptr != pAssociatedTransparencyStops) + pAssociatedTransparencyStops->removeSpaceAtStart(fOffset); + + // ...and create border value + SetBorder(static_cast<sal_uInt16>(std::lround(fOffset * 100.0))); + } + + if (bIsAxial) + { + // take back reverse + aColorStops.reverseColorStops(); + if (nullptr != pAssociatedTransparencyStops) + pAssociatedTransparencyStops->reverseColorStops(); + } +} + +void BGradient::tryToApplyBorder() +{ + // no border to apply, done + if (0 == GetBorder()) + return; + + // NOTE: no new start node is added. The new ColorStop + // mechanism does not need entries at 0.0 and 1.0. + // In case this is needed, do that in the caller + const double fOffset(GetBorder() * 0.01); + + if (css::awt::GradientStyle_AXIAL == GetGradientStyle()) + { + // for axial due to reverse used gradient work reversed + aColorStops.reverseColorStops(); + aColorStops.createSpaceAtStart(fOffset); + aColorStops.reverseColorStops(); + } + else + { + // apply border to GradientSteps + aColorStops.createSpaceAtStart(fOffset); + } + + // set changed values + SetBorder(0); +} + +void BGradient::tryToApplyStartEndIntensity() +{ + // already on default, nothing to apply + if (100 == GetStartIntens() && 100 == GetEndIntens()) + return; + + // apply 'old' blend stuff, blend against black + aColorStops.blendToIntensity(GetStartIntens() * 0.01, GetEndIntens() * 0.01, + BColor()); // COL_BLACK + + // set values to default + SetStartIntens(100); + SetEndIntens(100); +} + +void BGradient::tryToConvertToAxial() +{ + if (css::awt::GradientStyle_LINEAR != GetGradientStyle() || 0 != GetBorder() + || GetColorStops().empty()) + return; + + if (!GetColorStops().isSymmetrical()) + return; + + SetGradientStyle(css::awt::GradientStyle_AXIAL); + + // Stretch the first half of the color stops to double width + // and collect them in a new color stops vector. + BColorStops aAxialColorStops; + aAxialColorStops.reserve(std::ceil(GetColorStops().size() / 2.0)); + BColorStops::const_iterator aIter(GetColorStops().begin()); + while (basegfx::fTools::lessOrEqual(aIter->getStopOffset(), 0.5)) + { + BColorStop aNextStop(std::clamp((*aIter).getStopOffset() * 2.0, 0.0, 1.0), + (*aIter).getStopColor()); + aAxialColorStops.push_back(aNextStop); + ++aIter; + } + // Axial gradients have outmost color as last color stop. + aAxialColorStops.reverseColorStops(); + + SetColorStops(aAxialColorStops); +} + +void BGradient::tryToApplyAxial() +{ + // only need to do something if css::awt::GradientStyle_AXIAL, else done + if (GetGradientStyle() != css::awt::GradientStyle_AXIAL) + return; + + // apply the change to color stops + aColorStops.doApplyAxial(); + + // set style to GradientStyle_LINEAR + SetGradientStyle(css::awt::GradientStyle_LINEAR); +} + +void BGradient::tryToApplySteps() +{ + // check for zero or invalid steps setting -> done + if (0 == GetSteps() || GetSteps() > 100) + return; + + // do the action + aColorStops.doApplySteps(GetSteps()); + + // set value to default + SetSteps(0); +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/canvastools.cxx b/basegfx/source/tools/canvastools.cxx new file mode 100644 index 0000000000..70f4787710 --- /dev/null +++ b/basegfx/source/tools/canvastools.cxx @@ -0,0 +1,474 @@ +/* -*- 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 <com/sun/star/geometry/RealSize2D.hpp> +#include <com/sun/star/geometry/RealPoint2D.hpp> +#include <com/sun/star/geometry/RealRectangle2D.hpp> +#include <com/sun/star/geometry/RealRectangle3D.hpp> +#include <com/sun/star/geometry/RealBezierSegment2D.hpp> +#include <com/sun/star/geometry/AffineMatrix2D.hpp> +#include <com/sun/star/geometry/AffineMatrix3D.hpp> +#include <com/sun/star/geometry/IntegerSize2D.hpp> +#include <com/sun/star/geometry/IntegerRectangle2D.hpp> +#include <com/sun/star/lang/IllegalArgumentException.hpp> +#include <com/sun/star/rendering/XPolyPolygon2D.hpp> +#include <com/sun/star/rendering/XGraphicDevice.hpp> +#include <com/sun/star/awt/Rectangle.hpp> +#include <basegfx/utils/unopolypolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/matrix/b3dhommatrix.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/vector/b2dsize.hxx> +#include <basegfx/vector/b2ivector.hxx> +#include <basegfx/range/b3drange.hxx> +#include <basegfx/range/b2irange.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/utils/canvastools.hxx> + +using namespace ::com::sun::star; + +namespace basegfx::unotools +{ + namespace + { + uno::Sequence< geometry::RealBezierSegment2D > bezierSequenceFromB2DPolygon(const ::basegfx::B2DPolygon& rPoly) + { + const sal_uInt32 nPointCount(rPoly.count()); + uno::Sequence< geometry::RealBezierSegment2D > outputSequence(nPointCount); + geometry::RealBezierSegment2D* pOutput = outputSequence.getArray(); + + // fill sequences and imply closed polygon on this implementation layer + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const basegfx::B2DPoint aStart(rPoly.getB2DPoint(a)); + const basegfx::B2DPoint aControlA(rPoly.getNextControlPoint(a)); + const basegfx::B2DPoint aControlB(rPoly.getPrevControlPoint((a + 1) % nPointCount)); + + pOutput[a] = geometry::RealBezierSegment2D( + aStart.getX(), aStart.getY(), + aControlA.getX(), aControlA.getY(), + aControlB.getX(), aControlB.getY()); + } + + return outputSequence; + } + + uno::Sequence< geometry::RealPoint2D > pointSequenceFromB2DPolygon( const ::basegfx::B2DPolygon& rPoly ) + { + const sal_uInt32 nNumPoints( rPoly.count() ); + + uno::Sequence< geometry::RealPoint2D > outputSequence( nNumPoints ); + geometry::RealPoint2D* pOutput = outputSequence.getArray(); + + // fill sequence from polygon + sal_uInt32 i; + for( i=0; i<nNumPoints; ++i ) + { + const ::basegfx::B2DPoint aPoint( rPoly.getB2DPoint(i) ); + + pOutput[i] = geometry::RealPoint2D( aPoint.getX(), + aPoint.getY() ); + } + + return outputSequence; + } + } + + uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > > bezierSequenceSequenceFromB2DPolyPolygon( const ::basegfx::B2DPolyPolygon& rPolyPoly ) + { + const sal_uInt32 nNumPolies( rPolyPoly.count() ); + sal_uInt32 i; + + uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > > outputSequence( nNumPolies ); + uno::Sequence< geometry::RealBezierSegment2D >* pOutput = outputSequence.getArray(); + + for( i=0; i<nNumPolies; ++i ) + { + pOutput[i] = bezierSequenceFromB2DPolygon( rPolyPoly.getB2DPolygon(i) ); + } + + return outputSequence; + } + + uno::Sequence< uno::Sequence< geometry::RealPoint2D > > pointSequenceSequenceFromB2DPolyPolygon( const ::basegfx::B2DPolyPolygon& rPolyPoly ) + { + const sal_uInt32 nNumPolies( rPolyPoly.count() ); + sal_uInt32 i; + + uno::Sequence< uno::Sequence< geometry::RealPoint2D > > outputSequence( nNumPolies ); + uno::Sequence< geometry::RealPoint2D >* pOutput = outputSequence.getArray(); + + for( i=0; i<nNumPolies; ++i ) + { + pOutput[i] = pointSequenceFromB2DPolygon( rPolyPoly.getB2DPolygon(i) ); + } + + return outputSequence; + } + + uno::Reference< rendering::XPolyPolygon2D > xPolyPolygonFromB2DPolygon( const uno::Reference< rendering::XGraphicDevice >& xGraphicDevice, + const ::basegfx::B2DPolygon& rPoly ) + { + uno::Reference< rendering::XPolyPolygon2D > xRes; + + if( !xGraphicDevice.is() ) + return xRes; + + if( rPoly.areControlPointsUsed() ) + { + uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > > outputSequence{ bezierSequenceFromB2DPolygon( rPoly )}; + + xRes = xGraphicDevice->createCompatibleBezierPolyPolygon( outputSequence ); + } + else + { + uno::Sequence< uno::Sequence< geometry::RealPoint2D > > outputSequence{ + pointSequenceFromB2DPolygon( rPoly )}; + + xRes = xGraphicDevice->createCompatibleLinePolyPolygon( outputSequence ); + } + + if( xRes.is() && rPoly.isClosed() ) + xRes->setClosed( 0, true ); + + return xRes; + } + + uno::Reference< rendering::XPolyPolygon2D > xPolyPolygonFromB2DPolyPolygon( const uno::Reference< rendering::XGraphicDevice >& xGraphicDevice, + const ::basegfx::B2DPolyPolygon& rPolyPoly ) + { + uno::Reference< rendering::XPolyPolygon2D > xRes; + + if( !xGraphicDevice.is() ) + return xRes; + + const sal_uInt32 nNumPolies( rPolyPoly.count() ); + sal_uInt32 i; + + if( rPolyPoly.areControlPointsUsed() ) + { + xRes = xGraphicDevice->createCompatibleBezierPolyPolygon( + bezierSequenceSequenceFromB2DPolyPolygon( rPolyPoly ) ); + } + else + { + xRes = xGraphicDevice->createCompatibleLinePolyPolygon( + pointSequenceSequenceFromB2DPolyPolygon( rPolyPoly ) ); + } + + for( i=0; i<nNumPolies; ++i ) + { + xRes->setClosed( i, rPolyPoly.getB2DPolygon(i).isClosed() ); + } + + return xRes; + } + + ::basegfx::B2DPolygon polygonFromPoint2DSequence( const uno::Sequence< geometry::RealPoint2D >& points ) + { + const sal_Int32 nCurrSize( points.getLength() ); + + ::basegfx::B2DPolygon aPoly; + + for( sal_Int32 nCurrPoint=0; nCurrPoint<nCurrSize; ++nCurrPoint ) + aPoly.append( b2DPointFromRealPoint2D( points[nCurrPoint] ) ); + + return aPoly; + } + + ::basegfx::B2DPolyPolygon polyPolygonFromPoint2DSequenceSequence( const uno::Sequence< uno::Sequence< geometry::RealPoint2D > >& points ) + { + ::basegfx::B2DPolyPolygon aRes; + + for( const auto & p : points ) + { + aRes.append( polygonFromPoint2DSequence( p ) ); + } + + return aRes; + } + + ::basegfx::B2DPolygon polygonFromBezier2DSequence( const uno::Sequence< geometry::RealBezierSegment2D >& curves ) + { + const sal_Int32 nSize(curves.getLength()); + basegfx::B2DPolygon aRetval; + + if(nSize) + { + // prepare start with providing a start point. Use the first point from + // the sequence for this + const geometry::RealBezierSegment2D& rFirstSegment(curves[0]); // #i79917# first segment, not last + aRetval.append(basegfx::B2DPoint(rFirstSegment.Px, rFirstSegment.Py)); + + for(sal_Int32 a(0); a < nSize; a++) + { + const geometry::RealBezierSegment2D& rCurrSegment(curves[a]); + const geometry::RealBezierSegment2D& rNextSegment(curves[(a + 1) % nSize]); + + // append curved edge with the control points and the next point + aRetval.appendBezierSegment( + basegfx::B2DPoint(rCurrSegment.C1x, rCurrSegment.C1y), + basegfx::B2DPoint(rCurrSegment.C2x, rCurrSegment.C2y), // #i79917# Argh! An x for an y!! + basegfx::B2DPoint(rNextSegment.Px, rNextSegment.Py)); + } + + // rescue the control point and remove the now double-added point + aRetval.setPrevControlPoint(0, aRetval.getPrevControlPoint(aRetval.count() - 1)); + aRetval.remove(aRetval.count() - 1); + } + + return aRetval; + } + + ::basegfx::B2DPolyPolygon polyPolygonFromBezier2DSequenceSequence( const uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > >& curves ) + { + ::basegfx::B2DPolyPolygon aRes; + + for( const auto & c : curves ) + { + aRes.append( polygonFromBezier2DSequence( c ) ); + } + + return aRes; + } + + ::basegfx::B2DPolyPolygon b2DPolyPolygonFromXPolyPolygon2D( const uno::Reference< rendering::XPolyPolygon2D >& xPoly ) + { + ::basegfx::unotools::UnoPolyPolygon* pPolyImpl = + dynamic_cast< ::basegfx::unotools::UnoPolyPolygon* >( xPoly.get() ); + + if( pPolyImpl ) + { + return pPolyImpl->getPolyPolygon(); + } + else + { + // not a known implementation object - try data source + // interfaces + const sal_Int32 nPolys( xPoly->getNumberOfPolygons() ); + + uno::Reference< rendering::XBezierPolyPolygon2D > xBezierPoly( + xPoly, + uno::UNO_QUERY ); + + if( xBezierPoly.is() ) + { + return ::basegfx::unotools::polyPolygonFromBezier2DSequenceSequence( + xBezierPoly->getBezierSegments( 0, + nPolys, + 0, + -1 ) ); + } + else + { + uno::Reference< rendering::XLinePolyPolygon2D > xLinePoly( + xPoly, + uno::UNO_QUERY ); + + // no implementation class and no data provider + // found - contract violation. + if( !xLinePoly.is() ) + { + throw lang::IllegalArgumentException( + "basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(): Invalid input" + "poly-polygon, cannot retrieve vertex data", + uno::Reference< uno::XInterface >(), + 0 ); + } + + return ::basegfx::unotools::polyPolygonFromPoint2DSequenceSequence( + xLinePoly->getPoints( 0, + nPolys, + 0, + -1 )); + } + } + } + + ::basegfx::B2DHomMatrix& homMatrixFromAffineMatrix( ::basegfx::B2DHomMatrix& output, + const geometry::AffineMatrix2D& input ) + { + // ensure last row is [0,0,1] (and optimized away) + output.identity(); + + output.set(0,0, input.m00); + output.set(0,1, input.m01); + output.set(0,2, input.m02); + output.set(1,0, input.m10); + output.set(1,1, input.m11); + output.set(1,2, input.m12); + + return output; + } + + ::basegfx::B3DHomMatrix homMatrixFromAffineMatrix3D( const ::css::geometry::AffineMatrix3D& input ) + { + ::basegfx::B3DHomMatrix output; + + output.set(0,0, input.m00); + output.set(0,1, input.m01); + output.set(0,2, input.m02); + output.set(0,3, input.m03); + + output.set(1,0, input.m10); + output.set(1,1, input.m11); + output.set(1,2, input.m12); + output.set(1,3, input.m13); + + output.set(2,0, input.m20); + output.set(2,1, input.m21); + output.set(2,2, input.m22); + output.set(2,3, input.m23); + + return output; + } + + geometry::AffineMatrix2D& affineMatrixFromHomMatrix( geometry::AffineMatrix2D& output, + const ::basegfx::B2DHomMatrix& input) + { + output.m00 = input.get(0,0); + output.m01 = input.get(0,1); + output.m02 = input.get(0,2); + output.m10 = input.get(1,0); + output.m11 = input.get(1,1); + output.m12 = input.get(1,2); + + return output; + } + + geometry::AffineMatrix3D& affineMatrixFromHomMatrix3D( + geometry::AffineMatrix3D& output, + const ::basegfx::B3DHomMatrix& input) + { + output.m00 = input.get(0,0); + output.m01 = input.get(0,1); + output.m02 = input.get(0,2); + output.m03 = input.get(0,3); + + output.m10 = input.get(1,0); + output.m11 = input.get(1,1); + output.m12 = input.get(1,2); + output.m13 = input.get(1,3); + + output.m20 = input.get(2,0); + output.m21 = input.get(2,1); + output.m22 = input.get(2,2); + output.m23 = input.get(2,3); + + return output; + } + + geometry::RealSize2D size2DFromB2DSize(const ::basegfx::B2DSize& rSize) + { + return geometry::RealSize2D(rSize.getWidth(), rSize.getHeight()); + } + + geometry::RealPoint2D point2DFromB2DPoint( const ::basegfx::B2DPoint& rPoint ) + { + return geometry::RealPoint2D( rPoint.getX(), + rPoint.getY() ); + } + + geometry::RealRectangle2D rectangle2DFromB2DRectangle( const ::basegfx::B2DRange& rRect ) + { + return geometry::RealRectangle2D( rRect.getMinX(), + rRect.getMinY(), + rRect.getMaxX(), + rRect.getMaxY() ); + } + + geometry::RealRectangle3D rectangle3DFromB3DRectangle( const ::basegfx::B3DRange& rRect ) + { + return geometry::RealRectangle3D( rRect.getMinX(), + rRect.getMinY(), + rRect.getMinZ(), + rRect.getMaxX(), + rRect.getMaxY(), + rRect.getMaxZ()); + } + + ::basegfx::B2DPoint b2DPointFromRealPoint2D( const geometry::RealPoint2D& rPoint ) + { + return ::basegfx::B2DPoint( rPoint.X, + rPoint.Y ); + } + + ::basegfx::B2DRange b2DRectangleFromRealRectangle2D( const geometry::RealRectangle2D& rRect ) + { + return ::basegfx::B2DRange( rRect.X1, + rRect.Y1, + rRect.X2, + rRect.Y2 ); + } + + ::basegfx::B3DRange b3DRectangleFromRealRectangle3D( const geometry::RealRectangle3D& rRect ) + { + return ::basegfx::B3DRange( rRect.X1, + rRect.Y1, + rRect.Z1, + rRect.X2, + rRect.Y2, + rRect.Z2); + } + + geometry::IntegerSize2D integerSize2DFromB2ISize( const ::basegfx::B2ISize& rSize ) + { + return geometry::IntegerSize2D( rSize.getWidth(), + rSize.getHeight() ); + } + + basegfx::B2ISize b2ISizeFromIntegerSize2D( const geometry::IntegerSize2D& rSize ) + { + return basegfx::B2ISize(rSize.Width, rSize.Height); + } + + ::basegfx::B2IRange b2IRectangleFromIntegerRectangle2D( const geometry::IntegerRectangle2D& rRectangle ) + { + return ::basegfx::B2IRange( rRectangle.X1, rRectangle.Y1, + rRectangle.X2, rRectangle.Y2 ); + } + + ::basegfx::B2IRange b2IRectangleFromAwtRectangle( const awt::Rectangle& rRect ) + { + return ::basegfx::B2IRange( rRect.X, + rRect.Y, + rRect.X + rRect.Width, + rRect.Y + rRect.Height ); + } + + ::basegfx::B2IRange b2ISurroundingRangeFromB2DRange( const ::basegfx::B2DRange& rRange ) + { + return ::basegfx::B2IRange( static_cast<sal_Int32>( floor(rRange.getMinX()) ), + static_cast<sal_Int32>( floor(rRange.getMinY()) ), + static_cast<sal_Int32>( ceil(rRange.getMaxX()) ), + static_cast<sal_Int32>( ceil(rRange.getMaxY()) ) ); + } + + ::basegfx::B2DRange b2DSurroundingIntegerRangeFromB2DRange( const ::basegfx::B2DRange& rRange ) + { + return ::basegfx::B2DRange( floor(rRange.getMinX()), + floor(rRange.getMinY()), + ceil(rRange.getMaxX()), + ceil(rRange.getMaxY()) ); + } + +} // namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/gradienttools.cxx b/basegfx/source/tools/gradienttools.cxx new file mode 100644 index 0000000000..8f3e8ae83c --- /dev/null +++ b/basegfx/source/tools/gradienttools.cxx @@ -0,0 +1,756 @@ +/* -*- 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 <basegfx/utils/gradienttools.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> +#include <com/sun/star/awt/Gradient2.hpp> +#include <osl/endian.h> + +#include <algorithm> +#include <cmath> + +namespace basegfx +{ + bool ODFGradientInfo::operator==(const ODFGradientInfo& rODFGradientInfo) const + { + return getTextureTransform() == rODFGradientInfo.getTextureTransform() + && getAspectRatio() == rODFGradientInfo.getAspectRatio() + && getRequestedSteps() == rODFGradientInfo.getRequestedSteps(); + } + + const B2DHomMatrix& ODFGradientInfo::getBackTextureTransform() const + { + if(maBackTextureTransform.isIdentity()) + { + const_cast< ODFGradientInfo* >(this)->maBackTextureTransform = getTextureTransform(); + const_cast< ODFGradientInfo* >(this)->maBackTextureTransform.invert(); + } + + return maBackTextureTransform; + } + + /** Most of the setup for linear & axial gradient is the same, except + for the border treatment. Factored out here. + */ + static ODFGradientInfo init1DGradientInfo( + const B2DRange& rTargetRange, + sal_uInt32 nSteps, + double fBorder, + double fAngle, + bool bAxial) + { + B2DHomMatrix aTextureTransform; + + fAngle = -fAngle; + + double fTargetSizeX(rTargetRange.getWidth()); + double fTargetSizeY(rTargetRange.getHeight()); + double fTargetOffsetX(rTargetRange.getMinX()); + double fTargetOffsetY(rTargetRange.getMinY()); + + // add object expansion + const bool bAngleUsed(!fTools::equalZero(fAngle)); + + if(bAngleUsed) + { + const double fAbsCos(fabs(cos(fAngle))); + const double fAbsSin(fabs(sin(fAngle))); + const double fNewX(fTargetSizeX * fAbsCos + fTargetSizeY * fAbsSin); + const double fNewY(fTargetSizeY * fAbsCos + fTargetSizeX * fAbsSin); + + fTargetOffsetX -= (fNewX - fTargetSizeX) / 2.0; + fTargetOffsetY -= (fNewY - fTargetSizeY) / 2.0; + fTargetSizeX = fNewX; + fTargetSizeY = fNewY; + } + + const double fSizeWithoutBorder(1.0 - fBorder); + + if(bAxial) + { + aTextureTransform.scale(1.0, fSizeWithoutBorder * 0.5); + aTextureTransform.translate(0.0, 0.5); + } + else + { + if(!fTools::equal(fSizeWithoutBorder, 1.0)) + { + aTextureTransform.scale(1.0, fSizeWithoutBorder); + aTextureTransform.translate(0.0, fBorder); + } + } + + aTextureTransform.scale(fTargetSizeX, fTargetSizeY); + + // add texture rotate after scale to keep perpendicular angles + if(bAngleUsed) + { + const B2DPoint aCenter(0.5 * fTargetSizeX, 0.5 * fTargetSizeY); + + aTextureTransform *= basegfx::utils::createRotateAroundPoint(aCenter, fAngle); + } + + // add object translate + aTextureTransform.translate(fTargetOffsetX, fTargetOffsetY); + + // prepare aspect for texture + const double fAspectRatio(fTools::equalZero(fTargetSizeY) ? 1.0 : fTargetSizeX / fTargetSizeY); + + return ODFGradientInfo(aTextureTransform, fAspectRatio, nSteps); + } + + /** Most of the setup for radial & ellipsoidal gradient is the same, + except for the border treatment. Factored out here. + */ + static ODFGradientInfo initEllipticalGradientInfo( + const B2DRange& rTargetRange, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle, + bool bCircular) + { + B2DHomMatrix aTextureTransform; + + fAngle = -fAngle; + + double fTargetSizeX(rTargetRange.getWidth()); + double fTargetSizeY(rTargetRange.getHeight()); + double fTargetOffsetX(rTargetRange.getMinX()); + double fTargetOffsetY(rTargetRange.getMinY()); + + // add object expansion + if(bCircular) + { + const double fOriginalDiag(std::hypot(fTargetSizeX, fTargetSizeY)); + + fTargetOffsetX -= (fOriginalDiag - fTargetSizeX) / 2.0; + fTargetOffsetY -= (fOriginalDiag - fTargetSizeY) / 2.0; + fTargetSizeX = fOriginalDiag; + fTargetSizeY = fOriginalDiag; + } + else + { + fTargetOffsetX -= ((M_SQRT2 - 1) / 2.0 ) * fTargetSizeX; + fTargetOffsetY -= ((M_SQRT2 - 1) / 2.0 ) * fTargetSizeY; + fTargetSizeX = M_SQRT2 * fTargetSizeX; + fTargetSizeY = M_SQRT2 * fTargetSizeY; + } + + const double fHalfBorder((1.0 - fBorder) * 0.5); + + aTextureTransform.scale(fHalfBorder, fHalfBorder); + aTextureTransform.translate(0.5, 0.5); + aTextureTransform.scale(fTargetSizeX, fTargetSizeY); + + // add texture rotate after scale to keep perpendicular angles + if(!bCircular && !fTools::equalZero(fAngle)) + { + const B2DPoint aCenter(0.5 * fTargetSizeX, 0.5 * fTargetSizeY); + + aTextureTransform *= basegfx::utils::createRotateAroundPoint(aCenter, fAngle); + } + + // add defined offsets after rotation + if(!fTools::equal(0.5, rOffset.getX()) || !fTools::equal(0.5, rOffset.getY())) + { + // use original target size + fTargetOffsetX += (rOffset.getX() - 0.5) * rTargetRange.getWidth(); + fTargetOffsetY += (rOffset.getY() - 0.5) * rTargetRange.getHeight(); + } + + // add object translate + aTextureTransform.translate(fTargetOffsetX, fTargetOffsetY); + + // prepare aspect for texture + const double fAspectRatio(fTargetSizeY == 0.0 ? 1.0 : (fTargetSizeX / fTargetSizeY)); + + return ODFGradientInfo(aTextureTransform, fAspectRatio, nSteps); + } + + /** Setup for rect & square gradient is exactly the same. Factored out + here. + */ + static ODFGradientInfo initRectGradientInfo( + const B2DRange& rTargetRange, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle, + bool bSquare) + { + B2DHomMatrix aTextureTransform; + + fAngle = -fAngle; + + double fTargetSizeX(rTargetRange.getWidth()); + double fTargetSizeY(rTargetRange.getHeight()); + double fTargetOffsetX(rTargetRange.getMinX()); + double fTargetOffsetY(rTargetRange.getMinY()); + + // add object expansion + if(bSquare) + { + const double fSquareWidth(std::max(fTargetSizeX, fTargetSizeY)); + + fTargetOffsetX -= (fSquareWidth - fTargetSizeX) / 2.0; + fTargetOffsetY -= (fSquareWidth - fTargetSizeY) / 2.0; + fTargetSizeX = fTargetSizeY = fSquareWidth; + } + + // add object expansion + const bool bAngleUsed(!fTools::equalZero(fAngle)); + + if(bAngleUsed) + { + const double fAbsCos(fabs(cos(fAngle))); + const double fAbsSin(fabs(sin(fAngle))); + const double fNewX(fTargetSizeX * fAbsCos + fTargetSizeY * fAbsSin); + const double fNewY(fTargetSizeY * fAbsCos + fTargetSizeX * fAbsSin); + + fTargetOffsetX -= (fNewX - fTargetSizeX) / 2.0; + fTargetOffsetY -= (fNewY - fTargetSizeY) / 2.0; + fTargetSizeX = fNewX; + fTargetSizeY = fNewY; + } + + const double fHalfBorder((1.0 - fBorder) * 0.5); + + aTextureTransform.scale(fHalfBorder, fHalfBorder); + aTextureTransform.translate(0.5, 0.5); + aTextureTransform.scale(fTargetSizeX, fTargetSizeY); + + // add texture rotate after scale to keep perpendicular angles + if(bAngleUsed) + { + const B2DPoint aCenter(0.5 * fTargetSizeX, 0.5 * fTargetSizeY); + + aTextureTransform *= basegfx::utils::createRotateAroundPoint(aCenter, fAngle); + } + + // add defined offsets after rotation + if(!fTools::equal(0.5, rOffset.getX()) || !fTools::equal(0.5, rOffset.getY())) + { + // use original target size + fTargetOffsetX += (rOffset.getX() - 0.5) * rTargetRange.getWidth(); + fTargetOffsetY += (rOffset.getY() - 0.5) * rTargetRange.getHeight(); + } + + // add object translate + aTextureTransform.translate(fTargetOffsetX, fTargetOffsetY); + + // prepare aspect for texture + const double fAspectRatio(fTargetSizeY == 0.0 ? 1.0 : (fTargetSizeX / fTargetSizeY)); + + return ODFGradientInfo(aTextureTransform, fAspectRatio, nSteps); + } + + namespace utils + { + /* Tooling method to extract data from given BGradient + to ColorStops, doing some corrections, partially based + on given SingleColor */ + void prepareColorStops( + const basegfx::BGradient& rGradient, + BColorStops& rColorStops, + BColor& rSingleColor) + { + if (rGradient.GetColorStops().isSingleColor(rSingleColor)) + { + // when single color, preserve value in rSingleColor + // and clear the ColorStops, done. + rColorStops.clear(); + return; + } + + const bool bAdaptStartEndIntensity(100 != rGradient.GetStartIntens() || 100 != rGradient.GetEndIntens()); + const bool bAdaptBorder(0 != rGradient.GetBorder()); + + if (!bAdaptStartEndIntensity && !bAdaptBorder) + { + // copy unchanged ColorStops & done + rColorStops = rGradient.GetColorStops(); + return; + } + + // prepare a copy to work on + basegfx::BGradient aWorkCopy(rGradient); + + if (bAdaptStartEndIntensity) + { + aWorkCopy.tryToApplyStartEndIntensity(); + + // this can again lead to single color (e.g. both zero, so + // all black), so check again for it + if (aWorkCopy.GetColorStops().isSingleColor(rSingleColor)) + { + rColorStops.clear(); + return; + } + } + + if (bAdaptBorder) + { + aWorkCopy.tryToApplyBorder(); + } + + // extract ColorStops, that's all we need here + rColorStops = aWorkCopy.GetColorStops(); + } + + /* Tooling method to synchronize the given ColorStops. + The intention is that a color GradientStops and an + alpha/transparence GradientStops gets synchronized + for export. */ + void synchronizeColorStops( + BColorStops& rColorStops, + BColorStops& rAlphaStops, + const BColor& rSingleColor, + const BColor& rSingleAlpha) + { + if (rColorStops.empty()) + { + if (rAlphaStops.empty()) + { + // no AlphaStops and no ColorStops + // create two-stop fallbacks for both + rColorStops = BColorStops { + BColorStop(0.0, rSingleColor), + BColorStop(1.0, rSingleColor) }; + rAlphaStops = BColorStops { + BColorStop(0.0, rSingleAlpha), + BColorStop(1.0, rSingleAlpha) }; + } + else + { + // AlphaStops but no ColorStops + // create fallback synched with existing AlphaStops + for (const auto& cand : rAlphaStops) + { + rColorStops.emplace_back(cand.getStopOffset(), rSingleColor); + } + } + + // preparations complete, we are done + return; + } + else if (rAlphaStops.empty()) + { + // ColorStops but no AlphaStops + // create fallback AlphaStops synched with existing ColorStops using SingleAlpha + for (const auto& cand : rColorStops) + { + rAlphaStops.emplace_back(cand.getStopOffset(), rSingleAlpha); + } + + // preparations complete, we are done + return; + } + + // here we have ColorStops and AlphaStops not empty. Check if we need to + // synchronize both or if they are already usable/in a synched state so + // that they have same count and same StopOffsets + bool bNeedToSyncronize(rColorStops.size() != rAlphaStops.size()); + + if (!bNeedToSyncronize) + { + // check for same StopOffsets + BColorStops::const_iterator aCurrColor(rColorStops.begin()); + BColorStops::const_iterator aCurrAlpha(rAlphaStops.begin()); + + while (!bNeedToSyncronize && + aCurrColor != rColorStops.end() && + aCurrAlpha != rAlphaStops.end()) + { + if (fTools::equal(aCurrColor->getStopOffset(), aCurrAlpha->getStopOffset())) + { + aCurrColor++; + aCurrAlpha++; + } + else + { + bNeedToSyncronize = true; + } + } + } + + if (bNeedToSyncronize) + { + // synchronize sizes & StopOffsets + BColorStops::const_iterator aCurrColor(rColorStops.begin()); + BColorStops::const_iterator aCurrAlpha(rAlphaStops.begin()); + BColorStops aNewColor; + BColorStops aNewAlpha; + BColorStops::BColorStopRange aColorStopRange; + BColorStops::BColorStopRange aAlphaStopRange; + bool bRealChange(false); + + do { + const bool bColor(aCurrColor != rColorStops.end()); + const bool bAlpha(aCurrAlpha != rAlphaStops.end()); + + if (bColor && bAlpha) + { + const double fColorOff(aCurrColor->getStopOffset()); + const double fAlphaOff(aCurrAlpha->getStopOffset()); + + if (fTools::less(fColorOff, fAlphaOff)) + { + // copy color, create alpha + aNewColor.emplace_back(fColorOff, aCurrColor->getStopColor()); + aNewAlpha.emplace_back(fColorOff, rAlphaStops.getInterpolatedBColor(fColorOff, 0, aAlphaStopRange)); + bRealChange = true; + aCurrColor++; + } + else if (fTools::more(fColorOff, fAlphaOff)) + { + // copy alpha, create color + aNewColor.emplace_back(fAlphaOff, rColorStops.getInterpolatedBColor(fAlphaOff, 0, aColorStopRange)); + aNewAlpha.emplace_back(fAlphaOff, aCurrAlpha->getStopColor()); + bRealChange = true; + aCurrAlpha++; + } + else + { + // equal: copy both, advance + aNewColor.emplace_back(fColorOff, aCurrColor->getStopColor()); + aNewAlpha.emplace_back(fAlphaOff, aCurrAlpha->getStopColor()); + aCurrColor++; + aCurrAlpha++; + } + } + else if (bColor) + { + const double fColorOff(aCurrColor->getStopOffset()); + aNewAlpha.emplace_back(fColorOff, rAlphaStops.getInterpolatedBColor(fColorOff, 0, aAlphaStopRange)); + aNewColor.emplace_back(fColorOff, aCurrColor->getStopColor()); + bRealChange = true; + aCurrColor++; + } + else if (bAlpha) + { + const double fAlphaOff(aCurrAlpha->getStopOffset()); + aNewColor.emplace_back(fAlphaOff, rColorStops.getInterpolatedBColor(fAlphaOff, 0, aColorStopRange)); + aNewAlpha.emplace_back(fAlphaOff, aCurrAlpha->getStopColor()); + bRealChange = true; + aCurrAlpha++; + } + else + { + // no more input, break do..while loop + break; + } + } + while(true); + + if (bRealChange) + { + // copy on 'real' change, that means data was added. + // This should always be the cease and should have been + // detected as such above, see bNeedToSyncronize + rColorStops = aNewColor; + rAlphaStops = aNewAlpha; // MCGR: tdf#155537 used wrong result here + } + } + } + + sal_uInt32 calculateNumberOfSteps( + sal_uInt32 nRequestedSteps, + const BColor& rStart, + const BColor& rEnd) + { + const sal_uInt32 nMaxSteps(sal_uInt32((rStart.getMaximumDistance(rEnd) * 127.5) + 0.5)); + + if (0 == nRequestedSteps) + { + nRequestedSteps = nMaxSteps; + } + + if(nRequestedSteps > nMaxSteps) + { + nRequestedSteps = nMaxSteps; + } + + return std::max(sal_uInt32(1), nRequestedSteps); + } + + ODFGradientInfo createLinearODFGradientInfo( + const B2DRange& rTargetArea, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return init1DGradientInfo( + rTargetArea, + nSteps, + fBorder, + fAngle, + false); + } + + ODFGradientInfo createAxialODFGradientInfo( + const B2DRange& rTargetArea, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return init1DGradientInfo( + rTargetArea, + nSteps, + fBorder, + fAngle, + true); + } + + ODFGradientInfo createRadialODFGradientInfo( + const B2DRange& rTargetArea, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder) + { + return initEllipticalGradientInfo( + rTargetArea, + rOffset, + nSteps, + fBorder, + 0.0, + true); + } + + ODFGradientInfo createEllipticalODFGradientInfo( + const B2DRange& rTargetArea, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return initEllipticalGradientInfo( + rTargetArea, + rOffset, + nSteps, + fBorder, + fAngle, + false); + } + + ODFGradientInfo createSquareODFGradientInfo( + const B2DRange& rTargetArea, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return initRectGradientInfo( + rTargetArea, + rOffset, + nSteps, + fBorder, + fAngle, + true); + } + + ODFGradientInfo createRectangularODFGradientInfo( + const B2DRange& rTargetArea, + const B2DVector& rOffset, + sal_uInt32 nSteps, + double fBorder, + double fAngle) + { + return initRectGradientInfo( + rTargetArea, + rOffset, + nSteps, + fBorder, + fAngle, + false); + } + + double getLinearGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + + // Ignore X, this is not needed at all for Y-Oriented gradients + // if(aCoor.getX() < 0.0 || aCoor.getX() > 1.0) + // { + // return 0.0; + // } + + if(aCoor.getY() <= 0.0) + { + return 0.0; // start value for inside + } + + if(aCoor.getY() >= 1.0) + { + return 1.0; // end value for outside + } + + return aCoor.getY(); + } + + double getAxialGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + + // Ignore X, this is not needed at all for Y-Oriented gradients + //if(aCoor.getX() < 0.0 || aCoor.getX() > 1.0) + //{ + // return 0.0; + //} + + const double fAbsY(fabs(aCoor.getY())); + + if(fAbsY >= 1.0) + { + return 1.0; // use end value when outside in Y + } + + return fAbsY; + } + + double getRadialGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + + if(aCoor.getX() < -1.0 || aCoor.getX() > 1.0 || aCoor.getY() < -1.0 || aCoor.getY() > 1.0) + { + return 0.0; + } + + return 1.0 - std::hypot(aCoor.getX(), aCoor.getY()); + } + + double getEllipticalGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + + if(aCoor.getX() < -1.0 || aCoor.getX() > 1.0 || aCoor.getY() < -1.0 || aCoor.getY() > 1.0) + { + return 0.0; + } + + double fAspectRatio(rGradInfo.getAspectRatio()); + double t(1.0); + + // MCGR: Similar to getRectangularGradientAlpha (please + // see there) we need to use aspect ratio here. Due to + // initEllipticalGradientInfo using M_SQRT2 to make this + // gradient look 'nicer' this correction seems not 100% + // correct, but is close enough for now + if(fAspectRatio > 1.0) + { + t = 1.0 - std::hypot(aCoor.getX() / fAspectRatio, aCoor.getY()); + } + else if(fAspectRatio > 0.0) + { + t = 1.0 - std::hypot(aCoor.getX(), aCoor.getY() * fAspectRatio); + } + + return t; + } + + double getSquareGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + const double fAbsX(fabs(aCoor.getX())); + + if(fAbsX >= 1.0) + { + return 0.0; + } + + const double fAbsY(fabs(aCoor.getY())); + + if(fAbsY >= 1.0) + { + return 0.0; + } + + return 1.0 - std::max(fAbsX, fAbsY); + } + + double getRectangularGradientAlpha(const B2DPoint& rUV, const ODFGradientInfo& rGradInfo) + { + const B2DPoint aCoor(rGradInfo.getBackTextureTransform() * rUV); + double fAbsX(fabs(aCoor.getX())); + + if(fAbsX >= 1.0) + { + return 0.0; + } + + double fAbsY(fabs(aCoor.getY())); + + if(fAbsY >= 1.0) + { + return 0.0; + } + + // MCGR: Visualizations using the texturing method for + // displaying gradients (getBackTextureTransform is + // involved) show wrong results for GradientElliptical + // and GradientRect, this can be best seen when using + // less steps, e.g. just four. This thus has influence + // on cppcanvas (slideshow) and 3D textures, so needs + // to be corrected. + // Missing is to use the aspect ratio of the object + // in this [-1, -1, 1, 1] unified coordinate space + // after getBackTextureTransform is applied. Optically + // in the larger direction of the texturing the color + // step distances are too big *because* we are in that + // unit range now. + // To correct that, a kind of 'limo stretching' needs to + // be applied, adding space around the center + // proportional to the aspect ratio, so the intuitive + // idea would be to do + // + // fAbsX' = ((fAspectRatio - 1) + fAbsX) / fAspectRatio + // + // which scales from the center. This does not work, and + // after some thoughts it's clear why: It's not the + // position that needs to be moved (this cannot be + // changed), but the position *before* that scale has + // to be determined to get the correct, shifted color + // for the already 'new' position. Thus, turn around + // the expression as + // + // fAbsX' * fAspectRatio = fAspectRatio - 1 + fAbsX + // fAbsX' * fAspectRatio - fAspectRatio + 1 = fAbsX + // fAbsX = (fAbsX' - 1) * fAspectRatio + 1 + // + // This works and can even be simply adapted for + // fAspectRatio < 1.0 aka vertical is bigger. + double fAspectRatio(rGradInfo.getAspectRatio()); + if(fAspectRatio > 1.0) + { + fAbsX = ((fAbsX - 1) * fAspectRatio) + 1; + } + else if(fAspectRatio > 0.0) + { + fAbsY = ((fAbsY - 1) / fAspectRatio) + 1; + } + + return 1.0 - std::max(fAbsX, fAbsY); + } + } // namespace utils +} // namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/keystoplerp.cxx b/basegfx/source/tools/keystoplerp.cxx new file mode 100644 index 0000000000..e5d0d76304 --- /dev/null +++ b/basegfx/source/tools/keystoplerp.cxx @@ -0,0 +1,90 @@ +/* -*- 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 <basegfx/utils/keystoplerp.hxx> +#include <com/sun/star/uno/Sequence.hxx> +#include <osl/diagnose.h> + +#include <algorithm> + +static void validateInput(const std::vector<double>& rKeyStops) +{ +#ifdef DBG_UTIL + OSL_ENSURE( rKeyStops.size() > 1, + "KeyStopLerp::KeyStopLerp(): key stop vector must have two entries or more" ); + + // rKeyStops must be sorted in ascending order + for( std::size_t i=1, len=rKeyStops.size(); i<len; ++i ) + { + if( rKeyStops[i-1] > rKeyStops[i] ) + OSL_FAIL( "KeyStopLerp::KeyStopLerp(): time vector is not sorted in ascending order!" ); + } +#else + (void)rKeyStops; +#endif +} + +namespace basegfx::utils +{ + KeyStopLerp::KeyStopLerp( std::vector<double>&& rKeyStops ) : + maKeyStops(std::move(rKeyStops)), + mnLastIndex(0) + { + validateInput(maKeyStops); + } + + KeyStopLerp::KeyStopLerp( const ::css::uno::Sequence<double>& rKeyStops ) : + maKeyStops(rKeyStops.begin(), rKeyStops.end()), + mnLastIndex(0) + { + validateInput(maKeyStops); + } + + KeyStopLerp::ResultType KeyStopLerp::lerp(double fAlpha) const + { + // cached value still okay? + if( maKeyStops.at(mnLastIndex) < fAlpha || + maKeyStops.at(mnLastIndex+1) >= fAlpha ) + { + // nope, find new index + mnLastIndex = std::min<std::ptrdiff_t>( + maKeyStops.size()-2, + // range is ensured by max below + std::max<std::ptrdiff_t>( + 0, + std::distance( maKeyStops.begin(), + std::lower_bound( maKeyStops.begin(), + maKeyStops.end(), + fAlpha )) - 1 )); + } + + // lerp between stop and stop+1 + const double fRawLerp= + (fAlpha-maKeyStops.at(mnLastIndex)) / + (maKeyStops.at(mnLastIndex+1) - maKeyStops.at(mnLastIndex)); + + // clamp to permissible range (input fAlpha might be + // everything) + return ResultType( + mnLastIndex, + std::clamp(fRawLerp,0.0,1.0)); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/numbertools.cxx b/basegfx/source/tools/numbertools.cxx new file mode 100644 index 0000000000..4248068b7d --- /dev/null +++ b/basegfx/source/tools/numbertools.cxx @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <basegfx/utils/tools.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> + +#include <rtl/strbuf.hxx> +#include <rtl/math.hxx> + +namespace basegfx::utils +{ + B2DPolyPolygon number2PolyPolygon(double fValue, sal_Int32 nTotalDigits, sal_Int32 nDecPlaces, bool bLitSegments) + { + // config here + // { + const double fSpace=0.2; + // } + // config here + + OStringBuffer aNum; + rtl::math::doubleToStringBuffer(aNum, + fValue, + rtl_math_StringFormat_F, + nDecPlaces, '.', + nullptr, ','); + + B2DPolyPolygon aRes; + B2DHomMatrix aMat; + double fCurrX=std::max(nTotalDigits-aNum.getLength(), + sal_Int32(0)) * (1.0+fSpace); + for( sal_Int32 i=0; i<aNum.getLength(); ++i ) + { + B2DPolyPolygon aCurr=createSevenSegmentPolyPolygon(aNum[i], + bLitSegments); + + aMat.identity(); + aMat.translate(fCurrX,0.0); + aCurr.transform(aMat); + + fCurrX += 1.0+fSpace; + + aRes.append(aCurr); + } + + return aRes; + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/stringconversiontools.cxx b/basegfx/source/tools/stringconversiontools.cxx new file mode 100644 index 0000000000..79b6d60466 --- /dev/null +++ b/basegfx/source/tools/stringconversiontools.cxx @@ -0,0 +1,158 @@ +/* -*- 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 <stringconversiontools.hxx> +#include <rtl/math.hxx> + +namespace basegfx::internal +{ + void skipSpaces(sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen) + { + while( io_rPos < nLen && + rStr[io_rPos] == ' ' ) + { + ++io_rPos; + } + } + + static void skipSpacesAndCommas(sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen) + { + while(io_rPos < nLen + && (rStr[io_rPos] == ' ' || rStr[io_rPos] == ',')) + { + ++io_rPos; + } + } + + static bool getDoubleChar(double& o_fRetval, + sal_Int32& io_rPos, + std::u16string_view rStr) + { + const sal_Int64 nStrSize = rStr.size(); + sal_Unicode aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + const sal_Int32 nStartPos = io_rPos; + + // sign + if(aChar == '+' || aChar == '-') + { + aChar = rStr[++io_rPos]; + } + + // numbers before point + while('0' <= aChar && '9' >= aChar) + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + + // point + if(aChar == '.') + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + + // numbers after point + while ('0' <= aChar && '9' >= aChar) + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + + // 'e' + if(aChar == 'e' || aChar == 'E') + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + + // sign for 'e' + if(aChar == '+' || aChar == '-') + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + + // number for 'e' + while('0' <= aChar && '9' >= aChar) + { + io_rPos++; + aChar = io_rPos < nStrSize ? rStr[io_rPos] : 0; + } + } + + const sal_Int32 nLen = io_rPos - nStartPos; + if(nLen) + { + rStr = rStr.substr(nStartPos, nLen); + rtl_math_ConversionStatus eStatus; + o_fRetval = ::rtl::math::stringToDouble( rStr, + '.', + ',', + &eStatus ); + return ( eStatus == rtl_math_ConversionStatus_Ok ); + } + + return false; + } + + bool importDoubleAndSpaces(double& o_fRetval, + sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen ) + { + if( !getDoubleChar(o_fRetval, io_rPos, rStr) ) + return false; + + skipSpacesAndCommas(io_rPos, rStr, nLen); + + return true; + } + + bool importFlagAndSpaces(sal_Int32& o_nRetval, + sal_Int32& io_rPos, + std::u16string_view rStr, + const sal_Int32 nLen) + { + sal_Unicode aChar( rStr[io_rPos] ); + + if(aChar == '0') + { + o_nRetval = 0; + ++io_rPos; + } + else if (aChar == '1') + { + o_nRetval = 1; + ++io_rPos; + } + else + return false; + + skipSpacesAndCommas(io_rPos, rStr, nLen); + + return true; + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/systemdependentdata.cxx b/basegfx/source/tools/systemdependentdata.cxx new file mode 100644 index 0000000000..0d64d9982c --- /dev/null +++ b/basegfx/source/tools/systemdependentdata.cxx @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <basegfx/utils/systemdependentdata.hxx> +#include <config_fuzzers.h> +#include <math.h> + +namespace basegfx +{ + SystemDependentDataManager::SystemDependentDataManager() + { + } + + SystemDependentDataManager::~SystemDependentDataManager() + { + } +} // namespace basegfx + +namespace basegfx +{ + SystemDependentData::SystemDependentData( + SystemDependentDataManager& rSystemDependentDataManager) + : mrSystemDependentDataManager(rSystemDependentDataManager), + mnCalculatedCycles(0) + { + } + + SystemDependentData::~SystemDependentData() + { + } + + sal_uInt32 SystemDependentData::calculateCombinedHoldCyclesInSeconds() const + { +#if ENABLE_FUZZERS + return 0; +#endif + + if(0 == mnCalculatedCycles) + { + const sal_Int64 nBytes(estimateUsageInBytes()); + + // tdf#129845 as indicator for no need to buffer trivial data, stay at and + // return zero. As border, use 450 bytes. For polygons, this means to buffer + // starting with ca. 50 points (GDIPLUS uses 9 bytes per coordinate). For + // Bitmap data this means to more or less always buffer (as it was before). + // For the future, a more sophisticated differentiation may be added + if(nBytes > 450) + { + // HoldCyclesInSeconds + const sal_uInt32 nSeconds = 60; + + // default is Seconds (minimal is one) + sal_uInt32 nResult(0 == nSeconds ? 1 : nSeconds); + + if(0 != nBytes) + { + // use sqrt to get some curved shape. With a default of 60s we get + // a single second at 3600 byte. To get close to 10mb, multiply by + // a corresponding scaling factor + const double fScaleToMB(3600.0 / (1024.0 * 1024.0 * 10.0)); + + // also use a multiplier to move the start point higher + const double fMultiplierSeconds(10.0); + + // calculate + nResult = static_cast<sal_uInt32>((fMultiplierSeconds * nSeconds) / sqrt(nBytes * fScaleToMB)); + + // minimal value is 1 + if(nResult < 1) + { + nResult = 1; + } + + // maximal value is nSeconds + if(nResult > nSeconds) + { + nResult = nSeconds; + } + } + + // set locally (once, on-demand created, non-zero) + const_cast<SystemDependentData*>(this)->mnCalculatedCycles = nResult; + } + } + + return mnCalculatedCycles; + } + + sal_Int64 SystemDependentData::estimateUsageInBytes() const + { + // default implementation has no idea + return 0; + } +} // namespace basegfx + +namespace basegfx +{ + SystemDependentDataHolder::SystemDependentDataHolder() + { + } + + SystemDependentDataHolder::~SystemDependentDataHolder() + { + for(const auto& candidate : maSystemDependentReferences) + { + basegfx::SystemDependentData_SharedPtr aData(candidate.second.lock()); + + if(aData) + { + aData->getSystemDependentDataManager().endUsage(aData); + } + } + } + + void SystemDependentDataHolder::addOrReplaceSystemDependentData(basegfx::SystemDependentData_SharedPtr& rData) + { + const size_t hash_code(typeid(*rData).hash_code()); + auto result(maSystemDependentReferences.find(hash_code)); + + if(result != maSystemDependentReferences.end()) + { + basegfx::SystemDependentData_SharedPtr aData(result->second.lock()); + + if(aData) + { + aData->getSystemDependentDataManager().endUsage(aData); + } + + maSystemDependentReferences.erase(result); + result = maSystemDependentReferences.end(); + } + + maSystemDependentReferences[hash_code] = rData; + rData->getSystemDependentDataManager().startUsage(rData); + } + + SystemDependentData_SharedPtr SystemDependentDataHolder::getSystemDependentData(size_t hash_code) const + { + basegfx::SystemDependentData_SharedPtr aRetval; + auto result(maSystemDependentReferences.find(hash_code)); + + if(result != maSystemDependentReferences.end()) + { + aRetval = result->second.lock(); + + if(aRetval) + { + aRetval->getSystemDependentDataManager().touchUsage(aRetval); + } + else + { + const_cast< SystemDependentDataHolder* >(this)->maSystemDependentReferences.erase(result); + } + } + + return aRetval; + } +} // namespace basegfx + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/tools.cxx b/basegfx/source/tools/tools.cxx new file mode 100644 index 0000000000..4a7f2c962a --- /dev/null +++ b/basegfx/source/tools/tools.cxx @@ -0,0 +1,111 @@ +/* -*- 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 <basegfx/utils/tools.hxx> +#include <basegfx/range/b2drange.hxx> + +#include <algorithm> + +namespace basegfx::utils +{ + namespace + { + double distance( const double& nX, + const double& nY, + const ::basegfx::B2DVector& rNormal, + const double& nC ) + { + return nX*rNormal.getX() + nY*rNormal.getY() - nC; + } + + void moveLineOutsideRect( ::basegfx::B2DPoint& io_rStart, + ::basegfx::B2DPoint& io_rEnd, + const ::basegfx::B2DVector& rMoveDirection, + const ::basegfx::B2DRange& rFitTarget ) + { + // calc c for normal line form equation n x - c = 0 + const double nC( rMoveDirection.scalar( io_rStart ) ); + + // calc maximum orthogonal distance for all four bound + // rect corners to the line + const double nMaxDistance( std::max( + 0.0, + std::max( + distance(rFitTarget.getMinX(), + rFitTarget.getMinY(), + rMoveDirection, + nC), + std::max( + distance(rFitTarget.getMinX(), + rFitTarget.getMaxY(), + rMoveDirection, + nC), + std::max( + distance(rFitTarget.getMaxX(), + rFitTarget.getMinY(), + rMoveDirection, + nC), + distance(rFitTarget.getMaxX(), + rFitTarget.getMaxY(), + rMoveDirection, + nC) ) ) ) ) ); + + // now move line points, such that the bound rect + // points are all either 'on' or on the negative side + // of the half-plane + io_rStart += nMaxDistance*rMoveDirection; + io_rEnd += nMaxDistance*rMoveDirection; + } + } + + void infiniteLineFromParallelogram( ::basegfx::B2DPoint& io_rLeftTop, + ::basegfx::B2DPoint& io_rLeftBottom, + ::basegfx::B2DPoint& io_rRightTop, + ::basegfx::B2DPoint& io_rRightBottom, + const ::basegfx::B2DRange& rFitTarget ) + { + // For the top and bottom border line of the + // parallelogram, we determine the distance to all four + // corner points of the bound rect (tl, tr, bl, br). When + // using the unit normal form for lines (n x - c = 0), and + // choosing n to point 'outwards' the parallelogram, then + // all bound rect corner points having positive distance + // to the line lie outside the extended gradient rect, and + // thus, the corresponding border line must be moved the + // maximum distance outwards. + + // don't use the top and bottom border line direction, and + // calculate the normal from them. Instead, use the + // vertical lines (lt - lb or rt - rb), as they more + // faithfully represent the direction of the + // to-be-generated infinite line + ::basegfx::B2DVector aDirectionVertical( io_rLeftTop - io_rLeftBottom ); + aDirectionVertical.normalize(); + + const ::basegfx::B2DVector aNormalTop( aDirectionVertical ); + const ::basegfx::B2DVector aNormalBottom( -aDirectionVertical ); + + // now extend parallelogram, such that the bound rect + // point are included + moveLineOutsideRect( io_rLeftTop, io_rRightTop, aNormalTop, rFitTarget ); + moveLineOutsideRect( io_rLeftBottom, io_rRightBottom, aNormalBottom, rFitTarget ); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/unopolypolygon.cxx b/basegfx/source/tools/unopolypolygon.cxx new file mode 100644 index 0000000000..41f1497319 --- /dev/null +++ b/basegfx/source/tools/unopolypolygon.cxx @@ -0,0 +1,446 @@ +/* -*- 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 <com/sun/star/lang/IllegalArgumentException.hpp> + +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/utils/canvastools.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/utils/unopolypolygon.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> +#include <cppuhelper/supportsservice.hxx> +#include <utility> + +using namespace ::com::sun::star; + +namespace basegfx::unotools +{ + UnoPolyPolygon::UnoPolyPolygon( B2DPolyPolygon aPolyPoly ) : + maPolyPoly(std::move( aPolyPoly )), + meFillRule( rendering::FillRule_EVEN_ODD ) + { + } + + void SAL_CALL UnoPolyPolygon::addPolyPolygon( + const geometry::RealPoint2D& position, + const uno::Reference< rendering::XPolyPolygon2D >& polyPolygon ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + + // TODO(F1): Correctly fulfill the UNO API + // specification. This will probably result in a vector of + // poly-polygons to be stored in this object. + + const sal_Int32 nPolys( polyPolygon->getNumberOfPolygons() ); + + if( !polyPolygon.is() || !nPolys ) + { + // invalid or empty polygon - nothing to do. + return; + } + + B2DPolyPolygon aSrcPoly; + const UnoPolyPolygon* pSrc( dynamic_cast< UnoPolyPolygon* >(polyPolygon.get()) ); + + // try to extract polygon data from interface. First, + // check whether it's the same implementation object, + // which we can tunnel then. + if( pSrc ) + { + aSrcPoly = pSrc->getPolyPolygon(); + } + else + { + // not a known implementation object - try data source + // interfaces + uno::Reference< rendering::XBezierPolyPolygon2D > xBezierPoly( + polyPolygon, + uno::UNO_QUERY ); + + if( xBezierPoly.is() ) + { + aSrcPoly = unotools::polyPolygonFromBezier2DSequenceSequence( + xBezierPoly->getBezierSegments( 0, + nPolys, + 0, + -1 ) ); + } + else + { + uno::Reference< rendering::XLinePolyPolygon2D > xLinePoly( + polyPolygon, + uno::UNO_QUERY ); + + // no implementation class and no data provider + // found - contract violation. + if( !xLinePoly.is() ) + throw lang::IllegalArgumentException( + "UnoPolyPolygon::addPolyPolygon(): Invalid input " + "poly-polygon, cannot retrieve vertex data", + getXWeak(), 1); + + aSrcPoly = unotools::polyPolygonFromPoint2DSequenceSequence( + xLinePoly->getPoints( 0, + nPolys, + 0, + -1 ) ); + } + } + + const B2DRange aBounds( utils::getRange( aSrcPoly ) ); + const B2DVector aOffset( unotools::b2DPointFromRealPoint2D( position ) - + aBounds.getMinimum() ); + + if( !aOffset.equalZero() ) + { + const B2DHomMatrix aTranslate(utils::createTranslateB2DHomMatrix(aOffset)); + aSrcPoly.transform( aTranslate ); + } + + maPolyPoly.append( aSrcPoly ); + } + + sal_Int32 SAL_CALL UnoPolyPolygon::getNumberOfPolygons() + { + std::unique_lock const guard( m_aMutex ); + return maPolyPoly.count(); + } + + sal_Int32 SAL_CALL UnoPolyPolygon::getNumberOfPolygonPoints( + sal_Int32 polygon ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( polygon ); + + return maPolyPoly.getB2DPolygon(polygon).count(); + } + + rendering::FillRule SAL_CALL UnoPolyPolygon::getFillRule() + { + std::unique_lock const guard( m_aMutex ); + return meFillRule; + } + + void SAL_CALL UnoPolyPolygon::setFillRule( + rendering::FillRule fillRule ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + + meFillRule = fillRule; + } + + sal_Bool SAL_CALL UnoPolyPolygon::isClosed( + sal_Int32 index ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( index ); + + return maPolyPoly.getB2DPolygon(index).isClosed(); + } + + void SAL_CALL UnoPolyPolygon::setClosed( + sal_Int32 index, + sal_Bool closedState ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + + if( index == -1 ) + { + // set all + maPolyPoly.setClosed( closedState ); + } + else + { + checkIndex( index ); + + // fetch referenced polygon, change state + B2DPolygon aTmp( maPolyPoly.getB2DPolygon(index) ); + aTmp.setClosed( closedState ); + + // set back to container + maPolyPoly.setB2DPolygon( index, aTmp ); + } + } + + uno::Sequence< uno::Sequence< geometry::RealPoint2D > > SAL_CALL UnoPolyPolygon::getPoints( + sal_Int32 nPolygonIndex, + sal_Int32 nNumberOfPolygons, + sal_Int32 nPointIndex, + sal_Int32 nNumberOfPoints ) + { + return unotools::pointSequenceSequenceFromB2DPolyPolygon( + getSubsetPolyPolygon( nPolygonIndex, + nNumberOfPolygons, + nPointIndex, + nNumberOfPoints ) ); + } + + void SAL_CALL UnoPolyPolygon::setPoints( + const uno::Sequence< uno::Sequence< geometry::RealPoint2D > >& points, + sal_Int32 nPolygonIndex ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + + const B2DPolyPolygon& rNewPolyPoly( + unotools::polyPolygonFromPoint2DSequenceSequence( points ) ); + + if( nPolygonIndex == -1 ) + { + maPolyPoly = rNewPolyPoly; + } + else + { + checkIndex( nPolygonIndex ); + + maPolyPoly.insert( nPolygonIndex, rNewPolyPoly ); + } + } + + geometry::RealPoint2D SAL_CALL UnoPolyPolygon::getPoint( + sal_Int32 nPolygonIndex, + sal_Int32 nPointIndex ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + + const B2DPolygon& rPoly( maPolyPoly.getB2DPolygon( nPolygonIndex ) ); + + if( nPointIndex < 0 || o3tl::make_unsigned(nPointIndex) >= rPoly.count() ) + throw lang::IndexOutOfBoundsException(); + + return unotools::point2DFromB2DPoint( rPoly.getB2DPoint( nPointIndex ) ); + } + + void SAL_CALL UnoPolyPolygon::setPoint( + const geometry::RealPoint2D& point, + sal_Int32 nPolygonIndex, + sal_Int32 nPointIndex ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + modifying(); + + B2DPolygon aPoly( maPolyPoly.getB2DPolygon( nPolygonIndex ) ); + + if( nPointIndex < 0 || o3tl::make_unsigned(nPointIndex) >= aPoly.count() ) + throw lang::IndexOutOfBoundsException(); + + aPoly.setB2DPoint( nPointIndex, + unotools::b2DPointFromRealPoint2D( point ) ); + maPolyPoly.setB2DPolygon( nPolygonIndex, aPoly ); + } + + uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > > SAL_CALL UnoPolyPolygon::getBezierSegments( + sal_Int32 nPolygonIndex, + sal_Int32 nNumberOfPolygons, + sal_Int32 nPointIndex, + sal_Int32 nNumberOfPoints ) + { + return unotools::bezierSequenceSequenceFromB2DPolyPolygon( + getSubsetPolyPolygon( nPolygonIndex, + nNumberOfPolygons, + nPointIndex, + nNumberOfPoints ) ); + } + + void SAL_CALL UnoPolyPolygon::setBezierSegments( + const uno::Sequence< uno::Sequence< geometry::RealBezierSegment2D > >& points, + sal_Int32 nPolygonIndex ) + { + std::unique_lock const guard( m_aMutex ); + modifying(); + const B2DPolyPolygon& rNewPolyPoly( + unotools::polyPolygonFromBezier2DSequenceSequence( points ) ); + + if( nPolygonIndex == -1 ) + { + maPolyPoly = rNewPolyPoly; + } + else + { + checkIndex( nPolygonIndex ); + + maPolyPoly.insert( nPolygonIndex, rNewPolyPoly ); + } + } + + geometry::RealBezierSegment2D SAL_CALL UnoPolyPolygon::getBezierSegment( sal_Int32 nPolygonIndex, + sal_Int32 nPointIndex ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + + const B2DPolygon& rPoly( maPolyPoly.getB2DPolygon( nPolygonIndex ) ); + const sal_uInt32 nPointCount(rPoly.count()); + + if( nPointIndex < 0 || o3tl::make_unsigned(nPointIndex) >= nPointCount ) + throw lang::IndexOutOfBoundsException(); + + const B2DPoint& rPt( rPoly.getB2DPoint( nPointIndex ) ); + const B2DPoint& rCtrl0( rPoly.getNextControlPoint(nPointIndex) ); + const B2DPoint& rCtrl1( rPoly.getPrevControlPoint((nPointIndex + 1) % nPointCount) ); + + return geometry::RealBezierSegment2D( rPt.getX(), + rPt.getY(), + rCtrl0.getX(), + rCtrl0.getY(), + rCtrl1.getX(), + rCtrl1.getY() ); + } + + void SAL_CALL UnoPolyPolygon::setBezierSegment( const geometry::RealBezierSegment2D& segment, + sal_Int32 nPolygonIndex, + sal_Int32 nPointIndex ) + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + modifying(); + + B2DPolygon aPoly( maPolyPoly.getB2DPolygon( nPolygonIndex ) ); + const sal_uInt32 nPointCount(aPoly.count()); + + if( nPointIndex < 0 || o3tl::make_unsigned(nPointIndex) >= nPointCount ) + throw lang::IndexOutOfBoundsException(); + + aPoly.setB2DPoint( nPointIndex, + B2DPoint( segment.Px, + segment.Py ) ); + aPoly.setNextControlPoint(nPointIndex, + B2DPoint(segment.C1x, segment.C1y)); + aPoly.setPrevControlPoint((nPointIndex + 1) % nPointCount, + B2DPoint(segment.C2x, segment.C2y)); + + maPolyPoly.setB2DPolygon( nPolygonIndex, aPoly ); + } + + B2DPolyPolygon UnoPolyPolygon::getSubsetPolyPolygon( + sal_Int32 nPolygonIndex, + sal_Int32 nNumberOfPolygons, + sal_Int32 nPointIndex, + sal_Int32 nNumberOfPoints ) const + { + std::unique_lock const guard( m_aMutex ); + checkIndex( nPolygonIndex ); + + const sal_Int32 nPolyCount( maPolyPoly.count() ); + + // check for "full polygon" case + if( !nPolygonIndex && + !nPointIndex && + nNumberOfPolygons == nPolyCount && + nNumberOfPoints == -1 ) + { + return maPolyPoly; + } + + B2DPolyPolygon aSubsetPoly; + + // create temporary polygon (as an extract from maPoly, + // which contains the requested subset) + for( sal_Int32 i=nPolygonIndex; i<nNumberOfPolygons; ++i ) + { + checkIndex(i); + + const B2DPolygon& rCurrPoly( maPolyPoly.getB2DPolygon(i) ); + + sal_Int32 nFirstPoint(0); + sal_Int32 nLastPoint(nPolyCount-1); + + if( nPointIndex && i==nPolygonIndex ) + { + // very first polygon - respect nPointIndex, if + // not zero + + // empty polygon - impossible to specify _any_ + // legal value except 0 here! + if( !nPolyCount) + throw lang::IndexOutOfBoundsException(); + + nFirstPoint = nPointIndex; + } + + if( i==nNumberOfPolygons-1 && nNumberOfPoints != -1 ) + { + // very last polygon - respect nNumberOfPoints + + // empty polygon - impossible to specify _any_ + // legal value except -1 here! + if( !nPolyCount ) + throw lang::IndexOutOfBoundsException(); + + nLastPoint = nFirstPoint+nNumberOfPoints; + } + + if( !nPolyCount ) + { + // empty polygon - index checks already performed + // above, now simply append empty polygon + aSubsetPoly.append( rCurrPoly ); + } + else + { + if( nFirstPoint < 0 || nFirstPoint >= nPolyCount ) + throw lang::IndexOutOfBoundsException(); + + if( nLastPoint < 0 || nLastPoint >= nPolyCount ) + throw lang::IndexOutOfBoundsException(); + + B2DPolygon aTmp; + for( sal_Int32 j=nFirstPoint; j<nLastPoint; ++j ) + aTmp.append( rCurrPoly.getB2DPoint(j) ); + + aSubsetPoly.append( aTmp ); + } + } + + return aSubsetPoly; + } + + OUString SAL_CALL UnoPolyPolygon::getImplementationName() + { + return "gfx::internal::UnoPolyPolygon"; + } + + sal_Bool SAL_CALL UnoPolyPolygon::supportsService( const OUString& ServiceName ) + { + return cppu::supportsService(this, ServiceName); + } + + uno::Sequence< OUString > SAL_CALL UnoPolyPolygon::getSupportedServiceNames() + { + return { "com.sun.star.rendering.PolyPolygon2D" }; + } + + B2DPolyPolygon UnoPolyPolygon::getPolyPolygon() const + { + std::unique_lock const guard( m_aMutex ); + + return maPolyPoly; + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/basegfx/source/tools/zoomtools.cxx b/basegfx/source/tools/zoomtools.cxx new file mode 100644 index 0000000000..dd4c7a6cbb --- /dev/null +++ b/basegfx/source/tools/zoomtools.cxx @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <basegfx/utils/zoomtools.hxx> + +namespace basegfx::zoomtools +{ + +/** 2^(1/6) as the default step + + This ensures (unless the rounding is used) that 6 steps lead + to double / half zoom level. +*/ +const double ZOOM_FACTOR = 1.12246205; + +/** +* Round a value against a specified multiple. Values below half +* of the multiple are rounded down and all others are rounded up. +* +* @param nCurrent current value +* @param nMultiple multiple against which the current value is rounded +*/ +static sal_uInt16 roundMultiple(sal_uInt16 nCurrent, int nMultiple) +{ + // round zoom to a multiple of nMultiple + return (( nCurrent + nMultiple / 2 ) - ( nCurrent + nMultiple / 2 ) % nMultiple); +} + +/** +* Convert geometric progression results into more common values by +* rounding them against certain multiples depending on the size. +* Beginning with 50 the multiple is 5, with 100, 10, and so on. +* +* @param nCurrent current zoom factor +*/ +static sal_uInt16 roundZoom(double nCurrent) +{ + // convert nCurrent properly to int + sal_uInt16 nNew = nCurrent + 0.5; + + // round to more common numbers above 50 + if (nNew > 1000) { + nNew = roundMultiple(nNew, 100); + } else if ( nNew > 500 ) { + nNew = roundMultiple(nNew, 50); + } else if ( nNew > 100 ) { + nNew = roundMultiple(nNew, 10); + } else if ( nNew > 50 ) { + nNew = roundMultiple(nNew, 5); + } + + return nNew; +} + +/** +* Make sure that a certain step isn't skipped during the zooming +* progress. +* +* @param nCurrent current zoom factor +* @param nPrevious previous zoom factor +* @param nStep step which shouldn't be skipped +*/ +static sal_uInt16 enforceStep(sal_uInt16 nCurrent, sal_uInt16 nPrevious, unsigned int nStep) +{ + if ((( nCurrent > nStep ) && ( nPrevious < nStep )) + || (( nCurrent < nStep ) && ( nPrevious > nStep ))) + return nStep; + else + return nCurrent; +} + +/** +* Increasing the zoom level. +* +* @param nCurrent current zoom factor +*/ +sal_uInt16 zoomIn(sal_uInt16 nCurrent) +{ + sal_uInt16 nNew = roundZoom( nCurrent * ZOOM_FACTOR ); + // make sure some values are not skipped + nNew = enforceStep(nNew, nCurrent, 200); + nNew = enforceStep(nNew, nCurrent, 100); + nNew = enforceStep(nNew, nCurrent, 75); + nNew = enforceStep(nNew, nCurrent, 50); + nNew = enforceStep(nNew, nCurrent, 25); + return nNew; +} + +/** +* Decreasing the zoom level. +* +* @param nCurrent current zoom factor +*/ +sal_uInt16 zoomOut(sal_uInt16 nCurrent) +{ + sal_uInt16 nNew = roundZoom( nCurrent / ZOOM_FACTOR ); + // make sure some values are not skipped + nNew = enforceStep(nNew, nCurrent, 200); + nNew = enforceStep(nNew, nCurrent, 100); + nNew = enforceStep(nNew, nCurrent, 75); + nNew = enforceStep(nNew, nCurrent, 50); + nNew = enforceStep(nNew, nCurrent, 25); + return nNew; +} +} // namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |