gif module added

This commit is contained in:
T8RIN
2024-01-27 00:53:22 +03:00
parent de25671df2
commit 9646e50421
8 changed files with 2153 additions and 0 deletions

1
feature/gif-tools/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View 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"

View 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>

View File

@ -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())
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -17,6 +17,9 @@
@file:Suppress("UnstableApiUsage") @file:Suppress("UnstableApiUsage")
include(":feature:gif-tools")
pluginManagement { pluginManagement {
repositories { repositories {
includeBuild("build-logic") includeBuild("build-logic")