/*
GNU LESSER GENERAL PUBLIC LICENSE
Copyright (C) 2015 Uproot Labs India Pvt Ltd
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.lobobrowser.html.domimpl;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Stack;
import javax.imageio.ImageIO;
import org.lobobrowser.html.js.NotGetterSetter;
import org.lobobrowser.js.HideFromJS;
import org.lobobrowser.main.PlatformInit;
import org.lobobrowser.util.gui.ColorFactory;
import org.mozilla.javascript.typedarrays.NativeUint8ClampedArray;
import org.w3c.dom.html.HTMLElement;
public class HTMLCanvasElementImpl extends HTMLAbstractUIElement implements HTMLElement {
public HTMLCanvasElementImpl() {
super("CANVAS");
// The default width and height are defined by the spec to 300 x 150
setBounds(0, 0, 300, 150);
}
public String toDataURL() {
return toDataURL("image/png", 1);
}
public String toDataURL(final String type, final double encoderOptions) {
String format = "png";
if ("image/png".equals(type)) {
format = "png";
} else if ("image/gif".equals(type)) {
format = "gif";
} else if ("image/jpeg".equals(type)) {
format = "jpg";
}
if (computedWidth == 0 || computedHeight == 0) {
return "data:,";
}
try {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, format, outputStream);
final String outputStr = Base64.getEncoder().encodeToString(outputStream.toByteArray());
return "data:" + type + ";base64," + outputStr;
} catch (final IOException e) {
e.printStackTrace();
throw new RuntimeException("Unexpected exception while encoding canvas to data-url");
}
}
public int getHeight() {
return computedHeight;
}
private int computedWidth = 0;
private int computedHeight = 0;
public void setHeight(final double height) {
computedHeight = ((int) height);
this.setAttribute("height", "" + computedHeight);
refreshImageDimension();
}
public int getWidth() {
return computedWidth;
}
public void setWidth(final double width) {
computedWidth = ((int) width);
this.setAttribute("width", "" + computedWidth);
refreshImageDimension();
}
private BufferedImage image = null;
private int offsetX = 0;
private int offsetY = 0;
@HideFromJS
public void paintComponent(final Graphics g) {
if (image != null) {
// Draw a grid if debugging
if (PlatformInit.getInstance().debugOn) {
final Graphics newG = g.create(offsetX, offsetY, computedWidth, computedHeight);
try {
drawGrid(newG);
} finally {
newG.dispose();
}
}
g.drawImage(image, offsetX, offsetY, null);
}
}
@HideFromJS
public void setBounds(final int x, final int y, final int width, final int height) {
offsetX = x;
offsetY = y;
computedWidth = width;
computedHeight = height;
refreshImageDimension();
}
private void refreshImageDimension() {
if (image == null) {
createNewImage(computedWidth, computedHeight);
} else if (image.getWidth(null) != computedWidth || image.getHeight(null) != computedHeight) {
createNewImage(computedWidth, computedHeight);
}
}
private void createNewImage(final int width, final int height) {
if (width != 0 && height != 0) {
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
canvasContext.invalidate();
} else {
// TODO: Need to handle the case when width or height is zero. Buffered image doesn't accept zero width / height.
}
}
private void repaint() {
getUINode().repaint(HTMLCanvasElementImpl.this);
}
private static final Color gridColor = new Color(30, 30, 30, 30);
private static final int GRID_SIZE = 10;
private void drawGrid(final Graphics g) {
final Graphics2D g2 = (Graphics2D) g;
final int height = image.getHeight(null);
final int width = image.getWidth(null);
g2.setColor(gridColor);
for (int i = 0; i < height; i += GRID_SIZE) {
g2.drawLine(0, i, width, i);
}
for (int i = 0; i < width; i += GRID_SIZE) {
g2.drawLine(i, 0, i, height);
}
}
final public class CanvasContext {
public void fillRect(final int x, final int y, final int width, final int height) {
final Graphics2D g2 = getGraphics();
g2.setPaint(currDrawingState.paintFill);
g2.fillRect(x, y, width, height);
repaint();
}
public void clearRect(final int x, final int y, final int width, final int height) {
final Graphics2D g2 = getGraphics();
g2.clearRect(x, y, width, height);
repaint();
}
private AffineTransform getCurrentTransformMatrix() {
final Graphics2D g2 = getGraphics();
return g2.getTransform();
}
private Shape getCurrClip() {
final Graphics2D g2 = getGraphics();
return g2.getClip();
}
public void scale(final double x, final double y) {
final Graphics2D g2 = getGraphics();
g2.scale(x, y);
}
public void rotate(final double angle) {
final Graphics2D g2 = getGraphics();
g2.rotate(angle);
}
public void translate(final double x, final double y) {
final Graphics2D g2 = getGraphics();
g2.translate(x, y);
}
public void transform(final double a, final double b, final double c, final double d, final double e, final double f) {
final Graphics2D g2 = getGraphics();
final AffineTransform tx = new AffineTransform(a, b, c, d, e, f);
g2.transform(tx);
}
public void setTransform(final double a, final double b, final double c, final double d, final double e, final double f) {
final Graphics2D g2 = getGraphics();
final AffineTransform tx = new AffineTransform(a, b, c, d, e, f);
g2.setTransform(tx);
}
public void resetTransform() {
final Graphics2D g2 = getGraphics();
g2.setTransform(new AffineTransform());
}
private CanvasPath2D cpath2D = new CanvasPath2D();
public void beginPath() {
cpath2D = new CanvasPath2D();
}
public void closePath() {
cpath2D.closePath();
}
public void moveTo(final double x, final double y) {
cpath2D.moveToWithTransform(x, y, getCurrentTransformMatrix());
}
public void lineTo(final int x, final int y) {
cpath2D.lineToWithTransform(x, y, getCurrentTransformMatrix());
}
public void quadraticCurveTo(final double x1, final double y1, final double x2, final double y2) {
cpath2D.quadraticCurveToWithTransform(x1, y1, x2, y2, getCurrentTransformMatrix());
}
public void bezierCurveTo(final double x1, final double y1, final double x2, final double y2, final double x3, final double y3) {
cpath2D.bezierCurveToWithTransform(x1, y1, x2, y2, x3, y3, getCurrentTransformMatrix());
}
public void arc(final int x, final int y, final int radius, final double startAngle, final double endAngle) {
arc(x, y, radius, startAngle, endAngle, false);
}
public void arc(final int x, final int y, final int radius, final double startAngle, final double endAngle, final boolean antiClockwise) {
cpath2D.arcWithTransform(x, y, radius, startAngle, endAngle, antiClockwise, getCurrentTransformMatrix());
}
public void arcTo(final double x1, final double y1, final double x2, final double y2, final double radius) {
cpath2D.arcToWithTransform(x1, y1, x2, y2, radius, getCurrentTransformMatrix());
}
public void ellipse(final double x, final double y, final double radiusX, final double radiusY, final double rotation,
final double startAngle, final double endAngle, final boolean antiClockwise) {
cpath2D.ellipseWithTransform(x, y, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise, getCurrentTransformMatrix());
}
public void ellipse(final double x, final double y, final double radiusX, final double radiusY, final double rotation,
final double startAngle, final double endAngle) {
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, false);
}
public void rect(final double x, final double y, final double width, final double height) {
cpath2D.rectWithTransform(x, y, width, height, getCurrentTransformMatrix());
}
public void strokeRect(final double x, final double y, final double w, final double h) {
final Graphics2D g2 = getGraphics();
g2.setPaint(currDrawingState.paintStroke);
g2.draw(new Rectangle2D.Double(x, y, w, h));
}
public void stroke() {
final Graphics2D g2 = getGraphics();
final AffineTransform currAFT = g2.getTransform();
resetTransform();
stroke(cpath2D);
g2.setTransform(currAFT);
}
public void stroke(final CanvasPath2D cpath2D) {
final Graphics2D g2 = getGraphics();
g2.setPaint(currDrawingState.paintStroke);
g2.draw(cpath2D.path2D);
repaint();
}
public void fill() {
final Graphics2D g2 = getGraphics();
final AffineTransform currAFT = g2.getTransform();
resetTransform();
fill(cpath2D);
g2.setTransform(currAFT);
}
public void fill(final CanvasPath2D cpath2D) {
final Graphics2D g2 = getGraphics();
g2.setPaint(currDrawingState.paintFill);
g2.fill(cpath2D.path2D);
repaint();
}
public void clip() {
clip(cpath2D);
}
public void clip(final CanvasPath2D cpath2D) {
final Graphics2D g2 = getGraphics();
g2.clip(cpath2D.path2D);
}
public void resetClip() {
final Graphics2D g2 = getGraphics();
g2.setClip(null);
}
// TODO: Check if polymorphism can be handled in JavaObjectWrapper
public void setFillStyle(final Object style) {
if (style instanceof String) {
currDrawingState.paintFill = parseColor((String) style);
} else if (style instanceof CanvasGradient) {
currDrawingState.paintFill = ((CanvasGradient) style).toPaint();
} else {
throw new UnsupportedOperationException("Fill style not recognized");
}
}
private String toHex(final int r, final int g, final int b) {
return "#" + toBrowserHexValue(r) + toBrowserHexValue(g) + toBrowserHexValue(b);
}
private String toBrowserHexValue(final int number) {
final StringBuilder builder = new StringBuilder(Integer.toHexString(number & 0xff));
while (builder.length() < 2) {
builder.append("0");
}
return builder.toString().toLowerCase();
}
public Object getFillStyle() {
return formatStyle(currDrawingState.paintFill);
}
private Object formatStyle(final Paint paint) {
if (paint instanceof Color) {
final Color color = (Color) paint;
if (color.getAlpha() == 1) {
return toHex(color.getRed(), color.getGreen(), color.getBlue());
} else {
System.out.println("Alpha: " + color.getAlpha());
return "rgba(" + color.getRed() + ", " + color.getGreen() + ", " + color.getBlue() + ", " + (color.getAlpha()/255.0) + ")";
}
}
// TODO: Handle canvas pattern and canvas gradient
return null;
}
// TODO: Check if polymorphism can be handled in JavaObjectWrapper
public void setStrokeStyle(final Object style) {
if (style instanceof String) {
currDrawingState.paintStroke = parseColor((String) style);
} else if (style instanceof CanvasGradient) {
currDrawingState.paintStroke = ((CanvasGradient) style).toPaint();
} else {
throw new UnsupportedOperationException("Stroke style not recognized");
}
}
public Object getStrokeStyle() {
return formatStyle(currDrawingState.paintStroke);
}
private int rule = AlphaComposite.SRC_OVER;
public void setGlobalAlpha(final double alpha) {
final Graphics2D g2 = getGraphics();
currDrawingState.globalAlpha = (float) alpha;
final AlphaComposite a = AlphaComposite.getInstance(rule, currDrawingState.globalAlpha);
g2.setComposite(a);
}
public float getGlobalAlpha() {
return currDrawingState.globalAlpha;
}
private class CanvasState implements Cloneable {
private AffineTransform currTransformMatrix;
private Shape currClippingRegion;
private Paint paintFill = Color.BLACK;
private Paint paintStroke = Color.BLACK;
private float lineWidth = 1;
private int lineCap = BasicStroke.CAP_BUTT;
private int lineJoin = BasicStroke.JOIN_MITER;
private float miterLimit = 10;
private float[] lineDash = null;
private float lineDashOffset = 0;
private float globalAlpha = 1;
private String globalCompositeOperation = "source-over";
CanvasState() {
currTransformMatrix = null;
currClippingRegion = null;
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public void setGlobalCompositeOperation(final String composition) {
final Graphics2D g2 = getGraphics();
currDrawingState.globalCompositeOperation = composition;
if ("source-atop".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.SRC_ATOP;
} else if ("source-in".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.SRC_IN;
} else if ("source-out".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.SRC_OUT;
} else if ("source-over".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.SRC_OVER;
} else if ("destination-atop".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.DST_ATOP;
} else if ("destination-in".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.DST_IN;
} else if ("destination-out".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.DST_OUT;
} else if ("destination-over".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.DST_OVER;
} else if ("xor".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.XOR;
} else if ("clear".equals(currDrawingState.globalCompositeOperation)) {
rule = AlphaComposite.CLEAR;
}
final AlphaComposite a = AlphaComposite.getInstance(rule, currDrawingState.globalAlpha);
g2.setComposite(a);
}
public String getGlobalCompositeOperation() {
return currDrawingState.globalCompositeOperation;
}
public void setLineWidth(final double width) {
currDrawingState.lineWidth = (float) width;
setStroke();
}
public double getLineWidth() {
return currDrawingState.lineWidth;
}
public void setLineCap(final String cap) {
if ("butt".equals(cap)) {
currDrawingState.lineCap = BasicStroke.CAP_BUTT;
} else if ("round".equals(cap)) {
currDrawingState.lineCap = BasicStroke.CAP_ROUND;
} else if ("square".equals(cap)) {
currDrawingState.lineCap = BasicStroke.CAP_SQUARE;
}
setStroke();
}
public String getLineCap() {
if (currDrawingState.lineCap == BasicStroke.CAP_BUTT) {
return "butt";
} else if (currDrawingState.lineCap == BasicStroke.CAP_ROUND) {
return "round";
} else if (currDrawingState.lineCap == BasicStroke.CAP_SQUARE) {
return "square";
}
return null;
}
public void setLineJoin(final String join) {
if ("round".equals(join)) {
currDrawingState.lineJoin = BasicStroke.JOIN_ROUND;
} else if ("bevel".equals(join)) {
currDrawingState.lineJoin = BasicStroke.JOIN_BEVEL;
} else if ("miter".equals(join)) {
currDrawingState.lineJoin = BasicStroke.JOIN_MITER;
}
setStroke();
}
public String getLineJoin() {
if (currDrawingState.lineJoin == BasicStroke.JOIN_MITER) {
return "miter";
} else if (currDrawingState.lineCap == BasicStroke.JOIN_BEVEL) {
return "bevel";
} else if (currDrawingState.lineCap == BasicStroke.JOIN_ROUND) {
return "round";
}
return null;
}
public void setMiterLimit(final double miterLimit) {
currDrawingState.miterLimit = (float) miterLimit;
setStroke();
}
public float getMiterLimit() {
return currDrawingState.miterLimit;
}
@NotGetterSetter
public void setLineDash(final double[] segments) {
currDrawingState.lineDash = new float[segments.length];
for (int i = 0; i < segments.length; i++) {
currDrawingState.lineDash[i] = (float) segments[i];
}
setStroke();
}
@NotGetterSetter
public double[] getLineDash() {
final double[] lineDash1 = new double[currDrawingState.lineDash.length];
for (int i = 0; i < currDrawingState.lineDash.length; i++) {
lineDash1[i] = currDrawingState.lineDash[i];
}
return lineDash1;
}
public void setLineDashOffset(final double lineDashOffset) {
currDrawingState.lineDashOffset = (float) lineDashOffset;
setStroke();
}
public double getLineDashOffset() {
return currDrawingState.lineDashOffset;
}
private void setStroke() {
final Graphics2D g2 = getGraphics();
g2.setStroke(new BasicStroke(currDrawingState.lineWidth, currDrawingState.lineCap, currDrawingState.lineJoin,
currDrawingState.miterLimit, currDrawingState.lineDash, currDrawingState.lineDashOffset));
}
public ImageData createImageData(final int width, final int height) {
final NativeUint8ClampedArray data = new NativeUint8ClampedArray(width * height * 4);
return new ImageData(width, height, data);
}
public ImageData createImageData(final ImageData imgdata) {
final int width = imgdata.getWidth();
final int height = imgdata.getHeight();
final NativeUint8ClampedArray data = new NativeUint8ClampedArray(width * height * 4);
return new ImageData(width, height, data);
}
public ImageData getImageData(final int x, final int y, final int width, final int height) {
final int[] argbArray = new int[width * height];
image.getRGB(x, y, width, height, argbArray, 0, width);
final NativeUint8ClampedArray clampedBuffer = new NativeUint8ClampedArray(width * height * 4);
final byte[] clampedByteBuffer = clampedBuffer.getBuffer().getBuffer();
for (int i = 0, j = 0; i < argbArray.length; i++, j += 4) {
final int argb = argbArray[i];
clampedByteBuffer[j ] = (byte) ((argb >> 16) & 0xff);
clampedByteBuffer[j + 1] = (byte) ((argb >> 8) & 0xff);
clampedByteBuffer[j + 2] = (byte) ((argb ) & 0xff);
clampedByteBuffer[j + 3] = (byte) ((argb >> 24) & 0xff);
}
return new ImageData(width, height, clampedBuffer);
}
public void putImageData(final ImageData imgData, final int x, final int y) {
putImageData(imgData, x, y, imgData.width, imgData.height);
}
public void putImageData(final ImageData imgData, final int x, final int y, final int width, final int height) {
System.out.println("putImageData(imgData, x, y, width, height)" + java.util.Arrays.toString(new Object[] { x, y, width, height }));
if (x >= 0 && y >= 0) {
final byte[] dataBytes = imgData.getData().getBuffer().getBuffer();
final int[] argbArray = new int[imgData.width * imgData.height];
for (int i = 0, j = 0; i < argbArray.length; i++, j += 4) {
argbArray[i] = packBytes2Int(
dataBytes[j + 3], dataBytes[j ],
dataBytes[j + 1], dataBytes[j + 2]);
}
image.setRGB(x, y, Math.min(width, imgData.width), Math.min(height, imgData.height), argbArray, 0, imgData.width);
repaint();
}
}
private Graphics2D cachedGraphics = null;
@HideFromJS
public synchronized void invalidate() {
cachedGraphics = null;
}
private synchronized Graphics2D getGraphics() {
if (cachedGraphics == null) {
cachedGraphics = (Graphics2D) image.getGraphics();
cachedGraphics.setBackground(new Color(0, 0, 0, 0));
cachedGraphics.setPaint(Color.BLACK);
cachedGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
return cachedGraphics;
}
public CanvasGradient createLinearGradient(final float x0, final float y0, final float x1, final float y1) {
final LinearCanvasGradient linearGradient = new LinearCanvasGradient(x0, y0, x1, y1);
return linearGradient;
}
private final Stack<CanvasState> drawingStateStack = new Stack<>();
private CanvasState currDrawingState = new CanvasState();
public void save() {
try {
final CanvasState cloneDrawingState = (CanvasState) currDrawingState.clone();
cloneDrawingState.currTransformMatrix = this.getCurrentTransformMatrix();
cloneDrawingState.currClippingRegion = this.getCurrClip();
drawingStateStack.push(cloneDrawingState);
} catch (final CloneNotSupportedException e) {
e.printStackTrace();
throw new IllegalStateException(e);
}
}
public void restore() {
if (drawingStateStack.empty()) {
// Do nothing
} else {
currDrawingState = drawingStateStack.pop();
this.setGlobalAlpha(currDrawingState.globalAlpha);
this.setGlobalCompositeOperation(currDrawingState.globalCompositeOperation);
this.setStroke();
getGraphics().setTransform(currDrawingState.currTransformMatrix);
getGraphics().setClip(currDrawingState.currClippingRegion);
}
}
public void fillText(final String s, final double x, final double y) {
final char[] chars = s.toCharArray();
final Graphics2D g2 = getGraphics();
g2.setPaint(currDrawingState.paintFill);
g2.drawChars(chars, 0, chars.length, (int) x, (int) y);
}
}
public abstract class CanvasGradient {
final protected ArrayList<Float> offsets = new ArrayList<>();
final protected ArrayList<Color> colors = new ArrayList<>();
public void addColorStop(final float offset, final String color) {
this.offsets.add(offset);
this.colors.add(parseColor(color));
}
public abstract Paint toPaint();
}
public class LinearCanvasGradient extends CanvasGradient {
private final float x0;
private final float y0;
private final float x1;
private final float y1;
LinearCanvasGradient(final float x0, final float y0, final float x1, final float y1) {
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
}
public Paint toPaint() {
if (colors.size() == 0) {
return new Color(0, 0, 0, 0);
} else if (colors.size() == 1) {
return colors.get(0);
} else {
// TODO: See if this can be optimized
final float[] offsetsArray = new float[offsets.size()];
for (int i = 0; i < offsets.size(); i++) {
offsetsArray[i] = offsets.get(i);
}
return new LinearGradientPaint(x0, y0, x1, y1, offsetsArray, colors.toArray(new Color[colors.size()]));
}
}
}
private static int packBytes2Int(final byte a, final byte b, final byte c, final byte d) {
return (a << 24) | ((b & 0xff) << 16) | ((c & 0xff) << 8) | (d & 0xff);
}
public static final class ImageData {
final private int width;
final private int height;
final private NativeUint8ClampedArray dataInternal;
public ImageData(final int width, final int height, final NativeUint8ClampedArray data) {
this.width = width;
this.height = height;
this.dataInternal = data;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public NativeUint8ClampedArray getData() {
return dataInternal;
}
}
final private CanvasContext canvasContext = new CanvasContext();
public CanvasContext getContext(final String type) {
return canvasContext;
}
private static Color parseColor(final String color) {
return ColorFactory.getInstance().getColor(color);
}
}