diff --git a/samples/ImageLoading.Forms.Sample/Shared/Pages/SimpleGifPage.xaml b/samples/ImageLoading.Forms.Sample/Shared/Pages/SimpleGifPage.xaml index 0ae990daa..2d755f2d9 100644 --- a/samples/ImageLoading.Forms.Sample/Shared/Pages/SimpleGifPage.xaml +++ b/samples/ImageLoading.Forms.Sample/Shared/Pages/SimpleGifPage.xaml @@ -5,11 +5,13 @@ xmlns:fftransformations="clr-namespace:FFImageLoading.Transformations;assembly=FFImageLoading.Transformations" Title="Simple Gif"> - + + - - + + + diff --git a/source/FFImageLoading.Droid/Helpers/GifDecoder.cs b/source/FFImageLoading.Droid/Helpers/GifDecoder.cs index c8d959db3..210502830 100644 --- a/source/FFImageLoading.Droid/Helpers/GifDecoder.cs +++ b/source/FFImageLoading.Droid/Helpers/GifDecoder.cs @@ -12,25 +12,12 @@ namespace FFImageLoading.Helpers public class GifDecoder { object _lock = new object(); - /** - * File read status: No errors. - */ const int STATUS_OK = 0; - /** - * File read status: Error decoding file (may be partially decoded) - */ const int STATUS_FORMAT_ERROR = 1; - /** - * File read status: Unable to open source. - */ const int STATUS_OPEN_ERROR = 2; - /** max decoder pixel stack size */ const int MAX_STACK_SIZE = 4096; Stream input; int status; - - int insampleSize = 1; - bool firstTime = true; int width; int height; bool gctFlag; // global color table used @@ -65,10 +52,14 @@ public class GifDecoder byte[] pixels; IList frames; // frames read from current file int frameCount; - readonly Func> _decodingFunc; + readonly Func> _decodingFunc; + readonly int _downsampleWidth; + readonly int _downsampleHeight; - public GifDecoder(Func> decodingFunc) + public GifDecoder(int downsampleWidth, int downsampleHeight, Func> decodingFunc) { + _downsampleHeight = downsampleHeight; + _downsampleWidth = downsampleWidth; _decodingFunc = decodingFunc; status = STATUS_OK; frameCount = 0; @@ -77,13 +68,6 @@ public GifDecoder(Func> decodingFunc lct = null; } - /** - * Gets display duration for specified frame. - * - * @param n - * int index of frame - * @return delay in milliseconds - */ public int GetDelay(int n) { lock (_lock) @@ -93,15 +77,11 @@ public int GetDelay(int n) { delay = frames[n].Delay; } + return delay; } } - /** - * Gets the number of frames read from file. - * - * @return frame count - */ public int GetFrameCount() { lock (_lock) @@ -110,11 +90,6 @@ public int GetFrameCount() } } - /** - * Gets the first (or only) image read. - * - * @return BufferedBitmap containing first frame, or null if none. - */ public Bitmap GetBitmap() { lock (_lock) @@ -123,11 +98,6 @@ public Bitmap GetBitmap() } } - /** - * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely. - * - * @return iteration count if one was specified, else 1. - */ public int GetLoopCount() { lock (_lock) @@ -136,9 +106,6 @@ public int GetLoopCount() } } - /** - * Creates new frame image from current data (and previous frames as specified by their disposition codes). - */ protected async Task SetPixelsAsync() { int[] dest = new int[width * height]; @@ -162,19 +129,6 @@ protected async Task SetPixelsAsync() if (currentBitmap != null) { dest = lastBitmap1; - //int[] dest2 = new int[width * height]; - //lastBitmap.GetPixels(dest2, 0, width, 0, 0, width, height); - - //int desti = 0; - - //for (int i = 0; i < dest2.Length; i = i + insampleSize) - //{ - // for (int ins = 0; ins < insampleSize; ins++) - // { - // dest[desti] = dest2[i]; - // desti++; - // } - //} // copy pixels if (lastDispose == 2) @@ -254,47 +208,95 @@ protected async Task SetPixelsAsync() } } - byte[] result = new byte[dest.Length * sizeof(int)]; - Buffer.BlockCopy(dest, 0, result, 0, result.Length); + bool downsample = false; + int downsampleWidth = width; + int downsampleHeight = height; - currentBitmap = dest; + if (_downsampleWidth == 0 && _downsampleHeight != 0) + { + downsample = true; + downsampleWidth = (int)(((float)_downsampleHeight / height) * width); + downsampleHeight = _downsampleHeight; + } + else if (_downsampleHeight == 0 && _downsampleWidth != 0) + { + downsample = true; + downsampleWidth = _downsampleWidth; + downsampleHeight = (int)(((float)_downsampleWidth / width) * height); + } + + var result = dest; - //TODO need to optimze that too (bypass encoding to png) - using (var bitmap = Bitmap.CreateBitmap(dest, width, height, Bitmap.Config.Argb4444)) - using (var stream = new MemoryStream()) + //TODO fix downsampling issue (part of image is cut) + downsample = false; + if (downsample) { - await bitmap.CompressAsync(Bitmap.CompressFormat.Png, 100, stream); - image = await _decodingFunc.Invoke(stream, new BitmapFactory.Options() - { - OutWidth = width, - OutHeight = height, - }); + //if (downsampleWidth % 2 > 0) + //{ + // downsampleWidth--; + //} + //if (downsampleHeight % 2 > 0) + //{ + // downsampleHeight--; + //} - if (firstTime) - { - height = image.Height; - firstTime = false; - insampleSize = width / image.Width; - } + //double inSampleSize = 1D; + + //if (height > downsampleHeight || width > downsampleWidth) + //{ + // int halfHeight = (int)(height / 2); + // int halfWidth = (int)(width / 2); + + // // Calculate a inSampleSize that is a power of 2 - the decoder will use a value that is a power of two anyway. + // while ((halfHeight / inSampleSize) > downsampleHeight && (halfWidth / inSampleSize) > downsampleWidth) + // { + // inSampleSize *= 2; + // } + //} + + //var insample = (int)inSampleSize; + //if (insample > 1) + //{ + // downsample = true; + + // int idh = 0; + // int idw = 0; + // result = new int[downsampleWidth * downsampleHeight]; + + // for (int h = 0; h < downsampleHeight; h++) + // { + // idh = insample * h; + + // for (int w = 0; w < downsampleWidth; w++) + // { + // idw = insample * w; + // var destIdx = idh * width + idw; + + // if (destIdx < dest.Length) + // result[h * downsampleWidth + w] = dest[destIdx]; + // } + // } + //} + //else + //{ + // downsample = false; + //} } + + currentBitmap = dest; + var bitmap = Bitmap.CreateBitmap(result, downsample ? downsampleWidth : width, downsample ? downsampleHeight : height, Bitmap.Config.Argb4444); + image = await _decodingFunc.Invoke(bitmap).ConfigureAwait(false); } - /** - * Reads GIF image from stream - * - * @param is - * containing GIF file. - * @return read status code (0 = no errors) - */ public async Task ReadGifAsync(Stream inputStream) { if (inputStream != null) { input = inputStream; - await ReadHeaderAsync(); + await ReadHeaderAsync().ConfigureAwait(false); if (!Err()) { - await ReadContentsAsync(); + await ReadContentsAsync().ConfigureAwait(false); if (frameCount < 0) { status = STATUS_FORMAT_ERROR; @@ -309,9 +311,6 @@ public async Task ReadGifAsync(Stream inputStream) return status; } - /** - * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick. - */ protected async Task DecodeBitmapDataAsync() { int nullCode = -1; @@ -358,7 +357,7 @@ protected async Task DecodeBitmapDataAsync() if (count == 0) { // Read a new data block. - count = await ReadBlockAsync(); + count = await ReadBlockAsync().ConfigureAwait(false); if (count <= 0) { break; @@ -435,11 +434,6 @@ protected async Task DecodeBitmapDataAsync() } } - /** - * Gets the image contents of frame n. - * - * @return BufferedBitmap representation of frame, or null if n is invalid. - */ public Bitmap GetFrame(int n) { lock (_lock) @@ -459,17 +453,11 @@ public bool ContainsBitmap(Bitmap bitmap) } } - /** - * Returns true if an error was encountered during reading/decoding - */ protected bool Err() { return status != STATUS_OK; } - /** - * Reads a single byte from the input stream. - */ protected int Read() { int curByte = 0; @@ -477,18 +465,13 @@ protected int Read() { curByte = input.ReadByte(); } - catch (Exception e) + catch (Exception) { status = STATUS_FORMAT_ERROR; } return curByte; } - /** - * Reads next variable length block from input. - * - * @return number of bytes stored in "buffer" - */ protected async Task ReadBlockAsync() { blockSize = Read(); @@ -520,13 +503,6 @@ protected async Task ReadBlockAsync() return n; } - /** - * Reads color table as 256 RGB integer values - * - * @param ncolors - * int number of colors to read - * @return int array containing 256 colors (packed ARGB with full alpha) - */ protected async Task ReadColorTableAsync(int ncolors) { int nbytes = 3 * ncolors; @@ -561,9 +537,6 @@ protected async Task ReadColorTableAsync(int ncolors) return tab; } - /** - * Main file parser. Reads GIF content blocks. - */ protected async Task ReadContentsAsync() { // read GIF file content blocks @@ -574,7 +547,7 @@ protected async Task ReadContentsAsync() switch (code) { case 0x2C: // image separator - await ReadBitmapAsync(); + await ReadBitmapAsync().ConfigureAwait(false); break; case 0x21: // extension code = Read(); @@ -584,7 +557,7 @@ protected async Task ReadContentsAsync() ReadGraphicControlExt(); break; case 0xff: // application extension - await ReadBlockAsync(); + await ReadBlockAsync().ConfigureAwait(false); String app = ""; for (int i = 0; i < 11; i++) { @@ -592,21 +565,21 @@ protected async Task ReadContentsAsync() } if (app.Equals("NETSCAPE2.0", StringComparison.InvariantCultureIgnoreCase)) { - await ReadNetscapeExtAsync(); + await ReadNetscapeExtAsync().ConfigureAwait(false); } else { - await SkipAsync(); // don't care + await SkipAsync().ConfigureAwait(false); // don't care } break; case 0xfe:// comment extension - await SkipAsync(); + await SkipAsync().ConfigureAwait(false); break; case 0x01:// plain text extension - await SkipAsync(); + await SkipAsync().ConfigureAwait(false); break; default: // uninteresting extension - await SkipAsync(); + await SkipAsync().ConfigureAwait(false); break; } break; @@ -621,9 +594,6 @@ protected async Task ReadContentsAsync() } } - /** - * Reads Graphics Control Extension values - */ protected void ReadGraphicControlExt() { Read(); // block size @@ -639,9 +609,6 @@ protected void ReadGraphicControlExt() Read(); // block terminator } - /** - * Reads GIF file header information. - */ protected async Task ReadHeaderAsync() { String id = ""; @@ -662,9 +629,6 @@ protected async Task ReadHeaderAsync() } } - /** - * Reads next frame image - */ protected async Task ReadBitmapAsync() { ix = ReadShort(); // (sub)image position & size @@ -705,8 +669,8 @@ protected async Task ReadBitmapAsync() { return; } - await DecodeBitmapDataAsync(); // decode pixel data - await SkipAsync(); + await DecodeBitmapDataAsync().ConfigureAwait(false); // decode pixel data + await SkipAsync().ConfigureAwait(false); if (Err()) { return; @@ -714,7 +678,7 @@ protected async Task ReadBitmapAsync() frameCount++; // create new image to receive frame data //image = Bitmap.CreateBitmap(width, height, Bitmap.Config.Argb4444); - await SetPixelsAsync(); // transfer pixel data to image + await SetPixelsAsync().ConfigureAwait(false); // transfer pixel data to image frames.Add(new GifFrame(image, delay)); // add image to frame // list if (transparency) @@ -725,9 +689,6 @@ protected async Task ReadBitmapAsync() ResetFrame(); } - /** - * Reads Logical Screen Descriptor - */ protected void ReadLSD() { // logical screen size @@ -744,14 +705,11 @@ protected void ReadLSD() pixelAspect = Read(); // pixel aspect ratio } - /** - * Reads Netscape extenstion to obtain iteration count - */ protected async Task ReadNetscapeExtAsync() { do { - await ReadBlockAsync(); + await ReadBlockAsync().ConfigureAwait(false); if (block[0] == 1) { // loop count sub-block @@ -762,18 +720,12 @@ protected async Task ReadNetscapeExtAsync() } while ((blockSize > 0) && !Err()); } - /** - * Reads next 16-bit value, LSB first - */ protected int ReadShort() { // read 16-bit value, LSB first return Read() | (Read() << 8); } - /** - * Resets frame state for reading next image. - */ protected void ResetFrame() { lastDispose = dispose; @@ -789,14 +741,11 @@ protected void ResetFrame() lct = null; } - /** - * Skips variable length blocks up to and including next zero length block. - */ protected async Task SkipAsync() { do { - await ReadBlockAsync(); + await ReadBlockAsync().ConfigureAwait(false); } while ((blockSize > 0) && !Err()); } @@ -812,40 +761,39 @@ public GifFrame(Bitmap im, int del) public int Delay; } - //TODO need to optimize that!!! (read stream instead converting to string) public static bool CheckIfAnimated(Stream st) { - byte[] byteCode1 = { 0x00, 0x21, 0xF9, 0x04 }; - byte[] byteCode2 = { 0x00, 0x2C }; - string strTemp; - byte[] byteContents; - int iCount; - int iPos = 0; - int iPos1; - int iPos2; - - byteContents = new byte[st.Length]; - st.Read(byteContents, 0, (int)st.Length); - strTemp = System.Text.Encoding.ASCII.GetString(byteContents); - byteContents = null; - iCount = 0; - while (iCount < 2) - { - iPos1 = strTemp.IndexOf(System.Text.Encoding.ASCII.GetString(byteCode1), iPos, StringComparison.Ordinal); - if (iPos1 == -1) break; - iPos = iPos1 + 1; - iPos2 = strTemp.IndexOf(System.Text.Encoding.ASCII.GetString(byteCode2), iPos, StringComparison.Ordinal); - if (iPos2 == -1) break; - if ((iPos1 + 8) == iPos2) - iCount++; - iPos = iPos2 + 1; - } - - st.Position = 0; - - if (iCount > 1) return true; - - return false; + try + { + int headerCount = 0; + bool expectSecondPart = false; + int firstRead; + while ((firstRead = st.ReadByte()) >= 0) + { + if (firstRead == 0x00) + { + var secondRead = st.ReadByte(); + if (!expectSecondPart && secondRead == 0x2C) + { + expectSecondPart = true; + } + else if (expectSecondPart && secondRead == 0x21 && st.ReadByte() == 0xF9) + { + headerCount++; + expectSecondPart = false; + } + + if (headerCount > 1) + return true; + } + } + + return false; + } + finally + { + st.Position = 0; + } } } } diff --git a/source/FFImageLoading.Droid/Work/PlatformImageLoadingTask.cs b/source/FFImageLoading.Droid/Work/PlatformImageLoadingTask.cs index 9027d6481..e18bad769 100644 --- a/source/FFImageLoading.Droid/Work/PlatformImageLoadingTask.cs +++ b/source/FFImageLoading.Droid/Work/PlatformImageLoadingTask.cs @@ -182,6 +182,13 @@ async Task PlatformGenerateBitmapAsync(string path, ImageSource source, ThrowIfCancellationRequested(); + bitmap = await PlatformTransformAsync(path, source, enableTransformations, isPlaceholder, bitmap); + + return bitmap; + } + + async Task PlatformTransformAsync(string path, ImageSource source, bool enableTransformations, bool isPlaceholder, Bitmap bitmap) + { if (enableTransformations && Parameters.Transformations != null && Parameters.Transformations.Count > 0) { var transformations = Parameters.Transformations.ToList(); @@ -247,9 +254,24 @@ async Task PlatformGenerateGifImageAsync(string path, ImageSource try { - var gifDecoder = new GifDecoder((stream, options) => + int downsampleWidth = 0; + int downsampleHeight = 0; + + if (Parameters.DownSampleSize != null && (Parameters.DownSampleSize.Item1 > 0 || Parameters.DownSampleSize.Item2 > 0)) + { + downsampleWidth = Parameters.DownSampleSize.Item1; + downsampleHeight = Parameters.DownSampleSize.Item2; + } + + if (Parameters.DownSampleUseDipUnits) + { + downsampleWidth = downsampleWidth.DpToPixels(); + downsampleHeight = downsampleHeight.DpToPixels(); + } + + var gifDecoder = new GifDecoder(downsampleWidth, downsampleHeight, (bmp) => { - return PlatformGenerateBitmapAsync(Guid.NewGuid().ToString(), ImageSource.Stream, stream, new ImageInformation(), enableTransformations, isPlaceholder, options); + return PlatformTransformAsync(path, source, enableTransformations, isPlaceholder, bmp); }); await gifDecoder.ReadGifAsync(imageData);