소스 검색

gif support

milan jurkulák 2 년 전
부모
커밋
15e5dd353b
22개의 변경된 파일과 2764개의 추가작업 그리고 23개의 파일을 삭제
  1. 7 1
      build.gradle.kts
  2. 3 2
      gradle/libs.versions.toml
  3. 334 0
      src/main/java/eu/mjdev/desktop/helpers/gif/AnimatedGifEncoder.java
  4. 554 0
      src/main/java/eu/mjdev/desktop/helpers/gif/GifDecoder.java
  5. 190 0
      src/main/java/eu/mjdev/desktop/helpers/gif/LZWEncoder.java
  6. 343 0
      src/main/java/eu/mjdev/desktop/helpers/gif/NeuQuant.java
  7. 230 0
      src/main/java/eu/mjdev/desktop/helpers/image/AsyncImageScaler.java
  8. 556 0
      src/main/java/eu/mjdev/desktop/helpers/image/ImegeScaler.java
  9. 9 0
      src/main/java/eu/mjdev/desktop/helpers/image/Method.java
  10. 10 0
      src/main/java/eu/mjdev/desktop/helpers/image/Mode.java
  11. 9 0
      src/main/java/eu/mjdev/desktop/helpers/image/Rotation.java
  12. 11 1
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt
  13. 3 1
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/applets/DesktopPanelFavoriteApps.kt
  14. 47 0
      src/main/kotlin/eu/mjdev/desktop/components/image/GifView.kt
  15. 32 7
      src/main/kotlin/eu/mjdev/desktop/components/image/ImageAny.kt
  16. 11 9
      src/main/kotlin/eu/mjdev/desktop/components/video/VideoView.kt
  17. 19 0
      src/main/kotlin/eu/mjdev/desktop/extensions/BitmapUtils.kt
  18. 111 0
      src/main/kotlin/eu/mjdev/desktop/extensions/ColorUtils.kt
  19. 194 0
      src/main/kotlin/eu/mjdev/desktop/helpers/bitmap/Bitmap.kt
  20. 63 0
      src/main/kotlin/eu/mjdev/desktop/helpers/bitmap/BitmapFactory.kt
  21. 22 0
      src/main/kotlin/eu/mjdev/desktop/helpers/bitmap/Canvas.kt
  22. 6 2
      src/main/kotlin/eu/mjdev/desktop/helpers/internal/Palette.kt

+ 7 - 1
build.gradle.kts

@@ -19,6 +19,7 @@ allprojects {
         maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
         maven("https://gitlab.com/api/v4/projects/38224197/packages/maven")
         maven("https://jogamp.org/deployment/maven")
+        maven("https://oss.sonatype.org/content/repositories/snapshots")
         maven("https://jitpack.io")
         google()
         gradlePluginPortal()
@@ -55,7 +56,7 @@ allprojects {
         // ffmpeg
         implementation(libs.ffmpeg.platform)
         // palette
-        implementation(libs.material.kolor)
+//        implementation(libs.material.kolor)
         // json
         implementation(libs.google.gson)
         // no log
@@ -73,6 +74,11 @@ allprojects {
         // tts
         implementation(libs.tts)
         // testing yet
+        // anim
+        implementation("org.jetbrains.compose.animation:animation:1.7.0-alpha03")
+        implementation("org.jetbrains.compose.animation:animation-graphics:1.7.0-alpha03")
+//        // images metadata
+//        implementation("com.ashampoo:kim:0.18.4")
         // connectivity
         implementation("dev.tmapps:konnection:1.4.1")
         // web

+ 3 - 2
gradle/libs.versions.toml

@@ -14,7 +14,7 @@ gradle = "8.4.0"
 kotlin = "1.9.22"
 compose = "1.7.0-alpha03"
 okhttp3 = "5.0.0-alpha.12"
-materialKolor = "1.7.0"
+#materialKolor = "1.7.0"
 coil3 = "3.0.0-alpha06"
 ktxCoroutinesSwing = "1.8.0"
 slf4j-nop = "2.0.7"
@@ -39,7 +39,7 @@ gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "gra
 kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "ktxCoroutinesSwing" }
 mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "mozilla-rhino" }
 okhttp-client = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp3" }
-material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
+#material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
 google-gson = { module = "com.google.code.gson:gson", version.ref = "google-gson" }
 ini4j = { module = "org.ini4j:ini4j", version.ref = "ini4j" }
 coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" }
@@ -47,6 +47,7 @@ coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref
 coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3" }
 coil-mp = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
 coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" }
+coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
 slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j-nop" }
 fuzzywuzzy = { module = "me.xdrop:fuzzywuzzy", version.ref = "fuzzywuzzy" }
 ffmpeg-platform = { module = "org.bytedeco:ffmpeg-platform", version.ref = "ffmpeg-platform" }

+ 334 - 0
src/main/java/eu/mjdev/desktop/helpers/gif/AnimatedGifEncoder.java

@@ -0,0 +1,334 @@
+package eu.mjdev.desktop.helpers.gif;
+
+import java.io.*;
+import java.awt.*;
+import java.awt.image.*;
+
+@SuppressWarnings("unused")
+public class AnimatedGifEncoder {
+    protected int width;
+    protected int height;
+    protected Color transparent = null;
+    protected boolean transparentExactMatch = false;
+    protected Color background = null;
+    protected int transIndex;
+    protected int repeat = -1;
+    protected int delay = 0;
+    protected boolean started = false;
+    protected OutputStream out;
+    protected BufferedImage image;
+    protected byte[] pixels;
+    protected byte[] indexedPixels;
+    protected int colorDepth;
+    protected byte[] colorTab;
+    protected boolean[] usedEntry = new boolean[256];
+    protected int palSize = 7;
+    protected int dispose = -1;
+    protected boolean closeStream = false;
+    protected boolean firstFrame = true;
+    protected boolean sizeSet = false;
+    protected int sample = 10;
+
+    public void setDelay(int ms) {
+        delay = Math.round(ms / 10.0f);
+    }
+
+    public void setDispose(int code) {
+        if (code >= 0) {
+            dispose = code;
+        }
+    }
+
+    public void setRepeat(int iter) {
+        if (iter >= 0) {
+            repeat = iter;
+        }
+    }
+
+    public void setTransparent(Color c) {
+        setTransparent(c, false);
+    }
+
+    public void setTransparent(Color c, boolean exactMatch) {
+        transparent = c;
+        transparentExactMatch = exactMatch;
+    }
+
+    public void setBackground(Color c) {
+        background = c;
+    }
+
+    public boolean addFrame(BufferedImage im) {
+        if ((im == null) || !started) {
+            return false;
+        }
+        boolean ok = true;
+        try {
+            if (!sizeSet) {
+                setSize(im.getWidth(), im.getHeight());
+            }
+            image = im;
+            getImagePixels();
+            analyzePixels();
+            if (firstFrame) {
+                writeLSD();
+                writePalette();
+                if (repeat >= 0) {
+                    writeNetscapeExt();
+                }
+            }
+            writeGraphicCtrlExt();
+            writeImageDesc();
+            if (!firstFrame) {
+                writePalette();
+            }
+            writePixels();
+            firstFrame = false;
+        } catch (IOException e) {
+            ok = false;
+        }
+        return ok;
+    }
+
+    public boolean finish() {
+        if (!started) return false;
+        boolean ok = true;
+        started = false;
+        try {
+            out.write(0x3b);
+            out.flush();
+            if (closeStream) {
+                out.close();
+            }
+        } catch (IOException e) {
+            ok = false;
+        }
+        transIndex = 0;
+        out = null;
+        image = null;
+        pixels = null;
+        indexedPixels = null;
+        colorTab = null;
+        closeStream = false;
+        firstFrame = true;
+        return ok;
+    }
+
+    public void setFrameRate(float fps) {
+        if (fps != 0f) {
+            delay = Math.round(100f / fps);
+        }
+    }
+
+    public void setQuality(int quality) {
+        if (quality < 1) quality = 1;
+        sample = quality;
+    }
+
+    public void setSize(int w, int h) {
+        if (started && !firstFrame) return;
+        width = w;
+        height = h;
+        if (width < 1) width = 320;
+        if (height < 1) height = 240;
+        sizeSet = true;
+    }
+
+    public boolean start(OutputStream os) {
+        if (os == null) return false;
+        boolean ok = true;
+        closeStream = false;
+        out = os;
+        try {
+            writeString("GIF89a"); // header
+        } catch (IOException e) {
+            ok = false;
+        }
+        return started = ok;
+    }
+
+    @SuppressWarnings("UnusedAssignment")
+    public boolean start(String file) {
+        boolean ok = true;
+        try {
+            out = new BufferedOutputStream(new FileOutputStream(file));
+            ok = start(out);
+            closeStream = true;
+        } catch (IOException e) {
+            ok = false;
+        }
+        return started = ok;
+    }
+
+    public boolean isStarted() {
+        return started;
+    }
+
+    protected void analyzePixels() {
+        int len = pixels.length;
+        int nPix = len / 3;
+        indexedPixels = new byte[nPix];
+        NeuQuant nq = new NeuQuant(pixels, len, sample);
+        colorTab = nq.process();
+        for (int i = 0; i < colorTab.length; i += 3) {
+            byte temp = colorTab[i];
+            colorTab[i] = colorTab[i + 2];
+            colorTab[i + 2] = temp;
+            usedEntry[i / 3] = false;
+        }
+        int k = 0;
+        for (int i = 0; i < nPix; i++) {
+            int index = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff);
+            usedEntry[index] = true;
+            indexedPixels[i] = (byte) index;
+        }
+        pixels = null;
+        colorDepth = 8;
+        palSize = 7;
+        if (transparent != null) {
+            transIndex = transparentExactMatch ? findExact(transparent) : findClosest(transparent);
+        }
+    }
+
+    protected int findClosest(Color c) {
+        if (colorTab == null) return -1;
+        int r = c.getRed();
+        int g = c.getGreen();
+        int b = c.getBlue();
+        int minpos = 0;
+        int dmin = 256 * 256 * 256;
+        int len = colorTab.length;
+        for (int i = 0; i < len; ) {
+            int dr = r - (colorTab[i++] & 0xff);
+            int dg = g - (colorTab[i++] & 0xff);
+            int db = b - (colorTab[i] & 0xff);
+            int d = dr * dr + dg * dg + db * db;
+            int index = i / 3;
+            if (usedEntry[index] && (d < dmin)) {
+                dmin = d;
+                minpos = index;
+            }
+            i++;
+        }
+        return minpos;
+    }
+
+    boolean isColorUsed(Color c) {
+        return findExact(c) != -1;
+    }
+
+    protected int findExact(Color c) {
+        if (colorTab == null) {
+            return -1;
+        }
+        int r = c.getRed();
+        int g = c.getGreen();
+        int b = c.getBlue();
+        int len = colorTab.length / 3;
+        for (int index = 0; index < len; ++index) {
+            int i = index * 3;
+            if (usedEntry[index] && r == (colorTab[i] & 0xff) &&
+                    g == (colorTab[i + 1] & 0xff) &&
+                    b == (colorTab[i + 2] & 0xff)) {
+                return index;
+            }
+        }
+        return -1;
+    }
+
+    protected void getImagePixels() {
+        int w = image.getWidth();
+        int h = image.getHeight();
+        int type = image.getType();
+        if ((w != width) || (h != height) || (type != BufferedImage.TYPE_3BYTE_BGR)) {
+            BufferedImage temp = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
+            Graphics2D g = temp.createGraphics();
+            g.setColor(background);
+            g.fillRect(0, 0, width, height);
+            g.drawImage(image, 0, 0, null);
+            image = temp;
+        }
+        pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
+    }
+
+    protected void writeGraphicCtrlExt() throws IOException {
+        out.write(0x21);
+        out.write(0xf9);
+        out.write(4);
+        int transp, disp;
+        if (transparent == null) {
+            transp = 0;
+            disp = 0;
+        } else {
+            transp = 1;
+            disp = 2;
+        }
+        if (dispose >= 0) {
+            disp = dispose & 7;
+        }
+        disp <<= 2;
+        //noinspection PointlessBitwiseExpression
+        out.write(0 | disp | 0 | transp);
+        writeShort(delay);
+        out.write(transIndex);
+        out.write(0);
+    }
+
+    protected void writeImageDesc() throws IOException {
+        out.write(0x2c);
+        writeShort(0);
+        writeShort(0);
+        writeShort(width);
+        writeShort(height);
+        if (firstFrame) {
+            out.write(0);
+        } else {
+            //noinspection PointlessBitwiseExpression
+            out.write(0x80 | 0 | 0 | 0 | palSize);
+        }
+    }
+
+    protected void writeLSD() throws IOException {
+        writeShort(width);
+        writeShort(height);
+        //noinspection PointlessBitwiseExpression
+        out.write((0x80 | 0x70 | 0x00 | palSize));
+        out.write(0);
+        out.write(0);
+    }
+
+    protected void writeNetscapeExt() throws IOException {
+        out.write(0x21);
+        out.write(0xff);
+        out.write(11);
+        writeString("NETSCAPE" + "2.0");
+        out.write(3);
+        out.write(1);
+        writeShort(repeat);
+        out.write(0);
+    }
+
+    protected void writePalette() throws IOException {
+        out.write(colorTab, 0, colorTab.length);
+        int n = (3 * 256) - colorTab.length;
+        for (int i = 0; i < n; i++) {
+            out.write(0);
+        }
+    }
+
+    protected void writePixels() throws IOException {
+        LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
+        encoder.encode(out);
+    }
+
+    protected void writeShort(int value) throws IOException {
+        out.write(value & 0xff);
+        out.write((value >> 8) & 0xff);
+    }
+
+    protected void writeString(String s) throws IOException {
+        for (int i = 0; i < s.length(); i++) {
+            out.write((byte) s.charAt(i));
+        }
+    }
+}

