summaryrefslogtreecommitdiffstats
path: root/vcl/source/filter/png
diff options
context:
space:
mode:
Diffstat (limited to 'vcl/source/filter/png')
-rw-r--r--vcl/source/filter/png/PngImageReader.cxx532
-rw-r--r--vcl/source/filter/png/png.hxx33
-rw-r--r--vcl/source/filter/png/pngwrite.cxx660
3 files changed, 1225 insertions, 0 deletions
diff --git a/vcl/source/filter/png/PngImageReader.cxx b/vcl/source/filter/png/PngImageReader.cxx
new file mode 100644
index 000000000..6a3053a72
--- /dev/null
+++ b/vcl/source/filter/png/PngImageReader.cxx
@@ -0,0 +1,532 @@
+/* -*- 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/PngImageReader.hxx>
+#include <png.h>
+#include <rtl/crc.h>
+#include <tools/stream.hxx>
+#include <vcl/bitmap.hxx>
+#include <vcl/alpha.hxx>
+#include <vcl/BitmapTools.hxx>
+#include <unotools/configmgr.hxx>
+
+#include <bitmap/BitmapWriteAccess.hxx>
+#include <svdata.hxx>
+#include <salinst.hxx>
+
+#include "png.hxx"
+
+namespace
+{
+void lclReadStream(png_structp pPng, png_bytep pOutBytes, png_size_t nBytesToRead)
+{
+ png_voidp pIO = png_get_io_ptr(pPng);
+
+ if (pIO == nullptr)
+ return;
+
+ SvStream* pStream = static_cast<SvStream*>(pIO);
+
+ sal_Size nBytesRead = pStream->ReadBytes(pOutBytes, nBytesToRead);
+
+ if (nBytesRead != nBytesToRead)
+ {
+ if (!nBytesRead)
+ png_error(pPng, "Error reading");
+ else
+ {
+ // Make sure to not reuse old data (could cause infinite loop).
+ memset(pOutBytes + nBytesRead, 0, nBytesToRead - nBytesRead);
+ png_warning(pPng, "Short read");
+ }
+ }
+}
+
+constexpr int PNG_SIGNATURE_SIZE = 8;
+
+bool isPng(SvStream& rStream)
+{
+ // Check signature bytes
+ sal_uInt8 aHeader[PNG_SIGNATURE_SIZE];
+ if (rStream.ReadBytes(aHeader, PNG_SIGNATURE_SIZE) != PNG_SIGNATURE_SIZE)
+ return false;
+ return png_sig_cmp(aHeader, 0, PNG_SIGNATURE_SIZE) == 0;
+}
+
+struct PngDestructor
+{
+ ~PngDestructor() { png_destroy_read_struct(&pPng, &pInfo, nullptr); }
+ png_structp pPng;
+ png_infop pInfo;
+};
+
+#if defined __GNUC__ && __GNUC__ == 8 && !defined __clang__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wclobbered"
+#endif
+bool reader(SvStream& rStream, BitmapEx& rBitmapEx,
+ GraphicFilterImportFlags nImportFlags = GraphicFilterImportFlags::NONE,
+ BitmapScopedWriteAccess* pAccess = nullptr,
+ AlphaScopedWriteAccess* pAlphaAccess = nullptr)
+{
+ if (!isPng(rStream))
+ return false;
+
+ png_structp pPng = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ if (!pPng)
+ return false;
+
+ png_infop pInfo = png_create_info_struct(pPng);
+ if (!pInfo)
+ {
+ png_destroy_read_struct(&pPng, nullptr, nullptr);
+ return false;
+ }
+
+ PngDestructor pngDestructor = { pPng, pInfo };
+
+ // 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.
+ Bitmap aBitmap;
+ AlphaMask aBitmapAlpha;
+ Size prefSize;
+ BitmapScopedWriteAccess pWriteAccessInstance;
+ AlphaScopedWriteAccess pWriteAccessAlphaInstance;
+ const bool bFuzzing = utl::ConfigManager::IsFuzzing();
+ const bool bSupportsBitmap32 = bFuzzing || ImplGetSVData()->mpDefInst->supportsBitmap32();
+ const bool bOnlyCreateBitmap
+ = static_cast<bool>(nImportFlags & GraphicFilterImportFlags::OnlyCreateBitmap);
+ const bool bUseExistingBitmap
+ = static_cast<bool>(nImportFlags & GraphicFilterImportFlags::UseExistingBitmap);
+
+ if (setjmp(png_jmpbuf(pPng)))
+ {
+ if (!bUseExistingBitmap)
+ {
+ // Set the bitmap if it contains something, even on failure. This allows
+ // reading images that are only partially broken.
+ pWriteAccessInstance.reset();
+ pWriteAccessAlphaInstance.reset();
+ if (!aBitmap.IsEmpty() && !aBitmapAlpha.IsEmpty())
+ rBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
+ else if (!aBitmap.IsEmpty())
+ rBitmapEx = BitmapEx(aBitmap);
+ if (!rBitmapEx.IsEmpty() && !prefSize.IsEmpty())
+ {
+ rBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
+ rBitmapEx.SetPrefSize(prefSize);
+ }
+ }
+ return false;
+ }
+
+ png_set_option(pPng, PNG_MAXIMUM_INFLATE_WINDOW, PNG_OPTION_ON);
+
+ png_set_read_fn(pPng, &rStream, lclReadStream);
+
+ if (!bFuzzing)
+ png_set_crc_action(pPng, PNG_CRC_ERROR_QUIT, PNG_CRC_WARN_DISCARD);
+ else
+ png_set_crc_action(pPng, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE);
+
+ png_set_sig_bytes(pPng, PNG_SIGNATURE_SIZE);
+
+ png_read_info(pPng, pInfo);
+
+ png_uint_32 width = 0;
+ png_uint_32 height = 0;
+ int bitDepth = 0;
+ int colorType = -1;
+ int interlace = -1;
+
+ png_uint_32 returnValue = png_get_IHDR(pPng, pInfo, &width, &height, &bitDepth, &colorType,
+ &interlace, nullptr, nullptr);
+
+ if (returnValue != 1)
+ return false;
+
+ if (colorType == PNG_COLOR_TYPE_PALETTE)
+ png_set_palette_to_rgb(pPng);
+
+ if (colorType == PNG_COLOR_TYPE_GRAY)
+ png_set_expand_gray_1_2_4_to_8(pPng);
+
+ if (png_get_valid(pPng, pInfo, PNG_INFO_tRNS))
+ png_set_tRNS_to_alpha(pPng);
+
+ if (bitDepth == 16)
+ png_set_scale_16(pPng);
+
+ if (bitDepth < 8)
+ png_set_packing(pPng);
+
+ // Convert gray+alpha to RGBA, keep gray as gray.
+ if (colorType == PNG_COLOR_TYPE_GRAY_ALPHA
+ || (colorType == PNG_COLOR_TYPE_GRAY && png_get_valid(pPng, pInfo, PNG_INFO_tRNS)))
+ {
+ png_set_gray_to_rgb(pPng);
+ }
+
+ // Sets the filler byte - if RGB it converts to RGBA
+ // png_set_filler(pPng, 0xFF, PNG_FILLER_AFTER);
+
+ int nNumberOfPasses = png_set_interlace_handling(pPng);
+
+ png_read_update_info(pPng, pInfo);
+ returnValue = png_get_IHDR(pPng, pInfo, &width, &height, &bitDepth, &colorType, nullptr,
+ nullptr, nullptr);
+
+ if (returnValue != 1)
+ return false;
+
+ if (bitDepth != 8
+ || (colorType != PNG_COLOR_TYPE_RGB && colorType != PNG_COLOR_TYPE_RGB_ALPHA
+ && colorType != PNG_COLOR_TYPE_GRAY))
+ {
+ return false;
+ }
+
+ png_uint_32 res_x = 0;
+ png_uint_32 res_y = 0;
+ int unit_type = 0;
+ if (png_get_pHYs(pPng, pInfo, &res_x, &res_y, &unit_type) != 0
+ && unit_type == PNG_RESOLUTION_METER && res_x && res_y)
+ {
+ // convert into MapUnit::Map100thMM
+ prefSize = Size(static_cast<sal_Int32>((100000.0 * width) / res_x),
+ static_cast<sal_Int32>((100000.0 * height) / res_y));
+ }
+
+ if (!bUseExistingBitmap)
+ {
+ switch (colorType)
+ {
+ case PNG_COLOR_TYPE_RGB:
+ aBitmap = Bitmap(Size(width, height), vcl::PixelFormat::N24_BPP);
+ break;
+ case PNG_COLOR_TYPE_RGBA:
+ if (bSupportsBitmap32)
+ aBitmap = Bitmap(Size(width, height), vcl::PixelFormat::N32_BPP);
+ else
+ {
+ aBitmap = Bitmap(Size(width, height), vcl::PixelFormat::N24_BPP);
+ aBitmapAlpha = AlphaMask(Size(width, height), nullptr);
+ }
+ break;
+ case PNG_COLOR_TYPE_GRAY:
+ aBitmap = Bitmap(Size(width, height), vcl::PixelFormat::N8_BPP,
+ &Bitmap::GetGreyPalette(256));
+ break;
+ default:
+ abort();
+ }
+
+ if (bOnlyCreateBitmap)
+ {
+ if (!aBitmapAlpha.IsEmpty())
+ rBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
+ else
+ rBitmapEx = BitmapEx(aBitmap);
+ if (!prefSize.IsEmpty())
+ {
+ rBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
+ rBitmapEx.SetPrefSize(prefSize);
+ }
+ return true;
+ }
+
+ pWriteAccessInstance = BitmapScopedWriteAccess(aBitmap);
+ if (!pWriteAccessInstance)
+ return false;
+ if (!aBitmapAlpha.IsEmpty())
+ {
+ pWriteAccessAlphaInstance = AlphaScopedWriteAccess(aBitmapAlpha);
+ if (!pWriteAccessAlphaInstance)
+ return false;
+ }
+ }
+ BitmapScopedWriteAccess& pWriteAccess = pAccess ? *pAccess : pWriteAccessInstance;
+ AlphaScopedWriteAccess& pWriteAccessAlpha
+ = pAlphaAccess ? *pAlphaAccess : pWriteAccessAlphaInstance;
+
+ if (colorType == PNG_COLOR_TYPE_RGB)
+ {
+ ScanlineFormat eFormat = pWriteAccess->GetScanlineFormat();
+ if (eFormat == ScanlineFormat::N24BitTcBgr)
+ png_set_bgr(pPng);
+
+ for (int pass = 0; pass < nNumberOfPasses; pass++)
+ {
+ for (png_uint_32 y = 0; y < height; y++)
+ {
+ Scanline pScanline = pWriteAccess->GetScanline(y);
+ png_read_row(pPng, pScanline, nullptr);
+ }
+ }
+ }
+ else if (colorType == PNG_COLOR_TYPE_RGB_ALPHA)
+ {
+ size_t aRowSizeBytes = png_get_rowbytes(pPng, pInfo);
+
+ if (bSupportsBitmap32)
+ {
+ ScanlineFormat eFormat = pWriteAccess->GetScanlineFormat();
+ if (eFormat == ScanlineFormat::N32BitTcAbgr || eFormat == ScanlineFormat::N32BitTcBgra)
+ png_set_bgr(pPng);
+
+ for (int pass = 0; pass < nNumberOfPasses; pass++)
+ {
+ for (png_uint_32 y = 0; y < height; y++)
+ {
+ Scanline pScanline = pWriteAccess->GetScanline(y);
+ png_read_row(pPng, pScanline, nullptr);
+ }
+ }
+#if !ENABLE_WASM_STRIP_PREMULTIPLY
+ const vcl::bitmap::lookup_table& premultiply = vcl::bitmap::get_premultiply_table();
+#endif
+ if (eFormat == ScanlineFormat::N32BitTcAbgr || eFormat == ScanlineFormat::N32BitTcArgb)
+ { // alpha first and premultiply
+ for (png_uint_32 y = 0; y < height; y++)
+ {
+ Scanline pScanline = pWriteAccess->GetScanline(y);
+ for (size_t i = 0; i < aRowSizeBytes; i += 4)
+ {
+ const sal_uInt8 alpha = pScanline[i + 3];
+#if ENABLE_WASM_STRIP_PREMULTIPLY
+ pScanline[i + 3] = vcl::bitmap::premultiply(alpha, pScanline[i + 2]);
+ pScanline[i + 2] = vcl::bitmap::premultiply(alpha, pScanline[i + 1]);
+ pScanline[i + 1] = vcl::bitmap::premultiply(alpha, pScanline[i]);
+#else
+ pScanline[i + 3] = premultiply[alpha][pScanline[i + 2]];
+ pScanline[i + 2] = premultiply[alpha][pScanline[i + 1]];
+ pScanline[i + 1] = premultiply[alpha][pScanline[i]];
+#endif
+ pScanline[i] = alpha;
+ }
+ }
+ }
+ else
+ { // keep alpha last, only premultiply
+ for (png_uint_32 y = 0; y < height; y++)
+ {
+ Scanline pScanline = pWriteAccess->GetScanline(y);
+ for (size_t i = 0; i < aRowSizeBytes; i += 4)
+ {
+ const sal_uInt8 alpha = pScanline[i + 3];
+#if ENABLE_WASM_STRIP_PREMULTIPLY
+ pScanline[i] = vcl::bitmap::premultiply(alpha, pScanline[i]);
+ pScanline[i + 1] = vcl::bitmap::premultiply(alpha, pScanline[i + 1]);
+ pScanline[i + 2] = vcl::bitmap::premultiply(alpha, pScanline[i + 2]);
+#else
+ pScanline[i] = premultiply[alpha][pScanline[i]];
+ pScanline[i + 1] = premultiply[alpha][pScanline[i + 1]];
+ pScanline[i + 2] = premultiply[alpha][pScanline[i + 2]];
+#endif
+ }
+ }
+ }
+ }
+ else
+ {
+ ScanlineFormat eFormat = pWriteAccess->GetScanlineFormat();
+ if (eFormat == ScanlineFormat::N24BitTcBgr)
+ png_set_bgr(pPng);
+
+ if (nNumberOfPasses == 1)
+ {
+ // optimise the common case, where we can use a buffer of only a single row
+ std::vector<png_byte> aRow(aRowSizeBytes, 0);
+ for (png_uint_32 y = 0; y < height; y++)
+ {
+ Scanline pScanline = pWriteAccess->GetScanline(y);
+ Scanline pScanAlpha = pWriteAccessAlpha->GetScanline(y);
+ png_bytep pRow = aRow.data();
+ png_read_row(pPng, pRow, nullptr);
+ size_t iAlpha = 0;
+ size_t iColor = 0;
+ for (size_t i = 0; i < aRowSizeBytes; i += 4)
+ {
+ pScanline[iColor++] = pRow[i + 0];
+ pScanline[iColor++] = pRow[i + 1];
+ pScanline[iColor++] = pRow[i + 2];
+ pScanAlpha[iAlpha++] = 0xFF - pRow[i + 3];
+ }
+ }
+ }
+ else
+ {
+ std::vector<std::vector<png_byte>> aRows(height);
+ for (auto& rRow : aRows)
+ rRow.resize(aRowSizeBytes, 0);
+ for (int pass = 0; pass < nNumberOfPasses; pass++)
+ {
+ for (png_uint_32 y = 0; y < height; y++)
+ {
+ Scanline pScanline = pWriteAccess->GetScanline(y);
+ Scanline pScanAlpha = pWriteAccessAlpha->GetScanline(y);
+ png_bytep pRow = aRows[y].data();
+ png_read_row(pPng, pRow, nullptr);
+ size_t iAlpha = 0;
+ size_t iColor = 0;
+ for (size_t i = 0; i < aRowSizeBytes; i += 4)
+ {
+ pScanline[iColor++] = pRow[i + 0];
+ pScanline[iColor++] = pRow[i + 1];
+ pScanline[iColor++] = pRow[i + 2];
+ pScanAlpha[iAlpha++] = 0xFF - pRow[i + 3];
+ }
+ }
+ }
+ }
+ }
+ }
+ else if (colorType == PNG_COLOR_TYPE_GRAY)
+ {
+ for (int pass = 0; pass < nNumberOfPasses; pass++)
+ {
+ for (png_uint_32 y = 0; y < height; y++)
+ {
+ Scanline pScanline = pWriteAccess->GetScanline(y);
+ png_read_row(pPng, pScanline, nullptr);
+ }
+ }
+ }
+
+ png_read_end(pPng, pInfo);
+
+ if (!bUseExistingBitmap)
+ {
+ pWriteAccess.reset();
+ pWriteAccessAlpha.reset();
+ if (!aBitmapAlpha.IsEmpty())
+ rBitmapEx = BitmapEx(aBitmap, aBitmapAlpha);
+ else
+ rBitmapEx = BitmapEx(aBitmap);
+ if (!prefSize.IsEmpty())
+ {
+ rBitmapEx.SetPrefMapMode(MapMode(MapUnit::Map100thMM));
+ rBitmapEx.SetPrefSize(prefSize);
+ }
+ }
+
+ return true;
+}
+
+std::unique_ptr<sal_uInt8[]> getMsGifChunk(SvStream& rStream, sal_Int32* chunkSize)
+{
+ if (chunkSize)
+ *chunkSize = 0;
+ if (!isPng(rStream))
+ return nullptr;
+ // It's easier to read manually the contents and find the chunk than
+ // try to get it using libpng.
+ // https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_format
+ // Each chunk is: 4 bytes length, 4 bytes type, <length> bytes, 4 bytes crc
+ bool ignoreCrc = utl::ConfigManager::IsFuzzing();
+ for (;;)
+ {
+ sal_uInt32 length(0), type(0), crc(0);
+ rStream.ReadUInt32(length);
+ rStream.ReadUInt32(type);
+ if (!rStream.good())
+ return nullptr;
+ constexpr sal_uInt32 PNGCHUNK_msOG = 0x6d734f47; // Microsoft Office Animated GIF
+ constexpr sal_uInt64 MSGifHeaderSize = 11; // "MSOFFICE9.0"
+ if (type == PNGCHUNK_msOG && length > MSGifHeaderSize)
+ {
+ // calculate chunktype CRC (swap it back to original byte order)
+ sal_uInt32 typeForCrc = type;
+#if defined(__LITTLEENDIAN) || defined(OSL_LITENDIAN)
+ typeForCrc = OSL_SWAPDWORD(typeForCrc);
+#endif
+ sal_uInt32 computedCrc = rtl_crc32(0, &typeForCrc, 4);
+ const sal_uInt64 pos = rStream.Tell();
+ if (pos + length >= rStream.TellEnd())
+ return nullptr; // broken PNG
+
+ char msHeader[MSGifHeaderSize];
+ if (rStream.ReadBytes(msHeader, MSGifHeaderSize) != MSGifHeaderSize)
+ return nullptr;
+ computedCrc = rtl_crc32(computedCrc, msHeader, MSGifHeaderSize);
+ length -= MSGifHeaderSize;
+
+ std::unique_ptr<sal_uInt8[]> chunk(new sal_uInt8[length]);
+ if (rStream.ReadBytes(chunk.get(), length) != length)
+ return nullptr;
+ computedCrc = rtl_crc32(computedCrc, chunk.get(), length);
+ rStream.ReadUInt32(crc);
+ if (!ignoreCrc && crc != computedCrc)
+ continue; // invalid chunk, ignore
+ if (chunkSize)
+ *chunkSize = length;
+ return chunk;
+ }
+ if (rStream.remainingSize() < length)
+ return nullptr;
+ rStream.SeekRel(length);
+ rStream.ReadUInt32(crc);
+ constexpr sal_uInt32 PNGCHUNK_IEND = 0x49454e44;
+ if (type == PNGCHUNK_IEND)
+ return nullptr;
+ }
+}
+#if defined __GNUC__ && __GNUC__ == 8 && !defined __clang__
+#pragma GCC diagnostic pop
+#endif
+
+} // anonymous namespace
+
+namespace vcl
+{
+PngImageReader::PngImageReader(SvStream& rStream)
+ : mrStream(rStream)
+{
+}
+
+bool PngImageReader::read(BitmapEx& rBitmapEx) { return reader(mrStream, rBitmapEx); }
+
+BitmapEx PngImageReader::read()
+{
+ BitmapEx bitmap;
+ read(bitmap);
+ return bitmap;
+}
+
+std::unique_ptr<sal_uInt8[]> PngImageReader::getMicrosoftGifChunk(SvStream& rStream,
+ sal_Int32* chunkSize)
+{
+ sal_uInt64 originalPosition = rStream.Tell();
+ SvStreamEndian originalEndian = rStream.GetEndian();
+ rStream.SetEndian(SvStreamEndian::BIG);
+ std::unique_ptr<sal_uInt8[]> chunk = getMsGifChunk(rStream, chunkSize);
+ rStream.SetEndian(originalEndian);
+ rStream.Seek(originalPosition);
+ return chunk;
+}
+
+bool ImportPNG(SvStream& rInputStream, Graphic& rGraphic, GraphicFilterImportFlags nImportFlags,
+ 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))
+ {
+ if (!(nImportFlags & GraphicFilterImportFlags::UseExistingBitmap))
+ rGraphic = bitmap;
+ return true;
+ }
+ return false;
+}
+
+} // namespace vcl
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/source/filter/png/png.hxx b/vcl/source/filter/png/png.hxx
new file mode 100644
index 000000000..b3a1b7b17
--- /dev/null
+++ b/vcl/source/filter/png/png.hxx
@@ -0,0 +1,33 @@
+/* -*- 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/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#pragma once
+
+#include <vcl/graph.hxx>
+#include <vcl/graphicfilter.hxx>
+
+#include <bitmap/BitmapWriteAccess.hxx>
+
+namespace vcl
+{
+bool ImportPNG(SvStream& rInputStream, Graphic& rGraphic, GraphicFilterImportFlags nImportFlags,
+ BitmapScopedWriteAccess* pAccess, AlphaScopedWriteAccess* pAlphaAccess);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/source/filter/png/pngwrite.cxx b/vcl/source/filter/png/pngwrite.cxx
new file mode 100644
index 000000000..865fe38eb
--- /dev/null
+++ b/vcl/source/filter/png/pngwrite.cxx
@@ -0,0 +1,660 @@
+/* -*- 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/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <vcl/pngwrite.hxx>
+#include <vcl/bitmapex.hxx>
+
+#include <com/sun/star/beans/PropertyValue.hpp>
+#include <com/sun/star/uno/Sequence.hxx>
+
+#include <limits>
+#include <rtl/crc.h>
+#include <tools/solar.h>
+#include <tools/zcodec.hxx>
+#include <tools/stream.hxx>
+#include <vcl/BitmapReadAccess.hxx>
+#include <vcl/alpha.hxx>
+#include <osl/endian.h>
+#include <memory>
+#include <vcl/BitmapTools.hxx>
+
+#define PNG_DEF_COMPRESSION 6
+
+#define PNGCHUNK_IHDR 0x49484452
+#define PNGCHUNK_PLTE 0x504c5445
+#define PNGCHUNK_IDAT 0x49444154
+#define PNGCHUNK_IEND 0x49454e44
+#define PNGCHUNK_pHYs 0x70485973
+
+namespace vcl
+{
+class PNGWriterImpl
+{
+public:
+ PNGWriterImpl(const BitmapEx& BmpEx,
+ const css::uno::Sequence<css::beans::PropertyValue>* pFilterData);
+
+ bool Write(SvStream& rOutStream);
+
+ std::vector<vcl::PNGWriter::ChunkData>& GetChunks() { return maChunkSeq; }
+
+private:
+ std::vector<vcl::PNGWriter::ChunkData> maChunkSeq;
+
+ sal_Int32 mnCompLevel;
+ sal_Int32 mnInterlaced;
+ sal_uInt32 mnMaxChunkSize;
+ bool mbStatus;
+
+ Bitmap::ScopedReadAccess mpAccess;
+ BitmapReadAccess* mpMaskAccess;
+ ZCodec mpZCodec;
+
+ std::unique_ptr<sal_uInt8[]>
+ mpDeflateInBuf; // as big as the size of a scanline + alphachannel + 1
+ std::unique_ptr<sal_uInt8[]> mpPreviousScan; // as big as mpDeflateInBuf
+ std::unique_ptr<sal_uInt8[]> mpCurrentScan;
+ sal_uLong mnDeflateInSize;
+
+ sal_uLong mnWidth;
+ sal_uLong mnHeight;
+ sal_uInt8 mnBitsPerPixel;
+ sal_uInt8 mnFilterType; // 0 or 4;
+ sal_uLong mnBBP; // bytes per pixel ( needed for filtering )
+ bool mbTrueAlpha;
+
+ void ImplWritepHYs(const BitmapEx& rBitmapEx);
+ void ImplWriteIDAT();
+ sal_uLong ImplGetFilter(sal_uLong nY, sal_uLong nXStart = 0, sal_uLong nXAdd = 1);
+ void ImplClearFirstScanline();
+ bool ImplWriteHeader();
+ void ImplWritePalette();
+ void ImplOpenChunk(sal_uLong nChunkType);
+ void ImplWriteChunk(sal_uInt8 nNumb);
+ void ImplWriteChunk(sal_uInt32 nNumb);
+ void ImplWriteChunk(unsigned char const* pSource, sal_uInt32 nDatSize);
+};
+
+PNGWriterImpl::PNGWriterImpl(const BitmapEx& rBitmapEx,
+ const css::uno::Sequence<css::beans::PropertyValue>* pFilterData)
+ : mnCompLevel(PNG_DEF_COMPRESSION)
+ , mnInterlaced(0)
+ , mnMaxChunkSize(0)
+ , mbStatus(true)
+ , mpMaskAccess(nullptr)
+ , mnDeflateInSize(0)
+ , mnWidth(0)
+ , mnHeight(0)
+ , mnBitsPerPixel(0)
+ , mnFilterType(0)
+ , mnBBP(0)
+ , mbTrueAlpha(false)
+{
+ if (rBitmapEx.IsEmpty())
+ return;
+
+ BitmapEx aBitmapEx;
+
+ if (rBitmapEx.GetBitmap().getPixelFormat() == vcl::PixelFormat::N32_BPP)
+ {
+ if (!vcl::bitmap::convertBitmap32To24Plus8(rBitmapEx, aBitmapEx))
+ return;
+ }
+ else
+ {
+ aBitmapEx = rBitmapEx;
+ }
+
+ Bitmap aBmp(aBitmapEx.GetBitmap());
+
+ mnMaxChunkSize = std::numeric_limits<sal_uInt32>::max();
+ bool bTranslucent = true;
+
+ if (pFilterData)
+ {
+ for (const auto& rPropVal : *pFilterData)
+ {
+ if (rPropVal.Name == "Compression")
+ rPropVal.Value >>= mnCompLevel;
+ else if (rPropVal.Name == "Interlaced")
+ rPropVal.Value >>= mnInterlaced;
+ else if (rPropVal.Name == "Translucent")
+ {
+ tools::Long nTmp = 0;
+ rPropVal.Value >>= nTmp;
+ if (!nTmp)
+ bTranslucent = false;
+ }
+ else if (rPropVal.Name == "MaxChunkSize")
+ {
+ sal_Int32 nVal = 0;
+ if (rPropVal.Value >>= nVal)
+ mnMaxChunkSize = static_cast<sal_uInt32>(nVal);
+ }
+ }
+ }
+ mnBitsPerPixel = sal_uInt8(vcl::pixelFormatBitCount(aBmp.getPixelFormat()));
+
+ if (aBitmapEx.IsAlpha() && bTranslucent)
+ {
+ if (mnBitsPerPixel <= 8)
+ {
+ aBmp.Convert(BmpConversion::N24Bit);
+ mnBitsPerPixel = 24;
+ }
+
+ mpAccess = Bitmap::ScopedReadAccess(aBmp); // true RGB with alphachannel
+ if (mpAccess)
+ {
+ mbTrueAlpha = true;
+ AlphaMask aMask(aBitmapEx.GetAlpha());
+ mpMaskAccess = aMask.AcquireReadAccess();
+ if (mpMaskAccess)
+ {
+ if (ImplWriteHeader())
+ {
+ ImplWritepHYs(aBitmapEx);
+ ImplWriteIDAT();
+ }
+ aMask.ReleaseAccess(mpMaskAccess);
+ mpMaskAccess = nullptr;
+ }
+ else
+ {
+ mbStatus = false;
+ }
+ mpAccess.reset();
+ }
+ else
+ {
+ mbStatus = false;
+ }
+ }
+ else
+ {
+ mpAccess = Bitmap::ScopedReadAccess(aBmp); // palette + RGB without alphachannel
+ if (mpAccess)
+ {
+ if (ImplWriteHeader())
+ {
+ ImplWritepHYs(aBitmapEx);
+ if (mpAccess->HasPalette())
+ ImplWritePalette();
+
+ ImplWriteIDAT();
+ }
+ mpAccess.reset();
+ }
+ else
+ {
+ mbStatus = false;
+ }
+ }
+
+ if (mbStatus)
+ {
+ ImplOpenChunk(PNGCHUNK_IEND); // create an IEND chunk
+ }
+}
+
+bool PNGWriterImpl::Write(SvStream& rOStm)
+{
+ /* png signature is always an array of 8 bytes */
+ SvStreamEndian nOldMode = rOStm.GetEndian();
+ rOStm.SetEndian(SvStreamEndian::BIG);
+ rOStm.WriteUInt32(0x89504e47);
+ rOStm.WriteUInt32(0x0d0a1a0a);
+
+ for (auto const& chunk : maChunkSeq)
+ {
+ sal_uInt32 nType = chunk.nType;
+#if defined(__LITTLEENDIAN) || defined(OSL_LITENDIAN)
+ nType = OSL_SWAPDWORD(nType);
+#endif
+ sal_uInt32 nCRC = rtl_crc32(0, &nType, 4);
+ sal_uInt32 nDataSize = chunk.aData.size();
+ if (nDataSize)
+ nCRC = rtl_crc32(nCRC, chunk.aData.data(), nDataSize);
+ rOStm.WriteUInt32(nDataSize);
+ rOStm.WriteUInt32(chunk.nType);
+ if (nDataSize)
+ rOStm.WriteBytes(chunk.aData.data(), nDataSize);
+ rOStm.WriteUInt32(nCRC);
+ }
+ rOStm.SetEndian(nOldMode);
+ return mbStatus;
+}
+
+bool PNGWriterImpl::ImplWriteHeader()
+{
+ ImplOpenChunk(PNGCHUNK_IHDR);
+ mnWidth = mpAccess->Width();
+ ImplWriteChunk(sal_uInt32(mnWidth));
+ mnHeight = mpAccess->Height();
+ ImplWriteChunk(sal_uInt32(mnHeight));
+
+ if (mnWidth && mnHeight && mnBitsPerPixel && mbStatus)
+ {
+ sal_uInt8 nBitDepth = mnBitsPerPixel;
+ if (mnBitsPerPixel <= 8)
+ mnFilterType = 0;
+ else
+ mnFilterType = 4;
+
+ sal_uInt8 nColorType = 2; // colortype:
+
+ // bit 0 -> palette is used
+ if (mpAccess->HasPalette()) // bit 1 -> color is used
+ nColorType |= 1; // bit 2 -> alpha channel is used
+ else
+ nBitDepth /= 3;
+
+ if (mpMaskAccess)
+ nColorType |= 4;
+
+ ImplWriteChunk(nBitDepth);
+ ImplWriteChunk(nColorType); // colortype
+ ImplWriteChunk(static_cast<sal_uInt8>(0)); // compression type
+ ImplWriteChunk(static_cast<sal_uInt8>(0)); // filter type - is not supported in this version
+ ImplWriteChunk(static_cast<sal_uInt8>(mnInterlaced)); // interlace type
+ }
+ else
+ {
+ mbStatus = false;
+ }
+ return mbStatus;
+}
+
+void PNGWriterImpl::ImplWritePalette()
+{
+ const sal_uLong nCount = mpAccess->GetPaletteEntryCount();
+ std::unique_ptr<sal_uInt8[]> pTempBuf(new sal_uInt8[nCount * 3]);
+ sal_uInt8* pTmp = pTempBuf.get();
+
+ ImplOpenChunk(PNGCHUNK_PLTE);
+
+ for (sal_uLong i = 0; i < nCount; i++)
+ {
+ const BitmapColor& rColor = mpAccess->GetPaletteColor(i);
+ *pTmp++ = rColor.GetRed();
+ *pTmp++ = rColor.GetGreen();
+ *pTmp++ = rColor.GetBlue();
+ }
+ ImplWriteChunk(pTempBuf.get(), nCount * 3);
+}
+
+void PNGWriterImpl::ImplWritepHYs(const BitmapEx& rBmpEx)
+{
+ if (rBmpEx.GetPrefMapMode().GetMapUnit() != MapUnit::Map100thMM)
+ return;
+
+ Size aPrefSize(rBmpEx.GetPrefSize());
+
+ if (aPrefSize.Width() && aPrefSize.Height() && mnWidth && mnHeight)
+ {
+ ImplOpenChunk(PNGCHUNK_pHYs);
+ sal_uInt32 nPrefSizeX = static_cast<sal_uInt32>(
+ 100000.0 / (static_cast<double>(aPrefSize.Width()) / mnWidth) + 0.5);
+ sal_uInt32 nPrefSizeY = static_cast<sal_uInt32>(
+ 100000.0 / (static_cast<double>(aPrefSize.Height()) / mnHeight) + 0.5);
+ ImplWriteChunk(nPrefSizeX);
+ ImplWriteChunk(nPrefSizeY);
+ ImplWriteChunk(sal_uInt8(1)); // nMapUnit
+ }
+}
+
+void PNGWriterImpl::ImplWriteIDAT()
+{
+ mnDeflateInSize = mnBitsPerPixel;
+
+ if (mpMaskAccess)
+ mnDeflateInSize += 8;
+
+ mnBBP = (mnDeflateInSize + 7) >> 3;
+
+ mnDeflateInSize = mnBBP * mnWidth + 1;
+
+ mpDeflateInBuf.reset(new sal_uInt8[mnDeflateInSize]);
+
+ if (mnFilterType) // using filter type 4 we need memory for the scanline 3 times
+ {
+ mpPreviousScan.reset(new sal_uInt8[mnDeflateInSize]);
+ mpCurrentScan.reset(new sal_uInt8[mnDeflateInSize]);
+ ImplClearFirstScanline();
+ }
+ mpZCodec.BeginCompression(mnCompLevel);
+ SvMemoryStream aOStm;
+ if (mnInterlaced == 0)
+ {
+ for (sal_uLong nY = 0; nY < mnHeight; nY++)
+ {
+ mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY));
+ }
+ }
+ else
+ {
+ // interlace mode
+ sal_uLong nY;
+ for (nY = 0; nY < mnHeight; nY += 8) // pass 1
+ {
+ mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 0, 8));
+ }
+ ImplClearFirstScanline();
+
+ for (nY = 0; nY < mnHeight; nY += 8) // pass 2
+ {
+ mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 4, 8));
+ }
+ ImplClearFirstScanline();
+
+ if (mnHeight >= 5) // pass 3
+ {
+ for (nY = 4; nY < mnHeight; nY += 8)
+ {
+ mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 0, 4));
+ }
+ ImplClearFirstScanline();
+ }
+
+ for (nY = 0; nY < mnHeight; nY += 4) // pass 4
+ {
+ mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 2, 4));
+ }
+ ImplClearFirstScanline();
+
+ if (mnHeight >= 3) // pass 5
+ {
+ for (nY = 2; nY < mnHeight; nY += 4)
+ {
+ mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 0, 2));
+ }
+ ImplClearFirstScanline();
+ }
+
+ for (nY = 0; nY < mnHeight; nY += 2) // pass 6
+ {
+ mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 1, 2));
+ }
+ ImplClearFirstScanline();
+
+ if (mnHeight >= 2) // pass 7
+ {
+ for (nY = 1; nY < mnHeight; nY += 2)
+ {
+ mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY));
+ }
+ }
+ }
+ mpZCodec.EndCompression();
+
+ if (mnFilterType) // using filter type 4 we need memory for the scanline 3 times
+ {
+ mpCurrentScan.reset();
+ mpPreviousScan.reset();
+ }
+ mpDeflateInBuf.reset();
+
+ sal_uInt32 nIDATSize = aOStm.Tell();
+ sal_uInt32 nBytes, nBytesToWrite = nIDATSize;
+ while (nBytesToWrite)
+ {
+ nBytes = nBytesToWrite <= mnMaxChunkSize ? nBytesToWrite : mnMaxChunkSize;
+ ImplOpenChunk(PNGCHUNK_IDAT);
+ ImplWriteChunk(
+ const_cast<unsigned char*>(static_cast<unsigned char const*>(aOStm.GetData()))
+ + (nIDATSize - nBytesToWrite),
+ nBytes);
+ nBytesToWrite -= nBytes;
+ }
+}
+
+// ImplGetFilter writes the complete Scanline (nY) - in interlace mode the parameter nXStart and nXAdd
+// appends to the currently used pass
+// the complete size of scanline will be returned - in interlace mode zero is possible!
+
+sal_uLong PNGWriterImpl::ImplGetFilter(sal_uLong nY, sal_uLong nXStart, sal_uLong nXAdd)
+{
+ sal_uInt8* pDest;
+
+ if (mnFilterType)
+ pDest = mpCurrentScan.get();
+ else
+ pDest = mpDeflateInBuf.get();
+
+ if (nXStart < mnWidth)
+ {
+ *pDest++ = mnFilterType; // in this version the filter type is either 0 or 4
+
+ if (mpAccess
+ ->HasPalette()) // alphachannel is not allowed by pictures including palette entries
+ {
+ switch (mnBitsPerPixel)
+ {
+ case 1:
+ {
+ Scanline pScanline = mpAccess->GetScanline(nY);
+ sal_uLong nX, nXIndex;
+ for (nX = nXStart, nXIndex = 0; nX < mnWidth; nX += nXAdd, nXIndex++)
+ {
+ sal_uLong nShift = (nXIndex & 7) ^ 7;
+ if (nShift == 7)
+ *pDest = mpAccess->GetIndexFromData(pScanline, nX) << nShift;
+ else if (nShift == 0)
+ *pDest++ |= mpAccess->GetIndexFromData(pScanline, nX) << nShift;
+ else
+ *pDest |= mpAccess->GetIndexFromData(pScanline, nX) << nShift;
+ }
+ if ((nXIndex & 7) != 0)
+ pDest++; // byte is not completely used, so the bufferpointer is to correct
+ }
+ break;
+
+ case 4:
+ {
+ Scanline pScanline = mpAccess->GetScanline(nY);
+ sal_uLong nX, nXIndex;
+ for (nX = nXStart, nXIndex = 0; nX < mnWidth; nX += nXAdd, nXIndex++)
+ {
+ if (nXIndex & 1)
+ *pDest++ |= mpAccess->GetIndexFromData(pScanline, nX);
+ else
+ *pDest = mpAccess->GetIndexFromData(pScanline, nX) << 4;
+ }
+ if (nXIndex & 1)
+ pDest++;
+ }
+ break;
+
+ case 8:
+ {
+ Scanline pScanline = mpAccess->GetScanline(nY);
+ for (sal_uLong nX = nXStart; nX < mnWidth; nX += nXAdd)
+ {
+ *pDest++ = mpAccess->GetIndexFromData(pScanline, nX);
+ }
+ }
+ break;
+
+ default:
+ mbStatus = false;
+ break;
+ }
+ }
+ else
+ {
+ if (mpMaskAccess) // mpMaskAccess != NULL -> alphachannel is to create
+ {
+ if (mbTrueAlpha)
+ {
+ Scanline pScanline = mpAccess->GetScanline(nY);
+ Scanline pScanlineMask = mpMaskAccess->GetScanline(nY);
+ for (sal_uLong nX = nXStart; nX < mnWidth; nX += nXAdd)
+ {
+ const BitmapColor& rColor = mpAccess->GetPixelFromData(pScanline, nX);
+ *pDest++ = rColor.GetRed();
+ *pDest++ = rColor.GetGreen();
+ *pDest++ = rColor.GetBlue();
+ *pDest++ = 255 - mpMaskAccess->GetIndexFromData(pScanlineMask, nX);
+ }
+ }
+ else
+ {
+ const BitmapColor aTrans(mpMaskAccess->GetBestMatchingColor(COL_WHITE));
+ Scanline pScanline = mpAccess->GetScanline(nY);
+ Scanline pScanlineMask = mpMaskAccess->GetScanline(nY);
+
+ for (sal_uLong nX = nXStart; nX < mnWidth; nX += nXAdd)
+ {
+ const BitmapColor& rColor = mpAccess->GetPixelFromData(pScanline, nX);
+ *pDest++ = rColor.GetRed();
+ *pDest++ = rColor.GetGreen();
+ *pDest++ = rColor.GetBlue();
+
+ if (mpMaskAccess->GetPixelFromData(pScanlineMask, nX) == aTrans)
+ *pDest++ = 0;
+ else
+ *pDest++ = 0xff;
+ }
+ }
+ }
+ else
+ {
+ Scanline pScanline = mpAccess->GetScanline(nY);
+ for (sal_uLong nX = nXStart; nX < mnWidth; nX += nXAdd)
+ {
+ const BitmapColor& rColor = mpAccess->GetPixelFromData(pScanline, nX);
+ *pDest++ = rColor.GetRed();
+ *pDest++ = rColor.GetGreen();
+ *pDest++ = rColor.GetBlue();
+ }
+ }
+ }
+ }
+ // filter type4 ( PAETH ) will be used only for 24bit graphics
+ if (mnFilterType)
+ {
+ mnDeflateInSize = pDest - mpCurrentScan.get();
+ pDest = mpDeflateInBuf.get();
+ *pDest++ = 4; // filter type
+
+ sal_uInt8* p1 = mpCurrentScan.get() + 1; // Current Pixel
+ sal_uInt8* p2 = p1 - mnBBP; // left pixel
+ sal_uInt8* p3 = mpPreviousScan.get(); // upper pixel
+ sal_uInt8* p4 = p3 - mnBBP; // upperleft Pixel;
+
+ while (pDest < mpDeflateInBuf.get() + mnDeflateInSize)
+ {
+ sal_uLong nb = *p3++;
+ sal_uLong na, nc;
+ if (p2 >= mpCurrentScan.get() + 1)
+ {
+ na = *p2;
+ nc = *p4;
+ }
+ else
+ {
+ na = nc = 0;
+ }
+
+ tools::Long np = na + nb - nc;
+ tools::Long npa = np - na;
+ tools::Long npb = np - nb;
+ tools::Long npc = np - nc;
+
+ if (npa < 0)
+ npa = -npa;
+ if (npb < 0)
+ npb = -npb;
+ if (npc < 0)
+ npc = -npc;
+
+ if (npa <= npb && npa <= npc)
+ *pDest++ = *p1++ - static_cast<sal_uInt8>(na);
+ else if (npb <= npc)
+ *pDest++ = *p1++ - static_cast<sal_uInt8>(nb);
+ else
+ *pDest++ = *p1++ - static_cast<sal_uInt8>(nc);
+
+ p4++;
+ p2++;
+ }
+ for (tools::Long i = 0; i < static_cast<tools::Long>(mnDeflateInSize - 1); i++)
+ {
+ mpPreviousScan[i] = mpCurrentScan[i + 1];
+ }
+ }
+ else
+ {
+ mnDeflateInSize = pDest - mpDeflateInBuf.get();
+ }
+ return mnDeflateInSize;
+}
+
+void PNGWriterImpl::ImplClearFirstScanline()
+{
+ if (mnFilterType)
+ memset(mpPreviousScan.get(), 0, mnDeflateInSize);
+}
+
+void PNGWriterImpl::ImplOpenChunk(sal_uLong nChunkType)
+{
+ maChunkSeq.emplace_back();
+ maChunkSeq.back().nType = nChunkType;
+}
+
+void PNGWriterImpl::ImplWriteChunk(sal_uInt8 nSource)
+{
+ maChunkSeq.back().aData.push_back(nSource);
+}
+
+void PNGWriterImpl::ImplWriteChunk(sal_uInt32 nSource)
+{
+ vcl::PNGWriter::ChunkData& rChunkData = maChunkSeq.back();
+ rChunkData.aData.push_back(static_cast<sal_uInt8>(nSource >> 24));
+ rChunkData.aData.push_back(static_cast<sal_uInt8>(nSource >> 16));
+ rChunkData.aData.push_back(static_cast<sal_uInt8>(nSource >> 8));
+ rChunkData.aData.push_back(static_cast<sal_uInt8>(nSource));
+}
+
+void PNGWriterImpl::ImplWriteChunk(unsigned char const* pSource, sal_uInt32 nDatSize)
+{
+ if (nDatSize)
+ {
+ vcl::PNGWriter::ChunkData& rChunkData = maChunkSeq.back();
+ sal_uInt32 nSize = rChunkData.aData.size();
+ rChunkData.aData.resize(nSize + nDatSize);
+ memcpy(&rChunkData.aData[nSize], pSource, nDatSize);
+ }
+}
+
+PNGWriter::PNGWriter(const BitmapEx& rBmpEx,
+ const css::uno::Sequence<css::beans::PropertyValue>* pFilterData)
+ : mpImpl(new vcl::PNGWriterImpl(rBmpEx, pFilterData))
+{
+}
+
+PNGWriter::~PNGWriter() {}
+
+bool PNGWriter::Write(SvStream& rStream) { return mpImpl->Write(rStream); }
+
+std::vector<vcl::PNGWriter::ChunkData>& PNGWriter::GetChunks() { return mpImpl->GetChunks(); }
+
+} // namespace vcl
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */