From 9646e50421802c20da4fe3d2c026e98674d598ea Mon Sep 17 00:00:00 2001 From: T8RIN Date: Sat, 27 Jan 2024 00:53:22 +0300 Subject: [PATCH] gif module added --- feature/gif-tools/.gitignore | 1 + feature/gif-tools/build.gradle.kts | 25 + .../gif-tools/src/main/AndroidManifest.xml | 20 + .../gif_tools/data/AnimatedGifEncoder.kt | 475 ++++++++++ .../feature/gif_tools/data/GifDecoder.kt | 822 ++++++++++++++++++ .../feature/gif_tools/data/LZWEncoder.kt | 280 ++++++ .../feature/gif_tools/data/NeuQuant.kt | 527 +++++++++++ settings.gradle.kts | 3 + 8 files changed, 2153 insertions(+) create mode 100644 feature/gif-tools/.gitignore create mode 100644 feature/gif-tools/build.gradle.kts create mode 100644 feature/gif-tools/src/main/AndroidManifest.xml create mode 100644 feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/AnimatedGifEncoder.kt create mode 100644 feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/GifDecoder.kt create mode 100644 feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/LZWEncoder.kt create mode 100644 feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/NeuQuant.kt diff --git a/feature/gif-tools/.gitignore b/feature/gif-tools/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/gif-tools/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/gif-tools/build.gradle.kts b/feature/gif-tools/build.gradle.kts new file mode 100644 index 000000000..4ab2c8d69 --- /dev/null +++ b/feature/gif-tools/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * ImageToolbox is an image editor for android + * Copyright (c) 2024 T8RIN (Malik Mukhametzyanov) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * You should have received a copy of the Apache License + * along with this program. If not, see . + */ + +plugins { + alias(libs.plugins.image.toolbox.library) + alias(libs.plugins.image.toolbox.feature) + alias(libs.plugins.image.toolbox.hilt) + alias(libs.plugins.image.toolbox.compose) +} + +android.namespace = "ru.tech.imageresizershrinker.feature.gif_tools" \ No newline at end of file diff --git a/feature/gif-tools/src/main/AndroidManifest.xml b/feature/gif-tools/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0862d94c4 --- /dev/null +++ b/feature/gif-tools/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/AnimatedGifEncoder.kt b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/AnimatedGifEncoder.kt new file mode 100644 index 000000000..278dd3ad4 --- /dev/null +++ b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/AnimatedGifEncoder.kt @@ -0,0 +1,475 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "unused") + +package ru.tech.imageresizershrinker.feature.gif_tools.data + +import android.graphics.Bitmap +import android.graphics.Bitmap.Config +import android.graphics.Canvas +import android.graphics.Paint +import java.io.IOException +import java.io.OutputStream + +internal class AnimatedGifEncoder(val out: OutputStream) { + + var width: Int = 0 // image size + + var height: Int = 0 + + var x: Int = 0 + + var y: Int = 0 + + var transparent: Int = -1 // transparent color if given + + var transIndex: Int = 0 // transparent index in color table + + var repeat: Int = -1 // no repeat + /** + * 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. + */ + set(iter) { + if (iter >= 0) field = iter + } + + var delay: Int = 0 // frame delay (hundredths) + /** + * 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 + */ + set(ms) { + field = ms / 10 + } + + var started: Boolean = false // ready to output frames + + var colorDepth: Int = 0 // number of bit planes + + var usedEntry: BooleanArray = BooleanArray(256) // active palette entries + + var palSize: Int = 7 // color table size (bits-1) + + var dispose: Int = -1 // disposal code (-1 = use default) + /** + * 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. + */ + set(code) { + if (code >= 0) field = code + } + + var closeStream: Boolean = false // close stream when finished + + var firstFrame: Boolean = true + + var sizeSet: Boolean = false // if false, get size from first frame + + var sample: Int = 10 // default sample interval for quantizer + + data class AnalyzedData(val indexedPixels: ByteArray, val colorTab: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AnalyzedData + + if (!indexedPixels.contentEquals(other.indexedPixels)) return false + return colorTab.contentEquals(other.colorTab) + } + + override fun hashCode(): Int { + var result = indexedPixels.contentHashCode() + result = 31 * result + colorTab.contentHashCode() + return result + } + } + + /** + * 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 not invoked, the size of the first image is used + * for all subsequent frames. + + * @param im + * * BufferedImage containing frame to write. + * * + * @return true if successful. + */ + fun addFrame(im: Bitmap): Boolean { + if (!started) { + throw IllegalStateException("Encoder should had run start().") + } + var ok = true + try { + if (!sizeSet) { + // use first frame's size + setSize(im.width, im.height) + } + val pixels = getImagePixels(im) // convert to correct format if necessary + val analyzedData = analyzePixels(pixels) // build color table & map pixels + if (firstFrame) { + writeLSD() // logical screen descriptior + writePalette(analyzedData.colorTab) // global color table + if (repeat >= 0) { + // use NS app extension to indicate reps + writeNetscapeExt() + } + } + writeGraphicCtrlExt() // write graphic control extension + writeImageDesc() // image descriptor + if (!firstFrame) { + writePalette(analyzedData.colorTab) // local color table + } + writePixels(analyzedData.indexedPixels) // encode and write pixel data + firstFrame = false + } catch (e: IOException) { + ok = false + } + + return ok + } + + /** + * Flushes any pending data and closes output file. If writing to an + * OutputStream, the stream is not closed. + */ + fun finish(): Boolean { + if (!started) + return false + var ok = true + started = false + try { + out.write(59) // gif trailer + out.flush() + if (closeStream) { + out.close() + } + } catch (e: IOException) { + ok = false + } + + // reset for subsequent use + transIndex = 0 + closeStream = false + firstFrame = true + + return ok + } + + /** + * Sets frame rate in frames per second. + * @param fps + * * float frame rate (frames per second) + */ + fun setFrameRate(fps: Float) { + if (fps != 0.toFloat()) { + delay = (1000 / fps).toInt() + } + } + + /** + * 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. + * * + * @return + */ + fun setQuality(quality: Int) { + sample = if (quality < 1) 1 else quality + } + + /** + * Sets the GIF frame size. The default size is the size of the first frame + * added if this method is not invoked. + + * @param w + * * int frame width. + * * + * @param h + * * int frame height. + */ + fun setSize(w: Int, h: Int) { + width = w + height = h + if (width < 1) + width = 320 + if (height < 1) + height = 240 + sizeSet = true + } + + /** + * Sets the GIF frame position. The position is 0,0 by default. + * Useful for only updating a section of the image + + * @param x + * * int frame x position. + * * + * @param y + * * int frame y position. + */ + fun setPosition(x: Int, y: Int) { + this.x = x + this.y = y + } + + /** + * Initiates GIF file creation on the given stream. The stream is not closed + * automatically. + * @return false if initial write failed. + */ + fun start(): Boolean { + var ok = true + closeStream = false + try { + writeString("GIF89a") // header + } catch (e: IOException) { + ok = false + } + started = ok + return started + } + + /** + * Analyzes image colors and creates color map. + */ + private fun analyzePixels(pixels: ByteArray): AnalyzedData { + val len = pixels.size + val nPix = len / 3 + val indexedPixels = ByteArray(nPix) + val nq = NeuQuant(pixels, len, sample) + // initialize quantizer + val colorTab = nq.process() // create reduced palette + // convert map from BGR to RGB + for (i in 0..= 0) { + disp = dispose and 7 // user override + } + disp = disp shl 2 + + // packed fields + out.write( + 0 or // 1:3 reserved + disp or // 4:6 disposal + 0 or // 7 user input - 0 = none + internalTransparent + ) // 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 fun writeImageDesc() { + out.write(44) // image separator + writeShort(x) // image position x,y = 0,0 + 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( + 128 or // 1 local color table 1=yes + 0 or // 2 interlace - 0=no + 0 or // 3 sorted - 0=no + 0 or // 4-5 reserved + palSize + ) // 6-8 size of color table + } + } + + /** + * Writes Logical Screen Descriptor + */ + private fun writeLSD() { + // logical screen size + writeShort(width) + writeShort(height) + // packed fields + out.write( + (128 or // 1 : global color table flag = 1 (gct used) + 112 or // 2-4 : color resolution = 7 + 0 or // 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 fun writeNetscapeExt() { + out.write(33) // extension introducer + out.write(255) // 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 fun writePalette(colorTab: ByteArray) { + out.write(colorTab, 0, colorTab.size) + val n = (3 * 256) - colorTab.size + for (i in 0... + */ + +@file:Suppress("MemberVisibilityCanBePrivate", "unused") + +package ru.tech.imageresizershrinker.feature.gif_tools.data + +import android.graphics.Bitmap +import android.util.Log +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.nio.BufferUnderflowException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.pow + +/** + * Copyright (c) 2013 Xcellent Creations, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +/** + * Reads frame data from a GIF image source and decodes it into individual frames + * for animation purposes. Image data can be read from either and InputStream source + * or a byte[]. + * + * This class is optimized for running animations with the frames, there + * are no methods to get individual frame images, only to decode the next frame in the + * animation sequence. Instead, it lowers its memory footprint by only housing the minimum + * data necessary to decode the next frame in the animation sequence. + * + * The animation must be manually moved forward using [.advance] before requesting the next + * frame. This method must also be called before you request the first frame or an error will + * occur. + * + * Implementation adapted from sample code published in Lyons. (2004). *Java for Programmers*, + * republished under the MIT Open Source License + */ +class GifDecoder { + /** + * Global status code of GIF data parsing + */ + private var status = 0 + + //Global File Header values and parsing flags + private var width = 0 // full image width + private var height = 0 // full image height + private var gctFlag = false // global color table used + private var gctSize = 0 // size of global color table + + /** + * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely. + * + * @return iteration count if one was specified, else 1. + */ + var loopCount = 1 // iterations; 0 = repeat forever + private set + private var gct // global color table + : IntArray? = null + private var act // active color table + : IntArray? = null + private var bgIndex = 0 // background color index + private var bgColor = 0 // background color + private var pixelAspect = 0 // pixel aspect ratio + private var lctFlag = false // local color table flag + private var lctSize = 0 // local color table size + + // Raw GIF data from input source + private var rawData: ByteBuffer? = null + + // Raw data read working array + private var block = ByteArray(256) // current data block + private var blockSize = 0 // block size last graphic control extension info + + // LZW decoder working arrays + private var prefix: ShortArray? = null + private var suffix: ByteArray? = null + private var pixelStack: ByteArray? = null + private lateinit var mainPixels: ByteArray + private lateinit var mainScratch: IntArray + private lateinit var copyScratch: IntArray + private var frames: ArrayList? = null // frames read from current file + private var currentFrame: GifFrame? = null + private var previousImage: Bitmap? = null + private var currentImage: Bitmap? = null + private var renderImage: Bitmap? = null + + /** + * Gets the current index of the animation frame, or -1 if animation hasn't not yet started + * + * @return frame index + */ + var currentFrameIndex = 0 + private set + + /** + * Gets the number of frames read from file. + * + * @return frame count + */ + var frameCount = 0 + private set + + /** + * Inner model class housing metadata for each frame + */ + private class GifFrame { + var ix = 0 + var iy = 0 + var iw = 0 + var ih = 0 + + /* Control Flags */ + var interlace = false + var transparency = false + + /* Disposal Method */ + var dispose = 0 + + /* Transparency Index */ + var transIndex = 0 + + /* Delay, in ms, to next frame */ + var delay = 0 + + /* Index in the raw buffer where we need to start reading to decode */ + var bufferFrameStart = 0 + + /* Local Color Table */ + var lct: IntArray? = null + } + + /** + * Move the animation frame counter forward + */ + fun advance() { + currentFrameIndex = (currentFrameIndex + 1) % frameCount + } + + /** + * Gets display duration for specified frame. + * + * @param n int index of frame + * @return delay in milliseconds + */ + fun getDelay(n: Int): Int { + var delay = -1 + if (n in 0.. 0) contentLength + 4096 else 4096 + val buffer = ByteArrayOutputStream(capacity) + var nRead: Int + val data = ByteArray(16384) + while (inputStream.read(data, 0, data.size).also { nRead = it } != -1) { + buffer.write(data, 0, nRead) + } + buffer.flush() + read(buffer.toByteArray()) + } catch (e: IOException) { + Log.w(TAG, "Error reading data from stream", e) + } + } else { + status = STATUS_OPEN_ERROR + } + try { + inputStream!!.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing stream", e) + } + return status + } + + /** + * Reads GIF image from byte array + * + * @param data containing GIF file. + * @return read status code (0 = no errors) + */ + fun read(data: ByteArray?): Int { + init() + if (data != null) { + //Initiliaze the raw data buffer + rawData = ByteBuffer.wrap(data).also { + it.rewind() + it.order(ByteOrder.LITTLE_ENDIAN) + } + readHeader() + if (!err()) { + readContents() + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR + } + } + } else { + status = STATUS_OPEN_ERROR + } + return status + } + + /** + * Creates new frame image from current data (and previous frames as specified by their disposition codes). + */ + private fun setPixels(frameIndex: Int) { + val currentFrame = frames!![frameIndex] + var previousFrame: GifFrame? = null + val previousIndex = frameIndex - 1 + if (previousIndex >= 0) { + previousFrame = frames!![previousIndex] + } + + // final location of blended pixels + val dest = mainScratch + + // fill in starting image contents based on last image's dispose code + if (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) { + if (previousFrame.dispose == DISPOSAL_NONE && currentImage != null) { + // Start with the current image + currentImage!!.getPixels(dest, 0, width, 0, 0, width, height) + } + if (previousFrame.dispose == DISPOSAL_BACKGROUND) { + // Start with a canvas filled with the background color + var c = 0 + if (!currentFrame!!.transparency) { + c = bgColor + } + for (i in 0 until previousFrame.ih) { + val n1 = (previousFrame.iy + i) * width + previousFrame.ix + val n2 = n1 + previousFrame.iw + for (k in n1 until n2) { + dest[k] = c + } + } + } + if (previousFrame.dispose == DISPOSAL_PREVIOUS && previousImage != null) { + // Start with the previous frame + previousImage!!.getPixels(dest, 0, width, 0, 0, width, height) + } + } + + //Decode pixels for this frame into the global pixels[] scratch + decodeBitmapData(currentFrame, mainPixels) // decode pixel data + + // copy each source line to the appropriate place in the destination + var pass = 1 + var inc = 8 + var iline = 0 + for (i in 0 until currentFrame!!.ih) { + var line = i + if (currentFrame.interlace) { + if (iline >= currentFrame.ih) { + pass++ + when (pass) { + 2 -> iline = 4 + 3 -> { + iline = 2 + inc = 4 + } + + 4 -> { + iline = 1 + inc = 2 + } + + else -> {} + } + } + line = iline + iline += inc + } + line += currentFrame.iy + if (line < height) { + val k = line * width + var dx = k + currentFrame.ix // start of line in dest + var dlim = dx + currentFrame.iw // end of dest line + if (k + width < dlim) { + dlim = k + width // past dest edge + } + var sx = i * currentFrame.iw // start of line in source + while (dx < dlim) { + // map color and insert in destination + val index = mainPixels[sx++].toInt() and 0xff + val c = act!![index] + if (c != 0) { + dest[dx] = c + } + dx++ + } + } + } + + //Copy pixels into previous image + currentImage!!.getPixels(copyScratch, 0, width, 0, 0, width, height) + previousImage!!.setPixels(copyScratch, 0, width, 0, 0, width, height) + //Set pixels for current image + currentImage!!.setPixels(dest, 0, width, 0, 0, width, height) + } + + /** + * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick. + */ + private fun decodeBitmapData( + frame: GifFrame?, + dstPixels: ByteArray? + ) { + var tempDstPixels = dstPixels + + if (frame != null) { + //Jump to the frame start position + rawData!!.position(frame.bufferFrameStart) + } + val nullCode = -1 + val pixelCount = if (frame == null) width * height else frame.iw * frame.ih + var available: Int + val clear: Int + var codeMask: Int + var codeSize: Int + var inCode: Int + var oldCode: Int + var code: Int + if (tempDstPixels == null || tempDstPixels.size < pixelCount) { + tempDstPixels = ByteArray(pixelCount) // allocate new pixel array + } + if (prefix == null) { + prefix = ShortArray(MAX_STACK_SIZE) + } + if (suffix == null) { + suffix = ByteArray(MAX_STACK_SIZE) + } + if (pixelStack == null) { + pixelStack = ByteArray(MAX_STACK_SIZE + 1) + } + + // Initialize GIF data stream decoder. + val dataSize: Int = read() + clear = 1 shl dataSize + val endOfInformation: Int = clear + 1 + available = clear + 2 + oldCode = nullCode + codeSize = dataSize + 1 + codeMask = (1 shl codeSize) - 1 + code = 0 + while (code < clear) { + prefix!![code] = 0 // XXX ArrayIndexOutOfBoundsException + suffix!![code] = code.toByte() + code++ + } + + // Decode GIF pixel stream. + var bi = 0 + var pi = 0 + var top = 0 + var first = 0 + var count = 0 + var bits = 0 + var datum = 0 + var i = 0 + while (i < pixelCount) { + if (top == 0) { + if (bits < codeSize) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock() + if (count <= 0) { + break + } + bi = 0 + } + datum += block[bi].toInt() and 0xff shl bits + bits += 8 + bi++ + count-- + continue + } + // Get the next code. + code = datum and codeMask + datum = datum shr codeSize + bits -= codeSize + // Interpret the code + if (code > available || code == endOfInformation) { + break + } + if (code == clear) { + // Reset decoder. + codeSize = dataSize + 1 + codeMask = (1 shl codeSize) - 1 + available = clear + 2 + oldCode = nullCode + continue + } + if (oldCode == nullCode) { + pixelStack!![top++] = suffix!![code] + oldCode = code + first = code + continue + } + inCode = code + if (code == available) { + pixelStack!![top++] = first.toByte() + code = oldCode + } + while (code > clear) { + pixelStack!![top++] = suffix!![code] + code = prefix!![code].toInt() + } + first = suffix!![code].toInt() and 0xff + // Add a new string to the string table, + if (available >= MAX_STACK_SIZE) { + break + } + pixelStack!![top++] = first.toByte() + prefix!![available] = oldCode.toShort() + suffix!![available] = first.toByte() + available++ + if (available and codeMask == 0 && available < MAX_STACK_SIZE) { + codeSize++ + codeMask += available + } + oldCode = inCode + } + // Pop a pixel off the pixel stack. + top-- + tempDstPixels[pi++] = pixelStack!![top] + i++ + } + i = pi + while (i < pixelCount) { + tempDstPixels[i] = 0 // clear missing pixels + i++ + } + } + + /** + * Returns true if an error was encountered during reading/decoding + */ + private fun err(): Boolean { + return status != STATUS_OK + } + + /** + * Initializes or re-initializes reader + */ + private fun init() { + status = STATUS_OK + frameCount = 0 + currentFrameIndex = -1 + frames = ArrayList() + gct = null + } + + /** + * Reads a single byte from the input stream. + */ + private fun read(): Int { + var curByte = 0 + try { + curByte = rawData!!.get().toInt() and 0xFF + } catch (e: Exception) { + status = STATUS_FORMAT_ERROR + } + return curByte + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer" + */ + private fun readBlock(): Int { + blockSize = read() + var n = 0 + if (blockSize > 0) { + try { + var count: Int + while (n < blockSize) { + count = blockSize - n + rawData!![block, n, count] + n += count + } + } catch (e: Exception) { + Log.w(TAG, "Error Reading Block", e) + status = STATUS_FORMAT_ERROR + } + } + return n + } + + /** + * Reads color table as 256 RGB integer values + * + * @param ncolors int number of colors to read + * @return int array containing 256 colors (packed ARGB with full alpha) + */ + private fun readColorTable(ncolors: Int): IntArray? { + val nbytes = 3 * ncolors + var tab: IntArray? = null + val c = ByteArray(nbytes) + try { + rawData!![c] + tab = IntArray(256) // max size to avoid bounds checks + var i = 0 + var j = 0 + while (i < ncolors) { + val r = c[j++].toInt() and 0xff + val g = c[j++].toInt() and 0xff + val b = c[j++].toInt() and 0xff + tab[i++] = -0x1000000 or (r shl 16) or (g shl 8) or b + } + } catch (e: BufferUnderflowException) { + Log.w(TAG, "Format Error Reading Color Table", e) + status = STATUS_FORMAT_ERROR + } + return tab + } + + /** + * Main file parser. Reads GIF content blocks. + */ + private fun readContents() { + // read GIF file content blocks + var done = false + while (!(done || err())) { + var code = read() + when (code) { + 0x2C -> readBitmap() + 0x21 -> { + code = read() + when (code) { + 0xf9 -> { + //Start a new frame + currentFrame = GifFrame() + readGraphicControlExt() + } + + 0xff -> { + readBlock() + var app = "" + var i = 0 + while (i < 11) { + app += Char(block[i].toUShort()) + i++ + } + if (app == "NETSCAPE2.0") { + readNetscapeExt() + } else { + skip() // don't care + } + } + + 0xfe -> skip() + 0x01 -> skip() + else -> skip() + } + } + + 0x3b -> done = true + 0x00 -> status = STATUS_FORMAT_ERROR + else -> status = STATUS_FORMAT_ERROR + } + } + } + + /** + * Reads GIF file header information. + */ + private fun readHeader() { + var id = "" + for (i in 0..5) { + id += read().toChar() + } + if (!id.startsWith("GIF")) { + status = STATUS_FORMAT_ERROR + return + } + readLSD() + if (gctFlag && !err()) { + gct = readColorTable(gctSize) + bgColor = gct!![bgIndex] + } + } + + /** + * Reads Graphics Control Extension values + */ + private fun readGraphicControlExt() { + read() // block size + val packed = read() // packed fields + currentFrame!!.dispose = packed and 0x1c shr 2 // disposal method + if (currentFrame!!.dispose == 0) { + currentFrame!!.dispose = 1 // elect to keep old image if discretionary + } + currentFrame!!.transparency = packed and 1 != 0 + currentFrame!!.delay = readShort() * 10 // delay in milliseconds + currentFrame!!.transIndex = read() // transparent color index + read() // block terminator + } + + /** + * Reads next frame image + */ + private fun readBitmap() { + currentFrame!!.ix = readShort() // (sub)image position & size + currentFrame!!.iy = readShort() + currentFrame!!.iw = readShort() + currentFrame!!.ih = readShort() + val packed = read() + lctFlag = packed and 0x80 != 0 // 1 - local color table flag interlace + lctSize = 2.0.pow(((packed and 0x07) + 1).toDouble()).toInt() + // 3 - sort flag + // 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color + // table size + currentFrame!!.interlace = packed and 0x40 != 0 + if (lctFlag) { + currentFrame!!.lct = readColorTable(lctSize) // read table + } else { + currentFrame!!.lct = null //No local color table + } + currentFrame!!.bufferFrameStart = + rawData!!.position() //Save this as the decoding position pointer + decodeBitmapData(null, mainPixels) // false decode pixel data to advance buffer + skip() + if (err()) { + return + } + frameCount++ + frames!!.add(currentFrame) // add image to frame + } + + /** + * Reads Logical Screen Descriptor + */ + private fun readLSD() { + // logical screen size + width = readShort() + height = readShort() + // packed fields + val packed = read() + gctFlag = packed and 0x80 != 0 // 1 : global color table flag + // 2-4 : color resolution + // 5 : gct sort flag + gctSize = 2 shl (packed and 7) // 6-8 : gct size + bgIndex = read() // background color index + pixelAspect = read() // pixel aspect ratio + + //Now that we know the size, init scratch arrays + mainPixels = ByteArray(width * height) + mainScratch = IntArray(width * height) + copyScratch = IntArray(width * height) + previousImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) + currentImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) + } + + /** + * Reads Netscape extenstion to obtain iteration count + */ + private fun readNetscapeExt() { + do { + readBlock() + if (block[0].toInt() == 1) { + // loop count sub-block + val b1 = block[1].toInt() and 0xff + val b2 = block[2].toInt() and 0xff + loopCount = b2 shl 8 or b1 + } + } while (blockSize > 0 && !err()) + } + + /** + * Reads next 16-bit value, LSB first + */ + private fun readShort(): Int { + // read 16-bit value + return rawData!!.getShort().toInt() + } + + /** + * Skips variable length blocks up to and including next zero length block. + */ + private fun skip() { + do { + readBlock() + } while (blockSize > 0 && !err()) + } + + companion object { + private val TAG = GifDecoder::class.java.simpleName + + /** + * File read status: No errors. + */ + const val STATUS_OK = 0 + + /** + * File read status: Error decoding file (may be partially decoded) + */ + const val STATUS_FORMAT_ERROR = 1 + + /** + * File read status: Unable to open source. + */ + const val STATUS_OPEN_ERROR = 2 + + /** + * max decoder pixel stack size + */ + private const val MAX_STACK_SIZE = 4096 + + /** + * GIF Disposal Method meaning take no action + */ + private const val DISPOSAL_UNSPECIFIED = 0 + + /** + * GIF Disposal Method meaning leave canvas from previous frame + */ + private const val DISPOSAL_NONE = 1 + + /** + * GIF Disposal Method meaning clear canvas to background color + */ + private const val DISPOSAL_BACKGROUND = 2 + + /** + * GIF Disposal Method meaning clear canvas to frame before last + */ + private const val DISPOSAL_PREVIOUS = 3 + } +} \ No newline at end of file diff --git a/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/LZWEncoder.kt b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/LZWEncoder.kt new file mode 100644 index 000000000..7b8a227f5 --- /dev/null +++ b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/LZWEncoder.kt @@ -0,0 +1,280 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "LocalVariableName", "FunctionName", "PropertyName") + +package ru.tech.imageresizershrinker.feature.gif_tools.data + + +// ============================================================================== +// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. +// K Weiner 12/00 + +import java.io.OutputStream + +internal class LZWEncoder( + private val imgW: Int, + private val imgH: Int, + pixAry: ByteArray, + colorDepth: Int +) { + + private val pixelArray: IntArray + private val initCodeSize: Int + + private var remaining: Int = 0 + + // 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) + + var n_bits: Int = 0 // number of bits/code + + var maxbits = BITS // user settable max # bits/code + + var maxcode: Int = 0 // maximum code, given n_bits + + var maxmaxcode = 1 shl BITS // should NEVER generate this code + + var htab = IntArray(HSIZE) + + var codetab = IntArray(HSIZE) + + var hsize = HSIZE // for dynamic table sizing + + var free_ent = 0 // first unused entry + + // block compression parameters -- after all codes are used up, + // and compression rate changes, start over. + var 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. + + var g_init_bits: Int = 0 + + var ClearCode: Int = 0 + + var EOFCode: Int = 0 + + // 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. + + var cur_accum = 0 + + var cur_bits = 0 + + var masks = + arrayOf(0, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535) + + // Number of characters so far in this 'packet' + var a_count: Int = 0 + + // Define the storage for the packet accumulator + var accum = ByteArray(256) + + // Add a character to the end of the current packet, and if it is 254 + // characters, flush the packet to disk. + fun char_out(c: Byte, outs: OutputStream) { + accum[a_count++] = c + if (a_count >= 254) + flush_char(outs) + } + + // Clear out the hash table + + // table clear for block compress + fun cl_block(outs: OutputStream) { + cl_hash(hsize) + free_ent = ClearCode + 2 + clear_flg = true + + output(ClearCode, outs) + } + + // reset code table + fun cl_hash(hsize: Int) { + for (i in 0..= 0) { + disp = hsizeReg - i // secondary hash (after G. Knott) + if (i == 0) + disp = 1 + do { + i -= disp + if (i < 0) { + i += hsizeReg + } + if (htab[i] == fcode) { + ent = codetab[i] + continue + } + } while (htab[i] >= 0) + } + output(ent, outs) + ent = pixelArray[pixi] + 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) + } + + fun encode(os: OutputStream) { + os.write(initCodeSize) // write "initial code size" byte + + remaining = imgW * imgH // reset navigation variables + + 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 + fun flush_char(outs: OutputStream) { + if (a_count > 0) { + outs.write(a_count) + outs.write(accum, 0, a_count) + a_count = 0 + } + } + + fun MAXCODE(n_bits: Int): Int { + return (1 shl n_bits) - 1 + } + + fun output(code: Int, outs: OutputStream) { + cur_accum = cur_accum and masks[cur_bits] + + cur_accum = if (cur_bits > 0) cur_accum or (code shl cur_bits) else code + + cur_bits += n_bits + + while (cur_bits >= 8) { + char_out((cur_accum and 255).toByte(), outs) + cur_accum = cur_accum shr 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 + maxcode = if (n_bits == maxbits) maxmaxcode else MAXCODE(n_bits) + } + } + + if (code == EOFCode) { + // At EOF, write the rest of the buffer. + while (cur_bits > 0) { + char_out((cur_accum and 255).toByte(), outs) + cur_accum = cur_accum shr 8 + cur_bits -= 8 + } + + flush_char(outs) + } + } + + companion object { + + // GIFCOMPR.C - GIF Image compression routines + // + // Lempel-Ziv compression based on 'compress'. GIF modifications by + // David Rowley (mgardi@watdcsu.waterloo.edu) + + // General DEFINEs + + const val BITS = 12 + + const val HSIZE = 5003 // 80% occupancy + } + + init { + this.pixelArray = IntArray(pixAry.size) + for (i in pixAry.indices) { + pixelArray[i] = pixAry[i].toInt() and 255 + } + this.initCodeSize = 2.coerceAtLeast(colorDepth) + } +} \ No newline at end of file diff --git a/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/NeuQuant.kt b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/NeuQuant.kt new file mode 100644 index 000000000..7be8a95c3 --- /dev/null +++ b/feature/gif-tools/src/main/java/ru/tech/imageresizershrinker/feature/gif_tools/data/NeuQuant.kt @@ -0,0 +1,527 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "KotlinConstantConditions") + +package ru.tech.imageresizershrinker.feature.gif_tools.data + + +/* + * NeuQuant Neural-Net Quantization Algorithm + * ------------------------------------------ + * + * Copyright (c) 1994 Anthony Dekker + * + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See + * "Kohonen neural networks for optimal colour quantization" in "Network: + * Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of + * the algorithm. + * + * Any party obtaining a copy of these files from the author, directly or + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, + * world-wide, paid up, royalty-free, nonexclusive right and license to deal in + * this software and documentation files (the "Software"), including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons who + * receive copies from any such party to do so, with the only requirement being + * that this copyright notice remain intact. + */ + +// Ported to Java 12/00 K Weiner + +/* radpower for precomputation */ + +/* +* Initialise network in range (0,0,0) to (255,255,255) and set parameters +* ----------------------------------------------------------------------- +*/ +internal class NeuQuant( + private var thepicture: ByteArray, + private var lengthcount: Int, + private var samplefac: Int +) { + /* number of colours used */ + private val netsize: Int = 256 + + /* four primes near 500 - assume no image has a length so large */ + /* that it is divisible by all four primes */ + private val prime1: Int = 499 + + private val prime2: Int = 491 + + private val prime3: Int = 487 + + private val prime4: Int = 503 + + private val minpicturebytes: Int = (3 * prime4) + + /* minimum size for input image */ + + /* +* Network Definitions ------------------- +*/ + + private val maxnetpos: Int = (netsize - 1) + + private val netbiasshift: Int = 4 /* bias for colour values */ + + private val ncycles: Int = 100 /* no. of learning cycles */ + + /* defs for freq and bias */ + private val intbiasshift: Int = 16 /* bias for fractions */ + + private val intbias: Int = 1 shl intbiasshift + + private val gammashift: Int = 10 /* gamma = 1024 */ + + private val betashift: Int = 10 + + private val beta: Int = (intbias shr betashift) /* beta = 1/1024 */ + + private val betagamma: Int = (intbias shl (gammashift - betashift)) + + /* defs for decreasing radius factor */ + /* + * for 256 cols, radius + * starts + */ + private val initrad: Int = (netsize shr 3) + + private val radiusbiasshift: Int = 6 /* at 32.0 biased by 6 bits */ + + private val radiusbias: Int = 1 shl radiusbiasshift + + /* + * and + * decreases + * by a + */ + private val initradius: Int = (initrad * radiusbias) + + private val radiusdec: Int = 30 /* factor of 1/30 each cycle */ + + /* defs for decreasing alpha factor */ + private val alphabiasshift: Int = 10 /* alpha starts at 1.0 */ + + private val initalpha: Int = 1 shl alphabiasshift + + /* radbias and alpharadbias used for radpower calculation */ + private val radbiasshift: Int = 8 + + private val radbias: Int = 1 shl radbiasshift + + private val alpharadbshift: Int = (alphabiasshift + radbiasshift) + + private val alpharadbias: Int = 1 shl alpharadbshift + + private var alphadec: Int = 0 /* biased by 10 bits */ + + /* the network itself - [netsize][4] */ + private var network: Array + + private var netindex: IntArray = IntArray(256) + + /* for network lookup - really 256 */ + private var bias: IntArray = IntArray(netsize) + + /* bias and freq arrays for learning */ + private var freq: IntArray = IntArray(netsize) + + private var radpower: IntArray = IntArray(initrad) + + init { + network = Array(netsize) { i -> + val p = IntArray(4) + val temp = (i shl (netbiasshift + 8)) / netsize + p[0] = temp + p[1] = temp + p[2] = temp + freq[i] = intbias / netsize /* 1/netsize */ + bias[i] = 0 + p + } + } + + fun colorMap(): ByteArray { + val map = ByteArray(3 * netsize) + val index = IntArray(netsize) + for (i in 0..= lengthcount) + pix -= lengthcount + + i++ + if (delta == 0) + delta = 1 + if (i % delta == 0) { + alpha -= alpha / alphadec + radius -= radius / radiusdec + rad = radius shr radiusbiasshift + if (rad <= 1) + rad = 0 + run { + val rad2 = rad * rad + for (index in 0..= 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) + } + + fun process(): ByteArray { + learn() + unbiasnet() + inxbuild() + return colorMap() + } + + /* + * Unbias network to give byte values 0..255 and record position i to prepare + * for sort + * ----------------------------------------------------------------------------------- + */ + fun unbiasnet() { + for (i in 0.. netsize) + hi = netsize + + var j: Int = i + 1 + var k: Int = i - 1 + var 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) { + } + // 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) { + } + } + } + } + + /* + * Move neuron i towards biased (b,g,r) by factor alpha + * ---------------------------------------------------- + */ + private fun altersingle(alpha: Int, i: Int, b: Int, g: Int, r: Int) { + /* alter hit neuron */ + val n = network[i] + n[0] = n[0] - (alpha * (n[0] - b)) / initalpha + n[1] = n[1] - (alpha * (n[1] - g)) / initalpha + n[2] = n[2] - (alpha * (n[2] - r)) / initalpha + } + + /* + * Search for biased BGR values ---------------------------- + */ + private fun contest(b: Int, g: Int, r: Int): Int { + + /* 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]) */ + + var dist: Int + var a: Int + var biasdist: Int + var betafreq: Int + var bestpos: Int + var bestbiaspos: Int + var bestd: Int + var bestbiasd: Int + var n: IntArray + + bestd = (1 shl 31).inv() + bestbiasd = bestd + bestpos = -1 + bestbiaspos = bestpos + + for (i in 0..