tdf#48062: Add support for arithmetic in feComposite

Took https://github.com/w3c/csswg-drafts/issues/3831
as a reference

Change-Id: I42039c481ec114c3faeae51526a5f29b86960146
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/165828
Tested-by: Jenkins
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
diff --git a/include/vcl/BitmapArithmeticBlendFilter.hxx b/include/vcl/BitmapArithmeticBlendFilter.hxx
new file mode 100644
index 0000000..a2de3ae
--- /dev/null
+++ b/include/vcl/BitmapArithmeticBlendFilter.hxx
@@ -0,0 +1,31 @@
/* -*- 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/.
 *
 */

#ifndef INCLUDED_VCL_BITMAPARITHMETICBLENDFILTER_HXX
#define INCLUDED_VCL_BITMAPARITHMETICBLENDFILTER_HXX

#include <vcl/bitmapex.hxx>

class VCL_DLLPUBLIC BitmapArithmeticBlendFilter
{
private:
    BitmapEx maBitmapEx;
    BitmapEx maBitmapEx2;

public:
    BitmapArithmeticBlendFilter(BitmapEx const& rBmpEx, BitmapEx const& rBmpEx2);
    virtual ~BitmapArithmeticBlendFilter();

    BitmapEx execute(double aK1, double aK2, double aK3, double aK4);
};

#endif

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/svgio/inc/svgfecompositenode.hxx b/svgio/inc/svgfecompositenode.hxx
index fcbc24e..8a34851 100644
--- a/svgio/inc/svgfecompositenode.hxx
+++ b/svgio/inc/svgfecompositenode.hxx
@@ -31,6 +31,7 @@ enum class Operator
    Out,
    Xor,
    Atop,
    Arithmetic
};

class SvgFeCompositeNode : public SvgFilterNode
@@ -41,6 +42,11 @@ private:
    OUString maResult;
    Operator maOperator;

    SvgNumber maK1;
    SvgNumber maK2;
    SvgNumber maK3;
    SvgNumber maK4;

public:
    SvgFeCompositeNode(SvgDocument& rDocument, SvgNode* pParent);
    virtual ~SvgFeCompositeNode() override;
diff --git a/svgio/inc/svgtoken.hxx b/svgio/inc/svgtoken.hxx
index 9e69458..401a466 100644
--- a/svgio/inc/svgtoken.hxx
+++ b/svgio/inc/svgtoken.hxx
@@ -122,6 +122,10 @@ namespace svgio::svgreader
            Title,
            Desc,
            Overflow,
            K1,
            K2,
            K3,
            K4,

            // AspectRatio and params
            PreserveAspectRatio,
diff --git a/svgio/source/svgreader/svgfecompositenode.cxx b/svgio/source/svgreader/svgfecompositenode.cxx
index 88ba5c6..28f161d 100644
--- a/svgio/source/svgreader/svgfecompositenode.cxx
+++ b/svgio/source/svgreader/svgfecompositenode.cxx
@@ -23,6 +23,14 @@
#include <basegfx/polygon/b2dpolypolygon.hxx>
#include <drawinglayer/primitive2d/maskprimitive2d.hxx>
#include <drawinglayer/processor2d/contourextractor2d.hxx>
#include <drawinglayer/primitive2d/transformprimitive2d.hxx>
#include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
#include <vcl/bitmapex.hxx>
#include <vcl/BitmapArithmeticBlendFilter.hxx>
#include <drawinglayer/converters.hxx>
#include <basegfx/matrix/b2dhommatrixtools.hxx>
#include <vcl/BitmapWriteAccess.hxx>
#include <vcl/BitmapTools.hxx>

namespace svgio::svgreader
{
@@ -83,6 +91,50 @@ void SvgFeCompositeNode::parseAttribute(SVGToken aSVGToken, const OUString& aCon
                {
                    maOperator = Operator::Atop;
                }
                else if (o3tl::equalsIgnoreAsciiCase(o3tl::trim(aContent), u"arithmetic"))
                {
                    maOperator = Operator::Arithmetic;
                }
            }
            break;
        }
        case SVGToken::K1:
        {
            SvgNumber aNum;

            if (readSingleNumber(aContent, aNum))
            {
                maK1 = aNum;
            }
            break;
        }
        case SVGToken::K2:
        {
            SvgNumber aNum;

            if (readSingleNumber(aContent, aNum))
            {
                maK2 = aNum;
            }
            break;
        }
        case SVGToken::K3:
        {
            SvgNumber aNum;

            if (readSingleNumber(aContent, aNum))
            {
                maK3 = aNum;
            }
            break;
        }
        case SVGToken::K4:
        {
            SvgNumber aNum;

            if (readSingleNumber(aContent, aNum))
            {
                maK4 = aNum;
            }
            break;
        }
