PortableImage.java

package net.sourceforge.plantuml.klimt.awt;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;

// ::comment when __TEAVM__
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import javax.imageio.ImageIO;
// ::done

// ::uncomment when __TEAVM__
//import org.teavm.jso.JSBody;
//import org.teavm.jso.canvas.CanvasImageSource;
//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).
 */
public class PortableImage {

	// ::comment when __TEAVM__
	public static final int TYPE_INT_RGB = BufferedImage.TYPE_INT_RGB;
	public static final int TYPE_INT_ARGB = BufferedImage.TYPE_INT_ARGB;
	public static final int TYPE_INT_ARGB_PRE = BufferedImage.TYPE_INT_ARGB_PRE;

	private final BufferedImage image;

	public PortableImage(int width, int height, int imageType) {
		this.image = new BufferedImage(width, height, imageType);
	}

	public PortableImage(BufferedImage image) {
		this.image = image;
	}

	public int getWidth() {
		return image.getWidth();
	}

	public int getHeight() {
		return image.getHeight();
	}

	public int getRGB(int x, int y) {
		return image.getRGB(x, y);
	}

	public void setRGB(int x, int y, int rgb) {
		image.setRGB(x, y, rgb);
	}

	public int getType() {
		return image.getType();
	}

	public BufferedImage getBufferedImage() {
		return image;
	}

	public PortableImage getSubimage(int x, int y, int width, int height) {
		return new PortableImage(image.getSubimage(x, y, width, height));
	}

	public Graphics2D createGraphics() {
		return image.createGraphics();
	}

	public int getTransparency() {
		return image.getTransparency();
	}

	public Graphics getGraphics() {
		return image.getGraphics();
	}
	
	/**
	 * Converts this image to a PNG data URL (data:image/png;base64,...).
	 * In standard Java, uses ImageIO to encode the BufferedImage.
	 * 
	 * @return PNG data URL string
	 */
	public String toPngDataUrl() {
		try {
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ImageIO.write(image, "PNG", baos);
			byte[] bytes = baos.toByteArray();
			String base64 = Base64.getEncoder().encodeToString(bytes);
			return "data:image/png;base64," + base64;
		} catch (IOException e) {
			throw new RuntimeException("Failed to encode image as PNG", e);
		}
	}
	
	/**
	 * Returns all pixels as an ARGB int array (row-major order).
	 * 
	 * @return pixel array of size width * height
	 */
	public int[] getPixels() {
		int w = image.getWidth();
		int h = image.getHeight();
		return image.getRGB(0, 0, w, h, null, 0, w);
	}

	/**
	 * 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 AffineTransformOp interpolation type
	 * @return a new scaled PortableImage
	 */
	public PortableImage scale(double scaleFactor, int interpolationType) {
		final int w = (int) Math.round(image.getWidth() * scaleFactor);
		final int h = (int) Math.round(image.getHeight() * scaleFactor);
		final BufferedImage scaled = new BufferedImage(w, h, image.getType());
		final AffineTransform at = new AffineTransform();
		at.scale(scaleFactor, scaleFactor);
		final AffineTransformOp scaleOp = new AffineTransformOp(at, interpolationType);
		return new PortableImage(scaleOp.filter(image, scaled));
	}
	// ::done

	// ::uncomment when __TEAVM__
//	public static final int TYPE_INT_RGB = 1;
//	public static final int TYPE_INT_ARGB = 2;
//	public static final int TYPE_INT_ARGB_PRE = 3;
//
//	private final int width;
//	private final int height;
//	private final int imageType;
//	private final int[] pixels;
//
//	public PortableImage(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.
//	 */
//	public PortableImage(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 PortableImage(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 PortableImage(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 PortableImage(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 PortableImage(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)));
//	}
	// ::done

}