diff options
author | Paris Oplopoios <paris.oplopoios@collabora.com> | 2023-08-07 01:52:52 +0300 |
---|---|---|
committer | Tomaž Vajngerl <quikee@gmail.com> | 2023-08-11 08:51:16 +0200 |
commit | b29769bfab386c9d56e72c4e9c9ec5abeb09eb46 (patch) | |
tree | 0a8bc776b6dc72b48cb077f3d1497ebb7fcaefc8 | |
parent | 47cff47966ea2876765bdfb2dfb83eaeb1db1e1a (diff) |
Initial APNG export support
Change-Id: I27877d4bdf27cd92bdd939fd25e3820edad10f9e
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/155387
Tested-by: Jenkins
Reviewed-by: Tomaž Vajngerl <quikee@gmail.com>
-rw-r--r-- | include/vcl/filter/PngImageWriter.hxx | 4 | ||||
-rw-r--r-- | vcl/qa/cppunit/png/PngFilterTest.cxx | 44 | ||||
-rw-r--r-- | vcl/source/filter/png/PngImageWriter.cxx | 184 |
3 files changed, 218 insertions, 14 deletions
diff --git a/include/vcl/filter/PngImageWriter.hxx b/include/vcl/filter/PngImageWriter.hxx index db34e0826136..b43c304fc1d8 100644 --- a/include/vcl/filter/PngImageWriter.hxx +++ b/include/vcl/filter/PngImageWriter.hxx @@ -12,7 +12,7 @@ #include <com/sun/star/beans/PropertyValue.hpp> #include <com/sun/star/uno/Sequence.hxx> #include <tools/stream.hxx> -#include <vcl/bitmapex.hxx> +#include <vcl/graph.hxx> #include <vector> #pragma once @@ -39,7 +39,7 @@ public: PngImageWriter(SvStream& rStream); void setParameters(css::uno::Sequence<css::beans::PropertyValue> const& rParameters); - bool write(const BitmapEx& rBitmap); + bool write(const Graphic& rGraphic); }; } // namespace vcl diff --git a/vcl/qa/cppunit/png/PngFilterTest.cxx b/vcl/qa/cppunit/png/PngFilterTest.cxx index 8e9c15e6dd49..51833b870d05 100644 --- a/vcl/qa/cppunit/png/PngFilterTest.cxx +++ b/vcl/qa/cppunit/png/PngFilterTest.cxx @@ -382,12 +382,46 @@ void PngFilterTest::testApng() CPPUNIT_ASSERT(aGraphic.IsAnimated()); CPPUNIT_ASSERT_EQUAL(size_t(2), aGraphic.GetAnimation().GetAnimationFrames().size()); - auto aFrame1 = aGraphic.GetAnimation().GetAnimationFrames()[0]->maBitmapEx; - auto aFrame2 = aGraphic.GetAnimation().GetAnimationFrames()[1]->maBitmapEx; + AnimationFrame aFrame1 = *aGraphic.GetAnimation().GetAnimationFrames()[0]; + AnimationFrame aFrame2 = *aGraphic.GetAnimation().GetAnimationFrames()[1]; - CPPUNIT_ASSERT_EQUAL(COL_WHITE, aFrame1.GetPixelColor(0, 0)); - CPPUNIT_ASSERT_EQUAL(Color(0x72d1c8), aFrame1.GetPixelColor(2, 2)); - CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aFrame2.GetPixelColor(0, 0)); + CPPUNIT_ASSERT_EQUAL(COL_WHITE, aFrame1.maBitmapEx.GetPixelColor(0, 0)); + CPPUNIT_ASSERT_EQUAL(Color(0x72d1c8), aFrame1.maBitmapEx.GetPixelColor(2, 2)); + CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aFrame2.maBitmapEx.GetPixelColor(0, 0)); + + // Roundtrip the APNG + SvMemoryStream aOutStream; + vcl::PngImageWriter aPngWriter(aOutStream); + bSuccess = aPngWriter.write(aGraphic); + CPPUNIT_ASSERT(bSuccess); + + aOutStream.Seek(STREAM_SEEK_TO_BEGIN); + vcl::PngImageReader aPngReader2(aOutStream); + Graphic aGraphic2; + bSuccess = aPngReader2.read(aGraphic2); + CPPUNIT_ASSERT(bSuccess); + CPPUNIT_ASSERT(aGraphic2.IsAnimated()); + CPPUNIT_ASSERT_EQUAL(size_t(2), aGraphic2.GetAnimation().GetAnimationFrames().size()); + + AnimationFrame aFrame1Roundtripped = *aGraphic2.GetAnimation().GetAnimationFrames()[0]; + AnimationFrame aFrame2Roundtripped = *aGraphic2.GetAnimation().GetAnimationFrames()[1]; + + CPPUNIT_ASSERT_EQUAL(COL_WHITE, aFrame1Roundtripped.maBitmapEx.GetPixelColor(0, 0)); + CPPUNIT_ASSERT_EQUAL(Color(0x72d1c8), aFrame1Roundtripped.maBitmapEx.GetPixelColor(2, 2)); + CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aFrame2Roundtripped.maBitmapEx.GetPixelColor(0, 0)); + + // Make sure the two frames have the same properties + CPPUNIT_ASSERT_EQUAL(aFrame1.maPositionPixel, aFrame1Roundtripped.maPositionPixel); + CPPUNIT_ASSERT_EQUAL(aFrame1.maSizePixel, aFrame1Roundtripped.maSizePixel); + CPPUNIT_ASSERT_EQUAL(aFrame1.mnWait, aFrame1Roundtripped.mnWait); + CPPUNIT_ASSERT_EQUAL(aFrame1.meDisposal, aFrame1Roundtripped.meDisposal); + CPPUNIT_ASSERT_EQUAL(aFrame1.meBlend, aFrame1Roundtripped.meBlend); + + CPPUNIT_ASSERT_EQUAL(aFrame2.maPositionPixel, aFrame2Roundtripped.maPositionPixel); + CPPUNIT_ASSERT_EQUAL(aFrame2.maSizePixel, aFrame2Roundtripped.maSizePixel); + CPPUNIT_ASSERT_EQUAL(aFrame2.mnWait, aFrame2Roundtripped.mnWait); + CPPUNIT_ASSERT_EQUAL(aFrame2.meDisposal, aFrame2Roundtripped.meDisposal); + CPPUNIT_ASSERT_EQUAL(aFrame2.meBlend, aFrame2Roundtripped.meBlend); } void PngFilterTest::testPngSuite() diff --git a/vcl/source/filter/png/PngImageWriter.cxx b/vcl/source/filter/png/PngImageWriter.cxx index 13e23fcb2e9b..09bddf7b2f58 100644 --- a/vcl/source/filter/png/PngImageWriter.cxx +++ b/vcl/source/filter/png/PngImageWriter.cxx @@ -11,7 +11,10 @@ #include <png.h> #include <bitmap/BitmapWriteAccess.hxx> #include <vcl/bitmap.hxx> +#include <vcl/bitmapex.hxx> #include <vcl/BitmapTools.hxx> +#include <sal/log.hxx> +#include <rtl/crc.h> namespace { @@ -56,13 +59,80 @@ static void lclWriteStream(png_structp pPng, png_bytep pData, png_size_t pDataSi png_error(pPng, "Write Error"); } -static bool pngWrite(SvStream& rStream, const BitmapEx& rBitmapEx, int nCompressionLevel, +static void writeFctlChunk(std::vector<uint8_t>& aFctlChunk, sal_uInt32 nSequenceNumber, Size aSize, + Point aOffset, sal_uInt16 nDelayNum, sal_uInt16 nDelayDen, + Disposal nDisposeOp, Blend nBlendOp) +{ + if (aFctlChunk.size() != 26) + aFctlChunk.resize(26); + + sal_uInt32 nWidth = aSize.Width(); + sal_uInt32 nHeight = aSize.Height(); + sal_uInt32 nXOffset = aOffset.X(); + sal_uInt32 nYOffset = aOffset.Y(); + + // Writing each byte separately instead of using memcpy here for clarity + // about PNG chunks using big endian + + // Write sequence number + aFctlChunk[0] = (nSequenceNumber >> 24) & 0xFF; + aFctlChunk[1] = (nSequenceNumber >> 16) & 0xFF; + aFctlChunk[2] = (nSequenceNumber >> 8) & 0xFF; + aFctlChunk[3] = nSequenceNumber & 0xFF; + + // Write width + aFctlChunk[4] = (nWidth >> 24) & 0xFF; + aFctlChunk[5] = (nWidth >> 16) & 0xFF; + aFctlChunk[6] = (nWidth >> 8) & 0xFF; + aFctlChunk[7] = nWidth & 0xFF; + + // Write height + aFctlChunk[8] = (nHeight >> 24) & 0xFF; + aFctlChunk[9] = (nHeight >> 16) & 0xFF; + aFctlChunk[10] = (nHeight >> 8) & 0xFF; + aFctlChunk[11] = nHeight & 0xFF; + + // Write x offset + aFctlChunk[12] = (nXOffset >> 24) & 0xFF; + aFctlChunk[13] = (nXOffset >> 16) & 0xFF; + aFctlChunk[14] = (nXOffset >> 8) & 0xFF; + aFctlChunk[15] = nXOffset & 0xFF; + + // Write y offset + aFctlChunk[16] = (nYOffset >> 24) & 0xFF; + aFctlChunk[17] = (nYOffset >> 16) & 0xFF; + aFctlChunk[18] = (nYOffset >> 8) & 0xFF; + aFctlChunk[19] = nYOffset & 0xFF; + + // Write delay numerator + aFctlChunk[20] = (nDelayNum >> 8) & 0xFF; + aFctlChunk[21] = nDelayNum & 0xFF; + + // Write delay denominator + aFctlChunk[22] = (nDelayDen >> 8) & 0xFF; + aFctlChunk[23] = nDelayDen & 0xFF; + + // Write disposal method + aFctlChunk[24] = static_cast<uint8_t>(nDisposeOp); + + // Write blend operation + aFctlChunk[25] = static_cast<uint8_t>(nBlendOp); +} + +static bool pngWrite(SvStream& rStream, const Graphic& rGraphic, int nCompressionLevel, bool bInterlaced, bool bTranslucent, const std::vector<PngChunk>& aAdditionalChunks) { - if (rBitmapEx.IsEmpty()) + if (rGraphic.IsNone()) return false; + Animation aAnimation; + sal_uInt32 nSequenceNumber = 0; + bool bIsApng = rGraphic.IsAnimated(); + + if (bIsApng) + aAnimation = rGraphic.GetAnimation(); + png_structp pPng = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!pPng) @@ -76,14 +146,14 @@ static bool pngWrite(SvStream& rStream, const BitmapEx& rBitmapEx, int nCompress } BitmapEx aBitmapEx; - if (rBitmapEx.GetBitmap().getPixelFormat() == vcl::PixelFormat::N32_BPP) + if (rGraphic.GetBitmapEx().getPixelFormat() == vcl::PixelFormat::N32_BPP) { - if (!vcl::bitmap::convertBitmap32To24Plus8(rBitmapEx, aBitmapEx)) + if (!vcl::bitmap::convertBitmap32To24Plus8(rGraphic.GetBitmapExRef(), aBitmapEx)) return false; } else { - aBitmapEx = rBitmapEx; + aBitmapEx = rGraphic.GetBitmapExRef(); } if (!bTranslucent) @@ -228,6 +298,43 @@ static bool pngWrite(SvStream& rStream, const BitmapEx& rBitmapEx, int nCompress png_write_info(pPng, pInfo); + if (bIsApng) + { + // Write acTL chunk + sal_uInt32 nNumFrames = aAnimation.Count(); + sal_uInt32 nNumPlays = aAnimation.GetLoopCount(); + + std::vector<uint8_t> aActlChunk; + aActlChunk.resize(8); + + // Write number of frames + aActlChunk[0] = (nNumFrames >> 24) & 0xFF; + aActlChunk[1] = (nNumFrames >> 16) & 0xFF; + aActlChunk[2] = (nNumFrames >> 8) & 0xFF; + aActlChunk[3] = nNumFrames & 0xFF; + + // Write number of plays + aActlChunk[4] = (nNumPlays >> 24) & 0xFF; + aActlChunk[5] = (nNumPlays >> 16) & 0xFF; + aActlChunk[6] = (nNumPlays >> 8) & 0xFF; + aActlChunk[7] = nNumPlays & 0xFF; + + png_write_chunk(pPng, reinterpret_cast<png_const_bytep>("acTL"), + reinterpret_cast<png_const_bytep>(aActlChunk.data()), + aActlChunk.size()); + + // Write first frame fcTL chunk which is corresponding to the IDAT chunk + std::vector<uint8_t> aFctlChunk; + const AnimationFrame& rFirstFrame = *aAnimation.GetAnimationFrames()[0]; + writeFctlChunk(aFctlChunk, nSequenceNumber++, rFirstFrame.maSizePixel, + rFirstFrame.maPositionPixel, rFirstFrame.mnWait, 100, + rFirstFrame.meDisposal, rFirstFrame.meBlend); + + png_write_chunk(pPng, reinterpret_cast<png_const_bytep>("fcTL"), + reinterpret_cast<png_const_bytep>(aFctlChunk.data()), + aFctlChunk.size()); + } + int nNumberOfPasses = 1; Scanline pSourcePointer; @@ -259,6 +366,69 @@ static bool pngWrite(SvStream& rStream, const BitmapEx& rBitmapEx, int nCompress } } + if (bIsApng) + { + // Already wrote first frame as an IDAT chunk + // Need to write the rest of the frames as fcTL & fdAT chunks + const auto& rFrames = aAnimation.GetAnimationFrames(); + + for (uint32_t i = 0; i < rFrames.size() - 1; i++) + { + const AnimationFrame& rCurrentFrame = *rFrames[1 + i]; + SvMemoryStream aStream; + + if (!pngWrite(aStream, rCurrentFrame.maBitmapEx, nCompressionLevel, bInterlaced, + bTranslucent, {})) + return false; + + std::vector<uint8_t> aFdatChunk; + + aStream.SetEndian(SvStreamEndian::BIG); + + aStream.Seek(STREAM_SEEK_TO_BEGIN); + aStream.Seek(8); // Skip PNG signature + + while (aStream.good()) + { + sal_uInt32 nChunkSize; + char sChunkName[4] = { 0 }; + aStream.ReadUInt32(nChunkSize); + aStream.ReadBytes(sChunkName, 4); + + if (std::string(sChunkName, 4) == "IDAT") + { + // 4 extra bytes for the sequence number + aFdatChunk.resize(nChunkSize + 4); + aStream.ReadBytes(aFdatChunk.data() + 4, nChunkSize); + break; + } + else + { + aStream.SeekRel(nChunkSize + 4); + } + } + + std::vector<uint8_t> aFctlChunk; + writeFctlChunk(aFctlChunk, nSequenceNumber++, rCurrentFrame.maSizePixel, + rCurrentFrame.maPositionPixel, rCurrentFrame.mnWait, 100, + rCurrentFrame.meDisposal, rCurrentFrame.meBlend); + + // Write sequence number + aFdatChunk[0] = nSequenceNumber >> 24; + aFdatChunk[1] = nSequenceNumber >> 16; + aFdatChunk[2] = nSequenceNumber >> 8; + aFdatChunk[3] = nSequenceNumber; + nSequenceNumber++; + + png_write_chunk(pPng, reinterpret_cast<png_const_bytep>("fcTL"), + reinterpret_cast<png_const_bytep>(aFctlChunk.data()), + aFctlChunk.size()); + png_write_chunk(pPng, reinterpret_cast<png_const_bytep>("fdAT"), + reinterpret_cast<png_const_bytep>(aFdatChunk.data()), + aFdatChunk.size()); + } + } + if (!aAdditionalChunks.empty()) { for (const auto& aChunk : aAdditionalChunks) @@ -333,9 +503,9 @@ PngImageWriter::PngImageWriter(SvStream& rStream) { } -bool PngImageWriter::write(const BitmapEx& rBitmapEx) +bool PngImageWriter::write(const Graphic& rGraphic) { - return pngWrite(mrStream, rBitmapEx, mnCompressionLevel, mbInterlaced, mbTranslucent, + return pngWrite(mrStream, rGraphic, mnCompressionLevel, mbInterlaced, mbTranslucent, maAdditionalChunks); } |