@@ -96,61 +148,127 @@ void SvgFeCompositeNode::parseAttribute(SVGToken aSVGToken, const OUString& aCon
void SvgFeCompositeNode::apply(drawinglayer::primitive2d::Primitive2DContainer& rTarget,
                               const SvgFilterNode* pParent) const
{
    basegfx::B2DPolyPolygon aPolyPolygon, aPolyPolygon2;
    if (maOperator != Operator::Arithmetic)
    {
        basegfx::B2DPolyPolygon aPolyPolygon, aPolyPolygon2;

    // Process maIn2 first
    if (const drawinglayer::primitive2d::Primitive2DContainer* pSource2
        = pParent->findGraphicSource(maIn2))
    {
        rTarget.append(*pSource2);
        drawinglayer::processor2d::ContourExtractor2D aExtractor(
            drawinglayer::geometry::ViewInformation2D(), true);
        aExtractor.process(*pSource2);
        const basegfx::B2DPolyPolygonVector& rResult(aExtractor.getExtractedContour());
        aPolyPolygon2 = basegfx::utils::mergeToSinglePolyPolygon(rResult);
    }
        // Process maIn2 first
        if (const drawinglayer::primitive2d::Primitive2DContainer* pSource2
            = pParent->findGraphicSource(maIn2))
        {
            rTarget.append(*pSource2);
            drawinglayer::processor2d::ContourExtractor2D aExtractor(
                drawinglayer::geometry::ViewInformation2D(), true);
            aExtractor.process(*pSource2);
            const basegfx::B2DPolyPolygonVector& rResult(aExtractor.getExtractedContour());
            aPolyPolygon2 = basegfx::utils::mergeToSinglePolyPolygon(rResult);
        }

    if (const drawinglayer::primitive2d::Primitive2DContainer* pSource
        = pParent->findGraphicSource(maIn))
    {
        rTarget.append(*pSource);
        drawinglayer::processor2d::ContourExtractor2D aExtractor(
            drawinglayer::geometry::ViewInformation2D(), true);
        aExtractor.process(*pSource);
        const basegfx::B2DPolyPolygonVector& rResult(aExtractor.getExtractedContour());
        aPolyPolygon = basegfx::utils::mergeToSinglePolyPolygon(rResult);
    }
        if (const drawinglayer::primitive2d::Primitive2DContainer* pSource
            = pParent->findGraphicSource(maIn))
        {
            rTarget.append(*pSource);
            drawinglayer::processor2d::ContourExtractor2D aExtractor(
                drawinglayer::geometry::ViewInformation2D(), true);
            aExtractor.process(*pSource);
            const basegfx::B2DPolyPolygonVector& rResult(aExtractor.getExtractedContour());
            aPolyPolygon = basegfx::utils::mergeToSinglePolyPolygon(rResult);
        }

    basegfx::B2DPolyPolygon aResult;
    if (maOperator == Operator::Over)
    {
        aResult = basegfx::utils::solvePolygonOperationOr(aPolyPolygon, aPolyPolygon2);
    }
    else if (maOperator == Operator::Out)
    {
        aResult = basegfx::utils::solvePolygonOperationDiff(aPolyPolygon, aPolyPolygon2);
    }
    else if (maOperator == Operator::In)
    {
        aResult = basegfx::utils::solvePolygonOperationAnd(aPolyPolygon, aPolyPolygon2);
    }
    else if (maOperator == Operator::Xor)
    {
        aResult = basegfx::utils::solvePolygonOperationXor(aPolyPolygon, aPolyPolygon2);
    }
    else if (maOperator == Operator::Atop)
    {
        // Atop is the union of In and Out.
        // The parts of in2 graphic that do not overlap with the in graphic stay untouched.
        aResult = basegfx::utils::solvePolygonOperationDiff(aPolyPolygon2, aPolyPolygon);
        aResult.append(basegfx::utils::solvePolygonOperationAnd(aPolyPolygon, aPolyPolygon2));
    }
        basegfx::B2DPolyPolygon aResult;
        if (maOperator == Operator::Over)
        {
            aResult = basegfx::utils::solvePolygonOperationOr(aPolyPolygon, aPolyPolygon2);
        }
        else if (maOperator == Operator::Out)
        {
            aResult = basegfx::utils::solvePolygonOperationDiff(aPolyPolygon, aPolyPolygon2);
        }
        else if (maOperator == Operator::In)
        {
            aResult = basegfx::utils::solvePolygonOperationAnd(aPolyPolygon, aPolyPolygon2);
        }
        else if (maOperator == Operator::Xor)
        {
            aResult = basegfx::utils::solvePolygonOperationXor(aPolyPolygon, aPolyPolygon2);
        }
        else if (maOperator == Operator::Atop)
        {
            // Atop is the union of In and Out.
            // The parts of in2 graphic that do not overlap with the in graphic stay untouched.
            aResult = basegfx::utils::solvePolygonOperationDiff(aPolyPolygon2, aPolyPolygon);
            aResult.append(basegfx::utils::solvePolygonOperationAnd(aPolyPolygon, aPolyPolygon2));
        }

    rTarget = drawinglayer::primitive2d::Primitive2DContainer{
        new drawinglayer::primitive2d::MaskPrimitive2D(std::move(aResult), std::move(rTarget))
    };
        rTarget = drawinglayer::primitive2d::Primitive2DContainer{
            new drawinglayer::primitive2d::MaskPrimitive2D(std::move(aResult), std::move(rTarget))
        };

    pParent->addGraphicSourceToMapper(maResult, rTarget);
        pParent->addGraphicSourceToMapper(maResult, rTarget);
    }
    else
    {
        basegfx::B2DRange aRange, aRange2;
        BitmapEx aBmpEx, aBmpEx2;

        if (const drawinglayer::primitive2d::Primitive2DContainer* pSource
            = pParent->findGraphicSource(maIn))
        {
            const drawinglayer::geometry::ViewInformation2D aViewInformation2D;
            aRange = pSource->getB2DRange(aViewInformation2D);
            basegfx::B2DHomMatrix aEmbedding(
                basegfx::utils::createTranslateB2DHomMatrix(-aRange.getMinX(), -aRange.getMinY()));

            aEmbedding.scale(aRange.getWidth(), aRange.getHeight());

            const drawinglayer::primitive2d::Primitive2DReference xEmbedRef(
                new drawinglayer::primitive2d::TransformPrimitive2D(
                    aEmbedding, drawinglayer::primitive2d::Primitive2DContainer(*pSource)));
            drawinglayer::primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };

            aBmpEx = drawinglayer::convertToBitmapEx(std::move(xEmbedSeq), aViewInformation2D,
                                                     aRange.getWidth(), aRange.getHeight(), 500000);
        }

        if (const drawinglayer::primitive2d::Primitive2DContainer* pSource2
            = pParent->findGraphicSource(maIn2))
        {
            const drawinglayer::geometry::ViewInformation2D aViewInformation2D;
            aRange2 = pSource2->getB2DRange(aViewInformation2D);
            basegfx::B2DHomMatrix aEmbedding(basegfx::utils::createTranslateB2DHomMatrix(
                -aRange2.getMinX(), -aRange2.getMinY()));

            aEmbedding.scale(aRange2.getWidth(), aRange2.getHeight());

            const drawinglayer::primitive2d::Primitive2DReference xEmbedRef(
                new drawinglayer::primitive2d::TransformPrimitive2D(
                    aEmbedding, drawinglayer::primitive2d::Primitive2DContainer(*pSource2)));
            drawinglayer::primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };

            aBmpEx2
                = drawinglayer::convertToBitmapEx(std::move(xEmbedSeq), aViewInformation2D,
                                                  aRange2.getWidth(), aRange2.getHeight(), 500000);
        }

        basegfx::B2DRectangle aBaseRect(std::min(aRange.getMinX(), aRange2.getMinX()),
                                        std::min(aRange.getMinY(), aRange2.getMinY()),
                                        std::max(aRange.getMaxX(), aRange2.getMaxX()),
                                        std::max(aRange.getMaxY(), aRange2.getMaxY()));

        aBmpEx = vcl::bitmap::DrawBitmapInRect(aBmpEx, aRange, aBaseRect);
        aBmpEx2 = vcl::bitmap::DrawBitmapInRect(aBmpEx2, aRange2, aBaseRect);

        BitmapArithmeticBlendFilter* pArithmeticFilter
            = new BitmapArithmeticBlendFilter(aBmpEx, aBmpEx2);
        BitmapEx aResBmpEx = pArithmeticFilter->execute(maK1.getNumber(), maK2.getNumber(),
                                                        maK3.getNumber(), maK4.getNumber());

        const drawinglayer::primitive2d::Primitive2DReference xRef(
            new drawinglayer::primitive2d::BitmapPrimitive2D(
                aResBmpEx, basegfx::utils::createScaleTranslateB2DHomMatrix(
                               aBaseRect.getRange(), aBaseRect.getMinimum())));
        rTarget = drawinglayer::primitive2d::Primitive2DContainer{ xRef };
    }
}

} // end of namespace svgio::svgreader
diff --git a/svgio/source/svgreader/svgtoken.cxx b/svgio/source/svgreader/svgtoken.cxx
index b019f71..35e2fa1 100644
--- a/svgio/source/svgreader/svgtoken.cxx
+++ b/svgio/source/svgreader/svgtoken.cxx
@@ -120,6 +120,10 @@ constexpr auto aSVGTokenMap = frozen::make_unordered_map<std::u16string_view, SV
    { u"title", SVGToken::Title },
    { u"desc", SVGToken::Desc },
    { u"overflow", SVGToken::Overflow },
    { u"k1", SVGToken::K1 },
    { u"k2", SVGToken::K2 },
    { u"k3", SVGToken::K3 },
    { u"k4", SVGToken::K4 },
    { u"preserveAspectRatio", SVGToken::PreserveAspectRatio },
    { u"defer", SVGToken::Defer },
    { u"none", SVGToken::None },
