Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
package net.perfectdreams.discordbanner.gifs
import java.awt.Color
import java.awt.image.BufferedImage
import java.awt.image.DataBufferByte
import java.io.IOException
import java.io.OutputStream
import kotlin.experimental.and
/**
* Class AnimatedGifEncoder - Encodes a GIF file consisting of one or more
* frames.
*
* <pre>
* Example:
* AnimatedGifEncoder e = new AnimatedGifEncoder();
* e.start(outputFileName);
* e.setDelay(1000); // 1 frame per sec
* e.addFrame(image1);
* e.addFrame(image2);
* e.finish();
</pre> *
*
* No copyright asserted on the source code of this class. May be used for any
* purpose, however, refer to the Unisys LZW patent for restrictions on use of
* the associated LZWEncoder class. Please forward any corrections to
* kweiner@fmsware.com.
*
* @author Kevin Weiner, FM Software
* @version 1.03 November 2003
*/
class AnimatedGifEncoder(
private val out: OutputStream,
) {
protected var width // image size
= 0
protected var height = 0
var transparent: Color? = null // transparent color if given
protected var transIndex // transparent index in color table
= 0
var repeat = -1 // no repeat
var delay = 0 // frame delay (hundredths)
protected var started = false // ready to output frames
protected var image // current frame
: BufferedImage? = null
protected var pixels // BGR byte array from frame
: ByteArray? = null
protected var indexedPixels // converted frame indexed to palette
: ByteArray? = null
protected var colorDepth // number of bit planes
= 0
protected var colorTab // RGB palette
: ByteArray? = null
protected var usedEntry = BooleanArray(256) // active palette entries
protected var palSize = 7 // color table size (bits-1)
protected var dispose = -1 // disposal code (-1 = use default)
protected var closeStream = false // close stream when finished
protected var firstFrame = true
protected var sizeSet = false // if false, get size from first frame
protected var sample = 1 // default sample interval for quantizer
/**
* 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: BufferedImage?, frameDelay: Int = delay, xPosition: Int = 0, yPosition: Int = 0): Boolean {
if (im == null || !started) {
return false
}
var ok = true
try {
if (!sizeSet) {
// use first frame's size
setSize(im.width, im.height)
}
image = im
convertImagePixels(im, im.width, im.height) // convert to correct format if necessary
analyzePixels() // build color table & map pixels
if (firstFrame) {
// Because this is the first frame, we should NOT write the hacky north-east coords
writeLSD() // logical screen descriptior
writePalette() // global color table
if (repeat >= 0) {
// use NS app extension to indicate reps
writeNetscapeExt()
}
}
writeGraphicCtrlExt(frameDelay) // write graphic control extension
writeImageDesc(im.width, im.height, xPosition, yPosition) // image descriptor
if (!firstFrame) {
writePalette() // local color table
}
writePixels(im.width, im.height) // encode and write pixel data
firstFrame = false
} catch (e: IOException) {
e.printStackTrace()
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(0x3b) // gif trailer
out!!.flush()
if (closeStream) {
out!!.close()
}
} catch (e: IOException) {
e.printStackTrace()
ok = false
}
// reset for subsequent use
transIndex = 0
out.close()
// out = null
image = null
pixels = null
indexedPixels = null
colorTab = null
closeStream = false
firstFrame = true
return ok
}
/**
* 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) {
var quality = quality
if (quality < 1) quality = 1
sample = 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 width.
*/
fun setSize(w: Int, h: Int) {
if (started && !firstFrame) return
width = w
height = h
if (width < 1) width = 320
if (height < 1) height = 240
sizeSet = true
}
/**
* Initiates GIF file creation on the given stream. The stream is not closed
* automatically.
*
* @param os
* OutputStream on which GIF images are written.
* @return false if initial write failed.
*/
fun start(): Boolean {
var ok = true
closeStream = false
try {
writeString("GIF89a") // header
} catch (e: IOException) {
ok = false
}
return ok.also { started = it }
}
/**
* Analyzes image colors and creates color map.
*/
protected fun analyzePixels() {
val len = pixels!!.size
val nPix = len / 3
val transparent = transparent
// Check if the current frame has transparent pixels
var hasTransparentPixels = false
if (transparent != null) {
for (i in 0 until pixels!!.size step 3) {
val b = pixels!![i].toInt() and 0xff
val g = pixels!![i + 1].toInt() and 0xff
val r = pixels!![i + 2].toInt() and 0xff
if (r == transparent.red && g == transparent.green && b == transparent.blue) {
hasTransparentPixels = true
break
}
}
}
indexedPixels = ByteArray(nPix)
val nq = NeuQuant(pixels!!, len, sample)
// initialize quantizer
colorTab = nq.process() // create reduced palette
// convert map from BGR to RGB
run {
var i: Int = 0
while (i < colorTab!!.size) {
val temp: Byte = colorTab!!.get(i)
colorTab!![i] = colorTab!!.get(i + 2)
colorTab!![i + 2] = temp
usedEntry[i / 3] = false
i += 3
}
}
// map image pixels to new palette
var k = 0
for (i in 0 until nPix) {
val index = nq.map(pixels!![k++].toInt() and 0xff, pixels!![k++].toInt() and 0xff, pixels!![k++].toInt() and 0xff)
usedEntry[index] = true
indexedPixels!![i] = index.toByte()
}
pixels = null
colorDepth = 8
palSize = 7
// Get closest match to transparent color if specified and if the current frame has transparent pixels
// We check if they are present to avoid issues if the frame only has a solid color without any transparency
if (hasTransparentPixels) {
transIndex = findClosest(transparent!!)
} else {
transIndex = -1 // reset transparent index to avoid issues (magic value)
}
}
/**
* Returns index of palette color closest to c
*
*/
protected fun findClosest(c: Color): Int {
if (colorTab == null) return -1
val r = c.red
val g = c.green
val b = c.blue
var minpos = 0
var dmin = 256 * 256 * 256
val len = colorTab!!.size
var i = 0
while (i < len) {
val dr: Int = r - (colorTab!![i++].toInt() and 0xff)
val dg: Int = g - (colorTab!![i++].toInt() and 0xff)
val db: Int = b - (colorTab!![i].toInt() and 0xff)
val d = dr * dr + dg * dg + db * db
val index = i / 3
if (usedEntry[index] && d < dmin) {
dmin = d
minpos = index
}
i++
}
return minpos
}// create new image with right size/format
/**
* Extracts image pixels into byte array "pixels"
*/
protected fun convertImagePixels(image: BufferedImage, width: Int, height: Int) {
val w = image.width
val h = image.height
val type = image.type
val tempImage = if (w != width || h != height || type != BufferedImage.TYPE_3BYTE_BGR) {
// create new image with right size/format
val temp = BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR)
val g = temp.createGraphics()
g.drawImage(image, 0, 0, null)
temp
} else image
// println("Width: ${tempImage.width}; Height: ${tempImage.height}")
pixels = (tempImage.raster.dataBuffer as DataBufferByte).data
}
/**
* Writes Graphic Control Extension
*/
@Throws(IOException::class)
protected fun writeGraphicCtrlExt(frameDelay: Int = delay) {
out.write(0x21) // extension introducer
out.write(0xf9) // GCE label
out.write(4) // data block size
val transp: Int
var disp: Int
if (transparent == null) {
transp = 0
disp = 0 // dispose = no action
} else {
transp = if (transIndex != -1) 1 else 0 // If the trans index is our magic value, then it means that we do not have transparency on this frame!
// TODO: Allow customizing the dispose method
disp = 0 // do not dispose (original: 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
transp
) // 8 transparency flag
writeShort(frameDelay) // delay x 1/100 sec
out!!.write(transIndex) // transparent color index
out!!.write(0) // block terminator
}
/**
* Writes Image Descriptor
*/
@Throws(IOException::class)
protected fun writeImageDesc(width: Int, height: Int, xPosition: Int, yPosition: Int) {
out!!.write(0x2c) // image separator
// image position x,y = 0,0
if (firstFrame) { // The first frame NEEDS to have coordinates 0, 0, if not the animation won't play!
writeShort(0)
writeShort(0)
} else {
writeShort(xPosition)
writeShort(yPosition)
}
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(
(0x80 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
*/
@Throws(IOException::class)
protected fun writeLSD() {
// logical screen size
writeShort(width)
writeShort(height)
// packed fields
out!!.write(
((0x80 or // 1 : global color table flag = 1 (gct used)
0x70 or // 2-4 : color resolution = 7
0x00 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.
*/
@Throws(IOException::class)
protected fun writeNetscapeExt() {
out!!.write(0x21) // extension introducer
out!!.write(0xff) // 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
*/
@Throws(IOException::class)
protected fun writePalette() {
out!!.write(colorTab, 0, colorTab!!.size)
val n = (3 * 256) - colorTab!!.size
for (i in 0 until n) {
out!!.write(0)
}
}
/**
* Encodes and writes pixel data
*/
@Throws(IOException::class)
protected fun writePixels(width: Int, height: Int) {
val encoder = LZWEncoder(width, height, (indexedPixels)!!, colorDepth)
encoder.encode((out)!!)
}
/**
* Write 16-bit value to output stream, LSB first
*/
@Throws(IOException::class)
protected fun writeShort(value: Int) {
out!!.write(value and 0xff)
out!!.write((value shr 8) and 0xff)
}
/**
* Writes string to output stream
*/
@Throws(IOException::class)
protected fun writeString(s: String) {
for (i in 0 until s.length) {
out.write(s[i].toInt())
}
}
}
package net.perfectdreams.discordbanner.gifs
import net.perfectdreams.discordbanner.DiscordBannerGenerator
import java.awt.Color
import java.awt.image.BufferedImage
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import javax.imageio.IIOException
import javax.imageio.ImageIO
import javax.imageio.stream.FileImageOutputStream
class GIFOverdrive(
val outputStream: OutputStream,
val frames: MutableList<DiscordBannerGenerator.MemoryFrame>,
val delay: Int = 3
) {
private val TRANSPARENT_COLOR = Color(255, 0, 255)
private val DELAY = delay
private val DROP_FRAME_PIXEL_CHECK_THRESHOLD = 1.0 // original: 1.0
// we decrease this to avoid dropping colors that shouldn't be dropped
private val DROP_PIXEL_SIMILAR_DISTANCE = 1 // original: 3
fun convertFrames() {
println("GIFOverdrive!")
println("Frames: ${frames.size}")
println("Joining to a gifFrames list...")
val gifFrames = frames.map { GIFFrame(it.image, DELAY, it.dropPixelSimilarDistanceOverride) }.toMutableList()
val firstFrame = gifFrames.first().image
val width = firstFrame.width
val height = firstFrame.height
val pixelCount = width * height
val droppedDuplicatedFrames = mutableListOf<GIFFrame>(gifFrames.first()) // the first is always skipped
println("Dropping duplicate frames...")
for (index in gifFrames.indices) {
val previousFrame = gifFrames.getOrNull(index - 1) ?: continue
val currentFrame = gifFrames.getOrNull(index) ?: break
val previousFrameImage = previousFrame.image
val currentFrameImage = currentFrame.image
val intermediaryFrame = BufferedImage(currentFrame.image.width, currentFrame.image.height, BufferedImage.TYPE_INT_ARGB)
var equalPixelCount = 0
for (x in 0 until intermediaryFrame.width) {
for (y in 0 until intermediaryFrame.height) {
if (previousFrameImage.getRGB(x, y) == currentFrameImage.getRGB(x, y)) {
equalPixelCount++
}
}
}
val equalFramesPercentage = (equalPixelCount.toDouble() / pixelCount)
println("Equal Pixels for Frame $index: $equalPixelCount (${equalFramesPercentage * 100})")
if (equalFramesPercentage >= DROP_FRAME_PIXEL_CHECK_THRESHOLD) { // 450*180
println("Current frame $index pixel count is equal to the previous frame, dropping current frame...")
droppedDuplicatedFrames.last().delay += DELAY
} else {
droppedDuplicatedFrames.add(currentFrame)
}
}
println("Calculating intermediary frames...")
val intermediaryFrames = mutableListOf<IntermediaryGIFFrame>(
IntermediaryGIFFrame(
droppedDuplicatedFrames.first().image,
droppedDuplicatedFrames.first().delay,
droppedDuplicatedFrames.first().image.width,
droppedDuplicatedFrames.first().image.height
)
) // The first will always exist
for (index in droppedDuplicatedFrames.indices.drop(1)) {
println("Writing intermediary frames for $index...")
val previousFrame = droppedDuplicatedFrames[index - 1] // will never be null!
val currentFrame = droppedDuplicatedFrames[index]
val previousFrameImage = previousFrame.image
val currentFrameImage = currentFrame.image
val intermediaryFrame = BufferedImage(currentFrame.image.width, currentFrame.image.height, BufferedImage.TYPE_INT_ARGB)
val graphics = intermediaryFrame.createGraphics()
graphics.color = TRANSPARENT_COLOR
graphics.fillRect(0, 0, width, height)
for (x in 0 until intermediaryFrame.width) {
for (y in 0 until intermediaryFrame.height) {
val previousRgb = previousFrameImage.getRGB(x, y)
val currentRgb = currentFrameImage.getRGB(x, y)
if (previousRgb != currentRgb) {
val previousColor = Color(previousRgb)
val newColor = Color(currentRgb)
// TODO: Remove this, this is only a workaround because the TS1 recording is too "dirty" to be used in a GIF
val similarDistance = currentFrame.dropPixelSimilarDistanceOverride ?: DROP_PIXEL_SIMILAR_DISTANCE
if (
previousColor.red in (newColor.red - similarDistance)..(newColor.red + similarDistance) &&
previousColor.green in (newColor.green - similarDistance)..(newColor.green + similarDistance) &&
previousColor.blue in (newColor.blue - similarDistance)..(newColor.blue + similarDistance)
) {} else {
intermediaryFrame.setRGB(x, y, currentFrameImage.getRGB(x, y))
}
}
}
}
var topXPosition: Int? = null
var topYPosition: Int? = null
var bottomXPosition: Int? = null
var bottomYPosition: Int? = null
// Find the top x/top y
for (x in 0 until intermediaryFrame.width) {
for (y in 0 until intermediaryFrame.height) {
val color = Color(intermediaryFrame.getRGB(x, y))
if (color != TRANSPARENT_COLOR) {
if (topXPosition == null || topXPosition > x)
topXPosition = x
if (topYPosition == null || topYPosition > y)
topYPosition = y
}
if (color != TRANSPARENT_COLOR) {
if (bottomXPosition == null || x > bottomXPosition)
bottomXPosition = x
if (bottomYPosition == null || y > bottomYPosition)
bottomYPosition = y
}
}
}
val finalTopXPosition = topXPosition ?: 0
val finalTopYPosition = topYPosition ?: 0
val finalBottomXPosition = (bottomXPosition ?: 0) + 1
val finalBottomYPosition = (bottomYPosition ?: 0) + 1
println("Top X Position: $finalTopXPosition; Top Y Position: $finalTopYPosition")
println("Bottom X Position: $finalBottomXPosition; Bottom Y Position: $finalBottomYPosition")
val newWidth = finalBottomXPosition - finalTopXPosition
val newHeight = finalBottomYPosition - finalTopYPosition
println("Frame $index will have its content pasted from ($finalTopXPosition, $finalTopYPosition) with ($newWidth, $newHeight) to ($finalTopXPosition, $finalTopYPosition)")
val subImage = intermediaryFrame.getSubimage(finalTopXPosition, finalTopYPosition, newWidth, newHeight)
intermediaryFrames.add(
IntermediaryGIFFrame(
subImage,
currentFrame.delay,
finalTopXPosition,
finalTopYPosition
)
)
// graphics.color = Color.RED
// graphics.drawString("Frame $index; Top: $finalTopXPosition, $finalTopYPosition; Bottom: $finalBottomXPosition, $finalBottomYPosition", 80, 80)
// graphics.color = Color(20, 200, 255, 100)
// graphics.fillRect(finalTopXPosition, finalTopYPosition, newWidth, newHeight)
// write intermediary
ImageIO.write(
intermediaryFrame,
"png",
File("L:\\RandomProjects\\DiscordBanner\\intermediary\\$index.png")
)
}
println("Rendering GIF...")
if (false) {
for ((index, frame) in intermediaryFrames.filter { it.delay != 0 }.withIndex()) { // no need to render frames that have delay = 0
println("Frame $index/${intermediaryFrames.size}: ${frame.image.width}x${frame.image.height}")
val outputStreamGif = FileOutputStream(File("L:\\RandomProjects\\DiscordBanner\\encoded_gif_frames\\$index.gif"))
val encoder = AnimatedGifEncoder(outputStreamGif)
encoder.delay = DELAY
encoder.transparent = TRANSPARENT_COLOR
encoder.repeat = 0
encoder.start()
encoder.addFrame(
frame.image,
frame.delay,
frame.xPosition,
frame.yPosition
)
val outputStream = outputStreamGif
encoder.finish()
}
// encoder.finish()
} else {
val encoder = AnimatedGifEncoder(outputStream)
encoder.delay = DELAY
encoder.transparent = TRANSPARENT_COLOR
encoder.repeat = 0
encoder.start()
var previousSize = 0L
for ((index, frame) in intermediaryFrames.filter { it.delay != 0 }.withIndex()) { // no need to render frames that have delay = 0
println("Frame $index/${intermediaryFrames.size}: ${frame.image.width}x${frame.image.height}")
encoder.addFrame(
frame.image,
frame.delay,
frame.xPosition,
frame.yPosition
)
val outputStream = outputStream
if (outputStream is FileOutputStream) {
val currentSize = outputStream.channel.size()
println("Frame $index size is ${(currentSize - previousSize).toDouble() / 1000}Kb; Total size is: $currentSize")
previousSize = currentSize
}
}
encoder.finish()
}
}
data class GIFFrame(val image: BufferedImage, var delay: Int, val dropPixelSimilarDistanceOverride: Int? = null)
data class IntermediaryGIFFrame(
val image: BufferedImage,
var delay: Int,
val xPosition: Int,
val yPosition: Int
)
}
package net.perfectdreams.discordbanner.gifs
import java.io.IOException
import java.io.OutputStream
// ==============================================================================
// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott.
// K Weiner 12/00
internal class LZWEncoder(
private val imgW: Int,
private val imgH: Int,
private val pixAry: ByteArray,
color_depth: Int
) {
private val initCodeSize: Int
private var remaining = 0
private var curPixel = 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 // number of bits/code
= 0
var maxbits = BITS // user settable max # bits/code
var maxcode // maximum code, given n_bits
= 0
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 = 0
var ClearCode = 0
var EOFCode = 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 = intArrayOf(
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF,
0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
)
// Number of characters so far in this 'packet'
var a_count = 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.
@Throws(IOException::class)
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
@Throws(IOException::class)
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 until hsize) htab[i] = -1
}
@Throws(IOException::class)
fun compress(init_bits: Int, outs: OutputStream) {
var fcode: Int
var i /* = 0 */: Int
var c: Int
var ent: Int
var disp: Int
val hsize_reg: Int
var hshift: 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 = nextPixel()
hshift = 0
fcode = hsize
while (fcode < 65536) {
++hshift
fcode *= 2
}
hshift = 8 - hshift // set hash code range bound
hsize_reg = hsize
cl_hash(hsize_reg) // clear hash table
output(ClearCode, outs)
outer_loop@ while (nextPixel().also { c = it } != EOF) {
fcode = (c shl maxbits) + ent
i = c shl hshift xor ent // xor hashing
if (htab[i] == fcode) {
ent = codetab[i]
continue
} else if (htab[i] >= 0) // non-empty slot
{
disp = hsize_reg - i // secondary hash (after G. Knott)
if (i == 0) disp = 1
do {
if (disp.let { i -= it; i } < 0) i += hsize_reg
if (htab[i] == fcode) {
ent = codetab[i]
continue@outer_loop
}
} while (htab[i] >= 0)
}
output(ent, outs)
ent = c
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)
}
// ----------------------------------------------------------------------------
@Throws(IOException::class)
fun encode(os: OutputStream) {
os.write(initCodeSize) // write "initial code size" byte
remaining = imgW * imgH // reset navigation variables
curPixel = 0
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
@Throws(IOException::class)
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
}
// ----------------------------------------------------------------------------
// Return the next pixel from the image
// ----------------------------------------------------------------------------
private fun nextPixel(): Int {
if (remaining == 0) return EOF
--remaining
val pix = pixAry[curPixel++]
return pix.toInt() and 0xff
}
@Throws(IOException::class)
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 0xff).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(g_init_bits.also { n_bits = it })
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 0xff).toByte(), outs)
cur_accum = cur_accum shr 8
cur_bits -= 8
}
flush_char(outs)
}
}
companion object {
private const val EOF = -1
// 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 {
initCodeSize = Math.max(2, color_depth)
}
}
package net.perfectdreams.discordbanner.gifs
import java.lang.Exception
/*
* 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.
*/
/*
* 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
internal class NeuQuant(thepic: ByteArray, len: Int, sample: Int) {
protected var alphadec /* biased by 10 bits */ = 0
/*
* Types and Global Variables --------------------------
*/
protected var thepicture /* the input image itself */: ByteArray
protected var lengthcount /* lengthcount = H*W*3 */: Int
protected var samplefac /* sampling factor 1..30 */: Int
// typedef int pixel[4]; /* BGRc */
protected var network /* the network itself - [netsize][4] */: Array<IntArray?>
protected var netindex = IntArray(256)
/* for network lookup - really 256 */
protected var bias = IntArray(netsize)
/* bias and freq arrays for learning */
protected var freq = IntArray(netsize)
protected var radpower = IntArray(initrad)
fun colorMap(): ByteArray {
val map = ByteArray(3 * netsize)
val index = IntArray(netsize)
for (i in 0 until netsize) index[network[i]!![3]] = i
var k = 0
for (i in 0 until 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
i = 0
while (i < netsize) {
p = network[i]
smallpos = i
smallval = p!![1] /* index on g */
/* find smallest in i..netsize-1 */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
j = previouscol + 1
while (j < smallval) {
netindex[j] = i
j++
}
previouscol = smallval
startpos = i
}
i++
}
netindex[previouscol] = startpos + maxnetpos shr 1
j = previouscol + 1
while (j < 256) {
netindex[j] = maxnetpos /* really 256 */
j++
}
}
/*
* Main Learning Loop ------------------
*/
fun learn() {
var i: Int
var j: Int
var b: Int
var g: Int
var r: Int
var radius: Int
var rad: Int
var alpha: Int
val step: Int
var delta: Int
val samplepixels: Int
val p: ByteArray
var pix: Int
val lim: Int
if (lengthcount < minpicturebytes) samplefac = 1
alphadec = 30 + (samplefac - 1) / 3
p = thepicture
pix = 0
lim = lengthcount
samplepixels = lengthcount / (3 * samplefac)
delta = samplepixels / ncycles
alpha = initalpha
radius = initradius
rad = radius shr radiusbiasshift
if (rad <= 1) rad = 0
i = 0
while (i < rad) {
radpower[i] = alpha * ((rad * rad - i * i) * radbias / (rad * rad))
i++
}
// fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad);
step =
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
}
}
i = 0
while (i < samplepixels) {
b = p[pix + 0].toInt() and 0xff shl netbiasshift
g = p[pix + 1].toInt() and 0xff shl netbiasshift
r = p[pix + 2].toInt() and 0xff 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 >= lim) 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
j = 0
while (j < rad) {
radpower[j] = alpha * ((rad * rad - j * j) * radbias / (rad * rad))
j++
}
}
}
// fprintf(stderr,"finished 1D learning: final alpha=%f
// !\n",((float)alpha)/initalpha);
}
/*
* 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() {
var i: Int
var j: Int
i = 0
while (i < 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 */
i++
}
}
/*
* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in
* radpower[|i-j|]
* ---------------------------------------------------------------------------------
*/
protected fun alterneigh(rad: Int, i: Int, b: Int, g: Int, r: Int) {
var j: Int
var k: Int
var lo: Int
var hi: Int
var a: Int
var m: Int
var p: IntArray?
lo = i - rad
if (lo < -1) lo = -1
hi = i + rad
if (hi > netsize) hi = netsize
j = i + 1
k = i - 1
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 (e: 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 (e: Exception) {
}
}
}
}
/*
* Move neuron i towards biased (b,g,r) by factor alpha
* ----------------------------------------------------
*/
protected fun altersingle(alpha: Int, i: Int, b: Int, g: Int, r: Int) {
/* alter hit neuron */
val n = network[i]
n!![0] -= alpha * (n!![0] - b) / initalpha
n[1] -= alpha * (n[1] - g) / initalpha
n[2] -= alpha * (n[2] - r) / initalpha
}
/*
* Search for biased BGR values ----------------------------
*/
protected 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 i: Int
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
i = 0
while (i < 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
i++
}
freq[bestpos] += beta
bias[bestpos] -= betagamma
return bestbiaspos
}
companion object {
protected const val netsize = 256 /* number of colours used */
/* four primes near 500 - assume no image has a length so large */ /* that it is divisible by all four primes */
protected const val prime1 = 499
protected const val prime2 = 491
protected const val prime3 = 487
protected const val prime4 = 503
protected const val minpicturebytes = 3 * prime4
/* minimum size for input image */ /*
* Program Skeleton ---------------- [select samplefac in range 1..30] [read
* image from input file] pic = (unsigned char*) malloc(3*width*height);
* initnet(pic,3*width*height,samplefac); learn(); unbiasnet(); [write output
* image header, using writecolourmap(f)] inxbuild(); write output image using
* inxsearch(b,g,r)
*/
/*
* Network Definitions -------------------
*/
protected const val maxnetpos = netsize - 1
protected const val netbiasshift = 4 /* bias for colour values */
protected const val ncycles = 100 /* no. of learning cycles */
/* defs for freq and bias */
protected const val intbiasshift = 16 /* bias for fractions */
protected const val intbias = 1 shl intbiasshift
protected const val gammashift = 10 /* gamma = 1024 */
protected const val gamma = 1 shl gammashift
protected const val betashift = 10
protected const val beta = intbias shr betashift /* beta = 1/1024 */
protected const val betagamma = intbias shl gammashift - betashift
/* defs for decreasing radius factor */
protected const val initrad = netsize shr 3 /*
* for 256 cols, radius
* starts
*/
protected const val radiusbiasshift = 6 /* at 32.0 biased by 6 bits */
protected const val radiusbias = 1 shl radiusbiasshift
protected const val initradius = initrad * radiusbias /*
* and
* decreases
* by a
*/
protected const val radiusdec = 30 /* factor of 1/30 each cycle */
/* defs for decreasing alpha factor */
protected const val alphabiasshift = 10 /* alpha starts at 1.0 */
protected const val initalpha = 1 shl alphabiasshift
/* radbias and alpharadbias used for radpower calculation */
protected const val radbiasshift = 8
protected const val radbias = 1 shl radbiasshift
protected const val alpharadbshift = alphabiasshift + radbiasshift
protected const val alpharadbias = 1 shl alpharadbshift
}
/* radpower for precomputation */ /*
* Initialise network in range (0,0,0) to (255,255,255) and set parameters
* -----------------------------------------------------------------------
*/
init {
var i: Int
var p: IntArray?
thepicture = thepic
lengthcount = len
samplefac = sample
network = arrayOfNulls(netsize)
i = 0
while (i < netsize) {
network[i] = IntArray(4)
p = network[i]
p!![2] = (i shl netbiasshift + 8) / netsize
p!![1] = p!![2]
p!![0] = p!![1]
freq[i] = intbias / netsize /* 1/netsize */
bias[i] = 0
i++
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment