From b2c11c266d72620a20a706973fde7e62f11f4bdf Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 24 Aug 2020 18:20:15 +0200 Subject: [PATCH] Added GIF export --- .../handhistory/HandHistoryActivity.kt | 26 +- .../replayer/ReplayExportService.kt | 93 +- .../android/util/video/AnimatedGIFWriter.java | 1608 +++++++++++++++++ .../android/util/video/MMediaMuxer.kt | 150 +- app/src/main/res/values/strings.xml | 2 +- app/standard/release/output.json | 2 +- 6 files changed, 1786 insertions(+), 95 deletions(-) create mode 100644 app/src/main/java/net/pokeranalytics/android/util/video/AnimatedGIFWriter.java diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryActivity.kt index 769416a1..5b9a4d81 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/HandHistoryActivity.kt @@ -143,8 +143,8 @@ class HandHistoryActivity : BaseActivity() { builder.setItems( arrayOf( getString(R.string.text), - getString(R.string.video) -// "GIF" + getString(R.string.video), + "GIF" ) ) { _, index -> // The 'which' argument contains the index position @@ -152,13 +152,24 @@ class HandHistoryActivity : BaseActivity() { when (index) { 0 -> this.textExport() 1 -> this.videoExportAskForPermission() - 2 -> this.gifExport() + 2 -> this.gifExportAskForPermission() } } builder.create().show() } + private fun gifExportAskForPermission() { + + Toast.makeText(this, R.string.video_export_started, Toast.LENGTH_LONG).show() + + askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), RequestCode.PERMISSION_WRITE_EXTERNAL_STORAGE.value) { granted -> + if (granted) { + gifExport() + } + } + } + private fun videoExportAskForPermission() { Toast.makeText(this, R.string.video_export_started, Toast.LENGTH_LONG).show() @@ -171,16 +182,17 @@ class HandHistoryActivity : BaseActivity() { } private fun videoExport() { - val handHistoryId = this.handHistory.id - this.replayExportService?.export(handHistoryId) ?: run { + this.replayExportService?.videoExport(handHistoryId) ?: run { Toast.makeText(this, "Export service not available. Please contact support", Toast.LENGTH_LONG).show() } - } private fun gifExport() { - + val handHistoryId = this.handHistory.id + this.replayExportService?.gifExport(handHistoryId) ?: run { + Toast.makeText(this, "Export service not available. Please contact support", Toast.LENGTH_LONG).show() + } } private fun textExport() { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt index 8b357e4d..a03a6975 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt @@ -4,6 +4,7 @@ import android.app.PendingIntent import android.app.Service import android.content.Intent import android.os.Binder +import android.os.Environment import android.os.IBinder import androidx.core.content.FileProvider import io.realm.Realm @@ -16,10 +17,14 @@ import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.ui.extensions.toByteArray import net.pokeranalytics.android.util.TriggerNotification +import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted import net.pokeranalytics.android.util.extensions.findById +import net.pokeranalytics.android.util.video.AnimatedGIFWriter import net.pokeranalytics.android.util.video.MMediaMuxer import timber.log.Timber import java.io.File +import java.io.FileOutputStream +import java.util.* import kotlin.coroutines.CoroutineContext @@ -42,12 +47,17 @@ class ReplayExportService : Service() { } - fun export(handHistoryId: String) { + fun videoExport(handHistoryId: String) { this@ReplayExportService.handHistoryId = handHistoryId - startExport() + startVideoExport() } - private fun startExport() { + fun gifExport(handHistoryId: String) { + this@ReplayExportService.handHistoryId = handHistoryId + startGIFExport() + + } + private fun startGIFExport() { GlobalScope.launch(coroutineContext) { val c = GlobalScope.async { @@ -59,22 +69,80 @@ class ReplayExportService : Service() { val animator = ReplayerAnimator(handHistory, true) - val square = 1024f + val square = 1024 val width = square val height = square - animator.setDimension(width, height) + animator.setDimension(width.toFloat(), height.toFloat()) + TableDrawer.configurePaints(context, animator) + + val formattedDate = Date().dateTimeFileFormatted + val path = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), + "gif_${formattedDate}.gif" + ).toString() + + val writer = AnimatedGIFWriter(false) + val os = FileOutputStream(path) + writer.prepareForWrite(os, width, height) + + var animationCount = 0 + animator.frames(context) { bitmap, count -> + + when { + count > 10 -> { + writer.writeFrame(os, bitmap, count * 8) + animationCount = 0 + } + else -> { + if (animationCount % 2 == 0) { + writer.writeFrame(os, bitmap) + } + animationCount++ + } + } + + } + writer.finishWrite(os) + + realm.close() + notifyUser(path) + + } + c.await() + } + + } + + private fun startVideoExport() { + + GlobalScope.launch(coroutineContext) { + val c = GlobalScope.async { + + val realm = Realm.getDefaultInstance() + val handHistory = realm.findById(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId") + + val context = this@ReplayExportService + + val animator = ReplayerAnimator(handHistory, true) + + val square = 1024 + + val width = square + val height = square + + animator.setDimension(width.toFloat(), height.toFloat()) TableDrawer.configurePaints(context, animator) val muxer = MMediaMuxer() - muxer.Init(null, width.toInt(), height.toInt(), "hhVideo", "YES!") + muxer.init(null, width, height, "hhVideo", "YES!") animator.frames(context) { bitmap, count -> try { val byteArray = bitmap.toByteArray() - muxer.AddFrame(byteArray, count, false) + muxer.addFrame(byteArray, count, false) } catch (e: Exception) { Timber.e("error = ${e.message}") } @@ -83,7 +151,7 @@ class ReplayExportService : Service() { realm.close() - muxer.CreateVideo { path -> + muxer.createVideo { path -> notifyUser(path) } @@ -95,6 +163,8 @@ class ReplayExportService : Service() { private fun notifyUser(path: String) { + Timber.d("Show local notification") + val title = getString(R.string.video_available) val body = getString(R.string.video_retrieval_message) + ": " + path @@ -104,8 +174,13 @@ class ReplayExportService : Service() { File(path) ) + val type = when { + path.contains("gif") -> "image/gif" + else -> "video/*" + } + val intent = Intent(Intent.ACTION_VIEW) - intent.setDataAndType(uri, "video/*") + intent.setDataAndType(uri, type) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val chooser = Intent.createChooser(intent, getString(R.string.open_file_with)) diff --git a/app/src/main/java/net/pokeranalytics/android/util/video/AnimatedGIFWriter.java b/app/src/main/java/net/pokeranalytics/android/util/video/AnimatedGIFWriter.java new file mode 100644 index 00000000..e4ada07d --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/video/AnimatedGIFWriter.java @@ -0,0 +1,1608 @@ +package net.pokeranalytics.android.util.video; + +import android.graphics.Bitmap; +import android.graphics.Rect; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; + +public class AnimatedGIFWriter { + + // Fields + private int codeLen; + private int codeIndex; + private int clearCode; + private int endOfImage; + private int bufIndex; + private int empty_bits = 0x08; + + private int bitsPerPixel = 0x08; + + private byte bytes_buf[] = new byte[256]; + private int[] colorPalette; + private boolean isApplyDither; + + /** + * A child is made up of a parent(or prefix) code plus a suffix color + * and siblings are strings with a common parent(or prefix) and different + * suffix colors + */ + int child[] = new int[4097]; + + int siblings[] = new int[4097]; + int suffix[] = new int[4097]; + + private int logicalScreenWidth; + private int logicalScreenHeight; + + private boolean animated; + private int loopCount; + private boolean firstFrame = true; + + // Define constants + public static final byte IMAGE_SEPARATOR = 0x2c; // "," + public static final byte IMAGE_TRAILER = 0x3b; // ";" + public static final byte EXTENSION_INTRODUCER = 0x21; // "!" + public static final byte GRAPHIC_CONTROL_LABEL = (byte)0xf9; + public static final byte APPLICATION_EXTENSION_LABEL = (byte)0xff; + public static final byte COMMENT_EXTENSION_LABEL = (byte)0xfe; + public static final byte TEXT_EXTENSION_LABEL = 0x01; + + private static final int MASK[] = {0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff}; + + private static Rect getLogicalScreenSize(Bitmap[] images) { + // Determine the logical screen dimension assuming all the frames have the same + // left and top coordinates (0, 0) + int logicalScreenWidth = 0; + int logicalScreenHeight = 0; + + for(Bitmap image : images) { + if(image.getWidth() > logicalScreenWidth) + logicalScreenWidth = image.getWidth(); + if(image.getHeight() > logicalScreenHeight) + logicalScreenHeight = image.getHeight(); + } + + return new Rect(0, 0, logicalScreenWidth, logicalScreenHeight); + } + + private static Rect getLogicalScreenSize(GIFFrame[] frames) { + // Determine the logical screen dimension given all the frames with different + // left and top coordinates. + int logicalScreenWidth = 0; + int logicalScreenHeight = 0; + + for(GIFFrame frame : frames) { + int frameRightPosition = frame.getFrameWidth() + frame.getLeftPosition(); + int frameBottomPosition = frame.getFrameHeight() + frame.getTopPosition(); + if(frameRightPosition > logicalScreenWidth) + logicalScreenWidth = frameRightPosition; + if(frameBottomPosition > logicalScreenHeight) + logicalScreenHeight = frameBottomPosition; + } + + return new Rect(0, 0, logicalScreenWidth, logicalScreenHeight); + } + + public AnimatedGIFWriter() { this(false);} + + public AnimatedGIFWriter(boolean isApplyDither) { + this.isApplyDither = isApplyDither; + } + + // Write as a single frame GIF + public void write(Bitmap img, OutputStream os) throws Exception { + if(img == null) throw new NullPointerException("Input image is null"); + int imageWidth = img.getWidth(); + int imageHeight = img.getHeight(); + int pixels[] = new int[imageWidth * imageHeight]; + img.getPixels(pixels, 0, imageWidth, 0, 0, imageWidth, imageHeight); + write(pixels, imageWidth, imageHeight, os); + } + + private void encode(byte[] pixels, OutputStream os) throws Exception { + // Define local variables + int parent = 0; + int son = 0; + int brother = 0; + int color = 0; + int index = 0; + int dimension = pixels.length; + + // Write out the length of the root + os.write(bitsPerPixel = (bitsPerPixel == 1)?2:bitsPerPixel); + // Initialize the encoder + init_encoder(bitsPerPixel); + // Tell the decoder to initialize the string table + send_code_to_buffer(clearCode, os); + // Get the first color and assign it to parent + parent = (pixels[index++]&0xff); + + while (index < dimension) + { + color = (pixels[index++]&0xff); + son = child[parent]; + + if ( son > 0){ + if (suffix[son] == color) { + parent = son; + } else { + brother = son; + while (true) + { + if (siblings[brother] > 0) + { + brother = siblings[brother]; + if (suffix[brother] == color) + { + parent = brother; + break; + } + } else { + siblings[brother] = codeIndex; + suffix[codeIndex] = color; + send_code_to_buffer(parent,os); + parent = color; + codeIndex++; + // Check code length + if(codeIndex > ((1< ((1<>= empty_bits; + temp -= empty_bits; + // If the code is longer than the empty_bits + while (temp > 0) + { + if ( ++bufIndex >= 0xff) + { + flush_buf(os,0xff); + } + bytes_buf[bufIndex] |= (code&0xff); + code >>= 8; + temp -= 8; + } + empty_bits = -temp; + } + + public void setLoopCount(int loopCount) { + this.loopCount = loopCount; + } + + private void write(int[] pixels, int imageWidth, int imageHeight, OutputStream os) throws Exception { + // Write GIF header + writeHeader(os, true); + // Set logical screen size + logicalScreenWidth = imageWidth; + logicalScreenHeight = imageHeight; + firstFrame = true; + // We only need to write one frame, so disable animated flag + animated = false; + // Write the image frame + writeFrame(pixels, imageWidth, imageHeight, 0, 0, 0, os); + // Make a clean end up of the image + os.write(IMAGE_TRAILER); + os.close(); + } + + /** + * Writes an array of BufferedImage as an animated GIF + * + * @param images an array of BufferedImage + * @param delays delays in millisecond for each frame + * @param os OutputStream for the animated GIF + * @throws Exception + */ + public void writeAnimatedGIF(Bitmap[] images, int[] delays, OutputStream os) throws Exception { + // Header first + writeHeader(os, true); + + Rect logicalScreenSize = getLogicalScreenSize(images); + + logicalScreenWidth = logicalScreenSize.width(); + logicalScreenHeight = logicalScreenSize.height(); + // We are going to write animated GIF, so enable animated flag + animated = true; + + for(int i = 0; i < images.length; i++) { + // Retrieve image dimension + int imageWidth = images[i].getWidth(); + int imageHeight = images[i].getHeight(); + int[] pixels = new int[imageWidth * imageHeight]; + images[i].getPixels(pixels, 0, imageWidth, 0, 0, imageWidth, imageHeight); + writeFrame(pixels, imageWidth, imageHeight, 0, 0, delays[i], os); + } + + os.write(IMAGE_TRAILER); + os.close(); + } + + /** + * Writes an array of GIFFrame as an animated GIF + * + * @param frames an array of GIFFrame + * @param os OutputStream for the animated GIF + * @throws Exception + */ + public void writeAnimatedGIF(GIFFrame[] frames, OutputStream os) throws Exception { + // Header first + writeHeader(os, true); + + Rect logicalScreenSize = getLogicalScreenSize(frames); + + logicalScreenWidth = logicalScreenSize.width(); + logicalScreenHeight = logicalScreenSize.height(); + // We are going to write animated GIF, so enable animated flag + animated = true; + + for(int i = 0; i < frames.length; i++) { + // Retrieve image dimension + int imageWidth = frames[i].getFrameWidth(); + int imageHeight = frames[i].getFrameHeight(); + int[] pixels = new int[imageWidth * imageHeight]; + frames[i].getFrame().getPixels(pixels, 0, imageWidth, 0, 0, imageWidth, imageHeight); + if(frames[i].getTransparencyFlag() == GIFFrame.TRANSPARENCY_INDEX_SET && frames[i].getTransparentColor() != -1) { + int transColor = (frames[i].getTransparentColor() & 0x00ffffff); + for(int j = pixels.length - 1; j > 0; j--) { + int pixel = (pixels[j] & 0x00ffffff); + if(pixel == transColor) pixels[j] = pixel; + } + } + writeFrame(pixels, imageWidth, imageHeight, frames[i].getLeftPosition(), frames[i].getTopPosition(), + frames[i].getDelay(), frames[i].getDisposalMethod(), frames[i].getUserInputFlag(), os); + } + + os.write(IMAGE_TRAILER); + os.close(); + } + + /** + * Writes a list of GIFFrame as an animated GIF + * + * @param frames a list of GIFFrame + * @param os OutputStream for the animated GIF + * @throws Exception + */ + public void writeAnimatedGIF(List frames, OutputStream os) throws Exception { + writeAnimatedGIF(frames.toArray(new GIFFrame[0]), os); + } + + private void writeComment(OutputStream os, String comment) throws Exception { + os.write(EXTENSION_INTRODUCER); + os.write(COMMENT_EXTENSION_LABEL); + byte[] commentBytes = comment.getBytes(); + int numBlocks = commentBytes.length/0xff; + int leftOver = commentBytes.length % 0xff; + int offset = 0; + if(numBlocks > 0) { + for(int i = 0; i < numBlocks; i++) { + os.write(0xff); + os.write(commentBytes, offset, 0xff); + offset += 0xff; + } + } + if(leftOver > 0) { + os.write(leftOver); + os.write(commentBytes, offset, leftOver); + } + os.write(0); + } + + public void writeFrame(OutputStream os, GIFFrame frame) throws Exception { + // Retrieve image dimension + Bitmap image = frame.getFrame(); + int imageWidth = image.getWidth(); + int imageHeight = image.getHeight(); + int frameLeft = frame.getLeftPosition(); + int frameTop = frame.getTopPosition(); + // Determine the logical screen dimension + if(firstFrame) { + if(logicalScreenWidth <= 0) + logicalScreenWidth = imageWidth; + if(logicalScreenHeight <= 0) + logicalScreenHeight = imageHeight; + } + if(frameLeft >= logicalScreenWidth || frameTop >= logicalScreenHeight) return; + if((frameLeft + imageWidth) > logicalScreenWidth) imageWidth = logicalScreenWidth - frameLeft; + if((frameTop + imageHeight) > logicalScreenHeight) imageHeight = logicalScreenHeight - frameTop; + int[] pixels = new int[imageWidth * imageHeight]; + image.getPixels(pixels, 0, imageWidth, 0, 0, imageWidth, imageHeight); + // Handle transparency color if explicitly set + if(frame.getTransparencyFlag() == GIFFrame.TRANSPARENCY_INDEX_SET && frame.getTransparentColor() != -1) { + int transColor = (frame.getTransparentColor() & 0x00ffffff); + for(int j = pixels.length - 1; j > 0; j--) { + int pixel = (pixels[j] & 0x00ffffff); + if(pixel == transColor) pixels[j] = pixel; + } + } + writeFrame(pixels, imageWidth, imageHeight, frame.getLeftPosition(), frame.getTopPosition(), + frame.getDelay(), frame.getDisposalMethod(), frame.getUserInputFlag(), os); + } + + public void writeFrame(OutputStream os, Bitmap frame) throws Exception { + writeFrame(os, frame, 100); // default delay is 100 milliseconds + } + + public void writeFrame(OutputStream os, Bitmap frame, int delay) throws Exception { + // Retrieve image dimension + int imageWidth = frame.getWidth(); + int imageHeight = frame.getHeight(); + // Determine the logical screen dimension + if(firstFrame) { + if(logicalScreenWidth <= 0) + logicalScreenWidth = imageWidth; + if(logicalScreenHeight <= 0) + logicalScreenHeight = imageHeight; + } + if(delay <= 0) delay = 100; + if(imageWidth > logicalScreenWidth) imageWidth = logicalScreenWidth; + if(imageHeight > logicalScreenHeight) imageHeight = logicalScreenHeight; + int[] pixels = new int[imageWidth * imageHeight]; + frame.getPixels(pixels, 0, imageWidth, 0, 0, imageWidth, imageHeight); + writeFrame(pixels, imageWidth, imageHeight, 0, 0, delay, os); + } + + private void writeFrame(int[] pixels, int imageWidth, int imageHeight, int imageLeftPosition, int imageTopPosition, int delay, int disposalMethod, int userInputFlag, OutputStream os) throws Exception { + // Reset empty_bits + empty_bits = 0x08; + + int transparent_color = -1; + int[] colorInfo; + + // Reduce colors, if the color depth is less than 8 bits, reduce colors + // to the actual bits needed, otherwise reduce to 8 bits. + byte[] newPixels = new byte[imageWidth*imageHeight]; + colorPalette = new int[256]; + + colorInfo = checkColorDepth(pixels, newPixels, colorPalette); + + if(colorInfo[0] > 0x08) { + bitsPerPixel = 8; + if(isApplyDither) + colorInfo = reduceColorsDiffusionDither(pixels, imageWidth, imageHeight, bitsPerPixel, newPixels, colorPalette); + else + colorInfo = reduceColors(pixels, bitsPerPixel, newPixels, colorPalette); + } + + bitsPerPixel = colorInfo[0]; + + transparent_color = colorInfo[1]; + + int num_of_color = 1<= 0) + bgcolor = (byte)transparent_color; + // Write logical screen descriptor + writeLSD(os, (short)logicalScreenWidth, (short)logicalScreenHeight, flags, bgcolor, aspectRatio); + // Write the global colorPalette + writePalette(os, num_of_color); + writeComment(os, "Created by ICAFE - https://github.com/dragon66/icafe"); + if(animated)// Write Netscape extension block + writeNetscapeApplicationBlock(os, loopCount); + } + + // Output the graphic control block + writeGraphicControlBlock(os, delay, transparent_color, disposalMethod, userInputFlag); + // Output image descriptor + if(firstFrame) { + writeImageDescriptor(os, imageWidth, imageHeight, imageLeftPosition, imageTopPosition, -1); + firstFrame = false; + } else { + writeImageDescriptor(os, imageWidth, imageHeight, imageLeftPosition, imageTopPosition, bitsPerPixel - 1); + // Write local colorPalette + writePalette(os, num_of_color); + } + // LZW encode the image + encode(newPixels, os); + /** Write out a zero length data sub-block */ + os.write(0x00); + } + + private void writeFrame(int[] pixels, int imageWidth, int imageHeight, int imageLeftPosition, int imageTopPosition, int delay, OutputStream os) throws Exception { + writeFrame(pixels, imageWidth, imageHeight, imageLeftPosition, imageTopPosition, delay, GIFFrame.DISPOSAL_RESTORE_TO_BACKGROUND, GIFFrame.USER_INPUT_NONE, os); + } + + // Unit of delay is supposed to be in millisecond + private void writeGraphicControlBlock(OutputStream os, int delay, int transparent_color, int disposalMethod, int userInputFlag) throws Exception { + // Scale delay + delay = Math.round(delay/10.0f); + + byte[] buf = new byte[8]; + buf[0] = EXTENSION_INTRODUCER; // Extension introducer + buf[1] = GRAPHIC_CONTROL_LABEL; // Graphic control label + buf[2] = 0x04; // Block size + // Add disposalMethod and userInputFlag + buf[3] |= (((disposalMethod&0x07) << 2)|((userInputFlag&0x01) << 1)); + buf[4] = (byte)(delay&0xff);// Delay time + buf[5] = (byte)((delay>>8)&0xff); + buf[6] = (byte)transparent_color; + buf[7] = 0x00; + + if(transparent_color >= 0) // Add transparency indicator + buf[3] |= 0x01; + + os.write(buf, 0, 8); + } + + private void writeHeader(OutputStream os, boolean newFormat) throws IOException { + // 6 bytes: GIF signature (always "GIF") plus GIF version ("87a" or "89a") + if(newFormat) + os.write("GIF89a".getBytes()); + else + os.write("GIF87a".getBytes()); + } + + private void writeImageDescriptor(OutputStream os, int imageWidth, int imageHeight, int imageLeftPosition, int imageTopPosition, int colorTableSize) throws Exception { + byte imageDescriptor[] = new byte[10]; + imageDescriptor[0] = IMAGE_SEPARATOR;// Image separator "," + imageDescriptor[1] = (byte)(imageLeftPosition&0xff);// Image left position + imageDescriptor[2] = (byte)((imageLeftPosition>>8)&0xff); + imageDescriptor[3] = (byte)(imageTopPosition&0xff);// Image top position + imageDescriptor[4] = (byte)((imageTopPosition>>8)&0xff); + imageDescriptor[5] = (byte)(imageWidth&0xff); + imageDescriptor[6] = (byte)((imageWidth>>8)&0xff); + imageDescriptor[7] = (byte)(imageHeight&0xff); + imageDescriptor[8] = (byte)((imageHeight>>8)&0xff); + imageDescriptor[9] = (byte)0x20;//0b00100000 - Packed fields + + if(colorTableSize >= 0) // Local color table will follow + imageDescriptor[9] |= (1<<7|colorTableSize); + + os.write(imageDescriptor, 0, 10); + } + + // Write logical screen descriptor + private void writeLSD(OutputStream os, short screen_width, short screen_height, short flags, byte bgcolor, byte aspectRatio) throws IOException { + byte[] descriptor = new byte[7]; + // Screen_width + descriptor[0] = (byte)(screen_width&0xff); + descriptor[1] = (byte)((screen_width>>8)&0xff); + // Screen_height + descriptor[2] = (byte)(screen_height&0xff); + descriptor[3] = (byte)((screen_height>>8)&0xff); + // Global flags + descriptor[4] = (byte)(flags&0xff); + // Background color + descriptor[5] = bgcolor; + // AspectRatio + descriptor[6] = aspectRatio; + + os.write(descriptor); + } + + private void writeNetscapeApplicationBlock(OutputStream os, int loopCounts) throws Exception { + byte[] buf = new byte[19]; + buf[0] = EXTENSION_INTRODUCER; // Extension introducer + buf[1] = APPLICATION_EXTENSION_LABEL; // Application extension label + buf[2] = 0x0b; // Block size + buf[3] = 'N'; // Application Identifier (8 bytes) + buf[4] = 'E'; + buf[5] = 'T'; + buf[6] = 'S'; + buf[7] = 'C'; + buf[8] = 'A'; + buf[9] = 'P'; + buf[10]= 'E'; + buf[11]= '2';// Application Authentication Code (3 bytes) + buf[12]= '.'; + buf[13]= '0'; + buf[14]= 0x03; + buf[15]= 0x01; + buf[16]= (byte)(loopCounts&0xff); // Loop counts + buf[17]= (byte)((loopCounts>>8)&0xff); + buf[18]= 0x00; // Block terminator + + os.write(buf); + } + + private void writePalette(OutputStream os, int num_of_color) throws Exception { + int index = 0; + byte colors[] = new byte[num_of_color*3]; + + for (int i=0; i>16)&0xff)); + colors[index++] = (byte)(((colorPalette[i]>>8)&0xff)); + colors[index++] = (byte)(colorPalette[i]&0xff); + } + + os.write(colors, 0, num_of_color*3); + } + + private static int[] checkColorDepth(int[] rgbTriplets, byte[] newPixels, final int[] colorPalette) { + int index = 0; + int temp = 0; + int bitsPerPixel = 1; + int transparent_index = -1;// Transparent color index + int transparent_color = -1;// Transparent color + int[] colorInfo = new int[2];// Return value + + IntHashtable rgbHash = new IntHashtable(1023); + + for (int i = 0; i < rgbTriplets.length; i++) { + temp = (rgbTriplets[i]&0x00ffffff); + + if((rgbTriplets[i] >>> 24) < 0x80 ) { // Transparent + if (transparent_index < 0) { + transparent_index = index; + transparent_color = temp;// Remember transparent color + } + temp = Integer.MAX_VALUE; + } + + Integer entry = rgbHash.get(temp); + + if (entry!=null) { + newPixels[i] = entry.byteValue(); + } else { + if(index > 0xff) {// More than 256 colors, have to reduce + // Colors before saving as an indexed color image + colorInfo[0] = 24; + return colorInfo; + } + rgbHash.put(temp, index); + newPixels[i] = (byte)index; + colorPalette[index++] = ((0xff<<24)|temp); + } + } + if(transparent_index >= 0)// This line could be used to set a different background color + colorPalette[transparent_index] = transparent_color; + // Return the actual bits per pixel and the transparent color index if any + while ((1< 8 || colorDepth < 1) + throw new IllegalArgumentException("Invalid color depth " + colorDepth); + int[] colorInfo = new int[2]; + int colors = 0; + colors = new WuQuant(rgbTriplets, 1<>> 24) < 0x80 ) { + newPixels[index1] = (byte)transparent_index; + continue; + } + + red = ((rgbTriplet[index1]&0xff0000)>>>16) + thisErrR[col + 1]; + if (red > 255) red = 255; + else if (red < 0) red = 0; + + green = ((rgbTriplet[index1]&0x00ff00)>>>8) + thisErrG[col + 1]; + if (green > 255) green = 255; + else if (green < 0) green = 0; + + blue = (rgbTriplet[index1]&0x0000ff) + thisErrB[col + 1]; + if (blue > 255) blue = 255; + else if (blue < 0) blue = 0; + + // Find the nearest color index + index = invMap.getNearestColorIndex(red, green, blue); + newPixels[index1] = (byte)index;// The colorPalette index for this pixel + + // Find errors for different channels + err1 = red - ((colorPalette[index]>>16)&0xff);// Red channel + err2 = green - ((colorPalette[index]>>8)&0xff);// Green channel + err3 = blue - (colorPalette[index]&0xff);// Blue channel + // Diffuse error + // Red + thisErrR[col + 2] += ((err1*7)/16); + nextErrR[col ] += ((err1*3)/16); + nextErrR[col + 1] += ((err1*5)/16); + nextErrR[col + 2] += ((err1)/16); + // Green + thisErrG[col + 2] += ((err2*7)/16); + nextErrG[col ] += ((err2*3)/16); + nextErrG[col + 1] += ((err2*5)/16); + nextErrG[col + 2] += ((err2)/16); + // Blue + thisErrB[col + 2] += ((err3*7)/16); + nextErrB[col ] += ((err3*3)/16); + nextErrB[col + 1] += ((err3*5)/16); + nextErrB[col + 2] += ((err3)/16); + } + // We have finished one row, switch the error arrays + tempErr = thisErrR; + thisErrR = nextErrR; + nextErrR = tempErr; + + tempErr = thisErrG; + thisErrG = nextErrG; + nextErrG = tempErr; + + tempErr = thisErrB; + thisErrB = nextErrB; + nextErrB = tempErr; + + // Clear the error arrays + Arrays.fill(nextErrR, 0); + Arrays.fill(nextErrG, 0); + Arrays.fill(nextErrB, 0); + } + } + + public static class GIFFrame { + // Frame parameters + private Bitmap frame; + private int leftPosition; + private int topPosition; + private int frameWidth; + private int frameHeight; + private int delay; + private int disposalMethod = DISPOSAL_UNSPECIFIED; + private int userInputFlag = USER_INPUT_NONE; + private int transparencyFlag = TRANSPARENCY_INDEX_NONE; + + // The transparent color value in RRGGBB format. + // The highest order byte has no effect. + private int transparentColor = TRANSPARENCY_COLOR_NONE; // Default no transparent color + + public static final int DISPOSAL_UNSPECIFIED = 0; + public static final int DISPOSAL_LEAVE_AS_IS = 1; + public static final int DISPOSAL_RESTORE_TO_BACKGROUND = 2; + public static final int DISPOSAL_RESTORE_TO_PREVIOUS = 3; + // Values between 4-7 inclusive + public static final int DISPOSAL_TO_BE_DEFINED = 7; + + public static final int USER_INPUT_NONE = 0; + public static final int USER_INPUT_EXPECTED = 1; + + public static final int TRANSPARENCY_INDEX_NONE = 0; + public static final int TRANSPARENCY_INDEX_SET = 1; + + public static final int TRANSPARENCY_COLOR_NONE = -1; + + public GIFFrame(Bitmap frame) { + this(frame, 0, 0, 0, GIFFrame.DISPOSAL_UNSPECIFIED); + } + + public GIFFrame(Bitmap frame, int delay) { + this(frame, 0, 0, delay, GIFFrame.DISPOSAL_UNSPECIFIED); + } + + public GIFFrame(Bitmap frame, int delay, int disposalMethod) { + this(frame, 0, 0, delay, disposalMethod); + } + + public GIFFrame(Bitmap frame, int leftPosition, int topPosition, int delay, int disposalMethod) { + this(frame, leftPosition, topPosition, delay, disposalMethod, USER_INPUT_NONE, TRANSPARENCY_INDEX_NONE, TRANSPARENCY_COLOR_NONE); + } + + public GIFFrame(Bitmap frame, int leftPosition, int topPosition, int delay, int disposalMethod, int userInputFlag, int transparencyFlag, int transparentColor) { + if(frame == null) throw new IllegalArgumentException("Null input image"); + if(disposalMethod < DISPOSAL_UNSPECIFIED || disposalMethod > DISPOSAL_TO_BE_DEFINED) + throw new IllegalArgumentException("Invalid disposal method: " + disposalMethod); + if(userInputFlag < USER_INPUT_NONE || userInputFlag > USER_INPUT_EXPECTED) + throw new IllegalArgumentException("Invalid user input flag: " + userInputFlag); + if(transparencyFlag < TRANSPARENCY_INDEX_NONE || transparencyFlag > TRANSPARENCY_INDEX_SET) + throw new IllegalArgumentException("Invalid transparency flag: " + transparencyFlag); + if(leftPosition < 0 || topPosition < 0) + throw new IllegalArgumentException("Negative coordinates for frame top-left position"); + if(delay < 0) delay = 0; + this.frame = frame; + this.leftPosition = leftPosition; + this.topPosition = topPosition; + this.delay = delay; + this.disposalMethod = disposalMethod; + this.userInputFlag = userInputFlag; + this.transparencyFlag = transparencyFlag; + this.frameWidth = frame.getWidth(); + this.frameHeight = frame.getHeight(); + this.transparentColor = transparentColor; + } + + public int getDelay() { + return delay; + } + + public int getDisposalMethod() { + return disposalMethod; + } + + public Bitmap getFrame() { + return frame; + } + + public int getFrameHeight() { + return frameHeight; + } + + public int getFrameWidth() { + return frameWidth; + } + + public int getLeftPosition() { + return leftPosition; + } + + public int getTopPosition() { + return topPosition; + } + + public int getTransparentColor() { + return transparentColor; + } + + public int getTransparencyFlag() { + return transparencyFlag; + } + + public int getUserInputFlag() { + return userInputFlag; + } + } + + /** + * Java port of + * C Implementation of Wu's Color Quantizer (v. 2) + * (see Graphics Gems vol. II, pp. 126-133) + * Author: Xiaolin Wu + * Dept. of Computer Science + * Univ. of Western Ontario + * London, Ontario N6A 5B7 + * wu@csd.uwo.ca + * + * Algorithm: Greedy orthogonal bipartition of RGB space for variance + * minimization aided by inclusion-exclusion tricks. + * For speed no nearest neighbor search is done. Slightly + * better performance can be expected by more sophisticated + * but more expensive versions. + * + * The author thanks Tom Lane at Tom_Lane@G.GP.CS.CMU.EDU for much of + * additional documentation and a cure to a previous bug. + * + * Free to distribute, comments and suggestions are appreciated. + */ + private static class WuQuant { + private static final int MAXCOLOR = 256; + private static final int RED = 2; + private static final int GREEN = 1; + private static final int BLUE = 0; + + private static int QUANT_SIZE = 33;// quant size + + private static final class Box { + int r0; /* min value, exclusive */ + int r1; /* max value, inclusive */ + int g0; + int g1; + int b0; + int b1; + int vol; + }; + + private int size; /*image size*/ + private int lut_size; /*color look-up table size*/ + private int qadd[]; + private int pixels[]; + private int transparent_color = -1;// Transparent color + + private float m2[][][] = new float[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + private long wt[][][] = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + private long mr[][][] = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + private long mg[][][] = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + private long mb[][][] = new long[QUANT_SIZE][QUANT_SIZE][QUANT_SIZE]; + + public WuQuant(int[] pixels, int lut_size) { + this.pixels = pixels; + this.size = pixels.length; + this.lut_size = lut_size; + } + + public int quantize(final byte[] newPixels, final int[] lut, int[] colorInfo) { + Box cube[] = new Box[MAXCOLOR]; + int lut_r, lut_g, lut_b; + int tag[] = new int[QUANT_SIZE*QUANT_SIZE*QUANT_SIZE]; + + int next, i, k; + long weight; + float vv[] = new float[MAXCOLOR], temp; + + Hist3d(wt, mr, mg, mb, m2); + M3d(wt, mr, mg, mb, m2); + + for(i = 0; i < MAXCOLOR; i++) + cube[i] = new Box(); + + cube[0].r0 = cube[0].g0 = cube[0].b0 = 0; + cube[0].r1 = cube[0].g1 = cube[0].b1 = QUANT_SIZE - 1; + next = 0; + + if(transparent_color >= 0) lut_size--; + + for(i = 1; i < lut_size; ++i){ + if (Cut(cube[next], cube[i])) { + /* volume test ensures we won't try to cut one-cell box */ + vv[next] = (cube[next].vol > 1) ? Var(cube[next]) : 0.0f; + vv[i] = (cube[i].vol > 1) ? Var(cube[i]) : 0.0f; + } else { + vv[next] = 0.0f; /* don't try to split this box again */ + i--; /* didn't create box i */ + } + next = 0; temp = vv[0]; + for(k = 1; k <= i; ++k) + if (vv[k] > temp) { + temp = vv[k]; next = k; + } + if (temp <= 0.0f) { + k = i + 1; + break; + } + } + + for(k = 0; k < lut_size; ++k){ + Mark(cube[k], k, tag); + weight = Vol(cube[k], wt); + if (weight > 0) { + lut_r = (int)(Vol(cube[k], mr) / weight); + lut_g = (int)(Vol(cube[k], mg) / weight); + lut_b = (int)(Vol(cube[k], mb) / weight); + lut[k] = (255 << 24) | (lut_r << 16) | (lut_g << 8) | lut_b; + } + else { + lut[k] = 0; + } + } + + for(i = 0; i < size; ++i) { + if((pixels[i] >>> 24) < 0x80) + newPixels[i] = (byte)lut_size; + else + newPixels[i] = (byte)tag[qadd[i]]; + } + + int bitsPerPixel = 0; + while ((1<= 0) { + lut[lut_size] = transparent_color; // Set the transparent color + colorInfo[1] = lut_size; + } + + return lut_size; + } + + public int quantize(final int[] lut, int[] colorInfo) { + Box cube[] = new Box[MAXCOLOR]; + int lut_r, lut_g, lut_b; + + int next, i, k; + long weight; + float vv[] = new float[MAXCOLOR], temp; + + Hist3d(wt, mr, mg, mb, m2); + M3d(wt, mr, mg, mb, m2); + + for(i = 0; i < MAXCOLOR; i++) + cube[i] = new Box(); + + cube[0].r0 = cube[0].g0 = cube[0].b0 = 0; + cube[0].r1 = cube[0].g1 = cube[0].b1 = QUANT_SIZE - 1; + next = 0; + + if(transparent_color >= 0) lut_size--; + + for(i = 1; i < lut_size; ++i){ + if (Cut(cube[next], cube[i])) { + /* volume test ensures we won't try to cut one-cell box */ + vv[next] = (cube[next].vol > 1) ? Var(cube[next]) : 0.0f; + vv[i] = (cube[i].vol > 1) ? Var(cube[i]) : 0.0f; + } else { + vv[next] = 0.0f; /* don't try to split this box again */ + i--; /* didn't create box i */ + } + next = 0; temp = vv[0]; + for(k = 1; k <= i; ++k) + if (vv[k] > temp) { + temp = vv[k]; next = k; + } + if (temp <= 0.0f) { + k = i + 1; + break; + } + } + + for(k = 0; k < lut_size; ++k){ + weight = Vol(cube[k], wt); + if (weight > 0) { + lut_r = (int)(Vol(cube[k], mr) / weight); + lut_g = (int)(Vol(cube[k], mg) / weight); + lut_b = (int)(Vol(cube[k], mb) / weight); + lut[k] = (255 << 24) | (lut_r << 16) | (lut_g << 8) | lut_b; + } + else { + lut[k] = 0; + } + } + + int bitsPerPixel = 0; + while ((1<= 0) { + lut[lut_size] = transparent_color; // Set the transparent color + colorInfo[1] = lut_size; + } + + return lut_size; + } + + /* Histogram is in elements 1..HISTSIZE along each axis, + * element 0 is for base or marginal value + * NB: these must start out 0! + */ + private void Hist3d(long vwt[][][], long vmr[][][], long vmg[][][], long vmb[][][], float m2[][][]) { + /* build 3-D color histogram of counts, r/g/b, c^2 */ + int r, g, b; + int i, inr, ing, inb, table[] = new int[256]; + + for(i = 0; i < 256; ++i) table[i]= i*i; + + qadd = new int[size]; + + for(i = 0; i < size; ++i) { + int rgb = pixels[i]; + if((rgb >>> 24) < 0x80) { // Transparent + if (transparent_color < 0) // Find the transparent color + transparent_color = rgb; + } + r = ((rgb >> 16)& 0xff); + g = ((rgb >> 8 )& 0xff); + b = ( rgb & 0xff); + inr = (r >> 3) + 1; + ing = (g >> 3) + 1; + inb = (b >> 3) + 1; + qadd[i] = (inr << 10) + (inr << 6) + inr + (ing << 5) + ing + inb; + /*[inr][ing][inb]*/ + ++vwt[inr][ing][inb]; + vmr[inr][ing][inb] += r; + vmg[inr][ing][inb] += g; + vmb[inr][ing][inb] += b; + m2[inr][ing][inb] += table[r] + table[g] + table[b]; + } + } + + /* At conclusion of the histogram step, we can interpret + * wt[r][g][b] = sum over voxel of P(c) + * mr[r][g][b] = sum over voxel of r*P(c) , similarly for mg, mb + * m2[r][g][b] = sum over voxel of c^2*P(c) + * Actually each of these should be divided by 'size' to give the usual + * interpretation of P() as ranging from 0 to 1, but we needn't do that here. + */ + + /* We now convert histogram into moments so that we can rapidly calculate + * the sums of the above quantities over any desired box. + */ + private void M3d(long vwt[][][], long vmr[][][], long vmg[][][], long vmb[][][], float m2[][][]) { + /* compute cumulative moments. */ + int i, r, g, b; + int line, line_r, line_g, line_b; + int area[] = new int[QUANT_SIZE]; + int area_r[] = new int[QUANT_SIZE]; + int area_g[] = new int[QUANT_SIZE]; + int area_b[] = new int[QUANT_SIZE]; + float line2, area2[] = new float[QUANT_SIZE]; + + for(r = 1; r < QUANT_SIZE; ++r) { + for(i = 0; i < QUANT_SIZE; ++i) + area2[i] = area[i] = area_r[i] = area_g[i] = area_b[i] = 0; + for(g = 1; g < QUANT_SIZE; ++g) { + line2 = line = line_r = line_g = line_b = 0; + for(b = 1; b < QUANT_SIZE; ++b){ + line += vwt[r][g][b]; + line_r += vmr[r][g][b]; + line_g += vmg[r][g][b]; + line_b += vmb[r][g][b]; + line2 += m2[r][g][b]; + + area[b] += line; + area_r[b] += line_r; + area_g[b] += line_g; + area_b[b] += line_b; + area2[b] += line2; + + vwt[r][g][b] = vwt[r-1][g][b] + area[b]; + vmr[r][g][b] = vmr[r-1][g][b] + area_r[b]; + vmg[r][g][b] = vmg[r-1][g][b] + area_g[b]; + vmb[r][g][b] = vmb[r-1][g][b] + area_b[b]; + m2[r][g][b] = m2[r-1][g][b] + area2[b]; + } + } + } + } + + private long Vol(Box cube, long mmt[][][]) { + /* Compute sum over a box of any given statistic */ + return ( mmt[cube.r1][cube.g1][cube.b1] + -mmt[cube.r1][cube.g1][cube.b0] + -mmt[cube.r1][cube.g0][cube.b1] + +mmt[cube.r1][cube.g0][cube.b0] + -mmt[cube.r0][cube.g1][cube.b1] + +mmt[cube.r0][cube.g1][cube.b0] + +mmt[cube.r0][cube.g0][cube.b1] + -mmt[cube.r0][cube.g0][cube.b0] ); + } + + /* The next two routines allow a slightly more efficient calculation + * of Vol() for a proposed subbox of a given box. The sum of Top() + * and Bottom() is the Vol() of a subbox split in the given direction + * and with the specified new upper bound. + */ + + private long Bottom(Box cube, int dir, long mmt[][][]) { + /* Compute part of Vol(cube, mmt) that doesn't depend on r1, g1, or b1 */ + /* (depending on dir) */ + switch(dir) { + case RED: + return( -mmt[cube.r0][cube.g1][cube.b1] + +mmt[cube.r0][cube.g1][cube.b0] + +mmt[cube.r0][cube.g0][cube.b1] + -mmt[cube.r0][cube.g0][cube.b0] ); + case GREEN: + return( -mmt[cube.r1][cube.g0][cube.b1] + +mmt[cube.r1][cube.g0][cube.b0] + +mmt[cube.r0][cube.g0][cube.b1] + -mmt[cube.r0][cube.g0][cube.b0] ); + case BLUE: + return( -mmt[cube.r1][cube.g1][cube.b0] + +mmt[cube.r1][cube.g0][cube.b0] + +mmt[cube.r0][cube.g1][cube.b0] + -mmt[cube.r0][cube.g0][cube.b0] ); + default: + return 0; + } + } + + private long Top(Box cube, int dir, int pos, long mmt[][][]) { + /* Compute remainder of Vol(cube, mmt), substituting pos for */ + /* r1, g1, or b1 (depending on dir) */ + switch(dir) { + case RED: + return( mmt[pos][cube.g1][cube.b1] + -mmt[pos][cube.g1][cube.b0] + -mmt[pos][cube.g0][cube.b1] + +mmt[pos][cube.g0][cube.b0] ); + case GREEN: + return( mmt[cube.r1][pos][cube.b1] + -mmt[cube.r1][pos][cube.b0] + -mmt[cube.r0][pos][cube.b1] + +mmt[cube.r0][pos][cube.b0] ); + case BLUE: + return( mmt[cube.r1][cube.g1][pos] + -mmt[cube.r1][cube.g0][pos] + -mmt[cube.r0][cube.g1][pos] + +mmt[cube.r0][cube.g0][pos] ); + default: + return 0; + } + } + + private float Var(Box cube) { + /* Compute the weighted variance of a box */ + /* NB: as with the raw statistics, this is really the variance * size */ + float dr, dg, db, xx; + dr = Vol(cube, mr); + dg = Vol(cube, mg); + db = Vol(cube, mb); + xx = m2[cube.r1][cube.g1][cube.b1] + -m2[cube.r1][cube.g1][cube.b0] + -m2[cube.r1][cube.g0][cube.b1] + +m2[cube.r1][cube.g0][cube.b0] + -m2[cube.r0][cube.g1][cube.b1] + +m2[cube.r0][cube.g1][cube.b0] + +m2[cube.r0][cube.g0][cube.b1] + -m2[cube.r0][cube.g0][cube.b0]; + return xx - (dr*dr + dg*dg + db*db)/Vol(cube,wt); + } + + /* We want to minimize the sum of the variances of two subboxes. + * The sum(c^2) terms can be ignored since their sum over both subboxes + * is the same (the sum for the whole box) no matter where we split. + * The remaining terms have a minus sign in the variance formula, + * so we drop the minus sign and MAXIMIZE the sum of the two terms. + */ + private float Maximize(Box cube, int dir, int first, int last, int cut[], + long whole_r, long whole_g, long whole_b, long whole_w) { + long half_r, half_g, half_b, half_w; + long base_r, base_g, base_b, base_w; + int i; + float temp, max; + + base_r = Bottom(cube, dir, mr); + base_g = Bottom(cube, dir, mg); + base_b = Bottom(cube, dir, mb); + base_w = Bottom(cube, dir, wt); + + max = 0.0f; + cut[0] = -1; + + for(i = first; i < last; ++i){ + half_r = base_r + Top(cube, dir, i, mr); + half_g = base_g + Top(cube, dir, i, mg); + half_b = base_b + Top(cube, dir, i, mb); + half_w = base_w + Top(cube, dir, i, wt); + /* now half_x is sum over lower half of box, if split at i */ + if (half_w == 0) /* subbox could be empty of pixels! */ + continue; /* never split into an empty box */ + temp = (half_r*half_r + half_g*half_g + half_b*half_b)/(float)half_w; + half_r = whole_r - half_r; + half_g = whole_g - half_g; + half_b = whole_b - half_b; + half_w = whole_w - half_w; + if (half_w == 0) /* subbox could be empty of pixels! */ + continue; /* never split into an empty box */ + temp += (half_r*half_r + half_g*half_g + half_b*half_b)/(float)half_w; + + if (temp > max) { max = temp; cut[0] = i;} + } + + return max; + } + + private boolean Cut(Box set1, Box set2) { + int dir; + int cutr[] = new int[1]; + int cutg[] = new int[1]; + int cutb[] = new int[1]; + float maxr, maxg, maxb; + long whole_r, whole_g, whole_b, whole_w; + + whole_r = Vol(set1, mr); + whole_g = Vol(set1, mg); + whole_b = Vol(set1, mb); + whole_w = Vol(set1, wt); + + maxr = Maximize(set1, RED, set1.r0 + 1, set1.r1, cutr, + whole_r, whole_g, whole_b, whole_w); + maxg = Maximize(set1, GREEN, set1.g0 + 1, set1.g1, cutg, + whole_r, whole_g, whole_b, whole_w); + maxb = Maximize(set1, BLUE, set1.b0 + 1, set1.b1, cutb, + whole_r, whole_g, whole_b, whole_w); + + if(maxr >= maxg && maxr >= maxb) { + dir = RED; + if (cutr[0] < 0) return false; /* can't split the box */ + } else if(maxg >= maxr && maxg >= maxb) + dir = GREEN; + else + dir = BLUE; + + set2.r1 = set1.r1; + set2.g1 = set1.g1; + set2.b1 = set1.b1; + + switch (dir){ + case RED: + set2.r0 = set1.r1 = cutr[0]; + set2.g0 = set1.g0; + set2.b0 = set1.b0; + break; + case GREEN: + set2.g0 = set1.g1 = cutg[0]; + set2.r0 = set1.r0; + set2.b0 = set1.b0; + break; + case BLUE: + set2.b0 = set1.b1 = cutb[0]; + set2.r0 = set1.r0; + set2.g0 = set1.g0; + break; + } + set1.vol = (set1.r1 - set1.r0)*(set1.g1 - set1.g0)*(set1.b1 - set1.b0); + set2.vol = (set2.r1 - set2.r0)*(set2.g1 - set2.g0)*(set2.b1 - set2.b0); + + return true; + } + + private void Mark(Box cube, int label, int tag[]) { + int r, g, b; + + for(r = cube.r0 + 1; r <= cube.r1; ++r) + for(g = cube.g0 + 1; g <= cube.g1; ++g) + for(b = cube.b0 + 1; b <= cube.b1; ++b) + tag[(r<<10) + (r<<6) + r + (g<<5) + g + b] = label; + } + } + + /** + * A hash table using primitive integer keys. + * + * Based on + * QuadraticProbingHashTable.java + *

+ * Probing table implementation of hash tables. + * Note that all "matching" is based on the equals method. + * + * @author Mark Allen Weiss + */ + private static class IntHashtable { + + /** The array of HashEntry. */ + private HashEntry [ ] array; // The array of HashEntry + private int currentSize; // The number of occupied cells + + /** + * Construct the hash table. + * @param size the approximate initial size. + */ + @SuppressWarnings("unchecked") + public IntHashtable(int size) { + array = new HashEntry [size]; + makeEmpty( ); + } + + /** + * Insert into the hash table. If the item is + * already present, do nothing. + * @param key the item to insert. + */ + public void put(int key, E value) { + // Insert key as active + int currentPos = locate( key ); + if( isActive( currentPos ) ) + return; + + array[ currentPos ] = new HashEntry ( key, value, true ); + + // Rehash + if( ++currentSize > array.length / 2 ) + rehash( ); + } + + /** + * Expand the hash table. + */ + @SuppressWarnings("unchecked") + private void rehash( ) { + HashEntry [ ] oldArray = array; + + // Create a new double-sized, empty table + array = new HashEntry [nextPrime( 2 * oldArray.length )]; + currentSize = 0; + + // Copy table over + for( int i = 0; i < oldArray.length; i++ ) + if( oldArray[i] != null && oldArray[i].isActive ) + put( oldArray[i].key, oldArray[i].value ); + + return; + } + + /** + * Method that performs quadratic probing resolution. + * @param key the item to search for. + * @return the index of the item. + */ + private int locate(int key) { + int collisionNum = 0; + + // And with the largest positive integer + int currentPos = (key & 0x7FFFFFFF) % array.length; + + while( array[ currentPos ] != null && + array[ currentPos ].key != key ) + { + currentPos += 2 * ++collisionNum - 1; // Compute ith probe + if( currentPos >= array.length ) // Implement the mod + currentPos -= array.length; + } + return currentPos; + } + + /** + * Find an item in the hash table. + * @param key the item to search for. + * @return the value of the matching item. + */ + public E get(int key) { + int currentPos = locate( key ); + return isActive( currentPos ) ? array[ currentPos ].value : null; + } + + /** + * Return true if currentPos exists and is active. + * @param currentPos the result of a call to findPos. + * @return true if currentPos is active. + */ + private boolean isActive( int currentPos ) { + return array[ currentPos ] != null && array[ currentPos ].isActive; + } + + /** + * Make the hash table logically empty. + */ + public void makeEmpty( ) { + currentSize = 0; + for( int i = 0; i < array.length; i++ ) + array[ i ] = null; + } + + /** + * Internal method to find a prime number at least as large as n. + * @param n the starting number (must be positive). + * @return a prime number larger than or equal to n. + */ + private static int nextPrime(int n) { + if( n % 2 == 0 ) + n++; + + for( ; !isPrime( n ); n += 2 ) + ; + + return n; + } + + /** + * Internal method to test if a number is prime. + * Not an efficient algorithm. + * @param n the number to test. + * @return the result of the test. + */ + private static boolean isPrime(int n) { + if( n == 2 || n == 3 ) + return true; + + if( n == 1 || n % 2 == 0 ) + return false; + + for( int i = 3; i * i <= n; i += 2 ) + if( n % i == 0 ) + return false; + + return true; + } + + // The basic entry stored in ProbingHashTable + private static class HashEntry { + int key; // the key + V value; // the value + boolean isActive; // false if deleted + + @SuppressWarnings("unused") + HashEntry(int k, V val) { + this( k, val, true ); + } + + HashEntry(int k, V val, boolean i) { + key = k; + value = val; + isActive = i; + } + } + } + + private static class InverseColorMap { + private int bitsReserved;// Number of bits used in color quantization. + private int bitsDiscarded;// Number of discarded bits + private int maxColorVal;// Maximum value for each quantized color + private int invMapLen;// Length of the inverse color map + // The inverse color map itself + private byte[] invColorMap; + + // Default constructor using 5 for quantization bits + public InverseColorMap() { + this(5); + } + + // Constructor using bitsReserved bits for quantization + public InverseColorMap(int rbits) { + bitsReserved = rbits; + bitsDiscarded = 8 - bitsReserved; + maxColorVal = 1 << bitsReserved; + invMapLen = maxColorVal * maxColorVal * maxColorVal; + invColorMap = new byte[invMapLen]; + } + + // Fetch the forward color map index for this RGB + public int getNearestColorIndex(int red, int green, int blue) { + return invColorMap[(((red >> bitsDiscarded) << (bitsReserved<<1))) | + ((green >> bitsDiscarded) << bitsReserved) | + (blue >> bitsDiscarded)]&0xff; + } + + /** + * Create an inverse color map using the input forward RGB map. + */ + public void createInverseMap(int no_of_colors, int[] colorPalette) { + int red, green, blue, r, g, b; + int rdist, gdist, bdist, dist; + int rinc, ginc, binc; + + int x = (1 << bitsDiscarded);// Step size for each color + int xsqr = (1 << (bitsDiscarded + bitsDiscarded)); + int txsqr = xsqr + xsqr; + int buf_index; + + int[] dist_buf = new int[invMapLen]; + + // Initialize the distance buffer array with the largest integer value + for (int i = invMapLen; --i >= 0;) + dist_buf[i] = 0x7FFFFFFF; + // Now loop through all the colors in the color map + for (int i = 0; i < no_of_colors; i++) + { + red = ((colorPalette[i]>>16)&0xff); + green = ((colorPalette[i]>>8)&0xff); + blue = (colorPalette[i]&0xff); + /** + * We start from the origin (0,0,0) of the quantized colors, calculate + * the distance between the cell center of the quantized colors and + * the current color map entry as follows: + * (rcenter * x + x/2) - red, where rcenter is the center of the + * Quantized red color map entry which is 0 since we start from 0. + */ + rdist = (x>>1) - red;// Red distance + gdist = (x>>1) - green;// Green distance + bdist = (x>>1) - blue;// Blue distance + dist = rdist*rdist + gdist*gdist + bdist*bdist;//The modular + // The distance increment with each step value x + rinc = txsqr - (red << (bitsDiscarded + 1)); + ginc = txsqr - (green << (bitsDiscarded + 1)); + binc = txsqr - (blue << (bitsDiscarded + 1)); + + buf_index = 0; + // Loop through quantized RGB space + for (r = 0, rdist = dist; r < maxColorVal; rdist += rinc, rinc += txsqr, r++ ) + { + for (g = 0, gdist = rdist; g < maxColorVal; gdist += ginc, ginc += txsqr, g++) + { + for (b = 0, bdist = gdist; b < maxColorVal; bdist += binc, binc += txsqr, buf_index++, b++) + { + if (bdist < dist_buf[buf_index]) + { + dist_buf[buf_index] = bdist; + invColorMap[buf_index] = (byte)i; + } + } + } + } + } + } + } + +} diff --git a/app/src/main/java/net/pokeranalytics/android/util/video/MMediaMuxer.kt b/app/src/main/java/net/pokeranalytics/android/util/video/MMediaMuxer.kt index 10b4eafa..2aa57641 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/video/MMediaMuxer.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/video/MMediaMuxer.kt @@ -34,7 +34,7 @@ class MMediaMuxer { private var _mess: String? = null private var completion: ((String) -> (Unit))? = null - fun Init( + fun init( activity: Activity?, width: Int, height: Int, @@ -50,26 +50,26 @@ class MMediaMuxer { ShowProgressBar() } - fun AddFrame(byteFrame: ByteArray) { - CheckDataListState() + fun addFrame(byteFrame: ByteArray) { + checkDataListState() Thread(Runnable { Logd("Android get Frame") - val bit = BitmapFactory.decodeByteArray(byteFrame, 0, byteFrame.size) + val bitmap = BitmapFactory.decodeByteArray(byteFrame, 0, byteFrame.size) Logd("Android convert Bitmap") val byteConvertFrame = - getNV21(bit.width, bit.height, bit) + getNV21(bitmap.width, bitmap.height, bitmap) Logd("Android convert getNV21") bitList!!.add(byteConvertFrame) }).start() } - fun AddFrame(byteFrame: ByteArray, count: Int, isLast: Boolean) { + fun addFrame(byteFrame: ByteArray, count: Int, isLast: Boolean) { var bf = byteFrame - CheckDataListState() + checkDataListState() Logd("Android get Frames = $count") - val bit = BitmapFactory.decodeByteArray(bf, 0, bf.size) + val bitmap = BitmapFactory.decodeByteArray(bf, 0, bf.size) Logd("Android convert Bitmap") - bf = getNV21(bit.width, bit.height, bit) + bf = getNV21(bitmap.width, bitmap.height, bitmap) Logd("Android convert getNV21") for (i in 0 until count) { if (isLast) { @@ -80,7 +80,7 @@ class MMediaMuxer { } } - fun CreateVideo(handler: (String) -> (Unit)) { + fun createVideo(handler: (String) -> (Unit)) { this.completion = handler currentIndexFrame = 0 Logd("Prepare Frames Data") @@ -122,26 +122,18 @@ class MMediaMuxer { private fun bufferEncoder() { val runnable = Runnable { try { - Logd( - "PrepareEncoder start" - ) - PrepareEncoder() - Logd( - "PrepareEncoder end" - ) + Logd("PrepareEncoder start") + prepareEncoder() + Logd("PrepareEncoder end") } catch (e: IOException) { - Loge( - e.message - ) + Loge(e.message) } try { while (mRunning) { - Encode() + encode() } } finally { - Logd( - "release" - ) + Logd("release") Release() HideProgressBar() bitFirst = null @@ -159,45 +151,41 @@ class MMediaMuxer { } @Throws(IOException::class) - private fun PrepareEncoder() { - val codecInfo = - selectCodec( - MIME_TYPE - ) - if (codecInfo == null) { - Loge("Unable to find an appropriate codec for $MIME_TYPE") - } - Logd("found codec: " + codecInfo!!.name) + private fun prepareEncoder() { +// val codecInfo = selectCodec(MIME_TYPE) +// if (codecInfo == null) { +// Loge("Unable to find an appropriate codec for $MIME_TYPE") +// } + + val codec = MediaCodec.createEncoderByType(MIME_TYPE) + val colorFormat: Int colorFormat = try { - selectColorFormat( - codecInfo, - MIME_TYPE - ) + selectColorFormat(codec.codecInfo, MIME_TYPE) } catch (e: Exception) { + Timber.d(">>> color format exception: $e") CodecCapabilities.COLOR_FormatYUV420SemiPlanar } - mediaCodec = MediaCodec.createByCodecName(codecInfo.name) - val mediaFormat = MediaFormat.createVideoFormat( - MIME_TYPE, - _width, - _height - ) - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, - BIT_RATE - ) - mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, - FRAME_RATE - ) + + Logd("Selected codec: " + codec.name) + Logd("Selected color format: $colorFormat") + +// mediaCodec = MediaCodec.createByCodecName(codecInfo.name) + + mediaCodec = codec + + val mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, _width, _height) + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE) + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE) mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat) - mediaFormat.setInteger( - MediaFormat.KEY_I_FRAME_INTERVAL, - INFLAME_INTERVAL - ) + mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL) + mediaCodec?.let { it.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) it.start() + Timber.d("format2: ${it.outputFormat}") } + try { val formattedDate = Date().dateTimeFileFormatted outputPath = File( @@ -210,7 +198,7 @@ class MMediaMuxer { } } - private fun Encode() { + private fun encode() { while (true) { if (!mRunning) { break @@ -297,12 +285,8 @@ class MMediaMuxer { return yuv } - private fun encodeYUV420SP( - yuv420sp: ByteArray, - argb: IntArray, - width: Int, - height: Int - ) { + private fun encodeYUV420SP(yuv420sp: ByteArray, argb: IntArray, width: Int, height: Int) { + val frameSize = width * height var yIndex = 0 var uvIndex = frameSize @@ -337,7 +321,7 @@ class MMediaMuxer { } } - private fun CheckDataListState() { + private fun checkDataListState() { if (bitList == null) { bitList = ArrayList() } @@ -354,12 +338,12 @@ class MMediaMuxer { } companion object { - private const val MIME_TYPE = "video/avc" // H.264 Advanced Video Coding + private const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC // H.264 Advanced Video Coding private var _width = 512 private var _height = 512 - private const val BIT_RATE = 800000 - private const val INFLAME_INTERVAL = 1 - private const val FRAME_RATE = 50 + private const val BIT_RATE = 2000000 // 800000 + private const val IFRAME_INTERVAL = 1 + private const val FRAME_RATE = 30 // 50 // private const val DEBUG = false private const val TAG = "CODEC" /** @@ -368,19 +352,32 @@ class MMediaMuxer { */ private fun selectCodec(mimeType: String): MediaCodecInfo? { val numCodecs = MediaCodecList.getCodecCount() + + val validCodecs = mutableListOf() for (i in 0 until numCodecs) { val codecInfo = MediaCodecList.getCodecInfoAt(i) if (!codecInfo.isEncoder) { continue } + val types = codecInfo.supportedTypes for (j in types.indices) { if (types[j].equals(mimeType, ignoreCase = true)) { - return codecInfo + validCodecs.add(codecInfo) +// return codecInfo } } } - return null + /** + * OMX.qcom.video.encoder.avc + * OMX.google.h264.encoder + */ + + validCodecs.forEach { + Timber.d("VALID CODEC name = ${it.name}") + } + + return validCodecs.firstOrNull() } /** @@ -388,18 +385,17 @@ class MMediaMuxer { * code. If no match is found, this throws a test failure -- the set of * formats known to the test should be expanded for new platforms. */ - private fun selectColorFormat( - codecInfo: MediaCodecInfo, - mimeType: String - ): Int { - val capabilities = codecInfo - .getCapabilitiesForType(mimeType) + private fun selectColorFormat(codecInfo: MediaCodecInfo, mimeType: String): Int { + + val capabilities = codecInfo.getCapabilitiesForType(mimeType) + + capabilities.colorFormats.forEach { + Timber.d(">>> Color Format = $it") + } + for (i in capabilities.colorFormats.indices) { val colorFormat = capabilities.colorFormats[i] - if (isRecognizedFormat( - colorFormat - ) - ) { + if (isRecognizedFormat(colorFormat)) { return colorFormat } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6166e5fa..5d02b3da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -803,6 +803,6 @@ Video available! Your video has been generated at the following path Open file with - We\'ll send you a notification when your video is available. Expect approximately one minute! + We\'ll send you a notification when your file is available. Expect approximately one minute! diff --git a/app/standard/release/output.json b/app/standard/release/output.json index 158b448c..86ad6be4 100644 --- a/app/standard/release/output.json +++ b/app/standard/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":95,"versionName":"5.0.1","enabled":true,"outputFile":"PokerAnalytics_5.0.1(95)_200805_0932_release.apk","fullName":"standardRelease","baseName":"standard-release","dirName":""},"path":"PokerAnalytics_5.0.1(95)_200805_0932_release.apk","properties":{}}] \ No newline at end of file +[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":96,"versionName":"5.0.2","enabled":true,"outputFile":"PokerAnalytics_5.0.2(96)_200813_1136_release.apk","fullName":"standardRelease","baseName":"standard-release","dirName":""},"path":"PokerAnalytics_5.0.2(96)_200813_1136_release.apk","properties":{}}] \ No newline at end of file