+ 554 - 0
src/main/java/eu/mjdev/desktop/helpers/gif/GifDecoder.java

@@ -0,0 +1,554 @@
+package eu.mjdev.desktop.helpers.gif;
+
+import java.net.*;
+import java.io.*;
+import java.util.*;
+import java.awt.*;
+import java.awt.image.*;
+
+@SuppressWarnings("ALL")
+public class GifDecoder {
+    public static final int STATUS_OK = 0;
+    public static final int STATUS_FORMAT_ERROR = 1;
+    public static final int STATUS_OPEN_ERROR = 2;
+    protected BufferedInputStream in;
+    protected int status;
+    protected int width;
+    protected int height;
+    protected boolean gctFlag;
+    protected int gctSize;
+    protected int loopCount = 1;
+    protected int[] gct;
+    protected int[] lct;
+    protected int[] act;
+    protected int bgIndex;
+    protected int bgColor;
+    protected int lastBgColor;
+    protected int pixelAspect;
+    protected boolean lctFlag;
+    protected boolean interlace;
+    protected int lctSize;
+    protected int ix, iy, iw, ih;
+    protected Rectangle lastRect;
+    protected BufferedImage image;
+    protected BufferedImage lastImage;
+    protected byte[] block = new byte[256];
+    protected int blockSize = 0;
+    protected int dispose = 0;
+    protected int lastDispose = 0;
+    protected boolean transparency = false;
+    protected int delay = 0;
+    protected int transIndex;
+    protected static final int MaxStackSize = 4096;
+    protected short[] prefix;
+    protected byte[] suffix;
+    protected byte[] pixelStack;
+    protected byte[] pixels;
+    protected ArrayList frames;
+    protected int frameCount;
+
+    static class GifFrame {
+        public GifFrame(BufferedImage im, int del) {
+            image = im;
+            delay = del;
+        }
+
+        public BufferedImage image;
+        public int delay;
+    }
+
+    public int getDelay(int n) {
+        delay = -1;
+        if ((n >= 0) && (n < frameCount)) {
+            delay = ((GifFrame) frames.get(n)).delay;
+        }
+        return delay;
+    }
+
+    public int getFrameCount() {
+        return frameCount;
+    }
+
+    public BufferedImage getImage() {
+        return getFrame(0);
+    }
+
+    public int getLoopCount() {
+        return loopCount;
+    }
+
+    protected void setPixels() {
+        int[] dest = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
+        if (lastDispose > 0) {
+            if (lastDispose == 3) {
+                int n = frameCount - 2;
+                if (n > 0) {
+                    lastImage = getFrame(n - 1);
+                } else {
+                    lastImage = null;
+                }
+            }
+            if (lastImage != null) {
+                int[] prev = ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData();
+                System.arraycopy(prev, 0, dest, 0, width * height);
+                if (lastDispose == 2) {
+                    Graphics2D g = image.createGraphics();
+                    Color c;
+                    if (transparency) {
+                        c = new Color(0, 0, 0, 0);
+                    } else {
+                        c = new Color(lastBgColor);
+                    }
+                    g.setColor(c);
+                    g.setComposite(AlphaComposite.Src);
+                    g.fill(lastRect);
+                    g.dispose();
+                }
+            }
+        }
+        int pass = 1;
+        int inc = 8;
+        int iline = 0;
+        for (int i = 0; i < ih; i++) {
+            int line = i;
+            if (interlace) {
+                if (iline >= ih) {
+                    pass++;
+                    switch (pass) {
+                        case 2:
+                            iline = 4;
+                            break;
+                        case 3:
+                            iline = 2;
+                            inc = 4;
+                            break;
+                        case 4:
+                            iline = 1;
+                            inc = 2;
+                    }
+                }
+                line = iline;
+                iline += inc;
+            }
+            line += iy;
+            if (line < height) {
+                int k = line * width;
+                int dx = k + ix;
+                int dlim = dx + iw;
+                if ((k + width) < dlim) {
+                    dlim = k + width;
+                }
+                int sx = i * iw;
+                while (dx < dlim) {
+                    int index = ((int) pixels[sx++]) & 0xff;
+                    int c = act[index];
+                    if (c != 0) {
+                        dest[dx] = c;
+                    }
+                    dx++;
+                }
+            }
+        }
+    }
+
+    public BufferedImage getFrame(int n) {
+        BufferedImage im = null;
+        if ((n >= 0) && (n < frameCount)) {
+            im = ((GifFrame) frames.get(n)).image;
+        }
+        return im;
+    }
+
+    public Dimension getFrameSize() {
+        return new Dimension(width, height);
+    }
+
+    public int read(BufferedInputStream is) {
+        init();
+        if (is != null) {
+            in = is;
+            readHeader();
+            if (!err()) {
+                readContents();
+                if (frameCount < 0) {
+                    status = STATUS_FORMAT_ERROR;
+                }
+            }
+        } else {
+            status = STATUS_OPEN_ERROR;
+        }
+        try {
+            if (is != null) is.close();
+        } catch (IOException e) {
+            // no op
+        }
+        return status;
+    }
+
+    public int read(InputStream is) {
+        init();
+        if (is != null) {
+            if (!(is instanceof BufferedInputStream))
+                is = new BufferedInputStream(is);
+            in = (BufferedInputStream) is;
+            readHeader();
+            if (!err()) {
+                readContents();
+                if (frameCount < 0) {
+                    status = STATUS_FORMAT_ERROR;
+                }
+            }
+        } else {
+            status = STATUS_OPEN_ERROR;
+        }
+        try {
+           if(is != null) is.close();
+        } catch (IOException e) {
+            // no op
+        }
+        return status;
+    }
+
+    public int read(String name) {
+        status = STATUS_OK;
+        try {
+            name = name.trim().toLowerCase();
+            //noinspection IndexOfReplaceableByContains
+            if ((name.indexOf("file:") >= 0) || (name.indexOf(":/") > 0)) {
+                URL url = new URL(name);
+                in = new BufferedInputStream(url.openStream());
+            } else {
+                in = new BufferedInputStream(new FileInputStream(name));
+            }
+            status = read(in);
+        } catch (IOException e) {
+            status = STATUS_OPEN_ERROR;
+        }
+        return status;
+    }
+
+    protected void decodeImageData() {
+        int NullCode = -1;
+        int npix = iw * ih;
+        int available,
+                clear,
+                code_mask,
+                code_size,
+                end_of_information,
+                in_code,
+                old_code,
+                bits,
+                code,
+                count,
+                i,
+                datum,
+                data_size,
+                first,
+                top,
+                bi,
+                pi;
+        if ((pixels == null) || (pixels.length < npix)) {
+            pixels = new byte[npix]; // allocate new pixel array
+        }
+        if (prefix == null) prefix = new short[MaxStackSize];
+        if (suffix == null) suffix = new byte[MaxStackSize];
+        if (pixelStack == null) pixelStack = new byte[MaxStackSize + 1];
+        data_size = read();
+        clear = 1 << data_size;
+        end_of_information = clear + 1;
+        available = clear + 2;
+        old_code = NullCode;
+        code_size = data_size + 1;
+        code_mask = (1 << code_size) - 1;
+        for (code = 0; code < clear; code++) {
+            prefix[code] = 0;
+            suffix[code] = (byte) code;
+        }
+        datum = bits = count = first = top = pi = bi = 0;
+        for (i = 0; i < npix; ) {
+            if (top == 0) {
+                if (bits < code_size) {
+                    if (count == 0) {
+                        count = readBlock();
+                        if (count <= 0)
+                            break;
+                        bi = 0;
+                    }
+                    datum += (((int) block[bi]) & 0xff) << bits;
+                    bits += 8;
+                    bi++;
+                    count--;
+                    continue;
+                }
+                code = datum & code_mask;
+                datum >>= code_size;
+                bits -= code_size;
+                if ((code > available) || (code == end_of_information))
+                    break;
+                if (code == clear) {
+                    code_size = data_size + 1;
+                    code_mask = (1 << code_size) - 1;
+                    available = clear + 2;
+                    old_code = NullCode;
+                    continue;
+                }
+                if (old_code == NullCode) {
+                    pixelStack[top++] = suffix[code];
+                    old_code = code;
+                    first = code;
+                    continue;
+                }
+                in_code = code;
+                if (code == available) {
+                    pixelStack[top++] = (byte) first;
+                    code = old_code;
+                }
+                while (code > clear) {
+                    pixelStack[top++] = suffix[code];
+                    code = prefix[code];
+                }
+                first = ((int) suffix[code]) & 0xff;
+                if (available >= MaxStackSize) {
+                    pixelStack[top++] = (byte) first;
+                    continue;
+                }
+                pixelStack[top++] = (byte) first;
+                prefix[available] = (short) old_code;
+                suffix[available] = (byte) first;
+                available++;
+                if (((available & code_mask) == 0)
+                        && (available < MaxStackSize)) {
+                    code_size++;
+                    code_mask += available;
+                }
+                old_code = in_code;
+            }
+            top--;
+            pixels[pi++] = pixelStack[top];
+            i++;
+        }
+        for (i = pi; i < npix; i++) {
+            pixels[i] = 0;
+        }
+    }
+
+    protected boolean err() {
+        return status != STATUS_OK;
+    }
+
+    protected void init() {
+        status = STATUS_OK;
+        frameCount = 0;
+        frames = new ArrayList();
+        gct = null;
+        lct = null;
+    }
+
+    protected int read() {
+        int curByte = 0;
+        try {
+            curByte = in.read();
+        } catch (IOException e) {
+            status = STATUS_FORMAT_ERROR;
+        }
+        return curByte;
+    }
+
+    protected int readBlock() {
+        blockSize = read();
+        int n = 0;
+        if (blockSize > 0) {
+            try {
+                int count;
+                while (n < blockSize) {
+                    count = in.read(block, n, blockSize - n);
+                    if (count == -1)
+                        break;
+                    n += count;
+                }
+            } catch (IOException e) {
+                // no op
+            }
+            if (n < blockSize) {
+                status = STATUS_FORMAT_ERROR;
+            }
+        }
+        return n;
+    }
+
+    protected int[] readColorTable(int ncolors) {
+        int nbytes = 3 * ncolors;
+        int[] tab = null;
+        byte[] c = new byte[nbytes];
+        int n = 0;
+        try {
+            n = in.read(c);
+        } catch (IOException e) {
+            // no op
+        }
+        if (n < nbytes) {
+            status = STATUS_FORMAT_ERROR;
+        } else {
+            tab = new int[256]; // max size to avoid bounds checks
+            int i = 0;
+            int j = 0;
+            while (i < ncolors) {
+                int r = ((int) c[j++]) & 0xff;
+                int g = ((int) c[j++]) & 0xff;
+                int b = ((int) c[j++]) & 0xff;
+                tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
+            }
+        }
+        return tab;
+    }
+
+    protected void readContents() {
+        boolean done = false;
+        while (!(done || err())) {
+            int code = read();
+            switch (code) {
+                case 0x2C:
+                    readImage();
+                    break;
+                case 0x21:
+                    code = read();
+                    switch (code) {
+                        case 0xf9:
+                            readGraphicControlExt();
+                            break;
+                        case 0xff:
+                            readBlock();
+                            String app = "";
+                            for (int i = 0; i < 11; i++) {
+                                app += (char) block[i];
+                            }
+                            if (app.equals("NETSCAPE2.0")) {
+                                readNetscapeExt();
+                            } else
+                                skip();
+                            break;
+                        default:
+                            skip();
+                    }
+                    break;
+                case 0x3b:
+                    done = true;
+                    break;
+                case 0x00:
+                    break;
+                default:
+                    status = STATUS_FORMAT_ERROR;
+            }
+        }
+    }
+
+    protected void readGraphicControlExt() {
+        read();
+        int packed = read();
+        dispose = (packed & 0x1c) >> 2;
+        if (dispose == 0) {
+            dispose = 1;
+        }
+        transparency = (packed & 1) != 0;
+        delay = readShort() * 10;
+        transIndex = read();
+        read();
+    }
+
+    protected void readHeader() {
+        String id = "";
+        for (int i = 0; i < 6; i++) {
+            id += (char) read();
+        }
+        if (!id.startsWith("GIF")) {
+            status = STATUS_FORMAT_ERROR;
+            return;
+        }
+        readLSD();
+        if (gctFlag && !err()) {
+            gct = readColorTable(gctSize);
+            bgColor = gct[bgIndex];
+        }
+    }
+
+    protected void readImage() {
+        ix = readShort();
+        iy = readShort();
+        iw = readShort();
+        ih = readShort();
+        int packed = read();
+        lctFlag = (packed & 0x80) != 0;
+        interlace = (packed & 0x40) != 0;
+        lctSize = 2 << (packed & 7);
+        if (lctFlag) {
+            lct = readColorTable(lctSize);
+            act = lct;
+        } else {
+            act = gct;
+            if (bgIndex == transIndex)
+                bgColor = 0;
+        }
+        int save = 0;
+        if (transparency) {
+            save = act[transIndex];
+            act[transIndex] = 0;
+        }
+        if (act == null) {
+            status = STATUS_FORMAT_ERROR;
+        }
+        if (err()) return;
+        decodeImageData();
+        skip();
+        if (err()) return;
+        frameCount++;
+        image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
+        setPixels();
+        frames.add(new GifFrame(image, delay));
+        if (transparency) {
+            act[transIndex] = save;
+        }
+        resetFrame();
+    }
+
+    protected void readLSD() {
+        width = readShort();
+        height = readShort();
+        int packed = read();
+        gctFlag = (packed & 0x80) != 0;
+        gctSize = 2 << (packed & 7);
+        bgIndex = read();
+        pixelAspect = read();
+    }
+
+    protected void readNetscapeExt() {
+        do {
+            readBlock();
+            if (block[0] == 1) {
+                int b1 = ((int) block[1]) & 0xff;
+                int b2 = ((int) block[2]) & 0xff;
+                loopCount = (b2 << 8) | b1;
+            }
+        } while ((blockSize > 0) && !err());
+    }
+
+    protected int readShort() {
+        return read() | (read() << 8);
+    }
+
+    protected void resetFrame() {
+        lastDispose = dispose;
+        lastRect = new Rectangle(ix, iy, iw, ih);
+        lastImage = image;
+        lastBgColor = bgColor;
+        int dispose = 0;
+        boolean transparency = false;
+        int delay = 0;
+        lct = null;
+    }
+
+    protected void skip() {
+        do {
+            readBlock();
+        } while ((blockSize > 0) && !err());
+    }
+}

