changeset 11041:f4f1e91dbf87

8209457: [WebView] Canvas.toDataURL with image/jpeg MIME type fails Reviewed-by: kcr, psadhukhan
author arajkumar
date Mon, 27 Aug 2018 11:42:34 +0530
parents 2574c5120437
children 671146b1881a
files modules/javafx.web/src/main/java/com/sun/javafx/webkit/PasteboardImpl.java modules/javafx.web/src/main/java/com/sun/javafx/webkit/UIClientImpl.java modules/javafx.web/src/main/java/com/sun/javafx/webkit/prism/PrismImage.java modules/javafx.web/src/main/java/com/sun/webkit/graphics/WCImage.java modules/javafx.web/src/test/java/test/javafx/scene/web/CanvasTest.java
diffstat 5 files changed, 235 insertions(+), 241 deletions(-) [+]
line wrap: on
line diff
--- a/modules/javafx.web/src/main/java/com/sun/javafx/webkit/PasteboardImpl.java	Fri Aug 24 15:07:12 2018 -0700
+++ b/modules/javafx.web/src/main/java/com/sun/javafx/webkit/PasteboardImpl.java	Mon Aug 27 11:42:34 2018 +0530
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -26,9 +26,8 @@
 package com.sun.javafx.webkit;
 
 import com.sun.javafx.tk.Toolkit;
-import com.sun.javafx.webkit.UIClientImpl;
 import com.sun.webkit.Pasteboard;
-import com.sun.webkit.graphics.WCGraphicsManager;
+import com.sun.webkit.graphics.WCImage;
 import com.sun.webkit.graphics.WCImageFrame;
 import java.io.File;
 import java.io.IOException;
@@ -66,20 +65,17 @@
         clipboard.setContent(content);
     }
 
