diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index b522597..2c1aef3 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt b/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt index 0031a0e..f3e720d 100644 --- a/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt +++ b/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt @@ -30,6 +30,7 @@ class APNGDisassembler(val byteArray: ByteArray) { init { if (isApng(byteArray)) { + apng = Apng() val ihdr = IHDR() ihdr.parseIHDR(byteArray) maxWidth = ihdr.pngWidth @@ -47,6 +48,8 @@ class APNGDisassembler(val byteArray: ByteArray) { crC32.update(iend, 0, iend.size) cover!!.addAll(iend.toList()) cover!!.addAll(to4Bytes(crC32.value.toInt()).toList()) + + apng.cover = BitmapFactory.decodeByteArray(cover!!.toByteArray(), 0, cover!!.size) } png = ArrayList() val fcTL = fcTL(byteArray.copyOfRange(i - 4, i + 36)) @@ -205,8 +208,6 @@ class APNGDisassembler(val byteArray: ByteArray) { tnrs = byteArray.copyOfRange( i -4, i + 8 + bodySize) } } - - apng = Apng() apng.frames = pngList } else { diff --git a/apng_library/src/main/java/oupson/apng/Apng.kt b/apng_library/src/main/java/oupson/apng/Apng.kt index 92ba44d..c881ce7 100644 --- a/apng_library/src/main/java/oupson/apng/Apng.kt +++ b/apng_library/src/main/java/oupson/apng/Apng.kt @@ -2,6 +2,7 @@ package oupson.apng import android.graphics.Bitmap import android.graphics.BitmapFactory +import oupson.apng.ImageUtils.PnnQuantizer import oupson.apng.Utils.Companion.convertImage import oupson.apng.Utils.Companion.getBlend_op import oupson.apng.Utils.Companion.getDispose_op @@ -11,6 +12,7 @@ import oupson.apng.Utils.Companion.to4Bytes import oupson.apng.Utils.Companion.toByteArray import oupson.apng.chunks.IDAT import oupson.apng.exceptions.NoFrameException +import java.io.ByteArrayOutputStream import java.util.zip.CRC32 @@ -469,4 +471,18 @@ class Apng { res.addAll(to4Bytes(crc.value.toInt()).toList()) return res } + + fun optimise(quality : Int, maxColor : Int) { + val apng = Apng() + val pnn = PnnQuantizer(cover) + cover = pnn.convert(maxColor, false) + + frames.forEach { + val btm = BitmapFactory.decodeByteArray(it.byteArray, 0 , it.byteArray.size) + val pnn = PnnQuantizer(btm) + val btmOptimised = pnn.convert(maxColor, false) + apng.addFrames(btmOptimised, it.delay, it.x_offsets ?: 0, it.y_offsets ?: 0, it.dispose_op, it.blend_op) + } + frames = apng.frames + } } \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt index 9139802..aa86780 100644 --- a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt +++ b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt @@ -27,9 +27,7 @@ class ApngAnimator(val context: Context) { try { pause() play() - } catch ( e : Exception) { - e.printStackTrace() - } + } catch ( e : Exception) { } } private var bitmapBuffer: Bitmap? = null private var imageView: ImageView? = null @@ -67,9 +65,6 @@ class ApngAnimator(val context: Context) { * Load an APNG file and starts playing the animation. * @param context The current context. * @param url URL to load. - * @param animationListener The listener that will be invoked when there are specific animation events. - * @param frameDuration The duration to show each frame. If this is null then the duration specified - * in the APNG will be used instead. * @throws NotApngException */ fun loadUrl(url: URL, speed: Float? = null) { @@ -79,7 +74,6 @@ class ApngAnimator(val context: Context) { APNGDisassembler(Loader().load(context, url)).pngList.apply { draw(this) } - setupAnimationDrawableAndStart() } } @@ -88,9 +82,6 @@ class ApngAnimator(val context: Context) { /** * Load an APNG file and starts playing the animation. * @param byteArray ByteArray of the file - * @param animationListener The listener that will be invoked when there are specific animation events. - * @param frameDuration The duration to show each frame. If this is null then the duration specified - * in the APNG will be used instead. * @throws NotApngException */ fun load(byteArray: ByteArray, speed: Float? = null) { diff --git a/apng_library/src/main/java/oupson/apng/Frame.kt b/apng_library/src/main/java/oupson/apng/Frame.kt index cd0ef0c..47991d2 100644 --- a/apng_library/src/main/java/oupson/apng/Frame.kt +++ b/apng_library/src/main/java/oupson/apng/Frame.kt @@ -1,6 +1,9 @@ package oupson.apng +import android.graphics.BitmapFactory +import oupson.apng.Utils.Companion.convertImage import oupson.apng.Utils.Companion.isPng +import oupson.apng.Utils.Companion.toByteArray import oupson.apng.chunks.IDAT import oupson.apng.chunks.IHDR import oupson.apng.exceptions.NotPngException @@ -37,17 +40,19 @@ class Frame { constructor(byteArray: ByteArray) { if (isPng(byteArray)) { - this.byteArray = byteArray + val btm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + val bytes = toByteArray(btm) + this.byteArray = bytes // Get width and height for image ihdr = IHDR() - ihdr.parseIHDR(byteArray) + ihdr.parseIHDR(bytes) width = ihdr.pngWidth height = ihdr.pngHeight // Get IDAT Bytes idat = IDAT() - idat.parseIDAT(byteArray) + idat.parseIDAT(bytes) delay = 1000f @@ -59,17 +64,19 @@ class Frame { } constructor(byteArray: ByteArray, delay : Float) { if (isPng(byteArray)) { - this.byteArray = byteArray + val btm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + val bytes = toByteArray(btm) + this.byteArray = bytes // Get width and height for image ihdr = IHDR() - ihdr.parseIHDR(byteArray) + ihdr.parseIHDR(bytes) width = ihdr.pngWidth height = ihdr.pngHeight // Get IDAT Bytes idat = IDAT() - idat.parseIDAT(byteArray) + idat.parseIDAT(bytes) this.delay = delay blend_op = Utils.Companion.blend_op.APNG_BLEND_OP_SOURCE @@ -81,17 +88,19 @@ class Frame { constructor(byteArray: ByteArray, delay : Float, blend_op: Utils.Companion.blend_op, dispose_op: Utils.Companion.dispose_op) { if (isPng(byteArray)) { - this.byteArray = byteArray + val btm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + val bytes = toByteArray(btm) + this.byteArray = bytes // Get width and height for image ihdr = IHDR() - ihdr.parseIHDR(byteArray) + ihdr.parseIHDR(bytes) width = ihdr.pngWidth height = ihdr.pngHeight // Get IDAT Bytes idat = IDAT() - idat.parseIDAT(byteArray) + idat.parseIDAT(bytes) this.delay = delay @@ -107,17 +116,19 @@ class Frame { constructor(byteArray: ByteArray, delay : Float, xOffsets : Int, yOffsets : Int, blend_op: Utils.Companion.blend_op, dispose_op: Utils.Companion.dispose_op) { if (isPng(byteArray)) { - this.byteArray = byteArray + val btm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + val bytes = toByteArray(btm) + this.byteArray = bytes // Get width and height for image ihdr = IHDR() - ihdr.parseIHDR(byteArray) + ihdr.parseIHDR(bytes) width = ihdr.pngWidth height = ihdr.pngHeight // Get IDAT Bytes idat = IDAT() - idat.parseIDAT(byteArray) + idat.parseIDAT(bytes) this.delay = delay diff --git a/apng_library/src/main/java/oupson/apng/ImageUtils/PnnQuantizer.java b/apng_library/src/main/java/oupson/apng/ImageUtils/PnnQuantizer.java new file mode 100644 index 0000000..0e51e6f --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/ImageUtils/PnnQuantizer.java @@ -0,0 +1,569 @@ +package oupson.apng.ImageUtils; + +/* Fast pairwise nearest neighbor based algorithm for multilevel thresholding +Copyright (C) 2004-2016 Mark Tyler and Dmitry Groshev +Copyright (c) 2018 Miller Cy Chan +* error measure; time used is proportional to number of bins squared - WJ */ + +/* + https://github.com/mcychan/nQuant.android/blob/master/nQuant.master/src/main/java/com/android/nQuant/PnnQuantizer.java + */ + +import android.graphics.Color; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class PnnQuantizer { + protected final short SHORT_MAX = Short.MAX_VALUE; + protected final char BYTE_MAX = -Byte.MIN_VALUE + Byte.MAX_VALUE; + protected boolean hasTransparency = false, hasSemiTransparency = false; + protected int width, height; + protected int pixels[] = null; + protected Integer m_transparentColor; + protected Map closestMap = new HashMap<>(); + + public PnnQuantizer(String fname) throws IOException { + fromBitmap(fname); + } + + public PnnQuantizer(Bitmap bitmap) throws IOException{ + fromBitmap(bitmap); + } + + private void fromBitmap(Bitmap bitmap) throws IOException { + width = bitmap.getWidth(); + height = bitmap.getHeight(); + pixels = new int [width * height]; + bitmap.getPixels(pixels, 0, width, 0, 0, width, height); + } + + private void fromBitmap(String fname) throws IOException { + Bitmap bitmap = BitmapFactory.decodeFile(fname); + fromBitmap(bitmap); + } + + private static final class Pnnbin { + double ac = 0, rc = 0, gc = 0, bc = 0, err = 0; + int cnt = 0; + int nn, fw, bk, tm, mtm; + } + + protected int getColorIndex(final int c) + { + if(hasSemiTransparency) + return (Color.alpha(c) & 0xF0) << 8 | (Color.red(c) & 0xF0) << 4 | (Color.green(c) & 0xF0) | (Color.blue(c) >> 4); + if (hasTransparency) + return (Color.alpha(c) & 0x80) << 8 | (Color.red(c) & 0xF8) << 7 | (Color.green(c) & 0xF8) << 2 | (Color.blue(c) >> 3); + return (Color.red(c) & 0xF8) << 8 | (Color.green(c) & 0xFC) << 3 | (Color.blue(c) >> 3); + } + + protected double sqr(double value) + { + return value * value; + } + + private void find_nn(Pnnbin[] bins, int idx) + { + int nn = 0; + double err = 1e100; + + Pnnbin bin1 = bins[idx]; + int n1 = bin1.cnt; + double wa = bin1.ac; + double wr = bin1.rc; + double wg = bin1.gc; + double wb = bin1.bc; + for (int i = bin1.fw; i != 0; i = bins[i].fw) { + double nerr = sqr(bins[i].ac - wa) + sqr(bins[i].rc - wr) + sqr(bins[i].gc - wg) + sqr(bins[i].bc - wb); + double n2 = bins[i].cnt; + nerr *= (n1 * n2) / (n1 + n2); + if (nerr >= err) + continue; + err = nerr; + nn = i; + } + bin1.err = err; + bin1.nn = nn; + } + + private Integer[] pnnquan(final int[] pixels, int nMaxColors, boolean quan_sqrt) + { + Pnnbin[] bins = new Pnnbin[65536]; + int[] heap = new int[65537]; + double err, n1, n2; + + /* Build histogram */ + for (final int pixel : pixels) { + // !!! Can throw gamma correction in here, but what to do about perceptual + // !!! nonuniformity then? + int index = getColorIndex(pixel); + if(bins[index] == null) + bins[index] = new Pnnbin(); + Pnnbin tb = bins[index]; + tb.ac += Color.alpha(pixel); + tb.rc += Color.red(pixel); + tb.gc += Color.green(pixel); + tb.bc += Color.blue(pixel); + tb.cnt++; + } + + /* Cluster nonempty bins at one end of array */ + int maxbins = 0; + + for (int i = 0; i < bins.length; ++i) { + if (bins[i] == null) + continue; + + double d = 1.0 / (double)bins[i].cnt; + bins[i].ac *= d; + bins[i].rc *= d; + bins[i].gc *= d; + bins[i].bc *= d; + if (quan_sqrt) + bins[i].cnt = (int) Math.sqrt(bins[i].cnt); + bins[maxbins++] = bins[i]; + } + + for (int i = 0; i < maxbins - 1; i++) { + bins[i].fw = (i + 1); + bins[i + 1].bk = i; + } + // !!! Already zeroed out by calloc() + // bins[0].bk = bins[i].fw = 0; + + int h, l, l2; + /* Initialize nearest neighbors and build heap of them */ + for (int i = 0; i < maxbins; i++) { + find_nn(bins, i); + /* Push slot on heap */ + err = bins[i].err; + for (l = ++heap[0]; l > 1; l = l2) { + l2 = l >> 1; + if (bins[h = heap[l2]].err <= err) + break; + heap[l] = h; + } + heap[l] = i; + } + + /* Merge bins which increase error the least */ + int extbins = maxbins - nMaxColors; + for (int i = 0; i < extbins; ) { + Pnnbin tb = null; + /* Use heap to find which bins to merge */ + for (;;) { + int b1 = heap[1]; + tb = bins[b1]; /* One with least error */ + /* Is stored error up to date? */ + if ((tb.tm >= tb.mtm) && (bins[tb.nn].mtm <= tb.tm)) + break; + if (tb.mtm == 0xFFFF) /* Deleted node */ + b1 = heap[1] = heap[heap[0]--]; + else /* Too old error value */ + { + find_nn(bins, b1); + tb.tm = i; + } + /* Push slot down */ + err = bins[b1].err; + for (l = 1; (l2 = l + l) <= heap[0]; l = l2) { + if ((l2 < heap[0]) && (bins[heap[l2]].err > bins[heap[l2 + 1]].err)) + l2++; + if (err <= bins[h = heap[l2]].err) + break; + heap[l] = h; + } + heap[l] = b1; + } + + /* Do a merge */ + Pnnbin nb = bins[tb.nn]; + n1 = tb.cnt; + n2 = nb.cnt; + double d = 1.0 / (n1 + n2); + tb.ac = d * (n1 * tb.ac + n2 * nb.ac); + tb.rc = d * (n1 * tb.rc + n2 * nb.rc); + tb.gc = d * (n1 * tb.gc + n2 * nb.gc); + tb.bc = d * (n1 * tb.bc + n2 * nb.bc); + tb.cnt += nb.cnt; + tb.mtm = ++i; + + /* Unchain deleted bin */ + bins[nb.bk].fw = nb.fw; + bins[nb.fw].bk = nb.bk; + nb.mtm = 0xFFFF; + } + + /* Fill palette */ + List palette = new ArrayList<>(); + short k = 0; + for (int i = 0;; ++k) { + int alpha = (int) Math.rint(bins[i].ac); + palette.add(Color.argb(alpha, (int) Math.rint(bins[i].rc), (int) Math.rint(bins[i].gc), (int) Math.rint(bins[i].bc))); + if (hasTransparency && palette.get(k).equals(m_transparentColor)) { + Integer temp = palette.get(0); + palette.set(0, palette.get(k)); + palette.set(k, temp); + } + + if ((i = bins[i].fw) == 0) + break; + } + + return palette.toArray(new Integer[0]); + } + + private short nearestColorIndex(final Integer[] palette, final int[] squares3, final int c) + { + short k = 0; + int curdist, mindist = SHORT_MAX; + for (short i=0; i mindist) + continue; + + int rdist = Math.abs(Color.red(c2) - Color.red(c)); + curdist += squares3[rdist]; + if (curdist > mindist) + continue; + + int gdist = Math.abs(Color.green(c2) - Color.green(c)); + curdist += squares3[gdist]; + if (curdist > mindist) + continue; + + int bdist = Math.abs(Color.blue(c2) - Color.blue(c)); + curdist += squares3[bdist]; + if (curdist > mindist) + continue; + + mindist = curdist; + k = i; + } + return k; + } + + private short closestColorIndex(final Integer[] palette, final int[] squares3, final int c) + { + short k = 0; + short[] closest = new short[5]; + short[] got = closestMap.get(c); + if (got == null) { + closest[2] = closest[3] = SHORT_MAX; + + for (; k < palette.length; k++) { + int c2 = palette[k]; + + closest[4] = (short) (Math.abs(Color.alpha(c) - Color.alpha(c2)) + Math.abs(Color.red(c) - Color.red(c2)) + Math.abs(Color.green(c) - Color.green(c2)) + Math.abs(Color.blue(c) - Color.blue(c2))); + if (closest[4] < closest[2]) { + closest[1] = closest[0]; + closest[3] = closest[2]; + closest[0] = k; + closest[2] = closest[4]; + } + else if (closest[4] < closest[3]) { + closest[1] = k; + closest[3] = closest[4]; + } + } + + if (closest[3] == SHORT_MAX) + closest[2] = 0; + } + else + closest = got; + + Random rand = new Random(); + if (closest[2] == 0 || (rand.nextInt(SHORT_MAX) % (closest[3] + closest[2])) <= closest[3]) + k = closest[0]; + else + k = closest[1]; + + closestMap.put(c, closest); + return k; + } + + boolean quantize_image(final int[] pixels, final Integer[] palette, int[] qPixels, final boolean dither) + { + int nMaxColors = palette.length; + int[] sqr_tbl = new int[BYTE_MAX + BYTE_MAX + 1]; + + for (int i = (-BYTE_MAX); i <= BYTE_MAX; i++) + sqr_tbl[i + BYTE_MAX] = i * i; + + int[] squares3 = new int[sqr_tbl.length - BYTE_MAX]; + for (int i = 0; i < squares3.length; i++) + squares3[i] = sqr_tbl[i + BYTE_MAX]; + + int pixelIndex = 0; + if (dither) { + boolean odd_scanline = false; + short[] row0, row1; + int a_pix, r_pix, g_pix, b_pix, dir, k; + final int DJ = 4; + final int DITHER_MAX = 20; + final int err_len = (width + 2) * DJ; + int[] clamp = new int[DJ * 256]; + int[] limtb = new int[512]; + short[] erowerr = new short[err_len]; + short[] orowerr = new short[err_len]; + int[] lookup = new int[65536]; + + for (int i = 0; i < 256; i++) { + clamp[i] = 0; + clamp[i + 256] = (short) i; + clamp[i + 512] = BYTE_MAX; + clamp[i + 768] = BYTE_MAX; + + limtb[i] = -DITHER_MAX; + limtb[i + 256] = DITHER_MAX; + } + for (int i = -DITHER_MAX; i <= DITHER_MAX; i++) + limtb[i + 256] = i; + + for (short i = 0; i < height; i++) { + if (odd_scanline) { + dir = -1; + pixelIndex += (width - 1); + row0 = orowerr; + row1 = erowerr; + } + else { + dir = 1; + row0 = erowerr; + row1 = orowerr; + } + + int cursor0 = DJ, cursor1 = width * DJ; + row1[cursor1] = row1[cursor1 + 1] = row1[cursor1 + 2] = row1[cursor1 + 3] = 0; + for (short j = 0; j < width; j++) { + int c = pixels[pixelIndex]; + r_pix = clamp[((row0[cursor0] + 0x1008) >> 4) + Color.red(c)]; + g_pix = clamp[((row0[cursor0 + 1] + 0x1008) >> 4) + Color.green(c)]; + b_pix = clamp[((row0[cursor0 + 2] + 0x1008) >> 4) + Color.blue(c)]; + a_pix = clamp[((row0[cursor0 + 3] + 0x1008) >> 4) + Color.alpha(c)]; + + int c1 = Color.argb(a_pix, r_pix, g_pix, b_pix); + int offset = getColorIndex(c1); + if (lookup[offset] == 0) + lookup[offset] = nearestColorIndex(palette, squares3, c1) + 1; + + int c2 = qPixels[pixelIndex] = palette[lookup[offset] - 1]; + + r_pix = limtb[r_pix - Color.red(c2) + 256]; + g_pix = limtb[g_pix - Color.green(c2) + 256]; + b_pix = limtb[b_pix - Color.blue(c2) + 256]; + a_pix = limtb[a_pix - Color.alpha(c2) + 256]; + + k = r_pix * 2; + row1[cursor1 - DJ] = (short) r_pix; + row1[cursor1 + DJ] += (r_pix += k); + row1[cursor1] += (r_pix += k); + row0[cursor0 + DJ] += (r_pix += k); + + k = g_pix * 2; + row1[cursor1 + 1 - DJ] = (short) g_pix; + row1[cursor1 + 1 + DJ] += (g_pix += k); + row1[cursor1 + 1] += (g_pix += k); + row0[cursor0 + 1 + DJ] += (g_pix += k); + + k = b_pix * 2; + row1[cursor1 + 2 - DJ] = (short) b_pix; + row1[cursor1 + 2 + DJ] += (b_pix += k); + row1[cursor1 + 2] += (b_pix += k); + row0[cursor0 + 2 + DJ] += (b_pix += k); + + k = a_pix * 2; + row1[cursor1 + 3 - DJ] = (short) a_pix; + row1[cursor1 + 3 + DJ] += (a_pix += k); + row1[cursor1 + 3] += (a_pix += k); + row0[cursor0 + 3 + DJ] += (a_pix += k); + + cursor0 += DJ; + cursor1 -= DJ; + pixelIndex += dir; + } + if ((i % 2) == 1) + pixelIndex += (width + 1); + + odd_scanline = !odd_scanline; + } + return true; + } + + if(hasSemiTransparency || nMaxColors < 256) { + for (int i = 0; i < qPixels.length; i++) + qPixels[i] = palette[nearestColorIndex(palette, squares3, pixels[i])]; + } + else { + for (int i = 0; i < qPixels.length; i++) + qPixels[i] = palette[closestColorIndex(palette, squares3, pixels[i])]; + } + + return true; + } + + protected Bitmap quantize_image(final int[] pixels, int[] qPixels) + { + int pixelIndex = 0; + boolean odd_scanline = false; + short[] row0, row1; + int a_pix, r_pix, g_pix, b_pix, dir, k; + final int DJ = 4; + final int DITHER_MAX = 20; + final int err_len = (width + 2) * DJ; + short[] clamp = new short[DJ * 256]; + int[] limtb = new int[512]; + short[] erowerr = new short[err_len]; + short[] orowerr = new short[err_len]; + int[] lookup = new int[65536]; + + for (int i = 0; i < 256; i++) { + clamp[i] = 0; + clamp[i + 256] = (short) i; + clamp[i + 512] = BYTE_MAX; + clamp[i + 768] = BYTE_MAX; + + limtb[i] = -DITHER_MAX; + limtb[i + 256] = DITHER_MAX; + } + for (int i = -DITHER_MAX; i <= DITHER_MAX; i++) + limtb[i + 256] = i; + + for (int i = 0; i < height; i++) { + if (odd_scanline) { + dir = -1; + pixelIndex += (width - 1); + row0 = orowerr; + row1 = erowerr; + } + else { + dir = 1; + row0 = erowerr; + row1 = orowerr; + } + + int cursor0 = DJ, cursor1 = width * DJ; + row1[cursor1] = row1[cursor1 + 1] = row1[cursor1 + 2] = row1[cursor1 + 3] = 0; + for (short j = 0; j < width; j++) { + int c = pixels[pixelIndex]; + + r_pix = clamp[((row0[cursor0] + 0x1008) >> 4) + Color.red(c)]; + g_pix = clamp[((row0[cursor0 + 1] + 0x1008) >> 4) + Color.green(c)]; + b_pix = clamp[((row0[cursor0 + 2] + 0x1008) >> 4) + Color.blue(c)]; + a_pix = clamp[((row0[cursor0 + 3] + 0x1008) >> 4) + Color.alpha(c)]; + + int c1 = Color.argb(a_pix, r_pix, g_pix, b_pix); + int offset = getColorIndex(c1); + if (lookup[offset] == 0) { + int argb1 = Color.argb(BYTE_MAX, (Color.red(c1) & 0xF8), (Color.green(c1) & 0xFC), (Color.blue(c1) & 0xF8)); + if (hasSemiTransparency) + argb1 = Color.argb((Color.alpha(c1) & 0xF0), (Color.red(c1) & 0xF0), (Color.green(c1) & 0xF0), (Color.blue(c1) & 0xF0)); + else if (hasTransparency) + argb1 = Color.argb((Color.alpha(c1) < BYTE_MAX) ? 0 : BYTE_MAX, (Color.red(c1) & 0xF8), (Color.green(c1) & 0xF8), (Color.blue(c1) & 0xF8)); + lookup[offset] = argb1; + } + + int c2 = qPixels[pixelIndex] = lookup[offset]; + + r_pix = limtb[r_pix - Color.red(c2) + 256]; + g_pix = limtb[g_pix - Color.green(c2) + 256]; + b_pix = limtb[b_pix - Color.blue(c2) + 256]; + a_pix = limtb[a_pix - Color.alpha(c2) + 256]; + + k = r_pix * 2; + row1[cursor1 - DJ] = (short) r_pix; + row1[cursor1 + DJ] += (r_pix += k); + row1[cursor1] += (r_pix += k); + row0[cursor0 + DJ] += (r_pix += k); + + k = g_pix * 2; + row1[cursor1 + 1 - DJ] = (short) g_pix; + row1[cursor1 + 1 + DJ] += (g_pix += k); + row1[cursor1 + 1] += (g_pix += k); + row0[cursor0 + 1 + DJ] += (g_pix += k); + + k = b_pix * 2; + row1[cursor1 + 2 - DJ] = (short) b_pix; + row1[cursor1 + 2 + DJ] += (b_pix += k); + row1[cursor1 + 2] += (b_pix += k); + row0[cursor0 + 2 + DJ] += (b_pix += k); + + k = a_pix * 2; + row1[cursor1 + 3 - DJ] = (short) a_pix; + row1[cursor1 + 3 + DJ] += (a_pix += k); + row1[cursor1 + 3] += (a_pix += k); + row0[cursor0 + 3 + DJ] += (a_pix += k); + + cursor0 += DJ; + cursor1 -= DJ; + pixelIndex += dir; + } + if ((i % 2) == 1) + pixelIndex += (width + 1); + + odd_scanline = !odd_scanline; + } + + if (hasTransparency) + return Bitmap.createBitmap(qPixels, width, height, Bitmap.Config.ARGB_8888); + return Bitmap.createBitmap(qPixels, width, height, Bitmap.Config.RGB_565); + } + + public Bitmap convert(int nMaxColors, boolean dither) { + final int[] cPixels = new int[pixels.length]; + for (int i =0; i> 24) & 0xff; + int r = (pixel >> 16) & 0xff; + int g = (pixel >> 8) & 0xff; + int b = (pixel ) & 0xff; + cPixels[i] = Color.argb(alfa, r, g, b); + if (alfa < BYTE_MAX) { + hasSemiTransparency = true; + if (alfa == 0) { + hasTransparency = true; + m_transparentColor = cPixels[i]; + } + } + } + + if (nMaxColors > 256) { + int[] qPixels = new int[cPixels.length]; + return quantize_image(cPixels, qPixels); + } + + boolean quan_sqrt = nMaxColors > BYTE_MAX; + Integer[] palette = new Integer[nMaxColors]; + if (nMaxColors > 2) + palette = pnnquan(cPixels, nMaxColors, quan_sqrt); + else { + if (hasSemiTransparency) { + palette[0] = Color.argb(0, 0, 0, 0); + palette[1] = Color.BLACK; + } + else { + palette[0] = Color.BLACK; + palette[1] = Color.WHITE; + } + } + + int[] qPixels = new int[cPixels.length]; + quantize_image(cPixels, palette, qPixels, dither); + closestMap.clear(); + + if (hasTransparency) + return Bitmap.createBitmap(qPixels, width, height, Bitmap.Config.ARGB_8888); + return Bitmap.createBitmap(qPixels, width, height, Bitmap.Config.RGB_565); + } + +} \ No newline at end of file diff --git a/app-test/src/main/java/oupson/apngcreator/MainActivity.kt b/app-test/src/main/java/oupson/apngcreator/MainActivity.kt index 6cb0d21..310a79c 100644 --- a/app-test/src/main/java/oupson/apngcreator/MainActivity.kt +++ b/app-test/src/main/java/oupson/apngcreator/MainActivity.kt @@ -1,6 +1,8 @@ package oupson.apngcreator +import android.graphics.BitmapFactory import android.os.Bundle +import android.os.Environment import android.support.v7.app.AppCompatActivity import android.util.Log import android.widget.SeekBar @@ -8,20 +10,23 @@ import com.squareup.picasso.Picasso import kotlinx.android.synthetic.main.activity_main.* import oupson.apng.ApngAnimator import android.widget.Toast - - +import org.jetbrains.anko.doAsync +import oupson.apng.APNGDisassembler +import oupson.apng.Apng +import oupson.apng.Loader +import oupson.apngcreator.R.id.imageView +import java.io.File +import java.net.URL class MainActivity : AppCompatActivity() { lateinit var animator: ApngAnimator - //val imageUrl = "https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png" - val imageUrl = "https://raw.githubusercontent.com/tinify/iMessage-Panda-sticker/master/StickerPackExtension/Stickers.xcstickers/Sticker%20Pack.stickerpack/panda.sticker/panda.png" + val imageUrl = "https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png" + //val imageUrl = "https://raw.githubusercontent.com/tinify/iMessage-Panda-sticker/master/StickerPackExtension/Stickers.xcstickers/Sticker%20Pack.stickerpack/panda.sticker/panda.png" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - - animator = ApngAnimator(this).loadInto(imageView).apply { load(imageUrl) onLoaded { @@ -31,6 +36,15 @@ class MainActivity : AppCompatActivity() { } } + + doAsync { + Loader().load(applicationContext, URL(imageUrl)).apply { + val a = APNGDisassembler(this).apng + a.optimise(100, 75) + File(File(Environment.getExternalStorageDirectory(), "Documents"), "apng.png").writeBytes(a.toByteArray()) + } + } + this.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { internal var progress = 0