diff options
Diffstat (limited to 'image/decoders')
36 files changed, 9975 insertions, 0 deletions
diff --git a/image/decoders/EXIF.cpp b/image/decoders/EXIF.cpp new file mode 100644 index 000000000..8197c886c --- /dev/null +++ b/image/decoders/EXIF.cpp @@ -0,0 +1,331 @@ +/* -*- 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 "EXIF.h" + +#include "mozilla/EndianUtils.h" + +namespace mozilla { +namespace image { + +// Section references in this file refer to the EXIF v2.3 standard, also known +// as CIPA DC-008-Translation-2010. + +// See Section 4.6.4, Table 4. +// Typesafe enums are intentionally not used here since we're comparing to raw +// integers produced by parsing. +enum EXIFTag +{ + OrientationTag = 0x112, +}; + +// See Section 4.6.2. +enum EXIFType +{ + ByteType = 1, + ASCIIType = 2, + ShortType = 3, + LongType = 4, + RationalType = 5, + UndefinedType = 7, + SignedLongType = 9, + SignedRational = 10, +}; + +static const char* EXIFHeader = "Exif\0\0"; +static const uint32_t EXIFHeaderLength = 6; + +///////////////////////////////////////////////////////////// +// Parse EXIF data, typically found in a JPEG's APP1 segment. +///////////////////////////////////////////////////////////// +EXIFData +EXIFParser::ParseEXIF(const uint8_t* aData, const uint32_t aLength) +{ + if (!Initialize(aData, aLength)) { + return EXIFData(); + } + + if (!ParseEXIFHeader()) { + return EXIFData(); + } + + uint32_t offsetIFD; + if (!ParseTIFFHeader(offsetIFD)) { + return EXIFData(); + } + + JumpTo(offsetIFD); + + Orientation orientation; + if (!ParseIFD0(orientation)) { + return EXIFData(); + } + + // We only care about orientation at this point, so we don't bother with the + // other IFDs. If we got this far we're done. + return EXIFData(orientation); +} + +///////////////////////////////////////////////////////// +// Parse the EXIF header. (Section 4.7.2, Figure 30) +///////////////////////////////////////////////////////// +bool +EXIFParser::ParseEXIFHeader() +{ + return MatchString(EXIFHeader, EXIFHeaderLength); +} + +///////////////////////////////////////////////////////// +// Parse the TIFF header. (Section 4.5.2, Table 1) +///////////////////////////////////////////////////////// +bool +EXIFParser::ParseTIFFHeader(uint32_t& aIFD0OffsetOut) +{ + // Determine byte order. + if (MatchString("MM\0*", 4)) { + mByteOrder = ByteOrder::BigEndian; + } else if (MatchString("II*\0", 4)) { + mByteOrder = ByteOrder::LittleEndian; + } else { + return false; + } + + // Determine offset of the 0th IFD. (It shouldn't be greater than 64k, which + // is the maximum size of the entry APP1 segment.) + uint32_t ifd0Offset; + if (!ReadUInt32(ifd0Offset) || ifd0Offset > 64 * 1024) { + return false; + } + + // The IFD offset is relative to the beginning of the TIFF header, which + // begins after the EXIF header, so we need to increase the offset + // appropriately. + aIFD0OffsetOut = ifd0Offset + EXIFHeaderLength; + return true; +} + +///////////////////////////////////////////////////////// +// Parse the entries in IFD0. (Section 4.6.2) +///////////////////////////////////////////////////////// +bool +EXIFParser::ParseIFD0(Orientation& aOrientationOut) +{ + uint16_t entryCount; + if (!ReadUInt16(entryCount)) { + return false; + } + + for (uint16_t entry = 0 ; entry < entryCount ; ++entry) { + // Read the fields of the entry. + uint16_t tag; + if (!ReadUInt16(tag)) { + return false; + } + + // Right now, we only care about orientation, so we immediately skip to the + // next entry if we find anything else. + if (tag != OrientationTag) { + Advance(10); + continue; + } + + uint16_t type; + if (!ReadUInt16(type)) { + return false; + } + + uint32_t count; + if (!ReadUInt32(count)) { + return false; + } + + // We should have an orientation value here; go ahead and parse it. + if (!ParseOrientation(type, count, aOrientationOut)) { + return false; + } + + // Since the orientation is all we care about, we're done. + return true; + } + + // We didn't find an orientation field in the IFD. That's OK; we assume the + // default orientation in that case. + aOrientationOut = Orientation(); + return true; +} + +bool +EXIFParser::ParseOrientation(uint16_t aType, uint32_t aCount, Orientation& aOut) +{ + // Sanity check the type and count. + if (aType != ShortType || aCount != 1) { + return false; + } + + uint16_t value; + if (!ReadUInt16(value)) { + return false; + } + + switch (value) { + case 1: aOut = Orientation(Angle::D0, Flip::Unflipped); break; + case 2: aOut = Orientation(Angle::D0, Flip::Horizontal); break; + case 3: aOut = Orientation(Angle::D180, Flip::Unflipped); break; + case 4: aOut = Orientation(Angle::D180, Flip::Horizontal); break; + case 5: aOut = Orientation(Angle::D90, Flip::Horizontal); break; + case 6: aOut = Orientation(Angle::D90, Flip::Unflipped); break; + case 7: aOut = Orientation(Angle::D270, Flip::Horizontal); break; + case 8: aOut = Orientation(Angle::D270, Flip::Unflipped); break; + default: return false; + } + + // This is a 32-bit field, but the orientation value only occupies the first + // 16 bits. We need to advance another 16 bits to consume the entire field. + Advance(2); + return true; +} + +bool +EXIFParser::Initialize(const uint8_t* aData, const uint32_t aLength) +{ + if (aData == nullptr) { + return false; + } + + // An APP1 segment larger than 64k violates the JPEG standard. + if (aLength > 64 * 1024) { + return false; + } + + mStart = mCurrent = aData; + mLength = mRemainingLength = aLength; + mByteOrder = ByteOrder::Unknown; + return true; +} + +void +EXIFParser::Advance(const uint32_t aDistance) +{ + if (mRemainingLength >= aDistance) { + mCurrent += aDistance; + mRemainingLength -= aDistance; + } else { + mCurrent = mStart; + mRemainingLength = 0; + } +} + +void +EXIFParser::JumpTo(const uint32_t aOffset) +{ + if (mLength >= aOffset) { + mCurrent = mStart + aOffset; + mRemainingLength = mLength - aOffset; + } else { + mCurrent = mStart; + mRemainingLength = 0; + } +} + +bool +EXIFParser::MatchString(const char* aString, const uint32_t aLength) +{ + if (mRemainingLength < aLength) { + return false; + } + + for (uint32_t i = 0 ; i < aLength ; ++i) { + if (mCurrent[i] != aString[i]) { + return false; + } + } + + Advance(aLength); + return true; +} + +bool +EXIFParser::MatchUInt16(const uint16_t aValue) +{ + if (mRemainingLength < 2) { + return false; + } + + bool matched; + switch (mByteOrder) { + case ByteOrder::LittleEndian: + matched = LittleEndian::readUint16(mCurrent) == aValue; + break; + case ByteOrder::BigEndian: + matched = BigEndian::readUint16(mCurrent) == aValue; + break; + default: + NS_NOTREACHED("Should know the byte order by now"); + matched = false; + } + + if (matched) { + Advance(2); + } + + return matched; +} + +bool +EXIFParser::ReadUInt16(uint16_t& aValue) +{ + if (mRemainingLength < 2) { + return false; + } + + bool matched = true; + switch (mByteOrder) { + case ByteOrder::LittleEndian: + aValue = LittleEndian::readUint16(mCurrent); + break; + case ByteOrder::BigEndian: + aValue = BigEndian::readUint16(mCurrent); + break; + default: + NS_NOTREACHED("Should know the byte order by now"); + matched = false; + } + + if (matched) { + Advance(2); + } + + return matched; +} + +bool +EXIFParser::ReadUInt32(uint32_t& aValue) +{ + if (mRemainingLength < 4) { + return false; + } + + bool matched = true; + switch (mByteOrder) { + case ByteOrder::LittleEndian: + aValue = LittleEndian::readUint32(mCurrent); + break; + case ByteOrder::BigEndian: + aValue = BigEndian::readUint32(mCurrent); + break; + default: + NS_NOTREACHED("Should know the byte order by now"); + matched = false; + } + + if (matched) { + Advance(4); + } + + return matched; +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/EXIF.h b/image/decoders/EXIF.h new file mode 100644 index 000000000..9028f2cf8 --- /dev/null +++ b/image/decoders/EXIF.h @@ -0,0 +1,75 @@ +/* -*- 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_decoders_EXIF_h +#define mozilla_image_decoders_EXIF_h + +#include <stdint.h> +#include "nsDebug.h" + +#include "Orientation.h" + +namespace mozilla { +namespace image { + +enum class ByteOrder : uint8_t { + Unknown, + LittleEndian, + BigEndian +}; + +struct EXIFData +{ + EXIFData() { } + explicit EXIFData(Orientation aOrientation) : orientation(aOrientation) { } + + const Orientation orientation; +}; + +class EXIFParser +{ +public: + static EXIFData + Parse(const uint8_t* aData, const uint32_t aLength) + { + EXIFParser parser; + return parser.ParseEXIF(aData, aLength); + } + +private: + EXIFParser() + : mStart(nullptr) + , mCurrent(nullptr) + , mLength(0) + , mRemainingLength(0) + , mByteOrder(ByteOrder::Unknown) + { } + + EXIFData ParseEXIF(const uint8_t* aData, const uint32_t aLength); + bool ParseEXIFHeader(); + bool ParseTIFFHeader(uint32_t& aIFD0OffsetOut); + bool ParseIFD0(Orientation& aOrientationOut); + bool ParseOrientation(uint16_t aType, uint32_t aCount, Orientation& aOut); + + bool Initialize(const uint8_t* aData, const uint32_t aLength); + void Advance(const uint32_t aDistance); + void JumpTo(const uint32_t aOffset); + + bool MatchString(const char* aString, const uint32_t aLength); + bool MatchUInt16(const uint16_t aValue); + bool ReadUInt16(uint16_t& aOut); + bool ReadUInt32(uint32_t& aOut); + + const uint8_t* mStart; + const uint8_t* mCurrent; + uint32_t mLength; + uint32_t mRemainingLength; + ByteOrder mByteOrder; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_EXIF_h diff --git a/image/decoders/GIF2.h b/image/decoders/GIF2.h new file mode 100644 index 000000000..2521667d4 --- /dev/null +++ b/image/decoders/GIF2.h @@ -0,0 +1,65 @@ +/* -*- Mode: C; tab-width: 4; 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_decoders_GIF2_H +#define mozilla_image_decoders_GIF2_H + +#define MAX_LZW_BITS 12 +#define MAX_BITS 4097 // 2^MAX_LZW_BITS+1 +#define MAX_COLORS 256 +#define MIN_HOLD_SIZE 256 + +enum { GIF_TRAILER = 0x3B }; // ';' +enum { GIF_IMAGE_SEPARATOR = 0x2C }; // ',' +enum { GIF_EXTENSION_INTRODUCER = 0x21 }; // '!' +enum { GIF_GRAPHIC_CONTROL_LABEL = 0xF9 }; +enum { GIF_APPLICATION_EXTENSION_LABEL = 0xFF }; + +// A GIF decoder's state +typedef struct gif_struct { + // LZW decoder state machine + uint8_t* stackp; // Current stack pointer + int datasize; + int codesize; + int codemask; + int avail; // Index of next available slot in dictionary + int oldcode; + uint8_t firstchar; + int bits; // Number of unread bits in "datum" + int32_t datum; // 32-bit input buffer + + // Output state machine + int64_t pixels_remaining; // Pixels remaining to be output. + + // Parameters for image frame currently being decoded + int tpixel; // Index of transparent pixel + int32_t disposal_method; // Restore to background, leave in place, etc. + uint32_t* local_colormap; // Per-image colormap + int local_colormap_size; // Size of local colormap array. + uint32_t delay_time; // Display time, in milliseconds, + // for this image in a multi-image GIF + + // Global (multi-image) state + int version; // Either 89 for GIF89 or 87 for GIF87 + int32_t screen_width; // Logical screen width & height + int32_t screen_height; + uint8_t global_colormap_depth; // Depth of global colormap array + uint16_t global_colormap_count; // Number of colors in global colormap + int images_decoded; // Counts images for multi-part GIFs + int loop_count; // Netscape specific extension block to control + // the number of animation loops a GIF + // renders. + + bool is_transparent; // TRUE, if tpixel is valid + + uint16_t prefix[MAX_BITS]; // LZW decoding tables + uint32_t global_colormap[MAX_COLORS]; // Default colormap if local not + // supplied + uint8_t suffix[MAX_BITS]; // LZW decoding tables + uint8_t stack[MAX_BITS]; // Base of LZW decoder stack + +} gif_struct; + +#endif // mozilla_image_decoders_GIF2_H diff --git a/image/decoders/iccjpeg.c b/image/decoders/iccjpeg.c new file mode 100644 index 000000000..af014cf6b --- /dev/null +++ b/image/decoders/iccjpeg.c @@ -0,0 +1,195 @@ +/* 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/. */ + +/* + * iccjpeg.c + * + * This file provides code to read and write International Color Consortium + * (ICC) device profiles embedded in JFIF JPEG image files. The ICC has + * defined a standard format for including such data in JPEG "APP2" markers. + * The code given here does not know anything about the internal structure + * of the ICC profile data; it just knows how to put the profile data into + * a JPEG file being written, or get it back out when reading. + * + * This code depends on new features added to the IJG JPEG library as of + * IJG release 6b; it will not compile or work with older IJG versions. + * + * NOTE: this code would need surgery to work on 16-bit-int machines + * with ICC profiles exceeding 64K bytes in size. If you need to do that, + * change all the "unsigned int" variables to "INT32". You'll also need + * to find a malloc() replacement that can allocate more than 64K. + */ + +#include "iccjpeg.h" +#include <stdlib.h> /* define malloc() */ + + +/* + * Since an ICC profile can be larger than the maximum size of a JPEG marker + * (64K), we need provisions to split it into multiple markers. The format + * defined by the ICC specifies one or more APP2 markers containing the + * following data: + * Identifying string ASCII "ICC_PROFILE\0" (12 bytes) + * Marker sequence number 1 for first APP2, 2 for next, etc (1 byte) + * Number of markers Total number of APP2's used (1 byte) + * Profile data (remainder of APP2 data) + * Decoders should use the marker sequence numbers to reassemble the profile, + * rather than assuming that the APP2 markers appear in the correct sequence. + */ + +#define ICC_MARKER (JPEG_APP0 + 2) /* JPEG marker code for ICC */ +#define ICC_OVERHEAD_LEN 14 /* size of non-profile data in APP2 */ +#define MAX_BYTES_IN_MARKER 65533 /* maximum data len of a JPEG marker */ +#define MAX_DATA_BYTES_IN_MARKER (MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN) + +/* + * Prepare for reading an ICC profile + */ + +void +setup_read_icc_profile (j_decompress_ptr cinfo) +{ + /* Tell the library to keep any APP2 data it may find */ + jpeg_save_markers(cinfo, ICC_MARKER, 0xFFFF); +} + + +/* + * Handy subroutine to test whether a saved marker is an ICC profile marker. + */ + +static boolean +marker_is_icc (jpeg_saved_marker_ptr marker) +{ + return + marker->marker == ICC_MARKER && + marker->data_length >= ICC_OVERHEAD_LEN && + /* verify the identifying string */ + GETJOCTET(marker->data[0]) == 0x49 && + GETJOCTET(marker->data[1]) == 0x43 && + GETJOCTET(marker->data[2]) == 0x43 && + GETJOCTET(marker->data[3]) == 0x5F && + GETJOCTET(marker->data[4]) == 0x50 && + GETJOCTET(marker->data[5]) == 0x52 && + GETJOCTET(marker->data[6]) == 0x4F && + GETJOCTET(marker->data[7]) == 0x46 && + GETJOCTET(marker->data[8]) == 0x49 && + GETJOCTET(marker->data[9]) == 0x4C && + GETJOCTET(marker->data[10]) == 0x45 && + GETJOCTET(marker->data[11]) == 0x0; +} + + +/* + * See if there was an ICC profile in the JPEG file being read; + * if so, reassemble and return the profile data. + * + * TRUE is returned if an ICC profile was found, FALSE if not. + * If TRUE is returned, *icc_data_ptr is set to point to the + * returned data, and *icc_data_len is set to its length. + * + * IMPORTANT: the data at **icc_data_ptr has been allocated with malloc() + * and must be freed by the caller with free() when the caller no longer + * needs it. (Alternatively, we could write this routine to use the + * IJG library's memory allocator, so that the data would be freed implicitly + * at jpeg_finish_decompress() time. But it seems likely that many apps + * will prefer to have the data stick around after decompression finishes.) + * + * NOTE: if the file contains invalid ICC APP2 markers, we just silently + * return FALSE. You might want to issue an error message instead. + */ + +boolean +read_icc_profile (j_decompress_ptr cinfo, + JOCTET** icc_data_ptr, + unsigned int* icc_data_len) +{ + jpeg_saved_marker_ptr marker; + int num_markers = 0; + int seq_no; + JOCTET* icc_data; + unsigned int total_length; +#define MAX_SEQ_NO 255 /* sufficient since marker numbers are bytes */ + char marker_present[MAX_SEQ_NO+1]; /* 1 if marker found */ + unsigned int data_length[MAX_SEQ_NO+1]; /* size of profile data in marker */ + unsigned int data_offset[MAX_SEQ_NO+1]; /* offset for data in marker */ + + *icc_data_ptr = NULL; /* avoid confusion if FALSE return */ + *icc_data_len = 0; + + /* This first pass over the saved markers discovers whether there are + * any ICC markers and verifies the consistency of the marker numbering. + */ + + for (seq_no = 1; seq_no <= MAX_SEQ_NO; seq_no++) { + marker_present[seq_no] = 0; + } + + for (marker = cinfo->marker_list; marker != NULL; marker = marker->next) { + if (marker_is_icc(marker)) { + if (num_markers == 0) { + num_markers = GETJOCTET(marker->data[13]); + } else if (num_markers != GETJOCTET(marker->data[13])) { + return FALSE; /* inconsistent num_markers fields */ + } + seq_no = GETJOCTET(marker->data[12]); + if (seq_no <= 0 || seq_no > num_markers) { + return FALSE; /* bogus sequence number */ + } + if (marker_present[seq_no]) { + return FALSE; /* duplicate sequence numbers */ + } + marker_present[seq_no] = 1; + data_length[seq_no] = marker->data_length - ICC_OVERHEAD_LEN; + } + } + + if (num_markers == 0) { + return FALSE; + } + + /* Check for missing markers, count total space needed, + * compute offset of each marker's part of the data. + */ + + total_length = 0; + for (seq_no = 1; seq_no <= num_markers; seq_no++) { + if (marker_present[seq_no] == 0) { + return FALSE; /* missing sequence number */ + } + data_offset[seq_no] = total_length; + total_length += data_length[seq_no]; + } + + if (total_length <= 0) { + return FALSE; /* found only empty markers? */ + } + + /* Allocate space for assembled data */ + icc_data = (JOCTET*) malloc(total_length * sizeof(JOCTET)); + if (icc_data == NULL) { + return FALSE; /* oops, out of memory */ + } + + /* and fill it in */ + for (marker = cinfo->marker_list; marker != NULL; marker = marker->next) { + if (marker_is_icc(marker)) { + JOCTET FAR* src_ptr; + JOCTET* dst_ptr; + unsigned int length; + seq_no = GETJOCTET(marker->data[12]); + dst_ptr = icc_data + data_offset[seq_no]; + src_ptr = marker->data + ICC_OVERHEAD_LEN; + length = data_length[seq_no]; + while (length--) { + *dst_ptr++ = *src_ptr++; + } + } + } + + *icc_data_ptr = icc_data; + *icc_data_len = total_length; + + return TRUE; +} diff --git a/image/decoders/iccjpeg.h b/image/decoders/iccjpeg.h new file mode 100644 index 000000000..474df719c --- /dev/null +++ b/image/decoders/iccjpeg.h @@ -0,0 +1,67 @@ +/* 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/. */ + +/* + * iccjpeg.h + * + * This file provides code to read and write International Color Consortium + * (ICC) device profiles embedded in JFIF JPEG image files. The ICC has + * defined a standard format for including such data in JPEG "APP2" markers. + * The code given here does not know anything about the internal structure + * of the ICC profile data; it just knows how to put the profile data into + * a JPEG file being written, or get it back out when reading. + * + * This code depends on new features added to the IJG JPEG library as of + * IJG release 6b; it will not compile or work with older IJG versions. + * + * NOTE: this code would need surgery to work on 16-bit-int machines + * with ICC profiles exceeding 64K bytes in size. See iccprofile.c + * for details. + */ + +#ifndef mozilla_image_decoders_iccjpeg_h +#define mozilla_image_decoders_iccjpeg_h + +#include <stdio.h> /* needed to define "FILE", "NULL" */ +#include "jpeglib.h" + +/* + * Reading a JPEG file that may contain an ICC profile requires two steps: + * + * 1. After jpeg_create_decompress() but before jpeg_read_header(), + * call setup_read_icc_profile(). This routine tells the IJG library + * to save in memory any APP2 markers it may find in the file. + * + * 2. After jpeg_read_header(), call read_icc_profile() to find out + * whether there was a profile and obtain it if so. + */ + + +/* + * Prepare for reading an ICC profile + */ + +extern void setup_read_icc_profile JPP((j_decompress_ptr cinfo)); + + +/* + * See if there was an ICC profile in the JPEG file being read; + * if so, reassemble and return the profile data. + * + * TRUE is returned if an ICC profile was found, FALSE if not. + * If TRUE is returned, *icc_data_ptr is set to point to the + * returned data, and *icc_data_len is set to its length. + * + * IMPORTANT: the data at **icc_data_ptr has been allocated with malloc() + * and must be freed by the caller with free() when the caller no longer + * needs it. (Alternatively, we could write this routine to use the + * IJG library's memory allocator, so that the data would be freed implicitly + * at jpeg_finish_decompress() time. But it seems likely that many apps + * will prefer to have the data stick around after decompression finishes.) + */ + +extern boolean read_icc_profile JPP((j_decompress_ptr cinfo, + JOCTET** icc_data_ptr, + unsigned int* icc_data_len)); +#endif // mozilla_image_decoders_iccjpeg_h diff --git a/image/decoders/icon/android/moz.build b/image/decoders/icon/android/moz.build new file mode 100644 index 000000000..5e58ff0b6 --- /dev/null +++ b/image/decoders/icon/android/moz.build @@ -0,0 +1,13 @@ +# -*- 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 += [ + 'nsIconChannel.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' diff --git a/image/decoders/icon/android/nsIconChannel.cpp b/image/decoders/icon/android/nsIconChannel.cpp new file mode 100644 index 000000000..5670bf2f9 --- /dev/null +++ b/image/decoders/icon/android/nsIconChannel.cpp @@ -0,0 +1,145 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 <stdlib.h> +#include "mozilla/dom/ContentChild.h" +#include "nsMimeTypes.h" +#include "nsIURL.h" +#include "nsXULAppAPI.h" +#include "AndroidBridge.h" +#include "nsIconChannel.h" +#include "nsIStringStream.h" +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsNullPrincipal.h" + +NS_IMPL_ISUPPORTS(nsIconChannel, + nsIRequest, + nsIChannel) + +using namespace mozilla; +using mozilla::dom::ContentChild; + +static nsresult +GetIconForExtension(const nsACString& aFileExt, uint32_t aIconSize, + uint8_t* const aBuf) +{ + if (!AndroidBridge::Bridge()) { + return NS_ERROR_FAILURE; + } + + AndroidBridge::Bridge()->GetIconForExtension(aFileExt, aIconSize, aBuf); + + return NS_OK; +} + +static nsresult +CallRemoteGetIconForExtension(const nsACString& aFileExt, uint32_t aIconSize, + uint8_t* const aBuf) +{ + NS_ENSURE_TRUE(aBuf != nullptr, NS_ERROR_NULL_POINTER); + + // An array has to be used to get data from remote process + InfallibleTArray<uint8_t> bits; + uint32_t bufSize = aIconSize * aIconSize * 4; + + if (!ContentChild::GetSingleton()->SendGetIconForExtension( + PromiseFlatCString(aFileExt), aIconSize, &bits)) { + return NS_ERROR_FAILURE; + } + + NS_ASSERTION(bits.Length() == bufSize, "Pixels array is incomplete"); + if (bits.Length() != bufSize) { + return NS_ERROR_FAILURE; + } + + memcpy(aBuf, bits.Elements(), bufSize); + + return NS_OK; +} + +static nsresult +moz_icon_to_channel(nsIURI* aURI, const nsACString& aFileExt, + uint32_t aIconSize, nsIChannel** aChannel) +{ + NS_ENSURE_TRUE(aIconSize < 256 && aIconSize > 0, NS_ERROR_UNEXPECTED); + + int width = aIconSize; + int height = aIconSize; + + // moz-icon data should have two bytes for the size, + // then the ARGB pixel values with pre-multiplied Alpha + const int channels = 4; + long int buf_size = 2 + channels * height * width; + uint8_t* const buf = (uint8_t*)moz_xmalloc(buf_size); + NS_ENSURE_TRUE(buf, NS_ERROR_OUT_OF_MEMORY); + uint8_t* out = buf; + + *(out++) = width; + *(out++) = height; + + nsresult rv; + if (XRE_IsParentProcess()) { + rv = GetIconForExtension(aFileExt, aIconSize, out); + } else { + rv = CallRemoteGetIconForExtension(aFileExt, aIconSize, out); + } + NS_ENSURE_SUCCESS(rv, rv); + + // Encode the RGBA data + const uint8_t* in = out; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + uint8_t r = *(in++); + uint8_t g = *(in++); + uint8_t b = *(in++); + uint8_t a = *(in++); +#define DO_PREMULTIPLY(c_) uint8_t(uint16_t(c_) * uint16_t(a) / uint16_t(255)) + *(out++) = DO_PREMULTIPLY(b); + *(out++) = DO_PREMULTIPLY(g); + *(out++) = DO_PREMULTIPLY(r); + *(out++) = a; +#undef DO_PREMULTIPLY + } + } + + nsCOMPtr<nsIStringInputStream> stream = + do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stream->AdoptData((char*)buf, buf_size); + NS_ENSURE_SUCCESS(rv, rv); + + // nsIconProtocolHandler::NewChannel2 will provide the correct loadInfo for + // this iconChannel. Use the most restrictive security settings for the + // temporary loadInfo to make sure the channel can not be openend. + nsCOMPtr<nsIPrincipal> nullPrincipal = nsNullPrincipal::Create(); + return NS_NewInputStreamChannel(aChannel, + aURI, + stream, + nullPrincipal, + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + nsIContentPolicy::TYPE_INTERNAL_IMAGE, + NS_LITERAL_CSTRING(IMAGE_ICON_MS)); +} + +nsresult +nsIconChannel::Init(nsIURI* aURI) +{ + nsCOMPtr<nsIMozIconURI> iconURI = do_QueryInterface(aURI); + NS_ASSERTION(iconURI, "URI is not an nsIMozIconURI"); + + nsAutoCString stockIcon; + iconURI->GetStockIcon(stockIcon); + + uint32_t desiredImageSize; + iconURI->GetImageSize(&desiredImageSize); + + nsAutoCString iconFileExt; + iconURI->GetFileExtension(iconFileExt); + + return moz_icon_to_channel(iconURI, iconFileExt, desiredImageSize, + getter_AddRefs(mRealChannel)); +} diff --git a/image/decoders/icon/android/nsIconChannel.h b/image/decoders/icon/android/nsIconChannel.h new file mode 100644 index 000000000..be5542998 --- /dev/null +++ b/image/decoders/icon/android/nsIconChannel.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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_decoders_icon_android_nsIconChannel_h +#define mozilla_image_decoders_icon_android_nsIconChannel_h + +#include "mozilla/Attributes.h" + +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsIIconURI.h" +#include "nsCOMPtr.h" + +/** + * This class is the Android implementation of nsIconChannel. + * It asks Android for an icon, and creates a new channel for + * that file to which all calls will be proxied. + */ +class nsIconChannel final : public nsIChannel { + public: + NS_DECL_ISUPPORTS + NS_FORWARD_NSIREQUEST(mRealChannel->) + NS_FORWARD_NSICHANNEL(mRealChannel->) + + nsIconChannel() { } + + /** + * Called by nsIconProtocolHandler after it creates this channel. + * Must be called before calling any other function on this object. + * If this method fails, no other function must be called on this object. + */ + nsresult Init(nsIURI* aURI); + + private: + ~nsIconChannel() { } + + /** + * The channel to the temp icon file (e.g. to /tmp/2qy9wjqw.html). + * Will always be non-null after a successful Init. + */ + nsCOMPtr<nsIChannel> mRealChannel; +}; + +#endif // mozilla_image_decoders_icon_android_nsIconChannel_h diff --git a/image/decoders/icon/gtk/moz.build b/image/decoders/icon/gtk/moz.build new file mode 100644 index 000000000..a8116ca87 --- /dev/null +++ b/image/decoders/icon/gtk/moz.build @@ -0,0 +1,16 @@ +# -*- 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 += [ + 'nsIconChannel.cpp', +] + +FINAL_LIBRARY = 'xul' + +if CONFIG['MOZ_ENABLE_GNOMEUI']: + CXXFLAGS += CONFIG['MOZ_GNOMEUI_CFLAGS'] +else: + CXXFLAGS += CONFIG['TK_CFLAGS'] diff --git a/image/decoders/icon/gtk/nsIconChannel.cpp b/image/decoders/icon/gtk/nsIconChannel.cpp new file mode 100644 index 000000000..b014e4c04 --- /dev/null +++ b/image/decoders/icon/gtk/nsIconChannel.cpp @@ -0,0 +1,427 @@ +/* vim:set ts=2 sw=2 sts=2 cin et: */ +/* 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 "nsIconChannel.h" + +#include <stdlib.h> +#include <unistd.h> + +#include "mozilla/DebugOnly.h" +#include "mozilla/EndianUtils.h" +#include <algorithm> + +#ifdef MOZ_ENABLE_GIO +#include <gio/gio.h> +#endif + +#include <gtk/gtk.h> + +#include "nsMimeTypes.h" +#include "nsIMIMEService.h" + +#include "nsServiceManagerUtils.h" + +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsIStringStream.h" +#include "nsServiceManagerUtils.h" +#include "nsNullPrincipal.h" +#include "nsIURL.h" +#include "prlink.h" + +NS_IMPL_ISUPPORTS(nsIconChannel, + nsIRequest, + nsIChannel) + +static nsresult +moz_gdk_pixbuf_to_channel(GdkPixbuf* aPixbuf, nsIURI* aURI, + nsIChannel** aChannel) +{ + int width = gdk_pixbuf_get_width(aPixbuf); + int height = gdk_pixbuf_get_height(aPixbuf); + NS_ENSURE_TRUE(height < 256 && width < 256 && height > 0 && width > 0 && + gdk_pixbuf_get_colorspace(aPixbuf) == GDK_COLORSPACE_RGB && + gdk_pixbuf_get_bits_per_sample(aPixbuf) == 8 && + gdk_pixbuf_get_has_alpha(aPixbuf) && + gdk_pixbuf_get_n_channels(aPixbuf) == 4, + NS_ERROR_UNEXPECTED); + + const int n_channels = 4; + gsize buf_size = 2 + n_channels * height * width; + uint8_t* const buf = (uint8_t*)moz_xmalloc(buf_size); + NS_ENSURE_TRUE(buf, NS_ERROR_OUT_OF_MEMORY); + uint8_t* out = buf; + + *(out++) = width; + *(out++) = height; + + const guchar* const pixels = gdk_pixbuf_get_pixels(aPixbuf); + int rowextra = gdk_pixbuf_get_rowstride(aPixbuf) - width * n_channels; + + // encode the RGB data and the A data + const guchar* in = pixels; + for (int y = 0; y < height; ++y, in += rowextra) { + for (int x = 0; x < width; ++x) { + uint8_t r = *(in++); + uint8_t g = *(in++); + uint8_t b = *(in++); + uint8_t a = *(in++); +#define DO_PREMULTIPLY(c_) uint8_t(uint16_t(c_) * uint16_t(a) / uint16_t(255)) +#if MOZ_LITTLE_ENDIAN + *(out++) = DO_PREMULTIPLY(b); + *(out++) = DO_PREMULTIPLY(g); + *(out++) = DO_PREMULTIPLY(r); + *(out++) = a; +#else + *(out++) = a; + *(out++) = DO_PREMULTIPLY(r); + *(out++) = DO_PREMULTIPLY(g); + *(out++) = DO_PREMULTIPLY(b); +#endif +#undef DO_PREMULTIPLY + } + } + + NS_ASSERTION(out == buf + buf_size, "size miscalculation"); + + nsresult rv; + nsCOMPtr<nsIStringInputStream> stream = + do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv); + + // Prevent the leaking of buf + if (NS_WARN_IF(NS_FAILED(rv))) { + free(buf); + return rv; + } + + // stream takes ownership of buf and will free it on destruction. + // This function cannot fail. + rv = stream->AdoptData((char*)buf, buf_size); + + // If this no longer holds then re-examine buf's lifetime. + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // nsIconProtocolHandler::NewChannel2 will provide the correct loadInfo for + // this iconChannel. Use the most restrictive security settings for the + // temporary loadInfo to make sure the channel can not be openend. + nsCOMPtr<nsIPrincipal> nullPrincipal = nsNullPrincipal::Create(); + return NS_NewInputStreamChannel(aChannel, + aURI, + stream, + nullPrincipal, + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + nsIContentPolicy::TYPE_INTERNAL_IMAGE, + NS_LITERAL_CSTRING(IMAGE_ICON_MS)); +} + +static GtkWidget* gProtoWindow = nullptr; +static GtkWidget* gStockImageWidget = nullptr; + +static void +ensure_stock_image_widget() +{ + // Only the style of the GtkImage needs to be used, but the widget is kept + // to track dynamic style changes. + if (!gProtoWindow) { + gProtoWindow = gtk_window_new(GTK_WINDOW_POPUP); + GtkWidget* protoLayout = gtk_fixed_new(); + gtk_container_add(GTK_CONTAINER(gProtoWindow), protoLayout); + + gStockImageWidget = gtk_image_new(); + gtk_container_add(GTK_CONTAINER(protoLayout), gStockImageWidget); + + gtk_widget_ensure_style(gStockImageWidget); + } +} + +static GtkIconSize +moz_gtk_icon_size(const char* name) +{ + if (strcmp(name, "button") == 0) { + return GTK_ICON_SIZE_BUTTON; + } + + if (strcmp(name, "menu") == 0) { + return GTK_ICON_SIZE_MENU; + } + + if (strcmp(name, "toolbar") == 0) { + return GTK_ICON_SIZE_LARGE_TOOLBAR; + } + + if (strcmp(name, "toolbarsmall") == 0) { + return GTK_ICON_SIZE_SMALL_TOOLBAR; + } + + if (strcmp(name, "dnd") == 0) { + return GTK_ICON_SIZE_DND; + } + + if (strcmp(name, "dialog") == 0) { + return GTK_ICON_SIZE_DIALOG; + } + + return GTK_ICON_SIZE_MENU; +} + +#ifdef MOZ_ENABLE_GIO +static int32_t +GetIconSize(nsIMozIconURI* aIconURI) +{ + nsAutoCString iconSizeString; + + aIconURI->GetIconSize(iconSizeString); + if (iconSizeString.IsEmpty()) { + uint32_t size; + mozilla::DebugOnly<nsresult> rv = aIconURI->GetImageSize(&size); + NS_ASSERTION(NS_SUCCEEDED(rv), "GetImageSize failed"); + return size; + } else { + int size; + + GtkIconSize icon_size = moz_gtk_icon_size(iconSizeString.get()); + gtk_icon_size_lookup(icon_size, &size, nullptr); + return size; + } +} + +/* Scale icon buffer to preferred size */ +static nsresult +ScaleIconBuf(GdkPixbuf** aBuf, int32_t iconSize) +{ + // Scale buffer only if width or height differ from preferred size + if (gdk_pixbuf_get_width(*aBuf) != iconSize && + gdk_pixbuf_get_height(*aBuf) != iconSize) { + GdkPixbuf* scaled = gdk_pixbuf_scale_simple(*aBuf, iconSize, iconSize, + GDK_INTERP_BILINEAR); + // replace original buffer by scaled + g_object_unref(*aBuf); + *aBuf = scaled; + if (!scaled) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + return NS_OK; +} + +nsresult +nsIconChannel::InitWithGIO(nsIMozIconURI* aIconURI) +{ + GIcon *icon = nullptr; + nsCOMPtr<nsIURL> fileURI; + + // Read icon content + aIconURI->GetIconURL(getter_AddRefs(fileURI)); + + // Get icon for file specified by URI + if (fileURI) { + bool isFile; + nsAutoCString spec; + fileURI->GetAsciiSpec(spec); + if (NS_SUCCEEDED(fileURI->SchemeIs("file", &isFile)) && isFile) { + GFile* file = g_file_new_for_uri(spec.get()); + GFileInfo* fileInfo = g_file_query_info(file, + G_FILE_ATTRIBUTE_STANDARD_ICON, + G_FILE_QUERY_INFO_NONE, + nullptr, nullptr); + g_object_unref(file); + if (fileInfo) { + // icon from g_content_type_get_icon doesn't need unref + icon = g_file_info_get_icon(fileInfo); + if (icon) { + g_object_ref(icon); + } + g_object_unref(fileInfo); + } + } + } + + // Try to get icon by using MIME type + if (!icon) { + nsAutoCString type; + aIconURI->GetContentType(type); + // Try to get MIME type from file extension by using nsIMIMEService + if (type.IsEmpty()) { + nsCOMPtr<nsIMIMEService> ms(do_GetService("@mozilla.org/mime;1")); + if (ms) { + nsAutoCString fileExt; + aIconURI->GetFileExtension(fileExt); + ms->GetTypeFromExtension(fileExt, type); + } + } + char* ctype = nullptr; // character representation of content type + if (!type.IsEmpty()) { + ctype = g_content_type_from_mime_type(type.get()); + } + if (ctype) { + icon = g_content_type_get_icon(ctype); + g_free(ctype); + } + } + + // Get default icon theme + GtkIconTheme* iconTheme = gtk_icon_theme_get_default(); + GtkIconInfo* iconInfo = nullptr; + // Get icon size + int32_t iconSize = GetIconSize(aIconURI); + + if (icon) { + // Use icon and theme to get GtkIconInfo + iconInfo = gtk_icon_theme_lookup_by_gicon(iconTheme, + icon, iconSize, + (GtkIconLookupFlags)0); + g_object_unref(icon); + } + + if (!iconInfo) { + // Mozilla's mimetype lookup failed. Try the "unknown" icon. + iconInfo = gtk_icon_theme_lookup_icon(iconTheme, + "unknown", iconSize, + (GtkIconLookupFlags)0); + if (!iconInfo) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + // Create a GdkPixbuf buffer containing icon and scale it + GdkPixbuf* buf = gtk_icon_info_load_icon(iconInfo, nullptr); + gtk_icon_info_free(iconInfo); + if (!buf) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = ScaleIconBuf(&buf, iconSize); + NS_ENSURE_SUCCESS(rv, rv); + + rv = moz_gdk_pixbuf_to_channel(buf, aIconURI, + getter_AddRefs(mRealChannel)); + g_object_unref(buf); + return rv; +} +#endif // MOZ_ENABLE_GIO + +nsresult +nsIconChannel::Init(nsIURI* aURI) +{ + nsCOMPtr<nsIMozIconURI> iconURI = do_QueryInterface(aURI); + NS_ASSERTION(iconURI, "URI is not an nsIMozIconURI"); + + nsAutoCString stockIcon; + iconURI->GetStockIcon(stockIcon); + if (stockIcon.IsEmpty()) { +#ifdef MOZ_ENABLE_GIO + return InitWithGIO(iconURI); +#else + return NS_ERROR_NOT_AVAILABLE; +#endif + } + + // Search for stockIcon + nsAutoCString iconSizeString; + iconURI->GetIconSize(iconSizeString); + + nsAutoCString iconStateString; + iconURI->GetIconState(iconStateString); + + GtkIconSize icon_size = moz_gtk_icon_size(iconSizeString.get()); + GtkStateType state = iconStateString.EqualsLiteral("disabled") ? + GTK_STATE_INSENSITIVE : GTK_STATE_NORMAL; + + // First lookup the icon by stock id and text direction. + GtkTextDirection direction = GTK_TEXT_DIR_NONE; + if (StringEndsWith(stockIcon, NS_LITERAL_CSTRING("-ltr"))) { + direction = GTK_TEXT_DIR_LTR; + } else if (StringEndsWith(stockIcon, NS_LITERAL_CSTRING("-rtl"))) { + direction = GTK_TEXT_DIR_RTL; + } + + bool forceDirection = direction != GTK_TEXT_DIR_NONE; + nsAutoCString stockID; + bool useIconName = false; + if (!forceDirection) { + direction = gtk_widget_get_default_direction(); + stockID = stockIcon; + } else { + // GTK versions < 2.22 use icon names from concatenating stock id with + // -(rtl|ltr), which is how the moz-icon stock name is interpreted here. + stockID = Substring(stockIcon, 0, stockIcon.Length() - 4); + // However, if we lookup bidi icons by the stock name, then GTK versions + // >= 2.22 will use a bidi lookup convention that most icon themes do not + // yet follow. Therefore, we first check to see if the theme supports the + // old icon name as this will have bidi support (if found). + GtkIconTheme* icon_theme = gtk_icon_theme_get_default(); + // Micking what gtk_icon_set_render_icon does with sizes, though it's not + // critical as icons will be scaled to suit size. It just means we follow + // the same pathes and so share caches. + gint width, height; + if (gtk_icon_size_lookup(icon_size, &width, &height)) { + gint size = std::min(width, height); + // We use gtk_icon_theme_lookup_icon() without + // GTK_ICON_LOOKUP_USE_BUILTIN instead of gtk_icon_theme_has_icon() so + // we don't pick up fallback icons added by distributions for backward + // compatibility. + GtkIconInfo* icon = + gtk_icon_theme_lookup_icon(icon_theme, stockIcon.get(), + size, (GtkIconLookupFlags)0); + if (icon) { + useIconName = true; + gtk_icon_info_free(icon); + } + } + } + + ensure_stock_image_widget(); + GtkStyle* style = gtk_widget_get_style(gStockImageWidget); + GtkIconSet* icon_set = nullptr; + if (!useIconName) { + icon_set = gtk_style_lookup_icon_set(style, stockID.get()); + } + + if (!icon_set) { + // Either we have choosen icon-name lookup for a bidi icon, or stockIcon is + // not a stock id so we assume it is an icon name. + useIconName = true; + // Creating a GtkIconSet is a convenient way to allow the style to + // render the icon, possibly with variations suitable for insensitive + // states. + icon_set = gtk_icon_set_new(); + GtkIconSource* icon_source = gtk_icon_source_new(); + + gtk_icon_source_set_icon_name(icon_source, stockIcon.get()); + gtk_icon_set_add_source(icon_set, icon_source); + gtk_icon_source_free(icon_source); + } + + GdkPixbuf* icon = + gtk_icon_set_render_icon(icon_set, style, direction, state, + icon_size, gStockImageWidget, nullptr); + if (useIconName) { + gtk_icon_set_unref(icon_set); + } + + // According to documentation, gtk_icon_set_render_icon() never returns + // nullptr, but it does return nullptr when we have the problem reported + // here: https://bugzilla.gnome.org/show_bug.cgi?id=629878#c13 + if (!icon) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = moz_gdk_pixbuf_to_channel(icon, iconURI, + getter_AddRefs(mRealChannel)); + + g_object_unref(icon); + + return rv; +} + +void +nsIconChannel::Shutdown() { + if (gProtoWindow) { + gtk_widget_destroy(gProtoWindow); + gProtoWindow = nullptr; + gStockImageWidget = nullptr; + } +} diff --git a/image/decoders/icon/gtk/nsIconChannel.h b/image/decoders/icon/gtk/nsIconChannel.h new file mode 100644 index 000000000..5d59272d5 --- /dev/null +++ b/image/decoders/icon/gtk/nsIconChannel.h @@ -0,0 +1,43 @@ +/* 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_decoders_icon_gtk_nsIconChannel_h +#define mozilla_image_decoders_icon_gtk_nsIconChannel_h + +#include "mozilla/Attributes.h" + +#include "nsIChannel.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsIIconURI.h" +#include "nsCOMPtr.h" + +/// This class is the gnome implementation of nsIconChannel. It basically asks +/// gtk/gnome for an icon, saves it as a tmp icon, and creates a new channel for +/// that file to which all calls will be proxied. +class nsIconChannel final : public nsIChannel +{ + public: + NS_DECL_ISUPPORTS + NS_FORWARD_NSIREQUEST(mRealChannel->) + NS_FORWARD_NSICHANNEL(mRealChannel->) + + nsIconChannel() { } + + static void Shutdown(); + + /// Called by nsIconProtocolHandler after it creates this channel. + /// Must be called before calling any other function on this object. + /// If this method fails, no other function must be called on this object. + nsresult Init(nsIURI* aURI); + private: + ~nsIconChannel() { } + /// The channel to the temp icon file (e.g. to /tmp/2qy9wjqw.html). + /// Will always be non-null after a successful Init. + nsCOMPtr<nsIChannel> mRealChannel; + + nsresult InitWithGIO(nsIMozIconURI* aIconURI); +}; + +#endif // mozilla_image_decoders_icon_gtk_nsIconChannel_h diff --git a/image/decoders/icon/mac/moz.build b/image/decoders/icon/mac/moz.build new file mode 100644 index 000000000..158c326ea --- /dev/null +++ b/image/decoders/icon/mac/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 += [ + 'nsIconChannelCocoa.mm', +] + +FINAL_LIBRARY = 'xul' diff --git a/image/decoders/icon/mac/nsIconChannel.h b/image/decoders/icon/mac/nsIconChannel.h new file mode 100644 index 000000000..9fef17119 --- /dev/null +++ b/image/decoders/icon/mac/nsIconChannel.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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_icon_mac_nsIconChannel_h +#define mozilla_image_encoders_icon_mac_nsIconChannel_h + +#include "mozilla/Attributes.h" + +#include "nsCOMPtr.h" +#include "nsXPIDLString.h" +#include "nsIChannel.h" +#include "nsILoadGroup.h" +#include "nsILoadInfo.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" + +class nsIFile; + +class nsIconChannel final : public nsIChannel, public nsIStreamListener +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsIconChannel(); + + nsresult Init(nsIURI* uri); + +protected: + virtual ~nsIconChannel(); + + nsCOMPtr<nsIURI> mUrl; + nsCOMPtr<nsIURI> mOriginalURI; + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsISupports> mOwner; + nsCOMPtr<nsILoadInfo> mLoadInfo; + + nsCOMPtr<nsIInputStreamPump> mPump; + nsCOMPtr<nsIStreamListener> mListener; + + nsresult MakeInputStream(nsIInputStream** _retval, bool nonBlocking); + + nsresult ExtractIconInfoFromUrl(nsIFile** aLocalFile, + uint32_t* aDesiredImageSize, + nsACString& aContentType, + nsACString& aFileExtension); +}; + +#endif // mozilla_image_encoders_icon_mac_nsIconChannel_h diff --git a/image/decoders/icon/mac/nsIconChannelCocoa.mm b/image/decoders/icon/mac/nsIconChannelCocoa.mm new file mode 100644 index 000000000..9c2686cdd --- /dev/null +++ b/image/decoders/icon/mac/nsIconChannelCocoa.mm @@ -0,0 +1,565 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 "nsContentUtils.h" +#include "nsIconChannel.h" +#include "mozilla/EndianUtils.h" +#include "nsIIconURI.h" +#include "nsIServiceManager.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsXPIDLString.h" +#include "nsMimeTypes.h" +#include "nsMemory.h" +#include "nsIStringStream.h" +#include "nsIURL.h" +#include "nsNetCID.h" +#include "nsIPipe.h" +#include "nsIOutputStream.h" +#include "nsIMIMEService.h" +#include "nsCExternalHandlerService.h" +#include "nsILocalFileMac.h" +#include "nsIFileURL.h" +#include "nsTArray.h" +#include "nsObjCExceptions.h" +#include "nsProxyRelease.h" +#include "nsContentSecurityManager.h" + +#include <Cocoa/Cocoa.h> + +// nsIconChannel methods +nsIconChannel::nsIconChannel() +{ +} + +nsIconChannel::~nsIconChannel() +{ + if (mLoadInfo) { + NS_ReleaseOnMainThread(mLoadInfo.forget()); + } +} + +NS_IMPL_ISUPPORTS(nsIconChannel, + nsIChannel, + nsIRequest, + nsIRequestObserver, + nsIStreamListener) + +nsresult +nsIconChannel::Init(nsIURI* uri) +{ + NS_ASSERTION(uri, "no uri"); + mUrl = uri; + mOriginalURI = uri; + nsresult rv; + mPump = do_CreateInstance(NS_INPUTSTREAMPUMP_CONTRACTID, &rv); + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIRequest methods: + +NS_IMETHODIMP +nsIconChannel::GetName(nsACString& result) +{ + return mUrl->GetSpec(result); +} + +NS_IMETHODIMP +nsIconChannel::IsPending(bool* result) +{ + return mPump->IsPending(result); +} + +NS_IMETHODIMP +nsIconChannel::GetStatus(nsresult* status) +{ + return mPump->GetStatus(status); +} + +NS_IMETHODIMP +nsIconChannel::Cancel(nsresult status) +{ + return mPump->Cancel(status); +} + +NS_IMETHODIMP +nsIconChannel::Suspend(void) +{ + return mPump->Suspend(); +} + +NS_IMETHODIMP +nsIconChannel::Resume(void) +{ + return mPump->Resume(); +} + +// nsIRequestObserver methods +NS_IMETHODIMP +nsIconChannel::OnStartRequest(nsIRequest* aRequest, + nsISupports* aContext) +{ + if (mListener) { + return mListener->OnStartRequest(this, aContext); + } + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::OnStopRequest(nsIRequest* aRequest, + nsISupports* aContext, + nsresult aStatus) +{ + if (mListener) { + mListener->OnStopRequest(this, aContext, aStatus); + mListener = nullptr; + } + + // Remove from load group + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, aStatus); + } + + return NS_OK; +} + +// nsIStreamListener methods +NS_IMETHODIMP +nsIconChannel::OnDataAvailable(nsIRequest* aRequest, + nsISupports* aContext, + nsIInputStream* aStream, + uint64_t aOffset, + uint32_t aCount) +{ + if (mListener) { + return mListener->OnDataAvailable(this, aContext, aStream, aOffset, aCount); + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIChannel methods: + +NS_IMETHODIMP +nsIconChannel::GetOriginalURI(nsIURI** aURI) +{ + *aURI = mOriginalURI; + NS_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetOriginalURI(nsIURI* aURI) +{ + NS_ENSURE_ARG_POINTER(aURI); + mOriginalURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetURI(nsIURI** aURI) +{ + *aURI = mUrl; + NS_IF_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::Open(nsIInputStream** _retval) +{ + return MakeInputStream(_retval, false); +} + +NS_IMETHODIMP +nsIconChannel::Open2(nsIInputStream** aStream) +{ + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return Open(aStream); +} + +nsresult +nsIconChannel::ExtractIconInfoFromUrl(nsIFile** aLocalFile, + uint32_t* aDesiredImageSize, + nsACString& aContentType, + nsACString& aFileExtension) +{ + nsresult rv = NS_OK; + nsCOMPtr<nsIMozIconURI> iconURI (do_QueryInterface(mUrl, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + iconURI->GetImageSize(aDesiredImageSize); + iconURI->GetContentType(aContentType); + iconURI->GetFileExtension(aFileExtension); + + nsCOMPtr<nsIURL> url; + rv = iconURI->GetIconURL(getter_AddRefs(url)); + if (NS_FAILED(rv) || !url) return NS_OK; + + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(url, &rv); + if (NS_FAILED(rv) || !fileURL) return NS_OK; + + nsCOMPtr<nsIFile> file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv) || !file) return NS_OK; + + nsCOMPtr<nsILocalFileMac> localFileMac (do_QueryInterface(file, &rv)); + if (NS_FAILED(rv) || !localFileMac) return NS_OK; + + *aLocalFile = file; + NS_IF_ADDREF(*aLocalFile); + + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::AsyncOpen(nsIStreamListener* aListener, + nsISupports* ctxt) +{ + MOZ_ASSERT(!mLoadInfo || + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL && + nsContentUtils::IsSystemPrincipal(mLoadInfo->LoadingPrincipal())), + "security flags in loadInfo but asyncOpen2() not called"); + + nsCOMPtr<nsIInputStream> inStream; + nsresult rv = MakeInputStream(getter_AddRefs(inStream), true); + NS_ENSURE_SUCCESS(rv, rv); + + // Init our stream pump + rv = mPump->Init(inStream, int64_t(-1), int64_t(-1), 0, 0, false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mPump->AsyncRead(this, ctxt); + if (NS_SUCCEEDED(rv)) { + // Store our real listener + mListener = aListener; + // Add ourself to the load group, if available + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + } + + return rv; +} + +NS_IMETHODIMP +nsIconChannel::AsyncOpen2(nsIStreamListener* aListener) +{ + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return AsyncOpen(listener, nullptr); +} + +nsresult +nsIconChannel::MakeInputStream(nsIInputStream** _retval, + bool aNonBlocking) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsXPIDLCString contentType; + nsAutoCString fileExt; + nsCOMPtr<nsIFile> fileloc; // file we want an icon for + uint32_t desiredImageSize; + nsresult rv = ExtractIconInfoFromUrl(getter_AddRefs(fileloc), + &desiredImageSize, contentType, fileExt); + NS_ENSURE_SUCCESS(rv, rv); + + bool fileExists = false; + if (fileloc) { + // ensure that we DO NOT resolve aliases, very important for file views + fileloc->SetFollowLinks(false); + fileloc->Exists(&fileExists); + } + + NSImage* iconImage = nil; + + // first try to get the icon from the file if it exists + if (fileExists) { + nsCOMPtr<nsILocalFileMac> localFileMac(do_QueryInterface(fileloc, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + CFURLRef macURL; + if (NS_SUCCEEDED(localFileMac->GetCFURL(&macURL))) { + iconImage = [[NSWorkspace sharedWorkspace] + iconForFile:[(NSURL*)macURL path]]; + ::CFRelease(macURL); + } + } + + // if we don't have an icon yet try to get one by extension + if (!iconImage && !fileExt.IsEmpty()) { + NSString* fileExtension = [NSString stringWithUTF8String:fileExt.get()]; + iconImage = [[NSWorkspace sharedWorkspace] iconForFileType:fileExtension]; + } + + // If we still don't have an icon, get the generic document icon. + if (!iconImage) { + iconImage = [[NSWorkspace sharedWorkspace] + iconForFileType:NSFileTypeUnknown]; + } + + if (!iconImage) { + return NS_ERROR_FAILURE; + } + + // we have an icon now, size it + NSRect desiredSizeRect = NSMakeRect(0, 0, desiredImageSize, desiredImageSize); + [iconImage setSize:desiredSizeRect.size]; + + [iconImage lockFocus]; + NSBitmapImageRep* bitmapRep = [[[NSBitmapImageRep alloc] + initWithFocusedViewRect:desiredSizeRect] + autorelease]; + [iconImage unlockFocus]; + + // we expect the following things to be true about our bitmapRep + NS_ENSURE_TRUE(![bitmapRep isPlanar] && + // Not necessarily: on a HiDPI-capable system, we'll get + // a 2x bitmap + // (unsigned int)[bitmapRep bytesPerPlane] == + // desiredImageSize * desiredImageSize * 4 && + [bitmapRep bitsPerPixel] == 32 && + [bitmapRep samplesPerPixel] == 4 && + [bitmapRep hasAlpha] == YES, + NS_ERROR_UNEXPECTED); + + // check what size we actually got, and ensure it isn't too big to return + uint32_t actualImageSize = [bitmapRep bytesPerRow] / 4; + NS_ENSURE_TRUE(actualImageSize < 256, NS_ERROR_UNEXPECTED); + + // now we can validate the amount of data + NS_ENSURE_TRUE((unsigned int)[bitmapRep bytesPerPlane] == + actualImageSize * actualImageSize * 4, + NS_ERROR_UNEXPECTED); + + // rgba, pre-multiplied data + uint8_t* bitmapRepData = (uint8_t*)[bitmapRep bitmapData]; + + // create our buffer + int32_t bufferCapacity = 2 + [bitmapRep bytesPerPlane]; + AutoTArray<uint8_t, 3 + 16 * 16 * 5> iconBuffer; // initial size is for + // 16x16 + iconBuffer.SetLength(bufferCapacity); + + uint8_t* iconBufferPtr = iconBuffer.Elements(); + + // write header data into buffer + *iconBufferPtr++ = actualImageSize; + *iconBufferPtr++ = actualImageSize; + + uint32_t dataCount = [bitmapRep bytesPerPlane]; + uint32_t index = 0; + while (index < dataCount) { + // get data from the bitmap + uint8_t r = bitmapRepData[index++]; + uint8_t g = bitmapRepData[index++]; + uint8_t b = bitmapRepData[index++]; + uint8_t a = bitmapRepData[index++]; + + // write data out to our buffer + // non-cairo uses native image format, but the A channel is ignored. + // cairo uses ARGB (highest to lowest bits) +#if MOZ_LITTLE_ENDIAN + *iconBufferPtr++ = b; + *iconBufferPtr++ = g; + *iconBufferPtr++ = r; + *iconBufferPtr++ = a; +#else + *iconBufferPtr++ = a; + *iconBufferPtr++ = r; + *iconBufferPtr++ = g; + *iconBufferPtr++ = b; +#endif + } + + NS_ASSERTION(iconBufferPtr == iconBuffer.Elements() + bufferCapacity, + "buffer size miscalculation"); + + // Now, create a pipe and stuff our data into it + nsCOMPtr<nsIInputStream> inStream; + nsCOMPtr<nsIOutputStream> outStream; + rv = NS_NewPipe(getter_AddRefs(inStream), getter_AddRefs(outStream), + bufferCapacity, bufferCapacity, aNonBlocking); + + if (NS_SUCCEEDED(rv)) { + uint32_t written; + rv = outStream->Write((char*)iconBuffer.Elements(), bufferCapacity, + &written); + if (NS_SUCCEEDED(rv)) { + NS_IF_ADDREF(*_retval = inStream); + } + } + + // Drop notification callbacks to prevent cycles. + mCallbacks = nullptr; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadFlags(uint32_t* aLoadAttributes) +{ + return mPump->GetLoadFlags(aLoadAttributes); +} + +NS_IMETHODIMP +nsIconChannel::SetLoadFlags(uint32_t aLoadAttributes) +{ + return mPump->SetLoadFlags(aLoadAttributes); +} + +NS_IMETHODIMP +nsIconChannel::GetContentType(nsACString& aContentType) +{ + aContentType.AssignLiteral(IMAGE_ICON_MS); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetContentType(const nsACString& aContentType) +{ + //It doesn't make sense to set the content-type on this type + // of channel... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentCharset(nsACString& aContentCharset) +{ + aContentCharset.AssignLiteral(IMAGE_ICON_MS); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetContentCharset(const nsACString& aContentCharset) +{ + //It doesn't make sense to set the content-type on this type + // of channel... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentDisposition(uint32_t* aContentDisposition) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentDisposition(uint32_t aContentDisposition) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel:: + GetContentDispositionFilename(nsAString& aContentDispositionFilename) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel:: + SetContentDispositionFilename(const nsAString& aContentDispositionFilename) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel:: + GetContentDispositionHeader(nsACString& aContentDispositionHeader) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentLength(int64_t* aContentLength) +{ + *aContentLength = 0; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentLength(int64_t aContentLength) +{ + NS_NOTREACHED("nsIconChannel::SetContentLength"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) +{ + *aLoadGroup = mLoadGroup; + NS_IF_ADDREF(*aLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) +{ + mLoadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetOwner(nsISupports** aOwner) +{ + *aOwner = mOwner.get(); + NS_IF_ADDREF(*aOwner); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetOwner(nsISupports* aOwner) +{ + mOwner = aOwner; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) +{ + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) +{ + mLoadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel:: + GetNotificationCallbacks(nsIInterfaceRequestor** aNotificationCallbacks) +{ + *aNotificationCallbacks = mCallbacks.get(); + NS_IF_ADDREF(*aNotificationCallbacks); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel:: + SetNotificationCallbacks(nsIInterfaceRequestor* aNotificationCallbacks) +{ + mCallbacks = aNotificationCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetSecurityInfo(nsISupports** aSecurityInfo) +{ + *aSecurityInfo = nullptr; + return NS_OK; +} + diff --git a/image/decoders/icon/moz.build b/image/decoders/icon/moz.build new file mode 100644 index 000000000..9c6106fa7 --- /dev/null +++ b/image/decoders/icon/moz.build @@ -0,0 +1,32 @@ +# -*- 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/. + +UNIFIED_SOURCES += [ + 'nsIconModule.cpp', + 'nsIconProtocolHandler.cpp', + 'nsIconURI.cpp', +] + +FINAL_LIBRARY = 'xul' + +include('/ipc/chromium/chromium-config.mozbuild') + +platform = None + +if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + platform = 'gtk' + +if CONFIG['OS_ARCH'] == 'WINNT': + platform = 'win' + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa': + platform = 'mac' + +if CONFIG['OS_TARGET'] == 'Android': + platform = 'android' + +if platform: + LOCAL_INCLUDES += [platform] diff --git a/image/decoders/icon/nsIconModule.cpp b/image/decoders/icon/nsIconModule.cpp new file mode 100644 index 000000000..36225374d --- /dev/null +++ b/image/decoders/icon/nsIconModule.cpp @@ -0,0 +1,60 @@ +/* -*- 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 "mozilla/ModuleUtils.h" +#include "nsServiceManagerUtils.h" + +#include "nsIconProtocolHandler.h" +#include "nsIconURI.h" +#include "nsIconChannel.h" + +// objects that just require generic constructors +//***************************************************************************** +// Protocol CIDs + +#define NS_ICONPROTOCOL_CID { 0xd0f9db12, 0x249c, 0x11d5, \ + { 0x99, 0x5, 0x0, 0x10, 0x83, 0x1, 0xe, 0x9b } } + +NS_GENERIC_FACTORY_CONSTRUCTOR(nsIconProtocolHandler) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsMozIconURI) + +NS_DEFINE_NAMED_CID(NS_ICONPROTOCOL_CID); +NS_DEFINE_NAMED_CID(NS_MOZICONURI_CID); + +static const mozilla::Module::CIDEntry kIconCIDs[] = { + { &kNS_ICONPROTOCOL_CID, false, nullptr, nsIconProtocolHandlerConstructor }, + { &kNS_MOZICONURI_CID, false, nullptr, nsMozIconURIConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kIconContracts[] = { + { NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "moz-icon", &kNS_ICONPROTOCOL_CID }, + { nullptr } +}; + +static const mozilla::Module::CategoryEntry kIconCategories[] = { + { nullptr } +}; + +static void +IconDecoderModuleDtor() +{ +#if (MOZ_WIDGET_GTK == 2) + nsIconChannel::Shutdown(); +#endif +} + +static const mozilla::Module kIconModule = { + mozilla::Module::kVersion, + kIconCIDs, + kIconContracts, + kIconCategories, + nullptr, + nullptr, + IconDecoderModuleDtor +}; + +NSMODULE_DEFN(nsIconDecoderModule) = &kIconModule; diff --git a/image/decoders/icon/nsIconProtocolHandler.cpp b/image/decoders/icon/nsIconProtocolHandler.cpp new file mode 100644 index 000000000..03aaa2a14 --- /dev/null +++ b/image/decoders/icon/nsIconProtocolHandler.cpp @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 "nsIconProtocolHandler.h" + +#include "nsIconChannel.h" +#include "nsIconURI.h" +#include "nsIURL.h" +#include "nsCRT.h" +#include "nsCOMPtr.h" +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsNetCID.h" + +/////////////////////////////////////////////////////////////////////////////// + +nsIconProtocolHandler::nsIconProtocolHandler() +{ } + +nsIconProtocolHandler::~nsIconProtocolHandler() +{ } + +NS_IMPL_ISUPPORTS(nsIconProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) + + +/////////////////////////////////////////////////////////////////////////////// +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsIconProtocolHandler::GetScheme(nsACString& result) +{ + result = "moz-icon"; + return NS_OK; +} + +NS_IMETHODIMP +nsIconProtocolHandler::GetDefaultPort(int32_t* result) +{ + *result = 0; + return NS_OK; +} + +NS_IMETHODIMP +nsIconProtocolHandler::AllowPort(int32_t port, + const char* scheme, + bool* _retval) +{ + // don't override anything. + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsIconProtocolHandler::GetProtocolFlags(uint32_t* result) +{ + *result = URI_NORELATIVE | URI_NOAUTH | URI_IS_UI_RESOURCE | + URI_IS_LOCAL_RESOURCE; + return NS_OK; +} + +NS_IMETHODIMP +nsIconProtocolHandler::NewURI(const nsACString& aSpec, + const char* aOriginCharset, // ignored + nsIURI* aBaseURI, + nsIURI** result) +{ + nsCOMPtr<nsIMozIconURI> uri = new nsMozIconURI(); + if (!uri) return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv = uri->SetSpec(aSpec); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIURL> iconURL; + uri->GetIconURL(getter_AddRefs(iconURL)); + if (iconURL) { + uri = new nsNestedMozIconURI(); + rv = uri->SetSpec(aSpec); + if (NS_FAILED(rv)) { + return rv; + } + } + + NS_ADDREF(*result = uri); + return NS_OK; +} + +NS_IMETHODIMP +nsIconProtocolHandler::NewChannel2(nsIURI* url, + nsILoadInfo* aLoadInfo, + nsIChannel** result) +{ + NS_ENSURE_ARG_POINTER(url); + nsIconChannel* channel = new nsIconChannel; + if (!channel) { + return NS_ERROR_OUT_OF_MEMORY; + } + NS_ADDREF(channel); + + nsresult rv = channel->Init(url); + if (NS_FAILED(rv)) { + NS_RELEASE(channel); + return rv; + } + + // set the loadInfo on the new channel + rv = channel->SetLoadInfo(aLoadInfo); + if (NS_FAILED(rv)) { + NS_RELEASE(channel); + return rv; + } + + *result = channel; + return NS_OK; +} + +NS_IMETHODIMP +nsIconProtocolHandler::NewChannel(nsIURI* url, nsIChannel** result) +{ + return NewChannel2(url, nullptr, result); +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/image/decoders/icon/nsIconProtocolHandler.h b/image/decoders/icon/nsIconProtocolHandler.h new file mode 100644 index 000000000..9653d36f6 --- /dev/null +++ b/image/decoders/icon/nsIconProtocolHandler.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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_decoders_icon_nsIconProtocolHandler_h +#define mozilla_image_decoders_icon_nsIconProtocolHandler_h + +#include "nsWeakReference.h" +#include "nsIProtocolHandler.h" + +class nsIconProtocolHandler : public nsIProtocolHandler, + public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + + // nsIconProtocolHandler methods: + nsIconProtocolHandler(); + +protected: + virtual ~nsIconProtocolHandler(); +}; + +#endif // mozilla_image_decoders_icon_nsIconProtocolHandler_h diff --git a/image/decoders/icon/nsIconURI.cpp b/image/decoders/icon/nsIconURI.cpp new file mode 100644 index 000000000..2c2788c8f --- /dev/null +++ b/image/decoders/icon/nsIconURI.cpp @@ -0,0 +1,699 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set sw=2 sts=2 ts=2 et tw=80: + * + * 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 "nsIconURI.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/Sprintf.h" + +#include "nsIIOService.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "plstr.h" +#include <stdlib.h> + +using namespace mozilla; +using namespace mozilla::ipc; + +#define DEFAULT_IMAGE_SIZE 16 + +#if defined(MAX_PATH) +#define SANE_FILE_NAME_LEN MAX_PATH +#elif defined(PATH_MAX) +#define SANE_FILE_NAME_LEN PATH_MAX +#else +#define SANE_FILE_NAME_LEN 1024 +#endif + +// helper function for parsing out attributes like size, and contentType +// from the icon url. +static void extractAttributeValue(const char* aSearchString, + const char* aAttributeName, + nsCString& aResult); + +static const char* kSizeStrings[] = +{ + "button", + "toolbar", + "toolbarsmall", + "menu", + "dnd", + "dialog" +}; + +static const char* kStateStrings[] = +{ + "normal", + "disabled" +}; + +//////////////////////////////////////////////////////////////////////////////// + +nsMozIconURI::nsMozIconURI() + : mSize(DEFAULT_IMAGE_SIZE), + mIconSize(-1), + mIconState(-1) +{ } + +nsMozIconURI::~nsMozIconURI() +{ } + +NS_IMPL_ISUPPORTS(nsMozIconURI, nsIMozIconURI, nsIURI, nsIIPCSerializableURI) + +#define MOZICON_SCHEME "moz-icon:" +#define MOZICON_SCHEME_LEN (sizeof(MOZICON_SCHEME) - 1) + +//////////////////////////////////////////////////////////////////////////////// +// nsIURI methods: + +NS_IMETHODIMP +nsMozIconURI::GetSpec(nsACString& aSpec) +{ + aSpec = MOZICON_SCHEME; + + if (mIconURL) { + nsAutoCString fileIconSpec; + nsresult rv = mIconURL->GetSpec(fileIconSpec); + NS_ENSURE_SUCCESS(rv, rv); + aSpec += fileIconSpec; + } else if (!mStockIcon.IsEmpty()) { + aSpec += "//stock/"; + aSpec += mStockIcon; + } else { + aSpec += "//"; + aSpec += mFileName; + } + + aSpec += "?size="; + if (mIconSize >= 0) { + aSpec += kSizeStrings[mIconSize]; + } else { + char buf[20]; + SprintfLiteral(buf, "%d", mSize); + aSpec.Append(buf); + } + + if (mIconState >= 0) { + aSpec += "&state="; + aSpec += kStateStrings[mIconState]; + } + + if (!mContentType.IsEmpty()) { + aSpec += "&contentType="; + aSpec += mContentType.get(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetSpecIgnoringRef(nsACString& result) +{ + return GetSpec(result); +} + +NS_IMETHODIMP +nsMozIconURI::GetHasRef(bool* result) +{ + *result = false; + return NS_OK; +} + +// takes a string like ?size=32&contentType=text/html and returns a new string +// containing just the attribute value. i.e you could pass in this string with +// an attribute name of 'size=', this will return 32 +// Assumption: attribute pairs in the string are separated by '&'. +void +extractAttributeValue(const char* aSearchString, + const char* aAttributeName, + nsCString& aResult) +{ + //NS_ENSURE_ARG_POINTER(extractAttributeValue); + + aResult.Truncate(); + + if (aSearchString && aAttributeName) { + // search the string for attributeName + uint32_t attributeNameSize = strlen(aAttributeName); + const char* startOfAttribute = PL_strcasestr(aSearchString, aAttributeName); + if (startOfAttribute && + ( *(startOfAttribute-1) == '?' || *(startOfAttribute-1) == '&') ) { + startOfAttribute += attributeNameSize; // skip over the attributeName + // is there something after the attribute name + if (*startOfAttribute) { + const char* endofAttribute = strchr(startOfAttribute, '&'); + if (endofAttribute) { + aResult.Assign(Substring(startOfAttribute, endofAttribute)); + } else { + aResult.Assign(startOfAttribute); + } + } // if we have a attribute value + } // if we have a attribute name + } // if we got non-null search string and attribute name values +} + +NS_IMETHODIMP +nsMozIconURI::SetSpec(const nsACString& aSpec) +{ + // Reset everything to default values. + mIconURL = nullptr; + mSize = DEFAULT_IMAGE_SIZE; + mContentType.Truncate(); + mFileName.Truncate(); + mStockIcon.Truncate(); + mIconSize = -1; + mIconState = -1; + + nsAutoCString iconSpec(aSpec); + if (!Substring(iconSpec, 0, + MOZICON_SCHEME_LEN).EqualsLiteral(MOZICON_SCHEME)) { + return NS_ERROR_MALFORMED_URI; + } + + int32_t questionMarkPos = iconSpec.Find("?"); + if (questionMarkPos != -1 && + static_cast<int32_t>(iconSpec.Length()) > (questionMarkPos + 1)) { + extractAttributeValue(iconSpec.get(), "contentType=", mContentType); + + nsAutoCString sizeString; + extractAttributeValue(iconSpec.get(), "size=", sizeString); + if (!sizeString.IsEmpty()) { + const char* sizeStr = sizeString.get(); + for (uint32_t i = 0; i < ArrayLength(kSizeStrings); i++) { + if (PL_strcasecmp(sizeStr, kSizeStrings[i]) == 0) { + mIconSize = i; + break; + } + } + + int32_t sizeValue = atoi(sizeString.get()); + if (sizeValue > 0) { + mSize = sizeValue; + } + } + + nsAutoCString stateString; + extractAttributeValue(iconSpec.get(), "state=", stateString); + if (!stateString.IsEmpty()) { + const char* stateStr = stateString.get(); + for (uint32_t i = 0; i < ArrayLength(kStateStrings); i++) { + if (PL_strcasecmp(stateStr, kStateStrings[i]) == 0) { + mIconState = i; + break; + } + } + } + } + + int32_t pathLength = iconSpec.Length() - MOZICON_SCHEME_LEN; + if (questionMarkPos != -1) { + pathLength = questionMarkPos - MOZICON_SCHEME_LEN; + } + if (pathLength < 3) { + return NS_ERROR_MALFORMED_URI; + } + + nsAutoCString iconPath(Substring(iconSpec, MOZICON_SCHEME_LEN, pathLength)); + + // Icon URI path can have three forms: + // (1) //stock/<icon-identifier> + // (2) //<some dummy file with an extension> + // (3) a valid URL + + if (!strncmp("//stock/", iconPath.get(), 8)) { + mStockIcon.Assign(Substring(iconPath, 8)); + // An icon identifier must always be specified. + if (mStockIcon.IsEmpty()) { + return NS_ERROR_MALFORMED_URI; + } + return NS_OK; + } + + if (StringBeginsWith(iconPath, NS_LITERAL_CSTRING("//"))) { + // Sanity check this supposed dummy file name. + if (iconPath.Length() > SANE_FILE_NAME_LEN) { + return NS_ERROR_MALFORMED_URI; + } + iconPath.Cut(0, 2); + mFileName.Assign(iconPath); + } + + nsresult rv; + nsCOMPtr<nsIIOService> ioService(do_GetService(NS_IOSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> uri; + ioService->NewURI(iconPath, nullptr, nullptr, getter_AddRefs(uri)); + mIconURL = do_QueryInterface(uri); + if (mIconURL) { + mFileName.Truncate(); + } else if (mFileName.IsEmpty()) { + return NS_ERROR_MALFORMED_URI; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetPrePath(nsACString& prePath) +{ + prePath = MOZICON_SCHEME; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetScheme(nsACString& aScheme) +{ + aScheme = "moz-icon"; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::SetScheme(const nsACString& aScheme) +{ + // doesn't make sense to set the scheme of a moz-icon URL + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetUsername(nsACString& aUsername) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::SetUsername(const nsACString& aUsername) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetPassword(nsACString& aPassword) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::SetPassword(const nsACString& aPassword) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetUserPass(nsACString& aUserPass) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::SetUserPass(const nsACString& aUserPass) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetHostPort(nsACString& aHostPort) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::SetHostPort(const nsACString& aHostPort) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::SetHostAndPort(const nsACString& aHostPort) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetHost(nsACString& aHost) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::SetHost(const nsACString& aHost) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetPort(int32_t* aPort) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::SetPort(int32_t aPort) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetPath(nsACString& aPath) +{ + aPath.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::SetPath(const nsACString& aPath) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::GetRef(nsACString& aRef) +{ + aRef.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::SetRef(const nsACString& aRef) +{ + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMozIconURI::Equals(nsIURI* other, bool* result) +{ + *result = false; + NS_ENSURE_ARG_POINTER(other); + NS_PRECONDITION(result, "null pointer"); + + nsAutoCString spec1; + nsAutoCString spec2; + + nsresult rv = GetSpec(spec1); + NS_ENSURE_SUCCESS(rv, rv); + rv = other->GetSpec(spec2); + NS_ENSURE_SUCCESS(rv, rv); + + if (!PL_strcasecmp(spec1.get(), spec2.get())) { + *result = true; + } else { + *result = false; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::EqualsExceptRef(nsIURI* other, bool* result) +{ + // GetRef/SetRef not supported by nsMozIconURI, so + // EqualsExceptRef() is the same as Equals(). + return Equals(other, result); +} + +NS_IMETHODIMP +nsMozIconURI::SchemeIs(const char* aScheme, bool* aEquals) +{ + NS_ENSURE_ARG_POINTER(aEquals); + if (!aScheme) { + return NS_ERROR_INVALID_ARG; + } + + *aEquals = PL_strcasecmp("moz-icon", aScheme) ? false : true; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::Clone(nsIURI** result) +{ + nsCOMPtr<nsIURL> newIconURL; + if (mIconURL) { + nsCOMPtr<nsIURI> newURI; + nsresult rv = mIconURL->Clone(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) { + return rv; + } + newIconURL = do_QueryInterface(newURI, &rv); + if (NS_FAILED(rv)) { + return rv; + } + } + + nsMozIconURI* uri = new nsMozIconURI(); + newIconURL.swap(uri->mIconURL); + uri->mSize = mSize; + uri->mContentType = mContentType; + uri->mFileName = mFileName; + uri->mStockIcon = mStockIcon; + uri->mIconSize = mIconSize; + uri->mIconState = mIconState; + NS_ADDREF(*result = uri); + + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::CloneIgnoringRef(nsIURI** result) +{ + // GetRef/SetRef not supported by nsMozIconURI, so + // CloneIgnoringRef() is the same as Clone(). + return Clone(result); +} + +NS_IMETHODIMP +nsMozIconURI::CloneWithNewRef(const nsACString& newRef, nsIURI** result) +{ + // GetRef/SetRef not supported by nsMozIconURI, so + // CloneWithNewRef() is the same as Clone(). + return Clone(result); +} + + +NS_IMETHODIMP +nsMozIconURI::Resolve(const nsACString& relativePath, nsACString& result) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMozIconURI::GetAsciiSpec(nsACString& aSpecA) +{ + return GetSpec(aSpecA); +} + +NS_IMETHODIMP +nsMozIconURI::GetAsciiHostPort(nsACString& aHostPortA) +{ + return GetHostPort(aHostPortA); +} + +NS_IMETHODIMP +nsMozIconURI::GetAsciiHost(nsACString& aHostA) +{ + return GetHost(aHostA); +} + +NS_IMETHODIMP +nsMozIconURI::GetOriginCharset(nsACString& result) +{ + result.Truncate(); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIIconUri methods: + +NS_IMETHODIMP +nsMozIconURI::GetIconURL(nsIURL** aFileUrl) +{ + *aFileUrl = mIconURL; + NS_IF_ADDREF(*aFileUrl); + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::SetIconURL(nsIURL* aFileUrl) +{ + // this isn't called anywhere, needs to go through SetSpec parsing + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMozIconURI::GetImageSize(uint32_t* aImageSize) + // measured by # of pixels in a row. defaults to 16. +{ + *aImageSize = mSize; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::SetImageSize(uint32_t aImageSize) + // measured by # of pixels in a row. defaults to 16. +{ + mSize = aImageSize; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetContentType(nsACString& aContentType) +{ + aContentType = mContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::SetContentType(const nsACString& aContentType) +{ + mContentType = aContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetFileExtension(nsACString& aFileExtension) +{ + // First, try to get the extension from mIconURL if we have one + if (mIconURL) { + nsAutoCString fileExt; + if (NS_SUCCEEDED(mIconURL->GetFileExtension(fileExt))) { + if (!fileExt.IsEmpty()) { + // unfortunately, this code doesn't give us the required '.' in + // front of the extension so we have to do it ourselves. + aFileExtension.Assign('.'); + aFileExtension.Append(fileExt); + } + } + return NS_OK; + } + + if (!mFileName.IsEmpty()) { + // truncate the extension out of the file path... + const char* chFileName = mFileName.get(); // get the underlying buffer + const char* fileExt = strrchr(chFileName, '.'); + if (!fileExt) { + return NS_OK; + } + aFileExtension = fileExt; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetStockIcon(nsACString& aStockIcon) +{ + aStockIcon = mStockIcon; + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetIconSize(nsACString& aSize) +{ + if (mIconSize >= 0) { + aSize = kSizeStrings[mIconSize]; + } else { + aSize.Truncate(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMozIconURI::GetIconState(nsACString& aState) +{ + if (mIconState >= 0) { + aState = kStateStrings[mIconState]; + } else { + aState.Truncate(); + } + return NS_OK; +} +//////////////////////////////////////////////////////////////////////////////// +// nsIIPCSerializableURI methods: + +void +nsMozIconURI::Serialize(URIParams& aParams) +{ + IconURIParams params; + + if (mIconURL) { + URIParams iconURLParams; + SerializeURI(mIconURL, iconURLParams); + if (iconURLParams.type() == URIParams::T__None) { + // Serialization failed, bail. + return; + } + + params.uri() = iconURLParams; + } else { + params.uri() = void_t(); + } + + params.size() = mSize; + params.fileName() = mFileName; + params.stockIcon() = mStockIcon; + params.iconSize() = mIconSize; + params.iconState() = mIconState; + + aParams = params; +} + +bool +nsMozIconURI::Deserialize(const URIParams& aParams) +{ + if (aParams.type() != URIParams::TIconURIParams) { + MOZ_ASSERT_UNREACHABLE("Received unknown URI from other process!"); + return false; + } + + const IconURIParams& params = aParams.get_IconURIParams(); + if (params.uri().type() != OptionalURIParams::Tvoid_t) { + nsCOMPtr<nsIURI> uri = DeserializeURI(params.uri().get_URIParams()); + mIconURL = do_QueryInterface(uri); + if (!mIconURL) { + MOZ_ASSERT_UNREACHABLE("bad nsIURI passed"); + return false; + } + } + + mSize = params.size(); + mContentType = params.contentType(); + mFileName = params.fileName(); + mStockIcon = params.stockIcon(); + mIconSize = params.iconSize(); + mIconState = params.iconState(); + + return true; +} + +//////////////////////////////////////////////////////////// +// Nested version of nsIconURI + +nsNestedMozIconURI::nsNestedMozIconURI() +{ } + +nsNestedMozIconURI::~nsNestedMozIconURI() +{ } + +NS_IMPL_ISUPPORTS_INHERITED(nsNestedMozIconURI, nsMozIconURI, nsINestedURI) + +NS_IMETHODIMP +nsNestedMozIconURI::GetInnerURI(nsIURI** aURI) +{ + nsCOMPtr<nsIURI> iconURL = do_QueryInterface(mIconURL); + if (iconURL) { + iconURL.forget(aURI); + } else { + *aURI = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +nsNestedMozIconURI::GetInnermostURI(nsIURI** aURI) +{ + return NS_ImplGetInnermostURI(this, aURI); +} + diff --git a/image/decoders/icon/nsIconURI.h b/image/decoders/icon/nsIconURI.h new file mode 100644 index 000000000..1c0310bec --- /dev/null +++ b/image/decoders/icon/nsIconURI.h @@ -0,0 +1,63 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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_decoders_icon_nsIconURI_h +#define mozilla_image_decoders_icon_nsIconURI_h + +#include "nsIIconURI.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIIPCSerializableURI.h" +#include "nsINestedURI.h" + +class nsMozIconURI : public nsIMozIconURI + , public nsIIPCSerializableURI +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIURI + NS_DECL_NSIMOZICONURI + NS_DECL_NSIIPCSERIALIZABLEURI + + // nsMozIconURI + nsMozIconURI(); + +protected: + virtual ~nsMozIconURI(); + nsCOMPtr<nsIURL> mIconURL; // a URL that we want the icon for + uint32_t mSize; // the # of pixels in a row that we want for this image. + // Typically 16, 32, 128, etc. + nsCString mContentType; // optional field explicitly specifying the content + // type + nsCString mFileName; // for if we don't have an actual file path, we're just + // given a filename with an extension + nsCString mStockIcon; + int32_t mIconSize; // -1 if not specified, otherwise index into + // kSizeStrings + int32_t mIconState; // -1 if not specified, otherwise index into + // kStateStrings +}; + +// For moz-icon URIs that point to an actual file on disk and are +// therefore nested URIs +class nsNestedMozIconURI final : public nsMozIconURI + , public nsINestedURI +{ + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_NSIURI(nsMozIconURI::) + NS_FORWARD_NSIMOZICONURI(nsMozIconURI::) + NS_FORWARD_NSIIPCSERIALIZABLEURI(nsMozIconURI::) + + NS_DECL_NSINESTEDURI + + nsNestedMozIconURI(); + +protected: + virtual ~nsNestedMozIconURI(); + +}; + +#endif // mozilla_image_decoders_icon_nsIconURI_h diff --git a/image/decoders/icon/win/moz.build b/image/decoders/icon/win/moz.build new file mode 100644 index 000000000..6f763d953 --- /dev/null +++ b/image/decoders/icon/win/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 += [ + 'nsIconChannel.cpp', +] + +FINAL_LIBRARY = 'xul' diff --git a/image/decoders/icon/win/nsIconChannel.cpp b/image/decoders/icon/win/nsIconChannel.cpp new file mode 100644 index 000000000..9ddcbbc48 --- /dev/null +++ b/image/decoders/icon/win/nsIconChannel.cpp @@ -0,0 +1,841 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 "mozilla/ArrayUtils.h" + +#include "nsIconChannel.h" +#include "nsIIconURI.h" +#include "nsIServiceManager.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsXPIDLString.h" +#include "nsReadableUtils.h" +#include "nsMimeTypes.h" +#include "nsMemory.h" +#include "nsIStringStream.h" +#include "nsIURL.h" +#include "nsIOutputStream.h" +#include "nsIPipe.h" +#include "nsNetCID.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIMIMEService.h" +#include "nsCExternalHandlerService.h" +#include "nsDirectoryServiceDefs.h" +#include "nsProxyRelease.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" + +#ifdef _WIN32_WINNT +#undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 + +// we need windows.h to read out registry information... +#include <windows.h> +#include <shellapi.h> +#include <shlobj.h> +#include <objbase.h> +#include <wchar.h> + +using namespace mozilla; + +struct ICONFILEHEADER { + uint16_t ifhReserved; + uint16_t ifhType; + uint16_t ifhCount; +}; + +struct ICONENTRY { + int8_t ieWidth; + int8_t ieHeight; + uint8_t ieColors; + uint8_t ieReserved; + uint16_t iePlanes; + uint16_t ieBitCount; + uint32_t ieSizeImage; + uint32_t ieFileOffset; +}; + +// Match stock icons with names +static SHSTOCKICONID +GetStockIconIDForName(const nsACString& aStockName) +{ + return aStockName.EqualsLiteral("uac-shield") ? SIID_SHIELD : + SIID_INVALID; +} + +// nsIconChannel methods +nsIconChannel::nsIconChannel() +{ +} + +nsIconChannel::~nsIconChannel() +{ + if (mLoadInfo) { + NS_ReleaseOnMainThread(mLoadInfo.forget()); + } +} + +NS_IMPL_ISUPPORTS(nsIconChannel, + nsIChannel, + nsIRequest, + nsIRequestObserver, + nsIStreamListener) + +nsresult +nsIconChannel::Init(nsIURI* uri) +{ + NS_ASSERTION(uri, "no uri"); + mUrl = uri; + mOriginalURI = uri; + nsresult rv; + mPump = do_CreateInstance(NS_INPUTSTREAMPUMP_CONTRACTID, &rv); + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIRequest methods: + +NS_IMETHODIMP +nsIconChannel::GetName(nsACString& result) +{ + return mUrl->GetSpec(result); +} + +NS_IMETHODIMP +nsIconChannel::IsPending(bool* result) +{ + return mPump->IsPending(result); +} + +NS_IMETHODIMP +nsIconChannel::GetStatus(nsresult* status) +{ + return mPump->GetStatus(status); +} + +NS_IMETHODIMP +nsIconChannel::Cancel(nsresult status) +{ + return mPump->Cancel(status); +} + +NS_IMETHODIMP +nsIconChannel::Suspend(void) +{ + return mPump->Suspend(); +} + +NS_IMETHODIMP +nsIconChannel::Resume(void) +{ + return mPump->Resume(); +} +NS_IMETHODIMP +nsIconChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) +{ + *aLoadGroup = mLoadGroup; + NS_IF_ADDREF(*aLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) +{ + mLoadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadFlags(uint32_t* aLoadAttributes) +{ + return mPump->GetLoadFlags(aLoadAttributes); +} + +NS_IMETHODIMP +nsIconChannel::SetLoadFlags(uint32_t aLoadAttributes) +{ + return mPump->SetLoadFlags(aLoadAttributes); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIChannel methods: + +NS_IMETHODIMP +nsIconChannel::GetOriginalURI(nsIURI** aURI) +{ + *aURI = mOriginalURI; + NS_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetOriginalURI(nsIURI* aURI) +{ + NS_ENSURE_ARG_POINTER(aURI); + mOriginalURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetURI(nsIURI** aURI) +{ + *aURI = mUrl; + NS_IF_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::Open(nsIInputStream** _retval) +{ + return MakeInputStream(_retval, false); +} + +NS_IMETHODIMP +nsIconChannel::Open2(nsIInputStream** aStream) +{ + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return Open(aStream); +} + +nsresult +nsIconChannel::ExtractIconInfoFromUrl(nsIFile** aLocalFile, + uint32_t* aDesiredImageSize, nsCString& aContentType, + nsCString& aFileExtension) +{ + nsresult rv = NS_OK; + nsCOMPtr<nsIMozIconURI> iconURI (do_QueryInterface(mUrl, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + iconURI->GetImageSize(aDesiredImageSize); + iconURI->GetContentType(aContentType); + iconURI->GetFileExtension(aFileExtension); + + nsCOMPtr<nsIURL> url; + rv = iconURI->GetIconURL(getter_AddRefs(url)); + if (NS_FAILED(rv) || !url) return NS_OK; + + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(url, &rv); + if (NS_FAILED(rv) || !fileURL) return NS_OK; + + nsCOMPtr<nsIFile> file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv) || !file) return NS_OK; + + return file->Clone(aLocalFile); +} + +NS_IMETHODIMP +nsIconChannel::AsyncOpen(nsIStreamListener* aListener, + nsISupports* ctxt) +{ + MOZ_ASSERT(!mLoadInfo || + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL && + nsContentUtils::IsSystemPrincipal(mLoadInfo->LoadingPrincipal())), + "security flags in loadInfo but asyncOpen2() not called"); + + nsCOMPtr<nsIInputStream> inStream; + nsresult rv = MakeInputStream(getter_AddRefs(inStream), true); + if (NS_FAILED(rv)) { + return rv; + } + + // Init our streampump + rv = mPump->Init(inStream, int64_t(-1), int64_t(-1), 0, 0, false); + if (NS_FAILED(rv)) { + return rv; + } + + rv = mPump->AsyncRead(this, ctxt); + if (NS_SUCCEEDED(rv)) { + // Store our real listener + mListener = aListener; + // Add ourself to the load group, if available + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + } + return rv; +} + +NS_IMETHODIMP +nsIconChannel::AsyncOpen2(nsIStreamListener* aListener) +{ + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return AsyncOpen(listener, nullptr); +} + +static DWORD +GetSpecialFolderIcon(nsIFile* aFile, int aFolder, + SHFILEINFOW* aSFI, UINT aInfoFlags) +{ + DWORD shellResult = 0; + + if (!aFile) { + return shellResult; + } + + wchar_t fileNativePath[MAX_PATH]; + nsAutoString fileNativePathStr; + aFile->GetPath(fileNativePathStr); + ::GetShortPathNameW(fileNativePathStr.get(), fileNativePath, + ArrayLength(fileNativePath)); + + LPITEMIDLIST idList; + HRESULT hr = ::SHGetSpecialFolderLocation(nullptr, aFolder, &idList); + if (SUCCEEDED(hr)) { + wchar_t specialNativePath[MAX_PATH]; + ::SHGetPathFromIDListW(idList, specialNativePath); + ::GetShortPathNameW(specialNativePath, specialNativePath, + ArrayLength(specialNativePath)); + + if (!wcsicmp(fileNativePath, specialNativePath)) { + aInfoFlags |= (SHGFI_PIDL | SHGFI_SYSICONINDEX); + shellResult = ::SHGetFileInfoW((LPCWSTR)(LPCITEMIDLIST)idList, 0, + aSFI, + sizeof(*aSFI), aInfoFlags); + } + } + CoTaskMemFree(idList); + return shellResult; +} + +static UINT +GetSizeInfoFlag(uint32_t aDesiredImageSize) +{ + return + (UINT) (aDesiredImageSize > 16 ? SHGFI_SHELLICONSIZE : SHGFI_SMALLICON); +} + +nsresult +nsIconChannel::GetHIconFromFile(HICON* hIcon) +{ + nsXPIDLCString contentType; + nsCString fileExt; + nsCOMPtr<nsIFile> localFile; // file we want an icon for + uint32_t desiredImageSize; + nsresult rv = ExtractIconInfoFromUrl(getter_AddRefs(localFile), + &desiredImageSize, contentType, + fileExt); + NS_ENSURE_SUCCESS(rv, rv); + + // if the file exists, we are going to use it's real attributes... + // otherwise we only want to use it for it's extension... + SHFILEINFOW sfi; + UINT infoFlags = SHGFI_ICON; + + bool fileExists = false; + + nsAutoString filePath; + CopyASCIItoUTF16(fileExt, filePath); + if (localFile) { + rv = localFile->Normalize(); + NS_ENSURE_SUCCESS(rv, rv); + + localFile->GetPath(filePath); + if (filePath.Length() < 2 || filePath[1] != ':') { + return NS_ERROR_MALFORMED_URI; // UNC + } + + if (filePath.Last() == ':') { + filePath.Append('\\'); + } else { + localFile->Exists(&fileExists); + if (!fileExists) { + localFile->GetLeafName(filePath); + } + } + } + + if (!fileExists) { + infoFlags |= SHGFI_USEFILEATTRIBUTES; + } + + infoFlags |= GetSizeInfoFlag(desiredImageSize); + + // if we have a content type... then use it! but for existing files, + // we want to show their real icon. + if (!fileExists && !contentType.IsEmpty()) { + nsCOMPtr<nsIMIMEService> mimeService + (do_GetService(NS_MIMESERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString defFileExt; + mimeService->GetPrimaryExtension(contentType, fileExt, defFileExt); + // If the mime service does not know about this mime type, we show + // the generic icon. + // In any case, we need to insert a '.' before the extension. + filePath = NS_LITERAL_STRING(".") + + NS_ConvertUTF8toUTF16(defFileExt); + } + + // Is this the "Desktop" folder? + DWORD shellResult = GetSpecialFolderIcon(localFile, CSIDL_DESKTOP, + &sfi, infoFlags); + if (!shellResult) { + // Is this the "My Documents" folder? + shellResult = GetSpecialFolderIcon(localFile, CSIDL_PERSONAL, + &sfi, infoFlags); + } + + // There are other "Special Folders" and Namespace entities that we + // are not fetching icons for, see: + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ + // shellcc/platform/shell/reference/enums/csidl.asp + // If we ever need to get them, code to do so would be inserted here. + + // Not a special folder, or something else failed above. + if (!shellResult) { + shellResult = ::SHGetFileInfoW(filePath.get(), + FILE_ATTRIBUTE_ARCHIVE, + &sfi, sizeof(sfi), infoFlags); + } + + if (shellResult && sfi.hIcon) { + *hIcon = sfi.hIcon; + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + return rv; +} + +nsresult +nsIconChannel::GetStockHIcon(nsIMozIconURI* aIconURI, + HICON* hIcon) +{ + nsresult rv = NS_OK; + + // We can only do this on Vista or above + HMODULE hShellDLL = ::LoadLibraryW(L"shell32.dll"); + decltype(SHGetStockIconInfo)* pSHGetStockIconInfo = + (decltype(SHGetStockIconInfo)*) ::GetProcAddress(hShellDLL, + "SHGetStockIconInfo"); + + if (pSHGetStockIconInfo) { + uint32_t desiredImageSize; + aIconURI->GetImageSize(&desiredImageSize); + nsAutoCString stockIcon; + aIconURI->GetStockIcon(stockIcon); + + SHSTOCKICONID stockIconID = GetStockIconIDForName(stockIcon); + if (stockIconID == SIID_INVALID) { + return NS_ERROR_NOT_AVAILABLE; + } + + UINT infoFlags = SHGSI_ICON; + infoFlags |= GetSizeInfoFlag(desiredImageSize); + + SHSTOCKICONINFO sii = {0}; + sii.cbSize = sizeof(sii); + HRESULT hr = pSHGetStockIconInfo(stockIconID, infoFlags, &sii); + + if (SUCCEEDED(hr)) { + *hIcon = sii.hIcon; + } else { + rv = NS_ERROR_FAILURE; + } + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + if (hShellDLL) { + ::FreeLibrary(hShellDLL); + } + + return rv; +} + +// Given a BITMAPINFOHEADER, returns the size of the color table. +static int +GetColorTableSize(BITMAPINFOHEADER* aHeader) +{ + int colorTableSize = -1; + + // http://msdn.microsoft.com/en-us/library/dd183376%28v=VS.85%29.aspx + switch (aHeader->biBitCount) { + case 0: + colorTableSize = 0; + break; + case 1: + colorTableSize = 2 * sizeof(RGBQUAD); + break; + case 4: + case 8: { + // The maximum possible size for the color table is 2**bpp, so check for + // that and fail if we're not in those bounds + unsigned int maxEntries = 1 << (aHeader->biBitCount); + if (aHeader->biClrUsed > 0 && aHeader->biClrUsed <= maxEntries) { + colorTableSize = aHeader->biClrUsed * sizeof(RGBQUAD); + } else if (aHeader->biClrUsed == 0) { + colorTableSize = maxEntries * sizeof(RGBQUAD); + } + break; + } + case 16: + case 32: + // If we have BI_BITFIELDS compression, we would normally need 3 DWORDS for + // the bitfields mask which would be stored in the color table; However, + // we instead force the bitmap to request data of type BI_RGB so the color + // table should be of size 0. + // Setting aHeader->biCompression = BI_RGB forces the later call to + // GetDIBits to return to us BI_RGB data. + if (aHeader->biCompression == BI_BITFIELDS) { + aHeader->biCompression = BI_RGB; + } + colorTableSize = 0; + break; + case 24: + colorTableSize = 0; + break; + } + + if (colorTableSize < 0) { + NS_WARNING("Unable to figure out the color table size for this bitmap"); + } + + return colorTableSize; +} + +// Given a header and a size, creates a freshly allocated BITMAPINFO structure. +// It is the caller's responsibility to null-check and delete the structure. +static BITMAPINFO* +CreateBitmapInfo(BITMAPINFOHEADER* aHeader, size_t aColorTableSize) +{ + BITMAPINFO* bmi = (BITMAPINFO*) ::operator new(sizeof(BITMAPINFOHEADER) + + aColorTableSize, + mozilla::fallible); + if (bmi) { + memcpy(bmi, aHeader, sizeof(BITMAPINFOHEADER)); + memset(bmi->bmiColors, 0, aColorTableSize); + } + return bmi; +} + +nsresult +nsIconChannel::MakeInputStream(nsIInputStream** _retval, bool aNonBlocking) +{ + // Check whether the icon requested's a file icon or a stock icon + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + // GetDIBits does not exist on windows mobile. + HICON hIcon = nullptr; + + nsCOMPtr<nsIMozIconURI> iconURI(do_QueryInterface(mUrl, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString stockIcon; + iconURI->GetStockIcon(stockIcon); + if (!stockIcon.IsEmpty()) { + rv = GetStockHIcon(iconURI, &hIcon); + } else { + rv = GetHIconFromFile(&hIcon); + } + + NS_ENSURE_SUCCESS(rv, rv); + + if (hIcon) { + // we got a handle to an icon. Now we want to get a bitmap for the icon + // using GetIconInfo.... + ICONINFO iconInfo; + if (GetIconInfo(hIcon, &iconInfo)) { + // we got the bitmaps, first find out their size + HDC hDC = CreateCompatibleDC(nullptr); // get a device context for + // the screen. + BITMAPINFOHEADER maskHeader = {sizeof(BITMAPINFOHEADER)}; + BITMAPINFOHEADER colorHeader = {sizeof(BITMAPINFOHEADER)}; + int colorTableSize, maskTableSize; + if (GetDIBits(hDC, iconInfo.hbmMask, 0, 0, nullptr, + (BITMAPINFO*)&maskHeader, DIB_RGB_COLORS) && + GetDIBits(hDC, iconInfo.hbmColor, 0, 0, nullptr, + (BITMAPINFO*)&colorHeader, DIB_RGB_COLORS) && + maskHeader.biHeight == colorHeader.biHeight && + maskHeader.biWidth == colorHeader.biWidth && + colorHeader.biBitCount > 8 && + colorHeader.biSizeImage > 0 && + colorHeader.biWidth >= 0 && colorHeader.biWidth <= 255 && + colorHeader.biHeight >= 0 && colorHeader.biHeight <= 255 && + maskHeader.biSizeImage > 0 && + (colorTableSize = GetColorTableSize(&colorHeader)) >= 0 && + (maskTableSize = GetColorTableSize(&maskHeader)) >= 0) { + uint32_t iconSize = sizeof(ICONFILEHEADER) + + sizeof(ICONENTRY) + + sizeof(BITMAPINFOHEADER) + + colorHeader.biSizeImage + + maskHeader.biSizeImage; + + UniquePtr<char[]> buffer = MakeUnique<char[]>(iconSize); + if (!buffer) { + rv = NS_ERROR_OUT_OF_MEMORY; + } else { + char* whereTo = buffer.get(); + int howMuch; + + // the data starts with an icon file header + ICONFILEHEADER iconHeader; + iconHeader.ifhReserved = 0; + iconHeader.ifhType = 1; + iconHeader.ifhCount = 1; + howMuch = sizeof(ICONFILEHEADER); + memcpy(whereTo, &iconHeader, howMuch); + whereTo += howMuch; + + // followed by the single icon entry + ICONENTRY iconEntry; + iconEntry.ieWidth = static_cast<int8_t>(colorHeader.biWidth); + iconEntry.ieHeight = static_cast<int8_t>(colorHeader.biHeight); + iconEntry.ieColors = 0; + iconEntry.ieReserved = 0; + iconEntry.iePlanes = 1; + iconEntry.ieBitCount = colorHeader.biBitCount; + iconEntry.ieSizeImage = sizeof(BITMAPINFOHEADER) + + colorHeader.biSizeImage + + maskHeader.biSizeImage; + iconEntry.ieFileOffset = sizeof(ICONFILEHEADER) + sizeof(ICONENTRY); + howMuch = sizeof(ICONENTRY); + memcpy(whereTo, &iconEntry, howMuch); + whereTo += howMuch; + + // followed by the bitmap info header + // (doubling the height because icons have two bitmaps) + colorHeader.biHeight *= 2; + colorHeader.biSizeImage += maskHeader.biSizeImage; + howMuch = sizeof(BITMAPINFOHEADER); + memcpy(whereTo, &colorHeader, howMuch); + whereTo += howMuch; + colorHeader.biHeight /= 2; + colorHeader.biSizeImage -= maskHeader.biSizeImage; + + // followed by the XOR bitmap data (colorHeader) + // (you'd expect the color table to come here, but it apparently + // doesn't) + BITMAPINFO* colorInfo = CreateBitmapInfo(&colorHeader, + colorTableSize); + if (colorInfo && GetDIBits(hDC, iconInfo.hbmColor, 0, + colorHeader.biHeight, whereTo, colorInfo, + DIB_RGB_COLORS)) { + whereTo += colorHeader.biSizeImage; + + // and finally the AND bitmap data (maskHeader) + BITMAPINFO* maskInfo = CreateBitmapInfo(&maskHeader, maskTableSize); + if (maskInfo && GetDIBits(hDC, iconInfo.hbmMask, 0, + maskHeader.biHeight, whereTo, maskInfo, + DIB_RGB_COLORS)) { + // Now, create a pipe and stuff our data into it + nsCOMPtr<nsIInputStream> inStream; + nsCOMPtr<nsIOutputStream> outStream; + rv = NS_NewPipe(getter_AddRefs(inStream), + getter_AddRefs(outStream), + iconSize, iconSize, aNonBlocking); + if (NS_SUCCEEDED(rv)) { + uint32_t written; + rv = outStream->Write(buffer.get(), iconSize, &written); + if (NS_SUCCEEDED(rv)) { + NS_ADDREF(*_retval = inStream); + } + } + + } // if we got bitmap bits + delete maskInfo; + } // if we got mask bits + delete colorInfo; + } // if we allocated the buffer + } // if we got mask size + + DeleteDC(hDC); + DeleteObject(iconInfo.hbmColor); + DeleteObject(iconInfo.hbmMask); + } // if we got icon info + DestroyIcon(hIcon); + } // if we got an hIcon + + // If we didn't make a stream, then fail. + if (!*_retval && NS_SUCCEEDED(rv)) { + rv = NS_ERROR_NOT_AVAILABLE; + } + return rv; +} + +NS_IMETHODIMP +nsIconChannel::GetContentType(nsACString& aContentType) +{ + aContentType.AssignLiteral(IMAGE_ICO); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetContentType(const nsACString& aContentType) +{ + // It doesn't make sense to set the content-type on this type + // of channel... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsIconChannel::GetContentCharset(nsACString& aContentCharset) +{ + aContentCharset.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetContentCharset(const nsACString& aContentCharset) +{ + // It doesn't make sense to set the content-charset on this type + // of channel... + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentDisposition(uint32_t* aContentDisposition) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentDisposition(uint32_t aContentDisposition) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel:: + GetContentDispositionFilename(nsAString& aContentDispositionFilename) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel:: + SetContentDispositionFilename(const nsAString& aContentDispositionFilename) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel:: + GetContentDispositionHeader(nsACString& aContentDispositionHeader) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsIconChannel::GetContentLength(int64_t* aContentLength) +{ + *aContentLength = 0; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsIconChannel::SetContentLength(int64_t aContentLength) +{ + NS_NOTREACHED("nsIconChannel::SetContentLength"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsIconChannel::GetOwner(nsISupports** aOwner) +{ + *aOwner = mOwner.get(); + NS_IF_ADDREF(*aOwner); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetOwner(nsISupports* aOwner) +{ + mOwner = aOwner; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) +{ + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) +{ + mLoadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel:: + GetNotificationCallbacks(nsIInterfaceRequestor** aNotificationCallbacks) +{ + *aNotificationCallbacks = mCallbacks.get(); + NS_IF_ADDREF(*aNotificationCallbacks); + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel:: + SetNotificationCallbacks(nsIInterfaceRequestor* aNotificationCallbacks) +{ + mCallbacks = aNotificationCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::GetSecurityInfo(nsISupports** aSecurityInfo) +{ + *aSecurityInfo = nullptr; + return NS_OK; +} + +// nsIRequestObserver methods +NS_IMETHODIMP nsIconChannel::OnStartRequest(nsIRequest* aRequest, + nsISupports* aContext) +{ + if (mListener) { + return mListener->OnStartRequest(this, aContext); + } + return NS_OK; +} + +NS_IMETHODIMP +nsIconChannel::OnStopRequest(nsIRequest* aRequest, + nsISupports* aContext, + nsresult aStatus) +{ + if (mListener) { + mListener->OnStopRequest(this, aContext, aStatus); + mListener = nullptr; + } + + // Remove from load group + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, aStatus); + } + + // Drop notification callbacks to prevent cycles. + mCallbacks = nullptr; + + return NS_OK; +} + +// nsIStreamListener methods +NS_IMETHODIMP +nsIconChannel::OnDataAvailable(nsIRequest* aRequest, + nsISupports* aContext, + nsIInputStream* aStream, + uint64_t aOffset, + uint32_t aCount) +{ + if (mListener) { + return mListener->OnDataAvailable(this, aContext, aStream, aOffset, aCount); + } + return NS_OK; +} diff --git a/image/decoders/icon/win/nsIconChannel.h b/image/decoders/icon/win/nsIconChannel.h new file mode 100644 index 000000000..94df5a52e --- /dev/null +++ b/image/decoders/icon/win/nsIconChannel.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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_icon_win_nsIconChannel_h +#define mozilla_image_encoders_icon_win_nsIconChannel_h + +#include "mozilla/Attributes.h" + +#include "nsCOMPtr.h" +#include "nsXPIDLString.h" +#include "nsIChannel.h" +#include "nsILoadGroup.h" +#include "nsILoadInfo.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIURI.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamListener.h" +#include "nsIIconURI.h" + +#include <windows.h> + +class nsIFile; + +class nsIconChannel final : public nsIChannel, public nsIStreamListener +{ + ~nsIconChannel(); + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsIconChannel(); + + nsresult Init(nsIURI* uri); + +protected: + nsCOMPtr<nsIURI> mUrl; + nsCOMPtr<nsIURI> mOriginalURI; + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsISupports> mOwner; + nsCOMPtr<nsILoadInfo> mLoadInfo; + + nsCOMPtr<nsIInputStreamPump> mPump; + nsCOMPtr<nsIStreamListener> mListener; + + nsresult ExtractIconInfoFromUrl(nsIFile** aLocalFile, + uint32_t* aDesiredImageSize, + nsCString& aContentType, + nsCString& aFileExtension); + nsresult GetHIconFromFile(HICON* hIcon); + nsresult MakeInputStream(nsIInputStream** _retval, bool nonBlocking); + + // Functions specific to Vista and above +protected: + nsresult GetStockHIcon(nsIMozIconURI* aIconURI, HICON* hIcon); +}; + +#endif // mozilla_image_encoders_icon_win_nsIconChannel_h diff --git a/image/decoders/moz.build b/image/decoders/moz.build new file mode 100644 index 000000000..775c3eaa5 --- /dev/null +++ b/image/decoders/moz.build @@ -0,0 +1,47 @@ +# -*- 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/. + +toolkit = CONFIG['MOZ_WIDGET_TOOLKIT'] + +# The Icon Channel stuff really shouldn't live in decoders/icon, but we'll +# fix that another time. +if 'gtk' in toolkit: + DIRS += ['icon/gtk', 'icon'] + +if CONFIG['OS_ARCH'] == 'WINNT': + DIRS += ['icon/win', 'icon'] + +if toolkit == 'cocoa': + DIRS += ['icon/mac', 'icon'] +elif toolkit == 'android': + DIRS += ['icon/android', 'icon'] + +UNIFIED_SOURCES += [ + 'EXIF.cpp', + 'iccjpeg.c', + 'nsBMPDecoder.cpp', + 'nsGIFDecoder2.cpp', + 'nsICODecoder.cpp', + 'nsIconDecoder.cpp', + 'nsJPEGDecoder.cpp', + 'nsPNGDecoder.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +LOCAL_INCLUDES += [ + # Access to Skia headers for Downscaler. + '/gfx/2d', + # Decoders need ImageLib headers. + '/image', +] + +LOCAL_INCLUDES += CONFIG['SKIA_INCLUDES'] + +FINAL_LIBRARY = 'xul' + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/image/decoders/nsBMPDecoder.cpp b/image/decoders/nsBMPDecoder.cpp new file mode 100644 index 000000000..1f0449e4e --- /dev/null +++ b/image/decoders/nsBMPDecoder.cpp @@ -0,0 +1,1067 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 BMP Decoder, which should work everywhere, +// including big-endian machines like the PowerPC. +// +// BMP is a format that has been extended multiple times. To understand the +// decoder you need to understand this history. The summary of the history +// below was determined from the following documents. +// +// - http://www.fileformat.info/format/bmp/egff.htm +// - http://www.fileformat.info/format/os2bmp/egff.htm +// - http://fileformats.archiveteam.org/wiki/BMP +// - http://fileformats.archiveteam.org/wiki/OS/2_BMP +// - https://en.wikipedia.org/wiki/BMP_file_format +// - https://upload.wikimedia.org/wikipedia/commons/c/c4/BMPfileFormat.png +// +// WINDOWS VERSIONS OF THE BMP FORMAT +// ---------------------------------- +// WinBMPv1. +// - This version is no longer used and can be ignored. +// +// WinBMPv2. +// - First is a 14 byte file header that includes: the magic number ("BM"), +// file size, and offset to the pixel data (|mDataOffset|). +// - Next is a 12 byte info header which includes: the info header size +// (mBIHSize), width, height, number of color planes, and bits-per-pixel +// (|mBpp|) which must be 1, 4, 8 or 24. +// - Next is the semi-optional color table, which has length 2^|mBpp| and has 3 +// bytes per value (BGR). The color table is required if |mBpp| is 1, 4, or 8. +// - Next is an optional gap. +// - Next is the pixel data, which is pointed to by |mDataOffset|. +// +// WinBMPv3. This is the most widely used version. +// - It changed the info header to 40 bytes by taking the WinBMPv2 info +// header, enlargening its width and height fields, and adding more fields +// including: a compression type (|mCompression|) and number of colors +// (|mNumColors|). +// - The semi-optional color table is now 4 bytes per value (BGR0), and its +// length is |mNumColors|, or 2^|mBpp| if |mNumColors| is zero. +// - |mCompression| can be RGB (i.e. no compression), RLE4 (if |mBpp|==4) or +// RLE8 (if |mBpp|==8) values. +// +// WinBMPv3-NT. A variant of WinBMPv3. +// - It did not change the info header layout from WinBMPv3. +// - |mBpp| can now be 16 or 32, in which case |mCompression| can be RGB or the +// new BITFIELDS value; in the latter case an additional 12 bytes of color +// bitfields follow the info header. +// +// WinBMPv4. +// - It extended the info header to 108 bytes, including the 12 bytes of color +// mask data from WinBMPv3-NT, plus alpha mask data, and also color-space and +// gamma correction fields. +// +// WinBMPv5. +// - It extended the info header to 124 bytes, adding color profile data. +// - It also added an optional color profile table after the pixel data (and +// another optional gap). +// +// WinBMPv3-ICO. This is a variant of WinBMPv3. +// - It's the BMP format used for BMP images within ICO files. +// - The only difference with WinBMPv3 is that if an image is 32bpp and has no +// compression, then instead of treating the pixel data as 0RGB it is treated +// as ARGB, but only if one or more of the A values are non-zero. +// +// OS/2 VERSIONS OF THE BMP FORMAT +// ------------------------------- +// OS2-BMPv1. +// - Almost identical to WinBMPv2; the differences are basically ignorable. +// +// OS2-BMPv2. +// - Similar to WinBMPv3. +// - The info header is 64 bytes but can be reduced to as little as 16; any +// omitted fields are treated as zero. The first 40 bytes of these fields are +// nearly identical to the WinBMPv3 info header; the remaining 24 bytes are +// different. +// - Also adds compression types "Huffman 1D" and "RLE24", which we don't +// support. +// - We treat OS2-BMPv2 files as if they are WinBMPv3 (i.e. ignore the extra 24 +// bytes in the info header), which in practice is good enough. + +#include "ImageLogging.h" +#include "nsBMPDecoder.h" + +#include <stdlib.h> + +#include "mozilla/Attributes.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/Likely.h" + +#include "nsIInputStream.h" +#include "RasterImage.h" +#include <algorithm> + +using namespace mozilla::gfx; + +namespace mozilla { +namespace image { +namespace bmp { + +struct Compression { + enum { + RGB = 0, + RLE8 = 1, + RLE4 = 2, + BITFIELDS = 3 + }; +}; + +// RLE escape codes and constants. +struct RLE { + enum { + ESCAPE = 0, + ESCAPE_EOL = 0, + ESCAPE_EOF = 1, + ESCAPE_DELTA = 2, + + SEGMENT_LENGTH = 2, + DELTA_LENGTH = 2 + }; +}; + +} // namespace bmp + +using namespace bmp; + +/// Sets the pixel data in aDecoded to the given values. +/// @param aDecoded pointer to pixel to be set, will be incremented to point to +/// the next pixel. +static void +SetPixel(uint32_t*& aDecoded, uint8_t aRed, uint8_t aGreen, + uint8_t aBlue, uint8_t aAlpha = 0xFF) +{ + *aDecoded++ = gfxPackedPixel(aAlpha, aRed, aGreen, aBlue); +} + +static void +SetPixel(uint32_t*& aDecoded, uint8_t idx, + const UniquePtr<ColorTableEntry[]>& aColors) +{ + SetPixel(aDecoded, + aColors[idx].mRed, aColors[idx].mGreen, aColors[idx].mBlue); +} + +/// Sets two (or one if aCount = 1) pixels +/// @param aDecoded where the data is stored. Will be moved 4 resp 8 bytes +/// depending on whether one or two pixels are written. +/// @param aData The values for the two pixels +/// @param aCount Current count. Is decremented by one or two. +static void +Set4BitPixel(uint32_t*& aDecoded, uint8_t aData, uint32_t& aCount, + const UniquePtr<ColorTableEntry[]>& aColors) +{ + uint8_t idx = aData >> 4; + SetPixel(aDecoded, idx, aColors); + if (--aCount > 0) { + idx = aData & 0xF; + SetPixel(aDecoded, idx, aColors); + --aCount; + } +} + +static mozilla::LazyLogModule sBMPLog("BMPDecoder"); + +// The length of the mBIHSize field in the info header. +static const uint32_t BIHSIZE_FIELD_LENGTH = 4; + +nsBMPDecoder::nsBMPDecoder(RasterImage* aImage, State aState, size_t aLength) + : Decoder(aImage) + , mLexer(Transition::To(aState, aLength), Transition::TerminateSuccess()) + , mIsWithinICO(false) + , mMayHaveTransparency(false) + , mDoesHaveTransparency(false) + , mNumColors(0) + , mColors(nullptr) + , mBytesPerColor(0) + , mPreGapLength(0) + , mPixelRowSize(0) + , mCurrentRow(0) + , mCurrentPos(0) + , mAbsoluteModeNumPixels(0) +{ +} + +// Constructor for normal BMP files. +nsBMPDecoder::nsBMPDecoder(RasterImage* aImage) + : nsBMPDecoder(aImage, State::FILE_HEADER, FILE_HEADER_LENGTH) +{ +} + +// Constructor used for WinBMPv3-ICO files, which lack a file header. +nsBMPDecoder::nsBMPDecoder(RasterImage* aImage, uint32_t aDataOffset) + : nsBMPDecoder(aImage, State::INFO_HEADER_SIZE, BIHSIZE_FIELD_LENGTH) +{ + SetIsWithinICO(); + + // Even though the file header isn't present in this case, the dataOffset + // field is set as if it is, and so we must increment mPreGapLength + // accordingly. + mPreGapLength += FILE_HEADER_LENGTH; + + // This is the one piece of data we normally get from a BMP file header, so + // it must be provided via an argument. + mH.mDataOffset = aDataOffset; +} + +nsBMPDecoder::~nsBMPDecoder() +{ +} + +// Obtains the size of the compressed image resource. +int32_t +nsBMPDecoder::GetCompressedImageSize() const +{ + // In the RGB case mImageSize might not be set, so compute it manually. + MOZ_ASSERT(mPixelRowSize != 0); + return mH.mCompression == Compression::RGB + ? mPixelRowSize * AbsoluteHeight() + : mH.mImageSize; +} + +nsresult +nsBMPDecoder::BeforeFinishInternal() +{ + if (!IsMetadataDecode() && !mImageData) { + return NS_ERROR_FAILURE; // No image; something went wrong. + } + + return NS_OK; +} + +nsresult +nsBMPDecoder::FinishInternal() +{ + // We shouldn't be called in error cases. + MOZ_ASSERT(!HasError(), "Can't call FinishInternal on error!"); + + // We should never make multiple frames. + MOZ_ASSERT(GetFrameCount() <= 1, "Multiple BMP frames?"); + + // Send notifications if appropriate. + if (!IsMetadataDecode() && HasSize()) { + + // We should have image data. + MOZ_ASSERT(mImageData); + + // If it was truncated, fill in the missing pixels as black. + while (mCurrentRow > 0) { + uint32_t* dst = RowBuffer(); + while (mCurrentPos < mH.mWidth) { + SetPixel(dst, 0, 0, 0); + mCurrentPos++; + } + mCurrentPos = 0; + FinishRow(); + } + + // Invalidate. + nsIntRect r(0, 0, mH.mWidth, AbsoluteHeight()); + PostInvalidation(r); + + MOZ_ASSERT_IF(mDoesHaveTransparency, mMayHaveTransparency); + + // We have transparency if we either detected some in the image itself + // (i.e., |mDoesHaveTransparency| is true) or we're in an ICO, which could + // mean we have an AND mask that provides transparency (i.e., |mIsWithinICO| + // is true). + // XXX(seth): We can tell when we create the decoder if the AND mask is + // present, so we could be more precise about this. + const Opacity opacity = mDoesHaveTransparency || mIsWithinICO + ? Opacity::SOME_TRANSPARENCY + : Opacity::FULLY_OPAQUE; + + PostFrameStop(opacity); + PostDecodeDone(); + } + + return NS_OK; +} + +// ---------------------------------------- +// Actual Data Processing +// ---------------------------------------- + +void +BitFields::Value::Set(uint32_t aMask) +{ + mMask = aMask; + + // Handle this exceptional case first. The chosen values don't matter + // (because a mask of zero will always give a value of zero) except that + // mBitWidth: + // - shouldn't be zero, because that would cause an infinite loop in Get(); + // - shouldn't be 5 or 8, because that could cause a false positive match in + // IsR5G5B5() or IsR8G8B8(). + if (mMask == 0x0) { + mRightShift = 0; + mBitWidth = 1; + return; + } + + // Find the rightmost 1. + uint8_t i; + for (i = 0; i < 32; i++) { + if (mMask & (1 << i)) { + break; + } + } + mRightShift = i; + + // Now find the leftmost 1 in the same run of 1s. (If there are multiple runs + // of 1s -- which isn't valid -- we'll behave as if only the lowest run was + // present, which seems reasonable.) + for (i = i + 1; i < 32; i++) { + if (!(mMask & (1 << i))) { + break; + } + } + mBitWidth = i - mRightShift; +} + +MOZ_ALWAYS_INLINE uint8_t +BitFields::Value::Get(uint32_t aValue) const +{ + // Extract the unscaled value. + uint32_t v = (aValue & mMask) >> mRightShift; + + // Idea: to upscale v precisely we need to duplicate its bits, possibly + // repeatedly, possibly partially in the last case, from bit 7 down to bit 0 + // in v2. For example: + // + // - mBitWidth=1: v2 = v<<7 | v<<6 | ... | v<<1 | v>>0 k -> kkkkkkkk + // - mBitWidth=2: v2 = v<<6 | v<<4 | v<<2 | v>>0 jk -> jkjkjkjk + // - mBitWidth=3: v2 = v<<5 | v<<2 | v>>1 ijk -> ijkijkij + // - mBitWidth=4: v2 = v<<4 | v>>0 hijk -> hijkhijk + // - mBitWidth=5: v2 = v<<3 | v>>2 ghijk -> ghijkghi + // - mBitWidth=6: v2 = v<<2 | v>>4 fghijk -> fghijkfg + // - mBitWidth=7: v2 = v<<1 | v>>6 efghijk -> efghijke + // - mBitWidth=8: v2 = v>>0 defghijk -> defghijk + // - mBitWidth=9: v2 = v>>1 cdefghijk -> cdefghij + // - mBitWidth=10: v2 = v>>2 bcdefghijk -> bcdefghi + // - mBitWidth=11: v2 = v>>3 abcdefghijk -> abcdefgh + // - etc. + // + uint8_t v2 = 0; + int32_t i; // must be a signed integer + for (i = 8 - mBitWidth; i > 0; i -= mBitWidth) { + v2 |= v << uint32_t(i); + } + v2 |= v >> uint32_t(-i); + return v2; +} + +MOZ_ALWAYS_INLINE uint8_t +BitFields::Value::GetAlpha(uint32_t aValue, bool& aHasAlphaOut) const +{ + if (mMask == 0x0) { + return 0xff; + } + aHasAlphaOut = true; + return Get(aValue); +} + +MOZ_ALWAYS_INLINE uint8_t +BitFields::Value::Get5(uint32_t aValue) const +{ + MOZ_ASSERT(mBitWidth == 5); + uint32_t v = (aValue & mMask) >> mRightShift; + return (v << 3u) | (v >> 2u); +} + +MOZ_ALWAYS_INLINE uint8_t +BitFields::Value::Get8(uint32_t aValue) const +{ + MOZ_ASSERT(mBitWidth == 8); + uint32_t v = (aValue & mMask) >> mRightShift; + return v; +} + +void +BitFields::SetR5G5B5() +{ + mRed.Set(0x7c00); + mGreen.Set(0x03e0); + mBlue.Set(0x001f); +} + +void +BitFields::SetR8G8B8() +{ + mRed.Set(0xff0000); + mGreen.Set(0xff00); + mBlue.Set(0x00ff); +} + +bool +BitFields::IsR5G5B5() const +{ + return mRed.mBitWidth == 5 && + mGreen.mBitWidth == 5 && + mBlue.mBitWidth == 5 && + mAlpha.mMask == 0x0; +} + +bool +BitFields::IsR8G8B8() const +{ + return mRed.mBitWidth == 8 && + mGreen.mBitWidth == 8 && + mBlue.mBitWidth == 8 && + mAlpha.mMask == 0x0; +} + +uint32_t* +nsBMPDecoder::RowBuffer() +{ + if (mDownscaler) { + return reinterpret_cast<uint32_t*>(mDownscaler->RowBuffer()) + mCurrentPos; + } + + // Convert from row (1..mHeight) to absolute line (0..mHeight-1). + int32_t line = (mH.mHeight < 0) + ? -mH.mHeight - mCurrentRow + : mCurrentRow - 1; + int32_t offset = line * mH.mWidth + mCurrentPos; + return reinterpret_cast<uint32_t*>(mImageData) + offset; +} + +void +nsBMPDecoder::FinishRow() +{ + if (mDownscaler) { + mDownscaler->CommitRow(); + + if (mDownscaler->HasInvalidation()) { + DownscalerInvalidRect invalidRect = mDownscaler->TakeInvalidRect(); + PostInvalidation(invalidRect.mOriginalSizeRect, + Some(invalidRect.mTargetSizeRect)); + } + } else { + PostInvalidation(IntRect(0, mCurrentRow, mH.mWidth, 1)); + } + mCurrentRow--; +} + +LexerResult +nsBMPDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) +{ + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::FILE_HEADER: return ReadFileHeader(aData, aLength); + case State::INFO_HEADER_SIZE: return ReadInfoHeaderSize(aData, aLength); + case State::INFO_HEADER_REST: return ReadInfoHeaderRest(aData, aLength); + case State::BITFIELDS: return ReadBitfields(aData, aLength); + case State::COLOR_TABLE: return ReadColorTable(aData, aLength); + case State::GAP: return SkipGap(); + case State::AFTER_GAP: return AfterGap(); + case State::PIXEL_ROW: return ReadPixelRow(aData); + case State::RLE_SEGMENT: return ReadRLESegment(aData); + case State::RLE_DELTA: return ReadRLEDelta(aData); + case State::RLE_ABSOLUTE: return ReadRLEAbsolute(aData, aLength); + default: + MOZ_CRASH("Unknown State"); + } + }); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadFileHeader(const char* aData, size_t aLength) +{ + mPreGapLength += aLength; + + bool signatureOk = aData[0] == 'B' && aData[1] == 'M'; + if (!signatureOk) { + return Transition::TerminateFailure(); + } + + // We ignore the filesize (aData + 2) and reserved (aData + 6) fields. + + mH.mDataOffset = LittleEndian::readUint32(aData + 10); + + return Transition::To(State::INFO_HEADER_SIZE, BIHSIZE_FIELD_LENGTH); +} + +// We read the info header in two steps: (a) read the mBIHSize field to +// determine how long the header is; (b) read the rest of the header. +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadInfoHeaderSize(const char* aData, size_t aLength) +{ + mPreGapLength += aLength; + + mH.mBIHSize = LittleEndian::readUint32(aData); + + bool bihSizeOk = mH.mBIHSize == InfoHeaderLength::WIN_V2 || + mH.mBIHSize == InfoHeaderLength::WIN_V3 || + mH.mBIHSize == InfoHeaderLength::WIN_V4 || + mH.mBIHSize == InfoHeaderLength::WIN_V5 || + (mH.mBIHSize >= InfoHeaderLength::OS2_V2_MIN && + mH.mBIHSize <= InfoHeaderLength::OS2_V2_MAX); + if (!bihSizeOk) { + return Transition::TerminateFailure(); + } + // ICO BMPs must have a WinBMPv3 header. nsICODecoder should have already + // terminated decoding if this isn't the case. + MOZ_ASSERT_IF(mIsWithinICO, mH.mBIHSize == InfoHeaderLength::WIN_V3); + + return Transition::To(State::INFO_HEADER_REST, + mH.mBIHSize - BIHSIZE_FIELD_LENGTH); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadInfoHeaderRest(const char* aData, size_t aLength) +{ + mPreGapLength += aLength; + + // |mWidth| and |mHeight| may be signed (Windows) or unsigned (OS/2). We just + // read as unsigned because in practice that's good enough. + if (mH.mBIHSize == InfoHeaderLength::WIN_V2) { + mH.mWidth = LittleEndian::readUint16(aData + 0); + mH.mHeight = LittleEndian::readUint16(aData + 2); + // We ignore the planes (aData + 4) field; it should always be 1. + mH.mBpp = LittleEndian::readUint16(aData + 6); + } else { + mH.mWidth = LittleEndian::readUint32(aData + 0); + mH.mHeight = LittleEndian::readUint32(aData + 4); + // We ignore the planes (aData + 4) field; it should always be 1. + mH.mBpp = LittleEndian::readUint16(aData + 10); + + // For OS2-BMPv2 the info header may be as little as 16 bytes, so be + // careful for these fields. + mH.mCompression = aLength >= 16 ? LittleEndian::readUint32(aData + 12) : 0; + mH.mImageSize = aLength >= 20 ? LittleEndian::readUint32(aData + 16) : 0; + // We ignore the xppm (aData + 20) and yppm (aData + 24) fields. + mH.mNumColors = aLength >= 32 ? LittleEndian::readUint32(aData + 28) : 0; + // We ignore the important_colors (aData + 36) field. + + // For WinBMPv4, WinBMPv5 and (possibly) OS2-BMPv2 there are additional + // fields in the info header which we ignore, with the possible exception + // of the color bitfields (see below). + } + + // Run with MOZ_LOG=BMPDecoder:5 set to see this output. + MOZ_LOG(sBMPLog, LogLevel::Debug, + ("BMP: bihsize=%u, %d x %d, bpp=%u, compression=%u, colors=%u\n", + mH.mBIHSize, mH.mWidth, mH.mHeight, uint32_t(mH.mBpp), + mH.mCompression, mH.mNumColors)); + + // BMPs with negative width are invalid. Also, reject extremely wide images + // to keep the math sane. And reject INT_MIN as a height because you can't + // get its absolute value (because -INT_MIN is one more than INT_MAX). + const int32_t k64KWidth = 0x0000FFFF; + bool sizeOk = 0 <= mH.mWidth && mH.mWidth <= k64KWidth && + mH.mHeight != INT_MIN; + if (!sizeOk) { + return Transition::TerminateFailure(); + } + + // Check mBpp and mCompression. + bool bppCompressionOk = + (mH.mCompression == Compression::RGB && + (mH.mBpp == 1 || mH.mBpp == 4 || mH.mBpp == 8 || + mH.mBpp == 16 || mH.mBpp == 24 || mH.mBpp == 32)) || + (mH.mCompression == Compression::RLE8 && mH.mBpp == 8) || + (mH.mCompression == Compression::RLE4 && mH.mBpp == 4) || + (mH.mCompression == Compression::BITFIELDS && + // For BITFIELDS compression we require an exact match for one of the + // WinBMP BIH sizes; this clearly isn't an OS2 BMP. + (mH.mBIHSize == InfoHeaderLength::WIN_V3 || + mH.mBIHSize == InfoHeaderLength::WIN_V4 || + mH.mBIHSize == InfoHeaderLength::WIN_V5) && + (mH.mBpp == 16 || mH.mBpp == 32)); + if (!bppCompressionOk) { + return Transition::TerminateFailure(); + } + + // Initialize our current row to the top of the image. + mCurrentRow = AbsoluteHeight(); + + // Round it up to the nearest byte count, then pad to 4-byte boundary. + // Compute this even for a metadate decode because GetCompressedImageSize() + // relies on it. + mPixelRowSize = (mH.mBpp * mH.mWidth + 7) / 8; + uint32_t surplus = mPixelRowSize % 4; + if (surplus != 0) { + mPixelRowSize += 4 - surplus; + } + + size_t bitFieldsLengthStillToRead = 0; + if (mH.mCompression == Compression::BITFIELDS) { + // Need to read bitfields. + if (mH.mBIHSize >= InfoHeaderLength::WIN_V4) { + // Bitfields are present in the info header, so we can read them + // immediately. + mBitFields.ReadFromHeader(aData + 36, /* aReadAlpha = */ true); + } else { + // Bitfields are present after the info header, so we will read them in + // ReadBitfields(). + bitFieldsLengthStillToRead = BitFields::LENGTH; + } + } else if (mH.mBpp == 16) { + // No bitfields specified; use the default 5-5-5 values. + mBitFields.SetR5G5B5(); + } else if (mH.mBpp == 32) { + // No bitfields specified; use the default 8-8-8 values. + mBitFields.SetR8G8B8(); + } + + return Transition::To(State::BITFIELDS, bitFieldsLengthStillToRead); +} + +void +BitFields::ReadFromHeader(const char* aData, bool aReadAlpha) +{ + mRed.Set (LittleEndian::readUint32(aData + 0)); + mGreen.Set(LittleEndian::readUint32(aData + 4)); + mBlue.Set (LittleEndian::readUint32(aData + 8)); + if (aReadAlpha) { + mAlpha.Set(LittleEndian::readUint32(aData + 12)); + } +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadBitfields(const char* aData, size_t aLength) +{ + mPreGapLength += aLength; + + // If aLength is zero there are no bitfields to read, or we already read them + // in ReadInfoHeader(). + if (aLength != 0) { + mBitFields.ReadFromHeader(aData, /* aReadAlpha = */ false); + } + + // Note that RLE-encoded BMPs might be transparent because the 'delta' mode + // can skip pixels and cause implicit transparency. + mMayHaveTransparency = + mIsWithinICO || + mH.mCompression == Compression::RLE8 || + mH.mCompression == Compression::RLE4 || + (mH.mCompression == Compression::BITFIELDS && + mBitFields.mAlpha.IsPresent()); + if (mMayHaveTransparency) { + PostHasTransparency(); + } + + // Post our size to the superclass. + PostSize(mH.mWidth, AbsoluteHeight()); + + // We've now read all the headers. If we're doing a metadata decode, we're + // done. + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + // Set up the color table, if present; it'll be filled in by ReadColorTable(). + if (mH.mBpp <= 8) { + mNumColors = 1 << mH.mBpp; + if (0 < mH.mNumColors && mH.mNumColors < mNumColors) { + mNumColors = mH.mNumColors; + } + + // Always allocate and zero 256 entries, even though mNumColors might be + // smaller, because the file might erroneously index past mNumColors. + mColors = MakeUnique<ColorTableEntry[]>(256); + memset(mColors.get(), 0, 256 * sizeof(ColorTableEntry)); + + // OS/2 Bitmaps have no padding byte. + mBytesPerColor = (mH.mBIHSize == InfoHeaderLength::WIN_V2) ? 3 : 4; + } + + MOZ_ASSERT(!mImageData, "Already have a buffer allocated?"); + nsresult rv = AllocateFrame(/* aFrameNum = */ 0, OutputSize(), + FullOutputFrame(), + mMayHaveTransparency ? SurfaceFormat::B8G8R8A8 + : SurfaceFormat::B8G8R8X8); + if (NS_FAILED(rv)) { + return Transition::TerminateFailure(); + } + MOZ_ASSERT(mImageData, "Should have a buffer now"); + + if (mDownscaler) { + // BMPs store their rows in reverse order, so the downscaler needs to + // reverse them again when writing its output. Unless the height is + // negative! + rv = mDownscaler->BeginFrame(Size(), Nothing(), + mImageData, mMayHaveTransparency, + /* aFlipVertically = */ mH.mHeight >= 0); + if (NS_FAILED(rv)) { + return Transition::TerminateFailure(); + } + } + + return Transition::To(State::COLOR_TABLE, mNumColors * mBytesPerColor); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadColorTable(const char* aData, size_t aLength) +{ + MOZ_ASSERT_IF(aLength != 0, mNumColors > 0 && mColors); + + mPreGapLength += aLength; + + for (uint32_t i = 0; i < mNumColors; i++) { + // The format is BGR or BGR0. + mColors[i].mBlue = uint8_t(aData[0]); + mColors[i].mGreen = uint8_t(aData[1]); + mColors[i].mRed = uint8_t(aData[2]); + aData += mBytesPerColor; + } + + // We know how many bytes we've read so far (mPreGapLength) and we know the + // offset of the pixel data (mH.mDataOffset), so we can determine the length + // of the gap (possibly zero) between the color table and the pixel data. + // + // If the gap is negative the file must be malformed (e.g. mH.mDataOffset + // points into the middle of the color palette instead of past the end) and + // we give up. + if (mPreGapLength > mH.mDataOffset) { + return Transition::TerminateFailure(); + } + + uint32_t gapLength = mH.mDataOffset - mPreGapLength; + return Transition::ToUnbuffered(State::AFTER_GAP, State::GAP, gapLength); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::SkipGap() +{ + return Transition::ContinueUnbuffered(State::GAP); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::AfterGap() +{ + // If there are no pixels we can stop. + // + // XXX: normally, if there are no pixels we will have stopped decoding before + // now, outside of this decoder. However, if the BMP is within an ICO file, + // it's possible that the ICO claimed the image had a non-zero size while the + // BMP claims otherwise. This test is to catch that awkward case. If we ever + // come up with a more general solution to this ICO-and-BMP-disagree-on-size + // problem, this test can be removed. + if (mH.mWidth == 0 || mH.mHeight == 0) { + return Transition::TerminateSuccess(); + } + + bool hasRLE = mH.mCompression == Compression::RLE8 || + mH.mCompression == Compression::RLE4; + return hasRLE + ? Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH) + : Transition::To(State::PIXEL_ROW, mPixelRowSize); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadPixelRow(const char* aData) +{ + MOZ_ASSERT(mCurrentRow > 0); + MOZ_ASSERT(mCurrentPos == 0); + + const uint8_t* src = reinterpret_cast<const uint8_t*>(aData); + uint32_t* dst = RowBuffer(); + uint32_t lpos = mH.mWidth; + switch (mH.mBpp) { + case 1: + while (lpos > 0) { + int8_t bit; + uint8_t idx; + for (bit = 7; bit >= 0 && lpos > 0; bit--) { + idx = (*src >> bit) & 1; + SetPixel(dst, idx, mColors); + --lpos; + } + ++src; + } + break; + + case 4: + while (lpos > 0) { + Set4BitPixel(dst, *src, lpos, mColors); + ++src; + } + break; + + case 8: + while (lpos > 0) { + SetPixel(dst, *src, mColors); + --lpos; + ++src; + } + break; + + case 16: + if (mBitFields.IsR5G5B5()) { + // Specialize this common case. + while (lpos > 0) { + uint16_t val = LittleEndian::readUint16(src); + SetPixel(dst, mBitFields.mRed.Get5(val), + mBitFields.mGreen.Get5(val), + mBitFields.mBlue.Get5(val)); + --lpos; + src += 2; + } + } else { + bool anyHasAlpha = false; + while (lpos > 0) { + uint16_t val = LittleEndian::readUint16(src); + SetPixel(dst, mBitFields.mRed.Get(val), + mBitFields.mGreen.Get(val), + mBitFields.mBlue.Get(val), + mBitFields.mAlpha.GetAlpha(val, anyHasAlpha)); + --lpos; + src += 2; + } + if (anyHasAlpha) { + MOZ_ASSERT(mMayHaveTransparency); + mDoesHaveTransparency = true; + } + } + break; + + case 24: + while (lpos > 0) { + SetPixel(dst, src[2], src[1], src[0]); + --lpos; + src += 3; + } + break; + + case 32: + if (mH.mCompression == Compression::RGB && mIsWithinICO && + mH.mBpp == 32) { + // This is a special case only used for 32bpp WinBMPv3-ICO files, which + // could be in either 0RGB or ARGB format. We start by assuming it's + // an 0RGB image. If we hit a non-zero alpha value, then we know it's + // actually an ARGB image, and change tack accordingly. + // (Note: a fully-transparent ARGB image is indistinguishable from a + // 0RGB image, and we will render such an image as a 0RGB image, i.e. + // opaquely. This is unlikely to be a problem in practice.) + while (lpos > 0) { + if (!mDoesHaveTransparency && src[3] != 0) { + // Up until now this looked like an 0RGB image, but we now know + // it's actually an ARGB image. Which means every pixel we've seen + // so far has been fully transparent. So we go back and redo them. + + // Tell the Downscaler to go back to the start. + if (mDownscaler) { + mDownscaler->ResetForNextProgressivePass(); + } + + // Redo the complete rows we've already done. + MOZ_ASSERT(mCurrentPos == 0); + int32_t currentRow = mCurrentRow; + mCurrentRow = AbsoluteHeight(); + while (mCurrentRow > currentRow) { + dst = RowBuffer(); + for (int32_t i = 0; i < mH.mWidth; i++) { + SetPixel(dst, 0, 0, 0, 0); + } + FinishRow(); + } + + // Redo the part of this row we've already done. + dst = RowBuffer(); + int32_t n = mH.mWidth - lpos; + for (int32_t i = 0; i < n; i++) { + SetPixel(dst, 0, 0, 0, 0); + } + + MOZ_ASSERT(mMayHaveTransparency); + mDoesHaveTransparency = true; + } + + // If mDoesHaveTransparency is false, treat this as an 0RGB image. + // Otherwise, treat this as an ARGB image. + SetPixel(dst, src[2], src[1], src[0], + mDoesHaveTransparency ? src[3] : 0xff); + src += 4; + --lpos; + } + } else if (mBitFields.IsR8G8B8()) { + // Specialize this common case. + while (lpos > 0) { + uint32_t val = LittleEndian::readUint32(src); + SetPixel(dst, mBitFields.mRed.Get8(val), + mBitFields.mGreen.Get8(val), + mBitFields.mBlue.Get8(val)); + --lpos; + src += 4; + } + } else { + bool anyHasAlpha = false; + while (lpos > 0) { + uint32_t val = LittleEndian::readUint32(src); + SetPixel(dst, mBitFields.mRed.Get(val), + mBitFields.mGreen.Get(val), + mBitFields.mBlue.Get(val), + mBitFields.mAlpha.GetAlpha(val, anyHasAlpha)); + --lpos; + src += 4; + } + if (anyHasAlpha) { + MOZ_ASSERT(mMayHaveTransparency); + mDoesHaveTransparency = true; + } + } + break; + + default: + MOZ_CRASH("Unsupported color depth; earlier check didn't catch it?"); + } + + FinishRow(); + return mCurrentRow == 0 + ? Transition::TerminateSuccess() + : Transition::To(State::PIXEL_ROW, mPixelRowSize); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadRLESegment(const char* aData) +{ + if (mCurrentRow == 0) { + return Transition::TerminateSuccess(); + } + + uint8_t byte1 = uint8_t(aData[0]); + uint8_t byte2 = uint8_t(aData[1]); + + if (byte1 != RLE::ESCAPE) { + // Encoded mode consists of two bytes: byte1 specifies the number of + // consecutive pixels to be drawn using the color index contained in + // byte2. + // + // Work around bitmaps that specify too many pixels. + uint32_t pixelsNeeded = + std::min<uint32_t>(mH.mWidth - mCurrentPos, byte1); + if (pixelsNeeded) { + uint32_t* dst = RowBuffer(); + mCurrentPos += pixelsNeeded; + if (mH.mCompression == Compression::RLE8) { + do { + SetPixel(dst, byte2, mColors); + pixelsNeeded --; + } while (pixelsNeeded); + } else { + do { + Set4BitPixel(dst, byte2, pixelsNeeded, mColors); + } while (pixelsNeeded); + } + } + return Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH); + } + + if (byte2 == RLE::ESCAPE_EOL) { + mCurrentPos = 0; + FinishRow(); + return mCurrentRow == 0 + ? Transition::TerminateSuccess() + : Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH); + } + + if (byte2 == RLE::ESCAPE_EOF) { + return Transition::TerminateSuccess(); + } + + if (byte2 == RLE::ESCAPE_DELTA) { + return Transition::To(State::RLE_DELTA, RLE::DELTA_LENGTH); + } + + // Absolute mode. |byte2| gives the number of pixels. The length depends on + // whether it's 4-bit or 8-bit RLE. Also, the length must be even (and zero + // padding is used to achieve this when necessary). + MOZ_ASSERT(mAbsoluteModeNumPixels == 0); + mAbsoluteModeNumPixels = byte2; + uint32_t length = byte2; + if (mH.mCompression == Compression::RLE4) { + length = (length + 1) / 2; // halve, rounding up + } + if (length & 1) { + length++; + } + return Transition::To(State::RLE_ABSOLUTE, length); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadRLEDelta(const char* aData) +{ + // Delta encoding makes it possible to skip pixels making part of the image + // transparent. + MOZ_ASSERT(mMayHaveTransparency); + mDoesHaveTransparency = true; + + if (mDownscaler) { + // Clear the skipped pixels. (This clears to the end of the row, + // which is perfect if there's a Y delta and harmless if not). + mDownscaler->ClearRestOfRow(/* aStartingAtCol = */ mCurrentPos); + } + + // Handle the XDelta. + mCurrentPos += uint8_t(aData[0]); + if (mCurrentPos > mH.mWidth) { + mCurrentPos = mH.mWidth; + } + + // Handle the Y Delta. + int32_t yDelta = std::min<int32_t>(uint8_t(aData[1]), mCurrentRow); + mCurrentRow -= yDelta; + + if (mDownscaler && yDelta > 0) { + // Commit the current row (the first of the skipped rows). + mDownscaler->CommitRow(); + + // Clear and commit the remaining skipped rows. + for (int32_t line = 1; line < yDelta; line++) { + mDownscaler->ClearRow(); + mDownscaler->CommitRow(); + } + } + + return mCurrentRow == 0 + ? Transition::TerminateSuccess() + : Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH); +} + +LexerTransition<nsBMPDecoder::State> +nsBMPDecoder::ReadRLEAbsolute(const char* aData, size_t aLength) +{ + uint32_t n = mAbsoluteModeNumPixels; + mAbsoluteModeNumPixels = 0; + + if (mCurrentPos + n > uint32_t(mH.mWidth)) { + // Bad data. Stop decoding; at least part of the image may have been + // decoded. + return Transition::TerminateSuccess(); + } + + // In absolute mode, n represents the number of pixels that follow, each of + // which contains the color index of a single pixel. + uint32_t* dst = RowBuffer(); + uint32_t iSrc = 0; + uint32_t* oldPos = dst; + if (mH.mCompression == Compression::RLE8) { + while (n > 0) { + SetPixel(dst, aData[iSrc], mColors); + n--; + iSrc++; + } + } else { + while (n > 0) { + Set4BitPixel(dst, aData[iSrc], n, mColors); + iSrc++; + } + } + mCurrentPos += dst - oldPos; + + // We should read all the data (unless the last byte is zero padding). + MOZ_ASSERT(iSrc == aLength - 1 || iSrc == aLength); + + return Transition::To(State::RLE_SEGMENT, RLE::SEGMENT_LENGTH); +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsBMPDecoder.h b/image/decoders/nsBMPDecoder.h new file mode 100644 index 000000000..0cf2af689 --- /dev/null +++ b/image/decoders/nsBMPDecoder.h @@ -0,0 +1,235 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_decoders_nsBMPDecoder_h +#define mozilla_image_decoders_nsBMPDecoder_h + +#include "BMPHeaders.h" +#include "Decoder.h" +#include "gfxColor.h" +#include "StreamingLexer.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace image { + +namespace bmp { + +/// This struct contains the fields from the file header and info header that +/// we use during decoding. (Excluding bitfields fields, which are kept in +/// BitFields.) +struct Header { + uint32_t mDataOffset; // Offset to raster data. + uint32_t mBIHSize; // Header size. + int32_t mWidth; // Image width. + int32_t mHeight; // Image height. + uint16_t mBpp; // Bits per pixel. + uint32_t mCompression; // See struct Compression for valid values. + uint32_t mImageSize; // (compressed) image size. Can be 0 if + // mCompression==0. + uint32_t mNumColors; // Used colors. + + Header() + : mDataOffset(0) + , mBIHSize(0) + , mWidth(0) + , mHeight(0) + , mBpp(0) + , mCompression(0) + , mImageSize(0) + , mNumColors(0) + {} +}; + +/// An entry in the color table. +struct ColorTableEntry { + uint8_t mRed; + uint8_t mGreen; + uint8_t mBlue; +}; + +/// All the color-related bitfields for 16bpp and 32bpp images. We use this +/// even for older format BMPs that don't have explicit bitfields. +class BitFields { + class Value { + friend class BitFields; + + uint32_t mMask; // The mask for the value. + uint8_t mRightShift; // The amount to right-shift after masking. + uint8_t mBitWidth; // The width (in bits) of the value. + + /// Sets the mask (and thus the right-shift and bit-width as well). + void Set(uint32_t aMask); + + public: + Value() + { + mMask = 0; + mRightShift = 0; + mBitWidth = 0; + } + + /// Returns true if this channel is used. Only used for alpha. + bool IsPresent() const { return mMask != 0x0; } + + /// Extracts the single color value from the multi-color value. + uint8_t Get(uint32_t aVal) const; + + /// Like Get(), but specially for alpha. + uint8_t GetAlpha(uint32_t aVal, bool& aHasAlphaOut) const; + + /// Specialized versions of Get() for when the bit-width is 5 or 8. + /// (They will assert if called and the bit-width is not 5 or 8.) + uint8_t Get5(uint32_t aVal) const; + uint8_t Get8(uint32_t aVal) const; + }; + +public: + /// The individual color channels. + Value mRed; + Value mGreen; + Value mBlue; + Value mAlpha; + + /// Set bitfields to the standard 5-5-5 16bpp values. + void SetR5G5B5(); + + /// Set bitfields to the standard 8-8-8 32bpp values. + void SetR8G8B8(); + + /// Test if bitfields have the standard 5-5-5 16bpp values. + bool IsR5G5B5() const; + + /// Test if bitfields have the standard 8-8-8 32bpp values. + bool IsR8G8B8() const; + + /// Read the bitfields from a header. The reading of the alpha mask is + /// optional. + void ReadFromHeader(const char* aData, bool aReadAlpha); + + /// Length of the bitfields structure in the BMP file. + static const size_t LENGTH = 12; +}; + +} // namespace bmp + +class RasterImage; + +/// Decoder for BMP-Files, as used by Windows and OS/2. + +class nsBMPDecoder : public Decoder +{ +public: + ~nsBMPDecoder(); + + /// Obtains the internal output image buffer. + uint32_t* GetImageData() { return reinterpret_cast<uint32_t*>(mImageData); } + + /// Obtains the length of the internal output image buffer. + size_t GetImageDataLength() const { return mImageDataLength; } + + /// Obtains the size of the compressed image resource. + int32_t GetCompressedImageSize() const; + + /// Mark this BMP as being within an ICO file. Only used for testing purposes + /// because the ICO-specific constructor does this marking automatically. + void SetIsWithinICO() { mIsWithinICO = true; } + + /// Did the BMP file have alpha data of any kind? (Only use this after the + /// bitmap has been fully decoded.) + bool HasTransparency() const { return mDoesHaveTransparency; } + + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult BeforeFinishInternal() override; + nsresult FinishInternal() override; + +private: + friend class DecoderFactory; + + enum class State { + FILE_HEADER, + INFO_HEADER_SIZE, + INFO_HEADER_REST, + BITFIELDS, + COLOR_TABLE, + GAP, + AFTER_GAP, + PIXEL_ROW, + RLE_SEGMENT, + RLE_DELTA, + RLE_ABSOLUTE + }; + + // This is the constructor used for normal BMP images. + explicit nsBMPDecoder(RasterImage* aImage); + + // This is the constructor used for BMP resources in ICO images. + nsBMPDecoder(RasterImage* aImage, uint32_t aDataOffset); + + // Helper constructor called by the other two. + nsBMPDecoder(RasterImage* aImage, State aState, size_t aLength); + + int32_t AbsoluteHeight() const { return abs(mH.mHeight); } + + uint32_t* RowBuffer(); + + void FinishRow(); + + LexerTransition<State> ReadFileHeader(const char* aData, size_t aLength); + LexerTransition<State> ReadInfoHeaderSize(const char* aData, size_t aLength); + LexerTransition<State> ReadInfoHeaderRest(const char* aData, size_t aLength); + LexerTransition<State> ReadBitfields(const char* aData, size_t aLength); + LexerTransition<State> ReadColorTable(const char* aData, size_t aLength); + LexerTransition<State> SkipGap(); + LexerTransition<State> AfterGap(); + LexerTransition<State> ReadPixelRow(const char* aData); + LexerTransition<State> ReadRLESegment(const char* aData); + LexerTransition<State> ReadRLEDelta(const char* aData); + LexerTransition<State> ReadRLEAbsolute(const char* aData, size_t aLength); + + StreamingLexer<State> mLexer; + + bmp::Header mH; + + // If the BMP is within an ICO file our treatment of it differs slightly. + bool mIsWithinICO; + + bmp::BitFields mBitFields; + + // Might the image have transparency? Determined from the headers during + // metadata decode. (Does not guarantee the image actually has transparency.) + bool mMayHaveTransparency; + + // Does the image have transparency? Determined during full decoding, so only + // use this after that has been completed. + bool mDoesHaveTransparency; + + uint32_t mNumColors; // The number of used colors, i.e. the number of + // entries in mColors, if it's present. + UniquePtr<bmp::ColorTableEntry[]> mColors; // The color table, if it's present. + uint32_t mBytesPerColor; // 3 or 4, depending on the format + + // The number of bytes prior to the optional gap that have been read. This + // is used to find the start of the pixel data. + uint32_t mPreGapLength; + + uint32_t mPixelRowSize; // The number of bytes per pixel row. + + int32_t mCurrentRow; // Index of the row of the image that's currently + // being decoded: [height,1]. + int32_t mCurrentPos; // Index into the current line. Used when + // doing RLE decoding and when filling in pixels + // for truncated files. + + // Only used in RLE_ABSOLUTE state: the number of pixels to read. + uint32_t mAbsoluteModeNumPixels; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsBMPDecoder_h diff --git a/image/decoders/nsGIFDecoder2.cpp b/image/decoders/nsGIFDecoder2.cpp new file mode 100644 index 000000000..7955438e4 --- /dev/null +++ b/image/decoders/nsGIFDecoder2.cpp @@ -0,0 +1,1085 @@ +/* -*- 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/. */ +/* +The Graphics Interchange Format(c) is the copyright property of CompuServe +Incorporated. Only CompuServe Incorporated is authorized to define, redefine, +enhance, alter, modify or change in any way the definition of the format. + +CompuServe Incorporated hereby grants a limited, non-exclusive, royalty-free +license for the use of the Graphics Interchange Format(sm) in computer +software; computer software utilizing GIF(sm) must acknowledge ownership of the +Graphics Interchange Format and its Service Mark by CompuServe Incorporated, in +User and Technical Documentation. Computer software utilizing GIF, which is +distributed or may be distributed without User or Technical Documentation must +display to the screen or printer a message acknowledging ownership of the +Graphics Interchange Format and the Service Mark by CompuServe Incorporated; in +this case, the acknowledgement may be displayed in an opening screen or leading +banner, or a closing screen or trailing banner. A message such as the following +may be used: + + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +For further information, please contact : + + CompuServe Incorporated + Graphics Technology Department + 5000 Arlington Center Boulevard + Columbus, Ohio 43220 + U. S. A. + +CompuServe Incorporated maintains a mailing list with all those individuals and +organizations who wish to receive copies of this document when it is corrected +or revised. This service is offered free of charge; please provide us with your +mailing address. +*/ + +#include "nsGIFDecoder2.h" + +#include <stddef.h> + +#include "imgFrame.h" +#include "mozilla/EndianUtils.h" +#include "nsIInputStream.h" +#include "RasterImage.h" +#include "SurfacePipeFactory.h" + +#include "gfxColor.h" +#include "gfxPlatform.h" +#include "qcms.h" +#include <algorithm> +#include "mozilla/Telemetry.h" + +using namespace mozilla::gfx; + +using std::max; + +namespace mozilla { +namespace image { + +////////////////////////////////////////////////////////////////////// +// GIF Decoder Implementation + +static const size_t GIF_HEADER_LEN = 6; +static const size_t GIF_SCREEN_DESCRIPTOR_LEN = 7; +static const size_t BLOCK_HEADER_LEN = 1; +static const size_t SUB_BLOCK_HEADER_LEN = 1; +static const size_t EXTENSION_HEADER_LEN = 2; +static const size_t GRAPHIC_CONTROL_EXTENSION_LEN = 4; +static const size_t APPLICATION_EXTENSION_LEN = 11; +static const size_t IMAGE_DESCRIPTOR_LEN = 9; + +// Masks for reading color table information from packed fields in the screen +// descriptor and image descriptor blocks. +static const uint8_t PACKED_FIELDS_COLOR_TABLE_BIT = 0x80; +static const uint8_t PACKED_FIELDS_INTERLACED_BIT = 0x40; +static const uint8_t PACKED_FIELDS_TABLE_DEPTH_MASK = 0x07; + +nsGIFDecoder2::nsGIFDecoder2(RasterImage* aImage) + : Decoder(aImage) + , mLexer(Transition::To(State::GIF_HEADER, GIF_HEADER_LEN), + Transition::TerminateSuccess()) + , mOldColor(0) + , mCurrentFrameIndex(-1) + , mColorTablePos(0) + , mGIFOpen(false) + , mSawTransparency(false) +{ + // Clear out the structure, excluding the arrays. + memset(&mGIFStruct, 0, sizeof(mGIFStruct)); + + // Initialize as "animate once" in case no NETSCAPE2.0 extension is found. + mGIFStruct.loop_count = 1; +} + +nsGIFDecoder2::~nsGIFDecoder2() +{ + free(mGIFStruct.local_colormap); +} + +nsresult +nsGIFDecoder2::FinishInternal() +{ + MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!"); + + // If the GIF got cut off, handle it anyway + if (!IsMetadataDecode() && mGIFOpen) { + if (mCurrentFrameIndex == mGIFStruct.images_decoded) { + EndImageFrame(); + } + PostDecodeDone(mGIFStruct.loop_count - 1); + mGIFOpen = false; + } + + return NS_OK; +} + +void +nsGIFDecoder2::FlushImageData() +{ + Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect(); + if (!invalidRect) { + return; + } + + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); +} + +//****************************************************************************** +// GIF decoder callback methods. Part of public API for GIF2 +//****************************************************************************** + +//****************************************************************************** +void +nsGIFDecoder2::BeginGIF() +{ + if (mGIFOpen) { + return; + } + + mGIFOpen = true; + + PostSize(mGIFStruct.screen_width, mGIFStruct.screen_height); +} + +bool +nsGIFDecoder2::CheckForTransparency(const IntRect& aFrameRect) +{ + // Check if the image has a transparent color in its palette. + if (mGIFStruct.is_transparent) { + PostHasTransparency(); + return true; + } + + if (mGIFStruct.images_decoded > 0) { + return false; // We only care about first frame padding below. + } + + // If we need padding on the first frame, that means we don't draw into part + // of the image at all. Report that as transparency. + IntRect imageRect(0, 0, mGIFStruct.screen_width, mGIFStruct.screen_height); + if (!imageRect.IsEqualEdges(aFrameRect)) { + PostHasTransparency(); + mSawTransparency = true; // Make sure we don't optimize it away. + return true; + } + + return false; +} + +//****************************************************************************** +nsresult +nsGIFDecoder2::BeginImageFrame(const IntRect& aFrameRect, + uint16_t aDepth, + bool aIsInterlaced) +{ + MOZ_ASSERT(HasSize()); + + bool hasTransparency = CheckForTransparency(aFrameRect); + gfx::SurfaceFormat format = hasTransparency ? SurfaceFormat::B8G8R8A8 + : SurfaceFormat::B8G8R8X8; + + // Make sure there's no animation if we're downscaling. + MOZ_ASSERT_IF(Size() != OutputSize(), !GetImageMetadata().HasAnimation()); + + SurfacePipeFlags pipeFlags = aIsInterlaced + ? SurfacePipeFlags::DEINTERLACE + : SurfacePipeFlags(); + + Maybe<SurfacePipe> pipe; + if (mGIFStruct.images_decoded == 0) { + // The first frame may be displayed progressively. + pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY; + + // The first frame is always decoded into an RGB surface. + pipe = + SurfacePipeFactory::CreateSurfacePipe(this, mGIFStruct.images_decoded, + Size(), OutputSize(), + aFrameRect, format, pipeFlags); + } else { + // This is an animation frame (and not the first). To minimize the memory + // usage of animations, the image data is stored in paletted form. + MOZ_ASSERT(Size() == OutputSize()); + pipe = + SurfacePipeFactory::CreatePalettedSurfacePipe(this, mGIFStruct.images_decoded, + Size(), aFrameRect, format, + aDepth, pipeFlags); + } + + mCurrentFrameIndex = mGIFStruct.images_decoded; + + if (!pipe) { + mPipe = SurfacePipe(); + return NS_ERROR_FAILURE; + } + + mPipe = Move(*pipe); + return NS_OK; +} + + +//****************************************************************************** +void +nsGIFDecoder2::EndImageFrame() +{ + Opacity opacity = Opacity::SOME_TRANSPARENCY; + + if (mGIFStruct.images_decoded == 0) { + // We need to send invalidations for the first frame. + FlushImageData(); + + // The first frame was preallocated with alpha; if it wasn't transparent, we + // should fix that. We can also mark it opaque unconditionally if we didn't + // actually see any transparent pixels - this test is only valid for the + // first frame. + if (!mGIFStruct.is_transparent && !mSawTransparency) { + opacity = Opacity::FULLY_OPAQUE; + } + } + + // Unconditionally increment images_decoded, because we unconditionally + // append frames in BeginImageFrame(). This ensures that images_decoded + // always refers to the frame in mImage we're currently decoding, + // even if some of them weren't decoded properly and thus are blank. + mGIFStruct.images_decoded++; + + // Tell the superclass we finished a frame + PostFrameStop(opacity, + DisposalMethod(mGIFStruct.disposal_method), + FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time)); + + // Reset the transparent pixel + if (mOldColor) { + mColormap[mGIFStruct.tpixel] = mOldColor; + mOldColor = 0; + } + + mCurrentFrameIndex = -1; +} + +template <typename PixelSize> +PixelSize +nsGIFDecoder2::ColormapIndexToPixel(uint8_t aIndex) +{ + MOZ_ASSERT(sizeof(PixelSize) == sizeof(uint32_t)); + + // Retrieve the next color, clamping to the size of the colormap. + uint32_t color = mColormap[aIndex & mColorMask]; + + // Check for transparency. + if (mGIFStruct.is_transparent) { + mSawTransparency = mSawTransparency || color == 0; + } + + return color; +} + +template <> +uint8_t +nsGIFDecoder2::ColormapIndexToPixel<uint8_t>(uint8_t aIndex) +{ + return aIndex & mColorMask; +} + +template <typename PixelSize> +NextPixel<PixelSize> +nsGIFDecoder2::YieldPixel(const uint8_t* aData, + size_t aLength, + size_t* aBytesReadOut) +{ + MOZ_ASSERT(aData); + MOZ_ASSERT(aBytesReadOut); + MOZ_ASSERT(mGIFStruct.stackp >= mGIFStruct.stack); + + // Advance to the next byte we should read. + const uint8_t* data = aData + *aBytesReadOut; + + // If we don't have any decoded data to yield, try to read some input and + // produce some. + if (mGIFStruct.stackp == mGIFStruct.stack) { + while (mGIFStruct.bits < mGIFStruct.codesize && *aBytesReadOut < aLength) { + // Feed the next byte into the decoder's 32-bit input buffer. + mGIFStruct.datum += int32_t(*data) << mGIFStruct.bits; + mGIFStruct.bits += 8; + data += 1; + *aBytesReadOut += 1; + } + + if (mGIFStruct.bits < mGIFStruct.codesize) { + return AsVariant(WriteState::NEED_MORE_DATA); + } + + // Get the leading variable-length symbol from the data stream. + int code = mGIFStruct.datum & mGIFStruct.codemask; + mGIFStruct.datum >>= mGIFStruct.codesize; + mGIFStruct.bits -= mGIFStruct.codesize; + + const int clearCode = ClearCode(); + + // Reset the dictionary to its original state, if requested + if (code == clearCode) { + mGIFStruct.codesize = mGIFStruct.datasize + 1; + mGIFStruct.codemask = (1 << mGIFStruct.codesize) - 1; + mGIFStruct.avail = clearCode + 2; + mGIFStruct.oldcode = -1; + return AsVariant(WriteState::NEED_MORE_DATA); + } + + // Check for explicit end-of-stream code. It should only appear after all + // image data, but if that was the case we wouldn't be in this function, so + // this is always an error condition. + if (code == (clearCode + 1)) { + return AsVariant(WriteState::FAILURE); + } + + if (mGIFStruct.oldcode == -1) { + if (code >= MAX_BITS) { + return AsVariant(WriteState::FAILURE); // The code's too big; something's wrong. + } + + mGIFStruct.firstchar = mGIFStruct.oldcode = code; + + // Yield a pixel at the appropriate index in the colormap. + mGIFStruct.pixels_remaining--; + return AsVariant(ColormapIndexToPixel<PixelSize>(mGIFStruct.suffix[code])); + } + + int incode = code; + if (code >= mGIFStruct.avail) { + *mGIFStruct.stackp++ = mGIFStruct.firstchar; + code = mGIFStruct.oldcode; + + if (mGIFStruct.stackp >= mGIFStruct.stack + MAX_BITS) { + return AsVariant(WriteState::FAILURE); // Stack overflow; something's wrong. + } + } + + while (code >= clearCode) { + if ((code >= MAX_BITS) || (code == mGIFStruct.prefix[code])) { + return AsVariant(WriteState::FAILURE); + } + + *mGIFStruct.stackp++ = mGIFStruct.suffix[code]; + code = mGIFStruct.prefix[code]; + + if (mGIFStruct.stackp >= mGIFStruct.stack + MAX_BITS) { + return AsVariant(WriteState::FAILURE); // Stack overflow; something's wrong. + } + } + + *mGIFStruct.stackp++ = mGIFStruct.firstchar = mGIFStruct.suffix[code]; + + // Define a new codeword in the dictionary. + if (mGIFStruct.avail < 4096) { + mGIFStruct.prefix[mGIFStruct.avail] = mGIFStruct.oldcode; + mGIFStruct.suffix[mGIFStruct.avail] = mGIFStruct.firstchar; + mGIFStruct.avail++; + + // If we've used up all the codewords of a given length increase the + // length of codewords by one bit, but don't exceed the specified maximum + // codeword size of 12 bits. + if (((mGIFStruct.avail & mGIFStruct.codemask) == 0) && + (mGIFStruct.avail < 4096)) { + mGIFStruct.codesize++; + mGIFStruct.codemask += mGIFStruct.avail; + } + } + + mGIFStruct.oldcode = incode; + } + + if (MOZ_UNLIKELY(mGIFStruct.stackp <= mGIFStruct.stack)) { + MOZ_ASSERT_UNREACHABLE("No decoded data but we didn't return early?"); + return AsVariant(WriteState::FAILURE); + } + + // Yield a pixel at the appropriate index in the colormap. + mGIFStruct.pixels_remaining--; + return AsVariant(ColormapIndexToPixel<PixelSize>(*--mGIFStruct.stackp)); +} + +/// Expand the colormap from RGB to Packed ARGB as needed by Cairo. +/// And apply any LCMS transformation. +static void +ConvertColormap(uint32_t* aColormap, uint32_t aColors) +{ + // Apply CMS transformation if enabled and available + if (gfxPlatform::GetCMSMode() == eCMSMode_All) { + qcms_transform* transform = gfxPlatform::GetCMSRGBTransform(); + if (transform) { + qcms_transform_data(transform, aColormap, aColormap, aColors); + } + } + + // Convert from the GIF's RGB format to the Cairo format. + // Work from end to begin, because of the in-place expansion + uint8_t* from = ((uint8_t*)aColormap) + 3 * aColors; + uint32_t* to = aColormap + aColors; + + // Convert color entries to Cairo format + + // set up for loops below + if (!aColors) { + return; + } + uint32_t c = aColors; + + // copy as bytes until source pointer is 32-bit-aligned + // NB: can't use 32-bit reads, they might read off the end of the buffer + for (; (NS_PTR_TO_UINT32(from) & 0x3) && c; --c) { + from -= 3; + *--to = gfxPackedPixel(0xFF, from[0], from[1], from[2]); + } + + // bulk copy of pixels. + while (c >= 4) { + from -= 12; + to -= 4; + c -= 4; + GFX_BLOCK_RGB_TO_FRGB(from,to); + } + + // copy remaining pixel(s) + // NB: can't use 32-bit reads, they might read off the end of the buffer + while (c--) { + from -= 3; + *--to = gfxPackedPixel(0xFF, from[0], from[1], from[2]); + } +} + +LexerResult +nsGIFDecoder2::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) +{ + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch(aState) { + case State::GIF_HEADER: + return ReadGIFHeader(aData); + case State::SCREEN_DESCRIPTOR: + return ReadScreenDescriptor(aData); + case State::GLOBAL_COLOR_TABLE: + return ReadGlobalColorTable(aData, aLength); + case State::FINISHED_GLOBAL_COLOR_TABLE: + return FinishedGlobalColorTable(); + case State::BLOCK_HEADER: + return ReadBlockHeader(aData); + case State::EXTENSION_HEADER: + return ReadExtensionHeader(aData); + case State::GRAPHIC_CONTROL_EXTENSION: + return ReadGraphicControlExtension(aData); + case State::APPLICATION_IDENTIFIER: + return ReadApplicationIdentifier(aData); + case State::NETSCAPE_EXTENSION_SUB_BLOCK: + return ReadNetscapeExtensionSubBlock(aData); + case State::NETSCAPE_EXTENSION_DATA: + return ReadNetscapeExtensionData(aData); + case State::IMAGE_DESCRIPTOR: + return ReadImageDescriptor(aData); + case State::FINISH_IMAGE_DESCRIPTOR: + return FinishImageDescriptor(aData); + case State::LOCAL_COLOR_TABLE: + return ReadLocalColorTable(aData, aLength); + case State::FINISHED_LOCAL_COLOR_TABLE: + return FinishedLocalColorTable(); + case State::IMAGE_DATA_BLOCK: + return ReadImageDataBlock(aData); + case State::IMAGE_DATA_SUB_BLOCK: + return ReadImageDataSubBlock(aData); + case State::LZW_DATA: + return ReadLZWData(aData, aLength); + case State::SKIP_LZW_DATA: + return Transition::ContinueUnbuffered(State::SKIP_LZW_DATA); + case State::FINISHED_LZW_DATA: + return Transition::To(State::IMAGE_DATA_SUB_BLOCK, SUB_BLOCK_HEADER_LEN); + case State::SKIP_SUB_BLOCKS: + return SkipSubBlocks(aData); + case State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS: + return Transition::ContinueUnbuffered(State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS); + case State::FINISHED_SKIPPING_DATA: + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); + default: + MOZ_CRASH("Unknown State"); + } + }); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadGIFHeader(const char* aData) +{ + // We retrieve the version here but because many GIF encoders set header + // fields incorrectly, we barely use it; features which should only appear in + // GIF89a are always accepted. + if (strncmp(aData, "GIF87a", GIF_HEADER_LEN) == 0) { + mGIFStruct.version = 87; + } else if (strncmp(aData, "GIF89a", GIF_HEADER_LEN) == 0) { + mGIFStruct.version = 89; + } else { + return Transition::TerminateFailure(); + } + + return Transition::To(State::SCREEN_DESCRIPTOR, GIF_SCREEN_DESCRIPTOR_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadScreenDescriptor(const char* aData) +{ + mGIFStruct.screen_width = LittleEndian::readUint16(aData + 0); + mGIFStruct.screen_height = LittleEndian::readUint16(aData + 2); + + const uint8_t packedFields = aData[4]; + + // XXX: Should we be capturing these values even if there is no global color + // table? + mGIFStruct.global_colormap_depth = + (packedFields & PACKED_FIELDS_TABLE_DEPTH_MASK) + 1; + mGIFStruct.global_colormap_count = 1 << mGIFStruct.global_colormap_depth; + + // We ignore several fields in the header. We don't care about the 'sort + // flag', which indicates if the global color table's entries are sorted in + // order of importance - if we need to render this image for a device with a + // narrower color gamut than GIF supports we'll handle that at a different + // layer. We have no use for the pixel aspect ratio as well. Finally, we + // intentionally ignore the background color index, as implementing that + // feature would not be web compatible - when a GIF image frame doesn't cover + // the entire area of the image, the area that's not covered should always be + // transparent. + + if (packedFields & PACKED_FIELDS_COLOR_TABLE_BIT) { + MOZ_ASSERT(mColorTablePos == 0); + + // We read the global color table in unbuffered mode since it can be quite + // large and it'd be preferable to avoid unnecessary copies. + const size_t globalColorTableSize = 3 * mGIFStruct.global_colormap_count; + return Transition::ToUnbuffered(State::FINISHED_GLOBAL_COLOR_TABLE, + State::GLOBAL_COLOR_TABLE, + globalColorTableSize); + } + + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadGlobalColorTable(const char* aData, size_t aLength) +{ + uint8_t* dest = reinterpret_cast<uint8_t*>(mGIFStruct.global_colormap) + + mColorTablePos; + memcpy(dest, aData, aLength); + mColorTablePos += aLength; + return Transition::ContinueUnbuffered(State::GLOBAL_COLOR_TABLE); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::FinishedGlobalColorTable() +{ + ConvertColormap(mGIFStruct.global_colormap, mGIFStruct.global_colormap_count); + mColorTablePos = 0; + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadBlockHeader(const char* aData) +{ + // Determine what type of block we're dealing with. + switch (aData[0]) { + case GIF_EXTENSION_INTRODUCER: + return Transition::To(State::EXTENSION_HEADER, EXTENSION_HEADER_LEN); + + case GIF_IMAGE_SEPARATOR: + return Transition::To(State::IMAGE_DESCRIPTOR, IMAGE_DESCRIPTOR_LEN); + + case GIF_TRAILER: + FinishInternal(); + return Transition::TerminateSuccess(); + + default: + // If we get anything other than GIF_IMAGE_SEPARATOR, + // GIF_EXTENSION_INTRODUCER, or GIF_TRAILER, there is extraneous data + // between blocks. The GIF87a spec tells us to keep reading until we find + // an image separator, but GIF89a says such a file is corrupt. We follow + // GIF89a and bail out. + + if (mGIFStruct.images_decoded > 0) { + // The file is corrupt, but we successfully decoded some frames, so we + // may as well consider the decode successful and display them. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + // No images decoded; there is nothing to display. + return Transition::TerminateFailure(); + } +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadExtensionHeader(const char* aData) +{ + const uint8_t label = aData[0]; + const uint8_t extensionHeaderLength = aData[1]; + + // If the extension header is zero length, just treat it as a block terminator + // and move on to the next block immediately. + if (extensionHeaderLength == 0) { + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + switch (label) { + case GIF_GRAPHIC_CONTROL_LABEL: + // The GIF spec mandates that the Control Extension header block length is + // 4 bytes, and the parser for this block reads 4 bytes, so we must + // enforce that the buffer contains at least this many bytes. If the GIF + // specifies a different length, we allow that, so long as it's larger; + // the additional data will simply be ignored. + return Transition::To(State::GRAPHIC_CONTROL_EXTENSION, + max<uint8_t>(extensionHeaderLength, + GRAPHIC_CONTROL_EXTENSION_LEN)); + + case GIF_APPLICATION_EXTENSION_LABEL: + // Again, the spec specifies that an application extension header is 11 + // bytes, but for compatibility with GIFs in the wild, we allow deviation + // from the spec. This is important for real-world compatibility, as GIFs + // in the wild exist with application extension headers that are both + // shorter and longer than 11 bytes. However, we only try to actually + // interpret the application extension if the length is correct; + // otherwise, we just skip the block unconditionally. + return extensionHeaderLength == APPLICATION_EXTENSION_LEN + ? Transition::To(State::APPLICATION_IDENTIFIER, extensionHeaderLength) + : Transition::ToUnbuffered(State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + extensionHeaderLength); + + default: + // Skip over any other type of extension block, including comment and + // plain text blocks. + return Transition::ToUnbuffered(State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + extensionHeaderLength); + } +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadGraphicControlExtension(const char* aData) +{ + mGIFStruct.is_transparent = aData[0] & 0x1; + mGIFStruct.tpixel = uint8_t(aData[3]); + mGIFStruct.disposal_method = (aData[0] >> 2) & 0x7; + + if (mGIFStruct.disposal_method == 4) { + // Some encoders (and apparently some specs) represent + // DisposalMethod::RESTORE_PREVIOUS as 4, but 3 is used in the canonical + // spec and is more popular, so we normalize to 3. + mGIFStruct.disposal_method = 3; + } else if (mGIFStruct.disposal_method > 4) { + // This GIF is using a disposal method which is undefined in the spec. + // Treat it as DisposalMethod::NOT_SPECIFIED. + mGIFStruct.disposal_method = 0; + } + + DisposalMethod method = DisposalMethod(mGIFStruct.disposal_method); + if (method == DisposalMethod::CLEAR_ALL || method == DisposalMethod::CLEAR) { + // We may have to display the background under this image during animation + // playback, so we regard it as transparent. + PostHasTransparency(); + } + + mGIFStruct.delay_time = LittleEndian::readUint16(aData + 1) * 10; + if (mGIFStruct.delay_time > 0) { + PostIsAnimated(FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time)); + } + + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadApplicationIdentifier(const char* aData) +{ + if ((strncmp(aData, "NETSCAPE2.0", 11) == 0) || + (strncmp(aData, "ANIMEXTS1.0", 11) == 0)) { + // This is a Netscape application extension block. + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + } + + // This is an application extension we don't care about. Just skip it. + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadNetscapeExtensionSubBlock(const char* aData) +{ + const uint8_t blockLength = aData[0]; + if (blockLength == 0) { + // We hit the block terminator. + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + // We consume a minimum of 3 bytes in accordance with the specs for the + // Netscape application extension block, such as they are. + const size_t extensionLength = max<uint8_t>(blockLength, 3); + return Transition::To(State::NETSCAPE_EXTENSION_DATA, extensionLength); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadNetscapeExtensionData(const char* aData) +{ + // Documentation for NETSCAPE2.0 / ANIMEXTS1.0 extensions can be found at: + // https://wiki.whatwg.org/wiki/GIF + static const uint8_t NETSCAPE_LOOPING_EXTENSION_SUB_BLOCK_ID = 1; + static const uint8_t NETSCAPE_BUFFERING_EXTENSION_SUB_BLOCK_ID = 2; + + const uint8_t subBlockID = aData[0] & 7; + switch (subBlockID) { + case NETSCAPE_LOOPING_EXTENSION_SUB_BLOCK_ID: + // This is looping extension. + mGIFStruct.loop_count = LittleEndian::readUint16(aData + 1); + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + + case NETSCAPE_BUFFERING_EXTENSION_SUB_BLOCK_ID: + // We allow, but ignore, this extension. + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + + default: + return Transition::TerminateFailure(); + } +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadImageDescriptor(const char* aData) +{ + // On the first frame, we don't need to yield, and none of the other checks + // below apply, so we can just jump right into FinishImageDescriptor(). + if (mGIFStruct.images_decoded == 0) { + return FinishImageDescriptor(aData); + } + + if (!HasAnimation()) { + // We should've already called PostIsAnimated(); this must be a corrupt + // animated image with a first frame timeout of zero. Signal that we're + // animated now, before the first-frame decode early exit below, so that + // RasterImage can detect that this happened. + PostIsAnimated(FrameTimeout::FromRawMilliseconds(0)); + } + + if (IsFirstFrameDecode()) { + // We're about to get a second frame, but we only want the first. Stop + // decoding now. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + MOZ_ASSERT(Size() == OutputSize(), "Downscaling an animated image?"); + + // Yield to allow access to the previous frame before we start a new one. + return Transition::ToAfterYield(State::FINISH_IMAGE_DESCRIPTOR); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::FinishImageDescriptor(const char* aData) +{ + IntRect frameRect; + + // Get image offsets with respect to the screen origin. + frameRect.x = LittleEndian::readUint16(aData + 0); + frameRect.y = LittleEndian::readUint16(aData + 2); + frameRect.width = LittleEndian::readUint16(aData + 4); + frameRect.height = LittleEndian::readUint16(aData + 6); + + if (!mGIFStruct.images_decoded) { + // Work around GIF files where + // * at least one of the logical screen dimensions is smaller than the + // same dimension in the first image, or + // * GIF87a files where the first image's dimensions do not match the + // logical screen dimensions. + if (mGIFStruct.screen_height < frameRect.height || + mGIFStruct.screen_width < frameRect.width || + mGIFStruct.version == 87) { + mGIFStruct.screen_height = frameRect.height; + mGIFStruct.screen_width = frameRect.width; + frameRect.MoveTo(0, 0); + } + + // Create the image container with the right size. + BeginGIF(); + if (HasError()) { + // Setting the size led to an error. + return Transition::TerminateFailure(); + } + + // If we're doing a metadata decode, we're done. + if (IsMetadataDecode()) { + CheckForTransparency(frameRect); + FinishInternal(); + return Transition::TerminateSuccess(); + } + } + + // Work around broken GIF files that have zero frame width or height; in this + // case, we'll treat the frame as having the same size as the overall image. + if (frameRect.height == 0 || frameRect.width == 0) { + frameRect.height = mGIFStruct.screen_height; + frameRect.width = mGIFStruct.screen_width; + + // If that still resulted in zero frame width or height, give up. + if (frameRect.height == 0 || frameRect.width == 0) { + return Transition::TerminateFailure(); + } + } + + // Determine |depth| (log base 2 of the number of colors in the palette). + bool haveLocalColorTable = false; + uint16_t depth = 0; + uint8_t packedFields = aData[8]; + + if (packedFields & PACKED_FIELDS_COLOR_TABLE_BIT) { + // Get the palette depth from the local color table. + depth = (packedFields & PACKED_FIELDS_TABLE_DEPTH_MASK) + 1; + haveLocalColorTable = true; + } else { + // Get the palette depth from the global color table. + depth = mGIFStruct.global_colormap_depth; + } + + // If the transparent color index is greater than the number of colors in the + // color table, we may need a higher color depth than |depth| would specify. + // Our internal representation of the image will instead use |realDepth|, + // which is the smallest color depth that can accomodate the existing palette + // *and* the transparent color index. + uint16_t realDepth = depth; + while (mGIFStruct.tpixel >= (1 << realDepth) && + realDepth < 8) { + realDepth++; + } + + // Create a mask used to ensure that color values fit within the colormap. + mColorMask = 0xFF >> (8 - realDepth); + + // Determine if this frame is interlaced or not. + const bool isInterlaced = packedFields & PACKED_FIELDS_INTERLACED_BIT; + + // Create the SurfacePipe we'll use to write output for this frame. + if (NS_FAILED(BeginImageFrame(frameRect, realDepth, isInterlaced))) { + return Transition::TerminateFailure(); + } + + // Clear state from last image. + mGIFStruct.pixels_remaining = frameRect.width * frameRect.height; + + if (haveLocalColorTable) { + // We have a local color table, so prepare to read it into the palette of + // the current frame. + mGIFStruct.local_colormap_size = 1 << depth; + + if (mGIFStruct.images_decoded == 0) { + // The first frame has a local color table. Allocate space for it as we + // use a BGRA or BGRX surface for the first frame; such surfaces don't + // have their own palettes internally. + mColormapSize = sizeof(uint32_t) << realDepth; + if (!mGIFStruct.local_colormap) { + mGIFStruct.local_colormap = + static_cast<uint32_t*>(moz_xmalloc(mColormapSize)); + } + mColormap = mGIFStruct.local_colormap; + } + + const size_t size = 3 << depth; + if (mColormapSize > size) { + // Clear the part of the colormap which will be unused with this palette. + // If a GIF references an invalid palette entry, ensure the entry is opaque white. + // This is needed for Skia as if it isn't, RGBX surfaces will cause blending issues + // with Skia. + memset(reinterpret_cast<uint8_t*>(mColormap) + size, 0xFF, + mColormapSize - size); + } + + MOZ_ASSERT(mColorTablePos == 0); + + // We read the local color table in unbuffered mode since it can be quite + // large and it'd be preferable to avoid unnecessary copies. + return Transition::ToUnbuffered(State::FINISHED_LOCAL_COLOR_TABLE, + State::LOCAL_COLOR_TABLE, + size); + } + + // There's no local color table; copy the global color table into the palette + // of the current frame. + if (mGIFStruct.images_decoded > 0) { + memcpy(mColormap, mGIFStruct.global_colormap, mColormapSize); + } else { + mColormap = mGIFStruct.global_colormap; + } + + return Transition::To(State::IMAGE_DATA_BLOCK, BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadLocalColorTable(const char* aData, size_t aLength) +{ + uint8_t* dest = reinterpret_cast<uint8_t*>(mColormap) + mColorTablePos; + memcpy(dest, aData, aLength); + mColorTablePos += aLength; + return Transition::ContinueUnbuffered(State::LOCAL_COLOR_TABLE); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::FinishedLocalColorTable() +{ + ConvertColormap(mColormap, mGIFStruct.local_colormap_size); + mColorTablePos = 0; + return Transition::To(State::IMAGE_DATA_BLOCK, BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadImageDataBlock(const char* aData) +{ + // Make sure the transparent pixel is transparent in the colormap. + if (mGIFStruct.is_transparent) { + // Save the old value so we can restore it later. + if (mColormap == mGIFStruct.global_colormap) { + mOldColor = mColormap[mGIFStruct.tpixel]; + } + mColormap[mGIFStruct.tpixel] = 0; + } + + // Initialize the LZW decoder. + mGIFStruct.datasize = uint8_t(aData[0]); + const int clearCode = ClearCode(); + if (mGIFStruct.datasize > MAX_LZW_BITS || clearCode >= MAX_BITS) { + return Transition::TerminateFailure(); + } + + mGIFStruct.avail = clearCode + 2; + mGIFStruct.oldcode = -1; + mGIFStruct.codesize = mGIFStruct.datasize + 1; + mGIFStruct.codemask = (1 << mGIFStruct.codesize) - 1; + mGIFStruct.datum = mGIFStruct.bits = 0; + + // Initialize the tables. + for (int i = 0; i < clearCode; i++) { + mGIFStruct.suffix[i] = i; + } + + mGIFStruct.stackp = mGIFStruct.stack; + + // Begin reading image data sub-blocks. + return Transition::To(State::IMAGE_DATA_SUB_BLOCK, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadImageDataSubBlock(const char* aData) +{ + const uint8_t subBlockLength = aData[0]; + if (subBlockLength == 0) { + // We hit the block terminator. + EndImageFrame(); + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + if (mGIFStruct.pixels_remaining == 0) { + // We've already written to the entire image; we should've hit the block + // terminator at this point. This image is corrupt, but we'll tolerate it. + + if (subBlockLength == GIF_TRAILER) { + // This GIF is missing the block terminator for the final block; we'll put + // up with it. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + // We're not at the end of the image, so just skip the extra data. + return Transition::ToUnbuffered(State::FINISHED_LZW_DATA, + State::SKIP_LZW_DATA, + subBlockLength); + } + + // Handle the standard case: there's data in the sub-block and pixels left to + // fill in the image. We read the sub-block unbuffered so we can get pixels on + // the screen as soon as possible. + return Transition::ToUnbuffered(State::FINISHED_LZW_DATA, + State::LZW_DATA, + subBlockLength); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadLZWData(const char* aData, size_t aLength) +{ + const uint8_t* data = reinterpret_cast<const uint8_t*>(aData); + size_t length = aLength; + + while (mGIFStruct.pixels_remaining > 0 && + (length > 0 || mGIFStruct.bits >= mGIFStruct.codesize)) { + size_t bytesRead = 0; + + auto result = mGIFStruct.images_decoded == 0 + ? mPipe.WritePixels<uint32_t>([&]{ return YieldPixel<uint32_t>(data, length, &bytesRead); }) + : mPipe.WritePixels<uint8_t>([&]{ return YieldPixel<uint8_t>(data, length, &bytesRead); }); + + if (MOZ_UNLIKELY(bytesRead > length)) { + MOZ_ASSERT_UNREACHABLE("Overread?"); + bytesRead = length; + } + + // Advance our position in the input based upon what YieldPixel() consumed. + data += bytesRead; + length -= bytesRead; + + switch (result) { + case WriteState::NEED_MORE_DATA: + continue; + + case WriteState::FINISHED: + NS_WARNING_ASSERTION(mGIFStruct.pixels_remaining <= 0, + "too many pixels"); + mGIFStruct.pixels_remaining = 0; + break; + + case WriteState::FAILURE: + return Transition::TerminateFailure(); + } + } + + // We're done, but keep going until we consume all the data in the sub-block. + return Transition::ContinueUnbuffered(State::LZW_DATA); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::SkipSubBlocks(const char* aData) +{ + // In the SKIP_SUB_BLOCKS state we skip over data sub-blocks that we're not + // interested in. Blocks consist of a block header (which can be up to 255 + // bytes in length) and a series of data sub-blocks. Each data sub-block + // consists of a single byte length value, followed by the data itself. A data + // sub-block with a length of zero terminates the overall block. + // SKIP_SUB_BLOCKS reads a sub-block length value. If it's zero, we've arrived + // at the next block. Otherwise, we enter the SKIP_DATA_THEN_SKIP_SUB_BLOCKS + // state to skip over the sub-block data and return to SKIP_SUB_BLOCKS at the + // start of the next sub-block. + + const uint8_t nextSubBlockLength = aData[0]; + if (nextSubBlockLength == 0) { + // We hit the block terminator, so the sequence of data sub-blocks is over; + // begin processing another block. + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + // Skip to the next sub-block length value. + return Transition::ToUnbuffered(State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + nextSubBlockLength); +} + +Maybe<Telemetry::ID> +nsGIFDecoder2::SpeedHistogram() const +{ + return Some(Telemetry::IMAGE_DECODE_SPEED_GIF); +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsGIFDecoder2.h b/image/decoders/nsGIFDecoder2.h new file mode 100644 index 000000000..c903ce890 --- /dev/null +++ b/image/decoders/nsGIFDecoder2.h @@ -0,0 +1,152 @@ +/* -*- 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_decoders_nsGIFDecoder2_h +#define mozilla_image_decoders_nsGIFDecoder2_h + +#include "Decoder.h" +#include "GIF2.h" +#include "StreamingLexer.h" +#include "SurfacePipe.h" + +namespace mozilla { +namespace image { +class RasterImage; + +////////////////////////////////////////////////////////////////////// +// nsGIFDecoder2 Definition + +class nsGIFDecoder2 : public Decoder +{ +public: + ~nsGIFDecoder2(); + +protected: + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; + + Maybe<Telemetry::ID> SpeedHistogram() const override; + +private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsGIFDecoder2(RasterImage* aImage); + + /// Called when we begin decoding the image. + void BeginGIF(); + + /** + * Called when we begin decoding a frame. + * + * @param aFrameRect The region of the image that contains data. The region + * outside this rect is transparent. + * @param aDepth The palette depth of this frame. + * @param aIsInterlaced If true, this frame is an interlaced frame. + */ + nsresult BeginImageFrame(const gfx::IntRect& aFrameRect, + uint16_t aDepth, + bool aIsInterlaced); + + /// Called when we finish decoding a frame. + void EndImageFrame(); + + /// Called when we finish decoding the entire image. + void FlushImageData(); + + /// Transforms a palette index into a pixel. + template <typename PixelSize> PixelSize + ColormapIndexToPixel(uint8_t aIndex); + + /// A generator function that performs LZW decompression and yields pixels. + template <typename PixelSize> NextPixel<PixelSize> + YieldPixel(const uint8_t* aData, size_t aLength, size_t* aBytesReadOut); + + /// Checks if we have transparency, either because the header indicates that + /// there's alpha, or because the frame rect doesn't cover the entire image. + bool CheckForTransparency(const gfx::IntRect& aFrameRect); + + // @return the clear code used for LZW decompression. + int ClearCode() const { return 1 << mGIFStruct.datasize; } + + enum class State + { + FAILURE, + SUCCESS, + GIF_HEADER, + SCREEN_DESCRIPTOR, + GLOBAL_COLOR_TABLE, + FINISHED_GLOBAL_COLOR_TABLE, + BLOCK_HEADER, + EXTENSION_HEADER, + GRAPHIC_CONTROL_EXTENSION, + APPLICATION_IDENTIFIER, + NETSCAPE_EXTENSION_SUB_BLOCK, + NETSCAPE_EXTENSION_DATA, + IMAGE_DESCRIPTOR, + FINISH_IMAGE_DESCRIPTOR, + LOCAL_COLOR_TABLE, + FINISHED_LOCAL_COLOR_TABLE, + IMAGE_DATA_BLOCK, + IMAGE_DATA_SUB_BLOCK, + LZW_DATA, + SKIP_LZW_DATA, + FINISHED_LZW_DATA, + SKIP_SUB_BLOCKS, + SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + FINISHED_SKIPPING_DATA + }; + + LexerTransition<State> ReadGIFHeader(const char* aData); + LexerTransition<State> ReadScreenDescriptor(const char* aData); + LexerTransition<State> ReadGlobalColorTable(const char* aData, size_t aLength); + LexerTransition<State> FinishedGlobalColorTable(); + LexerTransition<State> ReadBlockHeader(const char* aData); + LexerTransition<State> ReadExtensionHeader(const char* aData); + LexerTransition<State> ReadGraphicControlExtension(const char* aData); + LexerTransition<State> ReadApplicationIdentifier(const char* aData); + LexerTransition<State> ReadNetscapeExtensionSubBlock(const char* aData); + LexerTransition<State> ReadNetscapeExtensionData(const char* aData); + LexerTransition<State> ReadImageDescriptor(const char* aData); + LexerTransition<State> FinishImageDescriptor(const char* aData); + LexerTransition<State> ReadLocalColorTable(const char* aData, size_t aLength); + LexerTransition<State> FinishedLocalColorTable(); + LexerTransition<State> ReadImageDataBlock(const char* aData); + LexerTransition<State> ReadImageDataSubBlock(const char* aData); + LexerTransition<State> ReadLZWData(const char* aData, size_t aLength); + LexerTransition<State> SkipSubBlocks(const char* aData); + + // The StreamingLexer used to manage input. The initial size of the buffer is + // chosen as a little larger than the maximum size of any fixed-length data we + // have to read for a state. We read variable-length data in unbuffered mode + // so the buffer shouldn't have to be resized during decoding. + StreamingLexer<State, 16> mLexer; + + uint32_t mOldColor; // The old value of the transparent pixel + + // The frame number of the currently-decoding frame when we're in the middle + // of decoding it, and -1 otherwise. + int32_t mCurrentFrameIndex; + + // When we're reading in the global or local color table, this records our + // current position - i.e., the offset into which the next byte should be + // written. + size_t mColorTablePos; + + uint8_t mColorMask; // Apply this to the pixel to keep within colormap + bool mGIFOpen; + bool mSawTransparency; + + gif_struct mGIFStruct; + + SurfacePipe mPipe; /// The SurfacePipe used to write to the output surface. +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsGIFDecoder2_h diff --git a/image/decoders/nsICODecoder.cpp b/image/decoders/nsICODecoder.cpp new file mode 100644 index 000000000..633bb1255 --- /dev/null +++ b/image/decoders/nsICODecoder.cpp @@ -0,0 +1,673 @@ +/* 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 <stdlib.h> + +#include "mozilla/EndianUtils.h" +#include "mozilla/Move.h" + +#include "RasterImage.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()) + , mBiggestResourceColorDepth(0) + , mBestResourceDelta(INT_MIN) + , mBestResourceColorDepth(0) + , 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() +{ + return GetFinalStateFromContainedDecoder(); +} + +nsresult +nsICODecoder::GetFinalStateFromContainedDecoder() +{ + if (!mContainedDecoder) { + return NS_OK; + } + + MOZ_ASSERT(mContainedSourceBuffer, + "Should have a SourceBuffer if we have a decoder"); + + // Let the contained decoder finish up if necessary. + if (!mContainedSourceBuffer->IsComplete()) { + mContainedSourceBuffer->Complete(NS_OK); + mContainedDecoder->Decode(); + } + + // 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(); + + // Propagate errors. + nsresult rv = HasError() || mContainedDecoder->HasError() + ? NS_ERROR_FAILURE + : NS_OK; + + MOZ_ASSERT(NS_FAILED(rv) || !mCurrentFrame || mCurrentFrame->IsFinished()); + return rv; +} + +bool +nsICODecoder::CheckAndFixBitmapSize(int8_t* aBIH) +{ + // Get the width from the BMP file information header. This is + // (unintuitively) a signed integer; see the documentation at: + // + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx + // + // However, we reject negative widths since they aren't meaningful. + const int32_t width = LittleEndian::readInt32(aBIH + 4); + if (width <= 0 || width > 256) { + return false; + } + + // Verify that the BMP width matches the width we got from the ICO directory + // entry. If not, decoding fails, because if we were to allow it to continue + // the intrinsic size of the image wouldn't match the size of the decoded + // surface. + if (width != int32_t(GetRealWidth())) { + return false; + } + + // Get the height from the BMP file information header. This is also signed, + // but in this case negative values are meaningful; see below. + int32_t height = LittleEndian::readInt32(aBIH + 8); + if (height == 0) { + return false; + } + + // BMPs can be stored inverted by having a negative height. + // XXX(seth): Should we really be writing the absolute value into the BIH + // below? Seems like this could be problematic for inverted BMPs. + height = abs(height); + + // The height field is double the actual height of the image to account for + // the AND mask. This is true even if the AND mask is not present. + height /= 2; + if (height > 256) { + return false; + } + + // Verify that the BMP height matches the height we got from the ICO directory + // entry. If not, again, decoding fails. + if (height != int32_t(GetRealHeight())) { + return false; + } + + // Fix the BMP height in the BIH so that the BMP decoder, which does not know + // about the AND mask that may follow the actual bitmap, can work properly. + LittleEndian::writeInt32(aBIH + 8, GetRealHeight()); + + return true; +} + +LexerTransition<ICOState> +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<ICOState> +nsICODecoder::ReadDirEntry(const char* aData) +{ + mCurrIcon++; + + // Read the directory entry. + IconDirEntry 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 = LittleEndian::readUint32(aData + 12); + + // If an explicit output size was specified, we'll try to select the resource + // that matches it best below. + const Maybe<IntSize> desiredSize = ExplicitOutputSize(); + + // Determine if this is the biggest resource we've seen so far. 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. + IntSize entrySize(GetRealWidth(e), GetRealHeight(e)); + if (e.mBitCount >= mBiggestResourceColorDepth && + entrySize.width * entrySize.height >= + mBiggestResourceSize.width * mBiggestResourceSize.height) { + mBiggestResourceSize = entrySize; + mBiggestResourceColorDepth = e.mBitCount; + mBiggestResourceHotSpot = IntSize(e.mXHotspot, e.mYHotspot); + + 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(entrySize.width - desiredSize->width, + entrySize.height - desiredSize->height); + if (e.mBitCount >= mBestResourceColorDepth && + ((mBestResourceDelta < 0 && delta >= mBestResourceDelta) || + (delta >= 0 && delta <= mBestResourceDelta))) { + mBestResourceDelta = delta; + mBestResourceColorDepth = e.mBitCount; + mDirEntry = e; + } + } + + if (mCurrIcon == mNumIcons) { + // Ensure the resource we selected has an offset past the ICO headers. + if (mDirEntry.mImageOffset < FirstResourceOffset()) { + return Transition::TerminateFailure(); + } + + // 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(mBiggestResourceHotSpot.width, + mBiggestResourceHotSpot.height); + } + + // 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(mBiggestResourceSize.width, mBiggestResourceSize.height); + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + // If the resource we selected matches the output size perfectly, we don't + // need to do any downscaling. + if (GetRealSize() == OutputSize()) { + MOZ_ASSERT_IF(desiredSize, GetRealSize() == *desiredSize); + MOZ_ASSERT_IF(!desiredSize, GetRealSize() == Size()); + mDownscaler.reset(); + } + + size_t offsetToResource = mDirEntry.mImageOffset - FirstResourceOffset(); + return Transition::ToUnbuffered(ICOState::FOUND_RESOURCE, + ICOState::SKIP_TO_RESOURCE, + offsetToResource); + } + + return Transition::To(ICOState::DIR_ENTRY, ICODIRENTRYSIZE); +} + +LexerTransition<ICOState> +nsICODecoder::SniffResource(const char* aData) +{ + // 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) { + // Create a PNG decoder which will do the rest of the work for us. + mContainedSourceBuffer = new SourceBuffer(); + mContainedSourceBuffer->ExpectLength(mDirEntry.mBytesInRes); + mContainedDecoder = + DecoderFactory::CreateDecoderForICOResource(DecoderType::PNG, + WrapNotNull(mContainedSourceBuffer), + WrapNotNull(this)); + + if (!WriteToContainedDecoder(aData, PNGSIGNATURESIZE)) { + return Transition::TerminateFailure(); + } + + if (mDirEntry.mBytesInRes <= PNGSIGNATURESIZE) { + return Transition::TerminateFailure(); + } + + // Read in the rest of the PNG unbuffered. + size_t toRead = mDirEntry.mBytesInRes - PNGSIGNATURESIZE; + return Transition::ToUnbuffered(ICOState::FINISHED_RESOURCE, + ICOState::READ_PNG, + toRead); + } else { + // Make sure we have a sane size for the bitmap information header. + int32_t bihSize = LittleEndian::readUint32(aData); + if (bihSize != static_cast<int32_t>(BITMAPINFOSIZE)) { + return Transition::TerminateFailure(); + } + + // Buffer the first part of the bitmap information header. + memcpy(mBIHraw, aData, PNGSIGNATURESIZE); + + // Read in the rest of the bitmap information header. + return Transition::To(ICOState::READ_BIH, + BITMAPINFOSIZE - PNGSIGNATURESIZE); + } +} + +LexerTransition<ICOState> +nsICODecoder::ReadPNG(const char* aData, uint32_t aLen) +{ + if (!WriteToContainedDecoder(aData, aLen)) { + return Transition::TerminateFailure(); + } + + // Raymond Chen says that 32bpp only are valid PNG ICOs + // http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx + if (!static_cast<nsPNGDecoder*>(mContainedDecoder.get())->IsValidICO()) { + return Transition::TerminateFailure(); + } + + return Transition::ContinueUnbuffered(ICOState::READ_PNG); +} + +LexerTransition<ICOState> +nsICODecoder::ReadBIH(const char* aData) +{ + // Buffer the rest of the bitmap information header. + memcpy(mBIHraw + PNGSIGNATURESIZE, aData, BITMAPINFOSIZE - PNGSIGNATURESIZE); + + // 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(mBIHraw + 14); + + // 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; + if (mDirEntry.mBitCount <= 8) { + // The color table is present only if BPP is <= 8. + uint16_t numColors = GetNumColors(); + if (numColors == (uint16_t)-1) { + return Transition::TerminateFailure(); + } + dataOffset += 4 * numColors; + } + + // 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. + mContainedSourceBuffer = new SourceBuffer(); + mContainedSourceBuffer->ExpectLength(mDirEntry.mBytesInRes); + mContainedDecoder = + DecoderFactory::CreateDecoderForICOResource(DecoderType::BMP, + WrapNotNull(mContainedSourceBuffer), + WrapNotNull(this), + Some(dataOffset)); + RefPtr<nsBMPDecoder> bmpDecoder = + static_cast<nsBMPDecoder*>(mContainedDecoder.get()); + + // Verify that the BIH width and height values match the ICO directory entry, + // and fix the BIH height value to compensate for the fact that the underlying + // BMP decoder doesn't know about AND masks. + if (!CheckAndFixBitmapSize(reinterpret_cast<int8_t*>(mBIHraw))) { + return Transition::TerminateFailure(); + } + + // Write out the BMP's bitmap info header. + if (!WriteToContainedDecoder(mBIHraw, sizeof(mBIHraw))) { + return Transition::TerminateFailure(); + } + + // Check to make sure we have valid color settings. + uint16_t numColors = GetNumColors(); + if (numColors == uint16_t(-1)) { + return Transition::TerminateFailure(); + } + + // 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_BMP, + bmpDataLength); +} + +LexerTransition<ICOState> +nsICODecoder::ReadBMP(const char* aData, uint32_t aLen) +{ + if (!WriteToContainedDecoder(aData, aLen)) { + return Transition::TerminateFailure(); + } + + return Transition::ContinueUnbuffered(ICOState::READ_BMP); +} + +LexerTransition<ICOState> +nsICODecoder::PrepareForMask() +{ + RefPtr<nsBMPDecoder> bmpDecoder = + static_cast<nsBMPDecoder*>(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. We can + // also obviously ignore it if the image has zero width or zero height. + if (bmpDecoder->HasTransparency() || + GetRealWidth() == 0 || GetRealHeight() == 0) { + return Transition::ToUnbuffered(ICOState::FINISHED_RESOURCE, + ICOState::SKIP_MASK, + maskLength); + } + + // Compute the row size for the mask. + mMaskRowSize = ((GetRealWidth() + 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 * GetRealHeight(); + 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<uint8_t[]>(bmpDecoder->GetImageDataLength()); + nsresult rv = mDownscaler->BeginFrame(GetRealSize(), Nothing(), + mMaskBuffer.get(), + /* aHasAlpha = */ true, + /* aFlipVertically = */ true); + if (NS_FAILED(rv)) { + return Transition::TerminateFailure(); + } + } + + mCurrMaskLine = GetRealHeight(); + return Transition::To(ICOState::READ_MASK_ROW, mMaskRowSize); +} + + +LexerTransition<ICOState> +nsICODecoder::ReadMaskRow(const char* aData) +{ + mCurrMaskLine--; + + uint8_t sawTransparency = 0; + + // Get the mask row we're reading. + const uint8_t* mask = reinterpret_cast<const uint8_t*>(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, GetRealWidth() * sizeof(uint32_t)); + + decoded = reinterpret_cast<uint32_t*>(mDownscaler->RowBuffer()); + } else { + RefPtr<nsBMPDecoder> bmpDecoder = + static_cast<nsBMPDecoder*>(mContainedDecoder.get()); + uint32_t* imageData = bmpDecoder->GetImageData(); + if (!imageData) { + return Transition::TerminateFailure(); + } + + decoded = imageData + mCurrMaskLine * GetRealWidth(); + } + + MOZ_ASSERT(decoded); + uint32_t* decodedRowEnd = decoded + GetRealWidth(); + + // 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<ICOState> +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<nsBMPDecoder> bmpDecoder = + static_cast<nsBMPDecoder*>(mContainedDecoder.get()); + uint8_t* imageData = reinterpret_cast<uint8_t*>(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]; + } + } + + return Transition::To(ICOState::FINISHED_RESOURCE, 0); +} + +LexerTransition<ICOState> +nsICODecoder::FinishResource() +{ + // Make sure the actual size of the resource matches the size in the directory + // entry. If not, we consider the image corrupt. + if (mContainedDecoder->HasSize() && + mContainedDecoder->Size() != GetRealSize()) { + return Transition::TerminateFailure(); + } + + 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::SKIP_TO_RESOURCE: + return Transition::ContinueUnbuffered(ICOState::SKIP_TO_RESOURCE); + case ICOState::FOUND_RESOURCE: + return Transition::To(ICOState::SNIFF_RESOURCE, PNGSIGNATURESIZE); + case ICOState::SNIFF_RESOURCE: + return SniffResource(aData); + case ICOState::READ_PNG: + return ReadPNG(aData, aLength); + case ICOState::READ_BIH: + return ReadBIH(aData); + case ICOState::READ_BMP: + return ReadBMP(aData, aLength); + 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::WriteToContainedDecoder(const char* aBuffer, uint32_t aCount) +{ + MOZ_ASSERT(mContainedDecoder); + MOZ_ASSERT(mContainedSourceBuffer); + + // Append the provided data to the SourceBuffer that the contained decoder is + // reading from. + mContainedSourceBuffer->Append(aBuffer, aCount); + + bool succeeded = true; + + // Write to the contained decoder. 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. + 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 diff --git a/image/decoders/nsICODecoder.h b/image/decoders/nsICODecoder.h new file mode 100644 index 000000000..46e1377aa --- /dev/null +++ b/image/decoders/nsICODecoder.h @@ -0,0 +1,137 @@ +/* vim:set tw=80 expandtab softtabstop=4 ts=4 sw=4: */ +/* 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_decoders_nsICODecoder_h +#define mozilla_image_decoders_nsICODecoder_h + +#include "StreamingLexer.h" +#include "Decoder.h" +#include "imgFrame.h" +#include "mozilla/gfx/2D.h" +#include "nsBMPDecoder.h" +#include "nsPNGDecoder.h" +#include "ICOFileHeaders.h" + +namespace mozilla { +namespace image { + +class RasterImage; + +enum class ICOState +{ + HEADER, + DIR_ENTRY, + SKIP_TO_RESOURCE, + FOUND_RESOURCE, + SNIFF_RESOURCE, + READ_PNG, + READ_BIH, + READ_BMP, + PREPARE_FOR_MASK, + READ_MASK_ROW, + FINISH_MASK, + SKIP_MASK, + FINISHED_RESOURCE +}; + +class nsICODecoder : public Decoder +{ +public: + virtual ~nsICODecoder() { } + + /// @return the width of the icon directory entry @aEntry. + static uint32_t GetRealWidth(const IconDirEntry& aEntry) + { + return aEntry.mWidth == 0 ? 256 : aEntry.mWidth; + } + + /// @return the width of the selected directory entry (mDirEntry). + uint32_t GetRealWidth() const { return GetRealWidth(mDirEntry); } + + /// @return the height of the icon directory entry @aEntry. + static uint32_t GetRealHeight(const IconDirEntry& aEntry) + { + return aEntry.mHeight == 0 ? 256 : aEntry.mHeight; + } + + /// @return the height of the selected directory entry (mDirEntry). + uint32_t GetRealHeight() const { return GetRealHeight(mDirEntry); } + + /// @return the size of the selected directory entry (mDirEntry). + gfx::IntSize GetRealSize() const + { + return gfx::IntSize(GetRealWidth(), GetRealHeight()); + } + + /// @return The offset from the beginning of the ICO to the first resource. + size_t FirstResourceOffset() const; + + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; + nsresult FinishWithErrorInternal() override; + +private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsICODecoder(RasterImage* aImage); + + // Writes to the contained decoder and sets the appropriate errors + // Returns true if there are no errors. + bool WriteToContainedDecoder(const char* aBuffer, uint32_t aCount); + + // Gets decoder state from the contained decoder so it's visible externally. + nsresult GetFinalStateFromContainedDecoder(); + + /** + * Verifies that the width and height values in @aBIH are valid and match the + * values we read from the ICO directory entry. If everything looks OK, the + * height value in @aBIH is updated to compensate for the AND mask, which the + * underlying BMP decoder doesn't know about. + * + * @return true if the width and height values in @aBIH are valid and correct. + */ + bool CheckAndFixBitmapSize(int8_t* aBIH); + + // Obtains the number of colors from the BPP, mBPP must be filled in + uint16_t GetNumColors(); + + LexerTransition<ICOState> ReadHeader(const char* aData); + LexerTransition<ICOState> ReadDirEntry(const char* aData); + LexerTransition<ICOState> SniffResource(const char* aData); + LexerTransition<ICOState> ReadPNG(const char* aData, uint32_t aLen); + LexerTransition<ICOState> ReadBIH(const char* aData); + LexerTransition<ICOState> ReadBMP(const char* aData, uint32_t aLen); + LexerTransition<ICOState> PrepareForMask(); + LexerTransition<ICOState> ReadMaskRow(const char* aData); + LexerTransition<ICOState> FinishMask(); + LexerTransition<ICOState> FinishResource(); + + StreamingLexer<ICOState, 32> mLexer; // The lexer. + RefPtr<Decoder> mContainedDecoder; // Either a BMP or PNG decoder. + RefPtr<SourceBuffer> mContainedSourceBuffer; // SourceBuffer for mContainedDecoder. + UniquePtr<uint8_t[]> mMaskBuffer; // A temporary buffer for the alpha mask. + char mBIHraw[bmp::InfoHeaderLength::WIN_ICO]; // The bitmap information header. + IconDirEntry mDirEntry; // The dir entry for the selected resource. + gfx::IntSize mBiggestResourceSize; // Used to select the intrinsic size. + gfx::IntSize mBiggestResourceHotSpot; // Used to select the intrinsic size. + uint16_t mBiggestResourceColorDepth; // Used to select the intrinsic size. + int32_t mBestResourceDelta; // Used to select the best resource. + uint16_t mBestResourceColorDepth; // Used to select the best resource. + uint16_t mNumIcons; // Stores the number of icons in the ICO file. + uint16_t mCurrIcon; // Stores the current dir entry index we are processing. + uint16_t mBPP; // The BPP of the resource we're decoding. + uint32_t mMaskRowSize; // The size in bytes of each row in the BMP alpha mask. + uint32_t mCurrMaskLine; // The line of the BMP alpha mask we're processing. + bool mIsCursor; // Is this ICO a cursor? + bool mHasMaskAlpha; // Did the BMP alpha mask have any transparency? +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsICODecoder_h diff --git a/image/decoders/nsIconDecoder.cpp b/image/decoders/nsIconDecoder.cpp new file mode 100644 index 000000000..9ca63f5ad --- /dev/null +++ b/image/decoders/nsIconDecoder.cpp @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsIconDecoder.h" +#include "RasterImage.h" +#include "SurfacePipeFactory.h" + +using namespace mozilla::gfx; + +namespace mozilla { +namespace image { + +static const uint32_t ICON_HEADER_SIZE = 2; + +nsIconDecoder::nsIconDecoder(RasterImage* aImage) + : Decoder(aImage) + , mLexer(Transition::To(State::HEADER, ICON_HEADER_SIZE), + Transition::TerminateSuccess()) + , mBytesPerRow() // set by ReadHeader() +{ + // Nothing to do +} + +nsIconDecoder::~nsIconDecoder() +{ } + +LexerResult +nsIconDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) +{ + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::HEADER: + return ReadHeader(aData); + case State::ROW_OF_PIXELS: + return ReadRowOfPixels(aData, aLength); + case State::FINISH: + return Finish(); + default: + MOZ_CRASH("Unknown State"); + } + }); +} + +LexerTransition<nsIconDecoder::State> +nsIconDecoder::ReadHeader(const char* aData) +{ + // Grab the width and height. + uint8_t width = uint8_t(aData[0]); + uint8_t height = uint8_t(aData[1]); + + // The input is 32bpp, so we expect 4 bytes of data per pixel. + mBytesPerRow = width * 4; + + // Post our size to the superclass. + PostSize(width, height); + + // Icons have alpha. + PostHasTransparency(); + + // If we're doing a metadata decode, we're done. + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + MOZ_ASSERT(!mImageData, "Already have a buffer allocated?"); + Maybe<SurfacePipe> pipe = + SurfacePipeFactory::CreateSurfacePipe(this, 0, Size(), OutputSize(), + FullFrame(), SurfaceFormat::B8G8R8A8, + SurfacePipeFlags()); + if (!pipe) { + return Transition::TerminateFailure(); + } + + mPipe = Move(*pipe); + + MOZ_ASSERT(mImageData, "Should have a buffer now"); + + return Transition::To(State::ROW_OF_PIXELS, mBytesPerRow); +} + +LexerTransition<nsIconDecoder::State> +nsIconDecoder::ReadRowOfPixels(const char* aData, size_t aLength) +{ + MOZ_ASSERT(aLength % 4 == 0, "Rows should contain a multiple of four bytes"); + + auto result = mPipe.WritePixels<uint32_t>([&]() -> NextPixel<uint32_t> { + if (aLength == 0) { + return AsVariant(WriteState::NEED_MORE_DATA); // Done with this row. + } + + uint32_t pixel; + memcpy(&pixel, aData, 4); + aData += 4; + aLength -= 4; + + return AsVariant(pixel); + }); + + MOZ_ASSERT(result != WriteState::FAILURE); + + Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect(); + if (invalidRect) { + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); + } + + return result == WriteState::FINISHED + ? Transition::To(State::FINISH, 0) + : Transition::To(State::ROW_OF_PIXELS, mBytesPerRow); +} + +LexerTransition<nsIconDecoder::State> +nsIconDecoder::Finish() +{ + PostFrameStop(); + PostDecodeDone(); + + return Transition::TerminateSuccess(); +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsIconDecoder.h b/image/decoders/nsIconDecoder.h new file mode 100644 index 000000000..69198315b --- /dev/null +++ b/image/decoders/nsIconDecoder.h @@ -0,0 +1,67 @@ +/* -*- 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_decoders_nsIconDecoder_h +#define mozilla_image_decoders_nsIconDecoder_h + +#include "Decoder.h" +#include "StreamingLexer.h" +#include "SurfacePipe.h" + +namespace mozilla { +namespace image { + +class RasterImage; + +//////////////////////////////////////////////////////////////////////////////// +// The icon decoder is a decoder specifically tailored for loading icons +// from the OS. We've defined our own little format to represent these icons +// and this decoder takes that format and converts it into 24-bit RGB with +// alpha channel support. It was modeled a bit off the PPM decoder. +// +// The format of the incoming data is as follows: +// +// The first two bytes contain the width and the height of the icon. +// The remaining bytes contain the icon data, 4 bytes per pixel, in +// ARGB order (platform endianness, A in highest bits, B in lowest +// bits), row-primary, top-to-bottom, left-to-right, with +// premultiplied alpha. +// +//////////////////////////////////////////////////////////////////////////////// + +class nsIconDecoder : public Decoder +{ +public: + virtual ~nsIconDecoder(); + + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + +private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsIconDecoder(RasterImage* aImage); + + enum class State { + HEADER, + ROW_OF_PIXELS, + FINISH + }; + + LexerTransition<State> ReadHeader(const char* aData); + LexerTransition<State> ReadRowOfPixels(const char* aData, size_t aLength); + LexerTransition<State> Finish(); + + StreamingLexer<State> mLexer; + SurfacePipe mPipe; + uint32_t mBytesPerRow; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsIconDecoder_h diff --git a/image/decoders/nsJPEGDecoder.cpp b/image/decoders/nsJPEGDecoder.cpp new file mode 100644 index 000000000..e76ffcbaf --- /dev/null +++ b/image/decoders/nsJPEGDecoder.cpp @@ -0,0 +1,1006 @@ +/* -*- 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" // Must appear first. + +#include "nsJPEGDecoder.h" + +#include <cstdint> + +#include "imgFrame.h" +#include "Orientation.h" +#include "EXIF.h" + +#include "nsIInputStream.h" + +#include "nspr.h" +#include "nsCRT.h" +#include "gfxColor.h" + +#include "jerror.h" + +#include "gfxPlatform.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/Telemetry.h" + +extern "C" { +#include "iccjpeg.h" +} + +#if MOZ_BIG_ENDIAN +#define MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB JCS_EXT_XRGB +#else +#define MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB JCS_EXT_BGRX +#endif + +static void cmyk_convert_rgb(JSAMPROW row, JDIMENSION width); + +namespace mozilla { +namespace image { + +static mozilla::LazyLogModule sJPEGLog("JPEGDecoder"); + +static mozilla::LazyLogModule sJPEGDecoderAccountingLog("JPEGDecoderAccounting"); + +static qcms_profile* +GetICCProfile(struct jpeg_decompress_struct& info) +{ + JOCTET* profilebuf; + uint32_t profileLength; + qcms_profile* profile = nullptr; + + if (read_icc_profile(&info, &profilebuf, &profileLength)) { + profile = qcms_profile_from_memory(profilebuf, profileLength); + free(profilebuf); + } + + return profile; +} + +METHODDEF(void) init_source (j_decompress_ptr jd); +METHODDEF(boolean) fill_input_buffer (j_decompress_ptr jd); +METHODDEF(void) skip_input_data (j_decompress_ptr jd, long num_bytes); +METHODDEF(void) term_source (j_decompress_ptr jd); +METHODDEF(void) my_error_exit (j_common_ptr cinfo); + +// Normal JFIF markers can't have more bytes than this. +#define MAX_JPEG_MARKER_LENGTH (((uint32_t)1 << 16) - 1) + +nsJPEGDecoder::nsJPEGDecoder(RasterImage* aImage, + Decoder::DecodeStyle aDecodeStyle) + : Decoder(aImage) + , mLexer(Transition::ToUnbuffered(State::FINISHED_JPEG_DATA, + State::JPEG_DATA, + SIZE_MAX), + Transition::TerminateSuccess()) + , mDecodeStyle(aDecodeStyle) + , mSampleSize(0) +{ + mState = JPEG_HEADER; + mReading = true; + mImageData = nullptr; + + mBytesToSkip = 0; + memset(&mInfo, 0, sizeof(jpeg_decompress_struct)); + memset(&mSourceMgr, 0, sizeof(mSourceMgr)); + mInfo.client_data = (void*)this; + + mSegment = nullptr; + mSegmentLen = 0; + + mBackBuffer = nullptr; + mBackBufferLen = mBackBufferSize = mBackBufferUnreadLen = 0; + + mInProfile = nullptr; + mTransform = nullptr; + + mCMSMode = 0; + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("nsJPEGDecoder::nsJPEGDecoder: Creating JPEG decoder %p", + this)); +} + +nsJPEGDecoder::~nsJPEGDecoder() +{ + // Step 8: Release JPEG decompression object + mInfo.src = nullptr; + jpeg_destroy_decompress(&mInfo); + + PR_FREEIF(mBackBuffer); + if (mTransform) { + qcms_transform_release(mTransform); + } + if (mInProfile) { + qcms_profile_release(mInProfile); + } + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("nsJPEGDecoder::~nsJPEGDecoder: Destroying JPEG decoder %p", + this)); +} + +Maybe<Telemetry::ID> +nsJPEGDecoder::SpeedHistogram() const +{ + return Some(Telemetry::IMAGE_DECODE_SPEED_JPEG); +} + +nsresult +nsJPEGDecoder::InitInternal() +{ + mCMSMode = gfxPlatform::GetCMSMode(); + if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) { + mCMSMode = eCMSMode_Off; + } + + // We set up the normal JPEG error routines, then override error_exit. + mInfo.err = jpeg_std_error(&mErr.pub); + // mInfo.err = jpeg_std_error(&mErr.pub); + mErr.pub.error_exit = my_error_exit; + // Establish the setjmp return context for my_error_exit to use. + if (setjmp(mErr.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error, and initialization + // has failed. + return NS_ERROR_FAILURE; + } + + // Step 1: allocate and initialize JPEG decompression object + jpeg_create_decompress(&mInfo); + // Set the source manager + mInfo.src = &mSourceMgr; + + // Step 2: specify data source (eg, a file) + + // Setup callback functions. + mSourceMgr.init_source = init_source; + mSourceMgr.fill_input_buffer = fill_input_buffer; + mSourceMgr.skip_input_data = skip_input_data; + mSourceMgr.resync_to_restart = jpeg_resync_to_restart; + mSourceMgr.term_source = term_source; + + // Record app markers for ICC data + for (uint32_t m = 0; m < 16; m++) { + jpeg_save_markers(&mInfo, JPEG_APP0 + m, 0xFFFF); + } + + return NS_OK; +} + +nsresult +nsJPEGDecoder::FinishInternal() +{ + // If we're not in any sort of error case, force our state to JPEG_DONE. + if ((mState != JPEG_DONE && mState != JPEG_SINK_NON_JPEG_TRAILER) && + (mState != JPEG_ERROR) && + !IsMetadataDecode()) { + mState = JPEG_DONE; + } + + return NS_OK; +} + +LexerResult +nsJPEGDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) +{ + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::JPEG_DATA: + return ReadJPEGData(aData, aLength); + case State::FINISHED_JPEG_DATA: + return FinishedJPEGData(); + } + MOZ_CRASH("Unknown State"); + }); +} + +LexerTransition<nsJPEGDecoder::State> +nsJPEGDecoder::ReadJPEGData(const char* aData, size_t aLength) +{ + mSegment = reinterpret_cast<const JOCTET*>(aData); + mSegmentLen = aLength; + + // Return here if there is a fatal error within libjpeg. + nsresult error_code; + // This cast to nsresult makes sense because setjmp() returns whatever we + // passed to longjmp(), which was actually an nsresult. + if ((error_code = static_cast<nsresult>(setjmp(mErr.setjmp_buffer))) != NS_OK) { + if (error_code == NS_ERROR_FAILURE) { + // Error due to corrupt data. Make sure that we don't feed any more data + // to libjpeg-turbo. + mState = JPEG_SINK_NON_JPEG_TRAILER; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (setjmp returned NS_ERROR_FAILURE)")); + } else { + // Error for another reason. (Possibly OOM.) + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (setjmp returned an error)")); + } + + return Transition::TerminateFailure(); + } + + MOZ_LOG(sJPEGLog, LogLevel::Debug, + ("[this=%p] nsJPEGDecoder::Write -- processing JPEG data\n", this)); + + switch (mState) { + case JPEG_HEADER: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, "nsJPEGDecoder::Write -- entering JPEG_HEADER" + " case"); + + // Step 3: read file parameters with jpeg_read_header() + if (jpeg_read_header(&mInfo, TRUE) == JPEG_SUSPENDED) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (JPEG_SUSPENDED)")); + return Transition::ContinueUnbuffered(State::JPEG_DATA); // I/O suspension + } + + // If we have a sample size specified for -moz-sample-size, use it. + if (mSampleSize > 0) { + mInfo.scale_num = 1; + mInfo.scale_denom = mSampleSize; + } + + // Used to set up image size so arrays can be allocated + jpeg_calc_output_dimensions(&mInfo); + + // Post our size to the superclass + PostSize(mInfo.output_width, mInfo.output_height, + ReadOrientationFromEXIF()); + if (HasError()) { + // Setting the size led to an error. + mState = JPEG_ERROR; + return Transition::TerminateFailure(); + } + + // If we're doing a metadata decode, we're done. + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + // We're doing a full decode. + if (mCMSMode != eCMSMode_Off && + (mInProfile = GetICCProfile(mInfo)) != nullptr) { + uint32_t profileSpace = qcms_profile_get_color_space(mInProfile); + bool mismatch = false; + +#ifdef DEBUG_tor + fprintf(stderr, "JPEG profileSpace: 0x%08X\n", profileSpace); +#endif + switch (mInfo.jpeg_color_space) { + case JCS_GRAYSCALE: + if (profileSpace == icSigRgbData) { + mInfo.out_color_space = JCS_RGB; + } else if (profileSpace != icSigGrayData) { + mismatch = true; + } + break; + case JCS_RGB: + if (profileSpace != icSigRgbData) { + mismatch = true; + } + break; + case JCS_YCbCr: + if (profileSpace == icSigRgbData) { + mInfo.out_color_space = JCS_RGB; + } else { + // qcms doesn't support ycbcr + mismatch = true; + } + break; + case JCS_CMYK: + case JCS_YCCK: + // qcms doesn't support cmyk + mismatch = true; + break; + default: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (unknown colorpsace (1))")); + return Transition::TerminateFailure(); + } + + if (!mismatch) { + qcms_data_type type; + switch (mInfo.out_color_space) { + case JCS_GRAYSCALE: + type = QCMS_DATA_GRAY_8; + break; + case JCS_RGB: + type = QCMS_DATA_RGB_8; + break; + default: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (unknown colorpsace (2))")); + return Transition::TerminateFailure(); + } +#if 0 + // We don't currently support CMYK profiles. The following + // code dealt with lcms types. Add something like this + // back when we gain support for CMYK. + + // Adobe Photoshop writes YCCK/CMYK files with inverted data + if (mInfo.out_color_space == JCS_CMYK) { + type |= FLAVOR_SH(mInfo.saw_Adobe_marker ? 1 : 0); + } +#endif + + if (gfxPlatform::GetCMSOutputProfile()) { + + // Calculate rendering intent. + int intent = gfxPlatform::GetRenderingIntent(); + if (intent == -1) { + intent = qcms_profile_get_rendering_intent(mInProfile); + } + + // Create the color management transform. + mTransform = qcms_transform_create(mInProfile, + type, + gfxPlatform::GetCMSOutputProfile(), + QCMS_DATA_RGB_8, + (qcms_intent)intent); + } + } else { +#ifdef DEBUG_tor + fprintf(stderr, "ICM profile colorspace mismatch\n"); +#endif + } + } + + if (!mTransform) { + switch (mInfo.jpeg_color_space) { + case JCS_GRAYSCALE: + case JCS_RGB: + case JCS_YCbCr: + // if we're not color managing we can decode directly to + // MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB + if (mCMSMode != eCMSMode_All) { + mInfo.out_color_space = MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB; + mInfo.out_color_components = 4; + } else { + mInfo.out_color_space = JCS_RGB; + } + break; + case JCS_CMYK: + case JCS_YCCK: + // libjpeg can convert from YCCK to CMYK, but not to RGB + mInfo.out_color_space = JCS_CMYK; + break; + default: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (unknown colorpsace (3))")); + return Transition::TerminateFailure(); + } + } + + // Don't allocate a giant and superfluous memory buffer + // when not doing a progressive decode. + mInfo.buffered_image = mDecodeStyle == PROGRESSIVE && + jpeg_has_multiple_scans(&mInfo); + + MOZ_ASSERT(!mImageData, "Already have a buffer allocated?"); + nsresult rv = AllocateFrame(/* aFrameNum = */ 0, OutputSize(), + FullOutputFrame(), SurfaceFormat::B8G8R8X8); + if (NS_FAILED(rv)) { + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (could not initialize image frame)")); + return Transition::TerminateFailure(); + } + + MOZ_ASSERT(mImageData, "Should have a buffer now"); + + if (mDownscaler) { + nsresult rv = mDownscaler->BeginFrame(Size(), Nothing(), + mImageData, + /* aHasAlpha = */ false); + if (NS_FAILED(rv)) { + mState = JPEG_ERROR; + return Transition::TerminateFailure(); + } + } + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + (" JPEGDecoderAccounting: nsJPEGDecoder::" + "Write -- created image frame with %ux%u pixels", + mInfo.output_width, mInfo.output_height)); + + mState = JPEG_START_DECOMPRESS; + MOZ_FALLTHROUGH; // to start decompressing. + } + + case JPEG_START_DECOMPRESS: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, "nsJPEGDecoder::Write -- entering" + " JPEG_START_DECOMPRESS case"); + // Step 4: set parameters for decompression + + // FIXME -- Should reset dct_method and dither mode + // for final pass of progressive JPEG + + mInfo.dct_method = JDCT_ISLOW; + mInfo.dither_mode = JDITHER_FS; + mInfo.do_fancy_upsampling = TRUE; + mInfo.enable_2pass_quant = FALSE; + mInfo.do_block_smoothing = TRUE; + + // Step 5: Start decompressor + if (jpeg_start_decompress(&mInfo) == FALSE) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_start_decompress())")); + return Transition::ContinueUnbuffered(State::JPEG_DATA); // I/O suspension + } + + // If this is a progressive JPEG ... + mState = mInfo.buffered_image ? + JPEG_DECOMPRESS_PROGRESSIVE : JPEG_DECOMPRESS_SEQUENTIAL; + MOZ_FALLTHROUGH; // to decompress sequential JPEG. + } + + case JPEG_DECOMPRESS_SEQUENTIAL: { + if (mState == JPEG_DECOMPRESS_SEQUENTIAL) { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, "nsJPEGDecoder::Write -- " + "JPEG_DECOMPRESS_SEQUENTIAL case"); + + bool suspend; + OutputScanlines(&suspend); + + if (suspend) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after OutputScanlines() - SEQUENTIAL)")); + return Transition::ContinueUnbuffered(State::JPEG_DATA); // I/O suspension + } + + // If we've completed image output ... + NS_ASSERTION(mInfo.output_scanline == mInfo.output_height, + "We didn't process all of the data!"); + mState = JPEG_DONE; + } + MOZ_FALLTHROUGH; // to decompress progressive JPEG. + } + + case JPEG_DECOMPRESS_PROGRESSIVE: { + if (mState == JPEG_DECOMPRESS_PROGRESSIVE) { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- JPEG_DECOMPRESS_PROGRESSIVE case"); + + int status; + do { + status = jpeg_consume_input(&mInfo); + } while ((status != JPEG_SUSPENDED) && + (status != JPEG_REACHED_EOI)); + + for (;;) { + if (mInfo.output_scanline == 0) { + int scan = mInfo.input_scan_number; + + // if we haven't displayed anything yet (output_scan_number==0) + // and we have enough data for a complete scan, force output + // of the last full scan + if ((mInfo.output_scan_number == 0) && + (scan > 1) && + (status != JPEG_REACHED_EOI)) + scan--; + + if (!jpeg_start_output(&mInfo, scan)) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_start_output() -" + " PROGRESSIVE)")); + return Transition::ContinueUnbuffered(State::JPEG_DATA); // I/O suspension + } + } + + if (mInfo.output_scanline == 0xffffff) { + mInfo.output_scanline = 0; + } + + bool suspend; + OutputScanlines(&suspend); + + if (suspend) { + if (mInfo.output_scanline == 0) { + // didn't manage to read any lines - flag so we don't call + // jpeg_start_output() multiple times for the same scan + mInfo.output_scanline = 0xffffff; + } + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after OutputScanlines() - PROGRESSIVE)")); + return Transition::ContinueUnbuffered(State::JPEG_DATA); // I/O suspension + } + + if (mInfo.output_scanline == mInfo.output_height) { + if (!jpeg_finish_output(&mInfo)) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_finish_output() -" + " PROGRESSIVE)")); + return Transition::ContinueUnbuffered(State::JPEG_DATA); // I/O suspension + } + + if (jpeg_input_complete(&mInfo) && + (mInfo.input_scan_number == mInfo.output_scan_number)) + break; + + mInfo.output_scanline = 0; + if (mDownscaler) { + mDownscaler->ResetForNextProgressivePass(); + } + } + } + + mState = JPEG_DONE; + } + MOZ_FALLTHROUGH; // to finish decompressing. + } + + case JPEG_DONE: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, "nsJPEGDecoder::ProcessData -- entering" + " JPEG_DONE case"); + + // Step 7: Finish decompression + + if (jpeg_finish_decompress(&mInfo) == FALSE) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_finish_decompress() - DONE)")); + return Transition::ContinueUnbuffered(State::JPEG_DATA); // I/O suspension + } + + // Make sure we don't feed any more data to libjpeg-turbo. + mState = JPEG_SINK_NON_JPEG_TRAILER; + + // We're done. + return Transition::TerminateSuccess(); + } + case JPEG_SINK_NON_JPEG_TRAILER: + MOZ_LOG(sJPEGLog, LogLevel::Debug, + ("[this=%p] nsJPEGDecoder::ProcessData -- entering" + " JPEG_SINK_NON_JPEG_TRAILER case\n", this)); + + MOZ_ASSERT_UNREACHABLE("Should stop getting data after entering state " + "JPEG_SINK_NON_JPEG_TRAILER"); + + return Transition::TerminateSuccess(); + + case JPEG_ERROR: + MOZ_ASSERT_UNREACHABLE("Should stop getting data after entering state " + "JPEG_ERROR"); + + return Transition::TerminateFailure(); + } + + MOZ_ASSERT_UNREACHABLE("Escaped the JPEG decoder state machine"); + return Transition::TerminateFailure(); +} + +LexerTransition<nsJPEGDecoder::State> +nsJPEGDecoder::FinishedJPEGData() +{ + // Since we set up an unbuffered read for SIZE_MAX bytes, if we actually read + // all that data something is really wrong. + MOZ_ASSERT_UNREACHABLE("Read the entire address space?"); + return Transition::TerminateFailure(); +} + +Orientation +nsJPEGDecoder::ReadOrientationFromEXIF() +{ + jpeg_saved_marker_ptr marker; + + // Locate the APP1 marker, where EXIF data is stored, in the marker list. + for (marker = mInfo.marker_list ; marker != nullptr ; marker = marker->next) { + if (marker->marker == JPEG_APP0 + 1) { + break; + } + } + + // If we're at the end of the list, there's no EXIF data. + if (!marker) { + return Orientation(); + } + + // Extract the orientation information. + EXIFData exif = EXIFParser::Parse(marker->data, + static_cast<uint32_t>(marker->data_length)); + return exif.orientation; +} + +void +nsJPEGDecoder::NotifyDone() +{ + PostFrameStop(Opacity::FULLY_OPAQUE); + PostDecodeDone(); +} + +void +nsJPEGDecoder::OutputScanlines(bool* suspend) +{ + *suspend = false; + + const uint32_t top = mInfo.output_scanline; + + while ((mInfo.output_scanline < mInfo.output_height)) { + uint32_t* imageRow = nullptr; + if (mDownscaler) { + imageRow = reinterpret_cast<uint32_t*>(mDownscaler->RowBuffer()); + } else { + imageRow = reinterpret_cast<uint32_t*>(mImageData) + + (mInfo.output_scanline * mInfo.output_width); + } + + MOZ_ASSERT(imageRow, "Should have a row buffer here"); + + if (mInfo.out_color_space == MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB) { + // Special case: scanline will be directly converted into packed ARGB + if (jpeg_read_scanlines(&mInfo, (JSAMPARRAY)&imageRow, 1) != 1) { + *suspend = true; // suspend + break; + } + if (mDownscaler) { + mDownscaler->CommitRow(); + } + continue; // all done for this row! + } + + JSAMPROW sampleRow = (JSAMPROW)imageRow; + if (mInfo.output_components == 3) { + // Put the pixels at end of row to enable in-place expansion + sampleRow += mInfo.output_width; + } + + // Request one scanline. Returns 0 or 1 scanlines. + if (jpeg_read_scanlines(&mInfo, &sampleRow, 1) != 1) { + *suspend = true; // suspend + break; + } + + if (mTransform) { + JSAMPROW source = sampleRow; + if (mInfo.out_color_space == JCS_GRAYSCALE) { + // Convert from the 1byte grey pixels at begin of row + // to the 3byte RGB byte pixels at 'end' of row + sampleRow += mInfo.output_width; + } + qcms_transform_data(mTransform, source, sampleRow, mInfo.output_width); + // Move 3byte RGB data to end of row + if (mInfo.out_color_space == JCS_CMYK) { + memmove(sampleRow + mInfo.output_width, + sampleRow, + 3 * mInfo.output_width); + sampleRow += mInfo.output_width; + } + } else { + if (mInfo.out_color_space == JCS_CMYK) { + // Convert from CMYK to RGB + // We cannot convert directly to Cairo, as the CMSRGBTransform + // may wants to do a RGB transform... + // Would be better to have platform CMSenabled transformation + // from CMYK to (A)RGB... + cmyk_convert_rgb((JSAMPROW)imageRow, mInfo.output_width); + sampleRow += mInfo.output_width; + } + if (mCMSMode == eCMSMode_All) { + // No embedded ICC profile - treat as sRGB + qcms_transform* transform = gfxPlatform::GetCMSRGBTransform(); + if (transform) { + qcms_transform_data(transform, sampleRow, sampleRow, + mInfo.output_width); + } + } + } + + // counter for while() loops below + uint32_t idx = mInfo.output_width; + + // copy as bytes until source pointer is 32-bit-aligned + for (; (NS_PTR_TO_UINT32(sampleRow) & 0x3) && idx; --idx) { + *imageRow++ = gfxPackedPixel(0xFF, sampleRow[0], sampleRow[1], + sampleRow[2]); + sampleRow += 3; + } + + // copy pixels in blocks of 4 + while (idx >= 4) { + GFX_BLOCK_RGB_TO_FRGB(sampleRow, imageRow); + idx -= 4; + sampleRow += 12; + imageRow += 4; + } + + // copy remaining pixel(s) + while (idx--) { + // 32-bit read of final pixel will exceed buffer, so read bytes + *imageRow++ = gfxPackedPixel(0xFF, sampleRow[0], sampleRow[1], + sampleRow[2]); + sampleRow += 3; + } + + if (mDownscaler) { + mDownscaler->CommitRow(); + } + } + + if (mDownscaler && mDownscaler->HasInvalidation()) { + DownscalerInvalidRect invalidRect = mDownscaler->TakeInvalidRect(); + PostInvalidation(invalidRect.mOriginalSizeRect, + Some(invalidRect.mTargetSizeRect)); + MOZ_ASSERT(!mDownscaler->HasInvalidation()); + } else if (!mDownscaler && top != mInfo.output_scanline) { + PostInvalidation(nsIntRect(0, top, + mInfo.output_width, + mInfo.output_scanline - top)); + } +} + +// Override the standard error method in the IJG JPEG decoder code. +METHODDEF(void) +my_error_exit (j_common_ptr cinfo) +{ + decoder_error_mgr* err = (decoder_error_mgr*) cinfo->err; + + // Convert error to a browser error code + nsresult error_code = err->pub.msg_code == JERR_OUT_OF_MEMORY + ? NS_ERROR_OUT_OF_MEMORY + : NS_ERROR_FAILURE; + +#ifdef DEBUG + char buffer[JMSG_LENGTH_MAX]; + + // Create the message + (*err->pub.format_message) (cinfo, buffer); + + fprintf(stderr, "JPEG decoding error:\n%s\n", buffer); +#endif + + // 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)); +} + +/******************************************************************************* + * This is the callback routine from the IJG JPEG library used to supply new + * data to the decompressor when its input buffer is exhausted. It juggles + * multiple buffers in an attempt to avoid unnecessary copying of input data. + * + * (A simpler scheme is possible: It's much easier to use only a single + * buffer; when fill_input_buffer() is called, move any unconsumed data + * (beyond the current pointer/count) down to the beginning of this buffer and + * then load new data into the remaining buffer space. This approach requires + * a little more data copying but is far easier to get right.) + * + * At any one time, the JPEG decompressor is either reading from the necko + * input buffer, which is volatile across top-level calls to the IJG library, + * or the "backtrack" buffer. The backtrack buffer contains the remaining + * unconsumed data from the necko buffer after parsing was suspended due + * to insufficient data in some previous call to the IJG library. + * + * When suspending, the decompressor will back up to a convenient restart + * point (typically the start of the current MCU). The variables + * next_input_byte & bytes_in_buffer indicate where the restart point will be + * if the current call returns FALSE. Data beyond this point must be + * rescanned after resumption, so it must be preserved in case the decompressor + * decides to backtrack. + * + * Returns: + * TRUE if additional data is available, FALSE if no data present and + * the JPEG library should therefore suspend processing of input stream + ******************************************************************************/ + +/******************************************************************************/ +/* data source manager method */ +/******************************************************************************/ + +/******************************************************************************/ +/* data source manager method + Initialize source. This is called by jpeg_read_header() before any + data is actually read. May leave + bytes_in_buffer set to 0 (in which case a fill_input_buffer() call + will occur immediately). +*/ +METHODDEF(void) +init_source (j_decompress_ptr jd) +{ +} + +/******************************************************************************/ +/* data source manager method + Skip num_bytes worth of data. The buffer pointer and count should + be advanced over num_bytes input bytes, refilling the buffer as + needed. This is used to skip over a potentially large amount of + uninteresting data (such as an APPn marker). In some applications + it may be possible to optimize away the reading of the skipped data, + but it's not clear that being smart is worth much trouble; large + skips are uncommon. bytes_in_buffer may be zero on return. + A zero or negative skip count should be treated as a no-op. +*/ +METHODDEF(void) +skip_input_data (j_decompress_ptr jd, long num_bytes) +{ + struct jpeg_source_mgr* src = jd->src; + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + if (num_bytes > (long)src->bytes_in_buffer) { + // Can't skip it all right now until we get more data from + // network stream. Set things up so that fill_input_buffer + // will skip remaining amount. + decoder->mBytesToSkip = (size_t)num_bytes - src->bytes_in_buffer; + src->next_input_byte += src->bytes_in_buffer; + src->bytes_in_buffer = 0; + + } else { + // Simple case. Just advance buffer pointer + + src->bytes_in_buffer -= (size_t)num_bytes; + src->next_input_byte += num_bytes; + } +} + +/******************************************************************************/ +/* data source manager method + This is called whenever bytes_in_buffer has reached zero and more + data is wanted. In typical applications, it should read fresh data + into the buffer (ignoring the current state of next_input_byte and + bytes_in_buffer), reset the pointer & count to the start of the + buffer, and return TRUE indicating that the buffer has been reloaded. + It is not necessary to fill the buffer entirely, only to obtain at + least one more byte. bytes_in_buffer MUST be set to a positive value + if TRUE is returned. A FALSE return should only be used when I/O + suspension is desired. +*/ +METHODDEF(boolean) +fill_input_buffer (j_decompress_ptr jd) +{ + struct jpeg_source_mgr* src = jd->src; + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + if (decoder->mReading) { + const JOCTET* new_buffer = decoder->mSegment; + uint32_t new_buflen = decoder->mSegmentLen; + + if (!new_buffer || new_buflen == 0) { + return false; // suspend + } + + decoder->mSegmentLen = 0; + + if (decoder->mBytesToSkip) { + if (decoder->mBytesToSkip < new_buflen) { + // All done skipping bytes; Return what's left. + new_buffer += decoder->mBytesToSkip; + new_buflen -= decoder->mBytesToSkip; + decoder->mBytesToSkip = 0; + } else { + // Still need to skip some more data in the future + decoder->mBytesToSkip -= (size_t)new_buflen; + return false; // suspend + } + } + + decoder->mBackBufferUnreadLen = src->bytes_in_buffer; + + src->next_input_byte = new_buffer; + src->bytes_in_buffer = (size_t)new_buflen; + decoder->mReading = false; + + return true; + } + + if (src->next_input_byte != decoder->mSegment) { + // Backtrack data has been permanently consumed. + decoder->mBackBufferUnreadLen = 0; + decoder->mBackBufferLen = 0; + } + + // Save remainder of netlib buffer in backtrack buffer + const uint32_t new_backtrack_buflen = src->bytes_in_buffer + + decoder->mBackBufferLen; + + // Make sure backtrack buffer is big enough to hold new data. + if (decoder->mBackBufferSize < new_backtrack_buflen) { + // Check for malformed MARKER segment lengths, before allocating space + // for it + if (new_backtrack_buflen > MAX_JPEG_MARKER_LENGTH) { + my_error_exit((j_common_ptr)(&decoder->mInfo)); + } + + // Round up to multiple of 256 bytes. + const size_t roundup_buflen = ((new_backtrack_buflen + 255) >> 8) << 8; + JOCTET* buf = (JOCTET*)PR_REALLOC(decoder->mBackBuffer, roundup_buflen); + // Check for OOM + if (!buf) { + decoder->mInfo.err->msg_code = JERR_OUT_OF_MEMORY; + my_error_exit((j_common_ptr)(&decoder->mInfo)); + } + decoder->mBackBuffer = buf; + decoder->mBackBufferSize = roundup_buflen; + } + + // Copy remainder of netlib segment into backtrack buffer. + memmove(decoder->mBackBuffer + decoder->mBackBufferLen, + src->next_input_byte, + src->bytes_in_buffer); + + // Point to start of data to be rescanned. + src->next_input_byte = decoder->mBackBuffer + decoder->mBackBufferLen - + decoder->mBackBufferUnreadLen; + src->bytes_in_buffer += decoder->mBackBufferUnreadLen; + decoder->mBackBufferLen = (size_t)new_backtrack_buflen; + decoder->mReading = true; + + return false; +} + +/******************************************************************************/ +/* data source manager method */ +/* + * Terminate source --- called by jpeg_finish_decompress() after all + * data has been read to clean up JPEG source manager. NOT called by + * jpeg_abort() or jpeg_destroy(). + */ +METHODDEF(void) +term_source (j_decompress_ptr jd) +{ + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + // This function shouldn't be called if we ran into an error we didn't + // recover from. + MOZ_ASSERT(decoder->mState != JPEG_ERROR, + "Calling term_source on a JPEG with mState == JPEG_ERROR!"); + + // Notify using a helper method to get around protectedness issues. + decoder->NotifyDone(); +} + +} // namespace image +} // namespace mozilla + +///*************** Inverted CMYK -> RGB conversion ************************* +/// Input is (Inverted) CMYK stored as 4 bytes per pixel. +/// Output is RGB stored as 3 bytes per pixel. +/// @param row Points to row buffer containing the CMYK bytes for each pixel +/// in the row. +/// @param width Number of pixels in the row. +static void cmyk_convert_rgb(JSAMPROW row, JDIMENSION width) +{ + // Work from end to front to shrink from 4 bytes per pixel to 3 + JSAMPROW in = row + width*4; + JSAMPROW out = in; + + for (uint32_t i = width; i > 0; i--) { + in -= 4; + out -= 3; + + // Source is 'Inverted CMYK', output is RGB. + // See: http://www.easyrgb.com/math.php?MATH=M12#text12 + // Or: http://www.ilkeratalay.com/colorspacesfaq.php#rgb + + // From CMYK to CMY + // C = ( C * ( 1 - K ) + K ) + // M = ( M * ( 1 - K ) + K ) + // Y = ( Y * ( 1 - K ) + K ) + + // From Inverted CMYK to CMY is thus: + // C = ( (1-iC) * (1 - (1-iK)) + (1-iK) ) => 1 - iC*iK + // Same for M and Y + + // Convert from CMY (0..1) to RGB (0..1) + // R = 1 - C => 1 - (1 - iC*iK) => iC*iK + // G = 1 - M => 1 - (1 - iM*iK) => iM*iK + // B = 1 - Y => 1 - (1 - iY*iK) => iY*iK + + // Convert from Inverted CMYK (0..255) to RGB (0..255) + const uint32_t iC = in[0]; + const uint32_t iM = in[1]; + const uint32_t iY = in[2]; + const uint32_t iK = in[3]; + out[0] = iC*iK/255; // Red + out[1] = iM*iK/255; // Green + out[2] = iY*iK/255; // Blue + } +} diff --git a/image/decoders/nsJPEGDecoder.h b/image/decoders/nsJPEGDecoder.h new file mode 100644 index 000000000..260e91303 --- /dev/null +++ b/image/decoders/nsJPEGDecoder.h @@ -0,0 +1,125 @@ +/* -*- 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_decoders_nsJPEGDecoder_h +#define mozilla_image_decoders_nsJPEGDecoder_h + +#include "RasterImage.h" +// On Windows systems, RasterImage.h brings in 'windows.h', which defines INT32. +// But the jpeg decoder has its own definition of INT32. To avoid build issues, +// we need to undefine the version from 'windows.h'. +#undef INT32 + +#include "Decoder.h" + +#include "nsIInputStream.h" +#include "nsIPipe.h" +#include "qcms.h" + +extern "C" { +#include "jpeglib.h" +} + +#include <setjmp.h> + +namespace mozilla { +namespace image { + +typedef struct { + struct jpeg_error_mgr pub; // "public" fields for IJG library + jmp_buf setjmp_buffer; // For handling catastropic errors +} decoder_error_mgr; + +typedef enum { + JPEG_HEADER, // Reading JFIF headers + JPEG_START_DECOMPRESS, + JPEG_DECOMPRESS_PROGRESSIVE, // Output progressive pixels + JPEG_DECOMPRESS_SEQUENTIAL, // Output sequential pixels + JPEG_DONE, + JPEG_SINK_NON_JPEG_TRAILER, // Some image files have a + // non-JPEG trailer + JPEG_ERROR +} jstate; + +class RasterImage; +struct Orientation; + +class nsJPEGDecoder : public Decoder +{ +public: + virtual ~nsJPEGDecoder(); + + virtual void SetSampleSize(int aSampleSize) override + { + mSampleSize = aSampleSize; + } + + void NotifyDone(); + +protected: + nsresult InitInternal() override; + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; + + Maybe<Telemetry::ID> SpeedHistogram() const override; + +protected: + Orientation ReadOrientationFromEXIF(); + void OutputScanlines(bool* suspend); + +private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + nsJPEGDecoder(RasterImage* aImage, Decoder::DecodeStyle aDecodeStyle); + + enum class State + { + JPEG_DATA, + FINISHED_JPEG_DATA + }; + + LexerTransition<State> ReadJPEGData(const char* aData, size_t aLength); + LexerTransition<State> FinishedJPEGData(); + + StreamingLexer<State> mLexer; + +public: + struct jpeg_decompress_struct mInfo; + struct jpeg_source_mgr mSourceMgr; + decoder_error_mgr mErr; + jstate mState; + + uint32_t mBytesToSkip; + + const JOCTET* mSegment; // The current segment we are decoding from + uint32_t mSegmentLen; // amount of data in mSegment + + JOCTET* mBackBuffer; + uint32_t mBackBufferLen; // Offset of end of active backtrack data + uint32_t mBackBufferSize; // size in bytes what mBackBuffer was created with + uint32_t mBackBufferUnreadLen; // amount of data currently in mBackBuffer + + JOCTET * mProfile; + uint32_t mProfileLength; + + qcms_profile* mInProfile; + qcms_transform* mTransform; + + bool mReading; + + const Decoder::DecodeStyle mDecodeStyle; + + uint32_t mCMSMode; + + int mSampleSize; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsJPEGDecoder_h diff --git a/image/decoders/nsPNGDecoder.cpp b/image/decoders/nsPNGDecoder.cpp new file mode 100644 index 000000000..0f385b339 --- /dev/null +++ b/image/decoders/nsPNGDecoder.cpp @@ -0,0 +1,1113 @@ +/* -*- 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" // Must appear first +#include "nsPNGDecoder.h" + +#include <algorithm> +#include <cstdint> + +#include "gfxColor.h" +#include "gfxPlatform.h" +#include "imgFrame.h" +#include "nsColor.h" +#include "nsIInputStream.h" +#include "nsMemory.h" +#include "nsRect.h" +#include "nspr.h" +#include "png.h" +#include "RasterImage.h" +#include "SurfacePipeFactory.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla::gfx; + +using std::min; + +namespace mozilla { +namespace image { + +static LazyLogModule sPNGLog("PNGDecoder"); +static LazyLogModule sPNGDecoderAccountingLog("PNGDecoderAccounting"); + +// limit image dimensions (bug #251381, #591822, #967656, and #1283961) +#ifndef MOZ_PNG_MAX_WIDTH +# define MOZ_PNG_MAX_WIDTH 0x7fffffff // Unlimited +#endif +#ifndef MOZ_PNG_MAX_HEIGHT +# define MOZ_PNG_MAX_HEIGHT 0x7fffffff // Unlimited +#endif + +nsPNGDecoder::AnimFrameInfo::AnimFrameInfo() + : mDispose(DisposalMethod::KEEP) + , mBlend(BlendMethod::OVER) + , mTimeout(0) +{ } + +#ifdef PNG_APNG_SUPPORTED + +int32_t GetNextFrameDelay(png_structp aPNG, png_infop aInfo) +{ + // Delay, in seconds, is delayNum / delayDen. + png_uint_16 delayNum = png_get_next_frame_delay_num(aPNG, aInfo); + png_uint_16 delayDen = png_get_next_frame_delay_den(aPNG, aInfo); + + if (delayNum == 0) { + return 0; // SetFrameTimeout() will set to a minimum. + } + + if (delayDen == 0) { + delayDen = 100; // So says the APNG spec. + } + + // Need to cast delay_num to float to have a proper division and + // the result to int to avoid a compiler warning. + return static_cast<int32_t>(static_cast<double>(delayNum) * 1000 / delayDen); +} + +nsPNGDecoder::AnimFrameInfo::AnimFrameInfo(png_structp aPNG, png_infop aInfo) + : mDispose(DisposalMethod::KEEP) + , mBlend(BlendMethod::OVER) + , mTimeout(0) +{ + png_byte dispose_op = png_get_next_frame_dispose_op(aPNG, aInfo); + png_byte blend_op = png_get_next_frame_blend_op(aPNG, aInfo); + + if (dispose_op == PNG_DISPOSE_OP_PREVIOUS) { + mDispose = DisposalMethod::RESTORE_PREVIOUS; + } else if (dispose_op == PNG_DISPOSE_OP_BACKGROUND) { + mDispose = DisposalMethod::CLEAR; + } else { + mDispose = DisposalMethod::KEEP; + } + + if (blend_op == PNG_BLEND_OP_SOURCE) { + mBlend = BlendMethod::SOURCE; + } else { + mBlend = BlendMethod::OVER; + } + + mTimeout = GetNextFrameDelay(aPNG, aInfo); +} +#endif + +// First 8 bytes of a PNG file +const uint8_t +nsPNGDecoder::pngSignatureBytes[] = { 137, 80, 78, 71, 13, 10, 26, 10 }; + +nsPNGDecoder::nsPNGDecoder(RasterImage* aImage) + : Decoder(aImage) + , mLexer(Transition::ToUnbuffered(State::FINISHED_PNG_DATA, + State::PNG_DATA, + SIZE_MAX), + Transition::TerminateSuccess()) + , mNextTransition(Transition::ContinueUnbuffered(State::PNG_DATA)) + , mLastChunkLength(0) + , mPNG(nullptr) + , mInfo(nullptr) + , mCMSLine(nullptr) + , interlacebuf(nullptr) + , mInProfile(nullptr) + , mTransform(nullptr) + , format(gfx::SurfaceFormat::UNKNOWN) + , mCMSMode(0) + , mChannels(0) + , mPass(0) + , mFrameIsHidden(false) + , mDisablePremultipliedAlpha(false) + , mNumFrames(0) +{ } + +nsPNGDecoder::~nsPNGDecoder() +{ + if (mPNG) { + png_destroy_read_struct(&mPNG, mInfo ? &mInfo : nullptr, nullptr); + } + if (mCMSLine) { + free(mCMSLine); + } + if (interlacebuf) { + free(interlacebuf); + } + if (mInProfile) { + qcms_profile_release(mInProfile); + + // mTransform belongs to us only if mInProfile is non-null + if (mTransform) { + qcms_transform_release(mTransform); + } + } +} + +nsPNGDecoder::TransparencyType +nsPNGDecoder::GetTransparencyType(SurfaceFormat aFormat, + const IntRect& aFrameRect) +{ + // Check if the image has a transparent color in its palette. + if (aFormat == SurfaceFormat::B8G8R8A8) { + return TransparencyType::eAlpha; + } + if (!aFrameRect.IsEqualEdges(FullFrame())) { + MOZ_ASSERT(HasAnimation()); + return TransparencyType::eFrameRect; + } + + return TransparencyType::eNone; +} + +void +nsPNGDecoder::PostHasTransparencyIfNeeded(TransparencyType aTransparencyType) +{ + switch (aTransparencyType) { + case TransparencyType::eNone: + return; + + case TransparencyType::eAlpha: + PostHasTransparency(); + return; + + case TransparencyType::eFrameRect: + // If the first frame of animated image doesn't draw into the whole image, + // then record that it is transparent. For subsequent frames, this doesn't + // affect transparency, because they're composited on top of all previous + // frames. + if (mNumFrames == 0) { + PostHasTransparency(); + } + return; + } +} + +// CreateFrame() is used for both simple and animated images. +nsresult +nsPNGDecoder::CreateFrame(const FrameInfo& aFrameInfo) +{ + MOZ_ASSERT(HasSize()); + MOZ_ASSERT(!IsMetadataDecode()); + + // Check if we have transparency, and send notifications if needed. + auto transparency = GetTransparencyType(aFrameInfo.mFormat, + aFrameInfo.mFrameRect); + PostHasTransparencyIfNeeded(transparency); + SurfaceFormat format = transparency == TransparencyType::eNone + ? SurfaceFormat::B8G8R8X8 + : SurfaceFormat::B8G8R8A8; + + // Make sure there's no animation or padding if we're downscaling. + MOZ_ASSERT_IF(Size() != OutputSize(), mNumFrames == 0); + MOZ_ASSERT_IF(Size() != OutputSize(), !GetImageMetadata().HasAnimation()); + MOZ_ASSERT_IF(Size() != OutputSize(), + transparency != TransparencyType::eFrameRect); + + // If this image is interlaced, we can display better quality intermediate + // results to the user by post processing them with ADAM7InterpolatingFilter. + SurfacePipeFlags pipeFlags = aFrameInfo.mIsInterlaced + ? SurfacePipeFlags::ADAM7_INTERPOLATE + : SurfacePipeFlags(); + + if (mNumFrames == 0) { + // The first frame may be displayed progressively. + pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY; + } + + Maybe<SurfacePipe> pipe = + SurfacePipeFactory::CreateSurfacePipe(this, mNumFrames, Size(), + OutputSize(), aFrameInfo.mFrameRect, + format, pipeFlags); + + if (!pipe) { + mPipe = SurfacePipe(); + return NS_ERROR_FAILURE; + } + + mPipe = Move(*pipe); + + mFrameRect = aFrameInfo.mFrameRect; + mPass = 0; + + MOZ_LOG(sPNGDecoderAccountingLog, LogLevel::Debug, + ("PNGDecoderAccounting: nsPNGDecoder::CreateFrame -- created " + "image frame with %dx%d pixels for decoder %p", + mFrameRect.width, mFrameRect.height, this)); + +#ifdef PNG_APNG_SUPPORTED + if (png_get_valid(mPNG, mInfo, PNG_INFO_acTL)) { + mAnimInfo = AnimFrameInfo(mPNG, mInfo); + + if (mAnimInfo.mDispose == DisposalMethod::CLEAR) { + // We may have to display the background under this image during + // animation playback, so we regard it as transparent. + PostHasTransparency(); + } + } +#endif + + return NS_OK; +} + +// set timeout and frame disposal method for the current frame +void +nsPNGDecoder::EndImageFrame() +{ + if (mFrameIsHidden) { + return; + } + + mNumFrames++; + + Opacity opacity = Opacity::SOME_TRANSPARENCY; + if (format == gfx::SurfaceFormat::B8G8R8X8) { + opacity = Opacity::FULLY_OPAQUE; + } + + PostFrameStop(opacity, mAnimInfo.mDispose, + FrameTimeout::FromRawMilliseconds(mAnimInfo.mTimeout), + mAnimInfo.mBlend, Some(mFrameRect)); +} + +nsresult +nsPNGDecoder::InitInternal() +{ + mCMSMode = gfxPlatform::GetCMSMode(); + if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) { + mCMSMode = eCMSMode_Off; + } + mDisablePremultipliedAlpha = + bool(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA); + +#ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED + static png_byte color_chunks[]= + { 99, 72, 82, 77, '\0', // cHRM + 105, 67, 67, 80, '\0'}; // iCCP + static png_byte unused_chunks[]= + { 98, 75, 71, 68, '\0', // bKGD + 104, 73, 83, 84, '\0', // hIST + 105, 84, 88, 116, '\0', // iTXt + 111, 70, 70, 115, '\0', // oFFs + 112, 67, 65, 76, '\0', // pCAL + 115, 67, 65, 76, '\0', // sCAL + 112, 72, 89, 115, '\0', // pHYs + 115, 66, 73, 84, '\0', // sBIT + 115, 80, 76, 84, '\0', // sPLT + 116, 69, 88, 116, '\0', // tEXt + 116, 73, 77, 69, '\0', // tIME + 122, 84, 88, 116, '\0'}; // zTXt +#endif + + // Initialize the container's source image header + // Always decode to 24 bit pixdepth + + mPNG = png_create_read_struct(PNG_LIBPNG_VER_STRING, + nullptr, nsPNGDecoder::error_callback, + nsPNGDecoder::warning_callback); + if (!mPNG) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mInfo = png_create_info_struct(mPNG); + if (!mInfo) { + png_destroy_read_struct(&mPNG, nullptr, nullptr); + return NS_ERROR_OUT_OF_MEMORY; + } + +#ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED + // Ignore unused chunks + if (mCMSMode == eCMSMode_Off || IsMetadataDecode()) { + png_set_keep_unknown_chunks(mPNG, 1, color_chunks, 2); + } + + png_set_keep_unknown_chunks(mPNG, 1, unused_chunks, + (int)sizeof(unused_chunks)/5); +#endif + +#ifdef PNG_SET_USER_LIMITS_SUPPORTED + png_set_user_limits(mPNG, MOZ_PNG_MAX_WIDTH, MOZ_PNG_MAX_HEIGHT); + if (mCMSMode != eCMSMode_Off) { + png_set_chunk_malloc_max(mPNG, 4000000L); + } +#endif + +#ifdef PNG_READ_CHECK_FOR_INVALID_INDEX_SUPPORTED + // Disallow palette-index checking, for speed; we would ignore the warning + // anyhow. This feature was added at libpng version 1.5.10 and is disabled + // in the embedded libpng but enabled by default in the system libpng. This + // call also disables it in the system libpng, for decoding speed. + // Bug #745202. + png_set_check_for_invalid_index(mPNG, 0); +#endif + +#ifdef PNG_SET_OPTION_SUPPORTED +#if defined(PNG_sRGB_PROFILE_CHECKS) && PNG_sRGB_PROFILE_CHECKS >= 0 + // Skip checking of sRGB ICC profiles + png_set_option(mPNG, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); +#endif + +#ifdef PNG_MAXIMUM_INFLATE_WINDOW + // Force a larger zlib inflate window as some images in the wild have + // incorrectly set metadata (specifically CMF bits) which prevent us from + // decoding them otherwise. + png_set_option(mPNG, PNG_MAXIMUM_INFLATE_WINDOW, PNG_OPTION_ON); +#endif +#endif + + // use this as libpng "progressive pointer" (retrieve in callbacks) + png_set_progressive_read_fn(mPNG, static_cast<png_voidp>(this), + nsPNGDecoder::info_callback, + nsPNGDecoder::row_callback, + nsPNGDecoder::end_callback); + + return NS_OK; +} + +LexerResult +nsPNGDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) +{ + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::PNG_DATA: + return ReadPNGData(aData, aLength); + case State::FINISHED_PNG_DATA: + return FinishedPNGData(); + } + MOZ_CRASH("Unknown State"); + }); +} + +LexerTransition<nsPNGDecoder::State> +nsPNGDecoder::ReadPNGData(const char* aData, size_t aLength) +{ + // If we were waiting until after returning from a yield to call + // CreateFrame(), call it now. + if (mNextFrameInfo) { + if (NS_FAILED(CreateFrame(*mNextFrameInfo))) { + return Transition::TerminateFailure(); + } + + MOZ_ASSERT(mImageData, "Should have a buffer now"); + mNextFrameInfo = Nothing(); + } + + // libpng uses setjmp/longjmp for error handling. + if (setjmp(png_jmpbuf(mPNG))) { + return Transition::TerminateFailure(); + } + + // Pass the data off to libpng. + mLastChunkLength = aLength; + mNextTransition = Transition::ContinueUnbuffered(State::PNG_DATA); + png_process_data(mPNG, mInfo, + reinterpret_cast<unsigned char*>(const_cast<char*>((aData))), + aLength); + + // Make sure that we've reached a terminal state if decoding is done. + MOZ_ASSERT_IF(GetDecodeDone(), mNextTransition.NextStateIsTerminal()); + MOZ_ASSERT_IF(HasError(), mNextTransition.NextStateIsTerminal()); + + // Continue with whatever transition the callback code requested. We + // initialized this to Transition::ContinueUnbuffered(State::PNG_DATA) above, + // so by default we just continue the unbuffered read. + return mNextTransition; +} + +LexerTransition<nsPNGDecoder::State> +nsPNGDecoder::FinishedPNGData() +{ + // Since we set up an unbuffered read for SIZE_MAX bytes, if we actually read + // all that data something is really wrong. + MOZ_ASSERT_UNREACHABLE("Read the entire address space?"); + return Transition::TerminateFailure(); +} + +// Sets up gamma pre-correction in libpng before our callback gets called. +// We need to do this if we don't end up with a CMS profile. +static void +PNGDoGammaCorrection(png_structp png_ptr, png_infop info_ptr) +{ + double aGamma; + + if (png_get_gAMA(png_ptr, info_ptr, &aGamma)) { + if ((aGamma <= 0.0) || (aGamma > 21474.83)) { + aGamma = 0.45455; + png_set_gAMA(png_ptr, info_ptr, aGamma); + } + png_set_gamma(png_ptr, 2.2, aGamma); + } else { + png_set_gamma(png_ptr, 2.2, 0.45455); + } +} + +// Adapted from http://www.littlecms.com/pngchrm.c example code +static qcms_profile* +PNGGetColorProfile(png_structp png_ptr, png_infop info_ptr, + int color_type, qcms_data_type* inType, uint32_t* intent) +{ + qcms_profile* profile = nullptr; + *intent = QCMS_INTENT_PERCEPTUAL; // Our default + + // First try to see if iCCP chunk is present + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP)) { + png_uint_32 profileLen; + png_bytep profileData; + png_charp profileName; + int compression; + + png_get_iCCP(png_ptr, info_ptr, &profileName, &compression, + &profileData, &profileLen); + + profile = qcms_profile_from_memory((char*)profileData, profileLen); + if (profile) { + uint32_t profileSpace = qcms_profile_get_color_space(profile); + + bool mismatch = false; + if (color_type & PNG_COLOR_MASK_COLOR) { + if (profileSpace != icSigRgbData) { + mismatch = true; + } + } else { + if (profileSpace == icSigRgbData) { + png_set_gray_to_rgb(png_ptr); + } else if (profileSpace != icSigGrayData) { + mismatch = true; + } + } + + if (mismatch) { + qcms_profile_release(profile); + profile = nullptr; + } else { + *intent = qcms_profile_get_rendering_intent(profile); + } + } + } + + // Check sRGB chunk + if (!profile && png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) { + profile = qcms_profile_sRGB(); + + if (profile) { + int fileIntent; + png_set_gray_to_rgb(png_ptr); + png_get_sRGB(png_ptr, info_ptr, &fileIntent); + uint32_t map[] = { QCMS_INTENT_PERCEPTUAL, + QCMS_INTENT_RELATIVE_COLORIMETRIC, + QCMS_INTENT_SATURATION, + QCMS_INTENT_ABSOLUTE_COLORIMETRIC }; + *intent = map[fileIntent]; + } + } + + // Check gAMA/cHRM chunks + if (!profile && + png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA) && + png_get_valid(png_ptr, info_ptr, PNG_INFO_cHRM)) { + qcms_CIE_xyYTRIPLE primaries; + qcms_CIE_xyY whitePoint; + + png_get_cHRM(png_ptr, info_ptr, + &whitePoint.x, &whitePoint.y, + &primaries.red.x, &primaries.red.y, + &primaries.green.x, &primaries.green.y, + &primaries.blue.x, &primaries.blue.y); + whitePoint.Y = + primaries.red.Y = primaries.green.Y = primaries.blue.Y = 1.0; + + double gammaOfFile; + + png_get_gAMA(png_ptr, info_ptr, &gammaOfFile); + + profile = qcms_profile_create_rgb_with_gamma(whitePoint, primaries, + 1.0/gammaOfFile); + + if (profile) { + png_set_gray_to_rgb(png_ptr); + } + } + + if (profile) { + uint32_t profileSpace = qcms_profile_get_color_space(profile); + if (profileSpace == icSigGrayData) { + if (color_type & PNG_COLOR_MASK_ALPHA) { + *inType = QCMS_DATA_GRAYA_8; + } else { + *inType = QCMS_DATA_GRAY_8; + } + } else { + if (color_type & PNG_COLOR_MASK_ALPHA || + png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + *inType = QCMS_DATA_RGBA_8; + } else { + *inType = QCMS_DATA_RGB_8; + } + } + } + + return profile; +} + +void +nsPNGDecoder::info_callback(png_structp png_ptr, png_infop info_ptr) +{ + png_uint_32 width, height; + int bit_depth, color_type, interlace_type, compression_type, filter_type; + unsigned int channels; + + png_bytep trans = nullptr; + int num_trans = 0; + + nsPNGDecoder* decoder = + static_cast<nsPNGDecoder*>(png_get_progressive_ptr(png_ptr)); + + // Always decode to 24-bit RGB or 32-bit RGBA + png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, + &interlace_type, &compression_type, &filter_type); + + const IntRect frameRect(0, 0, width, height); + + // Post our size to the superclass + decoder->PostSize(frameRect.width, frameRect.height); + if (decoder->HasError()) { + // Setting the size led to an error. + png_error(decoder->mPNG, "Sizing error"); + } + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + png_set_expand(png_ptr); + } + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { + png_set_expand(png_ptr); + } + + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + png_color_16p trans_values; + png_get_tRNS(png_ptr, info_ptr, &trans, &num_trans, &trans_values); + // libpng doesn't reject a tRNS chunk with out-of-range samples + // so we check it here to avoid setting up a useless opacity + // channel or producing unexpected transparent pixels (bug #428045) + if (bit_depth < 16) { + png_uint_16 sample_max = (1 << bit_depth) - 1; + if ((color_type == PNG_COLOR_TYPE_GRAY && + trans_values->gray > sample_max) || + (color_type == PNG_COLOR_TYPE_RGB && + (trans_values->red > sample_max || + trans_values->green > sample_max || + trans_values->blue > sample_max))) { + // clear the tRNS valid flag and release tRNS memory + png_free_data(png_ptr, info_ptr, PNG_FREE_TRNS, 0); + num_trans = 0; + } + } + if (num_trans != 0) { + png_set_expand(png_ptr); + } + } + + if (bit_depth == 16) { + png_set_scale_16(png_ptr); + } + + qcms_data_type inType = QCMS_DATA_RGBA_8; + uint32_t intent = -1; + uint32_t pIntent; + if (decoder->mCMSMode != eCMSMode_Off) { + intent = gfxPlatform::GetRenderingIntent(); + decoder->mInProfile = PNGGetColorProfile(png_ptr, info_ptr, + color_type, &inType, &pIntent); + // If we're not mandating an intent, use the one from the image. + if (intent == uint32_t(-1)) { + intent = pIntent; + } + } + if (decoder->mInProfile && gfxPlatform::GetCMSOutputProfile()) { + qcms_data_type outType; + + if (color_type & PNG_COLOR_MASK_ALPHA || num_trans) { + outType = QCMS_DATA_RGBA_8; + } else { + outType = QCMS_DATA_RGB_8; + } + + decoder->mTransform = qcms_transform_create(decoder->mInProfile, + inType, + gfxPlatform::GetCMSOutputProfile(), + outType, + (qcms_intent)intent); + } else { + png_set_gray_to_rgb(png_ptr); + + // only do gamma correction if CMS isn't entirely disabled + if (decoder->mCMSMode != eCMSMode_Off) { + PNGDoGammaCorrection(png_ptr, info_ptr); + } + + if (decoder->mCMSMode == eCMSMode_All) { + if (color_type & PNG_COLOR_MASK_ALPHA || num_trans) { + decoder->mTransform = gfxPlatform::GetCMSRGBATransform(); + } else { + decoder->mTransform = gfxPlatform::GetCMSRGBTransform(); + } + } + } + + // Let libpng expand interlaced images. + const bool isInterlaced = interlace_type == PNG_INTERLACE_ADAM7; + if (isInterlaced) { + png_set_interlace_handling(png_ptr); + } + + // now all of those things we set above are used to update various struct + // members and whatnot, after which we can get channels, rowbytes, etc. + png_read_update_info(png_ptr, info_ptr); + decoder->mChannels = channels = png_get_channels(png_ptr, info_ptr); + + //---------------------------------------------------------------// + // copy PNG info into imagelib structs (formerly png_set_dims()) // + //---------------------------------------------------------------// + + if (channels == 1 || channels == 3) { + decoder->format = gfx::SurfaceFormat::B8G8R8X8; + } else if (channels == 2 || channels == 4) { + decoder->format = gfx::SurfaceFormat::B8G8R8A8; + } else { + png_error(decoder->mPNG, "Invalid number of channels"); + } + +#ifdef PNG_APNG_SUPPORTED + bool isAnimated = png_get_valid(png_ptr, info_ptr, PNG_INFO_acTL); + if (isAnimated) { + int32_t rawTimeout = GetNextFrameDelay(png_ptr, info_ptr); + decoder->PostIsAnimated(FrameTimeout::FromRawMilliseconds(rawTimeout)); + + if (decoder->Size() != decoder->OutputSize() && + !decoder->IsFirstFrameDecode()) { + MOZ_ASSERT_UNREACHABLE("Doing downscale-during-decode " + "for an animated image?"); + png_error(decoder->mPNG, "Invalid downscale attempt"); // Abort decode. + } + } +#endif + + if (decoder->IsMetadataDecode()) { + // If we are animated then the first frame rect is either: + // 1) the whole image if the IDAT chunk is part of the animation + // 2) the frame rect of the first fDAT chunk otherwise. + // If we are not animated then we want to make sure to call + // PostHasTransparency in the metadata decode if we need to. So it's + // okay to pass IntRect(0, 0, width, height) here for animated images; + // they will call with the proper first frame rect in the full decode. + auto transparency = decoder->GetTransparencyType(decoder->format, + frameRect); + decoder->PostHasTransparencyIfNeeded(transparency); + + // We have the metadata we're looking for, so stop here, before we allocate + // buffers below. + return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); + } + +#ifdef PNG_APNG_SUPPORTED + if (isAnimated) { + png_set_progressive_frame_fn(png_ptr, nsPNGDecoder::frame_info_callback, + nullptr); + } + + if (png_get_first_frame_is_hidden(png_ptr, info_ptr)) { + decoder->mFrameIsHidden = true; + } else { +#endif + nsresult rv = decoder->CreateFrame(FrameInfo{ decoder->format, + frameRect, + isInterlaced }); + if (NS_FAILED(rv)) { + png_error(decoder->mPNG, "CreateFrame failed"); + } + MOZ_ASSERT(decoder->mImageData, "Should have a buffer now"); +#ifdef PNG_APNG_SUPPORTED + } +#endif + + if (decoder->mTransform && (channels <= 2 || isInterlaced)) { + uint32_t bpp[] = { 0, 3, 4, 3, 4 }; + decoder->mCMSLine = + static_cast<uint8_t*>(malloc(bpp[channels] * frameRect.width)); + if (!decoder->mCMSLine) { + png_error(decoder->mPNG, "malloc of mCMSLine failed"); + } + } + + if (interlace_type == PNG_INTERLACE_ADAM7) { + if (frameRect.height < INT32_MAX / (frameRect.width * int32_t(channels))) { + const size_t bufferSize = channels * frameRect.width * frameRect.height; + decoder->interlacebuf = static_cast<uint8_t*>(malloc(bufferSize)); + } + if (!decoder->interlacebuf) { + png_error(decoder->mPNG, "malloc of interlacebuf failed"); + } + } +} + +void +nsPNGDecoder::PostInvalidationIfNeeded() +{ + Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect(); + if (!invalidRect) { + return; + } + + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); +} + +static NextPixel<uint32_t> +PackRGBPixelAndAdvance(uint8_t*& aRawPixelInOut) +{ + const uint32_t pixel = + gfxPackedPixel(0xFF, aRawPixelInOut[0], aRawPixelInOut[1], + aRawPixelInOut[2]); + aRawPixelInOut += 3; + return AsVariant(pixel); +} + +static NextPixel<uint32_t> +PackRGBAPixelAndAdvance(uint8_t*& aRawPixelInOut) +{ + const uint32_t pixel = + gfxPackedPixel(aRawPixelInOut[3], aRawPixelInOut[0], + aRawPixelInOut[1], aRawPixelInOut[2]); + aRawPixelInOut += 4; + return AsVariant(pixel); +} + +static NextPixel<uint32_t> +PackUnpremultipliedRGBAPixelAndAdvance(uint8_t*& aRawPixelInOut) +{ + const uint32_t pixel = + gfxPackedPixelNoPreMultiply(aRawPixelInOut[3], aRawPixelInOut[0], + aRawPixelInOut[1], aRawPixelInOut[2]); + aRawPixelInOut += 4; + return AsVariant(pixel); +} + +void +nsPNGDecoder::row_callback(png_structp png_ptr, png_bytep new_row, + png_uint_32 row_num, int pass) +{ + /* libpng comments: + * + * 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 + * nullptr. 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. + * + * For the non-nullptr 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 nullptr 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. + */ + nsPNGDecoder* decoder = + static_cast<nsPNGDecoder*>(png_get_progressive_ptr(png_ptr)); + + if (decoder->mFrameIsHidden) { + return; // Skip this frame. + } + + MOZ_ASSERT_IF(decoder->IsFirstFrameDecode(), decoder->mNumFrames == 0); + + while (pass > decoder->mPass) { + // Advance to the next pass. We may have to do this multiple times because + // libpng will skip passes if the image is so small that no pixels have + // changed on a given pass, but ADAM7InterpolatingFilter needs to be reset + // once for every pass to perform interpolation properly. + decoder->mPipe.ResetToFirstRow(); + decoder->mPass++; + } + + const png_uint_32 height = + static_cast<png_uint_32>(decoder->mFrameRect.height); + + if (row_num >= height) { + // Bail if we receive extra rows. This is especially important because if we + // didn't, we might overflow the deinterlacing buffer. + MOZ_ASSERT_UNREACHABLE("libpng producing extra rows?"); + return; + } + + // Note that |new_row| may be null here, indicating that this is an interlaced + // image and |row_callback| is being called for a row that hasn't changed. + MOZ_ASSERT_IF(!new_row, decoder->interlacebuf); + uint8_t* rowToWrite = new_row; + + if (decoder->interlacebuf) { + uint32_t width = uint32_t(decoder->mFrameRect.width); + + // We'll output the deinterlaced version of the row. + rowToWrite = decoder->interlacebuf + (row_num * decoder->mChannels * width); + + // Update the deinterlaced version of this row with the new data. + png_progressive_combine_row(png_ptr, rowToWrite, new_row); + } + + decoder->WriteRow(rowToWrite); +} + +void +nsPNGDecoder::WriteRow(uint8_t* aRow) +{ + MOZ_ASSERT(aRow); + + uint8_t* rowToWrite = aRow; + uint32_t width = uint32_t(mFrameRect.width); + + // Apply color management to the row, if necessary, before writing it out. + if (mTransform) { + if (mCMSLine) { + qcms_transform_data(mTransform, rowToWrite, mCMSLine, width); + + // Copy alpha over. + if (mChannels == 2 || mChannels == 4) { + for (uint32_t i = 0; i < width; ++i) { + mCMSLine[4 * i + 3] = rowToWrite[mChannels * i + mChannels - 1]; + } + } + + rowToWrite = mCMSLine; + } else { + qcms_transform_data(mTransform, rowToWrite, rowToWrite, width); + } + } + + // Write this row to the SurfacePipe. + DebugOnly<WriteState> result = WriteState::FAILURE; + switch (format) { + case SurfaceFormat::B8G8R8X8: + result = mPipe.WritePixelsToRow<uint32_t>([&]{ + return PackRGBPixelAndAdvance(rowToWrite); + }); + break; + + case SurfaceFormat::B8G8R8A8: + if (mDisablePremultipliedAlpha) { + result = mPipe.WritePixelsToRow<uint32_t>([&]{ + return PackUnpremultipliedRGBAPixelAndAdvance(rowToWrite); + }); + } else { + result = mPipe.WritePixelsToRow<uint32_t>([&]{ + return PackRGBAPixelAndAdvance(rowToWrite); + }); + } + break; + + default: + png_error(mPNG, "Invalid SurfaceFormat"); + } + + MOZ_ASSERT(WriteState(result) != WriteState::FAILURE); + + PostInvalidationIfNeeded(); +} + +void +nsPNGDecoder::DoTerminate(png_structp aPNGStruct, TerminalState aState) +{ + // Stop processing data. Note that we intentionally ignore the return value of + // png_process_data_pause(), which tells us how many bytes of the data that + // was passed to png_process_data() have not been consumed yet, because now + // that we've reached a terminal state, we won't do any more decoding or call + // back into libpng anymore. + png_process_data_pause(aPNGStruct, /* save = */ false); + + mNextTransition = aState == TerminalState::SUCCESS + ? Transition::TerminateSuccess() + : Transition::TerminateFailure(); +} + +void +nsPNGDecoder::DoYield(png_structp aPNGStruct) +{ + // Pause data processing. png_process_data_pause() returns how many bytes of + // the data that was passed to png_process_data() have not been consumed yet. + // We use this information to tell StreamingLexer where to place us in the + // input stream when we come back from the yield. + png_size_t pendingBytes = png_process_data_pause(aPNGStruct, + /* save = */ false); + + MOZ_ASSERT(pendingBytes < mLastChunkLength); + size_t consumedBytes = mLastChunkLength - min(pendingBytes, mLastChunkLength); + + mNextTransition = + Transition::ContinueUnbufferedAfterYield(State::PNG_DATA, consumedBytes); +} + +#ifdef PNG_APNG_SUPPORTED +// got the header of a new frame that's coming +void +nsPNGDecoder::frame_info_callback(png_structp png_ptr, png_uint_32 frame_num) +{ + nsPNGDecoder* decoder = + static_cast<nsPNGDecoder*>(png_get_progressive_ptr(png_ptr)); + + // old frame is done + decoder->EndImageFrame(); + + const bool previousFrameWasHidden = decoder->mFrameIsHidden; + + if (!previousFrameWasHidden && decoder->IsFirstFrameDecode()) { + // We're about to get a second non-hidden frame, but we only want the first. + // Stop decoding now. (And avoid allocating the unnecessary buffers below.) + decoder->PostDecodeDone(); + return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); + } + + // Only the first frame can be hidden, so unhide unconditionally here. + decoder->mFrameIsHidden = false; + + // Save the information necessary to create the frame; we'll actually create + // it when we return from the yield. + const IntRect frameRect(png_get_next_frame_x_offset(png_ptr, decoder->mInfo), + png_get_next_frame_y_offset(png_ptr, decoder->mInfo), + png_get_next_frame_width(png_ptr, decoder->mInfo), + png_get_next_frame_height(png_ptr, decoder->mInfo)); + const bool isInterlaced = bool(decoder->interlacebuf); + +#ifndef MOZ_EMBEDDED_LIBPNG + // if using system library, check frame_width and height against 0 + if (frameRect.width == 0) { + png_error(png_ptr, "Frame width must not be 0"); + } + if (frameRect.height == 0) { + png_error(png_ptr, "Frame height must not be 0"); + } +#endif + + const FrameInfo info { decoder->format, frameRect, isInterlaced }; + + // If the previous frame was hidden, skip the yield (which will mislead the + // caller, who will think the previous frame was real) and just allocate the + // new frame here. + if (previousFrameWasHidden) { + if (NS_FAILED(decoder->CreateFrame(info))) { + return decoder->DoTerminate(png_ptr, TerminalState::FAILURE); + } + + MOZ_ASSERT(decoder->mImageData, "Should have a buffer now"); + return; // No yield, so we'll just keep decoding. + } + + // Yield to the caller to notify them that the previous frame is now complete. + decoder->mNextFrameInfo = Some(info); + return decoder->DoYield(png_ptr); +} +#endif + +void +nsPNGDecoder::end_callback(png_structp png_ptr, png_infop info_ptr) +{ + /* libpng comments: + * + * this function is called when the whole image has been read, + * including any chunks after the image (up to and including + * the IEND). You will usually have the same info chunk as you + * had in the header, although some data may have been added + * to the comments and time fields. + * + * Most people won't do much here, perhaps setting a flag that + * marks the image as finished. + */ + + nsPNGDecoder* decoder = + static_cast<nsPNGDecoder*>(png_get_progressive_ptr(png_ptr)); + + // We shouldn't get here if we've hit an error + MOZ_ASSERT(!decoder->HasError(), "Finishing up PNG but hit error!"); + + int32_t loop_count = 0; +#ifdef PNG_APNG_SUPPORTED + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_acTL)) { + int32_t num_plays = png_get_num_plays(png_ptr, info_ptr); + loop_count = num_plays - 1; + } +#endif + + // Send final notifications. + decoder->EndImageFrame(); + decoder->PostDecodeDone(loop_count); + return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); +} + + +void +nsPNGDecoder::error_callback(png_structp png_ptr, png_const_charp error_msg) +{ + MOZ_LOG(sPNGLog, LogLevel::Error, ("libpng error: %s\n", error_msg)); + png_longjmp(png_ptr, 1); +} + + +void +nsPNGDecoder::warning_callback(png_structp png_ptr, png_const_charp warning_msg) +{ + MOZ_LOG(sPNGLog, LogLevel::Warning, ("libpng warning: %s\n", warning_msg)); +} + +Maybe<Telemetry::ID> +nsPNGDecoder::SpeedHistogram() const +{ + return Some(Telemetry::IMAGE_DECODE_SPEED_PNG); +} + +bool +nsPNGDecoder::IsValidICO() const +{ + // Only 32-bit RGBA PNGs are valid ICO resources; see here: + // http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx + + // If there are errors in the call to png_get_IHDR, the error_callback in + // nsPNGDecoder.cpp is called. In this error callback we do a longjmp, so + // we need to save the jump buffer here. Otherwise we'll end up without a + // proper callstack. + if (setjmp(png_jmpbuf(mPNG))) { + // We got here from a longjmp call indirectly from png_get_IHDR + return false; + } + + png_uint_32 + png_width, // Unused + png_height; // Unused + + int png_bit_depth, + png_color_type; + + if (png_get_IHDR(mPNG, mInfo, &png_width, &png_height, &png_bit_depth, + &png_color_type, nullptr, nullptr, nullptr)) { + + return ((png_color_type == PNG_COLOR_TYPE_RGB_ALPHA || + png_color_type == PNG_COLOR_TYPE_RGB) && + png_bit_depth == 8); + } else { + return false; + } +} + +} // namespace image +} // namespace mozilla diff --git a/image/decoders/nsPNGDecoder.h b/image/decoders/nsPNGDecoder.h new file mode 100644 index 000000000..4b5c50ac5 --- /dev/null +++ b/image/decoders/nsPNGDecoder.h @@ -0,0 +1,158 @@ +/* -*- 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_decoders_nsPNGDecoder_h +#define mozilla_image_decoders_nsPNGDecoder_h + +#include "Decoder.h" +#include "png.h" +#include "qcms.h" +#include "StreamingLexer.h" +#include "SurfacePipe.h" + +namespace mozilla { +namespace image { +class RasterImage; + +class nsPNGDecoder : public Decoder +{ +public: + virtual ~nsPNGDecoder(); + + /// @return true if this PNG is a valid ICO resource. + bool IsValidICO() const; + +protected: + nsresult InitInternal() override; + LexerResult DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + + Maybe<Telemetry::ID> SpeedHistogram() const override; + +private: + friend class DecoderFactory; + + // Decoders should only be instantiated via DecoderFactory. + explicit nsPNGDecoder(RasterImage* aImage); + + /// The information necessary to create a frame. + struct FrameInfo + { + gfx::SurfaceFormat mFormat; + gfx::IntRect mFrameRect; + bool mIsInterlaced; + }; + + nsresult CreateFrame(const FrameInfo& aFrameInfo); + void EndImageFrame(); + + enum class TransparencyType + { + eNone, + eAlpha, + eFrameRect + }; + + TransparencyType GetTransparencyType(gfx::SurfaceFormat aFormat, + const gfx::IntRect& aFrameRect); + void PostHasTransparencyIfNeeded(TransparencyType aTransparencyType); + + void PostInvalidationIfNeeded(); + + void WriteRow(uint8_t* aRow); + + // Convenience methods to make interacting with StreamingLexer from inside + // a libpng callback easier. + void DoTerminate(png_structp aPNGStruct, TerminalState aState); + void DoYield(png_structp aPNGStruct); + + enum class State + { + PNG_DATA, + FINISHED_PNG_DATA + }; + + LexerTransition<State> ReadPNGData(const char* aData, size_t aLength); + LexerTransition<State> FinishedPNGData(); + + StreamingLexer<State> mLexer; + + // The next lexer state transition. We need to store it here because we can't + // directly return arbitrary values from libpng callbacks. + LexerTransition<State> mNextTransition; + + // We yield to the caller every time we finish decoding a frame. When this + // happens, we need to allocate the next frame after returning from the yield. + // |mNextFrameInfo| is used to store the information needed to allocate the + // next frame. + Maybe<FrameInfo> mNextFrameInfo; + + // The length of the last chunk of data passed to ReadPNGData(). We use this + // to arrange to arrive back at the correct spot in the data after yielding. + size_t mLastChunkLength; + +public: + png_structp mPNG; + png_infop mInfo; + nsIntRect mFrameRect; + uint8_t* mCMSLine; + uint8_t* interlacebuf; + qcms_profile* mInProfile; + qcms_transform* mTransform; + + gfx::SurfaceFormat format; + + // whether CMS or premultiplied alpha are forced off + uint32_t mCMSMode; + + uint8_t mChannels; + uint8_t mPass; + bool mFrameIsHidden; + bool mDisablePremultipliedAlpha; + + struct AnimFrameInfo + { + AnimFrameInfo(); +#ifdef PNG_APNG_SUPPORTED + AnimFrameInfo(png_structp aPNG, png_infop aInfo); +#endif + + DisposalMethod mDispose; + BlendMethod mBlend; + int32_t mTimeout; + }; + + AnimFrameInfo mAnimInfo; + + SurfacePipe mPipe; /// The SurfacePipe used to write to the output surface. + + // The number of frames we've finished. + uint32_t mNumFrames; + + // libpng callbacks + // We put these in the class so that they can access protected members. + static void PNGAPI info_callback(png_structp png_ptr, png_infop info_ptr); + static void PNGAPI row_callback(png_structp png_ptr, png_bytep new_row, + png_uint_32 row_num, int pass); +#ifdef PNG_APNG_SUPPORTED + static void PNGAPI frame_info_callback(png_structp png_ptr, + png_uint_32 frame_num); +#endif + static void PNGAPI end_callback(png_structp png_ptr, png_infop info_ptr); + static void PNGAPI error_callback(png_structp png_ptr, + png_const_charp error_msg); + static void PNGAPI warning_callback(png_structp png_ptr, + png_const_charp warning_msg); + + // This is defined in the PNG spec as an invariant. We use it to + // do manual validation without libpng. + static const uint8_t pngSignatureBytes[]; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_decoders_nsPNGDecoder_h |