#2612 gfx: setPixel and getPixel

Kevin Tue 20 Jun 2017

Are there no direct methods for these?

image.setPixel(x, y, color) color = image.getPixel(x,y)

I need to create an image buffer and draw each pixel individually, as well as read neighboring pixels. That will all get sent out as a PNG in the end.

brian Tue 20 Jun 2017

You can have a new graphics context to draw to a buffered image, and then use drawLine.

SlimerDude Tue 20 Jun 2017

I've wanted these methods many a time too!

From previous investigations, if I remember correctly, there's no reason why gfx.setPixel(x, y) can't be implemented (as long as the brush is just a simple a Colour and not a pen stroke or anything).

It's on my TODO list to submit a patch for this. For now, as Brian said, I just use gfx.drawLine() with the start and end coors set to the same coors.

As for gfx.getPixel() - that would be great (as would being able to create a transparent image!) but the underlying eclipse SWT doesn't make it easy. If you can find any documentation on it, I'll try to look into it again.

Kevin Tue 20 Jun 2017

Is there any way to directly access the image buffer as a 2d array? Without being able to read a pixel, I will be forced to use ... gulp! ... Java. Their image classes are very scary. :(

Also, isn't using a 1-pixel line horribly, horribly inefficient rather than just terribly inefficient? I considered it before discarding the thought. I'm already going to be running some heavy maths.

I suppose I could add these methods, but I'm not sure I currently have skillz mad enough to do it yet. I'm assuming just a wrapper around the Java methods would be needed, although I haven't fully explored them yet. Just getting a mutable image buffer made me cry.

SlimerDude Tue 20 Jun 2017

Hi Kevin,

Is there any way to directly access the image buffer as a 2d array?

Sadly, no. For now, if you're creating / drawing your own images, you could try create your own image buf at the same time.

It looks like a getPixel() may be possible in Java after all. Here are some links, mainly for myself for when I get round to looking at it:

isn't using a 1-pixel line horribly, horribly inefficient rather than just terribly inefficient?

Maybe, or maybe the underlying gfx lib is smart enough to notice the 2 coors are the same? Dunno, I've not profiled it. Anyway, right now, there's no other way!

brian Tue 20 Jun 2017

Maybe some confusion, but the gfx::Image API is not an API to work with low-level image data, color space, etc. It provides access to the underlying rendering framework for drawing to a buffered image to render to the screen or to encode to a file. As such drawLine with one pixel is going to have no noticeable performance difference if there was a drawPixel (which isn't exposed by most rendering APIs anyhow). I think maybe the core problem is you need to use a different sort of image library - accessing individual pixels is different then a rendering pipeline for shapes, fonts, alpha compositing, etc

Kevin Tue 20 Jun 2017

One last question before I give up: Can I create my own 2d array of pixels (i.e. rgba structs) to work on and then ship it off somewhere to be turned into a PNG?

The format is simple enough to write out as a byte stream, but I was hoping to enjoy some baked in goodness.

SlimerDude Tue 20 Jun 2017

rendering framework for drawing to a buffered image

Given buffered images are inherently RGB raster images, it is not unreasonable to expect to get and set RGB pixels. No one is asking for a low level colour space API, just a couple of higher level convenience methods - something that Fantom often excels at.

The coordinate system is already working at the pixel level, so I see no harm in having a Graphics.drawPixel(x, y).

Can I create my own 2d array of pixels ... turned into a PNG?

Tricky. You'd have to create an SWT Image, then set pixels from its getImageData() to create your image.

Then see the last comment in the forum post: gfx::Image Creation from IntArray for code on converting an SWT image to an FWT image.

Kevin Tue 20 Jun 2017

Haha! Thanks, but that's the sort of Java-esque gymnastics I'm trying to avoid. Maybe I can use JavaScript since it has an ImageData object that is stored in a flat array (image[0] = pixel1.r, image[1] = pixel1.g, etc.)

Anyway, all this groovy stuff is found in org.eclipse.swt.graphics.ImageData, but I can think of no reason why to have a separate class to do the pixel-level operations from the more comprehensive drawing ops. ¯\_(ツ)_/¯

go4 Sat 24 Jun 2017

I have written such api: getPixel and setPixel,

Example code:

//if you work in FWT
FwtToolkitEnv.init

Image p := BufImage.fromUri(`fan://icons/x16/folder.png`) |p|
{
  //image filter
  for (i:=0; i < p.size.w; ++i)
  {
    for (j:=0; j < p.size.h; ++j)
    {
      c := p.getPixel(i,j)
      //echo(c)
      c = c.and(0xffff0000)
      //echo(c)
      p.setPixel(i, j, c)
    }
  }
}
g := FwtToolkitEnv.toGraphics(g1)
g.drawImage(p, 0, 0)

Kevin Sun 25 Jun 2017

This is awesome! Thanks!

(However, I did my program in Java already...)

SlimerDude Mon 9 Oct 2017

drawPixel()

Some rendering APIs such as SWT provide an optimised drawPixel() method, whilst some (for instance, Javascript) do not.

For completeness, and to prevent future ambiguity, I see no harm in implementing a Graphics.drawPixel() method. This lets those subsystems that have it, use it - and those subsystems that don't, can default to calling fillRect().

The patch below defines a drawPixel() method, but drawPoint() may be more in keeping with the API.

diff -r a60c8b8decfb src/fwt/fan/FwtGraphics.fan
--- a/src/fwt/fan/FwtGraphics.fan	Fri Oct 06 09:21:02 2017 -0400
+++ b/src/fwt/fan/FwtGraphics.fan	Mon Oct 09 13:13:42 2017 +0100
@@ -20,6 +20,7 @@
   override native Bool antialias
   override native Int alpha
   override native GraphicsPath path()
+  override native This drawPixel(Int x, Int y)
   override native This drawLine(Int x1, Int y1, Int x2, Int y2)
   override native This drawPolyline(Point[] p)
   override native This drawPolygon(Point[] p)
diff -r a60c8b8decfb src/fwt/java/FwtGraphics.java
--- a/src/fwt/java/FwtGraphics.java	Fri Oct 06 09:21:02 2017 -0400
+++ b/src/fwt/java/FwtGraphics.java	Mon Oct 09 13:13:42 2017 +0100
@@ -194,6 +194,12 @@
     return new FwtGraphicsPath(this);
   }
 
+  public Graphics drawPixel(long x, long y)
+  {
+    gc.drawPoint((int)x, (int)y);
+    return this;
+  }
+
   public Graphics drawLine(long x1, long y1, long x2, long y2)
   {
     gc.drawLine((int)x1, (int)y1, (int)x2, (int)y2);
diff -r a60c8b8decfb src/fwt/js/FwtGraphics.js
--- a/src/fwt/js/FwtGraphics.js	Fri Oct 06 09:21:02 2017 -0400
+++ b/src/fwt/js/FwtGraphics.js	Mon Oct 09 13:13:42 2017 +0100
@@ -149,6 +149,13 @@
   return path;
 }
 
+// This drawPixel(Int x, Int y)
+fan.fwt.FwtGraphics.prototype.drawPixel = function(x, y)
+{
+  this.cx.fillRect(x, y, 1, 1);
+  return this;
+}
+
 // This drawLine(Int x1, Int y1, Int x2, Int y2)
 fan.fwt.FwtGraphics.prototype.drawLine = function(x1, y1, x2, y2)
 {
diff -r a60c8b8decfb src/gfx/fan/Graphics.fan
--- a/src/gfx/fan/Graphics.fan	Fri Oct 06 09:21:02 2017 -0400
+++ b/src/gfx/fan/Graphics.fan	Mon Oct 09 13:13:42 2017 +0100
@@ -49,6 +49,13 @@
 
   **
   ** Draw a line with the current pen and brush.
+  ** May default to calling 'fillRect(x, y, 1, 1)' if
+  ** a native implementation is not supported.
+  **
+  abstract This drawPixel(Int x, Int y)
+
+  **
+  ** Draw a line with the current pen and brush.
   **
   abstract This drawLine(Int x1, Int y1, Int x2, Int y2)

colorAt()

Retrieving the colour at a given coordinate of an image can be useful for things like generating alpha masks or determining collision detection.

A patch for SWT and JS implementations is given below:

diff -r a60c8b8decfb src/fwt/java/FwtEnvPeer.java
--- a/src/fwt/java/FwtEnvPeer.java	Fri Oct 06 09:21:02 2017 -0400
+++ b/src/fwt/java/FwtEnvPeer.java	Mon Oct 09 13:28:49 2017 +0100
@@ -17,6 +17,7 @@
 import org.eclipse.swt.graphics.ImageData;
 import org.eclipse.swt.graphics.ImageLoader;
 import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.RGB;
 
 public class FwtEnvPeer
 {
@@ -138,6 +139,20 @@
     }
   }
 
+  public fan.gfx.Color imageColorAt(FwtEnv self, fan.gfx.Image img, long x, long y)
+  {
+    // map Fantom image to SWT image
+    Fwt fwt = Fwt.get();
+    Image swtImg = fwt.image(img);
+    if (swtImg == null) throw Err.make("Image not valid or not loaded yet");
+
+    // return color at pixel coordinates
+    ImageData imageData = swtImg.getImageData();
+    RGB rgb  = imageData.palette.getRGB(imageData.getPixel((int) x, (int) y));
+    int a    = imageData.getAlpha((int) x, (int) y);
+    return fan.gfx.Color.makeArgb(a, rgb.red, rgb.green, rgb.blue);
+  }
+
 //////////////////////////////////////////////////////////////////////////
 // Font Support
 //////////////////////////////////////////////////////////////////////////
diff -r a60c8b8decfb src/fwt/js/FwtEnvPeer.js
--- a/src/fwt/js/FwtEnvPeer.js	Fri Oct 06 09:21:02 2017 -0400
+++ b/src/fwt/js/FwtEnvPeer.js	Mon Oct 09 13:28:49 2017 +0100
@@ -145,6 +145,23 @@
   return fan.gfx.Image.makeFields(uri, null);
 }
 