+ 190 - 0
src/main/java/eu/mjdev/desktop/helpers/gif/LZWEncoder.java

@@ -0,0 +1,190 @@
+package eu.mjdev.desktop.helpers.gif;
+
+import java.io.OutputStream;
+import java.io.IOException;
+
+class LZWEncoder {
+    private static final int EOF = -1;
+    private final int imgW;
+    private final int imgH;
+    private final byte[] pixAry;
+    private final int initCodeSize;
+    private int remaining;
+    private int curPixel;
+    static final int BITS = 12;
+    static final int HSIZE = 5003;
+    int n_bits;
+    int maxbits = BITS;
+    int maxcode;
+    int maxmaxcode = 1 << BITS;
+    int[] htab = new int[HSIZE];
+    int[] codetab = new int[HSIZE];
+    int hsize = HSIZE;
+    int free_ent = 0;
+    boolean clear_flg = false;
+    int g_init_bits;
+    int ClearCode;
+    int EOFCode;
+    int cur_accum = 0;
+    int cur_bits = 0;
+    int[] masks = {
+            0x0000,
+            0x0001,
+            0x0003,
+            0x0007,
+            0x000F,
+            0x001F,
+            0x003F,
+            0x007F,
+            0x00FF,
+            0x01FF,
+            0x03FF,
+            0x07FF,
+            0x0FFF,
+            0x1FFF,
+            0x3FFF,
+            0x7FFF,
+            0xFFFF
+    };
+    int a_count;
+    byte[] accum = new byte[256];
+
+    LZWEncoder(int width, int height, byte[] pixels, int color_depth) {
+        imgW = width;
+        imgH = height;
+        pixAry = pixels;
+        initCodeSize = Math.max(2, color_depth);
+    }
+
+    void char_out(byte c, OutputStream outs) throws IOException {
+        accum[a_count++] = c;
+        if (a_count >= 254) flush_char(outs);
+    }
+
+    void cl_block(OutputStream outs) throws IOException {
+        cl_hash(hsize);
+        free_ent = ClearCode + 2;
+        clear_flg = true;
+        output(ClearCode, outs);
+    }
+
+    void cl_hash(int hsize) {
+        for (int i = 0; i < hsize; ++i) htab[i] = -1;
+    }
+
+    void compress(int init_bits, OutputStream outs) throws IOException {
+        int fcode;
+        int i /* = 0 */;
+        int c;
+        int ent;
+        int disp;
+        int hsize_reg;
+        int hshift;
+        g_init_bits = init_bits;
+        clear_flg = false;
+        n_bits = g_init_bits;
+        maxcode = MAXCODE(n_bits);
+        ClearCode = 1 << (init_bits - 1);
+        EOFCode = ClearCode + 1;
+        free_ent = ClearCode + 2;
+        a_count = 0;
+        ent = nextPixel();
+        hshift = 0;
+        for (fcode = hsize; fcode < 65536; fcode *= 2) ++hshift;
+        hshift = 8 - hshift;
+        hsize_reg = hsize;
+        cl_hash(hsize_reg);
+        output(ClearCode, outs);
+        outer_loop:
+        while ((c = nextPixel()) != EOF) {
+            fcode = (c << maxbits) + ent;
+            i = (c << hshift) ^ ent;
+            if (htab[i] == fcode) {
+                ent = codetab[i];
+                continue;
+            } else if (htab[i] >= 0) {
+                disp = hsize_reg - i;
+                if (i == 0)
+                    disp = 1;
+                do {
+                    if ((i -= disp) < 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++;
+                htab[i] = fcode;
+            } else {
+                cl_block(outs);
+            }
+        }
+        output(ent, outs);
+        output(EOFCode, outs);
+    }
+
+    void encode(OutputStream os) throws IOException {
+        os.write(initCodeSize);
+        remaining = imgW * imgH;
+        curPixel = 0;
+        compress(initCodeSize + 1, os);
+        os.write(0);
+    }
+
+    void flush_char(OutputStream outs) throws IOException {
+        if (a_count > 0) {
+            outs.write(a_count);
+            outs.write(accum, 0, a_count);
+            a_count = 0;
+        }
+    }
+
+    final int MAXCODE(int n_bits) {
+        return (1 << n_bits) - 1;
+    }
+
+    private int nextPixel() {
+        if (remaining == 0) return EOF;
+        --remaining;
+        byte pix = pixAry[curPixel++];
+        return pix & 0xff;
+    }
+
+    void output(int code, OutputStream outs) throws IOException {
+        cur_accum &= masks[cur_bits];
+        if (cur_bits > 0)
+            cur_accum |= (code << cur_bits);
+        else
+            cur_accum = code;
+        cur_bits += n_bits;
+        while (cur_bits >= 8) {
+            char_out((byte) (cur_accum & 0xff), outs);
+            cur_accum >>= 8;
+            cur_bits -= 8;
+        }
+        if (free_ent > maxcode || clear_flg) {
+            if (clear_flg) {
+                maxcode = MAXCODE(n_bits = g_init_bits);
+                clear_flg = false;
+            } else {
+                ++n_bits;
+                if (n_bits == maxbits)
+                    maxcode = maxmaxcode;
+                else
+                    maxcode = MAXCODE(n_bits);
+            }
+        }
+        if (code == EOFCode) {
+            while (cur_bits > 0) {
+                char_out((byte) (cur_accum & 0xff), outs);
+                cur_accum >>= 8;
+                cur_bits -= 8;
+            }
+            flush_char(outs);
+        }
+    }
+}

+ 343 - 0
src/main/java/eu/mjdev/desktop/helpers/gif/NeuQuant.java

@@ -0,0 +1,343 @@
+package eu.mjdev.desktop.helpers.gif;
+
+
+@SuppressWarnings("ALL")
+public class NeuQuant {
+	protected static final int netsize = 256;
+	protected static final int prime1 = 499;
+	protected static final int prime2 = 491;
+	protected static final int prime3 = 487;
+	protected static final int prime4 = 503;
+	protected static final int minpicturebytes = (3 * prime4);
+	protected static final int maxnetpos = (netsize - 1);
+	protected static final int netbiasshift = 4;
+	protected static final int ncycles = 100;
+	protected static final int intbiasshift = 16;
+	protected static final int intbias = (1 << intbiasshift);
+	protected static final int gammashift = 10;
+	protected static final int gamma = (1 << gammashift);
+	protected static final int betashift = 10;
+	protected static final int beta = (intbias >> betashift);
+	protected static final int betagamma =		(intbias << (gammashift - betashift));
+	protected static final int initrad = (netsize >> 3);
+	protected static final int radiusbiasshift = 6;
+	protected static final int radiusbias = (1 << radiusbiasshift);
+	protected static final int initradius = (initrad * radiusbias);
+	protected static final int radiusdec = 30;
+	protected static final int alphabiasshift = 10;
+	protected static final int initalpha = (((int) 1) << alphabiasshift);
+	protected int alphadec;
+	protected static final int radbiasshift = 8;
+	protected static final int radbias = (1 << radbiasshift);
+	protected static final int alpharadbshift = (alphabiasshift + radbiasshift);
+	protected static final int alpharadbias = (((int) 1) << alpharadbshift);
+	protected byte[] thepicture;
+	protected int lengthcount;
+	protected int samplefac;
+	protected int[][] network;
+	protected int[] netindex = new int[256];
+	protected int[] bias = new int[netsize];
+	protected int[] freq = new int[netsize];
+	protected int[] radpower = new int[initrad];
+
+	public NeuQuant(byte[] thepic, int len, int sample) {
+		int i;
+		int[] p;
+		thepicture = thepic;
+		lengthcount = len;
+		samplefac = sample;
+		network = new int[netsize][];
+		for (i = 0; i < netsize; i++) {
+			network[i] = new int[4];
+			p = network[i];
+			p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize;
+			freq[i] = intbias / netsize; /* 1/netsize */
+			bias[i] = 0;
+		}
+	}
+	
+	public byte[] colorMap() {
+		byte[] map = new byte[3 * netsize];
+		int[] index = new int[netsize];
+		for (int i = 0; i < netsize; i++)
+			index[network[i][3]] = i;
+		int k = 0;
+		for (int i = 0; i < netsize; i++) {
+			int j = index[i];
+			map[k++] = (byte) (network[j][0]);
+			map[k++] = (byte) (network[j][1]);
+			map[k++] = (byte) (network[j][2]);
+		}
+		return map;
+	}
+	
+	public void inxbuild() {
+		int i, j, smallpos, smallval;
+		int[] p;
+		int[] q;
+		int previouscol, startpos;
+		previouscol = 0;
+		startpos = 0;
+		for (i = 0; i < netsize; i++) {
+			p = network[i];
+			smallpos = i;
+			smallval = p[1]; /* index on g */
+			for (j = i + 1; j < netsize; j++) {
+				q = network[j];
+				if (q[1] < smallval) { /* index on g */
+					smallpos = j;
+					smallval = q[1]; /* index on g */
+				}
+			}
+			q = network[smallpos];
+			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;
+			}
+			if (smallval != previouscol) {
+				netindex[previouscol] = (startpos + i) >> 1;
+				for (j = previouscol + 1; j < smallval; j++)
+					netindex[j] = i;
+				previouscol = smallval;
+				startpos = i;
+			}
+		}
+		netindex[previouscol] = (startpos + maxnetpos) >> 1;
+		for (j = previouscol + 1; j < 256; j++)
+			netindex[j] = maxnetpos; /* really 256 */
+	}
+	
+	public void learn() {
+		int i, j, b, g, r;
+		int radius, rad, alpha, step, delta, samplepixels;
+		byte[] p;
+		int pix, lim;
+		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 >> radiusbiasshift;
+		if (rad <= 1)
+			rad = 0;
+		for (i = 0; i < rad; i++)
+			radpower[i] =
+				alpha * (((rad * rad - i * i) * radbias) / (rad * rad));
+		if (lengthcount < minpicturebytes)
+			step = 3;
+		else if ((lengthcount % prime1) != 0)
+			step = 3 * prime1;
+		else {
+			if ((lengthcount % prime2) != 0)
+				step = 3 * prime2;
+			else {
+				if ((lengthcount % prime3) != 0)
+					step = 3 * prime3;
+				else
+					step = 3 * prime4;
+			}
+		}
+		i = 0;
+		while (i < samplepixels) {
+			b = (p[pix + 0] & 0xff) << netbiasshift;
+			g = (p[pix + 1] & 0xff) << netbiasshift;
+			r = (p[pix + 2] & 0xff) << 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 >> radiusbiasshift;
+				if (rad <= 1)
+					rad = 0;
+				for (j = 0; j < rad; j++)
+					radpower[j] =
+						alpha * (((rad * rad - j * j) * radbias) / (rad * rad));
+			}
+		}
+	}
+	
+	public int map(int b, int g, int r) {
+		int i, j, dist, a, bestd;
+		int[] p;
+		int best;
+		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);
+	}
+
+	public byte[] process() {
+		learn();
+		unbiasnet();
+		inxbuild();
+		return colorMap();
+	}
+	
+	public void unbiasnet() {
+		int i, j;
+		for (i = 0; i < netsize; i++) {
+			network[i][0] >>= netbiasshift;
+			network[i][1] >>= netbiasshift;
+			network[i][2] >>= netbiasshift;
+			network[i][3] = i; /* record colour no */
+		}
+	}
+	
+	protected void alterneigh(int rad, int i, int b, int g, int r) {
+		int j, k, lo, hi, a, m;
+		int[] p;
+		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 (Exception e) {
+				} // 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 e) {
+				}
+			}
+		}
+	}
+	
+	protected void altersingle(int alpha, int i, int b, int g, int r) {
+		int[] n = network[i];
+		n[0] -= (alpha * (n[0] - b)) / initalpha;
+		n[1] -= (alpha * (n[1] - g)) / initalpha;
+		n[2] -= (alpha * (n[2] - r)) / initalpha;
+	}
+	
+	protected int contest(int b, int g, int r) {
+		int i, dist, a, biasdist, betafreq;
+		int bestpos, bestbiaspos, bestd, bestbiasd;
+		int[] n;
+		bestd = ~(((int) 1) << 31);
+		bestbiasd = bestd;
+		bestpos = -1;
+		bestbiaspos = bestpos;
+		for (i = 0; i < netsize; i++) {
+			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]) >> (intbiasshift - netbiasshift));
+			if (biasdist < bestbiasd) {
+				bestbiasd = biasdist;
+				bestbiaspos = i;
+			}
+			betafreq = (freq[i] >> betashift);
+			freq[i] -= betafreq;
+			bias[i] += (betafreq << gammashift);
+		}
+		freq[bestpos] += beta;
+		bias[bestpos] -= betagamma;
+		return (bestbiaspos);
+	}
+}

