summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorofftkp <parisoplop@gmail.com>2021-03-07 13:48:39 +0900
committerTomaž Vajngerl <quikee@gmail.com>2022-07-19 09:57:19 +0200
commit6b4f4bdf9beb0c73fa8b8bc218cd206f2cd347fa (patch)
treea3a111406026fc39f78e97c65634ea3219df2713
parent40405372fe0bbc00e67f5b0185b0d4c2d6c1e08d (diff)
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 <quikee@gmail.com>
-rw-r--r--include/vcl/filter/PngImageWriter.hxx49
-rw-r--r--vcl/Library_vcl.mk1
-rw-r--r--vcl/qa/cppunit/png/PngFilterTest.cxx173
-rw-r--r--vcl/source/filter/png/PngImageWriter.cxx196
4 files changed, 419 insertions, 0 deletions
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: */