+// Color imageColorAt(Image i, Int x, Int y)
+fan.fwt.FwtEnvPeer.prototype.imageColorAt = function(self, fanImg, x, y)
+{
+  var jsOrig = fan.fwt.FwtEnvPeer.loadImage(fanImg);
+
+  // create temp canvas and draw our image to it
+  var canvas = document.createElement("canvas");
+  canvas.width = jsOrig.width;
+  canvas.height = jsOrig.height;
+  var cx = canvas.getContext("2d");
+  cx.drawImage(jsOrig, 0, 0);
+
+  // extract pixel color from canvas
+  var rgba = cx.getImageData(x, y, 1, 1).data;
+  return fan.gfx.Color.makeArgb(rgba[3], rgba[0], rgba[1], rgba[2]);
+}
+
 // Void imageDispose(Image i)
 fan.fwt.FwtEnvPeer.prototype.imageDispose = function(self, img)
 {
diff -r a60c8b8decfb src/gfx/fan/GfxEnv.fan
--- a/src/gfx/fan/GfxEnv.fan	Fri Oct 06 09:21:02 2017 -0400
+++ b/src/gfx/fan/GfxEnv.fan	Mon Oct 09 13:28:49 2017 +0100
@@ -64,6 +64,11 @@
   **
   abstract Void imageWrite(Image i, MimeType type, OutStream out)
 
+  **
+  ** Implementation of `Image.colorAt`
+  **
+  abstract Color imageColorAt(Image i, Int x, Int y)
+
 //////////////////////////////////////////////////////////////////////////
 // Font Support
 //////////////////////////////////////////////////////////////////////////
diff -r a60c8b8decfb src/gfx/fan/Image.fan
--- a/src/gfx/fan/Image.fan	Fri Oct 06 09:21:02 2017 -0400
+++ b/src/gfx/fan/Image.fan	Mon Oct 09 13:28:49 2017 +0100
@@ -94,6 +94,14 @@
   }
 
   **
