diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /image/encoders/png | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'image/encoders/png')
-rw-r--r-- | image/encoders/png/moz.build | 17 | ||||
-rw-r--r-- | image/encoders/png/nsPNGEncoder.cpp | 820 | ||||
-rw-r--r-- | image/encoders/png/nsPNGEncoder.h | 79 |
3 files changed, 916 insertions, 0 deletions
diff --git a/image/encoders/png/moz.build b/image/encoders/png/moz.build new file mode 100644 index 0000000000..fe26a2368e --- /dev/null +++ b/image/encoders/png/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +SOURCES += [ + "nsPNGEncoder.cpp", +] + +LOCAL_INCLUDES += [ + "/image", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/png/nsPNGEncoder.cpp b/image/encoders/png/nsPNGEncoder.cpp new file mode 100644 index 0000000000..f21bab67a6 --- /dev/null +++ b/image/encoders/png/nsPNGEncoder.cpp @@ -0,0 +1,820 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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 "ImageLogging.h" +#include "nsCRT.h" +#include "nsPNGEncoder.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "prprf.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/UniquePtrExtensions.h" + +using namespace mozilla; + +static LazyLogModule sPNGEncoderLog("PNGEncoder"); + +NS_IMPL_ISUPPORTS(nsPNGEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +#define DEFAULT_ZLIB_LEVEL 3 +#define DEFAULT_FILTERS PNG_FILTER_SUB + +nsPNGEncoder::nsPNGEncoder() + : mPNG(nullptr), + mPNGinfo(nullptr), + mIsAnimation(false), + mFinished(false), + mImageBuffer(nullptr), + mImageBufferSize(0), + mImageBufferUsed(0), + mImageBufferReadPoint(0), + mCallback(nullptr), + mCallbackTarget(nullptr), + mNotifyThreshold(0), + mReentrantMonitor("nsPNGEncoder.mReentrantMonitor") {} + +nsPNGEncoder::~nsPNGEncoder() { + if (mImageBuffer) { + free(mImageBuffer); + mImageBuffer = nullptr; + } + // don't leak if EndImageEncode wasn't called + if (mPNG) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + } +} + +// nsPNGEncoder::InitFromData +// +// One output option is supported: "transparency=none" means that the +// output PNG will not have an alpha channel, even if the input does. +// +// Based partially on gfx/cairo/cairo/src/cairo-png.c +// See also media/libpng/libpng-manual.txt + +NS_IMETHODIMP +nsPNGEncoder::InitFromData(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + NS_ENSURE_ARG(aData); + nsresult rv; + + rv = StartImageEncode(aWidth, aHeight, aInputFormat, aOutputOptions); + if (!NS_SUCCEEDED(rv)) { + return rv; + } + + rv = AddImageFrame(aData, aLength, aWidth, aHeight, aStride, aInputFormat, + aOutputOptions); + if (!NS_SUCCEEDED(rv)) { + return rv; + } + + rv = EndImageEncode(); + + return rv; +} + +// nsPNGEncoder::StartImageEncode +// +// +// See ::InitFromData for other info. +NS_IMETHODIMP +nsPNGEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + bool useTransparency = true, skipFirstFrame = false; + uint32_t numFrames = 1; + uint32_t numPlays = 0; // For animations, 0 == forever + int zlibLevel = DEFAULT_ZLIB_LEVEL; + int filters = DEFAULT_FILTERS; + + // can't initialize more than once + if (mImageBuffer != nullptr) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) + return NS_ERROR_INVALID_ARG; + + // parse and check any provided output options + nsresult rv = ParseOptions(aOutputOptions, &useTransparency, &skipFirstFrame, + &numFrames, &numPlays, &zlibLevel, &filters, + nullptr, nullptr, nullptr, nullptr, nullptr); + if (rv != NS_OK) { + return rv; + } + +#ifdef PNG_APNG_SUPPORTED + if (numFrames > 1) { + mIsAnimation = true; + } + +#endif + + // initialize + mPNG = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, ErrorCallback, + WarningCallback); + if (!mPNG) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mPNGinfo = png_create_info_struct(mPNG); + if (!mPNGinfo) { + png_destroy_write_struct(&mPNG, nullptr); + return NS_ERROR_FAILURE; + } + + // libpng's error handler jumps back here upon an error. + // Note: It's important that all png_* callers do this, or errors + // will result in a corrupt time-warped stack. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; + } + +#ifdef PNG_WRITE_CUSTOMIZE_COMPRESSION_SUPPORTED + png_set_compression_level(mPNG, zlibLevel); +#endif +#ifdef PNG_WRITE_FILTER_SUPPORTED + png_set_filter(mPNG, PNG_FILTER_TYPE_BASE, filters); +#endif + + // Set up to read the data into our image buffer, start out with an 8K + // estimated size. Note: we don't have to worry about freeing this data + // in this function. It will be freed on object destruction. + mImageBufferSize = 8192; + mImageBuffer = (uint8_t*)malloc(mImageBufferSize); + if (!mImageBuffer) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_OUT_OF_MEMORY; + } + mImageBufferUsed = 0; + + // set our callback for libpng to give us the data + png_set_write_fn(mPNG, this, WriteCallback, nullptr); + + // include alpha? + int colorType; + if ((aInputFormat == INPUT_FORMAT_HOSTARGB || + aInputFormat == INPUT_FORMAT_RGBA) && + useTransparency) + colorType = PNG_COLOR_TYPE_RGB_ALPHA; + else + colorType = PNG_COLOR_TYPE_RGB; + + png_set_IHDR(mPNG, mPNGinfo, aWidth, aHeight, 8, colorType, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + +#ifdef PNG_APNG_SUPPORTED + if (mIsAnimation) { + png_set_first_frame_is_hidden(mPNG, mPNGinfo, skipFirstFrame); + png_set_acTL(mPNG, mPNGinfo, numFrames, numPlays); + } +#endif + + // XXX: support PLTE, gAMA, tRNS, bKGD? + + png_write_info(mPNG, mPNGinfo); + + return NS_OK; +} + +// Returns the number of bytes in the image buffer used. +NS_IMETHODIMP +nsPNGEncoder::GetImageBufferUsed(uint32_t* aOutputSize) { + NS_ENSURE_ARG_POINTER(aOutputSize); + *aOutputSize = mImageBufferUsed; + return NS_OK; +} + +// Returns a pointer to the start of the image buffer +NS_IMETHODIMP +nsPNGEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast<char*>(mImageBuffer); + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::AddImageFrame(const uint8_t* aData, + uint32_t aLength, // (unused, req'd by JS) + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aFrameOptions) { + bool useTransparency = true; + int filters = DEFAULT_FILTERS; + uint32_t delay_ms = 500; +#ifdef PNG_APNG_SUPPORTED + uint32_t dispose_op = PNG_DISPOSE_OP_NONE; + uint32_t blend_op = PNG_BLEND_OP_SOURCE; +#else + uint32_t dispose_op; + uint32_t blend_op; +#endif + uint32_t x_offset = 0, y_offset = 0; + + // must be initialized + if (mImageBuffer == nullptr) { + return NS_ERROR_NOT_INITIALIZED; + } + + // EndImageEncode was done, or some error occurred earlier + if (!mPNG) { + return NS_BASE_STREAM_CLOSED; + } + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) + return NS_ERROR_INVALID_ARG; + + // libpng's error handler jumps back here upon an error. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; + } + + // parse and check any provided output options + nsresult rv = ParseOptions(aFrameOptions, &useTransparency, nullptr, nullptr, + nullptr, nullptr, &filters, &dispose_op, &blend_op, + &delay_ms, &x_offset, &y_offset); + if (rv != NS_OK) { + return rv; + } + +#ifdef PNG_APNG_SUPPORTED + if (mIsAnimation) { + // XXX the row pointers arg (#3) is unused, can it be removed? + png_write_frame_head(mPNG, mPNGinfo, nullptr, aWidth, aHeight, x_offset, + y_offset, delay_ms, 1000, dispose_op, blend_op); + } +#endif + + // Stride is the padded width of each row, so it better be longer + // (I'm afraid people will not understand what stride means, so + // check it well) + if ((aInputFormat == INPUT_FORMAT_RGB && aStride < aWidth * 3) || + ((aInputFormat == INPUT_FORMAT_RGBA || + aInputFormat == INPUT_FORMAT_HOSTARGB) && + aStride < aWidth * 4)) { + NS_WARNING("Invalid stride for InitFromData/AddImageFrame"); + return NS_ERROR_INVALID_ARG; + } + +#ifdef PNG_WRITE_FILTER_SUPPORTED + png_set_filter(mPNG, PNG_FILTER_TYPE_BASE, filters); +#endif + + // write each row: if we add more input formats, we may want to + // generalize the conversions + if (aInputFormat == INPUT_FORMAT_HOSTARGB) { + // PNG requires RGBA with post-multiplied alpha, so we need to + // convert + UniquePtr<uint8_t[]> row = MakeUniqueFallible<uint8_t[]>(aWidth * 4); + if (NS_WARN_IF(!row)) { + return NS_ERROR_OUT_OF_MEMORY; + } + for (uint32_t y = 0; y < aHeight; y++) { + ConvertHostARGBRow(&aData[y * aStride], row.get(), aWidth, + useTransparency); + png_write_row(mPNG, row.get()); + } + } else if (aInputFormat == INPUT_FORMAT_RGBA && !useTransparency) { + // RBGA, but we need to strip the alpha + UniquePtr<uint8_t[]> row = MakeUniqueFallible<uint8_t[]>(aWidth * 4); + if (NS_WARN_IF(!row)) { + return NS_ERROR_OUT_OF_MEMORY; + } + for (uint32_t y = 0; y < aHeight; y++) { + StripAlpha(&aData[y * aStride], row.get(), aWidth); + png_write_row(mPNG, row.get()); + } + } else if (aInputFormat == INPUT_FORMAT_RGB || + aInputFormat == INPUT_FORMAT_RGBA) { + // simple RBG(A), no conversion needed + for (uint32_t y = 0; y < aHeight; y++) { + png_write_row(mPNG, (uint8_t*)&aData[y * aStride]); + } + + } else { + MOZ_ASSERT_UNREACHABLE("Bad format type"); + return NS_ERROR_INVALID_ARG; + } + +#ifdef PNG_APNG_SUPPORTED + if (mIsAnimation) { + png_write_frame_tail(mPNG, mPNGinfo); + } +#endif + + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::EndImageEncode() { + // must be initialized + if (mImageBuffer == nullptr) { + return NS_ERROR_NOT_INITIALIZED; + } + + // EndImageEncode has already been called, or some error + // occurred earlier + if (!mPNG) { + return NS_BASE_STREAM_CLOSED; + } + + // libpng's error handler jumps back here upon an error. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; + } + + png_write_end(mPNG, mPNGinfo); + png_destroy_write_struct(&mPNG, &mPNGinfo); + + mFinished = true; + NotifyListener(); + + // if output callback can't get enough memory, it will free our buffer + if (!mImageBuffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +nsresult nsPNGEncoder::ParseOptions(const nsAString& aOptions, + bool* useTransparency, bool* skipFirstFrame, + uint32_t* numFrames, uint32_t* numPlays, + int* zlibLevel, int* filters, + uint32_t* frameDispose, + uint32_t* frameBlend, uint32_t* frameDelay, + uint32_t* offsetX, uint32_t* offsetY) { +#ifdef PNG_APNG_SUPPORTED + // Make a copy of aOptions, because strtok() will modify it. + nsAutoCString optionsCopy; + optionsCopy.Assign(NS_ConvertUTF16toUTF8(aOptions)); + char* options = optionsCopy.BeginWriting(); + + while (char* token = nsCRT::strtok(options, ";", &options)) { + // If there's an '=' character, split the token around it. + char* equals = token; + char* value = nullptr; + while (*equals != '=' && *equals) { + ++equals; + } + if (*equals == '=') { + value = equals + 1; + } + + if (value) { + *equals = '\0'; // temporary null + } + + // transparency=[yes|no|none] + if (nsCRT::strcmp(token, "transparency") == 0 && useTransparency) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "none") == 0 || + nsCRT::strcmp(value, "no") == 0) { + *useTransparency = false; + } else if (nsCRT::strcmp(value, "yes") == 0) { + *useTransparency = true; + } else { + return NS_ERROR_INVALID_ARG; + } + + // skipfirstframe=[yes|no] + } else if (nsCRT::strcmp(token, "skipfirstframe") == 0 && skipFirstFrame) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "no") == 0) { + *skipFirstFrame = false; + } else if (nsCRT::strcmp(value, "yes") == 0) { + *skipFirstFrame = true; + } else { + return NS_ERROR_INVALID_ARG; + } + + // frames=# + } else if (nsCRT::strcmp(token, "frames") == 0 && numFrames) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (PR_sscanf(value, "%u", numFrames) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // frames=0 is nonsense. + if (*numFrames == 0) { + return NS_ERROR_INVALID_ARG; + } + + // plays=# + } else if (nsCRT::strcmp(token, "plays") == 0 && numPlays) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + // plays=0 to loop forever, otherwise play sequence specified + // number of times + if (PR_sscanf(value, "%u", numPlays) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // png-zlib-level=# + } else if (nsCRT::strcmp(token, "png-zlib-level") == 0) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + int localZlibLevel = DEFAULT_ZLIB_LEVEL; + if (PR_sscanf(value, "%d", &localZlibLevel) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // zlib-level 0-9 are the only valid values + if (localZlibLevel < 0 || localZlibLevel > 9) { + return NS_ERROR_INVALID_ARG; + } + + if (zlibLevel) { + *zlibLevel = localZlibLevel; + } + + // png-filter=[no_filters|none|sub|up|avg|paeth|fast|all] + } else if (nsCRT::strcmp(token, "png-filter") == 0) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "no_filters") == 0) { + if (filters) { + *filters = PNG_NO_FILTERS; + } + } else if (nsCRT::strcmp(value, "none") == 0) { + if (filters) { + *filters = PNG_FILTER_NONE; + } + } else if (nsCRT::strcmp(value, "sub") == 0) { + if (filters) { + *filters = PNG_FILTER_SUB; + } + } else if (nsCRT::strcmp(value, "up") == 0) { + if (filters) { + *filters = PNG_FILTER_UP; + } + } else if (nsCRT::strcmp(value, "avg") == 0) { + if (filters) { + *filters = PNG_FILTER_AVG; + } + } else if (nsCRT::strcmp(value, "paeth") == 0) { + if (filters) { + *filters = PNG_FILTER_PAETH; + } + } else if (nsCRT::strcmp(value, "fast") == 0) { + if (filters) { + *filters = PNG_FAST_FILTERS; + } + } else if (nsCRT::strcmp(value, "all") == 0) { + if (filters) { + *filters = PNG_ALL_FILTERS; + } + } else { + return NS_ERROR_INVALID_ARG; + } + + // dispose=[none|background|previous] + } else if (nsCRT::strcmp(token, "dispose") == 0 && frameDispose) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "none") == 0) { + *frameDispose = PNG_DISPOSE_OP_NONE; + } else if (nsCRT::strcmp(value, "background") == 0) { + *frameDispose = PNG_DISPOSE_OP_BACKGROUND; + } else if (nsCRT::strcmp(value, "previous") == 0) { + *frameDispose = PNG_DISPOSE_OP_PREVIOUS; + } else { + return NS_ERROR_INVALID_ARG; + } + + // blend=[source|over] + } else if (nsCRT::strcmp(token, "blend") == 0 && frameBlend) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (nsCRT::strcmp(value, "source") == 0) { + *frameBlend = PNG_BLEND_OP_SOURCE; + } else if (nsCRT::strcmp(value, "over") == 0) { + *frameBlend = PNG_BLEND_OP_OVER; + } else { + return NS_ERROR_INVALID_ARG; + } + + // delay=# (in ms) + } else if (nsCRT::strcmp(token, "delay") == 0 && frameDelay) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (PR_sscanf(value, "%u", frameDelay) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // xoffset=# + } else if (nsCRT::strcmp(token, "xoffset") == 0 && offsetX) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (PR_sscanf(value, "%u", offsetX) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // yoffset=# + } else if (nsCRT::strcmp(token, "yoffset") == 0 && offsetY) { + if (!value) { + return NS_ERROR_INVALID_ARG; + } + + if (PR_sscanf(value, "%u", offsetY) != 1) { + return NS_ERROR_INVALID_ARG; + } + + // unknown token name + } else + return NS_ERROR_INVALID_ARG; + + if (value) { + *equals = '='; // restore '=' so strtok doesn't get lost + } + } + +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::Close() { + if (mImageBuffer != nullptr) { + free(mImageBuffer); + mImageBuffer = nullptr; + mImageBufferSize = 0; + mImageBufferUsed = 0; + mImageBufferReadPoint = 0; + } + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::Available(uint64_t* _retval) { + if (!mImageBuffer) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = mImageBufferUsed - mImageBufferReadPoint; + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::StreamStatus() { + return mImageBuffer ? NS_OK : NS_BASE_STREAM_CLOSED; +} + +NS_IMETHODIMP +nsPNGEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +NS_IMETHODIMP +nsPNGEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + // Avoid another thread reallocing the buffer underneath us + ReentrantMonitorAutoEnter autoEnter(mReentrantMonitor); + + uint32_t maxCount = mImageBufferUsed - mImageBufferReadPoint; + if (maxCount == 0) { + *_retval = 0; + return mFinished ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + + if (aCount > maxCount) { + aCount = maxCount; + } + + nsresult rv = aWriter( + this, aClosure, + reinterpret_cast<const char*>(mImageBuffer + mImageBufferReadPoint), 0, + aCount, _retval); + if (NS_SUCCEEDED(rv)) { + NS_ASSERTION(*_retval <= aCount, "bad write count"); + mImageBufferReadPoint += *_retval; + } + + // errors returned from the writer end here! + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::AsyncWait(nsIInputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aTarget) { + if (aFlags != 0) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + if (mCallback || mCallbackTarget) { + return NS_ERROR_UNEXPECTED; + } + + mCallbackTarget = aTarget; + // 0 means "any number of bytes except 0" + mNotifyThreshold = aRequestedCount; + if (!aRequestedCount) { + mNotifyThreshold = 1024; // We don't want to notify incessantly + } + + // We set the callback absolutely last, because NotifyListener uses it to + // determine if someone needs to be notified. If we don't set it last, + // NotifyListener might try to fire off a notification to a null target + // which will generally cause non-threadsafe objects to be used off the main + // thread + mCallback = aCallback; + + // What we are being asked for may be present already + NotifyListener(); + return NS_OK; +} + +NS_IMETHODIMP +nsPNGEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +// nsPNGEncoder::ConvertHostARGBRow +// +// Our colors are stored with premultiplied alphas, but PNGs use +// post-multiplied alpha. This swaps to PNG-style alpha. +// +// Copied from gfx/cairo/cairo/src/cairo-png.c + +void nsPNGEncoder::ConvertHostARGBRow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth, + bool aUseTransparency) { + uint32_t pixelStride = aUseTransparency ? 4 : 3; + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint32_t& pixelIn = ((const uint32_t*)(aSrc))[x]; + uint8_t* pixelOut = &aDest[x * pixelStride]; + + uint8_t alpha = (pixelIn & 0xff000000) >> 24; + pixelOut[pixelStride - 1] = alpha; // overwritten below if pixelStride == 3 + if (alpha == 255) { + pixelOut[0] = (pixelIn & 0xff0000) >> 16; + pixelOut[1] = (pixelIn & 0x00ff00) >> 8; + pixelOut[2] = (pixelIn & 0x0000ff); + } else if (alpha == 0) { + pixelOut[0] = pixelOut[1] = pixelOut[2] = 0; + } else { + pixelOut[0] = (((pixelIn & 0xff0000) >> 16) * 255 + alpha / 2) / alpha; + pixelOut[1] = (((pixelIn & 0x00ff00) >> 8) * 255 + alpha / 2) / alpha; + pixelOut[2] = (((pixelIn & 0x0000ff)) * 255 + alpha / 2) / alpha; + } + } +} + +// nsPNGEncoder::StripAlpha +// +// Input is RGBA, output is RGB + +void nsPNGEncoder::StripAlpha(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth) { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint8_t* pixelIn = &aSrc[x * 4]; + uint8_t* pixelOut = &aDest[x * 3]; + pixelOut[0] = pixelIn[0]; + pixelOut[1] = pixelIn[1]; + pixelOut[2] = pixelIn[2]; + } +} + +// nsPNGEncoder::WarningCallback + +void nsPNGEncoder::WarningCallback(png_structp png_ptr, + png_const_charp warning_msg) { + MOZ_LOG(sPNGEncoderLog, LogLevel::Warning, + ("libpng warning: %s\n", warning_msg)); +} + +// nsPNGEncoder::ErrorCallback + +void nsPNGEncoder::ErrorCallback(png_structp png_ptr, + png_const_charp error_msg) { + MOZ_LOG(sPNGEncoderLog, LogLevel::Error, ("libpng error: %s\n", error_msg)); + png_longjmp(png_ptr, 1); +} + +// nsPNGEncoder::WriteCallback + +void // static +nsPNGEncoder::WriteCallback(png_structp png, png_bytep data, png_size_t size) { + nsPNGEncoder* that = static_cast<nsPNGEncoder*>(png_get_io_ptr(png)); + if (!that->mImageBuffer) { + return; + } + + CheckedUint32 sizeNeeded = CheckedUint32(that->mImageBufferUsed) + size; + if (!sizeNeeded.isValid()) { + // Take the lock to ensure that nobody is trying to read from the buffer + // we are destroying + ReentrantMonitorAutoEnter autoEnter(that->mReentrantMonitor); + + that->NullOutImageBuffer(); + return; + } + + if (sizeNeeded.value() > that->mImageBufferSize) { + // When we're reallocing the buffer we need to take the lock to ensure + // that nobody is trying to read from the buffer we are destroying + ReentrantMonitorAutoEnter autoEnter(that->mReentrantMonitor); + + while (sizeNeeded.value() > that->mImageBufferSize) { + // expand buffer, just double each time + CheckedUint32 bufferSize = CheckedUint32(that->mImageBufferSize) * 2; + if (!bufferSize.isValid()) { + that->NullOutImageBuffer(); + return; + } + that->mImageBufferSize *= 2; + uint8_t* newBuf = + (uint8_t*)realloc(that->mImageBuffer, that->mImageBufferSize); + if (!newBuf) { + // can't resize, just zero (this will keep us from writing more) + that->NullOutImageBuffer(); + return; + } + that->mImageBuffer = newBuf; + } + } + + memcpy(&that->mImageBuffer[that->mImageBufferUsed], data, size); + that->mImageBufferUsed += size; + that->NotifyListener(); +} + +void nsPNGEncoder::NullOutImageBuffer() { + mReentrantMonitor.AssertCurrentThreadIn(); + + free(mImageBuffer); + mImageBuffer = nullptr; + mImageBufferSize = 0; + mImageBufferUsed = 0; +} + +void nsPNGEncoder::NotifyListener() { + // We might call this function on multiple threads (any threads that call + // AsyncWait and any that do encoding) so we lock to avoid notifying the + // listener twice about the same data (which generally leads to a truncated + // image). + ReentrantMonitorAutoEnter autoEnter(mReentrantMonitor); + + if (mCallback && + (mImageBufferUsed - mImageBufferReadPoint >= mNotifyThreshold || + mFinished)) { + nsCOMPtr<nsIInputStreamCallback> callback; + if (mCallbackTarget) { + callback = NS_NewInputStreamReadyEvent("nsPNGEncoder::NotifyListener", + mCallback, mCallbackTarget); + } else { + callback = mCallback; + } + + NS_ASSERTION(callback, "Shouldn't fail to make the callback"); + // Null the callback first because OnInputStreamReady could reenter + // AsyncWait + mCallback = nullptr; + mCallbackTarget = nullptr; + mNotifyThreshold = 0; + + callback->OnInputStreamReady(this); + } +} diff --git a/image/encoders/png/nsPNGEncoder.h b/image/encoders/png/nsPNGEncoder.h new file mode 100644 index 0000000000..76d695ccfa --- /dev/null +++ b/image/encoders/png/nsPNGEncoder.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +#ifndef mozilla_image_encoders_png_nsPNGEncoder_h +#define mozilla_image_encoders_png_nsPNGEncoder_h + +#include <png.h> + +#include "imgIEncoder.h" +#include "nsCOMPtr.h" + +#include "mozilla/Attributes.h" +#include "mozilla/ReentrantMonitor.h" + +#define NS_PNGENCODER_CID \ + { /* 38d1592e-b81e-432b-86f8-471878bbfe07 */ \ + 0x38d1592e, 0xb81e, 0x432b, { \ + 0x86, 0xf8, 0x47, 0x18, 0x78, 0xbb, 0xfe, 0x07 \ + } \ + } + +// Provides PNG encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. + +class nsPNGEncoder final : public imgIEncoder { + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsPNGEncoder(); + + protected: + ~nsPNGEncoder(); + nsresult ParseOptions(const nsAString& aOptions, bool* useTransparency, + bool* skipFirstFrame, uint32_t* numAnimatedFrames, + uint32_t* numIterations, int* zlibLevel, int* filters, + uint32_t* frameDispose, uint32_t* frameBlend, + uint32_t* frameDelay, uint32_t* offsetX, + uint32_t* offsetY); + void ConvertHostARGBRow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth, bool aUseTransparency); + void StripAlpha(const uint8_t* aSrc, uint8_t* aDest, uint32_t aPixelWidth); + static void WarningCallback(png_structp png_ptr, png_const_charp warning_msg); + static void ErrorCallback(png_structp png_ptr, png_const_charp error_msg); + static void WriteCallback(png_structp png, png_bytep data, png_size_t size); + void NullOutImageBuffer(); + void NotifyListener(); + + png_struct* mPNG; + png_info* mPNGinfo; + + bool mIsAnimation; + bool mFinished; + + // image buffer + uint8_t* mImageBuffer; + uint32_t mImageBufferSize; + uint32_t mImageBufferUsed; + + uint32_t mImageBufferReadPoint; + + nsCOMPtr<nsIInputStreamCallback> mCallback; + nsCOMPtr<nsIEventTarget> mCallbackTarget; + uint32_t mNotifyThreshold; + + // nsPNGEncoder is designed to allow one thread to pump data into it while + // another reads from it. We lock to ensure that the buffer remains + // append-only while we read from it (that it is not realloced) and to + // ensure that only one thread dispatches a callback for each call to + // AsyncWait. + ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED; +}; +#endif // mozilla_image_encoders_png_nsPNGEncoder_h |