From b783c2ede6333e4b790329ee0fc8e395986778c8 Mon Sep 17 00:00:00 2001 From: "philringnalda%gmail.com" Date: Wed, 25 Apr 2007 05:56:54 +0000 Subject: [PATCH] =?UTF-8?q?Bug=20375741=20=EF=BF=BD=EF=BF=BD=EF=BF=BD=20Ad?= =?UTF-8?q?d=20support=20for=20APNG=20encoding,=20patch=20by=20Justin=20Do?= =?UTF-8?q?lske=20,=20r=3Dasmith15,=20sr=3Dpavlov?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../libpr0n/encoders/jpeg/nsJPEGEncoder.cpp | 25 + modules/libpr0n/encoders/png/nsPNGEncoder.cpp | 329 +++++++++++-- modules/libpr0n/encoders/png/nsPNGEncoder.h | 16 + modules/libpr0n/public/imgIEncoder.idl | 72 ++- .../libpr0n/test/unit/test_encoder_apng.js | 465 ++++++++++++++++++ 5 files changed, 865 insertions(+), 42 deletions(-) create mode 100644 modules/libpr0n/test/unit/test_encoder_apng.js diff --git a/modules/libpr0n/encoders/jpeg/nsJPEGEncoder.cpp b/modules/libpr0n/encoders/jpeg/nsJPEGEncoder.cpp index ddffd65e72c..412f8768675 100644 --- a/modules/libpr0n/encoders/jpeg/nsJPEGEncoder.cpp +++ b/modules/libpr0n/encoders/jpeg/nsJPEGEncoder.cpp @@ -195,6 +195,31 @@ NS_IMETHODIMP nsJPEGEncoder::InitFromData(const PRUint8* aData, } +NS_IMETHODIMP nsJPEGEncoder::StartImageEncode(PRUint32 aWidth, + PRUint32 aHeight, + PRUint32 aInputFormat, + const nsAString& aOutputOptions) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsJPEGEncoder::AddImageFrame(const PRUint8* aData, + PRUint32 aLength, + PRUint32 aWidth, + PRUint32 aHeight, + PRUint32 aStride, + PRUint32 aFrameFormat, + const nsAString& aFrameOptions) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsJPEGEncoder::EndImageEncode() +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + + /* void close (); */ NS_IMETHODIMP nsJPEGEncoder::Close() { diff --git a/modules/libpr0n/encoders/png/nsPNGEncoder.cpp b/modules/libpr0n/encoders/png/nsPNGEncoder.cpp index 473fbbfc825..80b84ae8db3 100644 --- a/modules/libpr0n/encoders/png/nsPNGEncoder.cpp +++ b/modules/libpr0n/encoders/png/nsPNGEncoder.cpp @@ -22,6 +22,7 @@ * Contributor(s): * Brett Wilson * Stuart Parmenter + * Justin Dolske * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -37,8 +38,10 @@ * * ***** END LICENSE BLOCK ***** */ +#include "nsCRT.h" #include "nsPNGEncoder.h" #include "prmem.h" +#include "prprf.h" #include "nsString.h" #include "nsStreamUtils.h" @@ -53,8 +56,10 @@ // which read such a stream on a background thread. NS_IMPL_THREADSAFE_ISUPPORTS2(nsPNGEncoder, imgIEncoder, nsIInputStream) -nsPNGEncoder::nsPNGEncoder() : mImageBuffer(nsnull), mImageBufferSize(0), - mImageBufferUsed(0), mImageBufferReadPoint(0) +nsPNGEncoder::nsPNGEncoder() : mPNG(nsnull), mPNGinfo(nsnull), + mIsAnimation(PR_FALSE), + mImageBuffer(nsnull), mImageBufferSize(0), + mImageBufferUsed(0), mImageBufferReadPoint(0) { } @@ -82,49 +87,75 @@ NS_IMETHODIMP nsPNGEncoder::InitFromData(const PRUint8* aData, PRUint32 aInputFormat, const nsAString& aOutputOptions) { + nsresult rv; + + rv = StartImageEncode(aWidth, aHeight, aInputFormat, aOutputOptions); + if (!NS_SUCCEEDED(rv)) + return rv; + + rv = AddImageFrame(aData, aLength, aWidth, aHeight, aStride, aInputFormat, aOutputOptions); + if (!NS_SUCCEEDED(rv)) + return rv; + + rv = EndImageEncode(); + + return rv; +} + + +// nsPNGEncoder::StartImageEncode +// +// +// See ::InitFromData for other info. +NS_IMETHODIMP nsPNGEncoder::StartImageEncode(PRUint32 aWidth, + PRUint32 aHeight, + PRUint32 aInputFormat, + const nsAString& aOutputOptions) +{ + PRBool useTransparency = PR_TRUE, skipFirstFrame = PR_FALSE; + PRUint32 numFrames = 1; + PRUint32 numPlays = 0; // For animations, 0 == forever + + // can't initialize more than once + if (mImageBuffer != nsnull) + return NS_ERROR_ALREADY_INITIALIZED; + // validate input format if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA && aInputFormat != INPUT_FORMAT_HOSTARGB) return NS_ERROR_INVALID_ARG; - // Stride is the padded width of each row, so it better be longer (I'm afraid - // people will not understand what stride means, so check it well) - if ((aInputFormat == INPUT_FORMAT_RGB && - aStride < aWidth * 3) || - ((aInputFormat == INPUT_FORMAT_RGBA || aInputFormat == INPUT_FORMAT_HOSTARGB) && - aStride < aWidth * 4)) { - NS_WARNING("Invalid stride for InitFromData"); - return NS_ERROR_INVALID_ARG; - } + // parse and check any provided output options + nsresult rv = ParseOptions(aOutputOptions, &useTransparency, &skipFirstFrame, + &numFrames, &numPlays, nsnull, nsnull, + nsnull, nsnull, nsnull); + if (rv != NS_OK) { return rv; } - // can't initialize more than once - if (mImageBuffer != nsnull) - return NS_ERROR_ALREADY_INITIALIZED; - - // options: we only have one option so this is easy - PRBool useTransparency = PR_TRUE; - if (aOutputOptions.Length() >= 17) { - if (StringBeginsWith(aOutputOptions, NS_LITERAL_STRING("transparency=none"))) - useTransparency = PR_FALSE; + if (numFrames > 1) { + mIsAnimation = PR_TRUE; } // initialize - png_struct* png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, - png_voidp_NULL, - png_error_ptr_NULL, - png_error_ptr_NULL); - if (! png_ptr) + mPNG = png_create_write_struct(PNG_LIBPNG_VER_STRING, + png_voidp_NULL, + ErrorCallback, + ErrorCallback); + if (! mPNG) return NS_ERROR_OUT_OF_MEMORY; - png_info* info_ptr = png_create_info_struct(png_ptr); - if (! info_ptr) - { - png_destroy_write_struct(&png_ptr, nsnull); + + mPNGinfo = png_create_info_struct(mPNG); + if (! mPNGinfo) { + png_destroy_write_struct(&mPNG, nsnull); return NS_ERROR_FAILURE; } - if (setjmp(png_jmpbuf(png_ptr))) { - png_destroy_write_struct(&png_ptr, &info_ptr); - return NS_ERROR_OUT_OF_MEMORY; + + // libpng's error handler jumps back here upon an error. + // Note: It's important that all png_* callers do this, or errors + // will result in a corrupt time-warped stack. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; } // Set up to read the data into our image buffer, start out with an 8K @@ -133,13 +164,13 @@ NS_IMETHODIMP nsPNGEncoder::InitFromData(const PRUint8* aData, mImageBufferSize = 8192; mImageBuffer = (PRUint8*)PR_Malloc(mImageBufferSize); if (!mImageBuffer) { - png_destroy_write_struct(&png_ptr, &info_ptr); + png_destroy_write_struct(&mPNG, &mPNGinfo); return NS_ERROR_OUT_OF_MEMORY; } mImageBufferUsed = 0; // set our callback for libpng to give us the data - png_set_write_fn(png_ptr, this, WriteCallback, NULL); + png_set_write_fn(mPNG, this, WriteCallback, NULL); // include alpha? int colorType; @@ -149,11 +180,75 @@ NS_IMETHODIMP nsPNGEncoder::InitFromData(const PRUint8* aData, else colorType = PNG_COLOR_TYPE_RGB; - png_set_IHDR(png_ptr, info_ptr, aWidth, aHeight, 8, colorType, + png_set_IHDR(mPNG, mPNGinfo, aWidth, aHeight, 8, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); - png_write_info(png_ptr, info_ptr); + if (mIsAnimation) { + png_set_first_frame_is_hidden(mPNG, mPNGinfo, skipFirstFrame); + png_set_acTL(mPNG, mPNGinfo, numFrames, numPlays); + } + + // XXX: support PLTE, gAMA, tRNS, bKGD? + + png_write_info(mPNG, mPNGinfo); + + return NS_OK; +} + + +NS_IMETHODIMP nsPNGEncoder::AddImageFrame(const PRUint8* aData, + PRUint32 aLength, // (unused, req'd by JS) + PRUint32 aWidth, + PRUint32 aHeight, + PRUint32 aStride, + PRUint32 aInputFormat, + const nsAString& aFrameOptions) +{ + PRBool useTransparency= PR_TRUE; + PRUint32 delay_ms = 500; + PRUint32 dispose_op = PNG_DISPOSE_OP_NONE; + PRUint32 blend_op = PNG_BLEND_OP_SOURCE; + PRUint32 x_offset = 0, y_offset = 0; + + // must be initialized + if (mImageBuffer == nsnull) + return NS_ERROR_NOT_INITIALIZED; + + // validate input format + if (aInputFormat != INPUT_FORMAT_RGB && + aInputFormat != INPUT_FORMAT_RGBA && + aInputFormat != INPUT_FORMAT_HOSTARGB) + return NS_ERROR_INVALID_ARG; + + // libpng's error handler jumps back here upon an error. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; + } + + // parse and check any provided output options + nsresult rv = ParseOptions(aFrameOptions, &useTransparency, nsnull, + nsnull, nsnull, &dispose_op, &blend_op, + &delay_ms, &x_offset, &y_offset); + if (rv != NS_OK) { return rv; } + + if (mIsAnimation) { + // XXX the row pointers arg (#3) is unused, can it be removed? + png_write_frame_head(mPNG, mPNGinfo, nsnull, + aWidth, aHeight, x_offset, y_offset, + delay_ms, 1000, dispose_op, blend_op); + } + + // Stride is the padded width of each row, so it better be longer (I'm afraid + // people will not understand what stride means, so check it well) + if ((aInputFormat == INPUT_FORMAT_RGB && + aStride < aWidth * 3) || + ((aInputFormat == INPUT_FORMAT_RGBA || aInputFormat == INPUT_FORMAT_HOSTARGB) && + aStride < aWidth * 4)) { + NS_WARNING("Invalid stride for InitFromData/AddImageFrame"); + return NS_ERROR_INVALID_ARG; + } // write each row: if we add more input formats, we may want to // generalize the conversions @@ -162,7 +257,7 @@ NS_IMETHODIMP nsPNGEncoder::InitFromData(const PRUint8* aData, PRUint8* row = new PRUint8[aWidth * 4]; for (PRUint32 y = 0; y < aHeight; y ++) { ConvertHostARGBRow(&aData[y * aStride], row, aWidth, useTransparency); - png_write_row(png_ptr, row); + png_write_row(mPNG, row); } delete[] row; @@ -171,7 +266,7 @@ NS_IMETHODIMP nsPNGEncoder::InitFromData(const PRUint8* aData, PRUint8* row = new PRUint8[aWidth * 4]; for (PRUint32 y = 0; y < aHeight; y ++) { StripAlpha(&aData[y * aStride], row, aWidth); - png_write_row(png_ptr, row); + png_write_row(mPNG, row); } delete[] row; @@ -179,15 +274,36 @@ NS_IMETHODIMP nsPNGEncoder::InitFromData(const PRUint8* aData, aInputFormat == INPUT_FORMAT_RGBA) { // simple RBG(A), no conversion needed for (PRUint32 y = 0; y < aHeight; y ++) { - png_write_row(png_ptr, (PRUint8*)&aData[y * aStride]); + png_write_row(mPNG, (PRUint8*)&aData[y * aStride]); } } else { NS_NOTREACHED("Bad format type"); + return NS_ERROR_INVALID_ARG; } - png_write_end(png_ptr, info_ptr); - png_destroy_write_struct(&png_ptr, &info_ptr); + if (mIsAnimation) { + png_write_frame_tail(mPNG, mPNGinfo); + } + + return NS_OK; +} + + +NS_IMETHODIMP nsPNGEncoder::EndImageEncode() +{ + // must be initialized + if (mImageBuffer == nsnull) + return NS_ERROR_NOT_INITIALIZED; + + // libpng's error handler jumps back here upon an error. + if (setjmp(png_jmpbuf(mPNG))) { + png_destroy_write_struct(&mPNG, &mPNGinfo); + return NS_ERROR_FAILURE; + } + + png_write_end(mPNG, mPNGinfo); + png_destroy_write_struct(&mPNG, &mPNGinfo); // if output callback can't get enough memory, it will free our buffer if (!mImageBuffer) @@ -197,6 +313,125 @@ NS_IMETHODIMP nsPNGEncoder::InitFromData(const PRUint8* aData, } +nsresult +nsPNGEncoder::ParseOptions(const nsAString& aOptions, + PRBool* useTransparency, + PRBool* skipFirstFrame, + PRUint32* numFrames, + PRUint32* numPlays, + PRUint32* frameDispose, + PRUint32* frameBlend, + PRUint32* frameDelay, + PRUint32* offsetX, + PRUint32* offsetY) +{ + char* token; + char* options = nsCRT::strdup(PromiseFlatCString(NS_ConvertUTF16toUTF8(aOptions)).get()); + + while ((token = nsCRT::strtok(options, ";", &options))) { + // If there's an '=' character, split the token around it. + char* equals = token, *value = nsnull; + while(*equals != '=' && *equals) { ++equals; } + if (*equals == '=') { value = equals + 1; } + + if (value) { *equals = '\0'; } // temporary null + + // transparency=[yes|no|none] + if (nsCRT::strcmp(token, "transparency") == 0 && useTransparency) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + if (nsCRT::strcmp(value, "none") == 0 || nsCRT::strcmp(value, "no") == 0) { + *useTransparency = PR_FALSE; + } else if (nsCRT::strcmp(value, "yes") == 0) { + *useTransparency = PR_TRUE; + } else { + return NS_ERROR_INVALID_ARG; + } + + // skipfirstframe=[yes|no] + } else if (nsCRT::strcmp(token, "skipfirstframe") == 0 && skipFirstFrame) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + if (nsCRT::strcmp(value, "no") == 0) { + *skipFirstFrame = PR_FALSE; + } else if (nsCRT::strcmp(value, "yes") == 0) { + *skipFirstFrame = PR_TRUE; + } else { + return NS_ERROR_INVALID_ARG; + } + + // frames=# + } else if (nsCRT::strcmp(token, "frames") == 0 && numFrames) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + if (PR_sscanf(value, "%u", numFrames) != 1) { return NS_ERROR_INVALID_ARG; } + + // frames=0 is nonsense. + if (*numFrames == 0) { return NS_ERROR_INVALID_ARG; } + + // plays=# + } else if (nsCRT::strcmp(token, "plays") == 0 && numPlays) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + // plays=0 to loop forever, otherwise play sequence specified number of times + if (PR_sscanf(value, "%u", numPlays) != 1) { return NS_ERROR_INVALID_ARG; } + + // dispose=[none|background|previous] + } else if (nsCRT::strcmp(token, "dispose") == 0 && frameDispose) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + if (nsCRT::strcmp(value, "none") == 0) { + *frameDispose = PNG_DISPOSE_OP_NONE; + } else if (nsCRT::strcmp(value, "background") == 0) { + *frameDispose = PNG_DISPOSE_OP_BACKGROUND; + } else if (nsCRT::strcmp(value, "previous") == 0) { + *frameDispose = PNG_DISPOSE_OP_PREVIOUS; + } else { + return NS_ERROR_INVALID_ARG; + } + + // blend=[source|over] + } else if (nsCRT::strcmp(token, "blend") == 0 && frameBlend) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + if (nsCRT::strcmp(value, "source") == 0) { + *frameBlend = PNG_BLEND_OP_SOURCE; + } else if (nsCRT::strcmp(value, "over") == 0) { + *frameBlend = PNG_BLEND_OP_OVER; + } else { + return NS_ERROR_INVALID_ARG; + } + + // delay=# (in ms) + } else if (nsCRT::strcmp(token, "delay") == 0 && frameDelay) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + if (PR_sscanf(value, "%u", frameDelay) != 1) { return NS_ERROR_INVALID_ARG; } + + // xoffset=# + } else if (nsCRT::strcmp(token, "xoffset") == 0 && offsetX) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + if (PR_sscanf(value, "%u", offsetX) != 1) { return NS_ERROR_INVALID_ARG; } + + // yoffset=# + } else if (nsCRT::strcmp(token, "yoffset") == 0 && offsetY) { + if (!value) { return NS_ERROR_INVALID_ARG; } + + if (PR_sscanf(value, "%u", offsetY) != 1) { return NS_ERROR_INVALID_ARG; } + + // unknown token name + } else { + return NS_ERROR_INVALID_ARG; + } + + if (value) { *equals = '='; } // restore '=' so strtok doesn't get lost + } + + return NS_OK; +} + + /* void close (); */ NS_IMETHODIMP nsPNGEncoder::Close() { @@ -308,6 +543,18 @@ nsPNGEncoder::StripAlpha(const PRUint8* aSrc, PRUint8* aDest, } +// nsPNGEncoder::ErrorCallback + +void // static +nsPNGEncoder::ErrorCallback(png_structp png_ptr, png_const_charp warning_msg) +{ +#ifdef DEBUG + // XXX: these messages are probably useful callers... use nsIConsoleService? + PR_fprintf(PR_STDERR, "PNG Encoder: %s\n", warning_msg);; +#endif +} + + // nsPNGEncoder::WriteCallback void // static diff --git a/modules/libpr0n/encoders/png/nsPNGEncoder.h b/modules/libpr0n/encoders/png/nsPNGEncoder.h index 2fe3c6bae72..cc121d01cd0 100644 --- a/modules/libpr0n/encoders/png/nsPNGEncoder.h +++ b/modules/libpr0n/encoders/png/nsPNGEncoder.h @@ -63,12 +63,28 @@ private: ~nsPNGEncoder(); protected: + nsresult ParseOptions(const nsAString& aOptions, + PRBool* useTransparency, + PRBool* skipFirstFrame, + PRUint32* numAnimatedFrames, + PRUint32* numIterations, + PRUint32* frameDispose, + PRUint32* frameBlend, + PRUint32* frameDelay, + PRUint32* offsetX, + PRUint32* offsetY); void ConvertHostARGBRow(const PRUint8* aSrc, PRUint8* aDest, PRUint32 aPixelWidth, PRBool aUseTransparency); void StripAlpha(const PRUint8* aSrc, PRUint8* aDest, PRUint32 aPixelWidth); + static void ErrorCallback(png_structp png_ptr, png_const_charp warning_msg); static void WriteCallback(png_structp png, png_bytep data, png_size_t size); + png_struct* mPNG; + png_info* mPNGinfo; + + PRBool mIsAnimation; + // image buffer PRUint8* mImageBuffer; PRUint32 mImageBufferSize; diff --git a/modules/libpr0n/public/imgIEncoder.idl b/modules/libpr0n/public/imgIEncoder.idl index 9048bc592e8..0cfa4320a02 100755 --- a/modules/libpr0n/public/imgIEncoder.idl +++ b/modules/libpr0n/public/imgIEncoder.idl @@ -21,6 +21,7 @@ * Contributor(s): * Stuart Parmenter * Brett Wilson + * Justin Dolske * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -42,9 +43,55 @@ /** * imgIEncoder interface */ -[scriptable, uuid(CCC5B3AD-3E67-4e3d-97E1-B06B2E96FEF8)] +[scriptable, uuid(ba3a854b-fb8d-4881-8af9-5849df10e5e5)] interface imgIEncoder : nsIInputStream { + // Possible values for outputOptions. Multiple values are semicolon-separated. + // + // PNG: + // ---- + // usetransparency=[yes|no|none] -- default: "yes" + // Overrides default from input format. "no" and "none" are equivalent. + // + // + // APNG: + // ----- + // The following options can be used with startImageEncode(): + // + // usetransparency=[yes|no|none] -- default: "yes" + // Overrides default from input format. "no" and "none" are equivalent. + // skipfirstframe=[yes|no] -- default: "no" + // Controls display of the first frame in animations. PNG-only clients always + // display the first frame (and only that frame). + // frames=# -- default: "1" + // Total number of frames in the image. The first frame, even if skipped, is + // always included in the count. + // plays=# -- default: "0" + // Number of times to play the animation sequence. "0" will repeat forever. + // + // + // The following options can be used for each frame, with addImageFrame(): + // + // usetransparency=[yes|no|none] -- default: "yes" + // Overrides default from input format. "no" and "none" are equivalent. + // delay=# -- default: "500" + // Number of milliseconds to display the frame, before moving to the next frame. + // dispose=[none|background|previous] -- default: "none" + // What to do with the image's canvas before rendering the next frame. See APNG spec. + // blend=[source|over] -- default: "source" + // How to render the new frame on the canvas. See APNG spec. + // xoffset=# -- default: "0" + // yoffset=# -- default: "0" + // Where to draw the frame, relative to the canvas. + // + // + // JPEG: + // ----- + // + // quality=# -- default: "50" + // Quality of compression, 0-100 (worst-best). + + // Possible values for input format (note that not all image formats // support saving alpha channels): @@ -84,4 +131,27 @@ interface imgIEncoder : nsIInputStream in PRUint32 stride, in PRUint32 inputFormat, in AString outputOptions); + + /* + * For encoding images which may contain multiple frames, the 1-shot + * initFromData() interface is too simplistic. The alternative is to + * use startImageEncode(), call addImageFrame() one or more times, and + * then finish initialization with endImageEncode(). + * + * The arguments are basically the same as in initFromData(). + */ + void startImageEncode(in PRUint32 width, + in PRUint32 height, + in PRUint32 inputFormat, + in AString outputOptions); + + void addImageFrame( [array, size_is(length), const] in PRUint8 data, + in unsigned long length, + in PRUint32 width, + in PRUint32 height, + in PRUint32 stride, + in PRUint32 frameFormat, + in AString frameOptions); + + void endImageEncode(); }; diff --git a/modules/libpr0n/test/unit/test_encoder_apng.js b/modules/libpr0n/test/unit/test_encoder_apng.js new file mode 100644 index 00000000000..92474c3cf89 --- /dev/null +++ b/modules/libpr0n/test/unit/test_encoder_apng.js @@ -0,0 +1,465 @@ +/* + * Test for APNG encoding in libpr0n + * + */ + + +const Ci = Components.interfaces; +const Cc = Components.classes; + + // dispose=[none|background|previous] + // blend=[source|over] + +var apng1A = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGB format. + width : 3, height : 3, skipFirstFrame : false, + format : Ci.imgIEncoder.INPUT_FORMAT_RGB, + transparency : null, + plays : 0, + + frames : [ + { // frame #1 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, + transparency : null, + + pixels : [ + 255,0,0, 255,0,0, 255,0,0, + 255,0,0, 255,0,0, 255,0,0, + 255,0,0, 255,0,0, 255,0,0 + ] + }, + + { // frame #2 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, + transparency : null, + + pixels : [ + 0,255,0, 0,255,0, 0,255,0, + 0,255,0, 0,255,0, 0,255,0, + 0,255,0, 0,255,0, 0,255,0 + ], + }, + + { // frame #3 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, + transparency : null, + + pixels : [ + 0,0,255, 0,0,255, 0,0,255, + 0,0,255, 0,0,255, 0,0,255, + 0,0,255, 0,0,255, 0,0,255 + ], + } + + ], + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABBJREFUCJlj/M8ABUwMmCwAHVsBBdmfmJ4AAAAaZmNUTAAAAAEAAAADAAAAAwAAAAAAAAAAAfQD6AAA7mXKzAAAABZmZEFUAAAAAgiZY2T4zwABTAwMGCwAHFwBBVnd/YYAAAAaZmNUTAAAAAMAAAADAAAAAwAAAAAAAAAAAfQD6AAAA/MZJQAAABtmZEFUAAAABAiZY2Rg+M/AwMDAwMDEAAMIFgAbXQEFA/mlawAAAABJRU5ErkJggg==" +}; + + +var apng1B = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + width : 3, height : 3, skipFirstFrame : false, + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency : null, + plays : 0, + + frames : [ + { // frame #1 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 255,0,0,255, 255,0,0,255, 255,0,0,255, + 255,0,0,255, 255,0,0,255, 255,0,0,255, + 255,0,0,255, 255,0,0,255, 255,0,0,255 + ] + }, + + { // frame #2 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,255,0,255, 0,255,0,255, 0,255,0,255, + 0,255,0,255, 0,255,0,255, 0,255,0,255, + 0,255,0,255, 0,255,0,255, 0,255,0,255 + ], + }, + + { // frame #3 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,0,255,255, 0,0,255,255, 0,0,255,255, + 0,0,255,255, 0,0,255,255, 0,0,255,255, + 0,0,255,255, 0,0,255,255, 0,0,255,255 + ], + } + + ], + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABVJREFUCJlj/M/A8J8BCpgYkAAKBwBJUwIE1lTE1gAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAADuZcrMAAAAGWZkQVQAAAACCJljZPjP8J8BCpgYkAAKBwBIVAIEn70e9AAAABpmY1RMAAAAAwAAAAMAAAADAAAAAAAAAAAB9APoAAAD8xklAAAAGWZkQVQAAAAECJljZGD4/58BCpgYkAAKBwBHVQIEp0ldFQAAAABJRU5ErkJggg==" +}; + + +var apng1C = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // The first frame is skipped, so it will only flash green/blue (or static red in an APNG-unaware viewer) + width : 3, height : 3, skipFirstFrame : true, + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency : null, + plays : 0, + + frames : [ + { // frame #1 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 255,0,0,255, 255,0,0,255, 255,0,0,255, + 255,0,0,255, 255,0,0,255, 255,0,0,255, + 255,0,0,255, 255,0,0,255, 255,0,0,255 + ] + }, + + { // frame #2 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,255,0,255, 0,255,0,255, 0,255,0,255, + 0,255,0,255, 0,255,0,255, 0,255,0,255, + 0,255,0,255, 0,255,0,255, 0,255,0,255 + ], + }, + + { // frame #3 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,0,255,255, 0,0,255,255, 0,0,255,255, + 0,0,255,255, 0,0,255,255, 0,0,255,255, + 0,0,255,255, 0,0,255,255, 0,0,255,255 + ], + } + + ], + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAACAAAAAPONk3AAAAAVSURBVAiZY/zPwPCfAQqYGJAACgcASVMCBNZUxNYAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABlmZEFUAAAAAQiZY2T4z/CfAQqYGJAACgcASFQCBKbFs7QAAAAaZmNUTAAAAAIAAAADAAAAAwAAAAAAAAAAAfQD6AAAmIDz8QAAABlmZEFUAAAAAwiZY2Rg+P+fAQqYGJAACgcAR1UCBMKQY1UAAAAASUVORK5CYII=" +}; + + +var apng2A = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // blend = over mode + width : 3, height : 3, skipFirstFrame : false, + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency : null, + plays : 0, + + frames : [ + { // frame #1 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 255,0,0,255, 255,0,0,255, 255,0,0,255, + 255,0,0,255, 255,0,0,255, 255,0,0,255, + 255,0,0,255, 255,0,0,255, 255,0,0,255 + ] + }, + + { // frame #2 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "over", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,255,0,255, 0,255,0,180, 0,255,0,75, + 0,255,0,255, 0,255,0,180, 0,255,0,75, + 0,255,0,255, 0,255,0,180, 0,255,0,75 + ], + }, + + { // frame #3 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "over", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,0,255,75, 0,0,255,75, 0,0,255,75, + 0,0,255,180, 0,0,255,180, 0,0,255,180, + 0,0,255,255, 0,0,255,255, 0,0,255,255 + ], + } + + ], + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABVJREFUCJlj/M/A8J8BCpgYkAAKBwBJUwIE1lTE1gAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAAGZYvpaAAAAH2ZkQVQAAAACCJljYPjP8J/hP8MWhv8M3kwMSACFAwCjpAUABsIwXQAAABpmY1RMAAAAAwAAAAMAAAADAAAAAAAAAAAB9APoAAF09CmzAAAAHWZkQVQAAAAECJljZGD4780ABYwMDP+3IHP+wzgAZ+AE/2rZOPoAAAAASUVORK5CYII=" +}; + + +var apng2B = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // blend = over, dispose = background + width : 3, height : 3, skipFirstFrame : false, + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency : null, + plays : 0, + + frames : [ + { // frame #1 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "background", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 255,0,0,255, 255,0,0,255, 255,0,0,255, + 255,0,0,255, 255,0,0,255, 255,0,0,255, + 255,0,0,255, 255,0,0,255, 255,0,0,255 + ] + }, + + { // frame #2 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "background", blend : "over", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,255,0,255, 0,255,0,180, 0,255,0,75, + 0,255,0,255, 0,255,0,180, 0,255,0,75, + 0,255,0,255, 0,255,0,180, 0,255,0,75 + ], + }, + + { // frame #3 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "background", blend : "over", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,0,255,75, 0,0,255,75, 0,0,255,75, + 0,0,255,180, 0,0,255,180, 0,0,255,180, + 0,0,255,255, 0,0,255,255, 0,0,255,255 + ], + } + + ], + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AEAbA0RWQAAABVJREFUCJlj/M/A8J8BCpgYkAAKBwBJUwIE1lTE1gAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAQGAecsbAAAAH2ZkQVQAAAACCJljYPjP8J/hP8MWhv8M3kwMSACFAwCjpAUABsIwXQAAABpmY1RMAAAAAwAAAAMAAAADAAAAAAAAAAAB9APoAQFt7xjyAAAAHWZkQVQAAAAECJljZGD4780ABYwMDP+3IHP+wzgAZ+AE/2rZOPoAAAAASUVORK5CYII=" +}; + + +var apng3 = { + // A 3x3 image with 4 frames. First frame is white, then 1x1 frames draw a diagonal line + width : 3, height : 3, skipFirstFrame : false, + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency : null, + plays : 0, + + frames : [ + { // frame #1 + width : 3, height : 3, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + + 255,255,255,255, 255,255,255,255, 255,255,255,255, + 255,255,255,255, 255,255,255,255, 255,255,255,255, + 255,255,255,255, 255,255,255,255, 255,255,255,255 + ] + }, + + { // frame #2 + width : 1, height : 1, + x_offset : 0, y_offset : 0, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,0,0,255 + ], + }, + + { // frame #3 + width : 1, height : 1, + x_offset : 1, y_offset : 1, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,0,0,255 + ], + }, + + { // frame #4 + width : 1, height : 1, + x_offset : 2, y_offset : 2, + dispose : "none", blend : "source", delay : 500, + + format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, + + pixels : [ + 0,0,0,255 + ], + } + ], + + expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAAEAAAAAHzNZtAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABVJREFUCJlj/P///38GKGBiQAIoHACSCgQC2nCLJAAAABpmY1RMAAAAAQAAAAEAAAABAAAAAAAAAAAB9APoAAAyV32sAAAAEWZkQVQAAAACCJljYGBg+A8AAQQBAG6+ZqYAAAAaZmNUTAAAAAMAAAABAAAAAQAAAAEAAAABAfQD6AAAuDh6MQAAABFmZEFUAAAABAiZY2BgYPgPAAEEAQCWfC0QAAAAGmZjVEwAAAAFAAAAAQAAAAEAAAACAAAAAgH0A+gAAP34dNcAAAARZmRBVAAAAAYImWNgYGD4DwABBAEAdxLpvQAAAABJRU5ErkJggg==" +}; + +// Main test entry point. +function run_test() { + dump("Checking apng1A...\n"); + run_test_for(apng1A); + dump("Checking apng1B...\n"); + run_test_for(apng1B); + dump("Checking apng1C...\n"); + run_test_for(apng1C); + + dump("Checking apng2A...\n"); + run_test_for(apng2A); + dump("Checking apng2B...\n"); + run_test_for(apng2B); + + dump("Checking apng3...\n"); + run_test_for(apng3); +}; + + +function run_test_for(input) { + var encoder, dataURL; + + encoder = encodeImage(input); + dataURL = makeDataURL(encoder, "image/png"); + do_check_eq(dataURL, input.expected); +}; + + +function encodeImage(input) { + var encoder = Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); + encoder.QueryInterface(Ci.imgIEncoder); + + var options = ""; + if (input.transparency) { options += "transparency=" + input.transparency; } + options += ";frames=" + input.frames.length; + options += ";skipfirstframe=" + (input.skipFirstFrame ? "yes" : "no"); + options += ";plays=" + input.plays; + encoder.startImageEncode(input.width, input.height, input.format, options); + + for (var i = 0; i < input.frames.length; i++) { + var frame = input.frames[i]; + + options = ""; + if (frame.transparency) { options += "transparency=" + input.transparency; } + options += ";delay=" + frame.delay; + options += ";dispose=" + frame.dispose; + options += ";blend=" + frame.blend; + if (frame.x_offset > 0) { options += ";xoffset=" + frame.x_offset; } + if (frame.y_offset > 0) { options += ";yoffset=" + frame.y_offset; } + + encoder.addImageFrame(frame.pixels, frame.pixels.length, + frame.width, frame.height, frame.stride, frame.format, options); + } + + encoder.endImageEncode(); + + return encoder; +} + + +function makeDataURL(encoder, mimetype) { + var rawStream = encoder.QueryInterface(Ci.nsIInputStream); + + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); + stream.QueryInterface(Ci.nsIBinaryInputStream); + + stream.setInputStream(rawStream); + + var bytes = stream.readByteArray(stream.available()); // returns int[] + + var base64String = toBase64(bytes); + + return "data:" + mimetype + ";base64," + base64String; +} + +/* toBase64 copied from extensions/xml-rpc/src/nsXmlRpcClient.js */ + +/* Convert data (an array of integers) to a Base64 string. */ +const toBase64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + + '0123456789+/'; +const base64Pad = '='; +function toBase64(data) { + var result = ''; + var length = data.length; + var i; + // Convert every three bytes to 4 ascii characters. + for (i = 0; i < (length - 2); i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; + result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; + result += toBase64Table[data[i+2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + if (length%3) { + i = length - (length%3); + result += toBase64Table[data[i] >> 2]; + if ((length%3) == 2) { + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; + result += toBase64Table[(data[i+1] & 0x0f) << 2]; + result += base64Pad; + } else { + result += toBase64Table[(data[i] & 0x03) << 4]; + result += base64Pad + base64Pad; + } + } + + return result; +}