mirror of
https://github.com/T8RIN/ImageToolbox.git
synced 2025-05-17 21:45:59 +08:00
gif module added
This commit is contained in:
1
feature/gif-tools/.gitignore
vendored
Normal file
1
feature/gif-tools/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
25
feature/gif-tools/build.gradle.kts
Normal file
25
feature/gif-tools/build.gradle.kts
Normal file
@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
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"
|
20
feature/gif-tools/src/main/AndroidManifest.xml
Normal file
20
feature/gif-tools/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ 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 <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
-->
|
||||
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
@ -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..<colorTab.size / 3) {
|
||||
val tind = i * 3
|
||||
val temp = colorTab[tind]
|
||||
colorTab[tind] = colorTab[tind + 2]
|
||||
colorTab[tind + 2] = temp
|
||||
usedEntry[i] = false
|
||||
}
|
||||
|
||||
// map image pixels to new palette
|
||||
var k = 0
|
||||
for (i in 0..<nPix) {
|
||||
val index = nq.map(
|
||||
pixels[k++].toInt() and 255,
|
||||
pixels[k++].toInt() and 255,
|
||||
pixels[k++].toInt() and 255
|
||||
)
|
||||
usedEntry[index] = true
|
||||
indexedPixels[i] = index.toByte()
|
||||
}
|
||||
|
||||
colorDepth = 8
|
||||
palSize = 7
|
||||
// get closest match to transparent color if specified
|
||||
if (transparent != -1) {
|
||||
transIndex = findClosest(transparent, colorTab)
|
||||
}
|
||||
|
||||
return AnalyzedData(indexedPixels, colorTab)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns index of palette color closest to c
|
||||
|
||||
*/
|
||||
private fun findClosest(c: Int, colorTab: ByteArray): Int {
|
||||
val r = (c shr 16) and 255
|
||||
val g = (c shr 8) and 255
|
||||
val b = (c shr 0) and 255
|
||||
var minpos = 0
|
||||
var dmin = 256 * 256 * 256
|
||||
val len = colorTab.size
|
||||
run {
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
val dr = r - (colorTab[i++].toInt() and 255)
|
||||
val dg = g - (colorTab[i++].toInt() and 255)
|
||||
val db = b - (colorTab[i].toInt() and 255)
|
||||
val d = dr * dr + dg * dg + db * db
|
||||
val index = i / 3
|
||||
if (usedEntry[index] && (d < dmin)) {
|
||||
dmin = d
|
||||
minpos = index
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return minpos
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts image pixels into byte array "pixels"
|
||||
*/
|
||||
private fun getImagePixels(image: Bitmap): ByteArray {
|
||||
val w = image.width
|
||||
val h = image.height
|
||||
var temp: Bitmap? = null
|
||||
if ((w != width) || (h != height)) {
|
||||
// create new image with right size/format
|
||||
temp = Bitmap.createBitmap(width, height, Config.ARGB_8888)
|
||||
val g = Canvas(temp)
|
||||
g.drawBitmap(image, 0.toFloat(), 0.toFloat(), Paint())
|
||||
}
|
||||
val data = getImageData(temp ?: image)
|
||||
val pixels = ByteArray(data.size * 3)
|
||||
for (i in data.indices) {
|
||||
val tempIndex = i * 3
|
||||
pixels[tempIndex] = ((data[i] shr 0) and 255).toByte()
|
||||
pixels[tempIndex + 1] = ((data[i] shr 8) and 255).toByte()
|
||||
pixels[tempIndex + 2] = ((data[i] shr 16) and 255).toByte()
|
||||
}
|
||||
return pixels
|
||||
}
|
||||
|
||||
private fun getImageData(img: Bitmap): IntArray {
|
||||
val w = img.width
|
||||
val h = img.height
|
||||
|
||||
val data = IntArray(w * h)
|
||||
img.getPixels(data, 0, w, 0, 0, w, h)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes Graphic Control Extension
|
||||
*/
|
||||
private fun writeGraphicCtrlExt() {
|
||||
out.write(33) // extension introducer
|
||||
out.write(249) // GCE label
|
||||
out.write(4) // data block size
|
||||
val internalTransparent: Int
|
||||
var disp: Int
|
||||
if (transparent == -1) {
|
||||
internalTransparent = 0
|
||||
disp = 0 // dispose = no action
|
||||
} else {
|
||||
internalTransparent = 1
|
||||
disp = 2 // force clear if using transparent color
|
||||
}
|
||||
if (dispose >= 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..<n) {
|
||||
out.write(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes and writes pixel data
|
||||
*/
|
||||
private fun writePixels(indexedPixels: ByteArray) {
|
||||
val encoder = LZWEncoder(width, height, indexedPixels, colorDepth)
|
||||
encoder.encode(out)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write 16-bit value to output stream, LSB first
|
||||
*/
|
||||
private fun writeShort(value: Int) {
|
||||
out.write(value and 255)
|
||||
out.write((value shr 8) and 255)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes string to output stream
|
||||
*/
|
||||
fun writeString(s: String) {
|
||||
out.write(s.toByteArray())
|
||||
}
|
||||
}
|
@ -0,0 +1,822 @@
|
||||
/*
|
||||
* 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 <http://www.apache.org/licenses/LICENSE-2.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<GifFrame?>? = 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..<frameCount) {
|
||||
delay = frames!![n]!!.delay
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
val nextDelay: Int
|
||||
/**
|
||||
* Gets display duration for the upcoming frame
|
||||
*/
|
||||
get() = if (frameCount <= 0 || currentFrameIndex < 0) {
|
||||
-1
|
||||
} else getDelay(currentFrameIndex)
|
||||
val nextFrame: Bitmap?
|
||||
/**
|
||||
* Get the next frame in the animation sequence.
|
||||
*
|
||||
* @return Bitmap representation of frame
|
||||
*/
|
||||
get() {
|
||||
if (frameCount <= 0 || currentFrameIndex < 0 || currentImage == null) {
|
||||
return null
|
||||
}
|
||||
val frame = frames!![currentFrameIndex]
|
||||
|
||||
//Set the appropriate color table
|
||||
if (frame!!.lct == null) {
|
||||
act = gct
|
||||
} else {
|
||||
act = frame.lct
|
||||
if (bgIndex == frame.transIndex) {
|
||||
bgColor = 0
|
||||
}
|
||||
}
|
||||
var save = 0
|
||||
if (frame.transparency) {
|
||||
save = act!![frame.transIndex]
|
||||
act!![frame.transIndex] = 0 // set transparent color if specified
|
||||
}
|
||||
if (act == null) {
|
||||
Log.w(TAG, "No Valid Color Table")
|
||||
status = STATUS_FORMAT_ERROR // no color table defined
|
||||
return null
|
||||
}
|
||||
setPixels(currentFrameIndex) // transfer pixel data to image
|
||||
|
||||
// Reset the transparent pixel in the color table
|
||||
if (frame.transparency) {
|
||||
act!![frame.transIndex] = save
|
||||
}
|
||||
return currentImage
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads GIF image from stream
|
||||
*
|
||||
* @param inputStream containing GIF file.
|
||||
* @return read status code (0 = no errors)
|
||||
*/
|
||||
fun read(inputStream: InputStream?, contentLength: Int): Int {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
val capacity = if (contentLength > 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
|
||||
}
|
||||
}
|
@ -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..<hsize) {
|
||||
htab[i] = -1
|
||||
}
|
||||
}
|
||||
|
||||
fun compress(init_bits: Int, outs: OutputStream) {
|
||||
var fcode: Int
|
||||
var i /* = 0 */: Int
|
||||
var ent: Int
|
||||
var disp: Int
|
||||
|
||||
// Set up the globals: g_init_bits - initial number of bits
|
||||
g_init_bits = init_bits
|
||||
|
||||
// Set up the necessary values
|
||||
clear_flg = false
|
||||
n_bits = g_init_bits
|
||||
maxcode = MAXCODE(n_bits)
|
||||
|
||||
ClearCode = 1 shl (init_bits - 1)
|
||||
EOFCode = ClearCode + 1
|
||||
free_ent = ClearCode + 2
|
||||
|
||||
a_count = 0 // clear packet
|
||||
|
||||
ent = pixelArray.first()
|
||||
|
||||
var hshift = 0
|
||||
run {
|
||||
fcode = hsize
|
||||
while (fcode < 65536) {
|
||||
++hshift
|
||||
fcode *= 2
|
||||
}
|
||||
}
|
||||
hshift = 8 - hshift // set hash code range bound
|
||||
|
||||
val hsizeReg: Int = hsize
|
||||
cl_hash(hsizeReg) // clear hash table
|
||||
|
||||
output(ClearCode, outs)
|
||||
|
||||
for (pixi in pixelArray.indices) {
|
||||
fcode = (pixelArray[pixi] shl maxbits) + ent
|
||||
i = (pixelArray[pixi] shl hshift) xor ent // xor hashing
|
||||
|
||||
if (htab[i] == fcode) {
|
||||
ent = codetab[i]
|
||||
continue
|
||||
} else if (htab[i] >= 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)
|
||||
}
|
||||
}
|
@ -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<IntArray>
|
||||
|
||||
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..<netsize) {
|
||||
index[network[i][3]] = i
|
||||
}
|
||||
var k = 0
|
||||
for (i in 0..<netsize) {
|
||||
val j = index[i]
|
||||
map[k++] = network[j][0].toByte()
|
||||
map[k++] = network[j][1].toByte()
|
||||
map[k++] = network[j][2].toByte()
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/*
|
||||
* Insertion sort of network and building of netindex[0..255] (to do after
|
||||
* unbias)
|
||||
* -------------------------------------------------------------------------------
|
||||
*/
|
||||
fun inxbuild() {
|
||||
|
||||
var i: Int
|
||||
var j: Int
|
||||
var smallpos: Int
|
||||
var smallval: Int
|
||||
var p: IntArray
|
||||
var q: IntArray
|
||||
var previouscol: Int
|
||||
var startpos: Int
|
||||
|
||||
previouscol = 0
|
||||
startpos = 0
|
||||
run {
|
||||
i = 0
|
||||
while (i < netsize) {
|
||||
p = network[i]
|
||||
smallpos = i
|
||||
smallval = p[1] /* index on g */
|
||||
/* find smallest in i..netsize-1 */
|
||||
run {
|
||||
j = i + 1
|
||||
while (j < netsize) {
|
||||
q = network[j]
|
||||
if (q[1] < smallval) {
|
||||
/* index on g */
|
||||
smallpos = j
|
||||
smallval = q[1] /* index on g */
|
||||
}
|
||||
j++
|
||||
}
|
||||
}
|
||||
q = network[smallpos]
|
||||
/* swap p (i) and q (smallpos) entries */
|
||||
if (i != smallpos) {
|
||||
j = q[0]
|
||||
q[0] = p[0]
|
||||
p[0] = j
|
||||
j = q[1]
|
||||
q[1] = p[1]
|
||||
p[1] = j
|
||||
j = q[2]
|
||||
q[2] = p[2]
|
||||
p[2] = j
|
||||
j = q[3]
|
||||
q[3] = p[3]
|
||||
p[3] = j
|
||||
}
|
||||
/* smallval entry is now in position i */
|
||||
if (smallval != previouscol) {
|
||||
netindex[previouscol] = (startpos + i) shr 1
|
||||
run {
|
||||
j = previouscol + 1
|
||||
while (j < smallval) {
|
||||
netindex[j] = i
|
||||
j++
|
||||
}
|
||||
}
|
||||
previouscol = smallval
|
||||
startpos = i
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
netindex[previouscol] = (startpos + maxnetpos) shr 1
|
||||
run {
|
||||
j = previouscol + 1
|
||||
while (j < 256) {
|
||||
netindex[j] = maxnetpos
|
||||
j++
|
||||
}
|
||||
} /* really 256 */
|
||||
}
|
||||
|
||||
/*
|
||||
* Main Learning Loop ------------------
|
||||
*/
|
||||
fun learn() {
|
||||
|
||||
var j: Int
|
||||
var b: Int
|
||||
var g: Int
|
||||
var r: Int
|
||||
var rad: Int
|
||||
var delta: Int
|
||||
|
||||
if (lengthcount < minpicturebytes)
|
||||
samplefac = 1
|
||||
alphadec = 30 + ((samplefac - 1) / 3)
|
||||
var pix = 0
|
||||
val samplePixels: Int = lengthcount / (3 * samplefac)
|
||||
delta = samplePixels / ncycles
|
||||
var alpha: Int = initalpha
|
||||
var radius: Int = initradius
|
||||
|
||||
rad = radius shr radiusbiasshift
|
||||
if (rad <= 1) rad = 0
|
||||
|
||||
run {
|
||||
val rad2 = rad * rad
|
||||
for (index in 0..<rad) {
|
||||
radpower[index] = alpha * (((rad2 - index * index) * radbias) / rad2)
|
||||
}
|
||||
}
|
||||
|
||||
val step: Int = if (lengthcount < minpicturebytes)
|
||||
3
|
||||
else if ((lengthcount % prime1) != 0)
|
||||
3 * prime1
|
||||
else {
|
||||
if ((lengthcount % prime2) != 0)
|
||||
3 * prime2
|
||||
else {
|
||||
if ((lengthcount % prime3) != 0)
|
||||
3 * prime3
|
||||
else
|
||||
3 * prime4
|
||||
}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
while (i < samplePixels) {
|
||||
b = (thepicture[pix + 0].toInt() and 255) shl netbiasshift
|
||||
g = (thepicture[pix + 1].toInt() and 255) shl netbiasshift
|
||||
r = (thepicture[pix + 2].toInt() and 255) shl netbiasshift
|
||||
j = contest(b, g, r)
|
||||
|
||||
altersingle(alpha, j, b, g, r)
|
||||
if (rad != 0)
|
||||
alterneigh(rad, j, b, g, r) /* alter neighbours */
|
||||
|
||||
pix += step
|
||||
if (pix >= 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..<rad) {
|
||||
radpower[index] = alpha * (((rad2 - index * index) * radbias) / rad2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Search for BGR values 0..255 (after net is unbiased) and return colour
|
||||
* index
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
fun map(b: Int, g: Int, r: Int): Int {
|
||||
|
||||
var i: Int
|
||||
var j: Int
|
||||
var dist: Int
|
||||
var a: Int
|
||||
var bestd: Int
|
||||
var p: IntArray
|
||||
var best: Int
|
||||
|
||||
bestd = 1000 /* biggest possible dist is 256*3 */
|
||||
best = -1
|
||||
i = netindex[g] /* index on g */
|
||||
j = i - 1 /* start at netindex[g] and work outwards */
|
||||
|
||||
while ((i < netsize) || (j >= 0)) {
|
||||
if (i < netsize) {
|
||||
p = network[i]
|
||||
dist = p[1] - g /* inx key */
|
||||
if (dist >= bestd)
|
||||
i = netsize /* stop iter */
|
||||
else {
|
||||
i++
|
||||
if (dist < 0)
|
||||
dist = -dist
|
||||
a = p[0] - b
|
||||
if (a < 0)
|
||||
a = -a
|
||||
dist += a
|
||||
if (dist < bestd) {
|
||||
a = p[2] - r
|
||||
if (a < 0)
|
||||
a = -a
|
||||
dist += a
|
||||
if (dist < bestd) {
|
||||
bestd = dist
|
||||
best = p[3]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (j >= 0) {
|
||||
p = network[j]
|
||||
dist = g - p[1] /* inx key - reverse dif */
|
||||
if (dist >= bestd)
|
||||
j = -1 /* stop iter */
|
||||
else {
|
||||
j--
|
||||
if (dist < 0)
|
||||
dist = -dist
|
||||
a = p[0] - b
|
||||
if (a < 0)
|
||||
a = -a
|
||||
dist += a
|
||||
if (dist < bestd) {
|
||||
a = p[2] - r
|
||||
if (a < 0)
|
||||
a = -a
|
||||
dist += a
|
||||
if (dist < bestd) {
|
||||
bestd = dist
|
||||
best = p[3]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (best)
|
||||
}
|
||||
|
||||
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) {
|
||||
network[i][0] = network[i][0] shr netbiasshift
|
||||
network[i][1] = network[i][1] shr netbiasshift
|
||||
network[i][2] = network[i][2] shr netbiasshift
|
||||
network[i][3] = i /* record colour no */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in
|
||||
* radpower[|i-j|]
|
||||
* ---------------------------------------------------------------------------------
|
||||
*/
|
||||
private fun alterneigh(rad: Int, i: Int, b: Int, g: Int, r: Int) {
|
||||
|
||||
var lo: Int
|
||||
var hi: Int
|
||||
var a: Int
|
||||
var p: IntArray
|
||||
|
||||
lo = i - rad
|
||||
if (lo < -1)
|
||||
lo = -1
|
||||
hi = i + rad
|
||||
if (hi > 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..<netsize) {
|
||||
n = network[i]
|
||||
dist = n[0] - b
|
||||
if (dist < 0)
|
||||
dist = -dist
|
||||
a = n[1] - g
|
||||
if (a < 0)
|
||||
a = -a
|
||||
dist += a
|
||||
a = n[2] - r
|
||||
if (a < 0)
|
||||
a = -a
|
||||
dist += a
|
||||
if (dist < bestd) {
|
||||
bestd = dist
|
||||
bestpos = i
|
||||
}
|
||||
biasdist = dist - ((bias[i]) shr (intbiasshift - netbiasshift))
|
||||
if (biasdist < bestbiasd) {
|
||||
bestbiasd = biasdist
|
||||
bestbiaspos = i
|
||||
}
|
||||
betafreq = (freq[i] shr betashift)
|
||||
freq[i] -= betafreq
|
||||
bias[i] += (betafreq shl gammashift)
|
||||
}
|
||||
|
||||
freq[bestpos] += beta
|
||||
bias[bestpos] -= betagamma
|
||||
return (bestbiaspos)
|
||||
}
|
||||
}
|
@ -17,6 +17,9 @@
|
||||
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
include(":feature:gif-tools")
|
||||
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
includeBuild("build-logic")
|
||||
|
Reference in New Issue
Block a user