+ 230 - 0
src/main/java/eu/mjdev/desktop/helpers/image/AsyncImageScaler.java

@@ -0,0 +1,230 @@
+package eu.mjdev.desktop.helpers.image;
+
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import java.awt.image.BufferedImageOp;
+import java.awt.image.ImagingOpException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SuppressWarnings("ALL")
+public class AsyncImageScaler {
+    public static final String THREAD_COUNT_PROPERTY_NAME = "imgscalr.async.threadCount";
+    public static final int THREAD_COUNT = Integer.getInteger(THREAD_COUNT_PROPERTY_NAME, 2);
+
+    static {
+        if (THREAD_COUNT < 1)
+            throw new RuntimeException("System property '"
+                    + THREAD_COUNT_PROPERTY_NAME + "' set THREAD_COUNT to "
+                    + THREAD_COUNT + ", but THREAD_COUNT must be > 0.");
+    }
+
+    protected static ExecutorService service;
+
+    public static Future<BufferedImage> apply(
+            final BufferedImage src,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.apply(src, ops));
+    }
+
+    public static Future<BufferedImage> crop(
+            final BufferedImage src,
+            final int width, final int height,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(new Callable<BufferedImage>() {
+            public BufferedImage call() throws Exception {
+                return ImegeScaler.crop(src, width, height, ops);
+            }
+        });
+    }
+
+    public static Future<BufferedImage> crop(
+            final BufferedImage src,
+            final int x,
+            final int y,
+            final int width,
+            final int height,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(new Callable<BufferedImage>() {
+            public BufferedImage call() throws Exception {
+                return ImegeScaler.crop(src, x, y, width, height, ops);
+            }
+        });
+    }
+
+    public static Future<BufferedImage> pad(
+            final BufferedImage src,
+            final int padding,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.pad(src, padding, ops));
+    }
+
+    public static Future<BufferedImage> pad(
+            final BufferedImage src,
+            final int padding,
+            final Color color,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.pad(src, padding, color, ops));
+    }
+
+    public static Future<BufferedImage> resize(
+            final BufferedImage src,
+            final int targetSize, final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.resize(src, targetSize, ops));
+    }
+
+    public static Future<BufferedImage> resize(
+            final BufferedImage src,
+            final Method scalingMethod,
+            final int targetSize,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.resize(src, scalingMethod, targetSize, ops));
+    }
+
+    public static Future<BufferedImage> resize(
+            final BufferedImage src,
+            final Mode resizeMode,
+            final int targetSize,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(new Callable<BufferedImage>() {
+            public BufferedImage call() throws Exception {
+                return ImegeScaler.resize(src, resizeMode, targetSize, ops);
+            }
+        });
+    }
+
+    public static Future<BufferedImage> resize(
+            final BufferedImage src,
+            final Method scalingMethod,
+            final Mode resizeMode,
+            final int targetSize,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.resize(src, scalingMethod, resizeMode, targetSize, ops));
+    }
+
+    public static Future<BufferedImage> resize(
+            final BufferedImage src,
+            final int targetWidth,
+            final int targetHeight,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.resize(src, targetWidth, targetHeight, ops));
+    }
+
+    public static Future<BufferedImage> resize(
+            final BufferedImage src,
+            final Method scalingMethod,
+            final int targetWidth,
+            final int targetHeight,
+            final BufferedImageOp... ops
+    ) {
+        checkService();
+        return service.submit(() -> ImegeScaler.resize(src, scalingMethod, targetWidth, targetHeight, ops));
+    }
+
+    public static Future<BufferedImage> resize(
+            final BufferedImage src,
+            final Mode resizeMode,
+            final int targetWidth,
+            final int targetHeight,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.resize(src, resizeMode, targetWidth, targetHeight, ops));
+    }
+
+    public static Future<BufferedImage> resize(
+            final BufferedImage src,
+            final Method scalingMethod,
+            final Mode resizeMode,
+            final int targetWidth,
+            final int targetHeight,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.resize(src, scalingMethod, resizeMode, targetWidth, targetHeight, ops));
+    }
+
+    public static Future<BufferedImage> rotate(
+            final BufferedImage src,
+            final Rotation rotation,
+            final BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        checkService();
+        return service.submit(() -> ImegeScaler.rotate(src, rotation, ops));
+    }
+
+    protected static ExecutorService createService() {
+        return createService(new DefaultThreadFactory());
+    }
+
+    protected static ExecutorService createService(
+            ThreadFactory factory
+    ) throws IllegalArgumentException {
+        if (factory == null) {
+            throw new IllegalArgumentException("factory cannot be null");
+        }
+        return Executors.newFixedThreadPool(THREAD_COUNT, factory);
+    }
+
+    protected static void checkService() {
+        if (service == null || service.isShutdown() || service.isTerminated()) {
+            service = createService();
+        }
+    }
+
+    protected static class DefaultThreadFactory implements ThreadFactory {
+        protected static final AtomicInteger poolNumber = new AtomicInteger(1);
+
+        protected final ThreadGroup group;
+        protected final AtomicInteger threadNumber = new AtomicInteger(1);
+        protected final String namePrefix;
+
+        @SuppressWarnings("removal")
+        DefaultThreadFactory() {
+            SecurityManager manager = System.getSecurityManager();
+            group = (manager == null ? Thread.currentThread().getThreadGroup() : manager.getThreadGroup());
+            namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
+        }
+
+        public Thread newThread(Runnable r) {
+            Thread thread = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
+            thread.setDaemon(false);
+            thread.setPriority(Thread.NORM_PRIORITY);
+            return thread;
+        }
+    }
+
+    protected static class ServerThreadFactory extends DefaultThreadFactory {
+        @Override
+        public Thread newThread(Runnable r) {
+            Thread thread = super.newThread(r);
+            thread.setDaemon(true);
+            thread.setPriority(Thread.MIN_PRIORITY);
+            return thread;
+        }
+    }
+}

