/* vim:set tw=80 expandtab softtabstop=2 ts=2 sw=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/. */ /* This is a Cross-Platform ICO Decoder, which should work everywhere, including * Big-Endian machines like the PowerPC. */ #include "nsICODecoder.h" #include #include #include "RasterImage.h" #include "mozilla/EndianUtils.h" #include "mozilla/gfx/Swizzle.h" using namespace mozilla::gfx; namespace mozilla { namespace image { // Constants. static const uint32_t ICOHEADERSIZE = 6; static const uint32_t BITMAPINFOSIZE = bmp::InfoHeaderLength::WIN_ICO; // ---------------------------------------- // Actual Data Processing // ---------------------------------------- // Obtains the number of colors from the bits per pixel uint16_t nsICODecoder::GetNumColors() { uint16_t numColors = 0; if (mBPP <= 8) { switch (mBPP) { case 1: numColors = 2; break; case 4: numColors = 16; break; case 8: numColors = 256; break; default: numColors = (uint16_t)-1; } } return numColors; } nsICODecoder::nsICODecoder(RasterImage* aImage) : Decoder(aImage), mLexer(Transition::To(ICOState::HEADER, ICOHEADERSIZE), Transition::TerminateSuccess()), mDirEntry(nullptr), mNumIcons(0), mCurrIcon(0), mBPP(0), mMaskRowSize(0), mCurrMaskLine(0), mIsCursor(false), mHasMaskAlpha(false) {} nsresult nsICODecoder::FinishInternal() { // We shouldn't be called in error cases MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!"); return GetFinalStateFromContainedDecoder(); } nsresult nsICODecoder::FinishWithErrorInternal() { // No need to assert !mInFrame here because this condition is enforced by // mContainedDecoder. return GetFinalStateFromContainedDecoder(); } nsresult nsICODecoder::GetFinalStateFromContainedDecoder() { if (!mContainedDecoder) { return NS_OK; } // Let the contained decoder finish up if necessary. FlushContainedDecoder(); // Make our state the same as the state of the contained decoder. mDecodeDone = mContainedDecoder->GetDecodeDone(); mProgress |= mContainedDecoder->TakeProgress(); mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect()); mCurrentFrame = mContainedDecoder->GetCurrentFrameRef(); // Finalize the frame which we deferred to ensure we could modify the final // result (e.g. to apply the BMP mask). MOZ_ASSERT(!mContainedDecoder->GetFinalizeFrames()); if (mCurrentFrame) { mCurrentFrame->FinalizeSurface(); } // Propagate errors. nsresult rv = HasError() || mContainedDecoder->HasError() ? NS_ERROR_FAILURE : NS_OK; MOZ_ASSERT(NS_FAILED(rv) || !mCurrentFrame || mCurrentFrame->IsFinished()); return rv; } LexerTransition nsICODecoder::ReadHeader(const char* aData) { // If the third byte is 1, this is an icon. If 2, a cursor. if ((aData[2] != 1) && (aData[2] != 2)) { return Transition::TerminateFailure(); } mIsCursor = (aData[2] == 2); // The fifth and sixth bytes specify the number of resources in the file. mNumIcons = LittleEndian::readUint16(aData + 4); if (mNumIcons == 0) { return Transition::TerminateSuccess(); // Nothing to do. } // Downscale-during-decode can end up decoding different resources in the ICO // file depending on the target size. Since the resources are not necessarily // scaled versions of the same image, some may be transparent and some may not // be. We could be precise about transparency if we decoded the metadata of // every resource, but for now we don't and it's safest to assume that // transparency could be present. PostHasTransparency(); return Transition::To(ICOState::DIR_ENTRY, ICODIRENTRYSIZE); } size_t nsICODecoder::FirstResourceOffset() const { MOZ_ASSERT(mNumIcons > 0, "Calling FirstResourceOffset before processing header"); // The first resource starts right after the directory, which starts right // after the ICO header. return ICOHEADERSIZE + mNumIcons * ICODIRENTRYSIZE; } LexerTransition nsICODecoder::ReadDirEntry(const char* aData) { mCurrIcon++; // Ensure the resource has an offset past the ICO headers. uint32_t offset = LittleEndian::readUint32(aData + 12); if (offset >= FirstResourceOffset()) { // Read the directory entry. IconDirEntryEx e; e.mWidth = aData[0]; e.mHeight = aData[1]; e.mColorCount = aData[2]; e.mReserved = aData[3]; e.mPlanes = LittleEndian::readUint16(aData + 4); e.mBitCount = LittleEndian::readUint16(aData + 6); e.mBytesInRes = LittleEndian::readUint32(aData + 8); e.mImageOffset = offset; e.mSize = IntSize(e.mWidth, e.mHeight); // Only accept entries with sufficient resource data to actually contain // some image data. if (e.mBytesInRes > BITMAPINFOSIZE) { if (e.mWidth == 0 || e.mHeight == 0) { mUnsizedDirEntries.AppendElement(e); } else { mDirEntries.AppendElement(e); } } } if (mCurrIcon == mNumIcons) { if (mUnsizedDirEntries.IsEmpty()) { return Transition::To(ICOState::FINISHED_DIR_ENTRY, 0); } return Transition::To(ICOState::ITERATE_UNSIZED_DIR_ENTRY, 0); } return Transition::To(ICOState::DIR_ENTRY, ICODIRENTRYSIZE); } LexerTransition nsICODecoder::IterateUnsizedDirEntry() { MOZ_ASSERT(!mUnsizedDirEntries.IsEmpty()); if (!mDirEntry) { // The first time we are here, there is no entry selected. We must prepare a // new iterator for the contained decoder to advance as it wills. Cloning at // this point ensures it will begin at the end of the dir entries. mReturnIterator = mLexer.Clone(*mIterator, SIZE_MAX); if (mReturnIterator.isNothing()) { // If we cannot read further than this point, then there is no resource // data to read. return Transition::TerminateFailure(); } } else { // We have already selected an entry which means a metadata decoder has // finished. Verify the size is valid and if so, add to the discovered // resources. if (mDirEntry->mSize.width > 0 && mDirEntry->mSize.height > 0) { mDirEntries.AppendElement(*mDirEntry); } // Remove the entry from the unsized list either way. mDirEntry = nullptr; mUnsizedDirEntries.RemoveElementAt(0); // Our iterator is at an unknown point, so reset it to the point that we // saved. mIterator = mLexer.Clone(*mReturnIterator, SIZE_MAX); if (mIterator.isNothing()) { MOZ_ASSERT_UNREACHABLE("Cannot re-clone return iterator"); return Transition::TerminateFailure(); } } // There are no more unsized entries, so we can finally decide which entry to // select for decoding. if (mUnsizedDirEntries.IsEmpty()) { mReturnIterator.reset(); return Transition::To(ICOState::FINISHED_DIR_ENTRY, 0); } // Move to the resource data to start metadata decoding. mDirEntry = &mUnsizedDirEntries[0]; size_t offsetToResource = mDirEntry->mImageOffset - FirstResourceOffset(); return Transition::ToUnbuffered(ICOState::FOUND_RESOURCE, ICOState::SKIP_TO_RESOURCE, offsetToResource); } LexerTransition nsICODecoder::FinishDirEntry() { MOZ_ASSERT(!mDirEntry); if (mDirEntries.IsEmpty()) { return Transition::TerminateFailure(); } // If an explicit output size was specified, we'll try to select the resource // that matches it best below. const Maybe desiredSize = ExplicitOutputSize(); // Determine the biggest resource. We always use the biggest resource for the // intrinsic size, and if we don't have a specific desired size, we select it // as the best resource as well. int32_t bestDelta = INT32_MIN; IconDirEntryEx* biggestEntry = nullptr; for (size_t i = 0; i < mDirEntries.Length(); ++i) { IconDirEntryEx& e = mDirEntries[i]; mImageMetadata.AddNativeSize(e.mSize); if (!biggestEntry || (e.mBitCount >= biggestEntry->mBitCount && e.mSize.width * e.mSize.height >= biggestEntry->mSize.width * biggestEntry->mSize.height)) { biggestEntry = &e; if (!desiredSize) { mDirEntry = &e; } } if (desiredSize) { // Calculate the delta between this resource's size and the desired size, // so we can see if it is better than our current-best option. In the // case of several equally-good resources, we use the last one. "Better" // in this case is determined by |delta|, a measure of the difference in // size between the entry we've found and the desired size. We will choose // the smallest resource that is greater than or equal to the desired size // (i.e. we assume it's better to downscale a larger icon than to upscale // a smaller one). int32_t delta = std::min(e.mSize.width - desiredSize->width, e.mSize.height - desiredSize->height); if (!mDirEntry || (e.mBitCount >= mDirEntry->mBitCount && ((bestDelta < 0 && delta >= bestDelta) || (delta >= 0 && delta <= bestDelta)))) { mDirEntry = &e; bestDelta = delta; } } } MOZ_ASSERT(mDirEntry); MOZ_ASSERT(biggestEntry); // If this is a cursor, set the hotspot. We use the hotspot from the biggest // resource since we also use that resource for the intrinsic size. if (mIsCursor) { mImageMetadata.SetHotspot(biggestEntry->mXHotspot, biggestEntry->mYHotspot); } // We always report the biggest resource's size as the intrinsic size; this // is necessary for downscale-during-decode to work since we won't even // attempt to *upscale* while decoding. PostSize(biggestEntry->mSize.width, biggestEntry->mSize.height); if (HasError()) { return Transition::TerminateFailure(); } if (IsMetadataDecode()) { return Transition::TerminateSuccess(); } if (mDirEntry->mSize == OutputSize()) { // If the resource we selected matches the output size perfectly, we don't // need to do any downscaling. MOZ_ASSERT_IF(desiredSize, mDirEntry->mSize == *desiredSize); MOZ_ASSERT_IF(!desiredSize, mDirEntry->mSize == Size()); } else if (OutputSize().width < mDirEntry->mSize.width || OutputSize().height < mDirEntry->mSize.height) { // Create a downscaler if we need to downscale. // // TODO(aosmond): This is the last user of Downscaler. We should switch this // to SurfacePipe as well so we can remove the code from tree. mDownscaler.emplace(OutputSize()); } size_t offsetToResource = mDirEntry->mImageOffset - FirstResourceOffset(); return Transition::ToUnbuffered(ICOState::FOUND_RESOURCE, ICOState::SKIP_TO_RESOURCE, offsetToResource); } LexerTransition nsICODecoder::SniffResource(const char* aData) { MOZ_ASSERT(mDirEntry); // We have BITMAPINFOSIZE bytes buffered at this point. We know an embedded // BMP will have at least that many bytes by definition. We can also infer // that any valid embedded PNG will contain that many bytes as well because: // BITMAPINFOSIZE // < // signature (8 bytes) + // IHDR (12 bytes header + 13 bytes data) // IDAT (12 bytes header) // We use the first PNGSIGNATURESIZE bytes to determine whether this resource // is a PNG or a BMP. bool isPNG = !memcmp(aData, nsPNGDecoder::pngSignatureBytes, PNGSIGNATURESIZE); if (isPNG) { if (mDirEntry->mBytesInRes <= BITMAPINFOSIZE) { return Transition::TerminateFailure(); } // Prepare a new iterator for the contained decoder to advance as it wills. // Cloning at the point ensures it will begin at the resource offset. Maybe containedIterator = mLexer.Clone(*mIterator, mDirEntry->mBytesInRes); if (containedIterator.isNothing()) { return Transition::TerminateFailure(); } // Create a PNG decoder which will do the rest of the work for us. bool metadataDecode = mReturnIterator.isSome(); Maybe expectedSize = metadataDecode ? Nothing() : Some(mDirEntry->mSize); mContainedDecoder = DecoderFactory::CreateDecoderForICOResource( DecoderType::PNG, std::move(containedIterator.ref()), WrapNotNull(this), metadataDecode, expectedSize); // Read in the rest of the PNG unbuffered. size_t toRead = mDirEntry->mBytesInRes - BITMAPINFOSIZE; return Transition::ToUnbuffered(ICOState::FINISHED_RESOURCE, ICOState::READ_RESOURCE, toRead); } // Make sure we have a sane size for the bitmap information header. int32_t bihSize = LittleEndian::readUint32(aData); if (bihSize != static_cast(BITMAPINFOSIZE)) { return Transition::TerminateFailure(); } // Read in the rest of the bitmap information header. return ReadBIH(aData); } LexerTransition nsICODecoder::ReadResource() { if (!FlushContainedDecoder()) { return Transition::TerminateFailure(); } return Transition::ContinueUnbuffered(ICOState::READ_RESOURCE); } LexerTransition nsICODecoder::ReadBIH(const char* aData) { MOZ_ASSERT(mDirEntry); // Extract the BPP from the BIH header; it should be trusted over the one // we have from the ICO header which is usually set to 0. mBPP = LittleEndian::readUint16(aData + 14); // Check to make sure we have valid color settings. uint16_t numColors = GetNumColors(); if (numColors == uint16_t(-1)) { return Transition::TerminateFailure(); } // The color table is present only if BPP is <= 8. MOZ_ASSERT_IF(mBPP > 8, numColors == 0); // The ICO format when containing a BMP does not include the 14 byte // bitmap file header. So we create the BMP decoder via the constructor that // tells it to skip this, and pass in the required data (dataOffset) that // would have been present in the header. uint32_t dataOffset = bmp::FILE_HEADER_LENGTH + BITMAPINFOSIZE + 4 * numColors; // Prepare a new iterator for the contained decoder to advance as it wills. // Cloning at the point ensures it will begin at the resource offset. Maybe containedIterator = mLexer.Clone(*mIterator, mDirEntry->mBytesInRes); if (containedIterator.isNothing()) { return Transition::TerminateFailure(); } // Create a BMP decoder which will do most of the work for us; the exception // is the AND mask, which isn't present in standalone BMPs. bool metadataDecode = mReturnIterator.isSome(); Maybe expectedSize = metadataDecode ? Nothing() : Some(mDirEntry->mSize); mContainedDecoder = DecoderFactory::CreateDecoderForICOResource( DecoderType::BMP, std::move(containedIterator.ref()), WrapNotNull(this), metadataDecode, expectedSize, Some(dataOffset)); RefPtr bmpDecoder = static_cast(mContainedDecoder.get()); // Ensure the decoder has parsed at least the BMP's bitmap info header. if (!FlushContainedDecoder()) { return Transition::TerminateFailure(); } // If this is a metadata decode, FinishResource will any necessary checks. if (mContainedDecoder->IsMetadataDecode()) { return Transition::To(ICOState::FINISHED_RESOURCE, 0); } // Do we have an AND mask on this BMP? If so, we need to read it after we read // the BMP data itself. uint32_t bmpDataLength = bmpDecoder->GetCompressedImageSize() + 4 * numColors; bool hasANDMask = (BITMAPINFOSIZE + bmpDataLength) < mDirEntry->mBytesInRes; ICOState afterBMPState = hasANDMask ? ICOState::PREPARE_FOR_MASK : ICOState::FINISHED_RESOURCE; // Read in the rest of the BMP unbuffered. return Transition::ToUnbuffered(afterBMPState, ICOState::READ_RESOURCE, bmpDataLength); } LexerTransition nsICODecoder::PrepareForMask() { MOZ_ASSERT(mDirEntry); MOZ_ASSERT(mContainedDecoder->GetDecodeDone()); // We have received all of the data required by the BMP decoder so flushing // here guarantees the decode has finished. if (!FlushContainedDecoder()) { return Transition::TerminateFailure(); } MOZ_ASSERT(mContainedDecoder->GetDecodeDone()); RefPtr bmpDecoder = static_cast(mContainedDecoder.get()); uint16_t numColors = GetNumColors(); MOZ_ASSERT(numColors != uint16_t(-1)); // Determine the length of the AND mask. uint32_t bmpLengthWithHeader = BITMAPINFOSIZE + bmpDecoder->GetCompressedImageSize() + 4 * numColors; MOZ_ASSERT(bmpLengthWithHeader < mDirEntry->mBytesInRes); uint32_t maskLength = mDirEntry->mBytesInRes - bmpLengthWithHeader; // If the BMP provides its own transparency, we ignore the AND mask. if (bmpDecoder->HasTransparency()) { return Transition::ToUnbuffered(ICOState::FINISHED_RESOURCE, ICOState::SKIP_MASK, maskLength); } // Compute the row size for the mask. mMaskRowSize = ((mDirEntry->mSize.width + 31) / 32) * 4; // + 31 to round up // If the expected size of the AND mask is larger than its actual size, then // we must have a truncated (and therefore corrupt) AND mask. uint32_t expectedLength = mMaskRowSize * mDirEntry->mSize.height; if (maskLength < expectedLength) { return Transition::TerminateFailure(); } // If we're downscaling, the mask is the wrong size for the surface we've // produced, so we need to downscale the mask into a temporary buffer and then // combine the mask's alpha values with the color values from the image. if (mDownscaler) { MOZ_ASSERT(bmpDecoder->GetImageDataLength() == mDownscaler->TargetSize().width * mDownscaler->TargetSize().height * sizeof(uint32_t)); mMaskBuffer = MakeUnique(bmpDecoder->GetImageDataLength()); nsresult rv = mDownscaler->BeginFrame(mDirEntry->mSize, Nothing(), mMaskBuffer.get(), /* aHasAlpha = */ true, /* aFlipVertically = */ true); if (NS_FAILED(rv)) { return Transition::TerminateFailure(); } } mCurrMaskLine = mDirEntry->mSize.height; return Transition::To(ICOState::READ_MASK_ROW, mMaskRowSize); } LexerTransition nsICODecoder::ReadMaskRow(const char* aData) { MOZ_ASSERT(mDirEntry); mCurrMaskLine--; uint8_t sawTransparency = 0; // Get the mask row we're reading. const uint8_t* mask = reinterpret_cast(aData); const uint8_t* maskRowEnd = mask + mMaskRowSize; // Get the corresponding row of the mask buffer (if we're downscaling) or the // decoded image data (if we're not). uint32_t* decoded = nullptr; if (mDownscaler) { // Initialize the row to all white and fully opaque. memset(mDownscaler->RowBuffer(), 0xFF, mDirEntry->mSize.width * sizeof(uint32_t)); decoded = reinterpret_cast(mDownscaler->RowBuffer()); } else { RefPtr bmpDecoder = static_cast(mContainedDecoder.get()); uint32_t* imageData = bmpDecoder->GetImageData(); if (!imageData) { return Transition::TerminateFailure(); } decoded = imageData + mCurrMaskLine * mDirEntry->mSize.width; } MOZ_ASSERT(decoded); uint32_t* decodedRowEnd = decoded + mDirEntry->mSize.width; // Iterate simultaneously through the AND mask and the image data. while (mask < maskRowEnd) { uint8_t idx = *mask++; sawTransparency |= idx; for (uint8_t bit = 0x80; bit && decoded < decodedRowEnd; bit >>= 1) { // Clear pixel completely for transparency. if (idx & bit) { *decoded = 0; } decoded++; } } if (mDownscaler) { mDownscaler->CommitRow(); } // If any bits are set in sawTransparency, then we know at least one pixel was // transparent. if (sawTransparency) { mHasMaskAlpha = true; } if (mCurrMaskLine == 0) { return Transition::To(ICOState::FINISH_MASK, 0); } return Transition::To(ICOState::READ_MASK_ROW, mMaskRowSize); } LexerTransition nsICODecoder::FinishMask() { // If we're downscaling, we now have the appropriate alpha values in // mMaskBuffer. We just need to transfer them to the image. if (mDownscaler) { // Retrieve the image data. RefPtr bmpDecoder = static_cast(mContainedDecoder.get()); uint8_t* imageData = reinterpret_cast(bmpDecoder->GetImageData()); if (!imageData) { return Transition::TerminateFailure(); } // Iterate through the alpha values, copying from mask to image. MOZ_ASSERT(mMaskBuffer); MOZ_ASSERT(bmpDecoder->GetImageDataLength() > 0); for (size_t i = 3; i < bmpDecoder->GetImageDataLength(); i += 4) { imageData[i] = mMaskBuffer[i]; } int32_t stride = mDownscaler->TargetSize().width * sizeof(uint32_t); DebugOnly ret = // We know the format is OS_RGBA because we always assume bmp's inside // ico's are transparent. PremultiplyData(imageData, stride, SurfaceFormat::OS_RGBA, imageData, stride, SurfaceFormat::OS_RGBA, mDownscaler->TargetSize()); MOZ_ASSERT(ret); } return Transition::To(ICOState::FINISHED_RESOURCE, 0); } LexerTransition nsICODecoder::FinishResource() { MOZ_ASSERT(mDirEntry); // We have received all of the data required by the PNG/BMP decoder so // flushing here guarantees the decode has finished. if (!FlushContainedDecoder()) { return Transition::TerminateFailure(); } MOZ_ASSERT(mContainedDecoder->GetDecodeDone()); // If it is a metadata decode, all we were trying to get was the size // information missing from the dir entry. if (mContainedDecoder->IsMetadataDecode()) { if (mContainedDecoder->HasSize()) { mDirEntry->mSize = mContainedDecoder->Size(); } return Transition::To(ICOState::ITERATE_UNSIZED_DIR_ENTRY, 0); } // Raymond Chen says that 32bpp only are valid PNG ICOs // http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx if (!mContainedDecoder->IsValidICOResource()) { return Transition::TerminateFailure(); } // This size from the resource should match that from the dir entry. MOZ_ASSERT_IF(mContainedDecoder->HasSize(), mContainedDecoder->Size() == mDirEntry->mSize); return Transition::TerminateSuccess(); } LexerResult nsICODecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) { MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); return mLexer.Lex( aIterator, aOnResume, [=](ICOState aState, const char* aData, size_t aLength) { switch (aState) { case ICOState::HEADER: return ReadHeader(aData); case ICOState::DIR_ENTRY: return ReadDirEntry(aData); case ICOState::FINISHED_DIR_ENTRY: return FinishDirEntry(); case ICOState::ITERATE_UNSIZED_DIR_ENTRY: return IterateUnsizedDirEntry(); case ICOState::SKIP_TO_RESOURCE: return Transition::ContinueUnbuffered(ICOState::SKIP_TO_RESOURCE); case ICOState::FOUND_RESOURCE: return Transition::To(ICOState::SNIFF_RESOURCE, BITMAPINFOSIZE); case ICOState::SNIFF_RESOURCE: return SniffResource(aData); case ICOState::READ_RESOURCE: return ReadResource(); case ICOState::PREPARE_FOR_MASK: return PrepareForMask(); case ICOState::READ_MASK_ROW: return ReadMaskRow(aData); case ICOState::FINISH_MASK: return FinishMask(); case ICOState::SKIP_MASK: return Transition::ContinueUnbuffered(ICOState::SKIP_MASK); case ICOState::FINISHED_RESOURCE: return FinishResource(); default: MOZ_CRASH("Unknown ICOState"); } }); } bool nsICODecoder::FlushContainedDecoder() { MOZ_ASSERT(mContainedDecoder); bool succeeded = true; // If we run out of data, the ICO decoder will get resumed when there's more // data available, as usual, so we don't need the contained decoder to get // resumed too. To avoid that, we provide an IResumable which just does // nothing. All the caller needs to do is flush when there is new data. LexerResult result = mContainedDecoder->Decode(); if (result == LexerResult(TerminalState::FAILURE)) { succeeded = false; } MOZ_ASSERT(result != LexerResult(Yield::OUTPUT_AVAILABLE), "Unexpected yield"); // Make our state the same as the state of the contained decoder, and // propagate errors. mProgress |= mContainedDecoder->TakeProgress(); mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect()); if (mContainedDecoder->HasError()) { succeeded = false; } return succeeded; } } // namespace image } // namespace mozilla