diff options
author | offtkp <parisoplop@gmail.com> | 2021-03-07 13:48:39 +0900 |
---|---|---|
committer | Tomaž Vajngerl <quikee@gmail.com> | 2022-07-19 09:57:19 +0200 |
commit | 6b4f4bdf9beb0c73fa8b8bc218cd206f2cd347fa (patch) | |
tree | a3a111406026fc39f78e97c65634ea3219df2713 | |
parent | 40405372fe0bbc00e67f5b0185b0d4c2d6c1e08d (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.hxx | 49 | ||||
-rw-r--r-- | vcl/Library_vcl.mk | 1 | ||||
-rw-r--r-- | vcl/qa/cppunit/png/PngFilterTest.cxx | 173 | ||||
-rw-r--r-- | vcl/source/filter/png/PngImageWriter.cxx | 196 |
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: */ |