-    @Override public void writeImage(WCImageFrame wcImage) {
-        Object platformImage = WCGraphicsManager.getGraphicsManager().
-                toPlatformImage(wcImage.getFrame());
-        Image fxImage = Toolkit.getImageAccessor().fromPlatformImage(platformImage);
+    @Override public void writeImage(WCImageFrame frame) {
+        final WCImage img = frame.getFrame();
+        final Image fxImage = img != null && !img.isNull() ? Toolkit.getImageAccessor().fromPlatformImage(img.getPlatformImage()) : null;
         if (fxImage != null) {
             ClipboardContent content = new ClipboardContent();
             content.putImage(fxImage);
-            String fileExtension = wcImage.getFrame().getFileExtension();
+            String fileExtension = img.getFileExtension();
             try {
                 File imageDump = File.createTempFile("jfx", "." + fileExtension);
                 imageDump.deleteOnExit();
-                ImageIO.write(UIClientImpl.toBufferedImage(fxImage),
-                    fileExtension,
-                    imageDump);
+                ImageIO.write(img.toBufferedImage(), fileExtension, imageDump);
                 content.putFiles(Arrays.asList(imageDump));
             } catch (IOException | SecurityException e) {
                 // Nothing specific to be done as of now
--- a/modules/javafx.web/src/main/java/com/sun/javafx/webkit/UIClientImpl.java	Fri Aug 24 15:07:12 2018 -0700
+++ b/modules/javafx.web/src/main/java/com/sun/javafx/webkit/UIClientImpl.java	Mon Aug 27 11:42:34 2018 +0530
@@ -33,22 +33,14 @@
 import static javafx.scene.web.WebEvent.STATUS_CHANGED;
 import static javafx.scene.web.WebEvent.VISIBILITY_CHANGED;
 
-import com.sun.javafx.tk.Toolkit;
 import com.sun.webkit.UIClient;
 import com.sun.webkit.WebPage;
 import com.sun.webkit.graphics.WCImage;
 import com.sun.webkit.graphics.WCRectangle;
-import java.awt.AlphaComposite;
-import java.awt.Graphics2D;
-import java.awt.image.BufferedImage;
-import java.awt.image.DataBufferInt;
-import java.awt.image.SampleModel;
-import java.awt.image.SinglePixelPackedSampleModel;
 import java.io.File;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.nio.ByteBuffer;
-import java.nio.IntBuffer;
 import java.security.AccessControlContext;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
@@ -58,10 +50,6 @@
 import java.util.Map;
 import javafx.event.EventHandler;
 import javafx.geometry.Rectangle2D;
-import javafx.scene.image.Image;
-import javafx.scene.image.PixelFormat;
-import javafx.scene.image.PixelReader;
-import javafx.scene.image.WritablePixelFormat;
 import javafx.scene.input.ClipboardContent;
 import javafx.scene.input.DataFormat;
 import javafx.scene.input.Dragboard;
@@ -342,7 +330,7 @@
                 //never happens
             }
         }
-        if (image != null) {
+        if (image != null && !image.isNull()) {
             ByteBuffer dragImageOffset = ByteBuffer.allocate(8);
             dragImageOffset.rewind();
             dragImageOffset.putInt(imageOffsetX);
@@ -364,26 +352,18 @@
             //QuantumClipboard.putContent have to be rewritten in Glass manner
             //with postponed data requests (DelayedCallback data object).
             if (isImageSource) {
-                Object platformImage = image.getWidth() > 0 && image.getHeight() > 0 ?
-                        image.getPlatformImage() : null;
                 String fileExtension = image.getFileExtension();
-                if (platformImage != null) {
-                    try {
-                        File temp = File.createTempFile("jfx", "." + fileExtension);
-                        temp.deleteOnExit();
-                        ImageIO.write(
-                            toBufferedImage(Toolkit.getImageAccessor().fromPlatformImage(
-                                Toolkit.getToolkit().loadPlatformImage(
-                                    platformImage
-                                )
-                            )),
-                            fileExtension,
-                            temp);
-                        content.put(DataFormat.FILES, Arrays.asList(temp));
-                    } catch (IOException | SecurityException e) {
-                        //That is ok. It was just an attempt.
-                        //e.printStackTrace();
-                    }
+                try {
+                    File temp = File.createTempFile("jfx", "." + fileExtension);
+                    temp.deleteOnExit();
+                    ImageIO.write(
+                        image.toBufferedImage(),
+                        fileExtension,
+                        temp);
+                    content.put(DataFormat.FILES, Arrays.asList(temp));
+                } catch (IOException | SecurityException e) {
+                    //That is ok. It was just an attempt.
+                    //e.printStackTrace();
                 }
             }
         }
@@ -403,143 +383,4 @@
         return accessor.getView() != null && content != null;
     }
 
-    private static int
-            getBestBufferedImageType(PixelFormat<?> fxFormat, BufferedImage bimg,
-                                     boolean isOpaque)
-    {
-        if (bimg != null) {
-            int bimgType = bimg.getType();
-            if (bimgType == BufferedImage.TYPE_INT_ARGB ||
-                bimgType == BufferedImage.TYPE_INT_ARGB_PRE ||
-                (isOpaque &&
-                     (bimgType == BufferedImage.TYPE_INT_BGR ||
-                      bimgType == BufferedImage.TYPE_INT_RGB)))
-            {
-                // We will allow the caller to give us a BufferedImage
-                // that has an alpha channel, but we might not otherwise
-                // construct one ourselves.
-                // We will also allow them to choose their own premultiply
-                // type which may not match the image.
-                // If left to our own devices we might choose a more specific
-                // format as indicated by the choices below.
-                return bimgType;
-            }
-        }
-        switch (fxFormat.getType()) {
-            default:
-            case BYTE_BGRA_PRE:
-            case INT_ARGB_PRE:
-                return BufferedImage.TYPE_INT_ARGB_PRE;
-            case BYTE_BGRA:
-            case INT_ARGB:
-                return BufferedImage.TYPE_INT_ARGB;
-            case BYTE_RGB:
-                return BufferedImage.TYPE_INT_RGB;
-            case BYTE_INDEXED:
-                return (fxFormat.isPremultiplied()
-                        ? BufferedImage.TYPE_INT_ARGB_PRE
-                        : BufferedImage.TYPE_INT_ARGB);
-        }
-    }
-
-    private static WritablePixelFormat<IntBuffer>
-        getAssociatedPixelFormat(BufferedImage bimg)
-    {
-        switch (bimg.getType()) {
-            // We lie here for xRGB, but we vetted that the src data was opaque
-            // so we can ignore the alpha.  We use ArgbPre instead of Argb
-            // just to get a loop that does not have divides in it if the
-            // PixelReader happens to not know the data is opaque.
-            case BufferedImage.TYPE_INT_RGB:
-            case BufferedImage.TYPE_INT_ARGB_PRE:
-                return PixelFormat.getIntArgbPreInstance();
-            case BufferedImage.TYPE_INT_ARGB:
-                return PixelFormat.getIntArgbInstance();
-            default:
-                // Should not happen...
-                throw new InternalError("Failed to validate BufferedImage type");
-        }
-    }
-
-    private static boolean checkFXImageOpaque(PixelReader pr, int iw, int ih) {
-        for (int x = 0; x < iw; x++) {
-            for (int y = 0; y < ih; y++) {
-                Color color = pr.getColor(x,y);
-                if (color.getOpacity() != 1.0) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
-    private static BufferedImage fromFXImage(Image img, BufferedImage bimg) {
-        PixelReader pr = img.getPixelReader();
-        if (pr == null) {
-            return null;
-        }
-        int iw = (int) img.getWidth();
-        int ih = (int) img.getHeight();
-        PixelFormat<?> fxFormat = pr.getPixelFormat();
-        boolean srcPixelsAreOpaque = false;
-        switch (fxFormat.getType()) {
-            case INT_ARGB_PRE:
-            case INT_ARGB:
-            case BYTE_BGRA_PRE:
-            case BYTE_BGRA:
-                // Check fx image opacity only if
-                // supplied BufferedImage is without alpha channel
-                if (bimg != null &&
-                        (bimg.getType() == BufferedImage.TYPE_INT_BGR ||
-                         bimg.getType() == BufferedImage.TYPE_INT_RGB)) {
-                    srcPixelsAreOpaque = checkFXImageOpaque(pr, iw, ih);
-                }
-                break;
-            case BYTE_RGB:
-                srcPixelsAreOpaque = true;
-                break;
-        }
-        int prefBimgType = getBestBufferedImageType(pr.getPixelFormat(), bimg, srcPixelsAreOpaque);
-        if (bimg != null) {
-            int bw = bimg.getWidth();
-            int bh = bimg.getHeight();
-            if (bw < iw || bh < ih || bimg.getType() != prefBimgType) {
-                bimg = null;
-            } else if (iw < bw || ih < bh) {
-                Graphics2D g2d = bimg.createGraphics();
-                g2d.setComposite(AlphaComposite.Clear);
-                g2d.fillRect(0, 0, bw, bh);
-                g2d.dispose();
-            }
-        }
-        if (bimg == null) {
-            bimg = new BufferedImage(iw, ih, prefBimgType);
-        }
-        DataBufferInt db = (DataBufferInt)bimg.getRaster().getDataBuffer();
-        int data[] = db.getData();
-        int offset = bimg.getRaster().getDataBuffer().getOffset();
-        int scan =  0;
-        SampleModel sm = bimg.getRaster().getSampleModel();
-        if (sm instanceof SinglePixelPackedSampleModel) {
-            scan = ((SinglePixelPackedSampleModel)sm).getScanlineStride();
-        }
-
-        WritablePixelFormat<IntBuffer> pf = getAssociatedPixelFormat(bimg);
-        pr.getPixels(0, 0, iw, ih, pf, data, offset, scan);
-        return bimg;
-    }
-
-    // Method to implement the following via reflection:
-    //     SwingFXUtils.fromFXImage(img, null)
-    public static BufferedImage toBufferedImage(Image img) {
-        try {
-            return fromFXImage(img, null);
-        } catch (Exception ex) {
-            ex.printStackTrace(System.err);
-        }
-
-        // return null upon any exception
-        return null;
-    }
-
 }
--- a/modules/javafx.web/src/main/java/com/sun/javafx/webkit/prism/PrismImage.java	Fri Aug 24 15:07:12 2018 -0700
+++ b/modules/javafx.web/src/main/java/com/sun/javafx/webkit/prism/PrismImage.java	Mon Aug 27 11:42:34 2018 +0530
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,18 +25,23 @@
 
 package com.sun.javafx.webkit.prism;
 
-import com.sun.javafx.tk.Toolkit;
 import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.SampleModel;
+import java.awt.image.SinglePixelPackedSampleModel;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.nio.IntBuffer;
 import java.util.Base64;
 import java.util.Iterator;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.WritablePixelFormat;
 import javax.imageio.ImageIO;
 import javax.imageio.ImageWriter;
 
 import com.sun.javafx.webkit.UIClientImpl;
+import com.sun.prism.Graphics;
 import com.sun.prism.Image;
-import com.sun.prism.Graphics;
 import com.sun.webkit.graphics.WCImage;
 
 /**
@@ -65,8 +70,8 @@
 
     @Override
     protected final byte[] toData(String mimeType) {
-        Object image = UIClientImpl.toBufferedImage(Toolkit.getImageAccessor().fromPlatformImage(getImage()));
-        if (image instanceof BufferedImage) {
+        final BufferedImage image = toBufferedImage(mimeType.equals("image/jpeg"));
+        if (image != null) {
             Iterator<ImageWriter> it = ImageIO.getImageWritersByMIMEType(mimeType);
             while (it.hasNext()) {
                 ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -98,4 +103,79 @@
         }
         return null;
     }
+
+    private static int
+            getBestBufferedImageType(PixelFormat<?> fxFormat)
+    {
+        switch (fxFormat.getType()) {
+            default:
+            case BYTE_BGRA_PRE:
+            case INT_ARGB_PRE:
+                return BufferedImage.TYPE_INT_ARGB_PRE;
+            case BYTE_BGRA:
+            case INT_ARGB:
+                return BufferedImage.TYPE_INT_ARGB;
+            case BYTE_RGB:
+                return BufferedImage.TYPE_INT_RGB;
+            case BYTE_INDEXED:
+                return (fxFormat.isPremultiplied()
+                        ? BufferedImage.TYPE_INT_ARGB_PRE
+                        : BufferedImage.TYPE_INT_ARGB);
+        }
+    }
+
+    private static WritablePixelFormat<IntBuffer>
+        getAssociatedPixelFormat(BufferedImage bimg)
+    {
+        switch (bimg.getType()) {
+            // We lie here for xRGB, but we vetted that the src data was opaque
+            // so we can ignore the alpha.  We use ArgbPre instead of Argb
+            // just to get a loop that does not have divides in it if the
+            // PixelReader happens to not know the data is opaque.
+            case BufferedImage.TYPE_INT_RGB:
+            case BufferedImage.TYPE_INT_ARGB_PRE:
+                return PixelFormat.getIntArgbPreInstance();
+            case BufferedImage.TYPE_INT_ARGB:
+                return PixelFormat.getIntArgbInstance();
+            default:
+                // Should not happen...
+                throw new InternalError("Failed to validate BufferedImage type");
+        }
+    }
+
+    private static BufferedImage fromFXImage(Image img, boolean forceRGB) {
+        final int iw = (int) img.getWidth();
+        final int ih = (int) img.getHeight();
+        final int destImageType = forceRGB ? BufferedImage.TYPE_INT_RGB : getBestBufferedImageType(img.getPlatformPixelFormat());
+        final BufferedImage bimg = new BufferedImage(iw, ih, destImageType);
+        final DataBufferInt db = (DataBufferInt) bimg.getRaster().getDataBuffer();
+        final int data[] = db.getData();
+        final int offset = bimg.getRaster().getDataBuffer().getOffset();
+        int scan =  0;
+        final SampleModel sm = bimg.getRaster().getSampleModel();
+        if (sm instanceof SinglePixelPackedSampleModel) {
+            scan = ((SinglePixelPackedSampleModel)sm).getScanlineStride();
+        }
+
+        final WritablePixelFormat<IntBuffer> pf = getAssociatedPixelFormat(bimg);
+        img.getPixels(0, 0, iw, ih, pf, data, offset, scan);
+        return bimg;
+    }
+
+    private BufferedImage toBufferedImage(boolean forceRGB) {
+        try {
+            return fromFXImage(getImage(), forceRGB);
+        } catch (Exception ex) {
+            ex.printStackTrace(System.err);
+        }
+
+        // return null upon any exception
+        return null;
+    }
+
+    @Override
+    public BufferedImage toBufferedImage() {
+        return toBufferedImage(false);
+    }
+
 }
--- a/modules/javafx.web/src/main/java/com/sun/webkit/graphics/WCImage.java	Fri Aug 24 15:07:12 2018 -0700
+++ b/modules/javafx.web/src/main/java/com/sun/webkit/graphics/WCImage.java	Mon Aug 27 11:42:34 2018 +0530
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,7 @@
 
 package com.sun.webkit.graphics;
 
+import java.awt.image.BufferedImage;
 import java.nio.ByteBuffer;
 
 public abstract class WCImage extends Ref {
@@ -84,5 +85,10 @@
         return img;
     }
 
+    public boolean isNull() {
+        return getWidth() <= 0 || getHeight() <= 0 || getPlatformImage() == null;
+    }
+
     public abstract float getPixelScale();
+    public abstract BufferedImage toBufferedImage();
 }
--- a/modules/javafx.web/src/test/java/test/javafx/scene/web/CanvasTest.java	Fri Aug 24 15:07:12 2018 -0700
+++ b/modules/javafx.web/src/test/java/test/javafx/scene/web/CanvasTest.java	Mon Aug 27 11:42:34 2018 +0530
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,7 +25,17 @@
 
 package test.javafx.scene.web;
 
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Base64;
+import javax.imageio.ImageIO;
+
 import netscape.javascript.JSObject;
+import org.junit.After;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.w3c.dom.Document;
@@ -38,61 +48,10 @@
  */
 public class CanvasTest extends TestBase {
 
-    private static final String expectedURL = "data:image/png;base64," +
-        "iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAACPklEQVR42u3T" +
-        "MSsGAByE8b9BDJIiJSnpLTGIQZSkJCW9JQYxiJKUpCQlBjaTzWazmWw2m81m" +
-        "stlsNps6PsRluJ6nfh/ghquflhLgVil9t5YAt5iDfLWXALeYg3x2lAC3mIN8" +
-        "dJUAt5iDvHeXALeYg7z1lgC3mIO89pUAt5iDvAyUALeYgzwPlgC3mIM8NUqA" +
-        "W8xBHodLgFvMQR5GS4BbzEHux0qAW8xB7iZKgFvMQW4nS4BbzEFupkuAW8xB" +
-        "rmdKgFvMQa7mSoBbzEHO50uAW8xBThZLgFvMQQ6XSoBbzEH2miXALeYg2ysl" +
-        "wC3mIBtrJcAt5iCr6yXALeYgy5slwC3mIAtbJcAt5iCzOyXALeYgU7slwC3m" +
-        "IOP7JcAt5iAjByXALeYgQ0clwC3mIP3HJcAt5iA9pyXALeYgnWclwC3mIG0X" +
-        "JcAt5iB1+TcGcIvpok2AXUxnnQLsYjrtEWAX03G/ALuYjoYE2MV0MCLALqb9" +
-        "cQF2Me1OCbCLaWdWgF1MWwsC7GLaXBZgF9P6qgC7mNY2BNjFtLItwC6m5p4A" +
-        "u5iWDgXYxbR4IsAupvlzAXYxzV0JsItp5lqAXUzTNwLsYpq8FWAX08SdALuY" +
-        "xu4F2MU0+iDALqbhRwF2MTWeBNjFNPgswC6mgRcBdjH1vQqwi6n3TYBdTN3v" +
-        "Auxi6voQYBdTx6cAu5javwTYxdT6LcAuppYfAXZERERERERERERERERERERE" +
-        "RERERERERERERERERERERERERERERET/0S/+VJ8zeU9ECwAAAABJRU5ErkJg" +
-        "gg==";
-
-    private static final String htmlContent = "\n"
-        + "<!DOCTYPE html>\n"
-        + "<html>\n"
-        + "<body>\n"
-        + "<canvas id=\"theCanvas\" width=\"200\" height=\"100\">\n"
-        + "</canvas>\n"
-        + "<p id = \"encodedText\">\n"
-        + "</p>\n"
-        + "<script>\n"
-        + "var c = document.getElementById(\"theCanvas\");\n"
-        + "var ctx = c.getContext(\"2d\");\n"
-        + "var my_gradient=ctx.createLinearGradient(0,0,0,75);\n"
-        + "my_gradient.addColorStop(0,\"red\");\n"
-        + "my_gradient.addColorStop(0.5,\"green\");\n"
-        + "my_gradient.addColorStop(1,\"blue\");\n"
-        + "ctx.fillStyle=my_gradient;\n"
-        + "ctx.fillRect(0,0,150,75);\n"
-        + "var dataURL = c.toDataURL();\n"
-        + "document.getElementById(\"encodedText\").innerHTML=dataURL;\n"
-        + "</script>\n"
-        + "</body>\n"
-        + "</html>\n";
-
-    @Ignore("RT-40092")
-    @Test public void testImageToDataURL() {
-        loadContent(htmlContent);
-        submit(() -> {
-            final Document doc = getEngine().getDocument();
-            Element elem = doc.getElementById("encodedText");
-            String textContent = elem.getTextContent();
-            textContent = textContent.replaceAll("\\s", "");
-            assertEquals("Data URL not encoded correctly", expectedURL, textContent);
-        });
-    }
+    private static final PrintStream ERR = System.err;
 
     // JDK-8162922
     @Test public void testCanvasStrokeRect() {
-
         final String htmlCanvasContent = "\n"
             + "<!DOCTYPE html>\n"
             + "<html>\n"
@@ -166,4 +125,116 @@
                     (int) getEngine().executeScript("document.getElementById('canvas').getContext('2d').getImageData(300,75,1,1).data[0]"));
         });
     }
+
+    // Color comparison algorithm is based on WebKit's Tools/ImageDiff/PlaformImage.cpp#PlatformImage::difference implemenation.
+    // https://trac.webkit.org/browser/webkit/trunk/Tools/ImageDiff/PlatformImage.cpp
+    private static float getColorDifference(final Color base, final Color c) {
+        final float red = (c.getRed() - base.getRed()) / Math.max(255.0f - base.getRed(), base.getRed());
+        final float green = (c.getGreen() - base.getGreen()) / Math.max(255.0f - base.getGreen(), base.getGreen());
+        final float blue = (c.getBlue() - base.getBlue()) / Math.max(255.0f - base.getBlue(), base.getBlue());
+        final float alpha = (c.getAlpha() - base.getAlpha()) / Math.max(255.0f - base.getAlpha(), base.getAlpha());
+        final float distance = ((float) Math.sqrt(red * red + green * green + blue * blue + alpha * alpha)) / 2.0f;
+        return distance >= (1 / 255.0f) ? distance * 100.0f : 0;
+    }
+
+    private static boolean isColorsSimilar(final Color base, final Color c, float toleranceInPercentage) {
+        return toleranceInPercentage >= getColorDifference(base, c);
+    }
+
+    private BufferedImage htmlCanvasToBufferedImage(final String mime) throws Exception {
+        ByteArrayOutputStream errStream = new ByteArrayOutputStream();
+        System.setErr(new PrintStream(errStream));
+
+        final String html = String.format(""
+            + "<body>"
+            + "<script>"
+            + "canvas = document.createElement('canvas');"
+            + "canvas.width = canvas.height = 100;"
+            + "var ctx = canvas.getContext('2d');"
+            + "ctx.fillStyle = 'red';"
+            + "ctx.fillRect(0, 0, 50, 100);"
+            + "data = canvas.toDataURL('%s');"
+            + "</script>"
+            + "</body>"
+         , mime);
+
+        loadContent(html);
+        System.setErr(ERR);
+
+        // Check whether any exception thrown
+        final String exMessage = errStream.toString();
+        assertFalse(String.format("Test failed with exception:\n%s", exMessage),
+            exMessage.contains("Exception") || exMessage.contains("Error"));
+
+        String img = (String) executeScript("window.data");
+        assertNotNull("window.data must have base64 encoded image", img);
+        // get rid of mime type
+        img = img.split(",")[1];
+        assertNotNull(img);
+
+        final byte[] imgBytes = Base64.getMimeDecoder().decode(img);
+        assertNotNull("Base64 decoded image data must be valid", imgBytes);
+        final ByteArrayInputStream inputStream = new ByteArrayInputStream(imgBytes);
+        final BufferedImage decodedImg = ImageIO.read(inputStream);
+        assertNotNull(decodedImg);
+        return decodedImg;
+    }
+
+    @Test
+    public void testColorSimilarityAlgorithm() {
+        assertTrue("Two Color.WHITE must be 100% equal", isColorsSimilar(Color.WHITE, Color.WHITE, 0));
+        assertTrue("Color.BLACK & Color.WHITE must be 100% different", isColorsSimilar(Color.WHITE, Color.BLACK, 100));
+
+        assertFalse("Color.BLACK & Color.WHITE must be different by at least 80%", isColorsSimilar(Color.WHITE, Color.BLACK, 80));
+        assertFalse("(0, 0, 0, 0) & Color.WHITE must be at least 99.99% different", isColorsSimilar(Color.WHITE, new Color(0, true), 99.99f));
+
+        assertTrue("Color.RED must be 100% equal to (255, 0, 0, 255)", isColorsSimilar(Color.RED, new Color(255, 0, 0, 255), 0));
+        assertTrue("Color.RED must be at least 99% similar to (255, 0, 0, 250)", isColorsSimilar(Color.RED, new Color(255, 0, 0, 250), 1));
+        assertTrue("Color.RED must be at least 95% similar to (250, 5, 5, 250)", isColorsSimilar(Color.RED, new Color(250, 5, 5, 250), 5));
+
+        assertTrue("Color.GREEN must be 100% equal to (0, 255, 0, 255)", isColorsSimilar(Color.GREEN, new Color(0, 255, 0, 255), 0));
+        assertTrue("Color.GREEN must be at least 99% similar to (0, 255, 0, 250)", isColorsSimilar(Color.GREEN, new Color(0, 255, 0, 250), 1));
+        assertTrue("Color.GREEN must be at least 95% similar to (5, 250, 5, 250)", isColorsSimilar(Color.GREEN, new Color(5, 250, 5, 250), 5));
+
+        assertTrue("Color.BLUE must be 100% equal to (0, 255, 0, 255)", isColorsSimilar(Color.BLUE, new Color(0, 0, 255, 255), 0));
+        assertTrue("Color.BLUE must be at least 99% similar to (0, 0, 255, 250)", isColorsSimilar(Color.BLUE, new Color(0, 0, 255, 250), 1));
+        assertTrue("Color.BLUE must be at least 95% similar to (5, 5, 250, 250)", isColorsSimilar(Color.BLUE, new Color(5, 5, 250, 250), 5));
+
+        assertTrue("(0, 0, 0, 0) must be at least 95% similar to (5, 5, 5, 5)", isColorsSimilar(new Color(0, true), new Color(5, 5, 5, 5), 5));
+        assertFalse("(0, 0, 0, 0) and (5, 5, 5, 5) must be different by at least 1%", isColorsSimilar(new Color(0, true), new Color(5, 5, 5, 5), 1));
+
+        assertTrue("Color.RED must be at least 25% similar to Color.GREEN", isColorsSimilar(Color.RED, Color.GREEN, 75));
+        assertFalse("Color.RED and Color.GREEN must be different by at least 70%", isColorsSimilar(Color.RED, Color.GREEN, 70));
+    }
+
+    @Test
+    public void testToDataURLWithPNGMimeType() throws Exception {
+        final BufferedImage decodedImg = htmlCanvasToBufferedImage("image/png");
+
+        // Pixel at (25 x 25) must be red
+        final Color pixelAt25x25 = new Color(decodedImg.getRGB(25, 25), true);
+        assertTrue("Color should be opaque red:" + pixelAt25x25, isColorsSimilar(Color.RED, pixelAt25x25, 1));
+
+        // PNG supports transparency, Pixel at (75 x 25) must be transparent black
+        final Color pixelAt75x25 = new Color(decodedImg.getRGB(75, 25), true);
+        assertTrue("Color should be transparent black:" + pixelAt75x25, isColorsSimilar(new Color(0, true), pixelAt75x25, 1));
+    }
+
+    @Test
+    public void testToDataURLWithJPEGMimeType() throws Exception {
+        final BufferedImage decodedImg = htmlCanvasToBufferedImage("image/jpeg");
+
+        // Pixel at (25 x 25) must be red
+        final Color pixelAt25x25 = new Color(decodedImg.getRGB(25, 25), true);
+        assertTrue("Color should be opaque red:" + pixelAt25x25, isColorsSimilar(Color.RED, pixelAt25x25, 1));
+
+        // JPEG doesn't supports transparency, Pixel at (75 x 25) must be opaque black
+        final Color pixelAt75x25 = new Color(decodedImg.getRGB(75, 25), true);
+        assertTrue("Color should be transparent black:" + pixelAt75x25, isColorsSimilar(Color.BLACK, pixelAt75x25, 1));
+    }
+
+    @After
+    public void resetSystemErr() {
+        System.setErr(ERR);
+    }
 }