diff --git a/vcl/Library_vcl.mk b/vcl/Library_vcl.mk
index 7b54123..91580307 100644
--- a/vcl/Library_vcl.mk
+++ b/vcl/Library_vcl.mk
@@ -341,6 +341,7 @@ $(eval $(call gb_Library_add_exception_objects,vcl,\
    vcl/source/bitmap/bmpfast \
    vcl/source/bitmap/bitmapfilter \
    vcl/source/bitmap/bitmappaint \
    vcl/source/bitmap/BitmapArithmeticBlendFilter \
    vcl/source/bitmap/BitmapShadowFilter \
    vcl/source/bitmap/BitmapAlphaClampFilter \
    vcl/source/bitmap/BitmapBasicMorphologyFilter \
diff --git a/vcl/qa/cppunit/BitmapFilterTest.cxx b/vcl/qa/cppunit/BitmapFilterTest.cxx
index 00ba12b..902d5934 100644
--- a/vcl/qa/cppunit/BitmapFilterTest.cxx
+++ b/vcl/qa/cppunit/BitmapFilterTest.cxx
@@ -15,6 +15,7 @@
#include <tools/stream.hxx>
#include <vcl/graphicfilter.hxx>

#include <vcl/BitmapArithmeticBlendFilter.hxx>
#include <vcl/BitmapScreenBlendFilter.hxx>
#include <vcl/BitmapBasicMorphologyFilter.hxx>
#include <vcl/BitmapFilterStackBlur.hxx>
@@ -41,6 +42,7 @@ public:
    void testPerformance();
    void testGenerateStripRanges();
    void testScreenBlendFilter();
    void testArithmeticBlendFilter();

    CPPUNIT_TEST_SUITE(BitmapFilterTest);
    CPPUNIT_TEST(testBlurCorrectness);
@@ -48,6 +50,7 @@ public:
    CPPUNIT_TEST(testPerformance);
    CPPUNIT_TEST(testGenerateStripRanges);
    CPPUNIT_TEST(testScreenBlendFilter);
    CPPUNIT_TEST(testArithmeticBlendFilter);
    CPPUNIT_TEST_SUITE_END();

private:
@@ -333,6 +336,125 @@ void BitmapFilterTest::testScreenBlendFilter()
    }
}

