package net.cakenet.jsaton.model;
import net.cakenet.jsaton.util.CompressionUtil;
import net.cakenet.jsaton.util.DigestUtil;
import net.cakenet.jsaton.util.InvalidImageDataException;
import javax.xml.bind.DatatypeConverter;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.ref.WeakReference;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
public class SimpleImage {
private static final Map<String, WeakReference<SimpleImage>> decoded = new HashMap<>();
public final int width, height;
public final int[] pixels;
public SimpleImage(int[] pixels, int width, int height) {
this.pixels = pixels;
this.width = width;
this.height = height;
for(int i = 0; i < pixels.length; i++)
pixels[i] &= 0xffffff; // Remove alpha...
}
// Helpful script functions...
public Point findColor(int col) {
for (int i = 0; i < pixels.length; i++) {
if (pixels[i] == col) {
int y = i / width;
int x = i % width;
return new Point(x, y);
}
}
return null;
}
public boolean imageAt(SimpleImage image, int x, int y) {
if(x > width - image.width || y > height - image.height)
return false;
final int[] needle = image.pixels;
int off = y * width + x; // Offset into our pixel array to start at
int srchOff = 0; // Offset into needle pixel array to start at...
for(int y1 = 0; y1 < image.height; y1++) {
for(int x1 = 0; x1 < image.width; x1++) {
if(pixels[off + x1] != needle[srchOff + x1])
return false;
}
srchOff += image.width;
off += width; // Todo: inline this (do the addition during the inner loop and add less here...)
}
return true;
}
public Point findImage(SimpleImage image) {
final int endX = width - image.width;
final int endY = height - image.height;
for(int x = 0; x < endX; x++) {
for(int y = 0; y < endY; y++) {
if(imageAt(image, x, y))
return new Point(x, y);
}
}
return null;
}
public Point findImage(String encodedImage) {
try {
SimpleImage needle = decode(encodedImage);
return findImage(needle);
} catch (InvalidImageDataException e) {
e.printStackTrace();
}
return null;
}
// Todo: should we abstract this out and create an ArrayView class?
// would drastically reduce array creation and copying during runtime... (but would add overhead in access...)
public SimpleImage region(int x, int y, int width, int height) {
int[] dest = new int[width * height];
int srcOff = y * this.width + x;
int destOff = 0;
for(int i = y; i < height; i++) {
System.arraycopy(pixels, srcOff, dest, destOff, width);
srcOff += this.width;
destOff += width;
}
return new SimpleImage(dest, width, height);
}
// Todo: more codecs (simba, the new SCAR format...)
public byte[] toByteArray() {
int pl = width * height;
byte[] data = new byte[pl * 3];
int off = 0;
for (int p : pixels) {
data[off++] = (byte) ((p >> 16) & 0xff);
data[off++] = (byte) ((p >> 8) & 0xff);
data[off++] = (byte) (p & 0xff);
}
return data;
}
public String encode() {
byte[] raw = toByteArray();
byte[] complete = new byte[raw.length + 4]; // 2 bytes per dimension
complete[0] = (byte) ((width >> 8) & 0xff);
complete[1] = (byte) (width & 0xff);
complete[2] = (byte) ((height >> 8) & 0xff);
complete[3] = (byte) (height & 0xff);
System.arraycopy(raw, 0, complete, 4, raw.length);
return "g" + DatatypeConverter.printBase64Binary(CompressionUtil.deflate(complete, true));
}
public String encodeScar() {
return "c" + DatatypeConverter.printBase64Binary(CompressionUtil.deflate(toByteArray(), false));
}
public static SimpleImage decode(String str) throws InvalidImageDataException {
if(decoded.containsKey(str)) {
WeakReference<SimpleImage> ref = decoded.get(str);
SimpleImage val = ref.get();
if(val != null)
return val;
decoded.remove(str);
}
if (str.charAt(0) != 'g')
throw new InvalidImageDataException("Not a gzip compressed image");
byte[] data = CompressionUtil.inflate(DatatypeConverter.parseBase64Binary(str.substring(1)), true);
int width = ((data[0] & 0xff) << 8) | (data[1] & 0xff);
int height = ((data[2] & 0xff) << 8) | (data[3] & 0xff);
byte[] raw = new byte[data.length - 4];
System.arraycopy(data, 4, raw, 0, raw.length);
SimpleImage img = toImage(raw, width, height);
decoded.put(str, new WeakReference<>(img));
return img;
}
public static SimpleImage decodeScar(String src, int width, int height) throws InvalidImageDataException {
if(decoded.containsKey(src)) {
WeakReference<SimpleImage> ref = decoded.get(src);
SimpleImage val = ref.get();
if(val != null)
return val;
decoded.remove(src);
}
if (src.charAt(0) != 'c')
throw new InvalidImageDataException("Invalid SCAR string (doesn't start with 'c')");
SimpleImage img = toImage(CompressionUtil.inflate(DatatypeConverter.parseBase64Binary(src.substring(1)), false), width, height);
decoded.put(src, new WeakReference<>(img));
return img;
}
private static SimpleImage toImage(byte[] source, int width, int height) throws InvalidImageDataException {
int pl = width * height;
if (source.length != pl*3)
throw new InvalidImageDataException("Image data length doesn't match required length. act:" + source.length + " exp: " + (pl * 3));
int[] pix = new int[pl];
int off = 0;
for (int i = 0; i < pix.length; i++) {
int r = (source[off++] & 0xff), g = (source[off++] & 0xff), b = (source[off++] & 0xff);
pix[i] = (r << 16) | (g << 8) | b;
}
return new SimpleImage(pix, width, height);
}
public static SimpleImage fromImage(Image i) {
final BufferedImage bi;
if(i instanceof BufferedImage)
bi = (BufferedImage) i;
else
throw new RuntimeException("TODO");// Todo;
final int width = bi.getWidth();
final int height = bi.getHeight();
int[] pix = new int[width * height];
bi.getRGB(0, 0, width, height, pix, 0, width);
return new SimpleImage(pix, width, height);
}
public BufferedImage toImage() {
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
bi.setRGB(0, 0, width, height, pixels, 0, width);
return bi;
}
public boolean equals(Object o) {
return o == this || (o != null && o instanceof SimpleImage && equals((SimpleImage) o));
}
public int hashCode() {
// This may be slow (but it should be consistent). Todo: benchmark
MessageDigest md5 = DigestUtil.MD5;
for(int p: pixels) {
md5.update((byte) (p & 0xff));
md5.update((byte) ((p >> 8) & 0xff));
md5.update((byte) ((p >> 16) & 0xff));
}
byte[] hash = md5.digest();
String hex = DigestUtil.toString(hash);
return hex.hashCode();
}
public boolean equals(SimpleImage si) {
if(si.width != width || si.height != height)
return false;
for(int i = 0; i < pixels.length; i++)
if(pixels[i] != si.pixels[i])
return false;
return true;
}
public String toString() {
return String.format("SimpleImage %dx%d", width, height);
}
}