+ 556 - 0
src/main/java/eu/mjdev/desktop/helpers/image/ImegeScaler.java

@@ -0,0 +1,556 @@
+package eu.mjdev.desktop.helpers.image;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Transparency;
+import java.awt.color.ColorSpace;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.BufferedImageOp;
+import java.awt.image.ColorConvertOp;
+import java.awt.image.ConvolveOp;
+import java.awt.image.ImagingOpException;
+import java.awt.image.Kernel;
+import java.awt.image.RescaleOp;
+
+@SuppressWarnings("ALL")
+public class ImegeScaler {
+    public static final String DEBUG_PROPERTY_NAME = "imgscalr.debug";
+    public static final String LOG_PREFIX_PROPERTY_NAME = "imgscalr.logPrefix";
+    public static final boolean DEBUG = Boolean.getBoolean(DEBUG_PROPERTY_NAME);
+    public static final String LOG_PREFIX = System.getProperty(LOG_PREFIX_PROPERTY_NAME, "[imgscalr] ");
+    public static final ConvolveOp OP_ANTIALIAS = new ConvolveOp(
+            new Kernel(3, 3, new float[]{.0f, .08f, .0f, .08f, .68f, .08f, .0f, .08f, .0f}
+            ),
+            ConvolveOp.EDGE_NO_OP,
+            null
+    );
+    public static final RescaleOp OP_DARKER = new RescaleOp(0.9f, 0, null);
+    public static final RescaleOp OP_BRIGHTER = new RescaleOp(1.1f, 0, null);
+    public static final ColorConvertOp OP_GRAYSCALE = new ColorConvertOp(
+            ColorSpace.getInstance(ColorSpace.CS_GRAY),
+            null
+    );
+
+    static {
+        log(0, "Debug output ENABLED");
+    }
+
+    public static final int THRESHOLD_BALANCED_SPEED = 1600;
+    public static final int THRESHOLD_QUALITY_BALANCED = 800;
+
+    public static BufferedImage apply(
+            BufferedImage src,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        long t = -1;
+        if (DEBUG)
+            t = System.currentTimeMillis();
+        if (src == null)
+            throw new IllegalArgumentException("src cannot be null");
+        if (ops == null || ops.length == 0)
+            throw new IllegalArgumentException("ops cannot be null or empty");
+        int type = src.getType();
+        if (!(type == BufferedImage.TYPE_INT_RGB || type == BufferedImage.TYPE_INT_ARGB))
+            src = copyToOptimalImage(src);
+        if (DEBUG)
+            log(0, "Applying %d BufferedImageOps...", ops.length);
+        boolean hasReassignedSrc = false;
+        for (int i = 0; i < ops.length; i++) {
+            long subT = -1;
+            if (DEBUG)
+                subT = System.currentTimeMillis();
+            BufferedImageOp op = ops[i];
+            if (op == null)
+                continue;
+            if (DEBUG)
+                log(1, "Applying BufferedImageOp [class=%s, toString=%s]...",
+                        op.getClass(), op.toString());
+            Rectangle2D resultBounds = op.getBounds2D(src);
+            if (resultBounds == null)
+                throw new ImagingOpException(
+                        "BufferedImageOp ["
+                                + op
+                                + "] getBounds2D(src) returned null bounds for the target image; this should not happen and indicates a problem with application of this type of op.");
+            BufferedImage dest = createOptimalImage(src,
+                    (int) Math.round(resultBounds.getWidth()),
+                    (int) Math.round(resultBounds.getHeight()));
+            BufferedImage result = op.filter(src, dest);
+            if (hasReassignedSrc)
+                src.flush();
+            src = result;
+            hasReassignedSrc = true;
+            if (DEBUG)
+                log(1,
+                        "Applied BufferedImageOp in %d ms, result [width=%d, height=%d]",
+                        System.currentTimeMillis() - subT, result.getWidth(),
+                        result.getHeight());
+        }
+        if (DEBUG)
+            log(0, "All %d BufferedImageOps applied in %d ms", ops.length,
+                    System.currentTimeMillis() - t);
+        return src;
+    }
+
+    public static BufferedImage crop(
+            BufferedImage src,
+            int width,
+            int height,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        return crop(src, 0, 0, width, height, ops);
+    }
+
+    public static BufferedImage crop(
+            BufferedImage src,
+            int x,
+            int y,
+            int width,
+            int height,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        long t = -1;
+        if (DEBUG)
+            t = System.currentTimeMillis();
+        if (src == null)
+            throw new IllegalArgumentException("src cannot be null");
+        if (x < 0 || y < 0 || width < 0 || height < 0)
+            throw new IllegalArgumentException("Invalid crop bounds: x [" + x
+                    + "], y [" + y + "], width [" + width + "] and height ["
+                    + height + "] must all be >= 0");
+        int srcWidth = src.getWidth();
+        int srcHeight = src.getHeight();
+        if ((x + width) > srcWidth)
+            throw new IllegalArgumentException(
+                    "Invalid crop bounds: x + width [" + (x + width)
+                            + "] must be <= src.getWidth() [" + srcWidth + "]");
+        if ((y + height) > srcHeight)
+            throw new IllegalArgumentException(
+                    "Invalid crop bounds: y + height [" + (y + height)
+                            + "] must be <= src.getHeight() [" + srcHeight
+                            + "]");
+        if (DEBUG)
+            log(0,
+                    "Cropping Image [width=%d, height=%d] to [x=%d, y=%d, width=%d, height=%d]...",
+                    srcWidth, srcHeight, x, y, width, height);
+        BufferedImage result = createOptimalImage(src, width, height);
+        Graphics g = result.getGraphics();
+        g.drawImage(src, 0, 0, width, height, x, y, (x + width), (y + height), null);
+        g.dispose();
+        if (DEBUG)
+            log(0, "Cropped Image in %d ms", System.currentTimeMillis() - t);
+        if (ops != null && ops.length > 0)
+            result = apply(result, ops);
+        return result;
+    }
+
+    public static BufferedImage pad(
+            BufferedImage src,
+            int padding,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        return pad(src, padding, Color.BLACK, ops);
+    }
+
+    public static BufferedImage pad(
+            BufferedImage src,
+            int padding,
+            Color color,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        long t = -1;
+        if (DEBUG)
+            t = System.currentTimeMillis();
+        if (src == null)
+            throw new IllegalArgumentException("src cannot be null");
+        if (padding < 1)
+            throw new IllegalArgumentException("padding [" + padding + "] must be > 0");
+        if (color == null)
+            throw new IllegalArgumentException("color cannot be null");
+        int srcWidth = src.getWidth();
+        int srcHeight = src.getHeight();
+        int sizeDiff = (padding * 2);
+        int newWidth = srcWidth + sizeDiff;
+        int newHeight = srcHeight + sizeDiff;
+        if (DEBUG)
+            log(0,
+                    "Padding Image from [originalWidth=%d, originalHeight=%d, padding=%d] to [newWidth=%d, newHeight=%d]...",
+                    srcWidth, srcHeight, padding, newWidth, newHeight);
+
+        boolean colorHasAlpha = (color.getAlpha() != 255);
+        boolean imageHasAlpha = (src.getTransparency() != BufferedImage.OPAQUE);
+        BufferedImage result;
+        if (colorHasAlpha || imageHasAlpha) {
+            if (DEBUG)
+                log(1,
+                        "Transparency FOUND in source image or color, using ARGB image type...");
+            result = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
+        } else {
+            if (DEBUG)
+                log(1,
+                        "Transparency NOT FOUND in source image or color, using RGB image type...");
+            result = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
+        }
+        Graphics g = result.getGraphics();
+        g.setColor(color);
+        g.fillRect(0, 0, newWidth, padding);
+        g.fillRect(0, padding, padding, newHeight);
+        g.fillRect(padding, newHeight - padding, newWidth, newHeight);
+        g.fillRect(newWidth - padding, padding, newWidth, newHeight - padding);
+        g.drawImage(src, padding, padding, null);
+        g.dispose();
+        if (DEBUG)
+            log(0, "Padding Applied in %d ms", System.currentTimeMillis() - t);
+        if (ops != null && ops.length > 0)
+            result = apply(result, ops);
+        return result;
+    }
+
+    public static BufferedImage resize(
+            BufferedImage src,
+            int targetSize,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetSize, targetSize, ops);
+    }
+
+    public static BufferedImage resize(
+            BufferedImage src,
+            Method scalingMethod,
+            int targetSize,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        return resize(src, scalingMethod, Mode.AUTOMATIC, targetSize, targetSize, ops);
+    }
+
+    public static BufferedImage resize(
+            BufferedImage src,
+            Mode resizeMode,
+            int targetSize,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        return resize(src, Method.AUTOMATIC, resizeMode, targetSize, targetSize, ops);
+    }
+
+    public static BufferedImage resize(
+            BufferedImage src,
+            Method scalingMethod,
+            Mode resizeMode,
+            int targetSize,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        return resize(src, scalingMethod, resizeMode, targetSize, targetSize, ops);
+    }
+
+    public static BufferedImage resize(
+            BufferedImage src,
+            int targetWidth,
+            int targetHeight,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetWidth, targetHeight, ops);
+    }
+
+    public static BufferedImage resize(
+            BufferedImage src,
+            Method scalingMethod,
+            int targetWidth,
+            int targetHeight,
+            BufferedImageOp... ops
+    ) {
+        return resize(src, scalingMethod, Mode.AUTOMATIC, targetWidth, targetHeight, ops);
+    }
+
+    public static BufferedImage resize(
+            BufferedImage src,
+            Mode resizeMode,
+            int targetWidth,
+            int targetHeight,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        return resize(src, Method.AUTOMATIC, resizeMode, targetWidth, targetHeight, ops);
+    }
+
+    public static BufferedImage resize(
+            BufferedImage src,
+            Method scalingMethod,
+            Mode resizeMode,
+            int targetWidth,
+            int targetHeight,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        long t = -1;
+        if (DEBUG)
+            t = System.currentTimeMillis();
+        if (src == null)
+            throw new IllegalArgumentException("src cannot be null");
+        if (targetWidth < 0)
+            throw new IllegalArgumentException("targetWidth must be >= 0");
+        if (targetHeight < 0)
+            throw new IllegalArgumentException("targetHeight must be >= 0");
+        if (scalingMethod == null)
+            throw new IllegalArgumentException(
+                    "scalingMethod cannot be null. A good default value is Method.AUTOMATIC.");
+        if (resizeMode == null)
+            throw new IllegalArgumentException(
+                    "resizeMode cannot be null. A good default value is Mode.AUTOMATIC.");
+        BufferedImage result = null;
+        int currentWidth = src.getWidth();
+        int currentHeight = src.getHeight();
+        float ratio = ((float) currentHeight / (float) currentWidth);
+        if (DEBUG)
+            log(0,
+                    "Resizing Image [size=%dx%d, resizeMode=%s, orientation=%s, ratio(H/W)=%f] to [targetSize=%dx%d]",
+                    currentWidth, currentHeight, resizeMode,
+                    (ratio <= 1 ? "Landscape/Square" : "Portrait"), ratio,
+                    targetWidth, targetHeight);
+        if (resizeMode == Mode.FIT_EXACT) {
+            if (DEBUG)
+                log(1,
+                        "Resize Mode FIT_EXACT used, no width/height checking or re-calculation will be done.");
+        } else if (resizeMode == Mode.BEST_FIT_BOTH) {
+            float requestedHeightScaling = ((float) targetHeight / (float) currentHeight);
+            float requestedWidthScaling = ((float) targetWidth / (float) currentWidth);
+            float actualScaling = Math.min(requestedHeightScaling, requestedWidthScaling);
+            targetHeight = Math.round((float) currentHeight * actualScaling);
+            targetWidth = Math.round((float) currentWidth * actualScaling);
+            if (targetHeight == currentHeight && targetWidth == currentWidth)
+                return src;
+            if (DEBUG)
+                log(1, "Auto-Corrected width and height based on scalingRatio %d.", actualScaling);
+        } else {
+            if ((ratio <= 1 && resizeMode == Mode.AUTOMATIC)
+                    || (resizeMode == Mode.FIT_TO_WIDTH)) {
+                if (targetWidth == src.getWidth()) {
+                    return src;
+                }
+                int originalTargetHeight = targetHeight;
+                targetHeight = (int) Math.ceil((float) targetWidth * ratio);
+                if (DEBUG && originalTargetHeight != targetHeight)
+                    log(1,
+                            "Auto-Corrected targetHeight [from=%d to=%d] to honor image proportions.",
+                            originalTargetHeight, targetHeight);
+            } else {
+                if (targetHeight == src.getHeight())
+                    return src;
+                int originalTargetWidth = targetWidth;
+                targetWidth = Math.round((float) targetHeight / ratio);
+                if (DEBUG && originalTargetWidth != targetWidth)
+                    log(1,
+                            "Auto-Corrected targetWidth [from=%d to=%d] to honor image proportions.",
+                            originalTargetWidth, targetWidth);
+            }
+        }
+        if (scalingMethod == Method.AUTOMATIC)
+            scalingMethod = determineScalingMethod(targetWidth, targetHeight, ratio);
+        if (DEBUG)
+            log(1, "Using Scaling Method: %s", scalingMethod);
+        if (scalingMethod == Method.SPEED) {
+            result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
+        } else if (scalingMethod == Method.BALANCED) {
+            result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+        } else if (scalingMethod == Method.QUALITY || scalingMethod == Method.ULTRA_QUALITY) {
+            if (targetWidth > currentWidth || targetHeight > currentHeight) {
+                if (DEBUG)
+                    log(1,
+                            "QUALITY scale-up, a single BICUBIC scale operation will be used...");
+                result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+            } else {
+                if (DEBUG)
+                    log(1,
+                            "QUALITY scale-down, incremental scaling will be used...");
+                result = scaleImageIncrementally(
+                        src,
+                        targetWidth,
+                        targetHeight,
+                        scalingMethod,
+                        RenderingHints.VALUE_INTERPOLATION_BICUBIC
+                );
+            }
+        }
+        if (DEBUG) {
+            log(0, "Resized Image in %d ms", System.currentTimeMillis() - t);
+        }
+        if (ops != null && ops.length > 0) {
+            result = apply(result, ops);
+        }
+        return result;
+    }
+
+    public static BufferedImage rotate(
+            BufferedImage src, Rotation rotation,
+            BufferedImageOp... ops
+    ) throws IllegalArgumentException, ImagingOpException {
+        long t = -1;
+        if (DEBUG)
+            t = System.currentTimeMillis();
+        if (src == null)
+            throw new IllegalArgumentException("src cannot be null");
+        if (rotation == null)
+            throw new IllegalArgumentException("rotation cannot be null");
+        if (DEBUG)
+            log(0, "Rotating Image [%s]...", rotation);
+        int newWidth = src.getWidth();
+        int newHeight = src.getHeight();
+        AffineTransform tx = new AffineTransform();
+        switch (rotation) {
+            case CW_90:
+                newWidth = src.getHeight();
+                newHeight = src.getWidth();
+                tx.translate(newWidth, 0);
+                tx.quadrantRotate(1);
+                break;
+            case CW_270:
+                newWidth = src.getHeight();
+                newHeight = src.getWidth();
+                tx.translate(0, newHeight);
+                tx.quadrantRotate(3);
+                break;
+            case CW_180:
+                tx.translate(newWidth, newHeight);
+                tx.quadrantRotate(2);
+                break;
+            case FLIP_HORZ:
+                tx.translate(newWidth, 0);
+                tx.scale(-1.0, 1.0);
+                break;
+            case FLIP_VERT:
+                tx.translate(0, newHeight);
+                tx.scale(1.0, -1.0);
+                break;
+        }
+        BufferedImage result = createOptimalImage(src, newWidth, newHeight);
+        Graphics2D g2d = (Graphics2D) result.createGraphics();
+        g2d.drawImage(src, tx, null);
+        g2d.dispose();
+        if (DEBUG)
+            log(0, "Rotation Applied in %d ms, result [width=%d, height=%d]",
+                    System.currentTimeMillis() - t, result.getWidth(),
+                    result.getHeight());
+        if (ops != null && ops.length > 0)
+            result = apply(result, ops);
+        return result;
+    }
+
+    protected static void log(int depth, String message, Object... params) {
+        if (ImegeScaler.DEBUG) {
+            System.out.print(ImegeScaler.LOG_PREFIX);
+            for (int i = 0; i < depth; i++)
+                System.out.print("\t");
+            System.out.printf(message, params);
+            System.out.println();
+        }
+    }
+
+    protected static BufferedImage createOptimalImage(BufferedImage src) {
+        return createOptimalImage(src, src.getWidth(), src.getHeight());
+    }
+
+    protected static BufferedImage createOptimalImage(
+            BufferedImage src,
+            int width,
+            int height
+    ) throws IllegalArgumentException {
+        if (width <= 0 || height <= 0)
+            throw new IllegalArgumentException("width [" + width
+                    + "] and height [" + height + "] must be > 0");
+        return new BufferedImage(
+                width,
+                height,
+                (src.getTransparency() == Transparency.OPAQUE
+                        ? BufferedImage.TYPE_INT_RGB
+                        : BufferedImage.TYPE_INT_ARGB)
+        );
+    }
+
+    protected static BufferedImage copyToOptimalImage(
+            BufferedImage src
+    ) throws IllegalArgumentException {
+        if (src == null) {
+            throw new IllegalArgumentException("src cannot be null");
+        }
+        int type = (src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB);
+        BufferedImage result = new BufferedImage(src.getWidth(), src.getHeight(), type);
+        Graphics g = result.getGraphics();
+        g.drawImage(src, 0, 0, null);
+        g.dispose();
+        return result;
+    }
+
+    protected static Method determineScalingMethod(
+            int targetWidth,
+            int targetHeight,
+            float ratio
+    ) {
+        int length = (ratio <= 1 ? targetWidth : targetHeight);
+        Method result = Method.SPEED;
+        if (length <= ImegeScaler.THRESHOLD_QUALITY_BALANCED)
+            result = Method.QUALITY;
+        else if (length <= ImegeScaler.THRESHOLD_BALANCED_SPEED)
+            result = Method.BALANCED;
+        if (DEBUG)
+            log(2, "AUTOMATIC scaling method selected: %s", result.name());
+        return result;
+    }
+
+    protected static BufferedImage scaleImage(
+            BufferedImage src,
+            int targetWidth,
+            int targetHeight,
+            Object interpolationHintValue
+    ) {
+        BufferedImage result = createOptimalImage(src, targetWidth, targetHeight);
+        Graphics2D resultGraphics = result.createGraphics();
+        resultGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationHintValue);
+        resultGraphics.drawImage(src, 0, 0, targetWidth, targetHeight, null);
+        resultGraphics.dispose();
+        return result;
+    }
+
+    protected static BufferedImage scaleImageIncrementally(
+            BufferedImage src,
+            int targetWidth,
+            int targetHeight,
+            Method scalingMethod,
+            Object interpolationHintValue
+    ) {
+        boolean hasReassignedSrc = false;
+        int incrementCount = 0;
+        int currentWidth = src.getWidth();
+        int currentHeight = src.getHeight();
+        int fraction = (scalingMethod == Method.ULTRA_QUALITY ? 7 : 2);
+        do {
+            int prevCurrentWidth = currentWidth;
+            int prevCurrentHeight = currentHeight;
+            if (currentWidth > targetWidth) {
+                currentWidth -= (currentWidth / fraction);
+                if (currentWidth < targetWidth)
+                    currentWidth = targetWidth;
+            }
+            if (currentHeight > targetHeight) {
+                currentHeight -= (currentHeight / fraction);
+                if (currentHeight < targetHeight)
+                    currentHeight = targetHeight;
+            }
+            if (prevCurrentWidth == currentWidth && prevCurrentHeight == currentHeight)
+                break;
+            if (DEBUG)
+                log(2, "Scaling from [%d x %d] to [%d x %d]", prevCurrentWidth,
+                        prevCurrentHeight, currentWidth, currentHeight);
+            BufferedImage incrementalImage = scaleImage(src, currentWidth, currentHeight, interpolationHintValue);
+            if (hasReassignedSrc) {
+                src.flush();
+            }
+            src = incrementalImage;
+            hasReassignedSrc = true;
+            incrementCount++;
+        } while (currentWidth != targetWidth || currentHeight != targetHeight);
+        if (DEBUG) {
+            log(2, "Incrementally Scaled Image in %d steps.", incrementCount);
+        }
+        return src;
+    }
+}

