diff --git a/common/src/main/java/com/bumptech/glide/gifencoder/AnimatedGifEncoder.java b/common/src/main/java/com/bumptech/glide/gifencoder/AnimatedGifEncoder.java new file mode 100644 index 00000000..e7d40ee1 --- /dev/null +++ b/common/src/main/java/com/bumptech/glide/gifencoder/AnimatedGifEncoder.java @@ -0,0 +1,557 @@ +package com.bumptech.glide.gifencoder; + + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class AnimatedGifEncoder { + private static final String TAG = "AnimatedGifEncoder"; + + // The minimum % of an images pixels that must be transparent for us to set a transparent index + // automatically. + private static final double MIN_TRANSPARENT_PERCENTAGE = 4d; + + private int width; // image size + + private int height; + + private int fixedWidth; // set by setSize() + + private int fixedHeight; + + private Integer transparent = null; // transparent color if given + + private int transIndex; // transparent index in color table + + private int repeat = -1; // no repeat + + private int delay = 0; // frame delay (hundredths) + + private boolean started = false; // ready to output frames + + private OutputStream out; + + private Bitmap image; // current frame + + private byte[] pixels; // BGR byte array from frame + + private byte[] indexedPixels; // converted frame indexed to palette + + private int colorDepth; // number of bit planes + + private byte[] colorTab; // RGB palette + + private boolean[] usedEntry = new boolean[256]; // active palette entries + + private int palSize = 7; // color table size (bits-1) + + private int dispose = -1; // disposal code (-1 = use default) + + private boolean closeStream = false; // close stream when finished + + private boolean firstFrame = true; + + private boolean sizeSet = false; // if false, get size from first frame + + private int sample = 10; // default sample interval for quantizer + + private boolean hasTransparentPixels; + + /** + * Sets the delay time between each frame, or changes it for subsequent frames + * (applies to last frame added). + * + * @param ms + * int delay time in milliseconds + */ + public void setDelay(int ms) { + delay = Math.round(ms / 10.0f); + } + + /** + * Sets the GIF frame disposal code for the last added frame and any + * subsequent frames. Default is 0 if no transparent color has been set, + * otherwise 2. + * + * @param code + * int disposal code. + */ + public void setDispose(int code) { + if (code >= 0) { + dispose = code; + } + } + + /** + * Sets the number of times the set of GIF frames should be played. Default is + * 1; 0 means play indefinitely. Must be invoked before the first image is + * added. + * + * @param iter + * int number of iterations. + */ + public void setRepeat(int iter) { + if (iter >= 0) { + repeat = iter; + } + } + + /** + * Sets the transparent color for the last added frame and any subsequent + * frames. Since all colors are subject to modification in the quantization + * process, the color in the final palette for each frame closest to the given + * color becomes the transparent color for that frame. May be set to null to + * indicate no transparent color. + * + * @param color + * Color to be treated as transparent on display. + */ + public void setTransparent(int color) { + transparent = color; + } + + /** + * Adds next GIF frame. The frame is not written immediately, but is actually + * deferred until the next frame is received so that timing data can be + * inserted. Invoking finish() flushes all frames. If + * setSize was invoked, the size is used for all subsequent frames. + * Otherwise, the actual size of the image is used for each frames. + * + * @param im + * BufferedImage containing frame to write. + * @return true if successful. + */ + public boolean addFrame(@Nullable Bitmap im) { + return addFrame(im, 0, 0); + } + + /** + * Adds next GIF frame to the specified position. The frame is not written immediately, but is + * actually deferred until the next frame is received so that timing data can be inserted. + * Invoking finish() flushes all frames. If setSize was invoked, the + * size is used for all subsequent frames. Otherwise, the actual size of the image is used for + * each frame. + * + * See page 11 of http://giflib.sourceforge.net/gif89.txt for the position of the frame + * + * @param im + * BufferedImage containing frame to write. + * @param x + * Column number, in pixels, of the left edge of the image, with respect to the left + * edge of the Logical Screen. + * @param y + * Row number, in pixels, of the top edge of the image with respect to the top edge of + * the Logical Screen. + * @return true if successful. + */ + public boolean addFrame(@Nullable Bitmap im, int x, int y) { + if ((im == null) || !started) { + return false; + } + boolean ok = true; + try { + if (sizeSet) { + setFrameSize(fixedWidth, fixedHeight); + } else { + setFrameSize(im.getWidth(), im.getHeight()); + } + image = im; + getImagePixels(); // convert to correct format if necessary + analyzePixels(); // build color table & map pixels + if (firstFrame) { + writeLSD(); // logical screen descriptor + writePalette(); // global color table + if (repeat >= 0) { + // use NS app extension to indicate reps + writeNetscapeExt(); + } + } + writeGraphicCtrlExt(); // write graphic control extension + writeImageDesc(x, y); // image descriptor + if (!firstFrame) { + writePalette(); // local color table + } + writePixels(); // encode and write pixel data + firstFrame = false; + } catch (IOException e) { + ok = false; + } + + return ok; + } + + /** + * Flushes any pending data and closes output file. If writing to an + * OutputStream, the stream is not closed. + */ + public boolean finish() { + if (!started) + return false; + boolean ok = true; + started = false; + try { + out.write(0x3b); // GIF trailer + out.flush(); + if (closeStream) { + out.close(); + } + } catch (IOException e) { + ok = false; + } + + // reset for subsequent use + transIndex = 0; + out = null; + image = null; + pixels = null; + indexedPixels = null; + colorTab = null; + closeStream = false; + firstFrame = true; + + return ok; + } + + /** + * Sets frame rate in frames per second. Equivalent to + * setDelay(1000/fps). + * + * @param fps + * float frame rate (frames per second) + */ + public void setFrameRate(float fps) { + if (fps != 0f) { + delay = Math.round(100f / fps); + } + } + + /** + * Sets quality of color quantization (conversion of images to the maximum 256 + * colors allowed by the GIF specification). Lower values (minimum = 1) + * produce better colors, but slow processing significantly. 10 is the + * default, and produces good color mapping at reasonable speeds. Values + * greater than 20 do not yield significant improvements in speed. + * + * @param quality int greater than 0. + */ + public void setQuality(int quality) { + if (quality < 1) + quality = 1; + sample = quality; + } + + /** + * Sets the fixed GIF frame size for all the frames. + * This should be called before start. + * + * @param w + * int frame width. + * @param h + * int frame width. + */ + public void setSize(int w, int h) { + if (started) { + return; + } + + fixedWidth = w; + fixedHeight = h; + if (fixedWidth < 1) { + fixedWidth = 320; + } + if (fixedHeight < 1) { + fixedHeight = 240; + } + + sizeSet = true; + } + + /** + * Sets current GIF frame size. + * + * @param w + * int frame width. + * @param h + * int frame width. + */ + private void setFrameSize(int w, int h) { + width = w; + height = h; + } + + /** + * Initiates GIF file creation on the given stream. The stream is not closed + * automatically. + * + * @param os + * OutputStream on which GIF images are written. + * @return false if initial write failed. + */ + public boolean start(@Nullable OutputStream os) { + if (os == null) + return false; + boolean ok = true; + closeStream = false; + out = os; + try { + writeString("GIF89a"); // header + } catch (IOException e) { + ok = false; + } + return started = ok; + } + + /** + * Initiates writing of a GIF file with the specified name. + * + * @param file + * String containing output file name. + * @return false if open or initial write failed. + */ + public boolean start(@NonNull String file) { + boolean ok; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + ok = start(out); + closeStream = true; + } catch (IOException e) { + ok = false; + } + return started = ok; + } + + /** + * Analyzes image colors and creates color map. + */ + private void analyzePixels() { + int len = pixels.length; + int nPix = len / 3; + indexedPixels = new byte[nPix]; + NeuQuant nq = new NeuQuant(pixels, len, sample); + // initialize quantizer + colorTab = nq.process(); // create reduced palette + // convert map from BGR to RGB + for (int i = 0; i < colorTab.length; i += 3) { + byte temp = colorTab[i]; + colorTab[i] = colorTab[i + 2]; + colorTab[i + 2] = temp; + usedEntry[i / 3] = false; + } + // map image pixels to new palette + int k = 0; + for (int i = 0; i < nPix; i++) { + int index = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff); + usedEntry[index] = true; + indexedPixels[i] = (byte) index; + } + pixels = null; + colorDepth = 8; + palSize = 7; + // get closest match to transparent color if specified + if (transparent != null) { + transIndex = findClosest(transparent); + } else if (hasTransparentPixels) { + transIndex = findClosest(Color.TRANSPARENT); + } + } + + /** + * Returns index of palette color closest to c + * + */ + private int findClosest(int color) { + if (colorTab == null) + return -1; + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + int minpos = 0; + int dmin = 256 * 256 * 256; + int len = colorTab.length; + for (int i = 0; i < len;) { + int dr = r - (colorTab[i++] & 0xff); + int dg = g - (colorTab[i++] & 0xff); + int db = b - (colorTab[i] & 0xff); + int d = dr * dr + dg * dg + db * db; + int index = i / 3; + if (usedEntry[index] && (d < dmin)) { + dmin = d; + minpos = index; + } + i++; + } + return minpos; + } + + /** + * Extracts image pixels into byte array "pixels" + */ + private void getImagePixels() { + int w = image.getWidth(); + int h = image.getHeight(); + + if ((w != width) || (h != height)) { + // create new image with right size/format + Bitmap temp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(temp); + canvas.drawBitmap(temp, 0, 0, null); + image = temp; + } + int[] pixelsInt = new int[w * h]; + image.getPixels(pixelsInt, 0, w, 0, 0, w, h); + + // The algorithm requires 3 bytes per pixel as RGB. + pixels = new byte[pixelsInt.length * 3]; + + int pixelsIndex = 0; + hasTransparentPixels = false; + int totalTransparentPixels = 0; + for (final int pixel : pixelsInt) { + if (pixel == Color.TRANSPARENT) { + totalTransparentPixels++; + } + pixels[pixelsIndex++] = (byte) (pixel & 0xFF); + pixels[pixelsIndex++] = (byte) ((pixel >> 8) & 0xFF); + pixels[pixelsIndex++] = (byte) ((pixel >> 16) & 0xFF); + } + + double transparentPercentage = 100 * totalTransparentPixels / (double) pixelsInt.length; + // Assume images with greater where more than n% of the pixels are transparent actually have + // transparency. See issue #214. + hasTransparentPixels = transparentPercentage > MIN_TRANSPARENT_PERCENTAGE; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "got pixels for frame with " + transparentPercentage + + "% transparent pixels"); + } + } + + /** + * Writes Graphic Control Extension + */ + private void writeGraphicCtrlExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xf9); // GCE label + out.write(4); // data block size + int transp, disp; + if (transparent == null && !hasTransparentPixels) { + transp = 0; + disp = 0; // dispose = no action + } else { + transp = 1; + disp = 2; // force clear if using transparent color + } + if (dispose >= 0) { + disp = dispose & 7; // user override + } + disp <<= 2; + + // packed fields + out.write(0 | // 1:3 reserved + disp | // 4:6 disposal + 0 | // 7 user input - 0 = none + transp); // 8 transparency flag + + writeShort(delay); // delay x 1/100 sec + out.write(transIndex); // transparent color index + out.write(0); // block terminator + } + + /** + * Writes Image Descriptor + */ + private void writeImageDesc(int x, int y) throws IOException { + out.write(0x2c); // image separator + writeShort(x); // image position + writeShort(y); + writeShort(width); // image size + writeShort(height); + // packed fields + if (firstFrame) { + // no LCT - GCT is used for first (or only) frame + out.write(0); + } else { + // specify normal LCT + out.write(0x80 | // 1 local color table 1=yes + 0 | // 2 interlace - 0=no + 0 | // 3 sorted - 0=no + 0 | // 4-5 reserved + palSize); // 6-8 size of color table + } + } + + /** + * Writes Logical Screen Descriptor + */ + private void writeLSD() throws IOException { + // logical screen size + writeShort(width); + writeShort(height); + // packed fields + out.write((0x80 | // 1 : global color table flag = 1 (gct used) + 0x70 | // 2-4 : color resolution = 7 + 0x00 | // 5 : gct sort flag = 0 + palSize)); // 6-8 : gct size + + out.write(0); // background color index + out.write(0); // pixel aspect ratio - assume 1:1 + } + + /** + * Writes Netscape application extension to define repeat count. + */ + private void writeNetscapeExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xff); // app extension label + out.write(11); // block size + writeString("NETSCAPE" + "2.0"); // app id + auth code + out.write(3); // sub-block size + out.write(1); // loop sub-block id + writeShort(repeat); // loop count (extra iterations, 0=repeat forever) + out.write(0); // block terminator + } + + /** + * Writes color table + */ + private void writePalette() throws IOException { + out.write(colorTab, 0, colorTab.length); + int n = (3 * 256) - colorTab.length; + for (int i = 0; i < n; i++) { + out.write(0); + } + } + + /** + * Encodes and writes pixel data + */ + private void writePixels() throws IOException { + LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth); + encoder.encode(out); + } + + /** + * Write 16-bit value to output stream, LSB first + */ + private void writeShort(int value) throws IOException { + out.write(value & 0xff); + out.write((value >> 8) & 0xff); + } + + /** + * Writes string to output stream + */ + private void writeString(String s) throws IOException { + for (int i = 0; i < s.length(); i++) { + out.write((byte) s.charAt(i)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/bumptech/glide/gifencoder/LZWEncoder.java b/common/src/main/java/com/bumptech/glide/gifencoder/LZWEncoder.java new file mode 100644 index 00000000..addd103d --- /dev/null +++ b/common/src/main/java/com/bumptech/glide/gifencoder/LZWEncoder.java @@ -0,0 +1,296 @@ +package com.bumptech.glide.gifencoder; + +import java.io.IOException; +import java.io.OutputStream; + +// ============================================================================== +// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. +// K Weiner 12/00 +class LZWEncoder { + + private static final int EOF = -1; + + private int imgW, imgH; + + private byte[] pixAry; + + private int initCodeSize; + + private int remaining; + + private int curPixel; + + // GIFCOMPR.C - GIF Image compression routines + // + // Lempel-Ziv compression based on 'compress'. GIF modifications by + // David Rowley (mgardi@watdcsu.waterloo.edu) + + // General DEFINEs + + static final int BITS = 12; + + static final int HSIZE = 5003; // 80% occupancy + + // GIF Image compression - modified 'compress' + // + // Based on: compress.c - File compression ala IEEE Computer, June 1984. + // + // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + // Jim McKie (decvax!mcvax!jim) + // Steve Davies (decvax!vax135!petsd!peora!srd) + // Ken Turkowski (decvax!decwrl!turtlevax!ken) + // James A. Woods (decvax!ihnp4!ames!jaw) + // Joe Orost (decvax!vax135!petsd!joe) + + int n_bits; // number of bits/code + + int maxbits = BITS; // user settable max # bits/code + + int maxcode; // maximum code, given n_bits + + int maxmaxcode = 1 << BITS; // should NEVER generate this code + + int[] htab = new int[HSIZE]; + + int[] codetab = new int[HSIZE]; + + int hsize = HSIZE; // for dynamic table sizing + + int free_ent = 0; // first unused entry + + // block compression parameters -- after all codes are used up, + // and compression rate changes, start over. + boolean clear_flg = false; + + // Algorithm: use open addressing double hashing (no chaining) on the + // prefix code / next character combination. We do a variant of Knuth's + // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + // secondary probe. Here, the modular division first probe is gives way + // to a faster exclusive-or manipulation. Also do block compression with + // an adaptive reset, whereby the code table is cleared when the compression + // ratio decreases, but after the table fills. The variable-length output + // codes are re-sized at this point, and a special CLEAR code is generated + // for the decompressor. Late addition: construct the table according to + // file size for noticeable speed improvement on small files. Please direct + // questions about this implementation to ames!jaw. + + int g_init_bits; + + int ClearCode; + + int EOFCode; + + // output + // + // Output the given code. + // Inputs: + // code: A n_bits-bit integer. If == -1, then EOF. This assumes + // that n_bits =< wordsize - 1. + // Outputs: + // Outputs code to the file. + // Assumptions: + // Chars are 8 bits long. + // Algorithm: + // Maintain a BITS character long buffer (so that 8 codes will + // fit in it exactly). Use the VAX insv instruction to insert each + // code in turn. When the buffer fills up empty it and start over. + + int cur_accum = 0; + + int cur_bits = 0; + + int masks[] = {0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, + 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF}; + + // Number of characters so far in this 'packet' + int a_count; + + // Define the storage for the packet accumulator + byte[] accum = new byte[256]; + + // ---------------------------------------------------------------------------- + LZWEncoder(int width, int height, byte[] pixels, int color_depth) { + imgW = width; + imgH = height; + pixAry = pixels; + initCodeSize = Math.max(2, color_depth); + } + + // Add a character to the end of the current packet, and if it is 254 + // characters, flush the packet to disk. + void char_out(byte c, OutputStream outs) throws IOException { + accum[a_count++] = c; + if (a_count >= 254) + flush_char(outs); + } + + // Clear out the hash table + + // table clear for block compress + void cl_block(OutputStream outs) throws IOException { + cl_hash(hsize); + free_ent = ClearCode + 2; + clear_flg = true; + + output(ClearCode, outs); + } + + // reset code table + void cl_hash(int hsize) { + for (int i = 0; i < hsize; ++i) + htab[i] = -1; + } + + void compress(int init_bits, OutputStream outs) throws IOException { + int fcode; + int i /* = 0 */; + int c; + int ent; + int disp; + int hsize_reg; + int hshift; + + // Set up the globals: g_init_bits - initial number of bits + g_init_bits = init_bits; + + // Set up the necessary values + clear_flg = false; + n_bits = g_init_bits; + maxcode = MAXCODE(n_bits); + + ClearCode = 1 << (init_bits - 1); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + a_count = 0; // clear packet + + ent = nextPixel(); + + hshift = 0; + for (fcode = hsize; fcode < 65536; fcode *= 2) + ++hshift; + hshift = 8 - hshift; // set hash code range bound + + hsize_reg = hsize; + cl_hash(hsize_reg); // clear hash table + + output(ClearCode, outs); + + outer_loop: + while ((c = nextPixel()) != EOF) { + fcode = (c << maxbits) + ent; + i = (c << hshift) ^ ent; // xor hashing + + if (htab[i] == fcode) { + ent = codetab[i]; + continue; + } else if (htab[i] >= 0) // non-empty slot + { + disp = hsize_reg - i; // secondary hash (after G. Knott) + if (i == 0) + disp = 1; + do { + if ((i -= disp) < 0) + i += hsize_reg; + + if (htab[i] == fcode) { + ent = codetab[i]; + continue outer_loop; + } + } while (htab[i] >= 0); + } + output(ent, outs); + ent = c; + if (free_ent < maxmaxcode) { + codetab[i] = free_ent++; // code -> hashtable + htab[i] = fcode; + } else + cl_block(outs); + } + // Put out the final code. + output(ent, outs); + output(EOFCode, outs); + } + + // ---------------------------------------------------------------------------- + void encode(OutputStream os) throws IOException { + os.write(initCodeSize); // write "initial code size" byte + + remaining = imgW * imgH; // reset navigation variables + curPixel = 0; + + compress(initCodeSize + 1, os); // compress and write the pixel data + + os.write(0); // write block terminator + } + + // Flush the packet to disk, and reset the accumulator + void flush_char(OutputStream outs) throws IOException { + if (a_count > 0) { + outs.write(a_count); + outs.write(accum, 0, a_count); + a_count = 0; + } + } + + final int MAXCODE(int n_bits) { + return (1 << n_bits) - 1; + } + + // ---------------------------------------------------------------------------- + // Return the next pixel from the image + // ---------------------------------------------------------------------------- + private int nextPixel() { + if (remaining == 0) + return EOF; + + --remaining; + + byte pix = pixAry[curPixel++]; + + return pix & 0xff; + } + + void output(int code, OutputStream outs) throws IOException { + cur_accum &= masks[cur_bits]; + + if (cur_bits > 0) + cur_accum |= (code << cur_bits); + else + cur_accum = code; + + cur_bits += n_bits; + + while (cur_bits >= 8) { + char_out((byte) (cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if (free_ent > maxcode || clear_flg) { + if (clear_flg) { + maxcode = MAXCODE(n_bits = g_init_bits); + clear_flg = false; + } else { + ++n_bits; + if (n_bits == maxbits) + maxcode = maxmaxcode; + else + maxcode = MAXCODE(n_bits); + } + } + + if (code == EOFCode) { + // At EOF, write the rest of the buffer. + while (cur_bits > 0) { + char_out((byte) (cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + flush_char(outs); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/bumptech/glide/gifencoder/NeuQuant.java b/common/src/main/java/com/bumptech/glide/gifencoder/NeuQuant.java new file mode 100644 index 00000000..368a930b --- /dev/null +++ b/common/src/main/java/com/bumptech/glide/gifencoder/NeuQuant.java @@ -0,0 +1,485 @@ +package com.bumptech.glide.gifencoder; + +// Ported to Java 12/00 K Weiner +class NeuQuant { + + protected static final int netsize = 256; /* number of colours used */ + + /* four primes near 500 - assume no image has a length so large */ + /* that it is divisible by all four primes */ + protected static final int prime1 = 499; + + protected static final int prime2 = 491; + + protected static final int prime3 = 487; + + protected static final int prime4 = 503; + + protected static final int minpicturebytes = (3 * prime4); + + /* minimum size for input image */ + + /* + * Program Skeleton ---------------- [select samplefac in range 1..30] [read + * image from input file] pic = (unsigned char*) malloc(3*width*height); + * initnet(pic,3*width*height,samplefac); learn(); unbiasnet(); [write output + * image header, using writecolourmap(f)] inxbuild(); write output image using + * inxsearch(b,g,r) + */ + + /* + * Network Definitions ------------------- + */ + + protected static final int maxnetpos = (netsize - 1); + + protected static final int netbiasshift = 4; /* bias for colour values */ + + protected static final int ncycles = 100; /* no. of learning cycles */ + + /* defs for freq and bias */ + protected static final int intbiasshift = 16; /* bias for fractions */ + + protected static final int intbias = (((int) 1) << intbiasshift); + + protected static final int gammashift = 10; /* gamma = 1024 */ + + protected static final int gamma = (((int) 1) << gammashift); + + protected static final int betashift = 10; + + protected static final int beta = (intbias >> betashift); /* beta = 1/1024 */ + + protected static final int betagamma = (intbias << (gammashift - betashift)); + + /* defs for decreasing radius factor */ + protected static final int initrad = (netsize >> 3); /* + * for 256 cols, radius + * starts + */ + + protected static final int radiusbiasshift = 6; /* at 32.0 biased by 6 bits */ + + protected static final int radiusbias = (((int) 1) << radiusbiasshift); + + protected static final int initradius = (initrad * radiusbias); /* + * and + * decreases + * by a + */ + + protected static final int radiusdec = 30; /* factor of 1/30 each cycle */ + + /* defs for decreasing alpha factor */ + protected static final int alphabiasshift = 10; /* alpha starts at 1.0 */ + + protected static final int initalpha = (((int) 1) << alphabiasshift); + + protected int alphadec; /* biased by 10 bits */ + + /* radbias and alpharadbias used for radpower calculation */ + protected static final int radbiasshift = 8; + + protected static final int radbias = (((int) 1) << radbiasshift); + + protected static final int alpharadbshift = (alphabiasshift + radbiasshift); + + protected static final int alpharadbias = (((int) 1) << alpharadbshift); + + /* + * Types and Global Variables -------------------------- + */ + + protected byte[] thepicture; /* the input image itself */ + + protected int lengthcount; /* lengthcount = H*W*3 */ + + protected int samplefac; /* sampling factor 1..30 */ + + // typedef int pixel[4]; /* BGRc */ + protected int[][] network; /* the network itself - [netsize][4] */ + + protected int[] netindex = new int[256]; + + /* for network lookup - really 256 */ + + protected int[] bias = new int[netsize]; + + /* bias and freq arrays for learning */ + protected int[] freq = new int[netsize]; + + protected int[] radpower = new int[initrad]; + + /* radpower for precomputation */ + + /* + * Initialise network in range (0,0,0) to (255,255,255) and set parameters + * ----------------------------------------------------------------------- + */ + public NeuQuant(byte[] thepic, int len, int sample) { + + int i; + int[] p; + + thepicture = thepic; + lengthcount = len; + samplefac = sample; + + network = new int[netsize][]; + for (i = 0; i < netsize; i++) { + network[i] = new int[4]; + p = network[i]; + p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize; + freq[i] = intbias / netsize; /* 1/netsize */ + bias[i] = 0; + } + } + + public byte[] colorMap() { + byte[] map = new byte[3 * netsize]; + int[] index = new int[netsize]; + for (int i = 0; i < netsize; i++) + index[network[i][3]] = i; + int k = 0; + for (int i = 0; i < netsize; i++) { + int j = index[i]; + map[k++] = (byte) (network[j][0]); + map[k++] = (byte) (network[j][1]); + map[k++] = (byte) (network[j][2]); + } + return map; + } + + /* + * Insertion sort of network and building of netindex[0..255] (to do after + * unbias) + * ------------------------------------------------------------------------------- + */ + public void inxbuild() { + + int i, j, smallpos, smallval; + int[] p; + int[] q; + int previouscol, startpos; + + previouscol = 0; + startpos = 0; + for (i = 0; i < netsize; i++) { + p = network[i]; + smallpos = i; + smallval = p[1]; /* index on g */ + /* find smallest in i..netsize-1 */ + for (j = i + 1; j < netsize; j++) { + q = network[j]; + if (q[1] < smallval) { /* index on g */ + smallpos = j; + smallval = q[1]; /* index on g */ + } + } + q = network[smallpos]; + /* swap p (i) and q (smallpos) entries */ + if (i != smallpos) { + j = q[0]; + q[0] = p[0]; + p[0] = j; + j = q[1]; + q[1] = p[1]; + p[1] = j; + j = q[2]; + q[2] = p[2]; + p[2] = j; + j = q[3]; + q[3] = p[3]; + p[3] = j; + } + /* smallval entry is now in position i */ + if (smallval != previouscol) { + netindex[previouscol] = (startpos + i) >> 1; + for (j = previouscol + 1; j < smallval; j++) + netindex[j] = i; + previouscol = smallval; + startpos = i; + } + } + netindex[previouscol] = (startpos + maxnetpos) >> 1; + for (j = previouscol + 1; j < 256; j++) + netindex[j] = maxnetpos; /* really 256 */ + } + + /* + * Main Learning Loop ------------------ + */ + public void learn() { + + int i, j, b, g, r; + int radius, rad, alpha, step, delta, samplepixels; + byte[] p; + int pix, lim; + + if (lengthcount < minpicturebytes) + samplefac = 1; + alphadec = 30 + ((samplefac - 1) / 3); + p = thepicture; + pix = 0; + lim = lengthcount; + samplepixels = lengthcount / (3 * samplefac); + delta = samplepixels / ncycles; + alpha = initalpha; + radius = initradius; + + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (i = 0; i < rad; i++) + radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); + + // fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad); + + if (lengthcount < minpicturebytes) + step = 3; + else if ((lengthcount % prime1) != 0) + step = 3 * prime1; + else { + if ((lengthcount % prime2) != 0) + step = 3 * prime2; + else { + if ((lengthcount % prime3) != 0) + step = 3 * prime3; + else + step = 3 * prime4; + } + } + + i = 0; + while (i < samplepixels) { + b = (p[pix + 0] & 0xff) << netbiasshift; + g = (p[pix + 1] & 0xff) << netbiasshift; + r = (p[pix + 2] & 0xff) << netbiasshift; + j = contest(b, g, r); + + altersingle(alpha, j, b, g, r); + if (rad != 0) + alterneigh(rad, j, b, g, r); /* alter neighbours */ + + pix += step; + if (pix >= lim) + pix -= lengthcount; + + i++; + if (delta == 0) + delta = 1; + if (i % delta == 0) { + alpha -= alpha / alphadec; + radius -= radius / radiusdec; + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (j = 0; j < rad; j++) + radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); + } + } + // fprintf(stderr,"finished 1D learning: final alpha=%f + // !\n",((float)alpha)/initalpha); + } + + /* + * Search for BGR values 0..255 (after net is unbiased) and return colour + * index + * ---------------------------------------------------------------------------- + */ + public int map(int b, int g, int r) { + + int i, j, dist, a, bestd; + int[] p; + int best; + + bestd = 1000; /* biggest possible dist is 256*3 */ + best = -1; + i = netindex[g]; /* index on g */ + j = i - 1; /* start at netindex[g] and work outwards */ + + while ((i < netsize) || (j >= 0)) { + if (i < netsize) { + p = network[i]; + dist = p[1] - g; /* inx key */ + if (dist >= bestd) + i = netsize; /* stop iter */ + else { + i++; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + if (j >= 0) { + p = network[j]; + dist = g - p[1]; /* inx key - reverse dif */ + if (dist >= bestd) + j = -1; /* stop iter */ + else { + j--; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + } + return (best); + } + + public byte[] process() { + learn(); + unbiasnet(); + inxbuild(); + return colorMap(); + } + + /* + * Unbias network to give byte values 0..255 and record position i to prepare + * for sort + * ----------------------------------------------------------------------------------- + */ + public void unbiasnet() { + + int i, j; + + for (i = 0; i < netsize; i++) { + network[i][0] >>= netbiasshift; + network[i][1] >>= netbiasshift; + network[i][2] >>= netbiasshift; + network[i][3] = i; /* record colour no */ + } + } + + /* + * Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in + * radpower[|i-j|] + * --------------------------------------------------------------------------------- + */ + protected void alterneigh(int rad, int i, int b, int g, int r) { + + int j, k, lo, hi, a, m; + int[] p; + + lo = i - rad; + if (lo < -1) + lo = -1; + hi = i + rad; + if (hi > netsize) + hi = netsize; + + j = i + 1; + k = i - 1; + m = 1; + while ((j < hi) || (k > lo)) { + a = radpower[m++]; + if (j < hi) { + p = network[j++]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) { + } // prevents 1.3 miscompilation + } + if (k > lo) { + p = network[k--]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) { + } + } + } + } + + /* + * Move neuron i towards biased (b,g,r) by factor alpha + * ---------------------------------------------------- + */ + protected void altersingle(int alpha, int i, int b, int g, int r) { + + /* alter hit neuron */ + int[] n = network[i]; + n[0] -= (alpha * (n[0] - b)) / initalpha; + n[1] -= (alpha * (n[1] - g)) / initalpha; + n[2] -= (alpha * (n[2] - r)) / initalpha; + } + + /* + * Search for biased BGR values ---------------------------- + */ + protected int contest(int b, int g, int r) { + + /* finds closest neuron (min dist) and updates freq */ + /* finds best neuron (min dist-bias) and returns position */ + /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ + /* bias[i] = gamma*((1/netsize)-freq[i]) */ + + int i, dist, a, biasdist, betafreq; + int bestpos, bestbiaspos, bestd, bestbiasd; + int[] n; + + bestd = ~(((int) 1) << 31); + bestbiasd = bestd; + bestpos = -1; + bestbiaspos = bestpos; + + for (i = 0; i < netsize; i++) { + n = network[i]; + dist = n[0] - b; + if (dist < 0) + dist = -dist; + a = n[1] - g; + if (a < 0) + a = -a; + dist += a; + a = n[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + bestpos = i; + } + biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); + if (biasdist < bestbiasd) { + bestbiasd = biasdist; + bestbiaspos = i; + } + betafreq = (freq[i] >> betashift); + freq[i] -= betafreq; + bias[i] += (betafreq << gammashift); + } + freq[bestpos] += beta; + bias[bestpos] -= betagamma; + return (bestbiaspos); + } +} \ No newline at end of file diff --git a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt index bb907526..a62ac730 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt @@ -24,7 +24,6 @@ import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.content.Intent -import android.graphics.Bitmap import android.os.Build import android.provider.Telephony import android.telephony.PhoneNumberUtils @@ -42,6 +41,7 @@ import com.moez.QKSMS.model.Attachment import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message import com.moez.QKSMS.model.MmsPart +import com.moez.QKSMS.util.ImageUtils import com.moez.QKSMS.util.Preferences import com.moez.QKSMS.util.tryOrNull import io.realm.Case @@ -49,7 +49,6 @@ import io.realm.Realm import io.realm.RealmResults import io.realm.Sort import timber.log.Timber -import java.io.ByteArrayOutputStream import javax.inject.Inject import javax.inject.Singleton @@ -234,9 +233,8 @@ class MessageRepositoryImpl @Inject constructor( // TODO Add support for GIF compression parts += attachments .filter { attachment -> attachment.isGif(context) } - .map { attachment -> attachment.getUri() } - .map { uri -> context.contentResolver.openInputStream(uri) } - .map { inputStream -> inputStream.readBytes() } + .mapNotNull { attachment -> attachment.getUri() } + .map { uri -> ImageUtils.compressGif(context, uri, prefs.mmsSize.get() * 1024) } .map { bitmap -> MMSPart("image", ContentType.IMAGE_GIF, bitmap) } // Compress the images and add them as attachments @@ -248,7 +246,7 @@ class MessageRepositoryImpl @Inject constructor( .also { totalImageBytes = it.sumBy { it.allocationByteCount } } .map { bitmap -> val byteRatio = bitmap.allocationByteCount / totalImageBytes.toFloat() - compress(bitmap, (prefs.mmsSize.get() * 1024 * byteRatio).toInt()) + ImageUtils.compressBitmap(bitmap, (prefs.mmsSize.get() * 1024 * byteRatio).toInt()) } .map { bitmap -> MMSPart("image", ContentType.IMAGE_JPEG, bitmap) } @@ -284,38 +282,6 @@ class MessageRepositoryImpl @Inject constructor( smsManager.sendMultipartTextMessage(message.address, null, parts, ArrayList(sentIntents), ArrayList(deliveredIntents)) } - private fun compress(src: Bitmap, maxBytes: Int): ByteArray { - val quality = 90 - - val height = src.height - val width = src.width - - val stream = ByteArrayOutputStream() - src.compress(Bitmap.CompressFormat.JPEG, quality, stream) - - // Based on the byte size of the bitmap, we'll try to reduce the image's dimensions such - // that it will fit within the max byte size set. If we don't get it right the first time, - // use a slightly heavier compression until we fit within the max size - var attempts = 0 - var bytes = stream.size().toDouble() - while (maxBytes > 0 && bytes > maxBytes) { - val scale = Math.sqrt(maxBytes / bytes) * (1 - attempts * 0.1) - - stream.reset() - Bitmap.createScaledBitmap(src, (width * scale).toInt(), (height * scale).toInt(), true) - .compress(Bitmap.CompressFormat.JPEG, quality, stream) - - attempts++ - bytes = stream.size().toDouble() - } - - if (maxBytes > 0 && stream.size() > maxBytes) { - Timber.w(Exception("Compressed image size exceeds max byte size (${stream.size()}/$maxBytes) after $attempts attempts")) - } - - return stream.toByteArray().also { src.recycle() } - } - override fun cancelDelayedSms(id: Long) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager alarmManager.cancel(getIntentForDelayedSms(id)) diff --git a/data/src/main/java/com/moez/QKSMS/util/GifEncoder.kt b/data/src/main/java/com/moez/QKSMS/util/GifEncoder.kt new file mode 100644 index 00000000..7ffc6acf --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/util/GifEncoder.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2017 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.util + +import android.content.Context +import android.graphics.Bitmap +import androidx.annotation.VisibleForTesting +import com.bumptech.glide.gifdecoder.GifDecoder +import com.bumptech.glide.gifdecoder.GifHeaderParser +import com.bumptech.glide.gifdecoder.StandardGifDecoder +import com.bumptech.glide.gifencoder.AnimatedGifEncoder +import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapResource +import com.bumptech.glide.load.resource.gif.GifBitmapProvider +import com.bumptech.glide.load.resource.gif.GifDrawable +import java.io.OutputStream +import java.nio.ByteBuffer + +/** + * Allows converting an animated [GifDrawable] to an [OutputStream] + * Based on Glide's ReEncodingGifResourceEncoder + */ +class GifEncoder internal constructor( + private val context: Context, + private val bitmapPool: BitmapPool, + private val factory: Factory = Factory() +) { + + private val provider = GifBitmapProvider(bitmapPool) + + fun encodeTransformedToStream(drawable: GifDrawable, os: OutputStream): Boolean { + val transformation = drawable.frameTransformation + val decoder = decodeHeaders(drawable.buffer) + val encoder = factory.buildEncoder() + if (!encoder.start(os)) { + return false + } + + for (i in 0 until decoder.frameCount) { + val currentFrame = decoder.nextFrame + val transformedResource = getTransformedFrame(currentFrame, transformation, drawable) + try { + if (!encoder.addFrame(transformedResource.get())) { + return false + } + val currentFrameIndex = decoder.currentFrameIndex + val delay = decoder.getDelay(currentFrameIndex) + encoder.setDelay(delay) + + decoder.advance() + } finally { + transformedResource.recycle() + } + } + + return encoder.finish() + } + + private fun decodeHeaders(data: ByteBuffer): GifDecoder { + val parser = factory.buildParser() + parser.setData(data) + val header = parser.parseHeader() + + val decoder = factory.buildDecoder(provider) + decoder.setData(header, data) + decoder.advance() + + return decoder + } + + private fun getTransformedFrame(currentFrame: Bitmap?, transformation: Transformation, drawable: GifDrawable): Resource { + // TODO: what if current frame is null? + val bitmapResource = factory.buildFrameResource(currentFrame!!, bitmapPool) + val transformedResource = transformation.transform(context, bitmapResource, drawable.intrinsicWidth, drawable.intrinsicHeight) + if (bitmapResource != transformedResource) { + bitmapResource.recycle() + } + return transformedResource + } + + @VisibleForTesting + internal class Factory { + fun buildDecoder(bitmapProvider: GifDecoder.BitmapProvider) = StandardGifDecoder(bitmapProvider) + + fun buildParser() = GifHeaderParser() + + fun buildEncoder() = AnimatedGifEncoder() + + fun buildFrameResource(bitmap: Bitmap, bitmapPool: BitmapPool) = BitmapResource(bitmap, bitmapPool) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt b/data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt new file mode 100644 index 00000000..82dc3e9d --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.util + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import java.io.ByteArrayOutputStream + + +object ImageUtils { + + fun compressGif(context: Context, uri: Uri, maxBytes: Int): ByteArray { + val request = GlideApp + .with(context) + .asGif() + .load(uri) + .transform(CenterCrop()) + + val gif = request.submit().get() + + val width = gif.firstFrame.width + val height = gif.firstFrame.height + + val stream = ByteArrayOutputStream() + GifEncoder(context, GlideApp.get(context).bitmapPool) + .encodeTransformedToStream(gif, stream) + + val unscaledBytes = stream.size().toDouble() + + var attempts = 0 + var bytes = unscaledBytes + while (maxBytes > 0 && bytes > maxBytes) { + val scale = Math.sqrt(maxBytes / unscaledBytes) * (1 - attempts * 0.1) + + val scaledGif = request.submit((width * scale).toInt(), (height * scale).toInt()).get() + + stream.reset() + GifEncoder(context, GlideApp.get(context).bitmapPool) + .encodeTransformedToStream(scaledGif, stream) + + attempts++ + bytes = stream.size().toDouble() + } + + return stream.toByteArray() + } + + fun compressBitmap(src: Bitmap, maxBytes: Int): ByteArray { + val quality = 90 + + val height = src.height + val width = src.width + + val stream = ByteArrayOutputStream() + src.compress(Bitmap.CompressFormat.JPEG, quality, stream) + + val unscaledBytes = stream.size().toDouble() + + // Based on the byte size of the bitmap, we'll try to reduce the image's dimensions such + // that it will fit within the max byte size set. If we don't get it right the first time, + // use a slightly heavier compression until we fit within the max size + var attempts = 0 + var bytes = unscaledBytes + while (maxBytes > 0 && bytes > maxBytes) { + val scale = Math.sqrt(maxBytes / unscaledBytes) * (1 - attempts * 0.1) + + stream.reset() + Bitmap.createScaledBitmap(src, (width * scale).toInt(), (height * scale).toInt(), true) + .compress(Bitmap.CompressFormat.JPEG, quality, stream) + + attempts++ + bytes = stream.size().toDouble() + } + + return stream.toByteArray().also { src.recycle() } + } + +} \ No newline at end of file