summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorParis Oplopoios <paris.oplopoios@collabora.com>2023-02-06 14:19:41 +0200
committerTomaž Vajngerl <quikee@gmail.com>2023-06-10 16:10:39 +0200
commitbf944e33569e4a1d6236a54671b7320cdc6ffaf6 (patch)
tree682aa51909caed66065e36df17148f580196cf45
parenta7b5fe0c813977347a8d10ac5a52862f6150fbc0 (diff)
tdf#104877 Add basic APNG format support
Add basic APNG (animated PNG) format support that can correctly display simple files with no transparency Change-Id: Ibfb6e13953a8ba48a535a40b08792b3723b7dc0b Reviewed-on: https://gerrit.libreoffice.org/c/core/+/140089 Tested-by: Jenkins Reviewed-by: Tomaž Vajngerl <quikee@gmail.com>
-rw-r--r--filter/source/config/fragments/types/png_Portable_Network_Graphic.xcu2
-rw-r--r--include/vcl/filter/PngImageReader.hxx4
-rw-r--r--include/vcl/graphic/GraphicMetadata.hxx3
-rw-r--r--vcl/inc/graphic/GraphicFormatDetector.hxx4
-rw-r--r--vcl/qa/cppunit/GraphicFormatDetectorTest.cxx17
-rw-r--r--vcl/qa/cppunit/data/TypeDetectionExample.apngbin0 -> 239 bytes
-rw-r--r--vcl/qa/cppunit/png/PngFilterTest.cxx20
-rw-r--r--vcl/qa/cppunit/png/data/apng_simple.apngbin0 -> 239 bytes
-rw-r--r--vcl/source/filter/GraphicFormatDetector.cxx22
-rw-r--r--vcl/source/filter/graphicfilter.cxx7
-rw-r--r--vcl/source/filter/png/PngImageReader.cxx424
11 files changed, 475 insertions, 28 deletions
diff --git a/filter/source/config/fragments/types/png_Portable_Network_Graphic.xcu b/filter/source/config/fragments/types/png_Portable_Network_Graphic.xcu
index 7fe5e2adbfda..0b5be4b19923 100644
--- a/filter/source/config/fragments/types/png_Portable_Network_Graphic.xcu
+++ b/filter/source/config/fragments/types/png_Portable_Network_Graphic.xcu
@@ -18,7 +18,7 @@
<node oor:name="png_Portable_Network_Graphic" oor:op="replace" >
<prop oor:name="DetectService"><value>com.sun.star.comp.draw.FormatDetector</value></prop>
<prop oor:name="URLPattern"/>
- <prop oor:name="Extensions"><value>png</value></prop>
+ <prop oor:name="Extensions"><value>png apng</value></prop>
<prop oor:name="MediaType"><value>image/png</value></prop>
<prop oor:name="Preferred"><value>false</value></prop>
<prop oor:name="PreferredFilter"><value>PNG - Portable Network Graphic</value></prop>
diff --git a/include/vcl/filter/PngImageReader.hxx b/include/vcl/filter/PngImageReader.hxx
index 34b8279bc654..01fdc2915e54 100644
--- a/include/vcl/filter/PngImageReader.hxx
+++ b/include/vcl/filter/PngImageReader.hxx
@@ -26,6 +26,7 @@ namespace com::sun::star::task
class XStatusIndicator;
}
+class Graphic;
class BitmapEx;
class SvStream;
@@ -42,12 +43,15 @@ public:
// Returns true if image was successfully read without errors.
// A usable bitmap may be returned even if there were errors (e.g. incomplete image).
bool read(BitmapEx& rBitmap);
+ bool read(Graphic& rGraphic);
// Returns a bitmap without indicating if there were errors.
BitmapEx read();
// Returns the contents of the msOG chunk (containing a Gif image), if it exists.
// Does not change position in the stream.
static BinaryDataContainer getMicrosoftGifChunk(SvStream& rStream);
+
+ static bool isAPng(SvStream& rStream);
};
} // namespace vcl
diff --git a/include/vcl/graphic/GraphicMetadata.hxx b/include/vcl/graphic/GraphicMetadata.hxx
index da27fde01514..46c3d12a12a2 100644
--- a/include/vcl/graphic/GraphicMetadata.hxx
+++ b/include/vcl/graphic/GraphicMetadata.hxx
@@ -42,7 +42,8 @@ enum class GraphicFileFormat
SVG = 0x00f9,
WMZ = 0x00fa,
EMZ = 0x00fb,
- SVGZ = 0x00fc
+ SVGZ = 0x00fc,
+ APNG = 0x00fd
};
struct GraphicMetadata
{
diff --git a/vcl/inc/graphic/GraphicFormatDetector.hxx b/vcl/inc/graphic/GraphicFormatDetector.hxx
index 64d1e74de80a..2dfb41b461ba 100644
--- a/vcl/inc/graphic/GraphicFormatDetector.hxx
+++ b/vcl/inc/graphic/GraphicFormatDetector.hxx
@@ -52,6 +52,9 @@ static inline OUString getImportFormatShortName(GraphicFileFormat nFormat)
case GraphicFileFormat::PNG:
pKeyName = "PNG";
break;
+ case GraphicFileFormat::APNG:
+ pKeyName = "APNG";
+ break;
case GraphicFileFormat::XBM:
pKeyName = "XBM";
break;
@@ -163,6 +166,7 @@ public:
bool checkTIF();
bool checkGIF();
bool checkPNG();
+ bool checkAPNG();
bool checkJPG();
bool checkSVM();
bool checkPCD();
diff --git a/vcl/qa/cppunit/GraphicFormatDetectorTest.cxx b/vcl/qa/cppunit/GraphicFormatDetectorTest.cxx
index 30cd8f9ebe1e..945cb6d1c613 100644
--- a/vcl/qa/cppunit/GraphicFormatDetectorTest.cxx
+++ b/vcl/qa/cppunit/GraphicFormatDetectorTest.cxx
@@ -39,6 +39,7 @@ class GraphicFormatDetectorTest : public test::BootstrapFixtureBase
void testDetectPCX();
void testDetectJPG();
void testDetectPNG();
+ void testDetectAPNG();
void testDetectGIF();
void testDetectPSD();
void testDetectTGA();
@@ -63,6 +64,7 @@ class GraphicFormatDetectorTest : public test::BootstrapFixtureBase
CPPUNIT_TEST(testDetectPCX);
CPPUNIT_TEST(testDetectJPG);
CPPUNIT_TEST(testDetectPNG);
+ CPPUNIT_TEST(testDetectAPNG);
CPPUNIT_TEST(testDetectGIF);
CPPUNIT_TEST(testDetectPSD);
CPPUNIT_TEST(testDetectTGA);
@@ -186,6 +188,21 @@ void GraphicFormatDetectorTest::testDetectPNG()
CPPUNIT_ASSERT_EQUAL(OUString("PNG"), rFormatExtension);
}
+void GraphicFormatDetectorTest::testDetectAPNG()
+{
+ SvFileStream aFileStream(getFullUrl(u"TypeDetectionExample.apng"), StreamMode::READ);
+ vcl::GraphicFormatDetector aDetector(aFileStream, "APNG");
+
+ CPPUNIT_ASSERT(aDetector.detect());
+ CPPUNIT_ASSERT(aDetector.checkAPNG());
+
+ aFileStream.Seek(aDetector.mnStreamPosition);
+
+ OUString rFormatExtension;
+ CPPUNIT_ASSERT(vcl::peekGraphicFormat(aFileStream, rFormatExtension, false));
+ CPPUNIT_ASSERT_EQUAL(OUString("APNG"), rFormatExtension);
+}
+
void GraphicFormatDetectorTest::testDetectGIF()
{
SvFileStream aFileStream(getFullUrl(u"TypeDetectionExample.gif"), StreamMode::READ);
diff --git a/vcl/qa/cppunit/data/TypeDetectionExample.apng b/vcl/qa/cppunit/data/TypeDetectionExample.apng
new file mode 100644
index 000000000000..445b7acaf4c8
--- /dev/null
+++ b/vcl/qa/cppunit/data/TypeDetectionExample.apng
Binary files differ
diff --git a/vcl/qa/cppunit/png/PngFilterTest.cxx b/vcl/qa/cppunit/png/PngFilterTest.cxx
index fc3963c356e8..760e12f8b3fc 100644
--- a/vcl/qa/cppunit/png/PngFilterTest.cxx
+++ b/vcl/qa/cppunit/png/PngFilterTest.cxx
@@ -170,6 +170,7 @@ public:
}
void testPng();
+ void testApng();
void testPngSuite();
void testMsGifInPng();
void testPngRoundtrip8BitGrey();
@@ -182,6 +183,7 @@ public:
CPPUNIT_TEST_SUITE(PngFilterTest);
CPPUNIT_TEST(testPng);
+ CPPUNIT_TEST(testApng);
CPPUNIT_TEST(testPngSuite);
CPPUNIT_TEST(testMsGifInPng);
CPPUNIT_TEST(testPngRoundtrip8BitGrey);
@@ -370,6 +372,24 @@ void PngFilterTest::testPng()
}
}
+void PngFilterTest::testApng()
+{
+ SvFileStream aFileStream(getFullUrl(u"apng_simple.apng"), StreamMode::READ);
+ vcl::PngImageReader aPngReader(aFileStream);
+ Graphic aGraphic;
+ bool bSuccess = aPngReader.read(aGraphic);
+ CPPUNIT_ASSERT(bSuccess);
+ 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;
+
+ 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));
+}
+
void PngFilterTest::testPngSuite()
{
// Test the PngSuite test files by Willem van Schaik
diff --git a/vcl/qa/cppunit/png/data/apng_simple.apng b/vcl/qa/cppunit/png/data/apng_simple.apng
new file mode 100644
index 000000000000..445b7acaf4c8
--- /dev/null
+++ b/vcl/qa/cppunit/png/data/apng_simple.apng
Binary files differ
diff --git a/vcl/source/filter/GraphicFormatDetector.cxx b/vcl/source/filter/GraphicFormatDetector.cxx
index a0cdd2416c57..fc5a0fdc79c3 100644
--- a/vcl/source/filter/GraphicFormatDetector.cxx
+++ b/vcl/source/filter/GraphicFormatDetector.cxx
@@ -21,6 +21,7 @@
#include <algorithm>
+#include <vcl/filter/PngImageReader.hxx>
#include <graphic/GraphicFormatDetector.hxx>
#include <graphic/DetectorTools.hxx>
#include <tools/solar.h>
@@ -134,6 +135,16 @@ bool peekGraphicFormat(SvStream& rStream, OUString& rFormatExtension, bool bTest
}
}
+ if (!bTest || rFormatExtension.startsWith("APNG"))
+ {
+ bSomethingTested = true;
+ if (aDetector.checkAPNG())
+ {
+ rFormatExtension = getImportFormatShortName(aDetector.getMetadata().mnFormat);
+ return true;
+ }
+ }
+
if (!bTest || rFormatExtension.startsWith("PNG"))
{
bSomethingTested = true;
@@ -942,6 +953,17 @@ bool GraphicFormatDetector::checkPNG()
return false;
}
+bool GraphicFormatDetector::checkAPNG()
+{
+ mrStream.Seek(mnStreamPosition);
+ if (PngImageReader::isAPng(mrStream))
+ {
+ maMetadata.mnFormat = GraphicFileFormat::APNG;
+ return true;
+ }
+ return false;
+}
+
bool GraphicFormatDetector::checkJPG()
{
if ((mnFirstLong == 0xffd8ffe0 && maFirstBytes[6] == 0x4a && maFirstBytes[7] == 0x46
diff --git a/vcl/source/filter/graphicfilter.cxx b/vcl/source/filter/graphicfilter.cxx
index 7eb8918b600e..38983c7e3190 100644
--- a/vcl/source/filter/graphicfilter.cxx
+++ b/vcl/source/filter/graphicfilter.cxx
@@ -944,11 +944,12 @@ ErrCode GraphicFilter::readPNG(SvStream & rStream, Graphic & rGraphic, GfxLinkTy
}
// PNG has no GIF chunk
+ Graphic aGraphic;
vcl::PngImageReader aPNGReader(rStream);
- BitmapEx aBitmapEx(aPNGReader.read());
- if (!aBitmapEx.IsEmpty())
+ aPNGReader.read(aGraphic);
+ if (!aGraphic.GetBitmapEx().IsEmpty())
{
- rGraphic = aBitmapEx;
+ rGraphic = aGraphic;
rLinkType = GfxLinkType::NativePng;
}
else
diff --git a/vcl/source/filter/png/PngImageReader.cxx b/vcl/source/filter/png/PngImageReader.cxx
index 5b92c351d8d2..a23fcca6c80b 100644
--- a/vcl/source/filter/png/PngImageReader.cxx
+++ b/vcl/source/filter/png/PngImageReader.cxx
@@ -16,6 +16,8 @@
#include <vcl/alpha.hxx>
#include <vcl/BitmapTools.hxx>
#include <unotools/configmgr.hxx>
+#include <comphelper/scopeguard.hxx>
+#include <osl/endian.h>
#include <bitmap/BitmapWriteAccess.hxx>
#include <svdata.hxx>
@@ -50,6 +52,19 @@ void lclReadStream(png_structp pPng, png_bytep pOutBytes, png_size_t nBytesToRea
}
constexpr int PNG_SIGNATURE_SIZE = 8;
+constexpr int PNG_IHDR_SIZE = 13;
+constexpr int PNG_TYPE_SIZE = 4;
+constexpr int PNG_SIZE_SIZE = 4;
+constexpr int PNG_CRC_SIZE = 4;
+constexpr int PNG_IEND_SIZE = 0;
+constexpr sal_uInt64 PNG_SIGNATURE = 0x89504E470D0A1A0A;
+constexpr sal_uInt32 PNG_IHDR_SIGNATURE = 0x49484452;
+constexpr sal_uInt32 PNG_IDAT_SIGNATURE = 0x49444154;
+constexpr sal_uInt32 PNG_ACTL_SIGNATURE = 0x6163544C;
+constexpr sal_uInt32 PNG_FCTL_SIGNATURE = 0x6663544C;
+constexpr sal_uInt32 PNG_FDAT_SIGNATURE = 0x66644154;
+constexpr sal_uInt32 PNG_IEND_SIGNATURE = 0x49454E44;
+constexpr sal_uInt32 PNG_IEND_CRC = 0xAE426082;
bool isPng(SvStream& rStream)
{
@@ -67,11 +82,245 @@ struct PngDestructor
png_infop pInfo;
};
+/// Animation Control chunk for APNG files
+struct acTLChunk
+{
+ sal_uInt32 num_frames;
+ sal_uInt32 num_plays;
+};
+
+/// Base class for fcTL and fdAT chunks since both of these chunks use a sequence number for ordering
+struct FrameDataChunk
+{
+ sal_uInt32 sequence_number;
+ virtual ~FrameDataChunk() = default;
+};
+
+/// fcTL (Frame Control) chunk for APNG files
+struct fcTLChunk : public FrameDataChunk
+{
+ sal_uInt32 width;
+ sal_uInt32 height;
+ sal_uInt32 x_offset;
+ sal_uInt32 y_offset;
+ sal_uInt16 delay_num;
+ sal_uInt16 delay_den;
+ sal_uInt8 dispose_op;
+ sal_uInt8 blend_op;
+};
+
+/// fdAT (Frame Data) chunk for APNG files
+struct fdATChunk : public FrameDataChunk
+{
+ std::vector<sal_uInt8> frame_data;
+};
+
+/// APNG chunk holder class, used for the user pointer in the libpng callback function
+struct APNGInfo
+{
+ bool mbIsApng = false;
+ acTLChunk maACTLChunk;
+ std::vector<std::unique_ptr<FrameDataChunk>> maFrameData;
+};
+
+int handle_unknown_chunk(png_structp png, png_unknown_chunkp chunk)
+{
+ std::string sName(chunk->name, chunk->name + 4);
+ APNGInfo* aAPNGInfo = static_cast<APNGInfo*>(png_get_user_chunk_ptr(png));
+ if (sName == "acTL")
+ {
+ aAPNGInfo->maACTLChunk = *reinterpret_cast<acTLChunk*>(chunk->data);
+ aAPNGInfo->maACTLChunk.num_frames = OSL_SWAPDWORD(aAPNGInfo->maACTLChunk.num_frames);
+ aAPNGInfo->maACTLChunk.num_plays = OSL_SWAPDWORD(aAPNGInfo->maACTLChunk.num_plays);
+ aAPNGInfo->mbIsApng = true;
+ }
+ else
+ {
+ std::unique_ptr<FrameDataChunk> pBaseChunk;
+ sal_uInt32 nSequenceNumber = 0;
+ std::memcpy(&nSequenceNumber, chunk->data, 4);
+ nSequenceNumber = OSL_SWAPDWORD(nSequenceNumber);
+
+ if (sName == "fcTL")
+ {
+ // Can't check with sizeof(fcTLChunk) because it may not be packed
+ if (chunk->size != 26)
+ {
+ return -1;
+ }
+
+ // byte
+ // 0 sequence_number (unsigned int) Sequence number of the animation chunk, starting from 0
+ // 4 width (unsigned int) Width of the following frame
+ // 8 height (unsigned int) Height of the following frame
+ // 12 x_offset (unsigned int) X position at which to render the following frame
+ // 16 y_offset (unsigned int) Y position at which to render the following frame
+ // 20 delay_num (unsigned short) Frame delay fraction numerator
+ // 22 delay_den (unsigned short) Frame delay fraction denominator
+ // 24 dispose_op (byte) Type of frame area disposal to be done after rendering this frame
+ // 25 blend_op (byte) Type of frame area rendering for this frame
+
+ // memcpy each member instead of reinterpret_cast because struct may not be packed
+ std::unique_ptr<fcTLChunk> aChunk = std::make_unique<fcTLChunk>();
+ std::memcpy(&aChunk->width, chunk->data + 4, 4);
+ std::memcpy(&aChunk->height, chunk->data + 8, 4);
+ std::memcpy(&aChunk->x_offset, chunk->data + 12, 4);
+ std::memcpy(&aChunk->y_offset, chunk->data + 16, 4);
+ std::memcpy(&aChunk->delay_num, chunk->data + 20, 2);
+ std::memcpy(&aChunk->delay_den, chunk->data + 22, 2);
+ std::memcpy(&aChunk->dispose_op, chunk->data + 24, 1);
+ std::memcpy(&aChunk->blend_op, chunk->data + 25, 1);
+ aChunk->width = OSL_SWAPDWORD(aChunk->width);
+ aChunk->height = OSL_SWAPDWORD(aChunk->height);
+ aChunk->x_offset = OSL_SWAPDWORD(aChunk->x_offset);
+ aChunk->y_offset = OSL_SWAPDWORD(aChunk->y_offset);
+ aChunk->delay_num = OSL_SWAPWORD(aChunk->delay_num);
+ aChunk->delay_den = OSL_SWAPWORD(aChunk->delay_den);
+ pBaseChunk = std::move(aChunk);
+ }
+ else if (sName == "fdAT")
+ {
+ std::unique_ptr<fdATChunk> aChunk = std::make_unique<fdATChunk>();
+ size_t nDataSize = chunk->size;
+ aChunk->frame_data.resize(nDataSize);
+ // Replace sequence number with the IDAT signature
+ sal_uInt32 nIDATSwapped = OSL_SWAPDWORD(PNG_IDAT_SIGNATURE);
+ std::memcpy(aChunk->frame_data.data(), &nIDATSwapped, 4);
+ // Skip sequence number when copying
+ std::memcpy(aChunk->frame_data.data() + 4, chunk->data + 4, nDataSize - 4);
+ pBaseChunk = std::move(aChunk);
+ }
+ else
+ {
+ // Unknown ancilliary chunk
+ return 0;
+ }
+
+ pBaseChunk->sequence_number = nSequenceNumber;
+ if (pBaseChunk->sequence_number < aAPNGInfo->maFrameData.size())
+ {
+ // Make sure chunks are ordered based on their sequence number because the
+ // png specification does not impose ordering restrictions on ancillary chunks
+ aAPNGInfo->maFrameData.insert(aAPNGInfo->maFrameData.begin()
+ + pBaseChunk->sequence_number,
+ std::move(pBaseChunk));
+ }
+ else
+ {
+ aAPNGInfo->maFrameData.push_back(std::move(pBaseChunk));
+ }
+ }
+ return 1;
+}
+
+/// Gets the important chunks (IHDR, PLTE etc.) to a stream so that a stream can be constructed for each APNG frame
+void getImportantChunks(SvStream& rInStream, SvStream& rOutStream, sal_uInt32 nWidth,
+ sal_uInt32 nHeight)
+{
+ sal_uInt64 nPos = rInStream.Tell();
+ sal_uInt32 nChunkSize, nChunkType;
+ rInStream.SetEndian(SvStreamEndian::BIG);
+ rOutStream.SetEndian(SvStreamEndian::BIG);
+ rOutStream.WriteUInt64(PNG_SIGNATURE);
+ rOutStream.WriteUInt32(PNG_IHDR_SIZE);
+ rOutStream.WriteUInt32(PNG_IHDR_SIGNATURE);
+ rOutStream.WriteUInt32(nWidth);
+ rOutStream.WriteUInt32(nHeight);
+ rInStream.Seek(rOutStream.Tell());
+ sal_uInt32 nIHDRData1;
+ sal_uInt8 nIHDRData2;
+ rInStream.ReadUInt32(nIHDRData1);
+ rInStream.ReadUChar(nIHDRData2);
+ rOutStream.WriteUInt32(nIHDRData1);
+ rOutStream.WriteUChar(nIHDRData2);
+ rOutStream.SeekRel(-PNG_IHDR_SIZE - PNG_CRC_SIZE);
+ std::vector<uint8_t> aIHDRData(PNG_IHDR_SIZE + PNG_CRC_SIZE);
+ rOutStream.ReadBytes(aIHDRData.data(), aIHDRData.size());
+ rOutStream.WriteUInt32(rtl_crc32(0, aIHDRData.data(), aIHDRData.size()));
+ rInStream.Seek(PNG_SIGNATURE_SIZE + PNG_TYPE_SIZE + PNG_SIZE_SIZE + PNG_IHDR_SIZE
+ + PNG_CRC_SIZE);
+ while (rInStream.good())
+ {
+ rInStream.ReadUInt32(nChunkSize);
+ rInStream.ReadUInt32(nChunkType);
+ bool bBreakOuter = false;
+ switch (nChunkType)
+ {
+ case PNG_ACTL_SIGNATURE:
+ case PNG_FCTL_SIGNATURE:
+ case PNG_FDAT_SIGNATURE:
+ {
+ // skip apng chunks
+ rInStream.SeekRel(nChunkSize + PNG_CRC_SIZE);
+ continue;
+ }
+ case PNG_IDAT_SIGNATURE:
+ {
+ // IDAT chunk hit, no more important png chunks
+ bBreakOuter = true;
+ break;
+ }
+ default:
+ {
+ // Seek back to start of chunk
+ rInStream.SeekRel(-PNG_TYPE_SIZE - PNG_SIZE_SIZE);
+ // Copy chunk to rOutStream
+ std::vector<uint8_t> aData(nChunkSize + PNG_TYPE_SIZE + PNG_SIZE_SIZE);
+ rInStream.ReadBytes(aData.data(),
+ PNG_TYPE_SIZE + PNG_SIZE_SIZE + nChunkSize + PNG_CRC_SIZE);
+ rOutStream.WriteBytes(aData.data(),
+ PNG_TYPE_SIZE + PNG_SIZE_SIZE + nChunkSize + PNG_CRC_SIZE);
+ break;
+ }
+ }
+ if (bBreakOuter)
+ {
+ break;
+ }
+ }
+ rInStream.Seek(nPos);
+}
+
+sal_uInt32 NumDenToTime(sal_uInt16 nNumerator, sal_uInt16 nDenominator)
+{
+ if (nDenominator == 0)
+ nDenominator = 100;
+ return (static_cast<double>(nNumerator) / nDenominator) * 100;
+}
+
+bool fcTLbeforeIDAT(SvStream& rStream)
+{
+ sal_uInt64 nPos = rStream.Tell();
+ comphelper::ScopeGuard aGuard([&rStream, nPos]() { rStream.Seek(nPos); });
+ // Skip PNG header and IHDR
+ rStream.SetEndian(SvStreamEndian::BIG);
+ rStream.Seek(PNG_SIGNATURE_SIZE + PNG_TYPE_SIZE + PNG_SIZE_SIZE + PNG_IHDR_SIZE + PNG_CRC_SIZE);
+ sal_uInt32 nChunkSize, nChunkType;
+ while (rStream.good())
+ {
+ rStream.ReadUInt32(nChunkSize);
+ rStream.ReadUInt32(nChunkType);
+ switch (nChunkType)
+ {
+ case PNG_FCTL_SIGNATURE:
+ return true;
+ case PNG_IDAT_SIGNATURE:
+ return false;
+ default:
+ {
+ rStream.SeekRel(nChunkSize + PNG_CRC_SIZE);
+ break;
+ }
+ }
+ }
+ return false;
+}
+
#if defined __GNUC__ && __GNUC__ == 8 && !defined __clang__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wclobbered"
#endif
-bool reader(SvStream& rStream, BitmapEx& rBitmapEx,
+bool reader(SvStream& rStream, Graphic& rGraphic,
GraphicFilterImportFlags nImportFlags = GraphicFilterImportFlags::NONE,
BitmapScopedWriteAccess* pAccess = nullptr,
AlphaScopedWriteAccess* pAlphaAccess = nullptr)
@@ -83,6 +332,9 @@ bool reader(SvStream& rStream, BitmapEx& rBitmapEx,
if (!pPng)
return false;
+ APNGInfo aAPNGInfo;
+ png_set_read_user_chunk_fn(pPng, &aAPNGInfo, &handle_unknown_chunk);
+
png_infop pInfo = png_create_info_struct(pPng);
if (!pInfo)
{
@@ -95,6 +347,7 @@ bool reader(SvStream& rStream, BitmapEx& rBitmapEx,
// All variables holding resources need to be declared here in order to be
// properly cleaned up in case of an error, otherwise libpng's longjmp()
// jumps over the destructor calls.
+ BitmapEx aBitmapEx;
Bitmap aBitmap;
AlphaMask aBitmapAlpha;
Size prefSize;
@@ -116,14 +369,15 @@ bool reader(SvStream& rStream, BitmapEx& rBitmapEx,
pWriteAccessInstance.reset();
pWriteAccessAlphaInstance.reset();
if (!aBitmap.IsEmpty() && !aBitmapAlpha.IsEmpty())
- rBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
+ aBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
else if (!aBitmap.IsEmpty())
- rBitmapEx = BitmapEx(aBitmap);
- if (!rBitmapEx.IsEmpty() && !prefSize.IsEmpty())
+ aBitmapEx = BitmapEx(aBitmap);
+ if (!aBitmapEx.IsEmpty() && !prefSize.IsEmpty())
{
- rBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
- rBitmapEx.SetPrefSize(prefSize);
+ aBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
+ aBitmapEx.SetPrefSize(prefSize);
}
+ rGraphic = aBitmapEx;
}
return false;
}
@@ -232,14 +486,15 @@ bool reader(SvStream& rStream, BitmapEx& rBitmapEx,
if (bOnlyCreateBitmap)
{
if (!aBitmapAlpha.IsEmpty())
- rBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
+ aBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
else
- rBitmapEx = BitmapEx(aBitmap);
+ aBitmapEx = BitmapEx(aBitmap);
if (!prefSize.IsEmpty())
{
- rBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
- rBitmapEx.SetPrefSize(prefSize);
+ aBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
+ aBitmapEx.SetPrefSize(prefSize);
}
+ rGraphic = aBitmapEx;
return true;
}
@@ -408,16 +663,92 @@ bool reader(SvStream& rStream, BitmapEx& rBitmapEx,
pWriteAccess.reset();
pWriteAccessAlpha.reset();
if (!aBitmapAlpha.IsEmpty())
- rBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
+ aBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
else
- rBitmapEx = BitmapEx(aBitmap);
+ aBitmapEx = BitmapEx(aBitmap);
if (!prefSize.IsEmpty())
{
- rBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
- rBitmapEx.SetPrefSize(prefSize);
+ aBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
+ aBitmapEx.SetPrefSize(prefSize);
}
}
+ if (aAPNGInfo.mbIsApng)
+ {
+ Animation aAnimation;
+ // We create new pngs for each frame and use the PngImageReader to create
+ // the BitmapExs for each frame
+ bool bFctlBeforeIDAT = fcTLbeforeIDAT(rStream);
+ size_t nSequenceIndex = static_cast<size_t>(bFctlBeforeIDAT);
+ sal_uInt32 nFrames
+ = aAPNGInfo.maACTLChunk.num_frames - static_cast<sal_uInt32>(bFctlBeforeIDAT);
+ {
+ fcTLChunk* aFctlChunk = dynamic_cast<fcTLChunk*>(aAPNGInfo.maFrameData[0].get());
+ if (!aFctlChunk)
+ return false;
+ Size aCanvasSize(aFctlChunk->width, aFctlChunk->height);
+ aAnimation.SetDisplaySizePixel(aCanvasSize);
+ aAnimation.SetLoopCount(aAPNGInfo.maACTLChunk.num_plays);
+ if (bFctlBeforeIDAT)
+ {
+ Point aFirstPoint(0, 0);
+ auto aDisposal = static_cast<Disposal>(aFctlChunk->dispose_op);
+ if (aDisposal == Disposal::Previous)
+ aDisposal = Disposal::Back;
+ AnimationFrame aAnimationFrame(
+ aBitmapEx, aFirstPoint, aCanvasSize,
+ NumDenToTime(aFctlChunk->delay_num, aFctlChunk->delay_den), aDisposal);
+ aAnimation.Insert(aAnimationFrame);
+ }
+ }
+ for (sal_uInt32 i = 0; i < nFrames; i++)
+ {
+ // Guaranteed to be fcTL chunk here because it was checked earlier
+ fcTLChunk* aFctlChunk
+ = static_cast<fcTLChunk*>(aAPNGInfo.maFrameData[nSequenceIndex++].get());
+ Disposal aDisposal = static_cast<Disposal>(aFctlChunk->dispose_op);
+ if (i == 0 && aDisposal == Disposal::Back)
+ aDisposal = Disposal::Previous;
+ SvMemoryStream aFrameStream;
+ getImportantChunks(rStream, aFrameStream, aFctlChunk->width, aFctlChunk->height);
+ // A single frame can have multiple fdAT chunks
+ while (fdATChunk* pFdatChunk
+ = dynamic_cast<fdATChunk*>(aAPNGInfo.maFrameData[nSequenceIndex].get()))
+ {
+ // Write fdAT chunks as IDAT chunks
+ auto nDataSize = pFdatChunk->frame_data.size();
+ aFrameStream.WriteUInt32(nDataSize - PNG_TYPE_SIZE);
+ aFrameStream.WriteBytes(pFdatChunk->frame_data.data(), nDataSize);
+ sal_uInt32 nCrc = rtl_crc32(0, pFdatChunk->frame_data.data(), nDataSize);
+ aFrameStream.WriteUInt32(nCrc);
+ nSequenceIndex++;
+ if (nSequenceIndex >= aAPNGInfo.maFrameData.size())
+ break;
+ }
+ aFrameStream.WriteUInt32(PNG_IEND_SIZE);
+ aFrameStream.WriteUInt32(PNG_IEND_SIGNATURE);
+ aFrameStream.WriteUInt32(PNG_IEND_CRC);
+ Graphic aFrameGraphic;
+ aFrameStream.Seek(0);
+ bool bSuccess = reader(aFrameStream, aFrameGraphic);
+ if (!bSuccess)
+ return false;
+ BitmapEx aFrameBitmapEx = aFrameGraphic.GetBitmapEx();
+ Point aStartPoint(aFctlChunk->x_offset, aFctlChunk->y_offset);
+ Size aSize(aFctlChunk->width, aFctlChunk->height);
+ AnimationFrame aAnimationFrame(
+ aFrameBitmapEx, aStartPoint, aSize,
+ NumDenToTime(aFctlChunk->delay_num, aFctlChunk->delay_den), aDisposal);
+ aAnimation.Insert(aAnimationFrame);
+ }
+ rGraphic = aAnimation;
+ return true;
+ }
+ else
+ {
+ rGraphic = aBitmapEx;
+ }
+
return true;
}
@@ -470,8 +801,7 @@ BinaryDataContainer getMsGifChunk(SvStream& rStream)
return {};
rStream.SeekRel(length);
rStream.ReadUInt32(crc);
- constexpr sal_uInt32 PNGCHUNK_IEND = 0x49454e44;
- if (type == PNGCHUNK_IEND)
+ if (type == PNG_IEND_SIGNATURE)
return {};
}
}
@@ -488,13 +818,21 @@ PngImageReader::PngImageReader(SvStream& rStream)
{
}
-bool PngImageReader::read(BitmapEx& rBitmapEx) { return reader(mrStream, rBitmapEx); }
+bool PngImageReader::read(BitmapEx& rBitmapEx)
+{
+ Graphic aGraphic;
+ bool bRet = reader(mrStream, aGraphic);
+ rBitmapEx = aGraphic.GetBitmapEx();
+ return bRet;
+}
+
+bool PngImageReader::read(Graphic& rGraphic) { return reader(mrStream, rGraphic); }
BitmapEx PngImageReader::read()
{
- BitmapEx bitmap;
- read(bitmap);
- return bitmap;
+ Graphic aGraphic;
+ read(aGraphic);
+ return aGraphic.GetBitmapEx();
}
BinaryDataContainer PngImageReader::getMicrosoftGifChunk(SvStream& rStream)
@@ -512,16 +850,56 @@ bool ImportPNG(SvStream& rInputStream, Graphic& rGraphic, GraphicFilterImportFla
BitmapScopedWriteAccess* pAccess, AlphaScopedWriteAccess* pAlphaAccess)
{
// Creating empty bitmaps should be practically a no-op, and thus thread-safe.
- BitmapEx bitmap;
- if (reader(rInputStream, bitmap, nImportFlags, pAccess, pAlphaAccess))
+ Graphic aGraphic;
+ if (reader(rInputStream, aGraphic, nImportFlags, pAccess, pAlphaAccess))
{
if (!(nImportFlags & GraphicFilterImportFlags::UseExistingBitmap))
- rGraphic = bitmap;
+ rGraphic = aGraphic;
return true;
}
return false;
}
+bool PngImageReader::isAPng(SvStream& rStream)
+{
+ auto nStmPos = rStream.Tell();
+ comphelper::ScopeGuard aGuard([&rStream, &nStmPos] {
+ rStream.Seek(nStmPos);
+ rStream.SetEndian(SvStreamEndian::LITTLE);
+ });
+ if (!isPng(rStream))
+ return false;
+ rStream.SetEndian(SvStreamEndian::BIG);
+ sal_uInt32 nChunkSize, nChunkType;
+ rStream.ReadUInt32(nChunkSize);
+ rStream.ReadUInt32(nChunkType);
+ if (nChunkType != PNG_IHDR_SIGNATURE)
+ return false;
+ rStream.SeekRel(nChunkSize);
+ // Skip IHDR CRC
+ rStream.SeekRel(PNG_CRC_SIZE);
+ // Look for acTL chunk that exists before the first IDAT chunk
+ while (true)
+ {
+ rStream.ReadUInt32(nChunkSize);
+ if (!rStream.good())
+ return false;
+ rStream.ReadUInt32(nChunkType);
+ if (!rStream.good())
+ return false;
+ // Check if it's an IDAT chunk -> regular PNG
+ if (nChunkType == PNG_IDAT_SIGNATURE)
+ return false;
+ else if (nChunkType == PNG_ACTL_SIGNATURE)
+ return true;
+ else
+ {
+ rStream.SeekRel(nChunkSize);
+ rStream.SeekRel(PNG_CRC_SIZE);
+ }
+ }
+}
+
} // namespace vcl
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */