diff options
Diffstat (limited to 'image/encoders')
-rw-r--r-- | image/encoders/bmp/moz.build | 15 | ||||
-rw-r--r-- | image/encoders/bmp/nsBMPEncoder.cpp | 710 | ||||
-rw-r--r-- | image/encoders/bmp/nsBMPEncoder.h | 150 | ||||
-rw-r--r-- | image/encoders/ico/moz.build | 18 | ||||
-rw-r--r-- | image/encoders/ico/nsICOEncoder.cpp | 493 | ||||
-rw-r--r-- | image/encoders/ico/nsICOEncoder.h | 95 | ||||
-rw-r--r-- | image/encoders/jpeg/moz.build | 11 | ||||
-rw-r--r-- | image/encoders/jpeg/nsJPEGEncoder.cpp | 504 | ||||
-rw-r--r-- | image/encoders/jpeg/nsJPEGEncoder.h | 74 | ||||
-rw-r--r-- | image/encoders/moz.build | 12 | ||||
-rw-r--r-- | image/encoders/png/moz.build | 15 | ||||
-rw-r--r-- | image/encoders/png/nsPNGEncoder.cpp | 732 | ||||
-rw-r--r-- | image/encoders/png/nsPNGEncoder.h | 78 |
13 files changed, 2907 insertions, 0 deletions
diff --git a/image/encoders/bmp/moz.build b/image/encoders/bmp/moz.build new file mode 100644 index 0000000000..783fd37005 --- /dev/null +++ b/image/encoders/bmp/moz.build @@ -0,0 +1,15 @@ +# -*- 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 += [ + "nsBMPEncoder.cpp", +] + +LOCAL_INCLUDES += [ + "/image", +] + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/bmp/nsBMPEncoder.cpp b/image/encoders/bmp/nsBMPEncoder.cpp new file mode 100644 index 0000000000..0d035ec9f7 --- /dev/null +++ b/image/encoders/bmp/nsBMPEncoder.cpp @@ -0,0 +1,710 @@ +/* -*- 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 "nsCRT.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/UniquePtrExtensions.h" +#include "nsBMPEncoder.h" +#include "nsString.h" +#include "nsStreamUtils.h" +#include "nsTArray.h" +#include "mozilla/CheckedInt.h" +#include "BMPHeaders.h" + +using namespace mozilla; +using namespace mozilla::image; +using namespace mozilla::image::bmp; + +NS_IMPL_ISUPPORTS(nsBMPEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +nsBMPEncoder::nsBMPEncoder() + : mBMPInfoHeader{}, + mImageBufferStart(nullptr), + mImageBufferCurr(0), + mImageBufferSize(0), + mImageBufferReadPoint(0), + mFinished(false), + mCallback(nullptr), + mCallbackTarget(nullptr), + mNotifyThreshold(0) { + this->mBMPFileHeader.filesize = 0; + this->mBMPFileHeader.reserved = 0; + this->mBMPFileHeader.dataoffset = 0; +} + +nsBMPEncoder::~nsBMPEncoder() { + if (mImageBufferStart) { + free(mImageBufferStart); + mImageBufferStart = nullptr; + mImageBufferCurr = nullptr; + } +} + +// nsBMPEncoder::InitFromData +// +// One output option is supported: bpp=<bpp_value> +// bpp specifies the bits per pixel to use where bpp_value can be 24 or 32 +NS_IMETHODIMP +nsBMPEncoder::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) { + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) { + return NS_ERROR_INVALID_ARG; + } + + CheckedInt32 check = CheckedInt32(aWidth) * 4; + if (MOZ_UNLIKELY(!check.isValid())) { + return NS_ERROR_INVALID_ARG; + } + + // Stride is the padded width of each row, so it better be longer + 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"); + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + rv = StartImageEncode(aWidth, aHeight, aInputFormat, aOutputOptions); + if (NS_FAILED(rv)) { + return rv; + } + + rv = AddImageFrame(aData, aLength, aWidth, aHeight, aStride, aInputFormat, + aOutputOptions); + if (NS_FAILED(rv)) { + return rv; + } + + rv = EndImageEncode(); + return rv; +} + +// Just a helper method to make it explicit in calculations that we are dealing +// with bytes and not bits +static inline uint16_t BytesPerPixel(uint16_t aBPP) { return aBPP / 8; } + +// Calculates the number of padding bytes that are needed per row of image data +static inline uint32_t PaddingBytes(uint16_t aBPP, uint32_t aWidth) { + uint32_t rowSize = aWidth * BytesPerPixel(aBPP); + uint8_t paddingSize = 0; + if (rowSize % 4) { + paddingSize = (4 - (rowSize % 4)); + } + return paddingSize; +} + +// See ::InitFromData for other info. +NS_IMETHODIMP +nsBMPEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + // can't initialize more than once + if (mImageBufferStart || mImageBufferCurr) { + 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 + Version version; + uint16_t bpp; + nsresult rv = ParseOptions(aOutputOptions, version, bpp); + if (NS_FAILED(rv)) { + return rv; + } + MOZ_ASSERT(bpp <= 32); + + rv = InitFileHeader(version, bpp, aWidth, aHeight); + if (NS_FAILED(rv)) { + return rv; + } + rv = InitInfoHeader(version, bpp, aWidth, aHeight); + if (NS_FAILED(rv)) { + return rv; + } + + mImageBufferSize = mBMPFileHeader.filesize; + mImageBufferStart = static_cast<uint8_t*>(malloc(mImageBufferSize)); + if (!mImageBufferStart) { + return NS_ERROR_OUT_OF_MEMORY; + } + mImageBufferCurr = mImageBufferStart; + + EncodeFileHeader(); + EncodeInfoHeader(); + + return NS_OK; +} + +// Returns the number of bytes in the image buffer used. +// For a BMP file, this is all bytes in the buffer. +NS_IMETHODIMP +nsBMPEncoder::GetImageBufferUsed(uint32_t* aOutputSize) { + NS_ENSURE_ARG_POINTER(aOutputSize); + *aOutputSize = mImageBufferSize; + return NS_OK; +} + +// Returns a pointer to the start of the image buffer +NS_IMETHODIMP +nsBMPEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast<char*>(mImageBufferStart); + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::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) { + // must be initialized + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_NOT_INITIALIZED; + } + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) { + return NS_ERROR_INVALID_ARG; + } + + if (mBMPInfoHeader.width < 0) { + return NS_ERROR_ILLEGAL_VALUE; + } + + CheckedUint32 size = CheckedUint32(mBMPInfoHeader.width) * + CheckedUint32(BytesPerPixel(mBMPInfoHeader.bpp)); + if (MOZ_UNLIKELY(!size.isValid())) { + return NS_ERROR_FAILURE; + } + + auto row = MakeUniqueFallible<uint8_t[]>(size.value()); + if (!row) { + return NS_ERROR_OUT_OF_MEMORY; + } + + CheckedUint32 check = CheckedUint32(mBMPInfoHeader.height) * aStride; + if (MOZ_UNLIKELY(!check.isValid())) { + return NS_ERROR_FAILURE; + } + + // write each row: if we add more input formats, we may want to + // generalize the conversions + if (aInputFormat == INPUT_FORMAT_HOSTARGB) { + // BMP requires RGBA with post-multiplied alpha, so we need to convert + for (int32_t y = mBMPInfoHeader.height - 1; y >= 0; y--) { + ConvertHostARGBRow(&aData[y * aStride], row, mBMPInfoHeader.width); + if (mBMPInfoHeader.bpp == 24) { + EncodeImageDataRow24(row.get()); + } else { + EncodeImageDataRow32(row.get()); + } + } + } else if (aInputFormat == INPUT_FORMAT_RGBA) { + // simple RGBA, no conversion needed + for (int32_t y = 0; y < mBMPInfoHeader.height; y++) { + if (mBMPInfoHeader.bpp == 24) { + EncodeImageDataRow24(row.get()); + } else { + EncodeImageDataRow32(row.get()); + } + } + } else if (aInputFormat == INPUT_FORMAT_RGB) { + // simple RGB, no conversion needed + for (int32_t y = 0; y < mBMPInfoHeader.height; y++) { + if (mBMPInfoHeader.bpp == 24) { + EncodeImageDataRow24(&aData[y * aStride]); + } else { + EncodeImageDataRow32(&aData[y * aStride]); + } + } + } else { + MOZ_ASSERT_UNREACHABLE("Bad format type"); + return NS_ERROR_INVALID_ARG; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::EndImageEncode() { + // must be initialized + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_NOT_INITIALIZED; + } + + mFinished = true; + NotifyListener(); + + // if output callback can't get enough memory, it will free our buffer + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +// Parses the encoder options and sets the bits per pixel to use +// See InitFromData for a description of the parse options +nsresult nsBMPEncoder::ParseOptions(const nsAString& aOptions, + Version& aVersionOut, uint16_t& aBppOut) { + aVersionOut = VERSION_3; + aBppOut = 24; + + // Parse the input string into a set of name/value pairs. + // From a format like: name=value;bpp=<bpp_value>;name=value + // to format: [0] = name=value, [1] = bpp=<bpp_value>, [2] = name=value + nsTArray<nsCString> nameValuePairs; + ParseString(NS_ConvertUTF16toUTF8(aOptions), ';', nameValuePairs); + + // For each name/value pair in the set + for (uint32_t i = 0; i < nameValuePairs.Length(); ++i) { + // Split the name value pair [0] = name, [1] = value + nsTArray<nsCString> nameValuePair; + ParseString(nameValuePairs[i], '=', nameValuePair); + if (nameValuePair.Length() != 2) { + return NS_ERROR_INVALID_ARG; + } + + // Parse the bpp portion of the string name=value;version=<version_value>; + // name=value + if (nameValuePair[0].Equals("version", + nsCaseInsensitiveCStringComparator)) { + if (nameValuePair[1].EqualsLiteral("3")) { + aVersionOut = VERSION_3; + } else if (nameValuePair[1].EqualsLiteral("5")) { + aVersionOut = VERSION_5; + } else { + return NS_ERROR_INVALID_ARG; + } + } + + // Parse the bpp portion of the string name=value;bpp=<bpp_value>;name=value + if (nameValuePair[0].Equals("bpp", nsCaseInsensitiveCStringComparator)) { + if (nameValuePair[1].EqualsLiteral("24")) { + aBppOut = 24; + } else if (nameValuePair[1].EqualsLiteral("32")) { + aBppOut = 32; + } else { + return NS_ERROR_INVALID_ARG; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::Close() { + if (mImageBufferStart) { + free(mImageBufferStart); + mImageBufferStart = nullptr; + mImageBufferSize = 0; + mImageBufferReadPoint = 0; + mImageBufferCurr = nullptr; + } + + return NS_OK; +} + +// Obtains the available bytes to read +NS_IMETHODIMP +nsBMPEncoder::Available(uint64_t* _retval) { + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = GetCurrentImageBufferOffset() - mImageBufferReadPoint; + return NS_OK; +} + +// [noscript] Reads bytes which are available +NS_IMETHODIMP +nsBMPEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +// [noscript] Reads segments +NS_IMETHODIMP +nsBMPEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + uint32_t maxCount = GetCurrentImageBufferOffset() - 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*>(mImageBufferStart + 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 +nsBMPEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsBMPEncoder::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 +nsBMPEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +// nsBMPEncoder::ConvertHostARGBRow +// +// Our colors are stored with premultiplied alphas, but we need +// an output with no alpha in machine-independent byte order. +// +void nsBMPEncoder::ConvertHostARGBRow(const uint8_t* aSrc, + const UniquePtr<uint8_t[]>& aDest, + uint32_t aPixelWidth) { + uint16_t bytes = BytesPerPixel(mBMPInfoHeader.bpp); + + if (mBMPInfoHeader.bpp == 32) { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint32_t& pixelIn = ((const uint32_t*)(aSrc))[x]; + uint8_t* pixelOut = &aDest[x * bytes]; + + pixelOut[0] = (pixelIn & 0x00ff0000) >> 16; + pixelOut[1] = (pixelIn & 0x0000ff00) >> 8; + pixelOut[2] = (pixelIn & 0x000000ff) >> 0; + pixelOut[3] = (pixelIn & 0xff000000) >> 24; + } + } else { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint32_t& pixelIn = ((const uint32_t*)(aSrc))[x]; + uint8_t* pixelOut = &aDest[x * bytes]; + + pixelOut[0] = (pixelIn & 0xff0000) >> 16; + pixelOut[1] = (pixelIn & 0x00ff00) >> 8; + pixelOut[2] = (pixelIn & 0x0000ff) >> 0; + } + } +} + +void nsBMPEncoder::NotifyListener() { + if (mCallback && (GetCurrentImageBufferOffset() - mImageBufferReadPoint >= + mNotifyThreshold || + mFinished)) { + nsCOMPtr<nsIInputStreamCallback> callback; + if (mCallbackTarget) { + callback = NS_NewInputStreamReadyEvent("nsBMPEncoder::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); + } +} + +// Initializes the BMP file header mBMPFileHeader to the passed in values +nsresult nsBMPEncoder::InitFileHeader(Version aVersion, uint16_t aBPP, + uint32_t aWidth, uint32_t aHeight) { + memset(&mBMPFileHeader, 0, sizeof(mBMPFileHeader)); + mBMPFileHeader.signature[0] = 'B'; + mBMPFileHeader.signature[1] = 'M'; + + if (aVersion == VERSION_3) { + mBMPFileHeader.dataoffset = FILE_HEADER_LENGTH + InfoHeaderLength::WIN_V3; + } else { // aVersion == 5 + mBMPFileHeader.dataoffset = FILE_HEADER_LENGTH + InfoHeaderLength::WIN_V5; + } + + // The color table is present only if BPP is <= 8 + if (aBPP <= 8) { + uint32_t numColors = 1 << aBPP; + mBMPFileHeader.dataoffset += 4 * numColors; + CheckedUint32 filesize = CheckedUint32(mBMPFileHeader.dataoffset) + + CheckedUint32(aWidth) * aHeight; + if (MOZ_UNLIKELY(!filesize.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPFileHeader.filesize = filesize.value(); + } else { + CheckedUint32 filesize = CheckedUint32(mBMPFileHeader.dataoffset) + + (CheckedUint32(aWidth) * BytesPerPixel(aBPP) + + PaddingBytes(aBPP, aWidth)) * + aHeight; + if (MOZ_UNLIKELY(!filesize.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPFileHeader.filesize = filesize.value(); + } + + mBMPFileHeader.reserved = 0; + + return NS_OK; +} + +#define ENCODE(pImageBufferCurr, value) \ + memcpy(*pImageBufferCurr, &value, sizeof value); \ + *pImageBufferCurr += sizeof value; + +// Initializes the bitmap info header mBMPInfoHeader to the passed in values +nsresult nsBMPEncoder::InitInfoHeader(Version aVersion, uint16_t aBPP, + uint32_t aWidth, uint32_t aHeight) { + memset(&mBMPInfoHeader, 0, sizeof(mBMPInfoHeader)); + if (aVersion == VERSION_3) { + mBMPInfoHeader.bihsize = InfoHeaderLength::WIN_V3; + } else { + MOZ_ASSERT(aVersion == VERSION_5); + mBMPInfoHeader.bihsize = InfoHeaderLength::WIN_V5; + } + + CheckedInt32 width(aWidth); + CheckedInt32 height(aHeight); + if (MOZ_UNLIKELY(!width.isValid() || !height.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPInfoHeader.width = width.value(); + mBMPInfoHeader.height = height.value(); + + mBMPInfoHeader.planes = 1; + mBMPInfoHeader.bpp = aBPP; + mBMPInfoHeader.compression = 0; + mBMPInfoHeader.colors = 0; + mBMPInfoHeader.important_colors = 0; + + CheckedUint32 check = CheckedUint32(aWidth) * BytesPerPixel(aBPP); + if (MOZ_UNLIKELY(!check.isValid())) { + return NS_ERROR_INVALID_ARG; + } + + if (aBPP <= 8) { + CheckedUint32 imagesize = CheckedUint32(aWidth) * aHeight; + if (MOZ_UNLIKELY(!imagesize.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPInfoHeader.image_size = imagesize.value(); + } else { + CheckedUint32 imagesize = (CheckedUint32(aWidth) * BytesPerPixel(aBPP) + + PaddingBytes(aBPP, aWidth)) * + CheckedUint32(aHeight); + if (MOZ_UNLIKELY(!imagesize.isValid())) { + return NS_ERROR_INVALID_ARG; + } + mBMPInfoHeader.image_size = imagesize.value(); + } + mBMPInfoHeader.xppm = 0; + mBMPInfoHeader.yppm = 0; + if (aVersion >= VERSION_5) { + mBMPInfoHeader.red_mask = 0x000000FF; + mBMPInfoHeader.green_mask = 0x0000FF00; + mBMPInfoHeader.blue_mask = 0x00FF0000; + mBMPInfoHeader.alpha_mask = 0xFF000000; + mBMPInfoHeader.color_space = V5InfoHeader::COLOR_SPACE_LCS_SRGB; + mBMPInfoHeader.white_point.r.x = 0; + mBMPInfoHeader.white_point.r.y = 0; + mBMPInfoHeader.white_point.r.z = 0; + mBMPInfoHeader.white_point.g.x = 0; + mBMPInfoHeader.white_point.g.y = 0; + mBMPInfoHeader.white_point.g.z = 0; + mBMPInfoHeader.white_point.b.x = 0; + mBMPInfoHeader.white_point.b.y = 0; + mBMPInfoHeader.white_point.b.z = 0; + mBMPInfoHeader.gamma_red = 0; + mBMPInfoHeader.gamma_green = 0; + mBMPInfoHeader.gamma_blue = 0; + mBMPInfoHeader.intent = 0; + mBMPInfoHeader.profile_offset = 0; + mBMPInfoHeader.profile_size = 0; + mBMPInfoHeader.reserved = 0; + } + + return NS_OK; +} + +// Encodes the BMP file header mBMPFileHeader +void nsBMPEncoder::EncodeFileHeader() { + FileHeader littleEndianBFH = mBMPFileHeader; + NativeEndian::swapToLittleEndianInPlace(&littleEndianBFH.filesize, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianBFH.reserved, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianBFH.dataoffset, 1); + + ENCODE(&mImageBufferCurr, littleEndianBFH.signature); + ENCODE(&mImageBufferCurr, littleEndianBFH.filesize); + ENCODE(&mImageBufferCurr, littleEndianBFH.reserved); + ENCODE(&mImageBufferCurr, littleEndianBFH.dataoffset); +} + +// Encodes the BMP info header mBMPInfoHeader +void nsBMPEncoder::EncodeInfoHeader() { + V5InfoHeader littleEndianmBIH = mBMPInfoHeader; + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.bihsize, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.width, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.height, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.planes, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.bpp, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.compression, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.image_size, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.xppm, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.yppm, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.colors, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.important_colors, + 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.red_mask, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.green_mask, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.blue_mask, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.alpha_mask, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.color_space, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.r.x, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.r.y, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.r.z, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.g.x, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.g.y, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.g.z, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.b.x, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.b.y, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.white_point.b.z, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.gamma_red, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.gamma_green, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.gamma_blue, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.intent, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.profile_offset, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmBIH.profile_size, 1); + + ENCODE(&mImageBufferCurr, littleEndianmBIH.bihsize); + ENCODE(&mImageBufferCurr, littleEndianmBIH.width); + ENCODE(&mImageBufferCurr, littleEndianmBIH.height); + ENCODE(&mImageBufferCurr, littleEndianmBIH.planes); + ENCODE(&mImageBufferCurr, littleEndianmBIH.bpp); + ENCODE(&mImageBufferCurr, littleEndianmBIH.compression); + ENCODE(&mImageBufferCurr, littleEndianmBIH.image_size); + ENCODE(&mImageBufferCurr, littleEndianmBIH.xppm); + ENCODE(&mImageBufferCurr, littleEndianmBIH.yppm); + ENCODE(&mImageBufferCurr, littleEndianmBIH.colors); + ENCODE(&mImageBufferCurr, littleEndianmBIH.important_colors); + + if (mBMPInfoHeader.bihsize > InfoHeaderLength::WIN_V3) { + ENCODE(&mImageBufferCurr, littleEndianmBIH.red_mask); + ENCODE(&mImageBufferCurr, littleEndianmBIH.green_mask); + ENCODE(&mImageBufferCurr, littleEndianmBIH.blue_mask); + ENCODE(&mImageBufferCurr, littleEndianmBIH.alpha_mask); + ENCODE(&mImageBufferCurr, littleEndianmBIH.color_space); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.r.x); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.r.y); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.r.z); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.g.x); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.g.y); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.g.z); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.b.x); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.b.y); + ENCODE(&mImageBufferCurr, littleEndianmBIH.white_point.b.z); + ENCODE(&mImageBufferCurr, littleEndianmBIH.gamma_red); + ENCODE(&mImageBufferCurr, littleEndianmBIH.gamma_green); + ENCODE(&mImageBufferCurr, littleEndianmBIH.gamma_blue); + ENCODE(&mImageBufferCurr, littleEndianmBIH.intent); + ENCODE(&mImageBufferCurr, littleEndianmBIH.profile_offset); + ENCODE(&mImageBufferCurr, littleEndianmBIH.profile_size); + ENCODE(&mImageBufferCurr, littleEndianmBIH.reserved); + } +} + +// Sets a pixel in the image buffer that doesn't have alpha data +static inline void SetPixel24(uint8_t*& imageBufferCurr, uint8_t aRed, + uint8_t aGreen, uint8_t aBlue) { + *imageBufferCurr = aBlue; + *(imageBufferCurr + 1) = aGreen; + *(imageBufferCurr + 2) = aRed; +} + +// Sets a pixel in the image buffer with alpha data +static inline void SetPixel32(uint8_t*& imageBufferCurr, uint8_t aRed, + uint8_t aGreen, uint8_t aBlue, + uint8_t aAlpha = 0xFF) { + *imageBufferCurr = aBlue; + *(imageBufferCurr + 1) = aGreen; + *(imageBufferCurr + 2) = aRed; + *(imageBufferCurr + 3) = aAlpha; +} + +// Encodes a row of image data which does not have alpha data +void nsBMPEncoder::EncodeImageDataRow24(const uint8_t* aData) { + for (int32_t x = 0; x < mBMPInfoHeader.width; x++) { + uint32_t pos = x * BytesPerPixel(mBMPInfoHeader.bpp); + SetPixel24(mImageBufferCurr, aData[pos], aData[pos + 1], aData[pos + 2]); + mImageBufferCurr += BytesPerPixel(mBMPInfoHeader.bpp); + } + + for (uint32_t x = 0; + x < PaddingBytes(mBMPInfoHeader.bpp, mBMPInfoHeader.width); x++) { + *mImageBufferCurr++ = 0; + } +} + +// Encodes a row of image data which does have alpha data +void nsBMPEncoder::EncodeImageDataRow32(const uint8_t* aData) { + for (int32_t x = 0; x < mBMPInfoHeader.width; x++) { + uint32_t pos = x * BytesPerPixel(mBMPInfoHeader.bpp); + SetPixel32(mImageBufferCurr, aData[pos], aData[pos + 1], aData[pos + 2], + aData[pos + 3]); + mImageBufferCurr += 4; + } + + for (uint32_t x = 0; + x < PaddingBytes(mBMPInfoHeader.bpp, mBMPInfoHeader.width); x++) { + *mImageBufferCurr++ = 0; + } +} diff --git a/image/encoders/bmp/nsBMPEncoder.h b/image/encoders/bmp/nsBMPEncoder.h new file mode 100644 index 0000000000..1f40d9df34 --- /dev/null +++ b/image/encoders/bmp/nsBMPEncoder.h @@ -0,0 +1,150 @@ +/* -*- 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_bmp_nsBMPEncoder_h +#define mozilla_image_encoders_bmp_nsBMPEncoder_h + +#include "mozilla/Attributes.h" +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/UniquePtr.h" + +#include "imgIEncoder.h" + +#include "nsCOMPtr.h" + +#define NS_BMPENCODER_CID \ + { /* 13a5320c-4c91-4FA4-bd16-b081a3ba8c0b */ \ + 0x13a5320c, 0x4c91, 0x4fa4, { \ + 0xbd, 0x16, 0xb0, 0x81, 0xa3, 0Xba, 0x8c, 0x0b \ + } \ + } + +namespace mozilla { +namespace image { +namespace bmp { + +struct FileHeader { + char signature[2]; // String "BM". + uint32_t filesize; // File size. + int32_t reserved; // Zero. + uint32_t dataoffset; // Offset to raster data. +}; + +struct XYZ { + int32_t x, y, z; +}; + +struct XYZTriple { + XYZ r, g, b; +}; + +struct V5InfoHeader { + uint32_t bihsize; // Header size + int32_t width; // Uint16 in OS/2 BMPs + int32_t height; // Uint16 in OS/2 BMPs + uint16_t planes; // =1 + uint16_t bpp; // Bits per pixel. + uint32_t compression; // See Compression for valid values + uint32_t image_size; // (compressed) image size. Can be 0 if + // compression==0 + uint32_t xppm; // Pixels per meter, horizontal + uint32_t yppm; // Pixels per meter, vertical + uint32_t colors; // Used Colors + uint32_t important_colors; // Number of important colors. 0=all + // The rest of the header is not available in WIN_V3 BMP Files + uint32_t red_mask; // Bits used for red component + uint32_t green_mask; // Bits used for green component + uint32_t blue_mask; // Bits used for blue component + uint32_t alpha_mask; // Bits used for alpha component + uint32_t color_space; // 0x73524742=LCS_sRGB ... + // These members are unused unless color_space == LCS_CALIBRATED_RGB + XYZTriple white_point; // Logical white point + uint32_t gamma_red; // Red gamma component + uint32_t gamma_green; // Green gamma component + uint32_t gamma_blue; // Blue gamma component + uint32_t intent; // Rendering intent + // These members are unused unless color_space == LCS_PROFILE_* + uint32_t profile_offset; // Offset to profile data in bytes + uint32_t profile_size; // Size of profile data in bytes + uint32_t reserved; // =0 + + static const uint32_t COLOR_SPACE_LCS_SRGB = 0x73524742; +}; + +} // namespace bmp +} // namespace image +} // namespace mozilla + +// Provides BMP encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. + +class nsBMPEncoder final : public imgIEncoder { + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsBMPEncoder(); + + protected: + ~nsBMPEncoder(); + + enum Version { VERSION_3 = 3, VERSION_5 = 5 }; + + // See InitData in the cpp for valid parse options + nsresult ParseOptions(const nsAString& aOptions, Version& aVersionOut, + uint16_t& aBppOut); + // Obtains data with no alpha in machine-independent byte order + void ConvertHostARGBRow(const uint8_t* aSrc, + const mozilla::UniquePtr<uint8_t[]>& aDest, + uint32_t aPixelWidth); + // Thread safe notify listener + void NotifyListener(); + + // Initializes the bitmap file header member mBMPFileHeader + nsresult InitFileHeader(Version aVersion, uint16_t aBPP, uint32_t aWidth, + uint32_t aHeight); + // Initializes the bitmap info header member mBMPInfoHeader + nsresult InitInfoHeader(Version aVersion, uint16_t aBPP, uint32_t aWidth, + uint32_t aHeight); + + // Encodes the bitmap file header member mBMPFileHeader + void EncodeFileHeader(); + // Encodes the bitmap info header member mBMPInfoHeader + void EncodeInfoHeader(); + // Encodes a row of image data which does not have alpha data + void EncodeImageDataRow24(const uint8_t* aData); + // Encodes a row of image data which does have alpha data + void EncodeImageDataRow32(const uint8_t* aData); + // Obtains the current offset filled up to for the image buffer + inline int32_t GetCurrentImageBufferOffset() { + return static_cast<int32_t>(mImageBufferCurr - mImageBufferStart); + } + + // These headers will always contain endian independent stuff + // They store the BMP headers which will be encoded + mozilla::image::bmp::FileHeader mBMPFileHeader; + mozilla::image::bmp::V5InfoHeader mBMPInfoHeader; + + // Keeps track of the start of the image buffer + uint8_t* mImageBufferStart; + // Keeps track of the current position in the image buffer + uint8_t* mImageBufferCurr; + // Keeps track of the image buffer size + uint32_t mImageBufferSize; + // Keeps track of the number of bytes in the image buffer which are read + uint32_t mImageBufferReadPoint; + // Stores true if the image is done being encoded + bool mFinished; + + nsCOMPtr<nsIInputStreamCallback> mCallback; + nsCOMPtr<nsIEventTarget> mCallbackTarget; + uint32_t mNotifyThreshold; +}; + +#endif // mozilla_image_encoders_bmp_nsBMPEncoder_h diff --git a/image/encoders/ico/moz.build b/image/encoders/ico/moz.build new file mode 100644 index 0000000000..1a3d62956d --- /dev/null +++ b/image/encoders/ico/moz.build @@ -0,0 +1,18 @@ +# -*- 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 += [ + "nsICOEncoder.cpp", +] + +# Decoders need RasterImage.h +LOCAL_INCLUDES += [ + "/image", + "/image/encoders/bmp", + "/image/encoders/png", +] + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/ico/nsICOEncoder.cpp b/image/encoders/ico/nsICOEncoder.cpp new file mode 100644 index 0000000000..df611608e4 --- /dev/null +++ b/image/encoders/ico/nsICOEncoder.cpp @@ -0,0 +1,493 @@ +/* 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 "nsCRT.h" +#include "mozilla/EndianUtils.h" +#include "nsBMPEncoder.h" +#include "BMPHeaders.h" +#include "nsPNGEncoder.h" +#include "nsICOEncoder.h" +#include "nsString.h" +#include "nsStreamUtils.h" +#include "nsTArray.h" + +using namespace mozilla; +using namespace mozilla::image; + +NS_IMPL_ISUPPORTS(nsICOEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +nsICOEncoder::nsICOEncoder() + : mICOFileHeader{}, + mICODirEntry{}, + mImageBufferStart(nullptr), + mImageBufferCurr(0), + mImageBufferSize(0), + mImageBufferReadPoint(0), + mFinished(false), + mUsePNG(true), + mNotifyThreshold(0) {} + +nsICOEncoder::~nsICOEncoder() { + if (mImageBufferStart) { + free(mImageBufferStart); + mImageBufferStart = nullptr; + mImageBufferCurr = nullptr; + } +} + +// nsICOEncoder::InitFromData +// Two output options are supported: format=<png|bmp>;bpp=<bpp_value> +// format specifies whether to use png or bitmap format +// bpp specifies the bits per pixel to use where bpp_value can be 24 or 32 +NS_IMETHODIMP +nsICOEncoder::InitFromData(const uint8_t* aData, uint32_t aLength, + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) { + return NS_ERROR_INVALID_ARG; + } + + // Stride is the padded width of each row, so it better be longer + 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"); + return NS_ERROR_INVALID_ARG; + } + + nsresult rv; + rv = StartImageEncode(aWidth, aHeight, aInputFormat, aOutputOptions); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AddImageFrame(aData, aLength, aWidth, aHeight, aStride, aInputFormat, + aOutputOptions); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EndImageEncode(); + return rv; +} + +// Returns the number of bytes in the image buffer used +// For an ICO file, this is all bytes in the buffer. +NS_IMETHODIMP +nsICOEncoder::GetImageBufferUsed(uint32_t* aOutputSize) { + NS_ENSURE_ARG_POINTER(aOutputSize); + *aOutputSize = mImageBufferSize; + return NS_OK; +} + +// Returns a pointer to the start of the image buffer +NS_IMETHODIMP +nsICOEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast<char*>(mImageBufferStart); + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::AddImageFrame(const uint8_t* aData, uint32_t aLength, + uint32_t aWidth, uint32_t aHeight, uint32_t aStride, + uint32_t aInputFormat, + const nsAString& aFrameOptions) { + if (mUsePNG) { + mContainedEncoder = new nsPNGEncoder(); + nsresult rv; + nsAutoString noParams; + rv = mContainedEncoder->InitFromData(aData, aLength, aWidth, aHeight, + aStride, aInputFormat, noParams); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t PNGImageBufferSize; + mContainedEncoder->GetImageBufferUsed(&PNGImageBufferSize); + mImageBufferSize = + ICONFILEHEADERSIZE + ICODIRENTRYSIZE + PNGImageBufferSize; + mImageBufferStart = static_cast<uint8_t*>(malloc(mImageBufferSize)); + if (!mImageBufferStart) { + return NS_ERROR_OUT_OF_MEMORY; + } + mImageBufferCurr = mImageBufferStart; + mICODirEntry.mBytesInRes = PNGImageBufferSize; + + EncodeFileHeader(); + EncodeInfoHeader(); + + char* imageBuffer; + rv = mContainedEncoder->GetImageBuffer(&imageBuffer); + NS_ENSURE_SUCCESS(rv, rv); + memcpy(mImageBufferCurr, imageBuffer, PNGImageBufferSize); + mImageBufferCurr += PNGImageBufferSize; + } else { + mContainedEncoder = new nsBMPEncoder(); + nsresult rv; + + nsAutoString params; + params.AppendLiteral("bpp="); + params.AppendInt(mICODirEntry.mBitCount); + + rv = mContainedEncoder->InitFromData(aData, aLength, aWidth, aHeight, + aStride, aInputFormat, params); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t andMaskSize = ((GetRealWidth() + 31) / 32) * 4 * // row AND mask + GetRealHeight(); // num rows + + uint32_t BMPImageBufferSize; + mContainedEncoder->GetImageBufferUsed(&BMPImageBufferSize); + mImageBufferSize = + ICONFILEHEADERSIZE + ICODIRENTRYSIZE + BMPImageBufferSize + andMaskSize; + mImageBufferStart = static_cast<uint8_t*>(malloc(mImageBufferSize)); + if (!mImageBufferStart) { + return NS_ERROR_OUT_OF_MEMORY; + } + mImageBufferCurr = mImageBufferStart; + + // Icon files that wrap a BMP file must not include the BITMAPFILEHEADER + // section at the beginning of the encoded BMP data, so we must skip over + // bmp::FILE_HEADER_LENGTH bytes when adding the BMP content to the icon + // file. + mICODirEntry.mBytesInRes = + BMPImageBufferSize - bmp::FILE_HEADER_LENGTH + andMaskSize; + + // Encode the icon headers + EncodeFileHeader(); + EncodeInfoHeader(); + + char* imageBuffer; + rv = mContainedEncoder->GetImageBuffer(&imageBuffer); + NS_ENSURE_SUCCESS(rv, rv); + memcpy(mImageBufferCurr, imageBuffer + bmp::FILE_HEADER_LENGTH, + BMPImageBufferSize - bmp::FILE_HEADER_LENGTH); + // We need to fix the BMP height to be *2 for the AND mask + uint32_t fixedHeight = GetRealHeight() * 2; + NativeEndian::swapToLittleEndianInPlace(&fixedHeight, 1); + // The height is stored at an offset of 8 from the DIB header + memcpy(mImageBufferCurr + 8, &fixedHeight, sizeof(fixedHeight)); + mImageBufferCurr += BMPImageBufferSize - bmp::FILE_HEADER_LENGTH; + + // Calculate rowsize in DWORD's + uint32_t rowSize = ((GetRealWidth() + 31) / 32) * 4; // + 31 to round up + int32_t currentLine = GetRealHeight(); + + // Write out the AND mask + while (currentLine > 0) { + currentLine--; + uint8_t* encoded = mImageBufferCurr + currentLine * rowSize; + uint8_t* encodedEnd = encoded + rowSize; + while (encoded != encodedEnd) { + *encoded = 0; // make everything visible + encoded++; + } + } + + mImageBufferCurr += andMaskSize; + } + + return NS_OK; +} + +// See ::InitFromData for other info. +NS_IMETHODIMP +nsICOEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + // can't initialize more than once + if (mImageBufferStart || mImageBufferCurr) { + 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; + } + + // Icons are only 1 byte, so make sure our bitmap is in range + if (aWidth > 256 || aHeight > 256) { + return NS_ERROR_INVALID_ARG; + } + + // parse and check any provided output options + uint16_t bpp = 24; + bool usePNG = true; + nsresult rv = ParseOptions(aOutputOptions, bpp, usePNG); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(bpp <= 32); + + mUsePNG = usePNG; + + InitFileHeader(); + // The width and height are stored as 0 when we have a value of 256 + InitInfoHeader(bpp, aWidth == 256 ? 0 : (uint8_t)aWidth, + aHeight == 256 ? 0 : (uint8_t)aHeight); + + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::EndImageEncode() { + // must be initialized + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_NOT_INITIALIZED; + } + + mFinished = true; + NotifyListener(); + + // if output callback can't get enough memory, it will free our buffer + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +// Parses the encoder options and sets the bits per pixel to use and PNG or BMP +// See InitFromData for a description of the parse options +nsresult nsICOEncoder::ParseOptions(const nsAString& aOptions, + uint16_t& aBppOut, bool& aUsePNGOut) { + // If no parsing options just use the default of 24BPP and PNG yes + if (aOptions.Length() == 0) { + aUsePNGOut = true; + aBppOut = 24; + } + + // Parse the input string into a set of name/value pairs. + // From format: format=<png|bmp>;bpp=<bpp_value> + // to format: [0] = format=<png|bmp>, [1] = bpp=<bpp_value> + nsTArray<nsCString> nameValuePairs; + ParseString(NS_ConvertUTF16toUTF8(aOptions), ';', nameValuePairs); + + // For each name/value pair in the set + for (unsigned i = 0; i < nameValuePairs.Length(); ++i) { + // Split the name value pair [0] = name, [1] = value + nsTArray<nsCString> nameValuePair; + ParseString(nameValuePairs[i], '=', nameValuePair); + if (nameValuePair.Length() != 2) { + return NS_ERROR_INVALID_ARG; + } + + // Parse the format portion of the string format=<png|bmp>;bpp=<bpp_value> + if (nameValuePair[0].Equals("format", nsCaseInsensitiveCStringComparator)) { + if (nameValuePair[1].Equals("png", nsCaseInsensitiveCStringComparator)) { + aUsePNGOut = true; + } else if (nameValuePair[1].Equals("bmp", + nsCaseInsensitiveCStringComparator)) { + aUsePNGOut = false; + } else { + return NS_ERROR_INVALID_ARG; + } + } + + // Parse the bpp portion of the string format=<png|bmp>;bpp=<bpp_value> + if (nameValuePair[0].Equals("bpp", nsCaseInsensitiveCStringComparator)) { + if (nameValuePair[1].EqualsLiteral("24")) { + aBppOut = 24; + } else if (nameValuePair[1].EqualsLiteral("32")) { + aBppOut = 32; + } else { + return NS_ERROR_INVALID_ARG; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::Close() { + if (mImageBufferStart) { + free(mImageBufferStart); + mImageBufferStart = nullptr; + mImageBufferSize = 0; + mImageBufferReadPoint = 0; + mImageBufferCurr = nullptr; + } + + return NS_OK; +} + +// Obtains the available bytes to read +NS_IMETHODIMP +nsICOEncoder::Available(uint64_t* _retval) { + if (!mImageBufferStart || !mImageBufferCurr) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = GetCurrentImageBufferOffset() - mImageBufferReadPoint; + return NS_OK; +} + +// [noscript] Reads bytes which are available +NS_IMETHODIMP +nsICOEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +// [noscript] Reads segments +NS_IMETHODIMP +nsICOEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + uint32_t maxCount = GetCurrentImageBufferOffset() - 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*>(mImageBufferStart + 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 +nsICOEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsICOEncoder::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 +nsICOEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +void nsICOEncoder::NotifyListener() { + if (mCallback && (GetCurrentImageBufferOffset() - mImageBufferReadPoint >= + mNotifyThreshold || + mFinished)) { + nsCOMPtr<nsIInputStreamCallback> callback; + if (mCallbackTarget) { + callback = NS_NewInputStreamReadyEvent("nsICOEncoder::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); + } +} + +// Initializes the icon file header mICOFileHeader +void nsICOEncoder::InitFileHeader() { + memset(&mICOFileHeader, 0, sizeof(mICOFileHeader)); + mICOFileHeader.mReserved = 0; + mICOFileHeader.mType = 1; + mICOFileHeader.mCount = 1; +} + +// Initializes the icon directory info header mICODirEntry +void nsICOEncoder::InitInfoHeader(uint16_t aBPP, uint8_t aWidth, + uint8_t aHeight) { + memset(&mICODirEntry, 0, sizeof(mICODirEntry)); + mICODirEntry.mBitCount = aBPP; + mICODirEntry.mBytesInRes = 0; + mICODirEntry.mColorCount = 0; + mICODirEntry.mWidth = aWidth; + mICODirEntry.mHeight = aHeight; + mICODirEntry.mImageOffset = ICONFILEHEADERSIZE + ICODIRENTRYSIZE; + mICODirEntry.mPlanes = 1; + mICODirEntry.mReserved = 0; +} + +// Encodes the icon file header mICOFileHeader +void nsICOEncoder::EncodeFileHeader() { + IconFileHeader littleEndianIFH = mICOFileHeader; + NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mReserved, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mType, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mCount, 1); + + memcpy(mImageBufferCurr, &littleEndianIFH.mReserved, + sizeof(littleEndianIFH.mReserved)); + mImageBufferCurr += sizeof(littleEndianIFH.mReserved); + memcpy(mImageBufferCurr, &littleEndianIFH.mType, + sizeof(littleEndianIFH.mType)); + mImageBufferCurr += sizeof(littleEndianIFH.mType); + memcpy(mImageBufferCurr, &littleEndianIFH.mCount, + sizeof(littleEndianIFH.mCount)); + mImageBufferCurr += sizeof(littleEndianIFH.mCount); +} + +// Encodes the icon directory info header mICODirEntry +void nsICOEncoder::EncodeInfoHeader() { + IconDirEntry littleEndianmIDE = mICODirEntry; + + NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mPlanes, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mBitCount, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mBytesInRes, 1); + NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mImageOffset, 1); + + memcpy(mImageBufferCurr, &littleEndianmIDE.mWidth, + sizeof(littleEndianmIDE.mWidth)); + mImageBufferCurr += sizeof(littleEndianmIDE.mWidth); + memcpy(mImageBufferCurr, &littleEndianmIDE.mHeight, + sizeof(littleEndianmIDE.mHeight)); + mImageBufferCurr += sizeof(littleEndianmIDE.mHeight); + memcpy(mImageBufferCurr, &littleEndianmIDE.mColorCount, + sizeof(littleEndianmIDE.mColorCount)); + mImageBufferCurr += sizeof(littleEndianmIDE.mColorCount); + memcpy(mImageBufferCurr, &littleEndianmIDE.mReserved, + sizeof(littleEndianmIDE.mReserved)); + mImageBufferCurr += sizeof(littleEndianmIDE.mReserved); + memcpy(mImageBufferCurr, &littleEndianmIDE.mPlanes, + sizeof(littleEndianmIDE.mPlanes)); + mImageBufferCurr += sizeof(littleEndianmIDE.mPlanes); + memcpy(mImageBufferCurr, &littleEndianmIDE.mBitCount, + sizeof(littleEndianmIDE.mBitCount)); + mImageBufferCurr += sizeof(littleEndianmIDE.mBitCount); + memcpy(mImageBufferCurr, &littleEndianmIDE.mBytesInRes, + sizeof(littleEndianmIDE.mBytesInRes)); + mImageBufferCurr += sizeof(littleEndianmIDE.mBytesInRes); + memcpy(mImageBufferCurr, &littleEndianmIDE.mImageOffset, + sizeof(littleEndianmIDE.mImageOffset)); + mImageBufferCurr += sizeof(littleEndianmIDE.mImageOffset); +} diff --git a/image/encoders/ico/nsICOEncoder.h b/image/encoders/ico/nsICOEncoder.h new file mode 100644 index 0000000000..ef601ba97e --- /dev/null +++ b/image/encoders/ico/nsICOEncoder.h @@ -0,0 +1,95 @@ +/* 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_ico_nsICOEncoder_h +#define mozilla_image_encoders_ico_nsICOEncoder_h + +#include "mozilla/Attributes.h" +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/image/ICOFileHeaders.h" + +#include "imgIEncoder.h" + +#include "nsCOMPtr.h" + +#define NS_ICOENCODER_CID \ + { /*92AE3AB2-8968-41B1-8709-B6123BCEAF21 */ \ + 0x92ae3ab2, 0x8968, 0x41b1, { \ + 0x87, 0x09, 0xb6, 0x12, 0x3b, 0Xce, 0xaf, 0x21 \ + } \ + } + +// Provides ICO encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. + +class nsICOEncoder final : public imgIEncoder { + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsICOEncoder(); + + // Obtains the width of the icon directory entry + uint32_t GetRealWidth() const { + return mICODirEntry.mWidth == 0 ? 256 : mICODirEntry.mWidth; + } + + // Obtains the height of the icon directory entry + uint32_t GetRealHeight() const { + return mICODirEntry.mHeight == 0 ? 256 : mICODirEntry.mHeight; + } + + protected: + ~nsICOEncoder(); + + nsresult ParseOptions(const nsAString& aOptions, uint16_t& aBppOut, + bool& aUsePNGOut); + void NotifyListener(); + + // Initializes the icon file header mICOFileHeader + void InitFileHeader(); + // Initializes the icon directory info header mICODirEntry + void InitInfoHeader(uint16_t aBPP, uint8_t aWidth, uint8_t aHeight); + // Encodes the icon file header mICOFileHeader + void EncodeFileHeader(); + // Encodes the icon directory info header mICODirEntry + void EncodeInfoHeader(); + // Obtains the current offset filled up to for the image buffer + inline int32_t GetCurrentImageBufferOffset() { + return static_cast<int32_t>(mImageBufferCurr - mImageBufferStart); + } + + // Holds either a PNG or a BMP depending on the encoding options specified + // or if no encoding options specified will use the default (PNG) + nsCOMPtr<imgIEncoder> mContainedEncoder; + + // These headers will always contain endian independent stuff. + // Don't trust the width and height of mICODirEntry directly, + // instead use the accessors GetRealWidth() and GetRealHeight(). + mozilla::image::IconFileHeader mICOFileHeader; + mozilla::image::IconDirEntry mICODirEntry; + + // Keeps track of the start of the image buffer + uint8_t* mImageBufferStart; + // Keeps track of the current position in the image buffer + uint8_t* mImageBufferCurr; + // Keeps track of the image buffer size + uint32_t mImageBufferSize; + // Keeps track of the number of bytes in the image buffer which are read + uint32_t mImageBufferReadPoint; + // Stores true if the image is done being encoded + bool mFinished; + // Stores true if the contained image is a PNG + bool mUsePNG; + + nsCOMPtr<nsIInputStreamCallback> mCallback; + nsCOMPtr<nsIEventTarget> mCallbackTarget; + uint32_t mNotifyThreshold; +}; + +#endif // mozilla_image_encoders_ico_nsICOEncoder_h diff --git a/image/encoders/jpeg/moz.build b/image/encoders/jpeg/moz.build new file mode 100644 index 0000000000..6952fc1b9f --- /dev/null +++ b/image/encoders/jpeg/moz.build @@ -0,0 +1,11 @@ +# -*- 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 += [ + "nsJPEGEncoder.cpp", +] + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/jpeg/nsJPEGEncoder.cpp b/image/encoders/jpeg/nsJPEGEncoder.cpp new file mode 100644 index 0000000000..d4e7eced38 --- /dev/null +++ b/image/encoders/jpeg/nsJPEGEncoder.cpp @@ -0,0 +1,504 @@ +/* -*- 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 "nsJPEGEncoder.h" +#include "prprf.h" +#include "nsString.h" +#include "nsStreamUtils.h" +#include "gfxColor.h" +#include "mozilla/CheckedInt.h" + +extern "C" { +#include "jpeglib.h" +} + +#include <setjmp.h> +#include "jerror.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsJPEGEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +class nsJPEGEncoderInternal { + friend class nsJPEGEncoder; + + protected: + /** + * Initialize destination. This is called by jpeg_start_compress() before + * any data is actually written. It must initialize next_output_byte and + * free_in_buffer. free_in_buffer must be initialized to a positive value. + */ + static void initDestination(jpeg_compress_struct* cinfo); + + /** + * This is called whenever the buffer has filled (free_in_buffer reaches + * zero). In typical applications, it should write out the *entire* buffer + * (use the saved start address and buffer length; ignore the current state + * of next_output_byte and free_in_buffer). Then reset the pointer & count + * to the start of the buffer, and return TRUE indicating that the buffer + * has been dumped. free_in_buffer must be set to a positive value when + * TRUE is returned. A FALSE return should only be used when I/O suspension + * is desired (this operating mode is discussed in the next section). + */ + static boolean emptyOutputBuffer(jpeg_compress_struct* cinfo); + + /** + * Terminate destination --- called by jpeg_finish_compress() after all data + * has been written. In most applications, this must flush any data + * remaining in the buffer. Use either next_output_byte or free_in_buffer + * to determine how much data is in the buffer. + */ + static void termDestination(jpeg_compress_struct* cinfo); + + /** + * Override the standard error method in the IJG JPEG decoder code. This + * was mostly copied from nsJPEGDecoder.cpp + */ + static void errorExit(jpeg_common_struct* cinfo); +}; + +// used to pass error info through the JPEG library +struct encoder_error_mgr { + jpeg_error_mgr pub; + jmp_buf setjmp_buffer; +}; + +nsJPEGEncoder::nsJPEGEncoder() + : mFinished(false), + mImageBuffer(nullptr), + mImageBufferSize(0), + mImageBufferUsed(0), + mImageBufferReadPoint(0), + mCallback(nullptr), + mCallbackTarget(nullptr), + mNotifyThreshold(0), + mReentrantMonitor("nsJPEGEncoder.mReentrantMonitor") {} + +nsJPEGEncoder::~nsJPEGEncoder() { + if (mImageBuffer) { + free(mImageBuffer); + mImageBuffer = nullptr; + } +} + +// nsJPEGEncoder::InitFromData +// +// One output option is supported: "quality=X" where X is an integer in the +// range 0-100. Higher values for X give better quality. +// +// Transparency is always discarded. + +NS_IMETHODIMP +nsJPEGEncoder::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); + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) + return NS_ERROR_INVALID_ARG; + + // 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"); + return NS_ERROR_INVALID_ARG; + } + + // can't initialize more than once + if (mImageBuffer != nullptr) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + // options: we only have one option so this is easy + int quality = 92; + if (aOutputOptions.Length() > 0) { + // have options string + const nsString qualityPrefix(u"quality="_ns); + if (aOutputOptions.Length() > qualityPrefix.Length() && + StringBeginsWith(aOutputOptions, qualityPrefix)) { + // have quality string + nsCString value = NS_ConvertUTF16toUTF8( + Substring(aOutputOptions, qualityPrefix.Length())); + int newquality = -1; + if (PR_sscanf(value.get(), "%d", &newquality) == 1) { + if (newquality >= 0 && newquality <= 100) { + quality = newquality; + } else { + NS_WARNING( + "Quality value out of range, should be 0-100," + " using default"); + } + } else { + NS_WARNING( + "Quality value invalid, should be integer 0-100," + " using default"); + } + } else { + return NS_ERROR_INVALID_ARG; + } + } + + jpeg_compress_struct cinfo; + + // We set up the normal JPEG error routines, then override error_exit. + // This must be done before the call to create_compress + encoder_error_mgr errmgr; + cinfo.err = jpeg_std_error(&errmgr.pub); + errmgr.pub.error_exit = nsJPEGEncoderInternal::errorExit; + // Establish the setjmp return context for my_error_exit to use. + if (setjmp(errmgr.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error. + // We need to clean up the JPEG object, close the input file, and return. + return NS_ERROR_FAILURE; + } + + jpeg_create_compress(&cinfo); + cinfo.image_width = aWidth; + cinfo.image_height = aHeight; + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + cinfo.data_precision = 8; + + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, 1); // quality here is 0-100 + if (quality >= 90) { + int i; + for (i = 0; i < MAX_COMPONENTS; i++) { + cinfo.comp_info[i].h_samp_factor = 1; + cinfo.comp_info[i].v_samp_factor = 1; + } + } + + // set up the destination manager + jpeg_destination_mgr destmgr; + destmgr.init_destination = nsJPEGEncoderInternal::initDestination; + destmgr.empty_output_buffer = nsJPEGEncoderInternal::emptyOutputBuffer; + destmgr.term_destination = nsJPEGEncoderInternal::termDestination; + cinfo.dest = &destmgr; + cinfo.client_data = this; + + jpeg_start_compress(&cinfo, 1); + + // feed it the rows + if (aInputFormat == INPUT_FORMAT_RGB) { + while (cinfo.next_scanline < cinfo.image_height) { + const uint8_t* row = &aData[cinfo.next_scanline * aStride]; + jpeg_write_scanlines(&cinfo, const_cast<uint8_t**>(&row), 1); + } + } else if (aInputFormat == INPUT_FORMAT_RGBA) { + UniquePtr<uint8_t[]> rowptr = MakeUnique<uint8_t[]>(aWidth * 3); + uint8_t* row = rowptr.get(); + while (cinfo.next_scanline < cinfo.image_height) { + ConvertRGBARow(&aData[cinfo.next_scanline * aStride], row, aWidth); + jpeg_write_scanlines(&cinfo, &row, 1); + } + } else if (aInputFormat == INPUT_FORMAT_HOSTARGB) { + UniquePtr<uint8_t[]> rowptr = MakeUnique<uint8_t[]>(aWidth * 3); + uint8_t* row = rowptr.get(); + while (cinfo.next_scanline < cinfo.image_height) { + ConvertHostARGBRow(&aData[cinfo.next_scanline * aStride], row, aWidth); + jpeg_write_scanlines(&cinfo, &row, 1); + } + } + + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + + 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; +} + +NS_IMETHODIMP +nsJPEGEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight, + uint32_t aInputFormat, + const nsAString& aOutputOptions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// Returns the number of bytes in the image buffer used. +NS_IMETHODIMP +nsJPEGEncoder::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 +nsJPEGEncoder::GetImageBuffer(char** aOutputBuffer) { + NS_ENSURE_ARG_POINTER(aOutputBuffer); + *aOutputBuffer = reinterpret_cast<char*>(mImageBuffer); + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::AddImageFrame(const uint8_t* aData, uint32_t aLength, + uint32_t aWidth, uint32_t aHeight, + uint32_t aStride, uint32_t aFrameFormat, + const nsAString& aFrameOptions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsJPEGEncoder::EndImageEncode() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +nsJPEGEncoder::Close() { + if (mImageBuffer != nullptr) { + free(mImageBuffer); + mImageBuffer = nullptr; + mImageBufferSize = 0; + mImageBufferUsed = 0; + mImageBufferReadPoint = 0; + } + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::Available(uint64_t* _retval) { + if (!mImageBuffer) { + return NS_BASE_STREAM_CLOSED; + } + + *_retval = mImageBufferUsed - mImageBufferReadPoint; + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +NS_IMETHODIMP +nsJPEGEncoder::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 +nsJPEGEncoder::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +nsJPEGEncoder::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; // 1 KB seems good. 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 +nsJPEGEncoder::CloseWithStatus(nsresult aStatus) { return Close(); } + +// nsJPEGEncoder::ConvertHostARGBRow +// +// Our colors are stored with premultiplied alphas, but we need +// an output with no alpha in machine-independent byte order. +// +// See gfx/cairo/cairo/src/cairo-png.c +void nsJPEGEncoder::ConvertHostARGBRow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth) { + for (uint32_t x = 0; x < aPixelWidth; x++) { + const uint32_t& pixelIn = ((const uint32_t*)(aSrc))[x]; + uint8_t* pixelOut = &aDest[x * 3]; + + pixelOut[0] = (pixelIn & 0xff0000) >> 16; + pixelOut[1] = (pixelIn & 0x00ff00) >> 8; + pixelOut[2] = (pixelIn & 0x0000ff) >> 0; + } +} + +/** + * nsJPEGEncoder::ConvertRGBARow + * + * Input is RGBA, output is RGB, so we should alpha-premultiply. + */ +void nsJPEGEncoder::ConvertRGBARow(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]; + + uint8_t alpha = pixelIn[3]; + pixelOut[0] = gfxPreMultiply(pixelIn[0], alpha); + pixelOut[1] = gfxPreMultiply(pixelIn[1], alpha); + pixelOut[2] = gfxPreMultiply(pixelIn[2], alpha); + } +} + +void nsJPEGEncoder::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("nsJPEGEncoder::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); + } +} + +/* static */ +void nsJPEGEncoderInternal::initDestination(jpeg_compress_struct* cinfo) { + nsJPEGEncoder* that = static_cast<nsJPEGEncoder*>(cinfo->client_data); + NS_ASSERTION(!that->mImageBuffer, "Image buffer already initialized"); + + that->mImageBufferSize = 8192; + that->mImageBuffer = (uint8_t*)malloc(that->mImageBufferSize); + that->mImageBufferUsed = 0; + + cinfo->dest->next_output_byte = that->mImageBuffer; + cinfo->dest->free_in_buffer = that->mImageBufferSize; +} + +/* static */ +boolean nsJPEGEncoderInternal::emptyOutputBuffer(jpeg_compress_struct* cinfo) { + nsJPEGEncoder* that = static_cast<nsJPEGEncoder*>(cinfo->client_data); + NS_ASSERTION(that->mImageBuffer, "No buffer to empty!"); + + // 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); + + that->mImageBufferUsed = that->mImageBufferSize; + + // expand buffer, just double size each time + uint8_t* newBuf = nullptr; + CheckedInt<uint32_t> bufSize = + CheckedInt<uint32_t>(that->mImageBufferSize) * 2; + if (bufSize.isValid()) { + that->mImageBufferSize = bufSize.value(); + newBuf = (uint8_t*)realloc(that->mImageBuffer, that->mImageBufferSize); + } + + if (!newBuf) { + // can't resize, just zero (this will keep us from writing more) + free(that->mImageBuffer); + that->mImageBuffer = nullptr; + that->mImageBufferSize = 0; + that->mImageBufferUsed = 0; + + // This seems to be the only way to do errors through the JPEG library. We + // pass an nsresult masquerading as an int, which works because the + // setjmp() caller casts it back. + longjmp(((encoder_error_mgr*)(cinfo->err))->setjmp_buffer, + static_cast<int>(NS_ERROR_OUT_OF_MEMORY)); + } + that->mImageBuffer = newBuf; + + cinfo->dest->next_output_byte = &that->mImageBuffer[that->mImageBufferUsed]; + cinfo->dest->free_in_buffer = that->mImageBufferSize - that->mImageBufferUsed; + return 1; +} + +/* static */ +void nsJPEGEncoderInternal::termDestination(jpeg_compress_struct* cinfo) { + nsJPEGEncoder* that = static_cast<nsJPEGEncoder*>(cinfo->client_data); + if (!that->mImageBuffer) { + return; + } + that->mImageBufferUsed = cinfo->dest->next_output_byte - that->mImageBuffer; + NS_ASSERTION(that->mImageBufferUsed < that->mImageBufferSize, + "JPEG library busted, got a bad image buffer size"); + that->NotifyListener(); +} + +/* static */ +void nsJPEGEncoderInternal::errorExit(jpeg_common_struct* cinfo) { + nsresult error_code; + encoder_error_mgr* err = (encoder_error_mgr*)cinfo->err; + + // Convert error to a browser error code + switch (cinfo->err->msg_code) { + case JERR_OUT_OF_MEMORY: + error_code = NS_ERROR_OUT_OF_MEMORY; + break; + default: + error_code = NS_ERROR_FAILURE; + } + + // Return control to the setjmp point. We pass an nsresult masquerading as + // an int, which works because the setjmp() caller casts it back. + longjmp(err->setjmp_buffer, static_cast<int>(error_code)); +} diff --git a/image/encoders/jpeg/nsJPEGEncoder.h b/image/encoders/jpeg/nsJPEGEncoder.h new file mode 100644 index 0000000000..e0c17ef624 --- /dev/null +++ b/image/encoders/jpeg/nsJPEGEncoder.h @@ -0,0 +1,74 @@ +/* -*- 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_jpeg_nsJPEGEncoder_h +#define mozilla_image_encoders_jpeg_nsJPEGEncoder_h + +#include "imgIEncoder.h" + +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/Attributes.h" + +#include "nsCOMPtr.h" + +struct jpeg_compress_struct; +struct jpeg_common_struct; + +#define NS_JPEGENCODER_CID \ + { \ + /* ac2bb8fe-eeeb-4572-b40f-be03932b56e0 */ \ + 0xac2bb8fe, 0xeeeb, 0x4572, { \ + 0xb4, 0x0f, 0xbe, 0x03, 0x93, 0x2b, 0x56, 0xe0 \ + } \ + } + +// Provides JPEG encoding functionality. Use InitFromData() to do the +// encoding. See that function definition for encoding options. +class nsJPEGEncoderInternal; + +class nsJPEGEncoder final : public imgIEncoder { + friend class nsJPEGEncoderInternal; + typedef mozilla::ReentrantMonitor ReentrantMonitor; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_IMGIENCODER + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsJPEGEncoder(); + + private: + ~nsJPEGEncoder(); + + protected: + void ConvertHostARGBRow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth); + void ConvertRGBARow(const uint8_t* aSrc, uint8_t* aDest, + uint32_t aPixelWidth); + + void NotifyListener(); + + 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; + + // nsJPEGEncoder 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; +}; + +#endif // mozilla_image_encoders_jpeg_nsJPEGEncoder_h diff --git a/image/encoders/moz.build b/image/encoders/moz.build new file mode 100644 index 0000000000..c73a3167eb --- /dev/null +++ b/image/encoders/moz.build @@ -0,0 +1,12 @@ +# -*- 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/. + +DIRS += [ + "ico", + "png", + "jpeg", + "bmp", +] diff --git a/image/encoders/png/moz.build b/image/encoders/png/moz.build new file mode 100644 index 0000000000..ca29199dcd --- /dev/null +++ b/image/encoders/png/moz.build @@ -0,0 +1,15 @@ +# -*- 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", +] + +FINAL_LIBRARY = "xul" diff --git a/image/encoders/png/nsPNGEncoder.cpp b/image/encoders/png/nsPNGEncoder.cpp new file mode 100644 index 0000000000..3b952ae1ac --- /dev/null +++ b/image/encoders/png/nsPNGEncoder.cpp @@ -0,0 +1,732 @@ +/* -*- 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" + +using namespace mozilla; + +static LazyLogModule sPNGEncoderLog("PNGEncoder"); + +NS_IMPL_ISUPPORTS(nsPNGEncoder, imgIEncoder, nsIInputStream, + nsIAsyncInputStream) + +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 + + // 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, 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; + } + + // 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; + 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, + &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, PNG_FILTER_VALUE_NONE); +#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 = MakeUnique<uint8_t[]>(aWidth * 4); + 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 = MakeUnique<uint8_t[]>(aWidth * 4); + 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, + 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; + } + + // 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::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..bb1df4345c --- /dev/null +++ b/image/encoders/png/nsPNGEncoder.h @@ -0,0 +1,78 @@ +/* -*- 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, 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; +}; +#endif // mozilla_image_encoders_png_nsPNGEncoder_h |