+ 9 - 0
src/main/java/eu/mjdev/desktop/helpers/image/Method.java

@@ -0,0 +1,9 @@
+package eu.mjdev.desktop.helpers.image;
+
+public enum Method {
+    AUTOMATIC,
+    SPEED,
+    BALANCED,
+    QUALITY,
+    ULTRA_QUALITY
+}

+ 10 - 0
src/main/java/eu/mjdev/desktop/helpers/image/Mode.java

@@ -0,0 +1,10 @@
+package eu.mjdev.desktop.helpers.image;
+
+@SuppressWarnings("ALL")
+public enum Mode {
+    AUTOMATIC,
+    FIT_EXACT,
+    BEST_FIT_BOTH,
+    FIT_TO_WIDTH,
+    FIT_TO_HEIGHT
+}

+ 9 - 0
src/main/java/eu/mjdev/desktop/helpers/image/Rotation.java

@@ -0,0 +1,9 @@
+package eu.mjdev.desktop.helpers.image;
+
+public enum Rotation {
+    CW_90,
+    CW_180,
+    CW_270,
+    FLIP_HORZ,
+    FLIP_VERT
+}

+ 11 - 1
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -5,6 +5,7 @@ import androidx.compose.animation.ExitTransition
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation.Horizontal
+import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.itemsIndexed
@@ -17,6 +18,7 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.isSecondaryPressed
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.WindowPosition
@@ -24,6 +26,7 @@ import eu.mjdev.desktop.components.controlcenter.ControlCenterPage.ControlCenter
 import eu.mjdev.desktop.components.sliding.SlidingMenu
 import eu.mjdev.desktop.components.sliding.VisibilityState
 import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.extensions.ColorUtils.alpha
 import eu.mjdev.desktop.extensions.Modifier.rightShadow
 import eu.mjdev.desktop.helpers.animation.Animations.ControlCenterEnterAnimation
 import eu.mjdev.desktop.helpers.animation.Animations.ControlCenterExitAnimation
