include/vcl/filter/PngImageWriter.hxx    |    4 
 vcl/qa/cppunit/png/PngFilterTest.cxx     |   44 ++++++-
 vcl/source/filter/png/PngImageWriter.cxx |  184 +++++++++++++++++++++++++++++--
 3 files changed, 218 insertions(+), 14 deletions(-)

New commits:
commit b29769bfab386c9d56e72c4e9c9ec5abeb09eb46
Author:     Paris Oplopoios <[email protected]>
AuthorDate: Mon Aug 7 01:52:52 2023 +0300
Commit:     Tomaž Vajngerl <[email protected]>
CommitDate: Fri Aug 11 08:51:16 2023 +0200

    Initial APNG export support
    
    Change-Id: I27877d4bdf27cd92bdd939fd25e3820edad10f9e
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/155387
    Tested-by: Jenkins
    Reviewed-by: Tomaž Vajngerl <[email protected]>

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);
 }
 

Reply via email to