+  ** Returns the RGBA value at the given pixel position.
+  ** Some subsystems may throw 'UnsupportedErr'.
+  **
+  virtual Color colorAt(Int x, Int y) {
+    GfxEnv.cur(true).imageColorAt(this, x, y)
+  }
+
+  **
   ** The uri which identifies this image.  If this
   ** image maps to an image file, then this is the file's uri.
   **
@@ -153,3 +161,4 @@
   Void write(MimeType type, OutStream out) { GfxEnv.cur.imageWrite(this, type, out) }
 
 }
+

I'm aware the Javascript implementation has poor performance, but I'm not aware of a better implementation.

I also looked at obtaining a colour value from a Graphics class so you can see what you're drawing - but for SWT this info seems to be embedded in methods marked not for public use.

SlimerDude Tue 10 Oct 2017

I almost forgot, here's some test code for drawPixel() and colorAt(), tested in Java and JavaScript:

Void testGetSetOpaquePixels() {
    Actor.locals["gfx.env"] = Type.find("fwt::FwtEnv").make
    image := null as Image
    
    try {
        image = Image.makePainted(Size(7, 4)) |g| {
            g.brush = Color.black
            g.fillRect(0, 0, 7, 4)

            g.brush = Color.red
            g.drawPixel(1, 1)
            g.brush = Color.green
            g.drawPixel(3, 1)
            g.brush = Color.blue
            g.drawPixel(5, 1)
            
            g.alpha = 127

            g.brush = Color.red
            g.drawPixel(1, 2)
            g.brush = Color.green
            g.drawPixel(3, 2)
            g.brush = Color.blue
            g.drawPixel(5, 2)
        }

        verifyEq(image.colorAt(0, 1), Color.black)
        verifyEq(image.colorAt(1, 1), Color.red)
        verifyEq(image.colorAt(2, 1), Color.black)
        verifyEq(image.colorAt(3, 1), Color.green)
        verifyEq(image.colorAt(4, 1), Color.black)
        verifyEq(image.colorAt(5, 1), Color.blue)
        verifyEq(image.colorAt(6, 1), Color.black)

        verifyEq(image.colorAt(0, 2), Color.black)
        verifyEq(image.colorAt(1, 2), Color.red.darker(0.5f))
        verifyEq(image.colorAt(2, 2), Color.black)
        verifyEq(image.colorAt(3, 2), Color.green.darker(0.5f))
        verifyEq(image.colorAt(4, 2), Color.black)
        verifyEq(image.colorAt(5, 2), Color.blue.darker(0.5f))
        verifyEq(image.colorAt(6, 2), Color.black)      
        
    } finally {
        image?.dispose
        Actor.locals.remove("gfx.env")
    }
}

brian Mon 16 Oct 2017

We will probably never add a drawPixel to Graphics because it doesn't make sense in many graphic contexts. Many APIs don't expose that such as the HTML Canvas object. And it makes no sense for vector formats for SVG or PDF. Really these sorts of details are related to pixel based buffers or pixel based image APIs which is a slightly different beast then a graphics context.

Login or Signup to reply.