PortableImageTeaVM.java
package net.sourceforge.plantuml.klimt.awt;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
// ::comment when JAVA8
import org.teavm.jso.JSBody;
import org.teavm.jso.canvas.CanvasRenderingContext2D;
import org.teavm.jso.canvas.ImageData;
import org.teavm.jso.dom.html.HTMLCanvasElement;
import org.teavm.jso.dom.html.HTMLDocument;
import org.teavm.jso.typedarrays.Uint8ClampedArray;
// ::done
/**
* Portable image abstraction that works both in standard Java (using
* BufferedImage) and in TeaVM (using an in-memory pixel array with browser
* canvas for PNG export).
*/
class PortableImageTeaVM implements PortableImage {
// ::remove file when JAVA8
private final int width;
private final int height;
private final int imageType;
private final int[] pixels;
PortableImageTeaVM(int width, int height, int imageType) {
this.width = width;
this.height = height;
this.imageType = imageType;
this.pixels = new int[width * height];
// Initialize with default color based on type
if (imageType == TYPE_INT_RGB) {
// Default to white for RGB
java.util.Arrays.fill(pixels, 0xFFFFFFFF);
} else {
// Default to transparent for ARGB
java.util.Arrays.fill(pixels, 0x00000000);
}
}
/**
* Creates a PortableImage from an existing pixel array. The array is copied,
* not referenced.
*/
PortableImageTeaVM(int width, int height, int imageType, int[] sourcePixels) {
this.width = width;
this.height = height;
this.imageType = imageType;
this.pixels = new int[width * height];
System.arraycopy(sourcePixels, 0, this.pixels, 0, Math.min(sourcePixels.length, this.pixels.length));
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getRGB(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height)
return 0;
return pixels[y * width + x];
}
public void setRGB(int x, int y, int rgb) {
if (x < 0 || x >= width || y < 0 || y >= height)
return;
pixels[y * width + x] = rgb;
}
public int getType() {
return imageType;
}
public PortableImage getSubimage(int x, int y, int w, int h) {
// Clamp bounds
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (x + w > width)
w = width - x;
if (y + h > height)
h = height - y;
if (w <= 0 || h <= 0)
return new PortableImageTeaVM(1, 1, imageType);
int[] subPixels = new int[w * h];
for (int row = 0; row < h; row++) {
int srcOffset = (y + row) * width + x;
int dstOffset = row * w;
System.arraycopy(pixels, srcOffset, subPixels, dstOffset, w);
}
return new PortableImageTeaVM(w, h, imageType, subPixels);
}
public int getTransparency() {
// 1 = OPAQUE, 2 = BITMASK, 3 = TRANSLUCENT
return (imageType == TYPE_INT_RGB) ? 1 : 3;
}
/**
* Returns all pixels as an ARGB int array (row-major order). Returns a copy to
* prevent external modification.
*
* @return pixel array of size width * height
*/
public int[] getPixels() {
int[] copy = new int[pixels.length];
System.arraycopy(pixels, 0, copy, 0, pixels.length);
return copy;
}
/**
* Converts this image to a PNG data URL using browser's canvas API. Creates an
* offscreen canvas, writes pixels via ImageData, and exports as PNG.
*
* @return PNG data URL string (data:image/png;base64,...)
*/
public String toPngDataUrl() {
HTMLDocument doc = HTMLDocument.current();
HTMLCanvasElement canvas = (HTMLCanvasElement) doc.createElement("canvas");
canvas.setWidth(width);
canvas.setHeight(height);
CanvasRenderingContext2D ctx = (CanvasRenderingContext2D) canvas.getContext("2d");
ImageData imageData = ctx.createImageData(width, height);
Uint8ClampedArray data = imageData.getData();
// Convert ARGB (0xAARRGGBB) to RGBA bytes
int p = 0;
for (int i = 0; i < pixels.length; i++) {
int c = pixels[i];
int a = (c >>> 24) & 0xFF;
int r = (c >>> 16) & 0xFF;
int g = (c >>> 8) & 0xFF;
int b = c & 0xFF;
data.set(p++, r);
data.set(p++, g);
data.set(p++, b);
data.set(p++, a);
}
ctx.putImageData(imageData, 0, 0);
return canvasToDataUrl(canvas);
}
@JSBody(params = { "canvas" }, script = "return canvas.toDataURL('image/png');")
private static native String canvasToDataUrl(HTMLCanvasElement canvas);
/**
* Scales this image by the given factor using the specified interpolation type.
*
* @param scaleFactor the scale factor (e.g., 2.0 for double size)
* @param interpolationType 1=nearest-neighbor, 2=bilinear
* @return a new scaled PortableImage
*/
public PortableImage scale(double scaleFactor, int interpolationType) {
final int dstW = (int) Math.round(width * scaleFactor);
final int dstH = (int) Math.round(height * scaleFactor);
if (dstW <= 0 || dstH <= 0)
return new PortableImageTeaVM(1, 1, imageType);
final int[] dstPixels = new int[dstW * dstH];
if (interpolationType == 2)
scaleBilinear(pixels, width, height, dstPixels, dstW, dstH);
else
scaleNearestNeighbor(pixels, width, height, dstPixels, dstW, dstH);
return new PortableImageTeaVM(dstW, dstH, imageType, dstPixels);
}
/**
* Nearest-neighbor scaling - fast but can be blocky.
*/
private static void scaleNearestNeighbor(int[] src, int srcW, int srcH, int[] dst, int dstW, int dstH) {
for (int y = 0; y < dstH; y++) {
final int srcY = y * srcH / dstH;
for (int x = 0; x < dstW; x++) {
final int srcX = x * srcW / dstW;
dst[y * dstW + x] = src[srcY * srcW + srcX];
}
}
}
/**
* Bilinear interpolation scaling - smoother results.
*/
private static void scaleBilinear(int[] src, int srcW, int srcH, int[] dst, int dstW, int dstH) {
final double xRatio = (double) (srcW - 1) / dstW;
final double yRatio = (double) (srcH - 1) / dstH;
for (int y = 0; y < dstH; y++) {
final double srcYf = y * yRatio;
final int y0 = (int) srcYf;
final int y1 = Math.min(y0 + 1, srcH - 1);
final double yFrac = srcYf - y0;
for (int x = 0; x < dstW; x++) {
final double srcXf = x * xRatio;
final int x0 = (int) srcXf;
final int x1 = Math.min(x0 + 1, srcW - 1);
final double xFrac = srcXf - x0;
// Get 4 neighboring pixels
final int c00 = src[y0 * srcW + x0];
final int c10 = src[y0 * srcW + x1];
final int c01 = src[y1 * srcW + x0];
final int c11 = src[y1 * srcW + x1];
// Interpolate each channel
final int a = interpolateChannel(c00, c10, c01, c11, xFrac, yFrac, 24);
final int r = interpolateChannel(c00, c10, c01, c11, xFrac, yFrac, 16);
final int g = interpolateChannel(c00, c10, c01, c11, xFrac, yFrac, 8);
final int b = interpolateChannel(c00, c10, c01, c11, xFrac, yFrac, 0);
dst[y * dstW + x] = (a << 24) | (r << 16) | (g << 8) | b;
}
}
}
/**
* Bilinear interpolation for a single color channel.
*/
private static int interpolateChannel(int c00, int c10, int c01, int c11, double xFrac, double yFrac, int shift) {
final int v00 = (c00 >> shift) & 0xFF;
final int v10 = (c10 >> shift) & 0xFF;
final int v01 = (c01 >> shift) & 0xFF;
final int v11 = (c11 >> shift) & 0xFF;
final double top = v00 + xFrac * (v10 - v00);
final double bottom = v01 + xFrac * (v11 - v01);
final double result = top + yFrac * (bottom - top);
return Math.min(255, Math.max(0, (int) Math.round(result)));
}
@Override
public BufferedImage getBufferedImage() {
throw new UnsupportedOperationException("TEAVM9834");
}
@Override
public Graphics2D createGraphics() {
throw new UnsupportedOperationException("TEAVM9834");
}
@Override
public Graphics getGraphics() {
throw new UnsupportedOperationException("TEAVM9834");
}
}