diff --git a/src/com/t_oster/liblasercut/BlackWhiteRaster.java b/src/com/t_oster/liblasercut/BlackWhiteRaster.java
index e4368516c35ed9e6be839b3516ca247635e77fa0..2293629392e5ad78e5445dd9a225751217ad16a6 100644
--- a/src/com/t_oster/liblasercut/BlackWhiteRaster.java
+++ b/src/com/t_oster/liblasercut/BlackWhiteRaster.java
@@ -24,7 +24,7 @@ import com.t_oster.liblasercut.dithering.*;
  *
  * @author Thomas Oster <thomas.oster@rwth-aachen.de>
  */
-public class BlackWhiteRaster extends TimeIntensiveOperation
+public class BlackWhiteRaster extends TimeIntensiveOperation implements GreyscaleRaster
 {
 
   public static enum DitherAlgorithm
@@ -129,6 +129,22 @@ public class BlackWhiteRaster extends TimeIntensiveOperation
   {
     return raster[x][y];
   }
+  
+  /**
+   * Convenience function to pretend this B&W image is greyscale
+   * @param x
+   * @param y
+   * @return 0 for black, 255 for white
+   */
+  public int getGreyScale(int x, int y)
+  {
+    return isBlack(x, y) ? 0 : 255;
+  }
+  
+  public void setGreyScale(int x, int y, int color)
+  {
+    this.setBlack(x, y, color < 128);
+  }
 
   public int getWidth()
   {
diff --git a/src/com/t_oster/liblasercut/LaserCutter.java b/src/com/t_oster/liblasercut/LaserCutter.java
index 791d23ee84a6ad8e2af28619c68112c686f8dec7..5c5b1a87bb2a849b7eb8dc754c47f7105ef6a84d 100644
--- a/src/com/t_oster/liblasercut/LaserCutter.java
+++ b/src/com/t_oster/liblasercut/LaserCutter.java
@@ -181,100 +181,35 @@ public abstract class LaserCutter implements Cloneable, Customizable {
 
     public abstract String getModelName();
 
-    protected VectorPart convertRasterToVectorPart(RasterPart rp, LaserProperty blackPixelProperty, LaserProperty whitePixelProperty, double resolution, boolean unidirectional)
+    /**
+     * Converts a raster image (B&W or greyscale) into a series of vector
+     * instructions suitable for printing. Lets non-raster-native cutters
+     * emulate this functionality using gcode.
+     * @param rp the raster job to convert
+     * @param resolution resolution to output job at
+     * @param bidirectional cut in both directions
+     * @return a VectorPart job of VectorCommands
+     */
+    protected VectorPart convertRasterizableToVectorPart(RasterizableJobPart rp, double resolution, boolean bidirectional)
     {
-      boolean dirRight = true;
-      Point rasterStart = rp.getRasterStart();
-      VectorPart result = new VectorPart(blackPixelProperty, resolution);
-      for (int line = 0; line < rp.getRasterHeight(); line++)
+      VectorPart result = new VectorPart(rp.getLaserProperty(), resolution);
+      for (int y = 0; y < rp.getRasterHeight(); y++)
       {
-        Point lineStart = rasterStart.clone();
-        lineStart.y += line;
-        //Convert BlackWhite line into line of 0 and 255 bytes
-        BlackWhiteRaster bwr = rp.image;
-        List<Byte> bytes = new LinkedList<Byte>();
-        boolean lookForStart = true;
-        for (int x = 0; x < bwr.getWidth(); x++)
-        {
-          if (lookForStart)
-          {
-            if (bwr.isBlack(x, line))
-            {
-              lookForStart = false;
-              bytes.add((byte) 255);
-            }
-            else
-            {
-              lineStart.x += 1;
-            }
-          }
-          else
-          {
-            bytes.add(bwr.isBlack(x, line) ? (byte) 255 : (byte) 0);
-          }
-        }
-        //remove trailing zeroes
-        while (bytes.size() > 0 && bytes.get(bytes.size() - 1) == 0)
-        {
-          bytes.remove(bytes.size() - 1);
-        }
-        if (bytes.size() > 0)
+        if (rp.lineIsBlank(y) == false)
         {
-          if (dirRight)
+          Point lineStart = rp.getStartPosition(y);
+          
+          //move to the first point of the line
+          result.moveto(lineStart.x + rp.firstNonWhitePixel(y)+rp.cutCompensation(), lineStart.y);
+          
+          for (int x = rp.firstNonWhitePixel(y); !rp.hasFinishedCuttingLine(x, y);)
           {
-            //move to the first nonempyt point of the line
-            result.moveto(lineStart.x, lineStart.y);
-            byte old = bytes.get(0);
-            for (int pix = 0; pix < bytes.size(); pix++)
-            {
-              if (bytes.get(pix) != old)
-              {
-                if (old == 0)
-                {//beginning of "black" segment -> move with 0 power
-                  result.setProperty(whitePixelProperty);
-                  result.lineto(lineStart.x + pix, lineStart.y);
-                }
-                else
-                {//end of "black" segment -> move with power to pixel before
-                  result.setProperty(blackPixelProperty);
-                  result.lineto(lineStart.x + pix - 1, lineStart.y);
-                }
-                old = bytes.get(pix);
-              }
-            }
-            result.setProperty(blackPixelProperty);
-            result.lineto(lineStart.x + bytes.size() - 1, lineStart.y);
+            result.setProperty(rp.getPowerSpeedFocusPropertyForPixel(x, y));
+            x = rp.nextColorChange(x, y);
+            result.lineto(lineStart.x + x + rp.cutCompensation(), lineStart.y);
           }
-          else
-          {
-            //move to the last nonempty point of the line
-            result.moveto(lineStart.x + bytes.size() - 1, lineStart.y);
-            byte old = bytes.get(bytes.size() - 1);
-            for (int pix = bytes.size() - 1; pix >= 0; pix--)
-            {
-              if (bytes.get(pix) != old || pix == 0)
-              {
-                if (old == 0)
-                {
-                  result.setProperty(whitePixelProperty);
-                  result.lineto(lineStart.x + pix, lineStart.y);
-                }
-                else
-                {
-                  result.setProperty(blackPixelProperty);
-                  result.lineto(lineStart.x + pix + 1, lineStart.y);
-                }
-                old = bytes.get(pix);
-              }
-            }
-            //last pixel is always black (white pixels are stripped before)
-            result.setProperty(blackPixelProperty);
-            result.lineto(lineStart.x, lineStart.y);
-          }
-        }
-        if (!unidirectional)
-        {
-          dirRight = !dirRight;
+          
+          if (bidirectional) rp.toggleRasteringCutDirection();
         }
       }
       return result;
diff --git a/src/com/t_oster/liblasercut/Raster3dPart.java b/src/com/t_oster/liblasercut/Raster3dPart.java
index 08b69f2fd0412d31942464012d6a8fce4b73c14d..371ce605e57e5357d1bd70eca53a4b3df5be027c 100644
--- a/src/com/t_oster/liblasercut/Raster3dPart.java
+++ b/src/com/t_oster/liblasercut/Raster3dPart.java
@@ -26,12 +26,10 @@ import java.util.List;
  *
  * @author Thomas Oster <thomas.oster@rwth-aachen.de>
  */
-public class Raster3dPart extends JobPart
+public class Raster3dPart extends RasterizableJobPart
 {
 
-  private GreyscaleRaster image = null;
   private LaserProperty property = null;
-  protected Point start = null;
   private double resolution = 500;
 
   public Raster3dPart(GreyscaleRaster image, LaserProperty laserProperty, Point offset, double resolution)
@@ -127,4 +125,34 @@ public class Raster3dPart extends JobPart
     }
     return result;
   }
+
+  @Override
+  public FloatPowerSpeedFocusProperty getPowerSpeedFocusPropertyForColor(int color)
+  {
+    FloatPowerSpeedFocusProperty power = (FloatPowerSpeedFocusProperty) getLaserProperty().clone();
+    // convert 0-255 into <max power>-0. i.e....
+    //   - 0 (black) -> 100%
+    //   - 127 (mid) -> 50%
+    //   - 255 (white) -> 0%
+    
+    // y = mx + c
+    // x = color
+    // y = power
+    // 
+    // x = 0 -> y = <max power>
+    // x = 255 -> y = 0
+    // 
+    // x = 0  ->  y = <max>  ->  y = m*0 + c  ->  c = <max>
+    float c = (float) power.getPower();
+    
+    // x = 255  ->  y = 0  ->  y = m*255 + <max>  ->  0 = m*255 + <max>
+    // ->  -<max> = m*255  -> -<max>/255 = m
+    float m = -c / 255f;
+    
+    float x = (float) color;
+    float y = m*x + c;
+    
+    power.setPower((int) y);
+    return power;
+  }
 }
diff --git a/src/com/t_oster/liblasercut/RasterPart.java b/src/com/t_oster/liblasercut/RasterPart.java
index 6b591520fec9654fea87f1cb49a7c7e7ad69c20e..bb68105d7f4264d0392a467340f3ab6e400ce334 100644
--- a/src/com/t_oster/liblasercut/RasterPart.java
+++ b/src/com/t_oster/liblasercut/RasterPart.java
@@ -26,20 +26,21 @@ import java.util.List;
  *
  * @author Thomas Oster <thomas.oster@rwth-aachen.de>
  */
-public class RasterPart extends JobPart
+public class RasterPart extends RasterizableJobPart
 {
 
-  BlackWhiteRaster image = null;
-  LaserProperty property = null;
-  Point start = null;
+  LaserProperty blackPixelProperty = null;
+  LaserProperty whitePixelProperty = null;
   double resolution = 500;
 
   public RasterPart(BlackWhiteRaster image, LaserProperty laserProperty, Point offset, double resolution)
   {
     this.image = image;
-    this.property = laserProperty;
     this.start = offset;
     this.resolution = resolution;
+    this.blackPixelProperty = laserProperty;
+    this.whitePixelProperty = blackPixelProperty.clone();
+    whitePixelProperty.setProperty("power", 0.0f);
   }
 
   @Override
@@ -86,7 +87,6 @@ public class RasterPart extends JobPart
    * Returns one line of the given rasterpart
    * every byte represents 8 pixel and the value corresponds to
    * 1 when black or 0 when white
-   * @param raster
    * @param line
    * @return
    */
@@ -95,29 +95,28 @@ public class RasterPart extends JobPart
     List<Byte> result = new LinkedList<Byte>();
     for (int x = 0; x < (image.getWidth() + 7) / 8; x++)
     {
-      result.add(image.getByte(x, line));
+      result.add(((BlackWhiteRaster) image).getByte(x, line));
     }
     return result;
   }
 
   public boolean isBlack(int x, int y)
   {
-    return this.image.isBlack(x, y);
+    return ((BlackWhiteRaster) image).isBlack(x, y);
   }
 
-  public int getRasterWidth()
-  {
-    return this.image.getWidth();
-  }
-
-  public int getRasterHeight()
+  @Override
+  public LaserProperty getLaserProperty()
   {
-    return this.image.getHeight();
+    return this.blackPixelProperty;
   }
-
-  public LaserProperty getLaserProperty()
+  
+  @Override
+  public FloatPowerSpeedFocusProperty getPowerSpeedFocusPropertyForColor(int color)
   {
-      return this.property;
+    return color <= 127
+      ? (FloatPowerSpeedFocusProperty) blackPixelProperty
+      : (FloatPowerSpeedFocusProperty) whitePixelProperty;
   }
 
 }
diff --git a/src/com/t_oster/liblasercut/RasterizableJobPart.java b/src/com/t_oster/liblasercut/RasterizableJobPart.java
new file mode 100644
index 0000000000000000000000000000000000000000..75df4482f84e23b68148e43b75a90f138afbfead
--- /dev/null
+++ b/src/com/t_oster/liblasercut/RasterizableJobPart.java
@@ -0,0 +1,235 @@
+/**
+ * This file is part of LibLaserCut.
+ * Copyright (C) 2011 - 2014 Thomas Oster <mail@thomas-oster.de>
+ *
+ * LibLaserCut 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LibLaserCut 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 LibLaserCut. If not, see <http://www.gnu.org/licenses/>.
+ *
+ **/
+package com.t_oster.liblasercut;
+
+import com.t_oster.liblasercut.platform.Point;
+
+/**
+ * Common functions useful when rasterizing an image.
+ * @author Michael Adams <zap@michaeladams.org>
+ */
+abstract public class RasterizableJobPart extends JobPart
+{
+  protected GreyscaleRaster image;
+  protected Point start = null;
+  protected boolean cutDirectionleftToRight = true;
+  
+  /**
+   * The initial laser settings to start a rasterization job with.
+   * @return LaserProperty
+   */
+  public abstract LaserProperty getLaserProperty();
+  
+  /**
+   * Gets the height of the associated raster image
+   * @return height in pixels
+   */
+  public int getRasterHeight() {
+    return this.image.getHeight();
+  }
+  
+  /**
+   * Gets the width of the associated raster image 
+   * @return width in pixels
+   */
+  public int getRasterWidth() {
+    return this.image.getWidth();
+  }
+  
+  /**
+   * Determines whether an entire line in an image is blank; i.e. can it be skipped?
+   * @param y
+   * @return true if the line is blank
+   */
+  public boolean lineIsBlank(int y)
+  {
+    for (int x=0; x<getRasterWidth(); x++)
+      if (image.getGreyScale(x, y) < 255)
+        return false;
+    return true;
+  }
+  
+  /**
+   * Toggle the direction cutting is done in. Left to right by default; when changed
+   * then "start" of the line is the right-most side, and "end" is the left-most
+   * side.
+   */
+  public void toggleRasteringCutDirection()
+  {
+    cutDirectionleftToRight = !cutDirectionleftToRight;
+  }
+
+  
+  /**
+   * Adds any required compensation when cutting.
+   * Fixes off-by-one errors when cutting in reverse direction, since
+   * the pixel finding methods here always refer to the bottom left corner of 
+   * a pixel, but when cutting in reverse direction, we need to take pixel width
+   * into account.
+   * @return amount to adjust coordinates by
+   */
+  public int cutCompensation()
+  {
+    return cutDirectionleftToRight ? 0 : 1;
+  }
+  
+  /**
+   * Given an coordinate, and knowing the direction we are cutting in, decide
+   * if we have finished cutting a row of the image.
+   * @param x x coordinate of last pixel cut
+   * @param y y coordinate of last pixel cut
+   * @return true if we have finished cutting a line
+   */
+  public boolean hasFinishedCuttingLine(int x, int y)
+  {
+    return cutDirectionleftToRight 
+      ? (x > rightMostNonWhitePixel(y)) 
+      : (x < leftMostNonWhitePixel(y));
+  }
+  
+  /**
+   * Finds the x coordinate of the first pixel that needs lasering
+   * @param y
+   * @return x coordinate to start lasering from
+   */
+  public int firstNonWhitePixel(int y)
+  {
+    return cutDirectionleftToRight
+      ? leftMostNonWhitePixel(y)
+      : rightMostNonWhitePixel(y);
+  }
+  
+  /**
+   * Finds the x coordinate for the left most pixel, since "start" depends on
+   * what direction you are cutting in.
+   * @param y
+   * @return x coordinate of left most non-white pixel
+   */
+  protected int leftMostNonWhitePixel(int y)
+  {
+    for (int x=0; x<getRasterWidth(); x++)
+      if (image.getGreyScale(x, y) < 255)
+        return x;
+    return getRasterWidth();
+  }
+  
+  /**
+   * Finds the end of the line; points after this pixel are all blank
+   * @param y
+   * @return x coordinate to end lasering at
+   */
+  public int lastNonWhitePixel(int y)
+  {
+    return cutDirectionleftToRight
+      ? rightMostNonWhitePixel(y)
+      : leftMostNonWhitePixel(y);
+  }
+  
+  /**
+   * Finds the x coordinate for the right most pixel, since "end" depends on
+   * what direction you are cutting in.
+   * @param y
+   * @return x coordinate of right most non-white pixel
+   */
+  protected int rightMostNonWhitePixel(int y)
+  {
+    for (int x=getRasterWidth()-1; x >= 0; x--)
+      if (image.getGreyScale(x, y) < 255)
+        return x;
+    return 0;
+  }
+  
+  /**
+   * Given a pixel in a row of an image, finds the next pixel that has a different
+   * color. If no more color changes take place, returns the last interesting pixel.
+   * @param x x coordinate to start scanning from
+   * @param y y coordinate to start scanning from
+   * @return x coordinate of the next different color in this row
+   */
+  public int nextColorChange(int x, int y)
+  {
+    return cutDirectionleftToRight
+      ? nextColorChangeHeadingRight(x, y)
+      : nextColorChangeHeadingLeft(x, y);
+  }
+  
+  /**
+   * nextColorChange logic when heading ->
+   * @param x x coordinate to start scanning from
+   * @param y y coordinate to start scanning from
+   * @return x coordinate of the next different color in this row
+   */
+  protected int nextColorChangeHeadingRight(int x, int y)
+  {
+    int color = image.getGreyScale(x, y);
+    for (int i=x; i<getRasterWidth(); i++)
+      if (image.getGreyScale(i, y) != color)
+        return i;
+    // rest of line is the same color, so next colour change is past end of line
+    return getRasterWidth();
+  }
+  
+  /**
+   * nextColorChange logic when heading <-
+   * @param x x coordinate to start scanning from
+   * @param y y coordinate to start scanning from
+   * @return x coordinate of the next different color in this row
+   */
+  protected int nextColorChangeHeadingLeft(int x, int y)
+  {
+    int color = image.getGreyScale(x, y);
+    for (int i=x; i>=0; i--)
+      if (image.getGreyScale(i, y) != color)
+        return i;
+    // rest of line is the same color, so next colour change is past the beginning of line
+    return -1;
+  }
+  
+  /**
+   * Returns the start position of the first column (x=0) for a given line
+   * @param y y coordinate of the row in question
+   * @return Point representing start of this row
+   */
+  public Point getStartPosition(int y)
+  {
+    Point start = this.start.clone();
+    start.y += y;
+    return start;
+  }
+  
+  /**
+   * Calculate power/speed/focus required to laser a given pixel
+   * @param x x coordinate of pixel
+   * @param y y coordinate of pixel
+   * @return laser property appropriate for the color at this pixel
+   */
+  public FloatPowerSpeedFocusProperty getPowerSpeedFocusPropertyForPixel(int x, int y)
+  {
+    return getPowerSpeedFocusPropertyForColor(image.getGreyScale(x, y));
+  }
+  
+  /**
+   * Returns a power/speed/focus property appropriate for a given color.
+   * 255 = white = 0% laser.
+   * 0 = black = 100% laser.
+   * @param color 0-255 value representing the color. 0 = black and 255 = white.
+   * @return laser property appropriate for this color
+   */
+  public abstract FloatPowerSpeedFocusProperty getPowerSpeedFocusPropertyForColor(int color);
+}
diff --git a/src/com/t_oster/liblasercut/drivers/GenericGcodeDriver.java b/src/com/t_oster/liblasercut/drivers/GenericGcodeDriver.java
index 0c0f1c8f7794f344bd9aa41ade3fe849fc3aea15..9bd596afca2253474eb1eb355a40455b049cc3a1 100644
--- a/src/com/t_oster/liblasercut/drivers/GenericGcodeDriver.java
+++ b/src/com/t_oster/liblasercut/drivers/GenericGcodeDriver.java
@@ -68,6 +68,7 @@ public class GenericGcodeDriver extends LaserCutter {
   protected static final String SETTING_SERIAL_TIMEOUT = "Milliseconds to wait for response";
   protected static final String SETTING_BLANK_LASER_DURING_RAPIDS = "Force laser off during G0 moves";
   protected static final String SETTING_FILE_EXPORT_PATH = "Path to save exported gcode";
+  protected static final String SETTING_USE_BIDIRECTIONAL_RASTERING = "Use bidirectional rastering";
   
   protected static Locale FORMAT_LOCALE = Locale.US;
   
@@ -322,6 +323,22 @@ public class GenericGcodeDriver extends LaserCutter {
     this.blankLaserDuringRapids = blankLaserDuringRapids;
   }
   
+  /**
+   * When rastering, whether to always cut from left to right, or to cut in both
+   * directions? (i.e. use the return stroke to raster as well)
+   */
+  protected boolean useBidirectionalRastering = false;
+  
+  public boolean getUseBidirectionalRastering()
+  {
+    return useBidirectionalRastering;
+  }
+  
+  public void setUseBidirectionalRastering(boolean useBidirectionalRastering)
+  {
+    this.useBidirectionalRastering = useBidirectionalRastering;
+  }
+  
   @Override
   /**
    * We do not support Frequency atm, so we return power,speed and focus
@@ -721,13 +738,9 @@ public class GenericGcodeDriver extends LaserCutter {
       int max = job.getParts().size();
       for (JobPart p : job.getParts())
       {
-        if (p instanceof RasterPart)
+        if (p instanceof Raster3dPart || p instanceof RasterPart)
         {
-          RasterPart rp = (RasterPart) p;
-          LaserProperty black = rp.getLaserProperty();
-          LaserProperty white = black.clone();
-          white.setProperty("power", 0.0f);
-          p = convertRasterToVectorPart((RasterPart) p, black, white,  p.getDPI(), false);
+          p = convertRasterizableToVectorPart((RasterizableJobPart) p, p.getDPI(), getUseBidirectionalRastering());
         }
         if (p instanceof VectorPart)
         {
@@ -765,13 +778,9 @@ public void saveJob(java.io.PrintStream fileOutputStream, LaserJob job) throws I
 	writeInitializationCode();
 	for (JobPart p : job.getParts())
 	{
-		if (p instanceof RasterPart)
+		if (p instanceof Raster3dPart || p instanceof RasterPart)
 		{
-			RasterPart rp = (RasterPart) p;
-			LaserProperty black = rp.getLaserProperty();
-			LaserProperty white = black.clone();
-			white.setProperty("power", 0.0f);
-			p = convertRasterToVectorPart((RasterPart) p, black, white,  p.getDPI(), false);
+			p = convertRasterizableToVectorPart((RasterizableJobPart) p, p.getDPI(), getUseBidirectionalRastering());
 		}
     		if (p instanceof VectorPart)
 		{
@@ -859,7 +868,8 @@ public void saveJob(java.io.PrintStream fileOutputStream, LaserJob job) throws I
     SETTING_RESOLUTIONS,
     SETTING_WAIT_FOR_OK,
     SETTING_SERIAL_TIMEOUT,
-    SETTING_FILE_EXPORT_PATH
+    SETTING_FILE_EXPORT_PATH,
+    SETTING_USE_BIDIRECTIONAL_RASTERING
   };
 
   @Override
@@ -913,6 +923,8 @@ public void saveJob(java.io.PrintStream fileOutputStream, LaserJob job) throws I
       return this.getBlankLaserDuringRapids();
     } else if (SETTING_FILE_EXPORT_PATH.equals(attribute)) {
       return this.getExportPath();
+    } else if (SETTING_USE_BIDIRECTIONAL_RASTERING.equals(attribute)) {
+      return this.getUseBidirectionalRastering();
     }
     
     return null;
@@ -964,6 +976,8 @@ public void saveJob(java.io.PrintStream fileOutputStream, LaserJob job) throws I
       this.setBlankLaserDuringRapids((Boolean) value);
     } else if (SETTING_FILE_EXPORT_PATH.equals(attribute)) {
       this.setExportPath((String) value);
+    } else if (SETTING_USE_BIDIRECTIONAL_RASTERING.equals(attribute)) {
+      this.setUseBidirectionalRastering((Boolean) value);
     }
   }
 
diff --git a/test/com/t_oster/liblasercut/RasterizableJobPartTest.java b/test/com/t_oster/liblasercut/RasterizableJobPartTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2bb416564929c22e69742fe59361bc9e13cb5490
--- /dev/null
+++ b/test/com/t_oster/liblasercut/RasterizableJobPartTest.java
@@ -0,0 +1,566 @@
+/**
+ * This file is part of LibLaserCut.
+ * Copyright (C) 2011 - 2014 Thomas Oster <mail@thomas-oster.de>
+ *
+ * LibLaserCut 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LibLaserCut 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 LibLaserCut. If not, see <http://www.gnu.org/licenses/>.
+ *
+ **/
+package com.t_oster.liblasercut;
+
+import com.t_oster.liblasercut.platform.Point;
+import com.t_oster.liblasercut.utils.BufferedImageAdapter;
+import java.awt.image.BufferedImage;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Test class for RasterizableJobPart
+ * Michael Adams <zap@michaeladams.org>
+ */
+public class RasterizableJobPartTest
+{
+  /**
+   * Test of getLaserProperty method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testGetLaserProperty()
+  {
+    RasterizableJobPart instance = new RasterizableJobPartImpl();
+    FloatPowerSpeedFocusProperty expResult = new FloatPowerSpeedFocusProperty();
+    expResult.setPower(100.0f);
+    expResult.setSpeed(100.0f);
+    expResult.setFocus(0.0f);
+    LaserProperty result = instance.getLaserProperty();
+    assertEquals(expResult, result);
+  }
+
+  /**
+   * Test of getRasterHeight method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testGetRasterHeight()
+  {
+    RasterizableJobPart instance = new RasterizableJobPartImpl();
+    int expResult = 6;
+    int result = instance.getRasterHeight();
+    assertEquals(expResult, result);
+  }
+
+  /**
+   * Test of getRasterWidth method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testGetRasterWidth()
+  {
+    RasterizableJobPart instance = new RasterizableJobPartImpl();
+    int expResult = 9;
+    int result = instance.getRasterWidth();
+    assertEquals(expResult, result);
+  }
+
+  /**
+   * Test of lineIsBlank method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testLineIsBlank()
+  {
+    RasterizableJobPart instance = new RasterizableJobPartImpl();
+    
+    assertEquals(false, instance.lineIsBlank(0));
+    assertEquals(false, instance.lineIsBlank(1));
+    assertEquals(false, instance.lineIsBlank(2));
+    assertEquals(false, instance.lineIsBlank(3));
+    assertEquals(true, instance.lineIsBlank(4));
+  }
+
+  /**
+   * Test of setRasteringCutDirection method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testSetRasteringCutDirection()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    instance.setRasteringCutDirectionLeftToRight();
+    assertEquals("left to right", ((RasterizableJobPartImpl) instance).getRasteringCutDirection());
+    
+    instance.toggleRasteringCutDirection();
+    assertEquals("right to left", ((RasterizableJobPartImpl) instance).getRasteringCutDirection());
+  }
+
+  /**
+   * Test of cutCompensation method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testCutCompensation()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    instance.setRasteringCutDirectionLeftToRight();
+    assertEquals(0, instance.cutCompensation());
+    
+    instance.toggleRasteringCutDirection();
+    assertEquals(1, instance.cutCompensation());
+  }
+
+  /**
+   * Test of hasFinishedCuttingLine method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testHasFinishedCuttingLine()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    // cut -> --XX-XXX-
+    instance.setRasteringCutDirectionLeftToRight();
+    // pixels 0-7 are part of the job
+    for (int i=0; i<=7; i++) assertEquals(false, instance.hasFinishedCuttingLine(i, 0));
+    // once you get to pixel 8, the line is cut
+    assertEquals(true, instance.hasFinishedCuttingLine(8, 0));
+    
+    // cut <- ---X--XX-
+    instance.toggleRasteringCutDirection();
+    // coords 8-3 are part of the job
+    for (int i=8; i>=3; i--) assertEquals(false, instance.hasFinishedCuttingLine(i, 1));
+    // once you get to pixels 2-0 (heading left), the line is cut
+    for (int i=2; i>=0; i--) assertEquals(true, instance.hasFinishedCuttingLine(i, 1));
+  }
+
+  /**
+   * Test of firstNonWhitePixel method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testFirstNonWhitePixel()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    // cut -> --XX-XXX-
+    instance.setRasteringCutDirectionLeftToRight();
+    assertEquals(2, instance.firstNonWhitePixel(0));
+    
+    // cut <- ---X--XX-
+    instance.toggleRasteringCutDirection();
+    assertEquals(7, instance.firstNonWhitePixel(1));
+    
+    // cut -> 303333333
+    instance.toggleRasteringCutDirection();
+    assertEquals(0, instance.firstNonWhitePixel(2));
+    
+    // cut <- 007777777
+    instance.toggleRasteringCutDirection();
+    assertEquals(8, instance.firstNonWhitePixel(3));
+  }
+
+  /**
+   * Test of leftMostNonWhitePixel method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testLeftMostNonWhitePixel()
+  {
+    RasterizableJobPart instance = new RasterizableJobPartImpl();
+    
+    // --XX-XXX-
+    assertEquals(2, instance.leftMostNonWhitePixel(0));
+    
+    // ---X--XX-
+    assertEquals(3, instance.leftMostNonWhitePixel(1));
+    
+    // 303333333
+    assertEquals(0, instance.leftMostNonWhitePixel(2));
+    
+    // 007777777
+    assertEquals(2, instance.leftMostNonWhitePixel(3));
+  }
+
+  /**
+   * Test of lastNonWhitePixel method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testLastNonWhitePixel()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    // cut -> --XX-XXX-
+    instance.setRasteringCutDirectionLeftToRight();
+    assertEquals(7, instance.lastNonWhitePixel(0));
+    
+    // cut <- ---X--XX-
+    instance.toggleRasteringCutDirection();
+    assertEquals(3, instance.lastNonWhitePixel(1));
+    
+    // cut -> 303333333
+    instance.toggleRasteringCutDirection();
+    assertEquals(8, instance.lastNonWhitePixel(2));
+    
+    // cut <- 007777777
+    instance.toggleRasteringCutDirection();
+    assertEquals(2, instance.lastNonWhitePixel(3));
+  }
+
+  /**
+   * Test of rightMostNonWhitePixel method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testRightMostNonWhitePixel()
+  {
+    RasterizableJobPart instance = new RasterizableJobPartImpl();
+    
+    // --XX-XXX-
+    assertEquals(7, instance.rightMostNonWhitePixel(0));
+    
+    // ---X--XX-
+    assertEquals(7, instance.rightMostNonWhitePixel(1));
+    
+    // 303333333
+    assertEquals(8, instance.rightMostNonWhitePixel(2));
+    
+    // 007777777
+    assertEquals(8, instance.rightMostNonWhitePixel(3));
+  }
+
+  /**
+   * Test of nextColorChange method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testNextColorChange()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    // cut -> --XX-XXX-
+    instance.setRasteringCutDirectionLeftToRight();
+    assertEquals(2, instance.nextColorChange(0, 0));
+    assertEquals(2, instance.nextColorChange(1, 0));
+    assertEquals(4, instance.nextColorChange(2, 0));
+    assertEquals(4, instance.nextColorChange(3, 0));
+    assertEquals(5, instance.nextColorChange(4, 0));
+    assertEquals(8, instance.nextColorChange(5, 0));
+    
+    // cut <- ---X--XX-
+    instance.toggleRasteringCutDirection();
+    assertEquals(7, instance.nextColorChange(8, 1));
+    assertEquals(5, instance.nextColorChange(7, 1));
+    assertEquals(5, instance.nextColorChange(6, 1));
+    assertEquals(3, instance.nextColorChange(5, 1));
+    assertEquals(3, instance.nextColorChange(4, 1));
+    assertEquals(2, instance.nextColorChange(3, 1));
+  }
+
+  /**
+   * Test of nextColorChangeHeadingRight method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testNextColorChangeHeadingRight()
+  {
+    RasterizableJobPart instance = new RasterizableJobPartImpl();
+    
+    // --XX-XXX-
+    assertEquals(2, instance.nextColorChangeHeadingRight(0, 0));
+    assertEquals(2, instance.nextColorChangeHeadingRight(1, 0));
+    assertEquals(4, instance.nextColorChangeHeadingRight(2, 0));
+    assertEquals(4, instance.nextColorChangeHeadingRight(3, 0));
+    assertEquals(5, instance.nextColorChangeHeadingRight(4, 0));
+    assertEquals(8, instance.nextColorChangeHeadingRight(5, 0));
+    
+    // ---X--XX-
+    assertEquals(3, instance.nextColorChangeHeadingRight(0, 1));
+    assertEquals(3, instance.nextColorChangeHeadingRight(1, 1));
+    assertEquals(3, instance.nextColorChangeHeadingRight(2, 1));
+    assertEquals(4, instance.nextColorChangeHeadingRight(3, 1));
+    assertEquals(6, instance.nextColorChangeHeadingRight(4, 1));
+    assertEquals(6, instance.nextColorChangeHeadingRight(5, 1));
+    assertEquals(8, instance.nextColorChangeHeadingRight(6, 1));
+    assertEquals(8, instance.nextColorChangeHeadingRight(7, 1));
+    
+    // 3-3333333
+    assertEquals(9, instance.nextColorChangeHeadingRight(7, 2));
+  }
+
+  /**
+   * Test of nextColorChangeHeadingLeft method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testNextColorChangeHeadingLeft()
+  {
+    RasterizableJobPart instance = new RasterizableJobPartImpl();
+    
+    // --XX-XXX-
+    assertEquals(7, instance.nextColorChangeHeadingLeft(8, 0));
+    assertEquals(4, instance.nextColorChangeHeadingLeft(7, 0));
+    assertEquals(4, instance.nextColorChangeHeadingLeft(6, 0));
+    assertEquals(4, instance.nextColorChangeHeadingLeft(5, 0));
+    assertEquals(3, instance.nextColorChangeHeadingLeft(4, 0));
+    assertEquals(1, instance.nextColorChangeHeadingLeft(3, 0));
+    assertEquals(1, instance.nextColorChangeHeadingLeft(2, 0));
+    
+    // ---X--XX-
+    assertEquals(7, instance.nextColorChangeHeadingLeft(8, 1));
+    assertEquals(5, instance.nextColorChangeHeadingLeft(7, 1));
+    assertEquals(5, instance.nextColorChangeHeadingLeft(6, 1));
+    assertEquals(3, instance.nextColorChangeHeadingLeft(5, 1));
+    assertEquals(3, instance.nextColorChangeHeadingLeft(4, 1));
+    assertEquals(2, instance.nextColorChangeHeadingLeft(3, 1));
+    assertEquals(-1, instance.nextColorChangeHeadingLeft(2, 1));
+    assertEquals(-1, instance.nextColorChangeHeadingLeft(1, 1));
+    
+    // 3-3333333
+    assertEquals(1, instance.nextColorChangeHeadingLeft(2, 2));
+    assertEquals(0, instance.nextColorChangeHeadingLeft(1, 2));
+    assertEquals(-1, instance.nextColorChangeHeadingLeft(0, 2));
+  }
+
+  /**
+   * Test of getStartPosition method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testGetStartPosition()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    instance.setRasteringCutDirectionLeftToRight();
+    assertEquals(new Point(0, 0), instance.getStartPosition(0));
+    
+    instance.toggleRasteringCutDirection();
+    assertEquals(new Point(0, 1), instance.getStartPosition(1));
+  }
+
+  /**
+   * Test of getPowerSpeedFocusPropertyForPixel method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testGetPowerSpeedFocusPropertyForPixel()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    Raster3dPart raster = instance.toRaster3dPart();
+    
+    // --XX-XXX-
+    assertEquals(propertyForPower(0), raster.getPowerSpeedFocusPropertyForPixel(0, 0));
+    assertEquals(propertyForPower(0), raster.getPowerSpeedFocusPropertyForPixel(1, 0));
+    assertEquals(propertyForPower(100), raster.getPowerSpeedFocusPropertyForPixel(2, 0));
+    
+    // 303333333 - 3 = quite dark = 75% power
+    assertEquals(propertyForPower(75), raster.getPowerSpeedFocusPropertyForPixel(0, 2));
+    assertEquals(propertyForPower(0), raster.getPowerSpeedFocusPropertyForPixel(1, 2));
+    assertEquals(propertyForPower(75), raster.getPowerSpeedFocusPropertyForPixel(2, 2));
+  }
+  
+  private FloatPowerSpeedFocusProperty propertyForPower(int power)
+  {
+    FloatPowerSpeedFocusProperty prop = new FloatPowerSpeedFocusProperty();
+    prop.setFocus(0.0f);
+    prop.setSpeed(100.0f);
+    prop.setPower(power);
+    return prop;
+  }
+
+  /**
+   * Test of getPowerSpeedFocusPropertyForColor method, of class RasterizableJobPart.
+   */
+  @Test
+  public void testGetPowerSpeedFocusPropertyForColor()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    Raster3dPart raster = instance.toRaster3dPart();
+    
+    assertEquals(propertyForPower(100), raster.getPowerSpeedFocusPropertyForColor(0));
+    assertEquals(propertyForPower(50), raster.getPowerSpeedFocusPropertyForColor(127));
+    assertEquals(propertyForPower(0), raster.getPowerSpeedFocusPropertyForColor(255));
+  }
+  
+  /**
+   * Test the tricky case of a pixel on the very right edge
+   */
+  @Test
+  public void testPixelOnRightHandEdge()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    instance.setRasteringCutDirectionLeftToRight();
+    int y = 2;
+    int x = 8;
+    // 3-3333333
+    x = instance.nextColorChange(x, y);
+    assertEquals(9, x);
+    
+    boolean done = instance.hasFinishedCuttingLine(x, y);
+    assertEquals(true, done);
+  }
+  
+  /**
+   * Test the tricky case of a pixel on the very left edge
+   */
+  @Test
+  public void testPixelOnLeftHandEdge()
+  {
+    RasterizableJobPartImpl instance = new RasterizableJobPartImpl();
+    
+    instance.setRasteringCutDirectionRightToLeft();
+    int y = 5;
+    int x = 1;
+    // FF0000000
+    x = instance.nextColorChange(x, y);
+    assertEquals(-1, x);
+    
+    boolean done = instance.hasFinishedCuttingLine(x, y);
+    assertEquals(true, done);
+  }
+  
+
+  
+  public class RasterizableJobPartImpl extends RasterizableJobPart
+  {
+    public RasterizableJobPartImpl()
+    {
+      // set image data
+      BufferedImage imageData = new BufferedImage(9, 6, BufferedImage.TYPE_BYTE_GRAY);
+      BufferedImageAdapter image = new BufferedImageAdapter(imageData);
+      
+      // key...
+      // 255 = white = don't laser = -
+      // 0 = black = laser lots! = X
+      
+      // fist row (B&W) => --XX-XXX-
+      image.setGreyScale(0, 0, 255);
+      image.setGreyScale(1, 0, 255);
+      image.setGreyScale(2, 0, 0);
+      image.setGreyScale(3, 0, 0);
+      image.setGreyScale(4, 0, 255);
+      image.setGreyScale(5, 0, 0);
+      image.setGreyScale(6, 0, 0);
+      image.setGreyScale(7, 0, 0);
+      image.setGreyScale(8, 0, 255);
+      // second row (B&W) => ---X--XX-
+      image.setGreyScale(0, 1, 255);
+      image.setGreyScale(1, 1, 255);
+      image.setGreyScale(2, 1, 255);
+      image.setGreyScale(3, 1, 0);
+      image.setGreyScale(4, 1, 255);
+      image.setGreyScale(5, 1, 255);
+      image.setGreyScale(6, 1, 0);
+      image.setGreyScale(7, 1, 0);
+      image.setGreyScale(8, 1, 255);
+      // third row (greyscale) => 3-3333333
+      image.setGreyScale(0, 2, 63);
+      image.setGreyScale(1, 2, 255);
+      image.setGreyScale(2, 2, 63);
+      image.setGreyScale(3, 2, 63);
+      image.setGreyScale(4, 2, 63);
+      image.setGreyScale(5, 2, 63);
+      image.setGreyScale(6, 2, 63);
+      image.setGreyScale(7, 2, 63);
+      image.setGreyScale(8, 2, 63);
+      // fourth row (greyscale) => --7777777
+      image.setGreyScale(0, 3, 255);
+      image.setGreyScale(1, 3, 255);
+      image.setGreyScale(2, 3, 127);
+      image.setGreyScale(3, 3, 127);
+      image.setGreyScale(4, 3, 127);
+      image.setGreyScale(5, 3, 127);
+      image.setGreyScale(6, 3, 127);
+      image.setGreyScale(7, 3, 127);
+      image.setGreyScale(8, 3, 127);
+      // fifth row (blank) => 000000000
+      image.setGreyScale(0, 4, 255);
+      image.setGreyScale(1, 4, 255);
+      image.setGreyScale(2, 4, 255);
+      image.setGreyScale(3, 4, 255);
+      image.setGreyScale(4, 4, 255);
+      image.setGreyScale(5, 4, 255);
+      image.setGreyScale(6, 4, 255);
+      image.setGreyScale(7, 4, 255);
+      image.setGreyScale(8, 4, 255);
+      // sixth row => FF0000000
+      image.setGreyScale(0, 5, 0);
+      image.setGreyScale(1, 5, 0);
+      image.setGreyScale(2, 5, 255);
+      image.setGreyScale(3, 5, 255);
+      image.setGreyScale(4, 5, 255);
+      image.setGreyScale(5, 5, 255);
+      image.setGreyScale(6, 5, 255);
+      image.setGreyScale(7, 5, 255);
+      image.setGreyScale(8, 5, 255);      
+      // set image data
+      this.image = image;
+
+      // set start point
+      this.start = new Point(0, 0);
+    }
+    
+    public Raster3dPart toRaster3dPart()
+    {
+      return new Raster3dPart((GreyscaleRaster) this.image, this.getLaserProperty(), this.start, this.getDPI());
+    }
+    
+    public String getRasteringCutDirection()
+    {
+      return cutDirectionleftToRight
+        ? "left to right"
+        : "right to left";
+    }
+    
+    public void setRasteringCutDirectionLeftToRight() {
+      cutDirectionleftToRight = true;
+    }
+    
+    public void setRasteringCutDirectionRightToLeft() {
+      cutDirectionleftToRight = false;
+    }
+    
+    @Override
+    public int getMaxY()
+    {
+      throw new UnsupportedOperationException("Not supported yet.");
+    }
+    
+    @Override
+    public int getMaxX()
+    {
+      throw new UnsupportedOperationException("Not supported yet.");
+    }
+    
+    @Override
+    public int getMinY()
+    {
+      throw new UnsupportedOperationException("Not supported yet.");
+    }
+    
+    @Override
+    public int getMinX()
+    {
+      throw new UnsupportedOperationException("Not supported yet.");
+    }
+    
+    @Override
+    public LaserProperty getLaserProperty()
+    {
+      FloatPowerSpeedFocusProperty prop = new FloatPowerSpeedFocusProperty();
+      prop.setPower(100.0f);
+      prop.setSpeed(100.0f);
+      prop.setFocus(0.0f);
+      return prop;
+    }
+
+    @Override
+    public FloatPowerSpeedFocusProperty getPowerSpeedFocusPropertyForColor(int color)
+    {
+      throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public double getDPI()
+    {
+      return 72.0;
+    }
+  }
+  
+}