include/vcl/filter/PngImageWriter.hxx    |   49 +++++++
 vcl/Library_vcl.mk                       |    1 
 vcl/qa/cppunit/png/PngFilterTest.cxx     |  173 +++++++++++++++++++++++++++
 vcl/source/filter/png/PngImageWriter.cxx |  196 +++++++++++++++++++++++++++++++
 4 files changed, 419 insertions(+)

New commits:
commit 6b4f4bdf9beb0c73fa8b8bc218cd206f2cd347fa
Author:     offtkp <[email protected]>
AuthorDate: Sun Mar 7 13:48:39 2021 +0900
Commit:     Tomaž Vajngerl <[email protected]>
CommitDate: Tue Jul 19 09:57:19 2022 +0200

    vcl: add PNG writer based on libpng
    
    Add PngImageWriter, a new png writer that uses libpng to replace
    our own at pngwrite.cxx
    
    PS: most of the work on this commit is done by Tomaž
    
    Change-Id: I52ffd1b286162ee0dd9f694c4f3210385f71daf8
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/136008
    Tested-by: Jenkins
    Reviewed-by: Tomaž Vajngerl <[email protected]>

diff --git a/include/vcl/filter/PngImageWriter.hxx 
b/include/vcl/filter/PngImageWriter.hxx
new file mode 100644
index 000000000000..667dd540e332
--- /dev/null
+++ b/include/vcl/filter/PngImageWriter.hxx
@@ -0,0 +1,49 @@
+/* -*- 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 <vcl/dllapi.h>
+#include <com/sun/star/task/XStatusIndicator.hpp>
+#include <com/sun/star/beans/PropertyValue.hpp>
+#include <com/sun/star/uno/Sequence.hxx>
+#include <tools/stream.hxx>
+#include <vcl/bitmapex.hxx>
+
+#pragma once
+
+namespace vcl
+{
+class VCL_DLLPUBLIC PngImageWriter
+{
+    SvStream& mrStream;
+    css::uno::Reference<css::task::XStatusIndicator> mxStatusIndicator;
+
+    sal_Int32 mnCompressionLevel;
+    bool mbInterlaced;
+
+public:
+    PngImageWriter(SvStream& rStream);
+
+    virtual ~PngImageWriter() {}
+
+    void setParameters(css::uno::Sequence<css::beans::PropertyValue> const& 
rParameters)
+    {
+        for (auto const& rValue : rParameters)
+        {
+            if (rValue.Name == "Compression")
+                rValue.Value >>= mnCompressionLevel;
+            else if (rValue.Name == "Interlaced")
+                rValue.Value >>= mbInterlaced;
+        }
+    }
+    bool write(BitmapEx& rBitmap);
+};
+
+} // namespace vcl
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/Library_vcl.mk b/vcl/Library_vcl.mk
index 5688f1f22ecc..b265467d2696 100644
--- a/vcl/Library_vcl.mk
+++ b/vcl/Library_vcl.mk
@@ -472,6 +472,7 @@ $(eval $(call gb_Library_add_exception_objects,vcl,\
     vcl/source/filter/wmf/wmfexternal \
     vcl/source/filter/wmf/wmfwr \
     vcl/source/filter/png/PngImageReader \
+    vcl/source/filter/png/PngImageWriter \
     vcl/source/filter/png/pngwrite \
     vcl/source/filter/webp/reader \
     vcl/source/filter/webp/writer \
diff --git a/vcl/qa/cppunit/png/PngFilterTest.cxx 
b/vcl/qa/cppunit/png/PngFilterTest.cxx
index c167c4c9c636..727f86c9476b 100644
--- a/vcl/qa/cppunit/png/PngFilterTest.cxx
+++ b/vcl/qa/cppunit/png/PngFilterTest.cxx
@@ -24,14 +24,20 @@
 #include <test/bootstrapfixture.hxx>
 #include <tools/stream.hxx>
 #include <vcl/filter/PngImageReader.hxx>
+#include <vcl/filter/PngImageWriter.hxx>
 #include <vcl/BitmapReadAccess.hxx>
+#include <bitmap/BitmapWriteAccess.hxx>
 #include <vcl/alpha.hxx>
 #include <vcl/graphicfilter.hxx>
+#include <unotools/tempfile.hxx>
 
 using namespace css;
 
 class PngFilterTest : public test::BootstrapFixture
 {
+    // Should keep the temp files (should be false)
+    static constexpr bool bKeepTemp = true;
+
     OUString maDataUrl;
 
     OUString getFullUrl(std::u16string_view sFileName)
@@ -48,10 +54,18 @@ public:
 
     void testPng();
     void testMsGifInPng();
+    void testPngRoundtrip8BitGrey();
+    void testPngRoundtrip24();
+    void testPngRoundtrip24_8();
+    void testPngRoundtrip32();
 
     CPPUNIT_TEST_SUITE(PngFilterTest);
     CPPUNIT_TEST(testPng);
     CPPUNIT_TEST(testMsGifInPng);
+    CPPUNIT_TEST(testPngRoundtrip8BitGrey);
+    CPPUNIT_TEST(testPngRoundtrip24);
+    CPPUNIT_TEST(testPngRoundtrip24_8);
+    CPPUNIT_TEST(testPngRoundtrip32);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -245,6 +259,165 @@ void PngFilterTest::testMsGifInPng()
     CPPUNIT_ASSERT(aGraphic.IsAnimated());
 }
 
+void PngFilterTest::testPngRoundtrip8BitGrey()
+{
+    utl::TempFile aTempFile(u"testPngRoundtrip8BitGrey");
+    if (!bKeepTemp)
+        aTempFile.EnableKillingFile();
+    {
+        SvStream& rStream = *aTempFile.GetStream(StreamMode::WRITE);
+        Bitmap aBitmap(Size(16, 16), vcl::PixelFormat::N8_BPP, 
&Bitmap::GetGreyPalette(256));
+        {
+            BitmapScopedWriteAccess pWriteAccess(aBitmap);
+            pWriteAccess->Erase(COL_BLACK);
+            for (int i = 0; i < 8; ++i)
+            {
+                for (int j = 0; j < 8; ++j)
+                {
+                    pWriteAccess->SetPixel(i, j, COL_GRAY);
+                }
+            }
+            for (int i = 8; i < 16; ++i)
+            {
+                for (int j = 8; j < 16; ++j)
+                {
+                    pWriteAccess->SetPixel(i, j, COL_LIGHTGRAY);
+                }
+            }
+        }
+        BitmapEx aBitmapEx(aBitmap);
+
+        vcl::PngImageWriter aPngWriter(rStream);
+        CPPUNIT_ASSERT_EQUAL(true, aPngWriter.write(aBitmapEx));
+        aTempFile.CloseStream();
+    }
+    {
+        SvStream& rStream = *aTempFile.GetStream(StreamMode::READ);
+
+        vcl::PngImageReader aPngReader(rStream);
+        BitmapEx aBitmapEx;
+        CPPUNIT_ASSERT_EQUAL(true, aPngReader.read(aBitmapEx));
+
+        CPPUNIT_ASSERT_EQUAL(16L, aBitmapEx.GetSizePixel().Width());
+        CPPUNIT_ASSERT_EQUAL(16L, aBitmapEx.GetSizePixel().Height());
+
+        CPPUNIT_ASSERT_EQUAL(COL_GRAY, aBitmapEx.GetPixelColor(0, 0));
+        CPPUNIT_ASSERT_EQUAL(COL_LIGHTGRAY, aBitmapEx.GetPixelColor(15, 15));
+        CPPUNIT_ASSERT_EQUAL(COL_BLACK, aBitmapEx.GetPixelColor(15, 0));
+        CPPUNIT_ASSERT_EQUAL(COL_BLACK, aBitmapEx.GetPixelColor(0, 15));
+    }
+}
+
+void PngFilterTest::testPngRoundtrip24()
+{
+    utl::TempFile aTempFile(u"testPngRoundtrip24");
+    if (!bKeepTemp)
+        aTempFile.EnableKillingFile();
+    {
+        SvStream& rStream = *aTempFile.GetStream(StreamMode::WRITE);
+        Bitmap aBitmap(Size(16, 16), vcl::PixelFormat::N24_BPP);
+        {
+            BitmapScopedWriteAccess pWriteAccess(aBitmap);
+            pWriteAccess->Erase(COL_BLACK);
+            for (int i = 0; i < 8; ++i)
+            {
+                for (int j = 0; j < 8; ++j)
+                {
+                    pWriteAccess->SetPixel(i, j, COL_LIGHTRED);
+                }
+            }
+            for (int i = 8; i < 16; ++i)
+            {
+                for (int j = 8; j < 16; ++j)
+                {
+                    pWriteAccess->SetPixel(i, j, COL_LIGHTBLUE);
+                }
+            }
+        }
+        BitmapEx aBitmapEx(aBitmap);
+
+        vcl::PngImageWriter aPngWriter(rStream);
+        CPPUNIT_ASSERT_EQUAL(true, aPngWriter.write(aBitmapEx));
+    }
+    {
+        SvStream& rStream = *aTempFile.GetStream(StreamMode::READ);
+        rStream.Seek(0);
+
+        vcl::PngImageReader aPngReader(rStream);
+        BitmapEx aBitmapEx;
+        CPPUNIT_ASSERT_EQUAL(true, aPngReader.read(aBitmapEx));
+
+        CPPUNIT_ASSERT_EQUAL(16L, aBitmapEx.GetSizePixel().Width());
+        CPPUNIT_ASSERT_EQUAL(16L, aBitmapEx.GetSizePixel().Height());
+
+        CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aBitmapEx.GetPixelColor(0, 0));
+        CPPUNIT_ASSERT_EQUAL(COL_LIGHTBLUE, aBitmapEx.GetPixelColor(15, 15));
+        CPPUNIT_ASSERT_EQUAL(COL_BLACK, aBitmapEx.GetPixelColor(15, 0));
+        CPPUNIT_ASSERT_EQUAL(COL_BLACK, aBitmapEx.GetPixelColor(0, 15));
+    }
+}
+
+void PngFilterTest::testPngRoundtrip24_8()
+{
+    utl::TempFile aTempFile(u"testPngRoundtrip24_8");
+    if (!bKeepTemp)
+        aTempFile.EnableKillingFile();
+    {
+        SvStream& rStream = *aTempFile.GetStream(StreamMode::WRITE);
+        Bitmap aBitmap(Size(16, 16), vcl::PixelFormat::N24_BPP);
+        AlphaMask aAlpha(Size(16, 16));
+        {
+            BitmapScopedWriteAccess pWriteAccessBitmap(aBitmap);
+            AlphaScopedWriteAccess pWriteAccessAlpha(aAlpha);
+            pWriteAccessAlpha->Erase(Color(ColorTransparency, 0x00, 0xAA, 
0xAA, 0xAA));
+            pWriteAccessBitmap->Erase(COL_BLACK);
+            for (int i = 0; i < 8; ++i)
+            {
+                for (int j = 0; j < 8; ++j)
+                {
+                    pWriteAccessBitmap->SetPixel(i, j, COL_LIGHTRED);
+                    pWriteAccessAlpha->SetPixel(i, j,
+                                                Color(ColorTransparency, 0x00, 
0xBB, 0xBB, 0xBB));
+                }
+            }
+            for (int i = 8; i < 16; ++i)
+            {
+                for (int j = 8; j < 16; ++j)
+                {
+                    pWriteAccessBitmap->SetPixel(i, j, COL_LIGHTBLUE);
+                    pWriteAccessAlpha->SetPixel(i, j,
+                                                Color(ColorTransparency, 0x00, 
0xCC, 0xCC, 0xCC));
+                }
+            }
+        }
+        BitmapEx aBitmapEx(aBitmap, aAlpha);
+        vcl::PngImageWriter aPngWriter(rStream);
+        CPPUNIT_ASSERT_EQUAL(true, aPngWriter.write(aBitmapEx));
+    }
+    {
+        SvStream& rStream = *aTempFile.GetStream(StreamMode::READ);
+        rStream.Seek(0);
+
+        vcl::PngImageReader aPngReader(rStream);
+        BitmapEx aBitmapEx;
+        CPPUNIT_ASSERT_EQUAL(true, aPngReader.read(aBitmapEx));
+
+        CPPUNIT_ASSERT_EQUAL(16L, aBitmapEx.GetSizePixel().Width());
+        CPPUNIT_ASSERT_EQUAL(16L, aBitmapEx.GetSizePixel().Height());
+
+        CPPUNIT_ASSERT_EQUAL(Color(ColorTransparency, 0xBB, 0xFF, 0x00, 0x00),
+                             aBitmapEx.GetPixelColor(0, 0));
+        CPPUNIT_ASSERT_EQUAL(Color(ColorTransparency, 0xCC, 0x00, 0x00, 0xFF),
+                             aBitmapEx.GetPixelColor(15, 15));
+        CPPUNIT_ASSERT_EQUAL(Color(ColorTransparency, 0xAA, 0x00, 0x00, 0x00),
+                             aBitmapEx.GetPixelColor(15, 0));
+        CPPUNIT_ASSERT_EQUAL(Color(ColorTransparency, 0xAA, 0x00, 0x00, 0x00),
+                             aBitmapEx.GetPixelColor(0, 15));
+    }
+}
+
+void PngFilterTest::testPngRoundtrip32() {}
+
 CPPUNIT_TEST_SUITE_REGISTRATION(PngFilterTest);
 
 CPPUNIT_PLUGIN_IMPLEMENT();
diff --git a/vcl/source/filter/png/PngImageWriter.cxx 
b/vcl/source/filter/png/PngImageWriter.cxx
new file mode 100644
index 000000000000..b83683b181da
--- /dev/null
+++ b/vcl/source/filter/png/PngImageWriter.cxx
@@ -0,0 +1,196 @@
+/* -*- 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 <vcl/filter/PngImageWriter.hxx>
+#include <png.h>
+#include <bitmap/BitmapWriteAccess.hxx>
+#include <vcl/bitmap.hxx>
+
+namespace
+{
+void combineScanlineChannels(Scanline pRGBScanline, Scanline pAlphaScanline, 
Scanline pResult,
+                             sal_uInt32 nSize)
+{
+    assert(pRGBScanline && "RGB scanline is null");
+    assert(pAlphaScanline && "Alpha scanline is null");
+
+    for (sal_uInt32 i = 0; i < nSize; i++)
+    {
+        *pResult++ = *pRGBScanline++; // R
+        *pResult++ = *pRGBScanline++; // G
+        *pResult++ = *pRGBScanline++; // B
+        *pResult++ = *pAlphaScanline++; // A
+    }
+}
+}
+
+namespace vcl
+{
+static void lclWriteStream(png_structp pPng, png_bytep pData, png_size_t 
pDataSize)
+{
+    png_voidp pIO = png_get_io_ptr(pPng);
+
+    if (pIO == nullptr)
+        return;
+
+    SvStream* pStream = static_cast<SvStream*>(pIO);
+
+    sal_Size nBytesWritten = pStream->WriteBytes(pData, pDataSize);
+
+    if (nBytesWritten != pDataSize)
+        png_error(pPng, "Write Error");
+}
+
+static bool pngWrite(SvStream& rStream, BitmapEx& rBitmapEx, int 
nCompressionLevel)
+{
+    png_structp pPng = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, 
nullptr, nullptr);
+
+    if (!pPng)
+        return false;
+
+    png_infop pInfo = png_create_info_struct(pPng);
+    if (!pInfo)
+    {
+        png_destroy_write_struct(&pPng, nullptr);
+        return false;
+    }
+
+    Bitmap aBitmap;
+    AlphaMask aAlphaMask;
+    Bitmap::ScopedReadAccess pAccess;
+    Bitmap::ScopedReadAccess pAlphaAccess;
+
+    if (setjmp(png_jmpbuf(pPng)))
+    {
+        pAccess.reset();
+        pAlphaAccess.reset();
+        png_destroy_read_struct(&pPng, &pInfo, nullptr);
+        return false;
+    }
+
+    // Set our custom stream writer
+    png_set_write_fn(pPng, &rStream, lclWriteStream, nullptr);
+
+    aBitmap = rBitmapEx.GetBitmap();
+    aAlphaMask = rBitmapEx.GetAlpha();
+
+    {
+        pAccess = Bitmap::ScopedReadAccess(aBitmap);
+        pAlphaAccess = Bitmap::ScopedReadAccess(aAlphaMask);
+        Size aSize = rBitmapEx.GetSizePixel();
+
+        int bitDepth = -1;
+        int colorType = -1;
+
+        /* PNG_COLOR_TYPE_GRAY (1, 2, 4, 8, 16)
+           PNG_COLOR_TYPE_GRAY_ALPHA (8, 16)
+           PNG_COLOR_TYPE_PALETTE (bit depths 1, 2, 4, 8)
+           PNG_COLOR_TYPE_RGB (bit_depths 8, 16)
+           PNG_COLOR_TYPE_RGB_ALPHA (bit_depths 8, 16)
+           PNG_COLOR_MASK_PALETTE
+           PNG_COLOR_MASK_COLOR
+           PNG_COLOR_MASK_ALPHA
+        */
+        auto eScanlineFormat = pAccess->GetScanlineFormat();
+        switch (eScanlineFormat)
+        {
+            case ScanlineFormat::N8BitPal:
+            {
+                if (!aBitmap.HasGreyPalette8Bit())
+                    return false;
+                colorType = PNG_COLOR_TYPE_GRAY;
+                bitDepth = 8;
+                break;
+            }
+            case ScanlineFormat::N24BitTcBgr:
+            {
+                png_set_bgr(pPng);
+                [[fallthrough]];
+            }
+            case ScanlineFormat::N24BitTcRgb:
+            {
+                colorType = PNG_COLOR_TYPE_RGB;
+                bitDepth = 8;
+                if (pAlphaAccess)
+                    colorType = PNG_COLOR_TYPE_RGBA;
+                break;
+            }
+            default:
+            {
+                return false;
+            }
+        }
+
+        png_set_compression_level(pPng, nCompressionLevel);
+
+        int interlaceType = PNG_INTERLACE_NONE;
+        int compressionType = PNG_COMPRESSION_TYPE_DEFAULT;
+        int filterMethod = PNG_FILTER_TYPE_DEFAULT;
+
+        png_set_IHDR(pPng, pInfo, aSize.Width(), aSize.Height(), bitDepth, 
colorType, interlaceType,
+                     compressionType, filterMethod);
+
+        png_write_info(pPng, pInfo);
+
+        int nNumberOfPasses = 1;
+
+        Scanline pSourcePointer;
+
+        tools::Long nHeight = pAccess->Height();
+
+        for (int nPass = 0; nPass < nNumberOfPasses; nPass++)
+        {
+            for (tools::Long y = 0; y < nHeight; y++)
+            {
+                pSourcePointer = pAccess->GetScanline(y);
+                Scanline pFinalPointer = pSourcePointer;
+                std::vector<std::remove_pointer_t<Scanline>> aCombinedChannels;
+                if (pAlphaAccess)
+                {
+                    // Check that theres an alpha channel per 3 color/RGB 
channels
+                    assert(((pAlphaAccess->GetScanlineSize() * 3) == 
pAccess->GetScanlineSize())
+                           && "RGB and alpha channel size mismatch");
+                    // Allocate enough size to fit all 4 channels
+                    aCombinedChannels.resize(pAlphaAccess->GetScanlineSize()
+                                             + pAccess->GetScanlineSize());
+                    Scanline pAlphaPointer = pAlphaAccess->GetScanline(y);
+                    // Combine RGB and alpha channels
+                    combineScanlineChannels(pSourcePointer, pAlphaPointer, 
aCombinedChannels.data(),
+                                            pAlphaAccess->GetScanlineSize());
+                    pFinalPointer = aCombinedChannels.data();
+                    // Invert alpha channel (255 - a)
+                    png_set_invert_alpha(pPng);
+                }
+                png_write_row(pPng, pFinalPointer);
+            }
+        }
+    }
+
+    png_write_end(pPng, pInfo);
+
+    png_destroy_write_struct(&pPng, &pInfo);
+
+    return true;
+}
+
+PngImageWriter::PngImageWriter(SvStream& rStream)
+    : mrStream(rStream)
+    , mnCompressionLevel(6)
+    , mbInterlaced(false)
+{
+}
+
+bool PngImageWriter::write(BitmapEx& rBitmapEx)
+{
+    return pngWrite(mrStream, rBitmapEx, mnCompressionLevel);
+}
+
+} // namespace vcl
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

Reply via email to