@@ -38,7 +41,7 @@ import kotlinx.coroutines.launch
 @Composable
 fun ControlCenter(
     api: DesktopProvider = LocalDesktop.current,
-    shadowColor: Color = Color.Black.copy(alpha = 0.3f), // todo theme
+    shadowColor: Color = Color.Black.alpha(0.3f), // todo theme
     pagerState: MutableState<Int> = remember { mutableStateOf(0) },
     controlCenterState: VisibilityState = rememberVisibilityState(),
     scope: CoroutineScope = rememberCoroutineScope(),
@@ -117,6 +120,13 @@ fun ControlCenter(
                                                 color = if (isSelected) Color.White else Color.Transparent,
                                                 shape = RoundedCornerShape(8.dp)
                                             )
+                                            .pointerInput(Unit) {
+                                                detectTapGestures(
+                                                    onTap = {
+                                                        pagerState.value = idx
+                                                    }
+                                                )
+                                            }
                                             .mouseClickable {
                                                 if (buttons.isSecondaryPressed) {
                                                     onContextMenuClick()

+ 3 - 1
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/applets/DesktopPanelFavoriteApps.kt

@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -32,7 +34,7 @@ fun DesktopPanelFavoriteApps(
     onAppClick: (app: App) -> Unit,
     onContextMenuClick: (app: App) -> Unit
 ) {
-    val apps = remember(api.appsProvider.favoriteApps.size) { api.appsProvider.favoriteApps }
+    val apps by remember(api.appsProvider.favoriteApps.size) { mutableStateOf( api.appsProvider.favoriteApps) }
     Box(
         modifier = modifier
     ) {

+ 47 - 0
src/main/kotlin/eu/mjdev/desktop/components/image/GifView.kt

@@ -0,0 +1,47 @@
+package eu.mjdev.desktop.components.image
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import androidx.compose.ui.unit.IntSize
+import eu.mjdev.desktop.helpers.gif.GifDecoder
+import kotlinx.coroutines.delay
+import java.awt.image.BufferedImage
+import java.io.File
+import java.io.FileInputStream
+
+@Composable
+fun GifView(
+    modifier: Modifier = Modifier,
+    src: String
+) = BoxWithConstraints {
+    val file by remember(src) { mutableStateOf(File(src)) }
+    val streamData by remember(file) { mutableStateOf(FileInputStream(file)) }
+    val gifDecoder by remember(streamData) { mutableStateOf(
+        GifDecoder().apply { read(streamData) }) }
+    val framesCount by remember(gifDecoder) { mutableStateOf(gifDecoder.frameCount) }
+    var currentFrame by remember(gifDecoder) { mutableStateOf(0) }
+    var decodedImage by remember { mutableStateOf<BufferedImage?>(null) }
+    Canvas(
+        modifier = modifier
+    ) {
+        decodedImage?.toComposeImageBitmap()?.let { img ->
+            drawImage(
+                image = img,
+                srcSize = IntSize(img.width, img.height),
+                dstSize = IntSize(constraints.maxWidth, constraints.maxHeight),
+            )
+        }
+    }
+    LaunchedEffect(currentFrame, decodedImage) {
+        decodedImage = gifDecoder.getFrame(currentFrame)
+        delay(gifDecoder.getDelay(currentFrame).toLong())
+        var nextFrame = currentFrame + 1
+        if (nextFrame > framesCount) {
+            nextFrame = 0
+        }
+        currentFrame = nextFrame
+    }
+}

+ 32 - 7
src/main/kotlin/eu/mjdev/desktop/components/image/ImageAny.kt

@@ -8,13 +8,15 @@ import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.DefaultAlpha
 import androidx.compose.ui.graphics.FilterQuality
 import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality
+import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.layout.ContentScale
 import coil3.compose.AsyncImage
-import eu.mjdev.desktop.components.video.VideoPlayerView
+import eu.mjdev.desktop.components.video.VideoView
 import eu.mjdev.desktop.extensions.Compose.asyncImageLoader
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import java.io.File
 import androidx.compose.foundation.Image as ComposeImage
 
 @Suppress("FunctionName")
@@ -32,13 +34,26 @@ fun ImageAny(
     onLoading: () -> Unit = {},
     onFail: (error: Throwable) -> Unit = {}
 ) {
+    // todo from mime
+    val isGif = src.toString().trim().endsWith(".gif")
     val isVideo = src.toString().trim().let {
-        it.endsWith(".gif") || it.endsWith(".mp4") || it.endsWith(".mpg")
+        it.endsWith(".mp4") || it.endsWith(".mpg")
     }
     when {
-        src == null -> Box(modifier = modifier)
+        src == null -> Box(
+            modifier = modifier
+        )
 
-        isVideo -> VideoPlayerView(
+        isGif -> GifView(
+            modifier = modifier,
+            src = when (src) {
+                is File -> src.absolutePath
+                is String -> src
+                else -> src.toString()
+            }
+        )
+
+        isVideo -> VideoView(
             modifier = modifier,
             src = src,
             alignment = alignment,
@@ -58,6 +73,16 @@ fun ImageAny(
             contentScale = contentScale
         )
 
+        src is Painter -> ComposeImage(
+            modifier = modifier,
+            painter = src,
+            alignment = alignment,
+            alpha = alpha,
+            contentDescription = contentDescription,
+            colorFilter = colorFilter,
+            contentScale = contentScale
+        )
+
         else -> AsyncImage(
             modifier = modifier,
             model = src,
@@ -65,12 +90,12 @@ fun ImageAny(
             contentScale = contentScale,
             filterQuality = filterQuality,
             imageLoader = api.imageLoader ?: asyncImageLoader(),
-            onError = {
-                onFail(it.result.throwable)
-            },
             onLoading = {
                 onLoading()
             },
+            onError = {
+                onFail(it.result.throwable)
+            },
         )
     }
 }

+ 11 - 9
src/main/kotlin/eu/mjdev/desktop/components/video/VideoPlayerView.kt → src/main/kotlin/eu/mjdev/desktop/components/video/VideoView.kt

@@ -1,15 +1,17 @@
 package eu.mjdev.desktop.components.video
 
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.layout.ContentScale
+import java.io.File
 
 @Suppress("UNUSED_PARAMETER")
 @Composable
-fun VideoPlayerView(
+fun VideoView(
     modifier: Modifier = Modifier,
     src: Any?,
     alignment: Alignment = Alignment.Center,
@@ -22,12 +24,12 @@ fun VideoPlayerView(
     modifier = modifier
 ) {
     // todo
-//    VideoPlayerFFMpeg(
-//        modifier = Modifier.fillMaxSize().align(alignment),
-//        file = when (src) {
-//            is String -> src
-//            is File -> src.absolutePath
-//            else -> src.toString()
-//        }
-//    )
+    VideoPlayerFFMpeg(
+        modifier = Modifier.fillMaxSize().align(alignment),
+        file = when (src) {
+            is String -> src
+            is File -> src.absolutePath
+            else -> src.toString()
+        }
+    )
 }

+ 19 - 0
src/main/kotlin/eu/mjdev/desktop/extensions/BitmapUtils.kt

@@ -0,0 +1,19 @@
+package eu.mjdev.desktop.extensions
+
+import androidx.compose.ui.graphics.*
+import eu.mjdev.desktop.helpers.bitmap.Bitmap
+import org.jetbrains.skiko.toBufferedImage
+
+typealias  SkiaBitmap = org.jetbrains.skia.Bitmap
+
+@Suppress("MemberVisibilityCanBePrivate", "unused")
+object BitmapUtils {
+
+    fun SkiaBitmap.toBitmap() = Bitmap(toBufferedImage())
+
+    fun Bitmap.getTopMostColor(): Color = createScaledBitmap(this, 1, 1).getPixel(0, 0)
+
+    fun SkiaBitmap.getTopMostColor(): Color = toBitmap().getTopMostColor()
+
+    fun ImageBitmap.getTopMostColor(): Color = asSkiaBitmap().getTopMostColor()
+}

+ 111 - 0
src/main/kotlin/eu/mjdev/desktop/extensions/ColorUtils.kt

@@ -0,0 +1,111 @@
+package eu.mjdev.desktop.extensions
+
+import androidx.compose.ui.graphics.Color
+import java.util.*
+import kotlin.math.cos
+
+@Suppress("unused", "UnusedReceiverParameter")
+object ColorUtils {
+    val Color.randomAlpha: Float
+        get() {
+            val rnd = Random()
+            return (1f / 255f) * (128f + (rnd.nextInt(128)))
+        }
+
+    fun Color.darker(factor: Float): Color {
+        val a: Int = this.alpha.toInt()
+        val r = Math.round(this.red - factor)
+        val g = Math.round(this.green - factor)
+        val b = Math.round(this.blue - factor)
+        return Color(r, g, b, a)
+    }
+
+    fun Color.lighter(factor: Float): Color {
+        val a: Int = this.alpha.toInt()
+        val r = Math.round(this.red + factor)
+        val g = Math.round(this.green + factor)
+        val b = Math.round(this.blue + factor)
+        return Color(
+            if (r < 256) r else 255,
+            if (g < 256) g else 255,
+            if (b < 256) b else 255,
+            a
+        )
+    }
+
+    fun linearInterpolation(
+        start: Int,
+        end: Int,
+        normalizedValue: Float
+    ): Int {
+        return (start + ((end - start) * normalizedValue)).toInt()
+    }
+
+    fun sinInterpolation(
+        start: Int,
+        end: Int,
+        normalizedValue: Float
+    ): Int {
+        return ((start + (end - start) * (1 - cos(normalizedValue * Math.PI)) / 2)).toInt()
+    }
+
+    fun getAlpha(color: Int): Int {
+        return (color shr 24) and 0xff
+    }
+
+    fun Color.alpha(a: Float) = alpha((255 * a).toInt())
+
+    fun Color.alpha(alpha: Int): Color {
+        return Color(
+            this.red.toInt(),
+            this.green.toInt(),
+            this.blue.toInt(),
+            (this.alpha.toInt() and 0x00ffffff) or ((alpha and 0xff) shl 24)
+        )
+    }
+
+    fun Color.r(r: Int): Color {
+        return Color(
+            r,
+            this.green.toInt(),
+            this.blue.toInt(),
+            this.alpha.toInt()
+        )
+    }
+
+    fun Color.g(g: Int): Color {
+        return Color(
+            this.red.toInt(),
+            g,
+            this.blue.toInt(),
+            this.alpha.toInt()
+        )
+    }
+
+    fun Color.b(b: Int): Color {
+        return Color(
+            this.red.toInt(),
+            this.green.toInt(),
+            b,
+            this.alpha.toInt()
+        )
+    }
+
+    fun Color.isLightColor(): Boolean {
+        val r = this.red
+        val g = this.green
+        val b = this.blue
+        return ((r > 127) && (g > 127) && (b > 127))
+    }
+
+    fun Color.randomColor(alpha: Float): Color = Random().let { rnd ->
+        Color(
+            rnd.nextInt(256),
+            rnd.nextInt(256),
+            rnd.nextInt(256),
+            alpha.toInt(),
+        )
+    }
+
+
+}

+ 194 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/bitmap/Bitmap.kt

@@ -0,0 +1,194 @@
+package eu.mjdev.desktop.helpers.bitmap
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.colorspace.ColorSpace
+import androidx.compose.ui.graphics.colorspace.ColorSpaces
+import eu.mjdev.desktop.helpers.image.ImegeScaler
+import okio.Sink
+import okio.buffer
+import java.awt.image.BufferedImage
+import java.io.IOException
+import javax.imageio.IIOImage
+import javax.imageio.ImageIO
+import javax.imageio.ImageWriteParam
+import javax.imageio.stream.ImageOutputStream
+import kotlin.math.abs
+
+
+@Suppress("unused", "UNUSED_PARAMETER")
+class Bitmap(
+    val image: BufferedImage
+) {
+    val width: Int = image.width
+    val height: Int = image.height
+    val config: Config = Config.ARGB_8888
+    val colorSpace: ColorSpace = ColorSpaces.Srgb
+
+    enum class CompressFormat {
+        JPEG,
+        PNG,
+        WEBP_LOSSY,
+        WEBP_LOSSLESS,
+    }
+
+    enum class Config {
+        ALPHA_8,
+        RGB_565,
+        ARGB_4444,
+        ARGB_8888,
+        RGBA_F16,
+        HARDWARE,
+        RGBA_1010102,
+    }
+
+    fun compress(format: CompressFormat, quality: Int, sink: Sink): Boolean {
+        require(quality in 0..100) { "quality must be 0..100" }
+        val qualityFloat = quality.toFloat() / 100
+        val formatString = when (format) {
+            CompressFormat.PNG -> "png"
+            CompressFormat.JPEG -> "jpg"
+            else -> throw IllegalArgumentException("unsupported compression format!")
+        }
+        val writers = ImageIO.getImageWritersByFormatName(formatString)
+        check(writers.hasNext()) { "no image writers found for this format!" }
+        val writer = writers.next()
+        val ios: ImageOutputStream = try {
+            ImageIO.createImageOutputStream(sink.buffer().outputStream())
+        } catch (ex: IOException) {
+            throw RuntimeException(ex)
+        }
+        writer.output = ios
+        val param = writer.defaultWriteParam
+        if ("jpg" == formatString) {
+            param.compressionMode = ImageWriteParam.MODE_EXPLICIT
+            param.compressionQuality = qualityFloat
+        }
+        try {
+            writer.write(null, IIOImage(image, null, null), param)
+            ios.close()
+            writer.dispose()
+        } catch (ex: IOException) {
+            throw RuntimeException(ex)
+        }
+        return true
+    }
+
+    private fun checkPixelsAccess(
+        x: Int,
+        y: Int,
+        width: Int,
+        height: Int,
+        offset: Int,
+        stride: Int,
+        pixels: IntArray,
+    ) {
+        checkXYSign(x, y)
+        require(width >= 0) { "width must be >= 0" }
+        if (height < 0) {
+            throw IllegalArgumentException("height must be >= 0")
+        }
+        if (x + width > this.width) {
+            throw IllegalArgumentException("x + width must be <= bitmap.width()")
+        }
+        if (y + height > this.height) {
+            throw IllegalArgumentException("y + height must be <= bitmap.height()")
+        }
+        if (abs(stride) < width) {
+            throw IllegalArgumentException("abs(stride) must be >= width")
+        }
+        val lastScanline = offset + (height - 1) * stride
+        val length = pixels.size
+        if (offset < 0 || offset + width > length || lastScanline < 0 || lastScanline + width > length) {
+            throw ArrayIndexOutOfBoundsException()
+        }
+    }
+
+    fun createScaledBitmap(
+        src: Bitmap,
+        dstWidth: Int,
+        dstHeight: Int,
+    ): Bitmap = Bitmap(ImegeScaler.resize(src.image, dstWidth, dstHeight))
+
+    fun getPixel(x: Int, y: Int): Color = IntArray(4).let { pixelData ->
+        image.data.getPixel(x, y, pixelData)
+    }.let {
+        Color(it[0], it[1], it[2], it[3])
+    }
+
+    fun getPixels(
+        pixels: IntArray,
+        offset: Int,
+        stride: Int,
+        x: Int,
+        y: Int,
+        width: Int,
+        height: Int,
+    ) {
+        checkPixelsAccess(x, y, width, height, offset, stride, pixels)
+        val raster = image.data
+        val rasterPixels = raster.getPixels(x, y, width, height, null as IntArray?)
+        for (ht in 0 until height) {
+            val rowOffset = offset + stride * ht
+            System.arraycopy(rasterPixels, ht * width, pixels, rowOffset, width)
+        }
+    }
+
+    fun setPixels(
+        destination: IntArray,
+        topLeft: Int,
+        downsampledWidth: Int,
+        downsampledX: Int,
+        downsampledY: Int,
+        downsampledW: Int,
+        downsampledH: Int
+    ) : Bitmap {
+
+        // todo
+        return this
+    }
+
+    companion object {
+        fun Bitmap.applyCanvas(block: Canvas.() -> Unit): Bitmap {
+            Canvas(this).apply(block)
+            return this
+        }
+
+        private fun checkXYSign(x: Int, y: Int) {
+            if (x < 0) {
+                throw IllegalArgumentException("x must be >= 0")
+            }
+            if (y < 0) {
+                throw IllegalArgumentException("y must be >= 0")
+            }
+        }
+
+        private fun checkWidthHeight(width: Int, height: Int) {
+            if (width <= 0) {
+                throw IllegalArgumentException("width must be > 0")
+            }
+            if (height <= 0) {
+                throw IllegalArgumentException("height must be > 0")
+            }
+        }
+
+        fun createBitmap(width: Int, height: Int, config: Config): Bitmap {
+            val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
+            return Bitmap(image)
+        }
+
+        fun createBitmap(source: Bitmap, x: Int, y: Int, width: Int, height: Int): Bitmap {
+            checkXYSign(x, y)
+            checkWidthHeight(width, height)
+            if (x + width > source.width) {
+                throw IllegalArgumentException("x + width must be <= bitmap.width()")
+            }
+            if (y + height > source.height) {
+                throw IllegalArgumentException("y + height must be <= bitmap.height()")
+            }
+            val subImage = source.image.getSubimage(x, y, width, height)
+            val newImage = BufferedImage(subImage.width, subImage.height, subImage.type)
+            newImage.data = subImage.data
+            return Bitmap(newImage)
+        }
+    }
+}

+ 63 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/bitmap/BitmapFactory.kt

@@ -0,0 +1,63 @@
+package eu.mjdev.desktop.helpers.bitmap
+
+import okio.Source
+import okio.buffer
+import java.awt.image.BufferedImage
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import javax.imageio.ImageIO
+import javax.imageio.ImageReader
+
+@Suppress("unused")
+interface BitmapFactory {
+    fun decodeSource(source: Source): Bitmap
+
+    fun decodeByteArray(data: ByteArray, offset: Int, length: Int): Bitmap?
+
+    companion object : BitmapFactory {
+        private var instance: BitmapFactory = BitmapFactoryImageIoImpl()
+
+        override fun decodeSource(source: Source): Bitmap {
+            return instance.decodeSource(source)
+        }
+
+        override fun decodeByteArray(data: ByteArray, offset: Int, length: Int): Bitmap? {
+            return instance.decodeByteArray(data, offset, length)
+        }
+
+        fun setInstance(bitmapFactory: BitmapFactory) {
+            instance = bitmapFactory
+        }
+    }
+}
+
+private class BitmapFactoryImageIoImpl : BitmapFactory {
+
+    override fun decodeSource(source: Source): Bitmap {
+        val bitmap: Bitmap
+        try {
+            val imageInputStream = ImageIO.createImageInputStream(source.buffer().inputStream())
+            val imageReaders: Iterator<ImageReader> = ImageIO.getImageReaders(imageInputStream)
+            require(imageReaders.hasNext()) { "no reader for image" }
+            val imageReader: ImageReader = imageReaders.next()
+            imageReader.input = imageInputStream
+            val image: BufferedImage = imageReader.read(0, imageReader.defaultReadParam)
+            bitmap = Bitmap(image)
+            imageReader.dispose()
+        } catch (ex: IOException) {
+            throw RuntimeException(ex)
+        }
+        return bitmap
+    }
+
+    override fun decodeByteArray(data: ByteArray, offset: Int, length: Int): Bitmap {
+        val byteArrayStream = ByteArrayInputStream(data)
+        val bitmap: Bitmap = try {
+            val image = ImageIO.read(byteArrayStream)
+            Bitmap(image)
+        } catch (ex: IOException) {
+            throw RuntimeException(ex)
+        }
+        return bitmap
+    }
+}

+ 22 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/bitmap/Canvas.kt

@@ -0,0 +1,22 @@
+package eu.mjdev.desktop.helpers.bitmap
+
+import androidx.compose.ui.geometry.Rect
+import java.awt.Graphics2D
+import java.awt.image.BufferedImage
+
+@Suppress("unused")
+class Canvas (bitmap: Bitmap) {
+    private val canvasImage: BufferedImage = bitmap.image
+    private val canvas: Graphics2D = canvasImage.createGraphics()
+
+    fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect) {
+        val sourceImage = sourceBitmap.image
+        val sourceImageCropped :BufferedImage = sourceImage.getSubimage(
+            src.left.toInt(),
+            src.top.toInt(),
+            src.width.toInt(),
+            src.height.toInt()
+        )
+        canvas.drawImage(sourceImageCropped, null , dst.left.toInt(), dst.top.toInt())
+    }
+}

+ 6 - 2
src/main/kotlin/eu/mjdev/desktop/helpers/internal/Palette.kt

@@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.graphics.Color
-import com.materialkolor.ktx.themeColor
+import eu.mjdev.desktop.extensions.BitmapUtils.getTopMostColor
 import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
 import eu.mjdev.desktop.extensions.Image.loadPicture
 import kotlinx.coroutines.CoroutineScope
@@ -26,9 +26,13 @@ class Palette(
     fun update(src: Any?) = scope.launch {
         loadPicture(src)
             .getOrNull()
-            ?.themeColor(fallback = baseColor)?.also { color ->
+            ?.getTopMostColor()
+            ?.also { color ->
                 backgroundColor = color
             }
+//            ?.themeColor(fallback = baseColor)?.also { color ->
+//                backgroundColor = color
+//            }
     }
 
     companion object {