blob: cb37d138bd29caa3860fdbdb00875b763aa4a2d3 [file] [log] [blame]
/*
* Copyright (C) 2006 Apple Computer, Inc.
* Copyright (C) Research In Motion Limited 2009-2010. All rights reserved.
*
* Portions are Copyright (C) 2001 mozilla.org
*
* Other contributors:
* Stuart Parmenter <stuart@mozilla.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Alternatively, the contents of this file may be used under the terms
* of either the Mozilla Public License Version 1.1, found at
* http://www.mozilla.org/MPL/ (the "MPL") or the GNU General Public
* License Version 2.0, found at http://www.fsf.org/copyleft/gpl.html
* (the "GPL"), in which case the provisions of the MPL or the GPL are
* applicable instead of those above. If you wish to allow use of your
* version of this file only under the terms of one of those two
* licenses (the MPL or the GPL) and not to allow others to use your
* version of this file under the LGPL, indicate your decision by
* deletingthe provisions above and replace them with the notice and
* other provisions required by the MPL or the GPL, as the case may be.
* If you do not delete the provisions above, a recipient may use your
* version of this file under any of the LGPL, the MPL or the GPL.
*/
#include "platform/image-decoders/png/PNGImageDecoder.h"
namespace blink {
PNGImageDecoder::PNGImageDecoder(AlphaOption alphaOption,
const ColorBehavior& colorBehavior,
size_t maxDecodedBytes,
size_t offset)
: ImageDecoder(alphaOption, colorBehavior, maxDecodedBytes),
m_offset(offset),
m_currentFrame(0),
// It would be logical to default to cAnimationNone, but BitmapImage uses
// that as a signal to never check again, meaning the actual count will
// never be respected.
m_repetitionCount(cAnimationLoopOnce),
m_hasAlphaChannel(false),
m_currentBufferSawAlpha(false) {}
PNGImageDecoder::~PNGImageDecoder() {}
bool PNGImageDecoder::setFailed() {
m_reader.reset();
return ImageDecoder::setFailed();
}
size_t PNGImageDecoder::decodeFrameCount() {
parse(ParseQuery::MetaData);
return failed() ? m_frameBufferCache.size() : m_reader->frameCount();
}
void PNGImageDecoder::decode(size_t index) {
parse(ParseQuery::MetaData);
if (failed())
return;
updateAggressivePurging(index);
Vector<size_t> framesToDecode = findFramesToDecode(index);
for (auto i = framesToDecode.rbegin(); i != framesToDecode.rend(); i++) {
m_currentFrame = *i;
if (!m_reader->decode(*m_data, *i)) {
setFailed();
return;
}
// If this returns false, we need more data to continue decoding.
if (!postDecodeProcessing(*i))
break;
}
// It is also a fatal error if all data is received and we have decoded all
// frames available but the file is truncated.
if (index >= m_frameBufferCache.size() - 1 && isAllDataReceived() &&
m_reader && !m_reader->parseCompleted())
setFailed();
}
void PNGImageDecoder::parse(ParseQuery query) {
if (failed() || (m_reader && m_reader->parseCompleted()))
return;
if (!m_reader)
m_reader = WTF::makeUnique<PNGImageReader>(this, m_offset);
if (!m_reader->parse(*m_data, query))
setFailed();
}
void PNGImageDecoder::clearFrameBuffer(size_t index) {
if (m_reader)
m_reader->clearDecodeState(index);
ImageDecoder::clearFrameBuffer(index);
}
bool PNGImageDecoder::canReusePreviousFrameBuffer(size_t index) const {
DCHECK(index < m_frameBufferCache.size());
return m_frameBufferCache[index].getDisposalMethod() !=
ImageFrame::DisposeOverwritePrevious;
}
void PNGImageDecoder::setRepetitionCount(int repetitionCount) {
m_repetitionCount = repetitionCount;
}
int PNGImageDecoder::repetitionCount() const {
return failed() ? cAnimationLoopOnce : m_repetitionCount;
}
void PNGImageDecoder::initializeNewFrame(size_t index) {
const PNGImageReader::FrameInfo& frameInfo = m_reader->frameInfo(index);
ImageFrame& buffer = m_frameBufferCache[index];
DCHECK(IntRect(IntPoint(), size()).contains(frameInfo.frameRect));
buffer.setOriginalFrameRect(frameInfo.frameRect);
buffer.setDuration(frameInfo.duration);
buffer.setDisposalMethod(frameInfo.disposalMethod);
buffer.setAlphaBlendSource(frameInfo.alphaBlend);
size_t previousFrameIndex = findRequiredPreviousFrame(index, false);
buffer.setRequiredPreviousFrameIndex(previousFrameIndex);
}
inline sk_sp<SkColorSpace> readColorSpace(png_structp png, png_infop info) {
if (png_get_valid(png, info, PNG_INFO_sRGB))
return SkColorSpace::MakeSRGB();
png_charp name;
int compression;
png_bytep profile;
png_uint_32 length;
if (png_get_iCCP(png, info, &name, &compression, &profile, &length))
return SkColorSpace::MakeICC(profile, length);
png_fixed_point chrm[8];
if (!png_get_cHRM_fixed(png, info, &chrm[0], &chrm[1], &chrm[2], &chrm[3],
&chrm[4], &chrm[5], &chrm[6], &chrm[7]))
return nullptr;
png_fixed_point inverseGamma;
if (!png_get_gAMA_fixed(png, info, &inverseGamma))
return nullptr;
// cHRM and gAMA tags are both present. The PNG spec states that cHRM is
// valid even without gAMA but we cannot apply the cHRM without guessing
// a gAMA. Color correction is not a guessing game: match the behavior
// of Safari and Firefox instead (compat).
struct pngFixedToFloat {
explicit pngFixedToFloat(png_fixed_point value)
: floatValue(.00001f * value) {}
operator float() { return floatValue; }
float floatValue;
};
SkColorSpacePrimaries primaries;
primaries.fRX = pngFixedToFloat(chrm[2]);
primaries.fRY = pngFixedToFloat(chrm[3]);
primaries.fGX = pngFixedToFloat(chrm[4]);
primaries.fGY = pngFixedToFloat(chrm[5]);
primaries.fBX = pngFixedToFloat(chrm[6]);
primaries.fBY = pngFixedToFloat(chrm[7]);
primaries.fWX = pngFixedToFloat(chrm[0]);
primaries.fWY = pngFixedToFloat(chrm[1]);
SkMatrix44 toXYZD50(SkMatrix44::kUninitialized_Constructor);
if (!primaries.toXYZD50(&toXYZD50))
return nullptr;
SkColorSpaceTransferFn fn;
fn.fG = 1.0f / pngFixedToFloat(inverseGamma);
fn.fA = 1.0f;
fn.fB = fn.fC = fn.fD = fn.fE = fn.fF = 0.0f;
return SkColorSpace::MakeRGB(fn, toXYZD50);
}
void PNGImageDecoder::headerAvailable() {
png_structp png = m_reader->pngPtr();
png_infop info = m_reader->infoPtr();
png_uint_32 width, height;
int bitDepth, colorType, interlaceType, compressionType;
png_get_IHDR(png, info, &width, &height, &bitDepth, &colorType,
&interlaceType, &compressionType, nullptr);
// The options we set here match what Mozilla does.
// Expand to ensure we use 24-bit for RGB and 32-bit for RGBA.
if (colorType == PNG_COLOR_TYPE_PALETTE ||
(colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8))
png_set_expand(png);
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_expand(png);
if (bitDepth == 16)
png_set_strip_16(png);
if (colorType == PNG_COLOR_TYPE_GRAY ||
colorType == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png);
// Only set the size and the color space of the image once since non-first
// frames also use this method: there is no per-frame color space, and the
// image size is determined from the header width and height.
if (!isDecodedSizeAvailable()) {
// Protect against large PNGs. See http://bugzil.la/251381 for more details.
const unsigned long maxPNGSize = 1000000UL;
if (width > maxPNGSize || height > maxPNGSize) {
longjmp(JMPBUF(png), 1);
return;
}
// Set the image size now that the image header is available.
if (!setSize(width, height)) {
longjmp(JMPBUF(png), 1);
return;
}
if ((colorType & PNG_COLOR_MASK_COLOR) && !ignoresColorSpace()) {
// We only support color profiles for color PALETTE and RGB[A] PNG.
// TODO(msarret): Add GRAY profile support, block CYMK?
if (sk_sp<SkColorSpace> colorSpace = readColorSpace(png, info))
setEmbeddedColorSpace(colorSpace);
}
}
if (!hasEmbeddedColorSpace()) {
const double inverseGamma = 0.45455;
const double defaultGamma = 2.2;
double gamma;
if (!ignoresColorSpace() && png_get_gAMA(png, info, &gamma)) {
const double maxGamma = 21474.83;
if ((gamma <= 0.0) || (gamma > maxGamma)) {
gamma = inverseGamma;
png_set_gAMA(png, info, gamma);
}
png_set_gamma(png, defaultGamma, gamma);
} else {
png_set_gamma(png, defaultGamma, inverseGamma);
}
}
DCHECK(isDecodedSizeAvailable());
// Tell libpng to send us rows for interlaced pngs.
if (interlaceType == PNG_INTERLACE_ADAM7)
png_set_interlace_handling(png);
// Update our info now (so we can get color channel info).
png_read_update_info(png, info);
int channels = png_get_channels(png, info);
DCHECK(channels == 3 || channels == 4);
m_hasAlphaChannel = (channels == 4);
}
void PNGImageDecoder::rowAvailable(unsigned char* rowBuffer,
unsigned rowIndex,
int) {
if (m_currentFrame >= m_frameBufferCache.size())
return;
ImageFrame& buffer = m_frameBufferCache[m_currentFrame];
if (buffer.getStatus() == ImageFrame::FrameEmpty) {
png_structp png = m_reader->pngPtr();
if (!initFrameBuffer(m_currentFrame)) {
longjmp(JMPBUF(png), 1);
return;
}
DCHECK_EQ(ImageFrame::FramePartial, buffer.getStatus());
if (PNG_INTERLACE_ADAM7 ==
png_get_interlace_type(png, m_reader->infoPtr())) {
unsigned colorChannels = m_hasAlphaChannel ? 4 : 3;
m_reader->createInterlaceBuffer(colorChannels * size().area());
if (!m_reader->interlaceBuffer()) {
longjmp(JMPBUF(png), 1);
return;
}
}
m_currentBufferSawAlpha = false;
}
const IntRect& frameRect = buffer.originalFrameRect();
DCHECK(IntRect(IntPoint(), size()).contains(frameRect));
/* libpng comments (here to explain what follows).
*
* this function is called for every row in the image. If the
* image is interlacing, and you turned on the interlace handler,
* this function will be called for every row in every pass.
* Some of these rows will not be changed from the previous pass.
* When the row is not changed, the new_row variable will be NULL.
* The rows and passes are called in order, so you don't really
* need the row_num and pass, but I'm supplying them because it
* may make your life easier.
*/
// Nothing to do if the row is unchanged, or the row is outside the image
// bounds. In the case that a frame presents more data than the indicated
// frame size, ignore the extra rows and use the frame size as the source
// of truth. libpng can send extra rows: ignore them too, this to prevent
// memory writes outside of the image bounds (security).
if (!rowBuffer)
return;
DCHECK_GT(frameRect.height(), 0);
if (rowIndex >= static_cast<unsigned>(frameRect.height()))
return;
int y = rowIndex + frameRect.y();
if (y < 0)
return;
DCHECK_LT(y, size().height());
/* libpng comments (continued).
*
* For the non-NULL rows of interlaced images, you must call
* png_progressive_combine_row() passing in the row and the
* old row. You can call this function for NULL rows (it will
* just return) and for non-interlaced images (it just does the
* memcpy for you) if it will make the code easier. Thus, you
* can just do this for all cases:
*
* png_progressive_combine_row(png_ptr, old_row, new_row);
*
* where old_row is what was displayed for previous rows. Note
* that the first pass (pass == 0 really) will completely cover
* the old row, so the rows do not have to be initialized. After
* the first pass (and only for interlaced images), you will have
* to pass the current row, and the function will combine the
* old row and the new row.
*/
bool hasAlpha = m_hasAlphaChannel;
png_bytep row = rowBuffer;
if (png_bytep interlaceBuffer = m_reader->interlaceBuffer()) {
unsigned colorChannels = hasAlpha ? 4 : 3;
row = interlaceBuffer + (rowIndex * colorChannels * size().width());
png_progressive_combine_row(m_reader->pngPtr(), row, rowBuffer);
}
// Write the decoded row pixels to the frame buffer. The repetitive
// form of the row write loops is for speed.
ImageFrame::PixelData* const dstRow = buffer.getAddr(frameRect.x(), y);
int width = frameRect.width();
png_bytep srcPtr = row;
if (hasAlpha) {
// Here we apply the color space transformation to the dst space.
// It does not really make sense to transform to a gamma-encoded
// space and then immediately after, perform a linear premultiply.
// Ideally we would pass kPremul_SkAlphaType to xform->apply(),
// instructing SkColorSpaceXform to perform the linear premultiply
// while the pixels are a linear space.
// We cannot do this because when we apply the gamma encoding after
// the premultiply, we will very likely end up with valid pixels
// where R, G, and/or B are greater than A. The legacy drawing
// pipeline does not know how to handle this.
if (SkColorSpaceXform* xform = colorTransform()) {
SkColorSpaceXform::ColorFormat colorFormat =
SkColorSpaceXform::kRGBA_8888_ColorFormat;
xform->apply(colorFormat, dstRow, colorFormat, srcPtr, size().width(),
kUnpremul_SkAlphaType);
srcPtr = png_bytep(dstRow);
}
unsigned alphaMask = 255;
if (m_frameBufferCache[m_currentFrame].getAlphaBlendSource() ==
ImageFrame::BlendAtopBgcolor) {
if (buffer.premultiplyAlpha()) {
for (auto *dstPixel = dstRow; dstPixel < dstRow + width;
dstPixel++, srcPtr += 4) {
buffer.setRGBAPremultiply(dstPixel, srcPtr[0], srcPtr[1], srcPtr[2],
srcPtr[3]);
alphaMask &= srcPtr[3];
}
} else {
for (auto *dstPixel = dstRow; dstPixel < dstRow + width;
dstPixel++, srcPtr += 4) {
buffer.setRGBARaw(dstPixel, srcPtr[0], srcPtr[1], srcPtr[2],
srcPtr[3]);
alphaMask &= srcPtr[3];
}
}
} else {
// Now, the blend method is ImageFrame::BlendAtopPreviousFrame. Since the
// frame data of the previous frame is copied at initFrameBuffer, we can
// blend the pixel of this frame, stored in |srcPtr|, over the previous
// pixel stored in |dstPixel|.
if (buffer.premultiplyAlpha()) {
for (auto *dstPixel = dstRow; dstPixel < dstRow + width;
dstPixel++, srcPtr += 4) {
buffer.blendRGBAPremultiplied(dstPixel, srcPtr[0], srcPtr[1],
srcPtr[2], srcPtr[3]);
alphaMask &= srcPtr[3];
}
} else {
for (auto *dstPixel = dstRow; dstPixel < dstRow + width;
dstPixel++, srcPtr += 4) {
buffer.blendRGBARaw(dstPixel, srcPtr[0], srcPtr[1], srcPtr[2],
srcPtr[3]);
alphaMask &= srcPtr[3];
}
}
}
if (alphaMask != 255)
m_currentBufferSawAlpha = true;
} else {
for (auto *dstPixel = dstRow; dstPixel < dstRow + width;
srcPtr += 3, ++dstPixel) {
buffer.setRGBARaw(dstPixel, srcPtr[0], srcPtr[1], srcPtr[2], 255);
}
// We'll apply the color space xform to opaque pixels after they have been
// written to the ImageFrame, purely because SkColorSpaceXform supports
// RGBA (and not RGB).
if (SkColorSpaceXform* xform = colorTransform()) {
xform->apply(xformColorFormat(), dstRow, xformColorFormat(), dstRow,
size().width(), kOpaque_SkAlphaType);
}
}
buffer.setPixelsChanged(true);
}
void PNGImageDecoder::frameComplete() {
if (m_currentFrame >= m_frameBufferCache.size())
return;
if (m_reader->interlaceBuffer())
m_reader->clearInterlaceBuffer();
ImageFrame& buffer = m_frameBufferCache[m_currentFrame];
if (buffer.getStatus() == ImageFrame::FrameEmpty) {
longjmp(JMPBUF(m_reader->pngPtr()), 1);
return;
}
if (!m_currentBufferSawAlpha)
correctAlphaWhenFrameBufferSawNoAlpha(m_currentFrame);
buffer.setStatus(ImageFrame::FrameComplete);
}
bool PNGImageDecoder::frameIsCompleteAtIndex(size_t index) const {
if (!isDecodedSizeAvailable())
return false;
DCHECK(!failed() && m_reader);
// For non-animated images, return whether the status of the frame is
// ImageFrame::FrameComplete with ImageDecoder::frameIsCompleteAtIndex.
// This matches the behavior of WEBPImageDecoder.
if (m_reader->parseCompleted() && m_reader->frameCount() == 1)
return ImageDecoder::frameIsCompleteAtIndex(index);
return m_reader->frameIsReceivedAtIndex(index);
}
float PNGImageDecoder::frameDurationAtIndex(size_t index) const {
if (index < m_frameBufferCache.size())
return m_frameBufferCache[index].duration();
return 0;
}
} // namespace blink