void BitmapFilterTest::testArithmeticBlendFilter()
{
    Bitmap aRedBitmap(Size(4, 4), vcl::PixelFormat::N24_BPP);
    CPPUNIT_ASSERT_EQUAL(vcl::PixelFormat::N24_BPP, aRedBitmap.getPixelFormat());
    {
        BitmapScopedWriteAccess aWriteAccess(aRedBitmap);
        aWriteAccess->Erase(COL_LIGHTRED);
    }

    Bitmap aGreenBitmap(Size(4, 4), vcl::PixelFormat::N24_BPP);
    CPPUNIT_ASSERT_EQUAL(vcl::PixelFormat::N24_BPP, aGreenBitmap.getPixelFormat());
    {
        BitmapScopedWriteAccess aWriteAccess(aGreenBitmap);
        aWriteAccess->Erase(COL_GREEN);
    }

    Bitmap aTransparentBitmap(Size(4, 4), vcl::PixelFormat::N24_BPP);
    CPPUNIT_ASSERT_EQUAL(vcl::PixelFormat::N24_BPP, aTransparentBitmap.getPixelFormat());
    {
        BitmapScopedWriteAccess aWriteAccess(aTransparentBitmap);
        aWriteAccess->Erase(COL_AUTO);
    }

    BitmapEx aRedBitmapEx(aRedBitmap);
    BitmapEx aGreenBitmapEx(aGreenBitmap);
    BitmapEx aTransparentBitmapEx(aTransparentBitmap);

    // same color
    {
        BitmapArithmeticBlendFilter* pArithmeticFilter
            = new BitmapArithmeticBlendFilter(aRedBitmapEx, aRedBitmapEx);
        BitmapEx aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x00, 0x00, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(1, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 1, 0, 0);
        CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 1, 0);
        CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 1);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0xFF, 0xFF, 0xFF, 0xFF),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0.5, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0.5, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0.5, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 0.5);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0xFF, 0xFF),
                             aResBitmapEx.GetPixelColor(0, 0));
    }

    // Different colors
    {
        BitmapArithmeticBlendFilter* pArithmeticFilter
            = new BitmapArithmeticBlendFilter(aRedBitmapEx, aGreenBitmapEx);
        BitmapEx aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x00, 0x00, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(1, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(COL_BLACK, aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 1, 0, 0);
        CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 1, 0);
        CPPUNIT_ASSERT_EQUAL(COL_GREEN, aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 1);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0xFF, 0xFF, 0xFF, 0xFF),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0.5, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0x00, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0.5, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0.5, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0x00, 0x81, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 0.5);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0xFF, 0xFF),
                             aResBitmapEx.GetPixelColor(0, 0));
    }

    // transparent
    {
        BitmapArithmeticBlendFilter* pArithmeticFilter
            = new BitmapArithmeticBlendFilter(aRedBitmapEx, aTransparentBitmapEx);
        BitmapEx aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x00, 0x00, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(1, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 1, 0, 0);
        CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 1, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0xFF, 0xFF, 0xFF, 0xFF),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 1);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0xFF, 0xFF, 0xFF, 0xFF),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0.5, 0, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0.5, 0, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0x00, 0x00),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0.5, 0);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0xFF, 0xFF),
                             aResBitmapEx.GetPixelColor(0, 0));
        aResBitmapEx = pArithmeticFilter->execute(0, 0, 0, 0.5);
        CPPUNIT_ASSERT_EQUAL(Color(ColorAlpha, 0x7F, 0xFF, 0xFF, 0xFF),
                             aResBitmapEx.GetPixelColor(0, 0));
    }
}

} // namespace

