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")
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
|
include(":feature:gif-tools")
|
||||||
|
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
includeBuild("build-logic")
|
includeBuild("build-logic")
|
||||||
|
Reference in New Issue
Block a user