CPPUNIT_TEST_SUITE_REGISTRATION(BitmapFilterTest);
diff --git a/vcl/source/bitmap/BitmapArithmeticBlendFilter.cxx b/vcl/source/bitmap/BitmapArithmeticBlendFilter.cxx
new file mode 100644
index 0000000..da52a43
--- /dev/null
+++ b/vcl/source/bitmap/BitmapArithmeticBlendFilter.cxx
@@ -0,0 +1,105 @@
/* -*- 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 <comphelper/diagnose_ex.hxx>
#include <vcl/BitmapArithmeticBlendFilter.hxx>
#include <vcl/BitmapWriteAccess.hxx>
#include <vcl/BitmapTools.hxx>

BitmapArithmeticBlendFilter::BitmapArithmeticBlendFilter(BitmapEx const& rBitmapEx,
                                                         BitmapEx const& rBitmapEx2)
    : maBitmapEx(rBitmapEx)
    , maBitmapEx2(rBitmapEx2)
{
}

BitmapArithmeticBlendFilter::~BitmapArithmeticBlendFilter() {}

static sal_uInt8 lcl_calculate(sal_uInt8 aColor, sal_uInt8 aColor2, double aK1, double aK2,
                               double aK3, double aK4)
{
    const double i1 = aColor / 255.0;
    const double i2 = aColor2 / 255.0;
    const double result = aK1 * i1 * i2 + aK2 * i1 + aK3 * i2 + aK4;

    return std::clamp(result, 0.0, 1.0) * 255.0;
}

static BitmapColor premultiply(const BitmapColor c)
{
    return BitmapColor(ColorAlpha, vcl::bitmap::premultiply(c.GetRed(), c.GetAlpha()),
                       vcl::bitmap::premultiply(c.GetGreen(), c.GetAlpha()),
                       vcl::bitmap::premultiply(c.GetBlue(), c.GetAlpha()), c.GetAlpha());
}

static BitmapColor unpremultiply(const BitmapColor c)
{
    return BitmapColor(ColorAlpha, vcl::bitmap::unpremultiply(c.GetRed(), c.GetAlpha()),
                       vcl::bitmap::unpremultiply(c.GetGreen(), c.GetAlpha()),
                       vcl::bitmap::unpremultiply(c.GetBlue(), c.GetAlpha()), c.GetAlpha());
}

BitmapEx BitmapArithmeticBlendFilter::execute(double aK1, double aK2, double aK3, double aK4)
{
    if (maBitmapEx.IsEmpty() || maBitmapEx2.IsEmpty())
        return BitmapEx();

    Size aSize = maBitmapEx.GetBitmap().GetSizePixel();
    Size aSize2 = maBitmapEx2.GetBitmap().GetSizePixel();
    sal_Int32 nHeight = std::min(aSize.getHeight(), aSize2.getHeight());
    sal_Int32 nWidth = std::min(aSize.getWidth(), aSize2.getWidth());

    BitmapScopedReadAccess pReadAccess(maBitmapEx.GetBitmap());
    Bitmap aDstBitmap(Size(nWidth, nHeight), maBitmapEx.GetBitmap().getPixelFormat(),
                      &pReadAccess->GetPalette());
    Bitmap aDstAlpha(AlphaMask(Size(nWidth, nHeight)).GetBitmap());

    {
        // just to be on the safe side: let the
        // ScopedAccessors get destructed before
        // copy-constructing the resulting bitmap. This will
        // rule out the possibility that cached accessor data
        // is not yet written back.

        BitmapScopedWriteAccess pWriteAccess(aDstBitmap);
        BitmapScopedWriteAccess pAlphaWriteAccess(aDstAlpha);

        if (pWriteAccess.get() != nullptr && pAlphaWriteAccess.get() != nullptr)
        {
            for (tools::Long y(0); y < nHeight; ++y)
            {
                Scanline pScanline = pWriteAccess->GetScanline(y);
                Scanline pScanAlpha = pAlphaWriteAccess->GetScanline(y);
                for (tools::Long x(0); x < nWidth; ++x)
                {
                    BitmapColor i1 = premultiply(maBitmapEx.GetPixelColor(x, y));
                    BitmapColor i2 = premultiply(maBitmapEx2.GetPixelColor(x, y));
                    sal_uInt8 r(lcl_calculate(i1.GetRed(), i2.GetRed(), aK1, aK2, aK3, aK4));
                    sal_uInt8 g(lcl_calculate(i1.GetGreen(), i2.GetGreen(), aK1, aK2, aK3, aK4));
                    sal_uInt8 b(lcl_calculate(i1.GetBlue(), i2.GetBlue(), aK1, aK2, aK3, aK4));
                    sal_uInt8 a(lcl_calculate(i1.GetAlpha(), i2.GetAlpha(), aK1, aK2, aK3, aK4));

                    pWriteAccess->SetPixelOnData(
                        pScanline, x, unpremultiply(BitmapColor(ColorAlpha, r, g, b, a)));
                    pAlphaWriteAccess->SetPixelOnData(pScanAlpha, x, BitmapColor(a));
                }
            }
        }
        else
        {
            // TODO(E2): Error handling!
            ENSURE_OR_THROW(false, "BitmapScreenBlendFilter: could not access bitmap");
        }
    }

    return BitmapEx(aDstBitmap, AlphaMask(aDstAlpha));
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */