Ender 5 3D Printer to Laser Etcher Conversion

Project Overview

April 16, 2023: This project will be updated in the coming weeks. There is an article on LinkedIn about this project, here: https://www.linkedin.com/pulse/demonstration-power-powershell-lance-fordham

This project shows how to convert an Ender 5 3D printer into a dual-use printer and laser etcher. The code and other files are offered without warranty or guarantee they will work for you. I also don’t offer guidance aside from what is presented here.

Electrical Conversion

The fan signal of the Ender 5 is used as the controlling signal for the laser. In my case, the fan signal is a 24 Vdc PWM signal, which needs to be converted to a 5 Vdc PWM signal. I do this with a simple resistor divider circuit. A PCB was created to facilitate connecting everything. You only need the resistors and the JST connectors. Get the PCB from OSHPark here: https://oshpark.com/shared_projects/3DCg3O8A

PowerShell Code

There are several different ways I could have written the code for generating the images and G-Code, but I wanted to challenge myself by doing everything with PowerShell. With this PowerShell code, I only needed to create images with rectangles and then convert them to G-Code. There is also a converter from G-Code back to an image to see if your G-Code is correct. You may not find the Designer useful if you need something other than rectangles. In that case, you can create your image outside of PowerShell if you make the image dimensions 830 x 830 pixels. The ImageToGCode and GCodeToImage modules should still work for you.

Save the files as named, put them into the same directory, and run the ImageToGCodeForm.ps1 file for the GUI.

Using Module ".\ImageToGCode.psm1";

Using Module ".\GCodeToImage.psm1";

Using Module ".\MouseToImage.psm1";

$DebugPreference = "Continue";
$VerbosePreference = "Continue";

Set-Location $PSScriptRoot;


enum ButtonNames{
    
    BUTTON1 = 1;
    BUTTON2 = 2;
    BUTTON3 = 3;
    BUTTON4 = 4;
    BUTTON5 = 5;
}

enum RadioButtonNames{
    
    RADIO1 = 1;
    RADIO2 = 2;
    RADIO3 = 3;
}

class ImageToGCodeForm{

    #Paths
    [String] $basepath = $PSScriptRoot;
    [String] $canvaspath = "$($this.basepath)\canvas.bmp";
    [String] $inputpath = "$($this.basepath)\canvas.bmp";
    [String] $outputpath = $null;

    #Form Elements
    [System.Windows.Forms.Form] $applicationform;
    [System.Windows.Forms.TabControl] $tabcontrol;
    [System.Windows.Forms.Tabpage[]] $tabs;
    [System.Windows.Forms.TextBox[]] $tab_textboxes;
    [System.Windows.Forms.TextBox[]] $form_textboxes;
    [System.Windows.Forms.Button[]] $form_buttons;
    [System.Windows.Forms.Button[]] $tab_buttons;
    [System.Windows.Forms.RadioButton[]] $tab_radiobuttons;
    [System.Windows.Forms.RadioButton[]] $form_radiobuttons;
    [System.Windows.Forms.GroupBox[]] $form_groupboxes;
    [System.Windows.Forms.ProgressBar[]] $form_progressbars;
    [System.Windows.Forms.PictureBox[]] $tab_pictureboxes;
    [System.Windows.Forms.Label[]] $form_labels;

    #Styling
    [System.Drawing.Font] $font = [System.Drawing.Font]::new("Tahoma",12,[System.Drawing.FontStyle]::Regular);
    [System.Drawing.Size] $buttonsize = [System.Drawing.Size]::new(250,25);
    [System.Drawing.Color] $backcolor = [System.Drawing.Color]::BlanchedAlmond;

    [ImageToGCode] $image2gcode;
    [GCodeToImage] $gcodetoimage;
    [MouseToImage] $mousetoimage;

    [int] $progressStep = 0;

    ImageToGCodeForm(){
    
        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing");

        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms");

        $this.start();
    }

    ImageToGCodeForm([String] $inputpath, [String] $outputpath){
    
        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing");

        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms");

        $this.inputpath = $inputpath;

        $this.outputpath = $outputpath;

        $this.start();
    }

    start(){

        $this.instantiateObjects();

        $this.setupApplicationForm();

        $this.setupTabControl();

        $this.setupTabs();

        $this.setupTextboxes();

        $this.setupButtons();

        $this.setupRadioButtons();

        $this.setupProgressBar();

        $this.setupPictureBoxes();

        $this.setupLabels();

        $this.showForm();

        $this.millerTime();
    }

    millerTime(){

        if($this.tab_pictureboxes -ne $null){
          
            $this.tab_pictureboxes.ForEach({
               
                if($_.BackgroundImage -ne $null){

                    $_.BackgroundImage.Dispose();
                }
                
            });
        }

        if($this.image2gcode -ne $null){

            if($this.image2gcode.image.file -ne $null){

                $this.image2gcode.image.file.Dispose();
            }
        }

        $this.applicationform.Dispose();
    }

    setupApplicationForm(){

        $thisForm = $this;

        $this.applicationform = [System.Windows.Forms.Form]::new();
        $this.applicationform.Name = "MainForm";
        $this.applicationform.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen;
        $this.applicationform.Topmost = $true;
        $this.applicationform.Size = [System.Drawing.Size]::new(1920,1080);
        $this.applicationform.Text = "ImageToGCode";
        $this.applicationform.Font = $this.font;
        $this.applicationform.AutoScale = $true;

    }

    setupTabControl(){

        $this.tabcontrol = [System.Windows.Forms.TabControl]::new();
        $this.tabcontrol.Size = [System.Drawing.Size]::new(1880,850);
        $this.tabcontrol.Location = [System.Drawing.Point]::new(15,60);
        $this.tabcontrol.Font = $this.font;
        $this.applicationform.Controls.Add($this.tabcontrol);
    }

    setupTabs(){

        $this.addTabs(2,$this.tabcontrol.Controls);

        $this.tabs[0].Name = "Tab1";
        $this.tabs[0].Text = "Images";
        $this.tabs[0].DataBindings.DefaultDataSourceUpdateMode = 0;
        $this.tabs[0].UseVisualStyleBackColor = $True;
        $this.tabs[0].BackColor = $this.backcolor;
        $this.tabs[0].Font = $this.font;

        $this.tabs[1].Name = "Tab2";
        $this.tabs[1].Text = "Settings";
        $this.tabs[1].DataBindings.DefaultDataSourceUpdateMode = 0;
        $this.tabs[1].UseVisualStyleBackColor = $True;
        $this.tabs[1].BackColor = $this.backcolor;
        $this.tabs[1].Font = $this.font;
    }

    setupTextboxes(){
    <#
        $this.addTextBoxes(1,$this.tabs[0].Controls);
        $this.addTextBoxes(1,$this.tabs[1].Controls);
        
        $this.tab_textboxes[0].Name = "Textbox1";
        $this.tab_textboxes[0].Text = "Hello";
        $this.tab_textboxes[0].Size = [System.Drawing.Size]::new(200,50);
        $this.tab_textboxes[0].Location = [System.Drawing.Point]::new(15,0);
        $this.tab_textboxes[0].Multiline = $false;
        $this.tab_textboxes[0].Font = $this.font;
        $this.tab_textboxes[0].ReadOnly = $false;

        $this.tab_textboxes[1].Name = "Textbox2";
        $this.tab_textboxes[1].Text = "World";
        $this.tab_textboxes[1].Size = [System.Drawing.Size]::new(200,400);
        $this.tab_textboxes[1].Location = [System.Drawing.Point]::new(15,0);
        $this.tab_textboxes[1].Multiline = $true;
        $this.tab_textboxes[1].Font = $this.font;
        $this.tab_textboxes[1].ReadOnly = $false;
        
        $this.addTextBoxes(2);

        $this.form_textboxes[0].Name = "TextBox3";
        $this.form_textboxes[0].Text = "Hello";
        $this.form_textboxes[0].Size = [System.Drawing.Size]::new(200,50);
        $this.form_textboxes[0].Location = [System.Drawing.Point]::new(15,0);
        $this.form_textboxes[0].Multiline = $false;
        $this.form_textboxes[0].Font = $this.font;
        $this.form_textboxes[0].ReadOnly = $false;

        $this.form_textboxes[1].Name = "TextBox4";
        $this.form_textboxes[1].Text = "World";
        $this.form_textboxes[1].Size = [System.Drawing.Size]::new(200,50);
        $this.form_textboxes[1].Location = [System.Drawing.Point]::new(300,0);
        $this.form_textboxes[1].Multiline = $false;
        $this.form_textboxes[1].Font = $this.font;
        $this.form_textboxes[1].ReadOnly = $false;
      #>
    }

    setupButtons(){
    
        $this.addButtons(5);

        $thisForm = $this;

        $this.form_buttons[0].Name = [ButtonNames]::BUTTON1;
        $this.form_buttons[0].Text = "Designer";
        $this.form_buttons[0].Size = $this.buttonsize;
        $this.form_buttons[0].Location = [System.Drawing.Point]::new(15,30);
        $this.form_buttons[0].Font = $this.font;
        $this.form_buttons[0].Add_Click({ param($sender, $eventArgs) $thisForm.eventHandler($sender, $eventArgs) }.GetNewClosure() );

        $this.form_buttons[1].Name = [ButtonNames]::BUTTON2;
        $this.form_buttons[1].Text = "Open Image";
        $this.form_buttons[1].Size = $this.buttonsize;
        $this.form_buttons[1].Location = [System.Drawing.Point]::new(($this.buttonsize.Width + 10),30);
        $this.form_buttons[1].Font = $this.font;
        $this.form_buttons[1].Add_Click({ param($sender, $eventArgs) $thisForm.eventHandler($sender, $eventArgs) }.GetNewClosure() );

        $this.form_buttons[2].Name = [ButtonNames]::BUTTON3;
        $this.form_buttons[2].Text = "Image to GCode";
        $this.form_buttons[2].Size = $this.buttonsize;
        $this.form_buttons[2].Location = [System.Drawing.Point]::new(($this.buttonsize.Width * 2 + 5),30);
        $this.form_buttons[2].Font = $this.font;
        $this.form_buttons[2].Add_Click({ param($sender, $eventArgs) $thisForm.eventHandler($sender, $eventArgs) }.GetNewClosure() );

        $this.form_buttons[3].Name = [ButtonNames]::BUTTON4;
        $this.form_buttons[3].Text = "GCode to Image";
        $this.form_buttons[3].Size = $this.buttonsize;
        $this.form_buttons[3].Location = [System.Drawing.Point]::new(($this.buttonsize.Width * 3 + 5),30);
        $this.form_buttons[3].Font = $this.font;
        $this.form_buttons[3].Add_Click({ param($sender, $eventArgs) $thisForm.eventHandler($sender, $eventArgs) }.GetNewClosure() );

        $this.form_buttons[4].Name = [ButtonNames]::BUTTON5;
        $this.form_buttons[4].Text = "Change Output";
        $this.form_buttons[4].Size = $this.buttonsize;
        $this.form_buttons[4].Location = [System.Drawing.Point]::new(15,910);
        $this.form_buttons[4].Font = $this.font;
        $this.form_buttons[4].Add_Click({ param($sender, $eventArgs) $thisForm.eventHandler($sender, $eventArgs) }.GetNewClosure() );

    }

    setupRadioButtons(){
    <#
        $thisForm = $this;

        $this.form_groupboxes += [System.Windows.Forms.GroupBox]::new();
        $this.form_groupboxes[0].Size = [System.Drawing.Size]::new(400,50);
        $this.form_groupboxes[0].Location = [System.Drawing.Point]::new(300,50);
        $this.applicationform.Controls.Add($this.form_groupboxes[$this.form_groupboxes.Count - 1]);

        $this.addRadioButtons(1,$this.form_groupboxes[0].Controls);

        $this.form_radiobuttons[0].Name = [RadioButtonNames]::RADIO1;
        $this.form_radiobuttons[0].Text = "Grayscale";
        $this.form_radiobuttons[0].Location = [System.Drawing.Point]::new(15,20);
        $this.form_radiobuttons[0].ForeColor = $this.backcolor;
        $this.form_radiobuttons[0].Checked = $false;
        $this.form_radiobuttons[0].Add_Click({ param($sender, $eventArgs) $thisForm.eventHandler($sender, $eventArgs) }.GetNewClosure() );
        $this.form_radiobuttons[0].Font = $this.font;
    #>
    }

    setupProgressBar(){

        # Progress Bar
        $this.form_progressbars += [System.Windows.Forms.ProgressBar]::new();
        $this.applicationform.Controls.Add($this.form_progressbars[$this.form_progressbars.Count - 1]);
        $this.form_progressbars[0].Location = [System.Drawing.Point]::new(15,950);
        $this.form_progressbars[0].Maximum = 1000
        $this.form_progressbars[0].Minimum = 0
        $this.form_progressbars[0].Size = [System.Drawing.Size]::new(1860,20);
        $this.form_progressbars[0].Step = 1
        $this.form_progressbars[0].Style = [System.Windows.Forms.ProgressBarStyle]::Continuous;
        $this.form_progressbars[0].BackColor = [System.Drawing.Color]::GreenYellow;
        $this.form_progressbars[0].Hide();

    }

    setupPictureBoxes(){

        if(!(Test-Path $this.canvaspath)){

            $this.generateCanvas();
        }

        #Input PictureBox
        $this.tab_pictureboxes += [System.Windows.Forms.PictureBox]::new();
        $this.tab_pictureboxes[0].Location = [System.Drawing.Point]::new(15,15);
        $this.tab_pictureboxes[0].ClientSize = [System.Drawing.Size]::new(831,831);
        $this.tab_pictureboxes[0].BackColor = [System.Drawing.Color]::CadetBlue;
        $this.tab_pictureboxes[0].SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom;
        $this.tab_pictureboxes[0].BackgroundImageLayout = [System.Windows.Forms.ImageLayout]::Center;

        if(Test-Path $this.inputpath){

            $this.tab_pictureboxes[0].BackgroundImage = [System.Drawing.Bitmap]::FromFile($this.inputpath);

        }else{

            $this.generateCanvas();
        }

        $this.tabs[0].Controls.Add($this.tab_pictureboxes[0]);

        #Output PictureBox
        $this.tab_pictureboxes += [System.Windows.Forms.PictureBox]::new();
        $this.tab_pictureboxes[1].Location = [System.Drawing.Point]::new(980,15);
        $this.tab_pictureboxes[1].ClientSize = [System.Drawing.Size]::new(831,831);
        $this.tab_pictureboxes[1].BackColor = [System.Drawing.Color]::CadetBlue;
        $this.tab_pictureboxes[1].SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom;
        $this.tab_pictureboxes[1].BackgroundImageLayout = [System.Windows.Forms.ImageLayout]::Center;

        if($this.outputpath){

            $this.tab_pictureboxes[1].BackgroundImage = [System.Drawing.Bitmap]::FromFile($this.outputpath);

        }else{

            $this.generateCanvas();
        }

        $this.tabs[0].Controls.Add($this.tab_pictureboxes[1]);
    }

    setupLabels(){

        $this.form_labels += [System.Windows.Forms.Label]::new();
        $this.form_labels[0].Location = [System.Drawing.Point]::new(275,912);
        $this.form_labels[0].Size = [System.Drawing.Size]::new(920,20);
        $this.form_labels[0].ForeColor = [System.Drawing.Color]::DarkBlue;
        $this.form_labels[0].Text = "Output saved to $($this.outputpath)";
        $this.applicationform.Controls.Add($this.form_labels[0]);
    }

    addTabs([int] $qty, [System.Windows.Forms.TabControl+ControlCollection] $attachto){

        1..$qty | %{
             
             $this.tabs += [System.Windows.Forms.Tabpage]::new();

             $attachto.Add($this.tabs[$this.tabs.Count - 1]);
        }
    }

    addTextBoxes([int] $qty, [System.Windows.Forms.TabPage+TabPageControlCollection] $attachto){

        1..$qty | %{

            $this.tab_textboxes += [System.Windows.Forms.TextBox]::new();

            $attachto.Add($this.tab_textboxes[$this.tab_textboxes.Count - 1]);
        }
    }

    addTextBoxes([int] $qty){

        1..$qty | %{

            $this.form_textboxes += [System.Windows.Forms.TextBox]::new();

            $this.applicationform.Controls.Add($this.form_textboxes[$this.form_textboxes.Count - 1]);
        }
    }

    addButtons([int] $qty,[System.Windows.Forms.TabPage+TabPageControlCollection] $attachto){

        1..$qty | %{
            
            $this.tab_buttons += [System.Windows.Forms.Button]::new();

            $attachto.Add($this.tab_buttons[$this.tab_buttons.Count - 1]);
        }
    }

    addButtons([int] $qty){

        1..$qty | %{

            $this.form_buttons += [System.Windows.Forms.Button]::new();

            $this.applicationform.Controls.Add($this.form_buttons[$this.form_buttons.Count - 1]);
        }
    }

    addRadioButtons([int] $qty,[System.Windows.Forms.TabPage+TabPageControlCollection] $attachto){

        1..$qty | %{
            
            $this.tab_radiobuttons += [System.Windows.Forms.RadioButton]::new();

            $attachto.Add($this.tab_radiobuttons[$this.tab_radiobuttons.Count - 1]);
        }        
    }

    addRadioButtons([int] $qty, $attachto){

        1..$qty | %{

            $this.form_radiobuttons += [System.Windows.Forms.RadioButton]::new();

            $attachto.Add($this.form_radiobuttons[$this.form_radiobuttons.Count - 1]);
        }
    }

    openDesigner(){

        Write-Debug "Opening designer";
        
        $this.mousetoimage = [MouseToImage]::new();

        $this.mousetoimage.start();

        $this.tab_pictureboxes[0].BackgroundImage = [System.Drawing.Bitmap]::FromFile($this.mousetoimage.outputpath);

        $this.inputpath = $this.mousetoimage.outputpath;

        if($this.image2gcode -ne $null){

            $this.image2gcode.inputpath = $this.inputpath;
        }
    }

    getInputPath(){

        if($this.image2gcode.selectFile()){

            Write-Debug "Path: $($this.image2gcode.image.path)";

            $this.inputpath = "$($this.image2gcode.image.path)\$($this.image2gcode.image.filename)";

            $this.tab_pictureboxes[0].BackgroundImage = $this.image2gcode.image.file;
        }
    }

    getOutputPath(){

        if($this.image2gcode.image.path -eq $null){
       
            if($this.image2gcode.selectFile()){

                Write-Debug "Path: $($this.image2gcode.image.path)";

            }
        }

        $this.image2gcode.outputpath = "$($this.image2gcode.image.path)\$($this.image2gcode.image.filenamenoext).gcode";

        $this.outputpath = "$($this.image2gcode.image.path)\$($this.image2gcode.image.filenamenoext).bmp";

        $this.gcodetoimage.outputpath = $this.outputpath;

        $this.mousetoimage.outputpath = $this.outputpath;

        $this.form_labels[0].Text = "Output saved to $($this.outputpath)";

    }

    scanImage(){

        $this.form_progressbars[0].Show();

        if($this.image2gcode.processsettings.scandirection -eq [ScanDirection]::X){

            Write-Verbose "Scanning direction is X";

            $this.form_progressbars[0].Maximum = $this.image2gcode.image.file.Height - 1;

            for($y = 0; $y -lt $this.image2gcode.image.file.Height; $y++){

                $this.form_progressbars[0].PerformStep();

                Write-Host "Row $($y) of $($this.image2gcode.image.file.Height)";

                for($x = 0; $x -lt $this.image2gcode.image.file.Width; $x++){

                    $this.image2gcode.processPixel($x, $y);
                }

                $this.image2gcode.processsettings.scanlines += 1;
            }


        }else{

            Write-Verbose "Scanning direction is Y";

            $this.form_progressbars[0].Maximum = $this.image2gcode.image.file.Width - 1;

            for($x = 0; $x -lt $this.image2gcode.image.file.Width; $x++){

                $this.form_progressbars[0].PerformStep();

                Write-Host "Row $($x) of $($this.image2gcode.image.file.Width)";

                for($y = 0; $y -lt $this.image2gcode.image.file.Height; $y++){

                    $this.image2gcode.processPixel($x, $y);
                }

                $this.image2gcode.processsettings.scanlines += 1;
            }
        }

        $this.form_progressbars[0].Value = 0;

        $this.form_progressbars[0].Hide();
    }

    runImageToGCode(){

        $this.image2gcode.getImageMetadata();

        Write-Debug "Done with image metadata";

        $this.image2gcode.initializeOffsets();

        Write-Debug "Done with initialize offsets";

        $this.image2gcode.initializeGCode();

        Write-Debug "Done with initialize gcode";

        $this.scanImage();
        
        Write-Debug "Done scanning image";
        
        $this.image2gcode.finalizeGCode();

        Write-Debug "Done finalizing GCode";

        $this.image2gcode.millerTime();

        Write-Debug "Miller time!";
       
    }

    runGCodeToImage(){

        if(!$this.gcodetoimage.inputpath){
			
			$this.gcodetoimage.getInputFile();
		}
		
		$this.gcodetoimage.getDPI();

        $this.gcodetoimage.getScreenScaleFactor();

        $this.gcodetoimage.dpmm = ($this.gcodetoimage.dpi * $this.gcodetoimage.screenscalefactor) / 25.4;

        Write-Verbose "dpi: $($this.gcodetoimage.dpi)";

        Write-Verbose "dpmm: $($this.gcodetoimage.dpmm)";

        $this.gcodetoimage.bedx = [Math]::Round((220 * $this.gcodetoimage.dpmm)) + 1;

        $this.gcodetoimage.bedy = [Math]::Round((220 * $this.gcodetoimage.dpmm)) + 1;

        Write-Verbose "bedx: $($this.gcodetoimage.bedx)";

        Write-Verbose "bedy: $($this.gcodetoimage.bedy)";

        $this.gcodetoimage.bitmap = [System.Drawing.Bitmap]::new($this.gcodetoimage.bedx, $this.gcodetoimage.bedy);

        $this.gcodetoimage.graphics = [System.Drawing.Graphics]::FromImage($this.gcodetoimage.bitmap);

        $this.gcodetoimage.graphics.Clear([System.Drawing.Color]::White);

        $this.gcodetoimage.regex = [Regex]::new($this.gcodetoimage.filter);

        $this.gcodetoimage.getGCodeFromFile();

        Write-Host $this.gcodetoimage.gcode;

        $this.parseGCode();

        $this.gcodetoimage.saveFile();

        $this.gcodetoimage.millerTime();

        $this.tab_pictureboxes[1].BackgroundImage = [System.Drawing.Bitmap]::FromFile($this.gcodetoimage.outputpath);
        
    }

    parseGCode(){

        $this.form_progressbars[0].Show();

        $this.gcodetoimage.matches = $this.gcodetoimage.regex.Matches($this.gcodetoimage.gcode);

        Write-Debug "Matches: $($this.gcodetoimage.matches)";

        $this.form_progressbars[0].Maximum = $this.gcodetoimage.matches.Count - 1;

        Write-Verbose "Progress maximum: $($this.form_progressbars[0].Maximum)";

        for($i = 0; $i -lt $this.gcodetoimage.matches.Count; $i+=2){

            $this.form_progressbars[0].PerformStep();

            $this.gcodetoimage.xpixel = (([int]([String] $this.gcodetoimage.matches[$i]).Replace("X",""))) * $this.gcodetoimage.dpmm;

            $this.gcodetoimage.ypixel = (([int]([String] $this.gcodetoimage.matches[$i + 1]).Replace("Y",""))) * $this.gcodetoimage.dpmm;

            if(($this.gcodetoimage.xpixel -gt $this.gcodetoimage.bedx) -or ($this.gcodetoimage.xpixel -lt 0)){

                $this.gcodetoimage.xpixel = $this.gcodetoimage.bedx;
            }

            if(($this.gcodetoimage.ypixel -gt $this.gcodetoimage.bedy) -or ($this.gcodetoimage.ypixel -lt 0)){

                $this.gcodetoimage.ypixel = $this.gcodetoimage.bedy;
            }

            Write-Verbose "x: $($this.gcodetoimage.xpixel)";

            Write-Verbose "y: $($this.gcodetoimage.ypixel)";

            $this.gcodetoimage.bitmap.SetPixel($this.gcodetoimage.xPixel, $this.gcodetoimage.yPixel, [System.Drawing.Color]::FromArgb($this.gcodetoimage.red, $this.gcodetoimage.green, $this.gcodetoimage.blue));
        }

        $this.form_progressbars[0].Value = 0;

        $this.form_progressbars[0].Hide();
    }

    generateCanvas(){

        if(!(Test-Path $this.canvaspath)){

            $this.inputpath = $this.canvaspath;

            [System.Drawing.SolidBrush] $bisquebrush = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::Bisque);

            [System.Drawing.Pen] $bisquepen = [System.Drawing.Pen]::new([System.Drawing.Color]::Bisque);

            [System.Drawing.Bitmap] $bmp = [System.Drawing.Bitmap]::new(831, 831);

            [System.Drawing.Graphics] $g = [System.Drawing.Graphics]::FromImage($bmp);

            [System.Drawing.Rectangle] $rect = [System.Drawing.Rectangle]::new(0,0,$bmp.Width,$bmp.Height);

            $g.FillRectangles($bisquebrush, $rect);

            $g.DrawRectangle($bisquepen, $rect);

            $bmp.Save($this.canvaspath, [System.Drawing.Imaging.ImageFormat]::Bmp);

            $g.Dispose();

            $bmp.Dispose();

        }
    }

    instantiateObjects(){

        if($this.image2gcode -eq $null){

            $this.image2gcode = [ImageToGCode]::new();
        }

        if($this.gcodetoimage -eq $null){

            $this.gcodetoimage = [GCodeToImage]::new();
        }

        if($this.mousetoimage -eq $null){

            $this.mousetoimage = [MouseToImage]::new();
        }
    }

    eventHandler([object] $sender, [System.EventArgs] $eventArgs) {

        if($sender){

            Write-Debug ($sender.Name);

            $args = ($eventArgs | ConvertTo-Json -Depth 100);

            switch($sender.Name){
        
                ([ButtonNames]::BUTTON1){

                    #$this.getOutputPath();

                    $this.openDesigner();
                }

                ([ButtonNames]::BUTTON2){

                     $this.getInputPath();
                }

                ([ButtonNames]::BUTTON3){

                    $this.getOutputPath();

                    $this.runImageToGCode();
                    
                }

                ([ButtonNames]::BUTTON4){

                    $this.runGCodeToImage();
                }

                ([ButtonNames]::BUTTON5){

                    $this.getOutputPath();

                    $this.form_labels[0].Text = "Output saved to $($this.outputpath)";
                }

                ([RadioButtonNames]::RADIO1){

                    [System.Windows.Forms.MessageBox]::Show("Hello $($sender.Name).`n`n$($args)" , $sender.Name);
                }

                ("MainFormClosing"){

                   $this.MainForm_FormClosing($sender,$eventArgs);

                }

                default{

                    [System.Windows.Forms.MessageBox]::Show("Hello $($sender.Name).`n`n$($args)" , "Default");
                }
            }
        }
   }

    showForm(){

        [System.Windows.Forms.Application]::Run($this.applicationform);

        #[void] $this.applicationform.ShowDialog();
    }
   
}

$itgcf = [ImageToGCodeForm]::new();
Set-Location $PSScriptRoot;

class MouseToImage{
    
    [String] $basepath = $PSScriptRoot;
	[String] $canvaspath = "$($this.basepath)\canvas.bmp";
    [String] $inputpath = "$($this.basepath)\canvas.bmp";
	[String] $outputpath = $null;
    [System.Windows.Forms.Form] $applicationform;
    [System.Drawing.Point] $lastPoint = [System.Drawing.Point]::Empty;
    [System.Drawing.Point] $startPoint = [System.Drawing.Point]::Empty;
    [bool] $isMouseDown = $false; 
    [System.Windows.Forms.PictureBox] $pictureBox1;
    [System.Drawing.Rectangle[]] $rectangles;
    [System.Drawing.Font] $font = [System.Drawing.Font]::new("Tahoma",8,[System.Drawing.FontStyle]::Regular);
    [System.Windows.Forms.Button[]] $form_buttons;
    [bool] $drawlines = $true;
    [int] $dpi = 96;
    [int] $numcellsx = 220;
    [int] $numcellsy = 220;
    [int] $laseroffsetx = -12;
    [int] $laseroffsety = 55;
    [double] $dpmm;
    [int] $cellsize;
    [int] $cellcount = 10;
    [double] $screenscalefactor = 1;
    [System.Drawing.Pen] $blackpen = [System.Drawing.Pen]::new([System.Drawing.Color]::Black);
    [System.Drawing.Pen] $whitepen = [System.Drawing.Pen]::new([System.Drawing.Color]::White);
    [System.Drawing.Pen] $redpen = [System.Drawing.Pen]::new([System.Drawing.Color]::Red);
    [System.Drawing.SolidBrush] $blackbrush = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::Black);
    [System.Drawing.SolidBrush] $whitebrush = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::White);

    MouseToImage($path){

        $this.inputpath = $path; 

        $this.start();
    }

    MouseToImage(){
		
		$this.inputpath = $this.canvaspath;
    }

    start(){

        $thisForm = $this;

        $this.getScreenScaleFactor();

        #$this.dpi = $this.getDPI();

        Write-Debug "This.dpi: $($this.dpi)";

        Write-Debug "This screenscalefactor: $($this.screenscalefactor)";

        $this.dpmm = ($this.dpi * $this.screenscalefactor) / 25.4;

        Write-Debug "This.dpmm: $($this.dpmm)";

        $this.cellsize = $this.round($this.cellcount * $this.dpmm);

        Write-Debug "cellsize: $($this.cellsize)";

        $this.applicationform = [System.Windows.Forms.Form]::new();
        $this.applicationform.Name = "MainForm";
        $this.applicationform.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen;
        $this.applicationform.Topmost = $true;
        $this.applicationform.Size = [System.Drawing.Size]::new(1000,1000);
        $this.applicationform.Text = "ImageToGCode";
        $this.applicationform.AutoScale = $true;
        $this.applicationform.Add_FormClosing({ param($sender, $FormClosingEventArgs) $thisForm.MainForm_FormClosing(@{Name = "MainFormClosing"}, $FormClosingEventArgs) }.GetNewClosure());

        $this.pictureBox1 = [System.Windows.Forms.PictureBox]::new();
        $this.pictureBox1.Location = [System.Drawing.Point]::new(15,15);
        $this.pictureBox1.ClientSize = [System.Drawing.Size]::new([Math]::Round(($this.numcellsx * $this.dpmm)),[Math]::Round(($this.numcellsy * $this.dpmm)));
        $this.pictureBox1.BackColor = [System.Drawing.Color]::CadetBlue;
        $this.pictureBox1.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom;
        $this.pictureBox1.BackgroundImageLayout = [System.Windows.Forms.ImageLayout]::Center;

        if(!(Test-Path $this.inputpath)){

            $this.generateBitmap();
        }

        $this.pictureBox1.BackgroundImage = [System.Drawing.Bitmap]::FromFile($this.inputpath);
        #$this.pictureBox1.ClientSize = $this.pictureBox1.BackgroundImage.Size;
        $this.pictureBox1.Add_Click({ param($sender, $eventArgs) $thisForm.clearButton_Click(@{Name = "Picturebox1Click"}, $eventArgs) }.GetNewClosure() );
        $this.pictureBox1.Add_MouseDown({ param($sender, $eventArgs) $thisForm.pictureBox1_MouseDown(@{Name="PictureBox0_MouseDown"}, $eventArgs) }.GetNewClosure() );
        $this.pictureBox1.Add_MouseUp({ param($sender, $eventArgs) $thisForm.pictureBox1_MouseUp(@{Name="PictureBox0_MouseUp"}, $eventArgs) }.GetNewClosure() );
        $this.pictureBox1.Add_MouseMove({ param($sender, $eventArgs) $thisForm.pictureBox1_MouseMove(@{Name="PictureBox0_MouseMove"}, $eventArgs) }.GetNewClosure() );
        $this.pictureBox1.Add_Paint({ param($sender, $PaintEventArgs) $thisForm.pictureBox1_Paint(@{Name="PictureBox0_Paint"}, $PaintEventArgs) }.GetNewClosure() );

        $this.applicationform.Controls.Add($this.pictureBox1);

        $this.form_buttons += [System.Windows.Forms.Button]::new();
        $this.applicationform.Controls.Add($this.form_buttons[$this.form_buttons.Count - 1]);
        $this.form_buttons[0].Name = "clearButton";
        $this.form_buttons[0].Text = "Clear";
        $this.form_buttons[0].Size = [System.Drawing.Size]::new(100,25);
        $this.form_buttons[0].Location = [System.Drawing.Point]::new(30,900);
        $this.form_buttons[0].Font = $this.font;
        $this.form_buttons[0].Add_Click({ param($sender, $eventArgs) $thisForm.clearButton_Click(@{Name = "clearButton"}, $eventArgs) }.GetNewClosure() );

        $this.form_buttons += [System.Windows.Forms.Button]::new();
        $this.applicationform.Controls.Add($this.form_buttons[$this.form_buttons.Count - 1]);
        $this.form_buttons[1].Name = "saveButton";
        $this.form_buttons[1].Text = "Save";
        $this.form_buttons[1].Size = [System.Drawing.Size]::new(100,25);
        $this.form_buttons[1].Location = [System.Drawing.Point]::new(130,900);
        $this.form_buttons[1].Font = $this.font;
        $this.form_buttons[1].Add_Click({ param($sender, $eventArgs) $thisForm.Save_Click(@{Name = "saveButton"}, $eventArgs) }.GetNewClosure() );
		
		$this.showForm();
    }

    showForm(){

        #[System.Windows.Forms.Application]::Run($this.applicationform);

        [void] $this.applicationform.ShowDialog();

        $this.applicationform.Invalidate();
    }
	
	millerTime(){

        if($this.pictureBox1 -ne $null){

            #Write-Debug "Disposing";

            $this.pictureBox1.BackgroundImage.Dispose();
        }

        $this.applicationform.Dispose();
		
		$this.applicationform.Dispose();
    }

    generateBitmap(){

        $this.inputpath = $this.canvaspath;

        [System.Drawing.Bitmap] $bmp = [System.Drawing.Bitmap]::new($this.pictureBox1.Width, $this.pictureBox1.Height);

        [System.Drawing.Graphics] $g = [System.Drawing.Graphics]::FromImage($bmp);

        [System.Drawing.Rectangle] $rect = [System.Drawing.Rectangle]::new(0,0,$bmp.Width,$bmp.Height);

        $g.FillRectangles($this.whitebrush, $rect);

        $g.DrawRectangle($this.whitepen, $rect);

        $bmp.Save($this.canvaspath, [System.Drawing.Imaging.ImageFormat]::Bmp);

        $g.Dispose();

        $bmp.Dispose();

    }

    Save_Click([Object] $sender, [System.Windows.Forms.MouseEventArgs] $m ){

        Write-Debug "Saving";

        if($sender.Name -eq "saveButton"){

            #$this.drawlines = $false;
       
            [System.Drawing.Bitmap] $bmp = [System.Drawing.Bitmap]::new($this.pictureBox1.Width, $this.pictureBox1.Height);

            [System.Drawing.Graphics] $g = [System.Drawing.Graphics]::FromImage($bmp);

            [System.Drawing.Rectangle] $rect = [System.Drawing.Rectangle]::new(0,0,$bmp.Width,$bmp.Height);

            $g.FillRectangles($this.whitebrush, $rect);

            $g.DrawRectangle($this.whitepen, $rect);

            if($this.rectangles.Count -gt 0){

                $g.FillRectangles($this.blackbrush, $this.rectangles);
            
                $g.DrawRectangles($this.blackpen, $this.rectangles);
            }
			
			if(!$this.outputpath){
				
				$this.getOutputFile();				
			}
			
			Write-Debug "Output Path: $($this.outputpath)";

            $bmp.Save($this.outputpath, [System.Drawing.Imaging.ImageFormat]::Bmp);

            $g.Dispose();

            $bmp.Dispose();

            $this.drawlines = $true;
			
			$this.millerTime();
        }
    }
	
	getOutputFile(){
		
		$FileBrowser = New-Object System.Windows.Forms.SaveFileDialog -Property @{
 
			InitialDirectory = [Environment]::GetFolderPath('MyComputer');
     
            Filter = 'Image Files (*.jpg,*.jpeg,*.png,*.tif,*.tiff,*.bmp)|*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp';
			
			CheckFileExists = $false;

            CheckPathExists = $false;

            ValidateNames = $false;
		}

		$null = $FileBrowser.ShowDialog();

		if($($FileBrowser.FileName)){

            $this.outputpath = $FileBrowser.FileName;
		
		}else{
			
			Write-Host "No file selected. Cancelling.";

			break;
		}  
	}

    [System.Drawing.Rectangle] getNewRectangle(){
        
        Write-Debug "New Rectangle";

        return  [System.Drawing.Rectangle]::new(
                [Math]::Min($this.lastPoint.X, $this.startPoint.X),
                [Math]::Min($this.lastPoint.Y, $this.startPoint.Y),
                [Math]::Abs($this.lastPoint.X - $this.startPoint.X),
                [Math]::Abs($this.lastPoint.Y - $this.startPoint.Y));
    }

    [double] round([double] $pre){

        return ([Math]::Round($pre,2));
    }

    pictureBox1_Paint([object] $sender, [System.Windows.Forms.PaintEventArgs] $e){

        [System.Drawing.Graphics] $g = $e.Graphics;

        if($this.drawlines){

            for ([int] $y = 0; $y -lt $this.numcellsy; ++$y){

                $g.DrawLine($this.blackpen, 0, $this.round($y * $this.cellsize), $this.round($this.numcellsy * $this.cellcount), $this.round($y * $this.cellsize));

                $g.DrawString(($this.round(($y * $this.cellsize) / $this.dpmm)).ToString(),$this.font,[System.Drawing.SolidBrush]::new([System.Drawing.Color]::Black),0,$this.round($y * $this.cellsize));
            }

            for ([int] $x = 0; $x -lt $this.numcellsx; ++$x){

                $g.DrawLine($this.blackpen, $x * $this.cellsize, 0, $x * $this.cellsize, $this.numcellsx * $this.cellcount);

                $g.DrawString(($this.round(($x * $this.cellsize) / $this.dpmm)).ToString(),$this.font,[System.Drawing.SolidBrush]::new([System.Drawing.Color]::Black),$this.round($x * $this.cellsize),0);
            }

            if($this.laseroffsetx -lt 0){

                $xmax = [Math]::Round(($this.numcellsx + $this.laseroffsetx) * $this.dpmm);

                #Write-Debug "xmax: $($xmax) ";

                $g.DrawLine($this.redpen, $this.round($xmax), 0, $this.round($xmax),$this.numcellsx * $this.cellcount);

            }else{

                $g.DrawLine($this.redpen, $this.round($this.laseroffsetx * $this.dpmm), 0, $this.round($this.laseroffsetx * $this.dpmm),$this.numcellsx * $this.cellcount);
            }

            $g.DrawLine($this.redpen, 0, $this.round($this.laseroffsety * $this.dpmm), $this.numcellsy * $this.cellcount, $this.round($this.laseroffsety * $this.dpmm));

        }else{

            $g.Clear([System.Drawing.Color]::White);
        }

        if ($this.rectangles.Count -gt 0){

            #Write-Debug "Painting permanent rectangle";

            $g.FillRectangles($this.blackbrush, $this.rectangles);
            
            $g.DrawRectangles($this.blackpen, $this.rectangles);

            $this.pictureBox1.Invalidate();
        }


        if(($this.isMouseDown)){

            #Write-Debug "Painting temporary rectangle";

            [System.Drawing.Rectangle] $rect = $this.getNewRectangle();

            $g.DrawRectangle($this.redpen, $rect);
        }

         $this.pictureBox1.Invalidate();

    }

    pictureBox1_MouseMove([object] $sender, [System.Windows.Forms.MouseEventArgs] $m){

        #Write-Debug "Move";
      
        $this.lastPoint = $m.Location;    
    }  
      
    pictureBox1_MouseDown([object] $sender, [System.Windows.Forms.MouseEventArgs] $m){
        
        #Write-Debug "Mouse down";

        $this.startPoint = $m.Location;

        $this.lastPoint = $m.Location;

        $this.isMouseDown = $true;

    }

    pictureBox1_MouseUp([object] $sender, [System.Windows.Forms.MouseEventArgs] $e){

        #Write-Debug "Mouse Up";

        if($this.isMouseDown){

            $this.isMouseDown = $false;

            [System.Drawing.Rectangle] $rect = $this.getNewRectangle();

            if (($rect.Width -gt 0) -and $rect.Height -gt 0){

                #Write-Debug "Rectangle added";

                $this.rectangles += $rect;
            }

            $this.lastPoint = [System.Drawing.Point]::Empty;

            $this.startPoint = [System.Drawing.Point]::Empty;

            Write-Debug "Rectangle Count: $($this.rectangles.Count)";
        }
    }

    clearButton_Click([object] $sender, [EventArgs] $e){

        Write-Debug ($sender | ConvertTo-Json);

        if (($sender.Name -eq "clearButton") -and $this.rectangles.Count -gt 0){

            Write-Debug "Clearing";

            $this.rectangles = $null

            $this.pictureBox1.Invalidate();
        }
    }

    getScreenScaleFactor(){

        $rh=[int](Get-CimInstance -ClassName Win32_VideoController)[0].CurrentVerticalResolution;

        $vh=[int][System.Windows.Forms.SystemInformation]::VirtualScreen.Height;

        Write-Debug "rh: $($rh)`nvh: $($vh)";

        if(($rh -eq 0) -or $vh -eq 0){

            $this.screenscalefactor = 1;

        }else{

            $this.screenscalefactor = $rh / $vh;
        }
    }

    getDPI(){

        $this.dpi = (Get-ItemProperty -Path "HKCU:\Control Panel\Desktop\").LogPixels;
		
		if($this.dpi -eq 0){
			
			$this.dpi = (Get-ItemProperty -path "HKCU:\Control Panel\Desktop\WindowMetrics").AppliedDPI;	
		}
		
		if($this.dpi -eq 0){
			
			$this.dpi = (Get-ItemProperty -path "HKCU:\Control Panel\Desktop\PerMonitorSettings\*").DpiValue;
		}	

        if($this.dpi -eq 0){

            $this.dpi = 96;
        }
		
		Write-Debug "getDPI: $($this.dpi)";
    }

    MainForm_FormClosing([object] $sender, [System.Windows.Forms.FormClosingEventArgs] $f){

        #[System.Windows.Forms.MessageBox]::Show("Hello $($sender.Name).`n`n$($e)" , "Default");

        $this.millerTime();
    }
}
Set-Location $PSScriptRoot;

enum MCode{

    M106 = 106; #Laser Power
    M420 = 420; #Bed Leveling
}

enum GCode{

    G0 = 0; #Non-etch move
    G1 = 1; #Etch Move
    G4 = 4; #Wait S seconds or P milliseconds
    G21 = 21; #Set to mm
    G90 = 90; #Absolute positioning
    G91 = 91; #Relative positioning
}

enum ScanDirection{

    X = 0;
    Y = 1;
}

enum MirrorAxis{

    OFF = 0;
    X = 1;
    Y = 2;
}

enum Origin{

    TopLeft = 0;
    TopRight = 1;
    BottomLeft = 2;
    BottomRight = 3;
    Center = 4;
}

class Position{ #A position such as a pixel x and y coordinate

    [double] $x = 0;
    [double] $y = 0;

    Position([double] $x, [double] $y){

        $this.x = $x;
        $this.y = $y;
    }
}

class ImagePixel{

    [System.Drawing.Color] $color;
    [Position] $imageposition;
    [double] $grey;
    [byte] $negativered;
    [byte] $negativegreen;
    [byte] $negativeblue;

    ImagePixel([int] $x, [int] $y, [System.Drawing.Color] $color){

        $this.imageposition = [Position]::new($x,$y);

        $this.color = $color;
    }

    convertToGrey(){

        $this.grey = ($this.color.R * 0.2126) + ($this.color.G * 0.7152) + ($this.color.B * 0.0722)  # Luminosity Method
    }

    convertToNegative(){

        $this.negativered = 255 - $this.color.R;

        $this.negativegreen = 255 - $this.color.G;

        $this.negativeblue = 255 - $this.color.b;
    }
}

class EtcherSettings{

    [int] $spotsizex; #microns
    [int] $spotsizey;
    [int] $bedsizex; #mm
    [int] $bedsizey;
    [byte] $thresholdpower;
    [double] $laseroffsetx;
    [double] $laseroffsety;
    [double] $defaultfocus; #mm Distance from laser aperture to focus point
    [bool] $levelingenabled;
    [double] $h_pixelsperdot;
    [double] $v_pixelsperdot;

    EtcherSettings(){

        $this.spotsizex = 100;
        $this.spotsizey = 100;
        $this.bedsizex = 220;
        $this.bedsizey = 220;
        $this.thresholdpower = 5;
        $this.laseroffsetx = -12;
        $this.laseroffsety = 55;
        $this.defaultfocus = 42.18;
        $this.levelingenabled = $true;
    }     
}

class ProcessSettings{

    [byte] $minlaserpower;
    [byte] $maxlaserpower;
    [byte] $previouspower;
    [byte] $whitethreshold;
    [int] $skipspeed; #mm/sec
    [int] $etchspeed; #mm/sec
    [int] $fillspacing; #How much, in percent, to retard the move to the next line (think overlap when mowing the lawn)
    [bool] $invertcolor; #Make black white and white black?
    [bool] $mirrorimage; #Switch the direction of the pixels
    [MirrorAxis] $mirroron; #the axis on which to flip the image (requires $mirrorimage to be true)
    [Origin] $imageorigin; #The start position to analyze the image
    [Origin] $etcherorigin; #The start position, or 0,0 point, of the printer/etcher
    [ScanDirection] $scandirection; #Which way to etch the sample?
    [int] $cycles; #How many times to go over the same area
    [int] $scanlines; #The number of scan lines to process the image
    [int] $timeestimate; #An estimate of how long it's going to take to etch the image
    [String] $gcode;
    
    
    ProcessSettings(){
        
        $this.maxlaserpower = 255;
        $this.minlaserpower = 127;
        $this.previouspower = 0;
        $this.whitethreshold = 230;
        $this.skipspeed = 10;
        $this.etchspeed = 10;
        $this.invertcolor = $false;
        $this.mirrorimage = $false;
        $this.mirroron = [MirrorAxis]::OFF;
        $this.imageorigin = [Origin]::TopLeft; #I think all image handling software considers top-left to be origin
        $this.etcherorigin = [Origin]::BottomLeft;
        $this.scandirection = [ScanDirection]::X;
        $this.cycles = 1; 
        $this.fillspacing = 50;
        $this.scanlines = 0;
        $this.timeestimate = 0;
        $this.gcode = "";
    }
}

class EtchImage{
    
    [String] $path;
    [String] $filename;
    [String] $filenamenoext;
    [String] $extension;
    [System.Drawing.Bitmap] $file;
    [ImagePixel] $firstpixel; #the first pixel that's not white (or black if inverted)
    [ImagePixel] $lastpixel; #the last pixel that's not white (or black if inverted)
    [int] $etchpixels; #the number of image pixels that are over the threshold
    [double] $horizontal_dpmm;
    [double] $vertical_dpmm;
    [double] $totalpixels;
    
    EtchImage(){

    }
}

class ImageToGCode{

    [EtcherSettings] $etchersettings;
    [ProcessSettings] $processsettings;
    [EtchImage] $image; #The image to be processed
    [Position] $offsets;
    [String] $basepath = $PSScriptRoot;
	[String] $inputpath = $null;
	[String] $outputpath = $null;
    
    ImageToGCode(){

        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing");
         
        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms");

        $this.etchersettings = [EtcherSettings]::new();

        $this.processsettings = [ProcessSettings]::new();

        $this.image = [EtchImage]::new();

        $this.offsets = [Position]::new(0,0);

    }

    ImageToGCode([String] $path){

        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing");
         
        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms");

        $this.etchersettings = [EtcherSettings]::new();

        $this.processsettings = [ProcessSettings]::new();

        $this.image = [EtchImage]::new();

        $this.offsets = [Position]::new(0,0);

        $this.inputpath = $path;

        $this.getFileFromPath();

        $this.start();
    }

    start(){

        $this.processImage();
    }

    millerTime(){
        #Thank you, Ron

        #$this.image.file.Dispose();

        $this.outputpath = "$($this.image.path)\$($this.image.filenamenoext).gcode";

        $this.processsettings.gcode | Out-File -FilePath $this.outputpath;

        [System.IO.File]::WriteAllLines($this.outputpath, $this.processsettings.gcode);

        Write-Host "File saved to $($this.outputpath)";

    }

    processImage(){

        if($this.inputpath -eq $null){

            $this.selectFile()
        }

        if($this.image){

            Write-Debug "Image is okay";

            $this.getImageMetadata();

            $this.initializeOffsets();

            $this.initializeGCode();

            $this.scanImage();
            
            $this.finalizeGCode();

            $this.millerTime();
        }
    }

    [bool] selectFile() {

        $VerbosePreference = "Continue";

	    $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
 
            InitialDirectory = [Environment]::GetFolderPath('MyComputer');
     
            Filter = 'Image Files (*.jpg,*.jpeg,*.png,*.tif,*.tiff,*.bmp)|*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp';

            Multiselect = $false;

            CheckFileExists = $false;

            CheckPathExists = $false;

            ValidateNames = $false;
        }

        $null = $FileBrowser.ShowDialog();

        Write-Verbose ($FileBrowser | ConvertTo-Json -Depth 100);

        if($($FileBrowser.FileName)){

            $this.image.path = (Split-Path -Path $($FileBrowser.FileName) -Parent);

            $this.image.filename = $FileBrowser.SafeFileName;

            $this.image.filenamenoext = [IO.Path]::GetFileNameWithoutExtension($this.image.filename);

            $this.image.extension = ($this.image.filename -split "\.")[-1];

            Write-Debug ($this.image | ConvertTo-Json -Depth 100);

            $this.image.file = [System.Drawing.Bitmap]::Fromfile($FileBrowser.FileName);
            
            return $true;
        }

        return $false;
    }

    getFileFromPath(){

        try{

            if((Test-Path -LiteralPath $this.inputpath)){

                $this.image.path = (Split-Path -Path $($this.inputpath) -Parent);

                $this.image.filename = [System.IO.Path]::GetFileName($this.inputpath);

                $this.image.filenamenoext = [System.IO.Path]::GetFileNameWithoutExtension($this.inputpath);

                $this.image.extension = ($this.image.filename -split "\.")[-1];

                $this.image.file = [System.Drawing.Bitmap]::Fromfile($this.inputpath);
            }

        }catch{

            Write-Warning "Cannot find file from path";
        }            
    }

    scanImage(){

        if($this.processsettings.scandirection -eq [ScanDirection]::X){

            Write-Verbose "Scanning direction is X";

            for($y = 0; $y -lt $this.image.file.Height; $y++){

                Write-Host "Row $($y) of $($this.image.file.Height)";

                for($x = 0; $x -lt $this.image.file.Width; $x++){

                    $this.processPixel($x, $y);
                }

                $this.processsettings.scanlines += 1;
            }


        }else{

            Write-Verbose "Scanning direction is Y";

            for($x = 0; $x -lt $this.image.file.Width; $x++){

                Write-Host "Row $($x) of $($this.image.file.Width)";

                for($y = 0; $y -lt $this.image.file.Height; $y++){

                    $this.processPixel($x, $y);
                }

                $this.processsettings.scanlines += 1;
            }
        }

        Write-Debug "Pixels to be processed: $($this.image.etchpixels)";

        Write-Debug ($this.image | ConvertTo-Json);

        Write-Debug "It will take approximately $($this.processsettings.timeestimate) seconds to process.";

        Write-Debug "There are $($this.processsettings.scanlines ) scanlines.";

        Write-Debug "Etcher Settings: $($this.etchersettings | ConvertTo-Json )";

        Write-Debug "Process Settings: $($this.processsettings | ConvertTo-Json)";
    }

    processPixel([int] $x, [int] $y){

        [ImagePixel] $pixel = [ImagePixel]::new($x,$y,($this.image.file.GetPixel($x,$y)));

        if($this.checkColor($pixel)){

            #the pixel is good!

            $this.processLaserData($pixel);

            $this.image.etchpixels += 1;

            if($this.image.etchpixels -eq 1){

                $this.image.firstpixel = $pixel;
            }

            $this.image.lastpixel = $pixel;
        }
    }

    [bool] checkColor([ImagePixel] $pixel){

        $invertedthreshold = 255 - $this.processsettings.whitethreshold;

        if($this.processsettings.invertcolor){
            
            $pixel.convertToNegative();

            $pixel.convertToGrey();

            if($pixel.grey -le $invertedthreshold){

                return $true;
            }

        }else{

            $pixel.convertToGrey();

            if($pixel.grey -le $this.processsettings.whitethreshold){

                return $true;
            }
        }

        return $false;
    }

    processLaserData([ImagePixel] $pixel){

        $power = ($this.convertLaserPower($pixel.grey));

        if($this.processsettings.previouspower -ne $power){

            $this.processsettings.gcode += "$([MCode]::M106) S$($power)`n";

            $this.processsettings.gcode += "$([GCode]::G4) P100`n";

            $this.processsettings.previouspower = $power;
        }

        [double] $fillspacingpermm = 0;

        if($this.processsettings.scandirection -eq [ScanDirection]::X){

            if($this.processsettings.scanlines -gt 0){

                $fillspacingpermm = (($this.etchersettings.spotsizey / 1000) * ($this.processsettings.fillspacing / 100));
            
            }

            $x = $this.offsets.x + (( $pixel.imageposition.x) / ($this.image.horizontal_dpmm )  );

            $y = $this.offsets.y + (($pixel.imageposition.y + $fillspacingpermm) / ($this.image.vertical_dpmm )  );

            $f = $this.getFSpeed($this.processsettings.etchspeed);

            $this.processsettings.gcode += "$([GCode]::G1) X$([Math]::Round($x,5))  Y$([Math]::Round($y,5)) F$($f)`n";

        }else{

            if($this.processsettings.scanlines -gt 0){

                $fillspacingpermm = ($this.etchersettings.spotsizex / 1000) * ($this.processsettings.fillspacing / 100);
            } 
            
            $x = $this.offsets.x + ($pixel.imageposition.x + $fillspacingpermm);

            $y = $this.offsets.y + $pixel.imageposition.y ;

            $f = $this.getFSpeed($this.processsettings.etchspeed);           

            $this.processsettings.gcode += "$([GCode]::G1) X$($x)  Y$($y) F$($f)`n";
        }
    }

    initializeOffsets(){

        #$this.addOffset(([Position]::new(($this.etchersettings.bedsizex / 2), ($this.etchersettings.bedsizey / 2))));

        #$this.addOffset(([Position]::new($this.etchersettings.laseroffsetx, -$this.etchersettings.laseroffsety)));

        $this.addOffset(([Position]::new((($this.etchersettings.spotsizex/1000) / 2), (($this.etchersettings.spotsizey / 1000) / 2))));

        Write-Debug "Current Offsets: $($this.offsets | ConvertTo-Json)";
    }

    initializeGCode(){

        $this.processsettings.gcode = ";Created using the ImageToGcode Powershell script from Laser-Lance (https://www.laserlance.com)`n";

        $this.processsettings.gcode += ";LAYER_COUNT:0`n;LAYER:0`n"; #OctoPrint complains if it doesn't see a layer marker
        
        $this.processsettings.gcode += "$([GCode]::G90); From this moment, all movements are absolute`n";

        $this.processsettings.gcode += "$([GCode]::G21); Set units to mm`n";

        $this.processsettings.gcode += "$([MCode]::M106) S0; Turn the laser off`n"; 

        $this.processsettings.gcode += "$([GCode]::G1) F$($this.getFSpeed($this.processsettings.etchspeed)); Set default feed speed to the etch speed`n";

        $this.processsettings.gcode += "$([GCode]::G0) X$($this.offsets.x) Y$($this.offsets.y) Z$($this.etchersettings.defaultfocus) F$($this.getFSpeed($this.processsettings.skipspeed)); Offset from the nozzle to center lase`n";

        #$this.processsettings.gcode += "$([GCode]::G91); From this moment, all movements are relative`n";
    }

    finalizeGCode(){

        $this.processsettings.gcode += "`n$([MCode]::M106) S0; Turn off the laser`n";

        #$this.processsettings.gcode += "$([GCode]::G90); From this moment, all movements are absolute`n";

        $this.processsettings.gcode += "`n$([GCode]::G0) X$($this.offsets.x) Y$($this.offsets.y)";
    }

    [byte] convertLaserPower([double] $grey){

        $power = ($this.mapValue($grey,255,0,$this.processsettings.minlaserpower,$this.processsettings.maxlaserpower));

        #Write-Debug "Power $([Math]::Round($power))";

        return ([Math]::Round($power));  
    }

    [double] mapValue([double] $value, [double] $fromLow, [double] $fromHigh, [double] $toLow, [double] $toHigh) {

        $fromRange = $fromHigh - $fromLow;

        $toRange = $toHigh - $toLow;

        $scaleFactor = $toRange / $fromRange;

        # Re-zero the value within the from range
        $tmpValue = $value - $fromLow;

        # Rescale the value to the to range
        $tmpValue *= $scaleFactor;

        # Re-zero back to the to range
        return ($tmpValue + $toLow);
    }

    addOffset([Position] $offsets){

        $this.offsets.x += $offsets.x;

        $this.offsets.y += $offsets.y;
    }

    [int] getFSpeed([int] $speed){

        return ($speed * 60); #mm/sec to mm/min
    }

    getImageMetadata(){

        $this.getTotalPixels();

        $this.getPixelsPerMM();

        $this.getPixelsPerMM();

        $this.getPixelsPerDot();
    }

    getTotalPixels(){

        $this.image.totalpixels = $this.image.file.Width * $this.image.file.Height;
    }

    getPixelsPerMM(){

        $this.image.horizontal_dpmm = $this.image.file.HorizontalResolution / 25.4;

        $this.image.vertical_dpmm = $this.image.file.VerticalResolution / 25.4;
    }

    
    getPixelsPerDot(){
    
        $this.etchersettings.h_pixelsperdot = ($this.etchersettings.spotsizex / 1000) * $this.image.horizontal_dpmm;

        $this.etchersettings.v_pixelsperdot = ($this.etchersettings.spotsizey / 1000) * $this.image.vertical_dpmm;
    }  
}
Set-Location $PSScriptRoot;

class GCodeToImage{

    [String] $basepath = $PSScriptRoot;
	[String] $inputpath = $null;
    [String] $outputpath = $null;
    [int] $bedx = 830; #220mm 96dpi = 8.66 inches or 830 pixels
    [int] $bedy = 830;
    [int] $dpi = 0;
    [double] $dpmm;
    [System.Drawing.Bitmap] $bitmap;
    [System.Drawing.Graphics] $graphics;
    [byte] $red = 0;
    [byte] $green = 0;
    [byte] $blue = 0;
    [int] $xpixel = 0;
    [int] $ypixel = 0;
    [String] $gcode = "";
    [String] $filter = "([^G0|^G1][X|Y]\d+.\d+)";
    [Regex] $regex;
    [Object] $matches = @{};
    [double] $screenscalefactor;
    
    GCodeToImage(){

        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing");

    }

    GCodeToImage([int] $bedx, [int] $bedy){

        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing");

        $this.bedx = $bedx;

        $this.bedy = $bedy;

        $this.start();
    }

    GCodeToImage([String] $inputpath, [String] $outputpath){

        [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing");

        $this.inputpath = $inputpath;

        $this.outputpath = $outputpath;

        $this.start();
    }

    [bool] start(){
		
		if(!$this.inputpath){
			
			$this.getInputFile();
		}
		
		$this.getDPI();

        $this.getScreenScaleFactor();

        $this.dpmm = ($this.dpi * $this.screenscalefactor) / 25.4;

        Write-Verbose "dpi: $($this.dpi)";

        Write-Verbose "dpmm: $($this.dpmm)";

        $this.bedx = [Math]::Round((220 * $this.dpmm)) + 1;

        $this.bedy = [Math]::Round((220 * $this.dpmm)) + 1;

        Write-Verbose "bedx: $($this.bedx)";

        Write-Verbose "bedy: $($this.bedy)";

        $this.bitmap = [System.Drawing.Bitmap]::new($this.bedx, $this.bedy);

        $this.graphics = [System.Drawing.Graphics]::FromImage($this.bitmap);

        $this.graphics.Clear([System.Drawing.Color]::White);

        $this.regex = [Regex]::new($this.filter);

        $this.getGCodeFromFile();

        $this.parseGCode();

        $this.saveFile();

        $this.millerTime();

        return $true;
    }
	
	[bool] getInputFile(){
		
		$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
 
            InitialDirectory = [Environment]::GetFolderPath('MyComputer');
     
            Filter = 'GCode (*.gcode,*.GCODE)|*.gcode;*.GCODE;';

            Multiselect = $false;

            CheckFileExists = $true;

            CheckPathExists = $true;

            ValidateNames = $false;
        }

        $null = $FileBrowser.ShowDialog();

        Write-Verbose ($FileBrowser | ConvertTo-Json -Depth 100);

        if($($FileBrowser.FileName)){

            $this.inputpath = $FileBrowser.FileName;

            return $true;
        }

        return $false;
		
	}
	
	[bool] getOutputFile(){
		
		if($this.inputpath){
			
			$this.outputpath = "$($(Split-Path $this.inputpath))\$($([System.IO.Path]::GetFileNameWithoutExtension($this.inputpath)))g.bmp";
		
			return $true;
			
		}else{
			
			Write-Host "No file selected. Cancelling.";

			break;
			
			return $false;
		}
		<#
		$FileBrowser = New-Object System.Windows.Forms.SaveFileDialog -Property @{
 
			InitialDirectory = [Environment]::GetFolderPath('MyComputer');
     
            Filter = 'Image Files (*.jpg,*.jpeg,*.png,*.tif,*.tiff,*.bmp)|*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp';
			
			CheckFileExists = $false;

            CheckPathExists = $false;

            ValidateNames = $false;
		}

		$null = $FileBrowser.ShowDialog();

		if($($FileBrowser.FileName)){

            $this.outputpath = $FileBrowser.FileName;

			return $true;

		}else{
			
			Write-Host "No file selected. Cancelling.";

			break;
			
			return $false;
		}
		#>		
	}

    getGCodeFromFile(){

        $this.gcode = Get-Content -LiteralPath $this.inputpath;
    }

    parseGCode(){

        $this.matches = $this.regex.Matches($this.gcode);

        for($i = 0; $i -lt $this.matches.Count; $i+=2){

            $this.xpixel = (([int]([String] $this.matches[$i]).Replace("X",""))) * $this.dpmm;

            $this.ypixel = (([int]([String] $this.matches[$i + 1]).Replace("Y",""))) * $this.dpmm;

            if(($this.xpixel -gt $this.bedx) -or ($this.xpixel -lt 0)){

                $this.xpixel = $this.bedx;
            }

            if(($this.ypixel -gt $this.bedy) -or ($this.ypixel -lt 0)){

                $this.ypixel = $this.bedy;
            }

            Write-Verbose "x: $($this.xpixel)";

            Write-Verbose "y: $($this.ypixel)";

            $this.bitmap.SetPixel($this.xPixel, $this.yPixel, [System.Drawing.Color]::FromArgb($this.red, $this.green, $this.blue));
        }
    }

    saveFile(){
		
		if(!$this.outputpath){
			
			$this.getOutputFile();
		}

        $this.bitmap.Save($this.outputpath);

        if(!(Test-Path -LiteralPath $this.outputpath)){

            Write-Host "Error saving file";

        }else{

            #Invoke-Item $path;
        }
    }

    getScreenScaleFactor(){

        $rh=[int](Get-CimInstance -ClassName Win32_VideoController)[0].CurrentVerticalResolution;

        $vh=[int][System.Windows.Forms.SystemInformation]::VirtualScreen.Height;

        Write-Debug "rh: $($rh)`nvh: $($vh)";

        if(($rh -eq 0) -or $vh -eq 0){

            $this.screenscalefactor = 1;

        }else{

            $this.screenscalefactor = $rh / $vh;
        }
    }
	
	getDPI(){

        $this.dpi = (Get-ItemProperty -Path "HKCU:\Control Panel\Desktop\").LogPixels;
		
		if($this.dpi -eq 0){
			
			$this.dpi = (Get-ItemProperty -path "HKCU:\Control Panel\Desktop\WindowMetrics").AppliedDPI;	
		}
		
		if($this.dpi -eq 0){
			
			$this.dpi = (Get-ItemProperty -path "HKCU:\Control Panel\Desktop\PerMonitorSettings\*").DpiValue;
		}	

        if($this.dpi -eq 0){

            $this.dpi = 96;
        }
		
		Write-Debug "getDPI: $($this.dpi)";
    }

    millerTime(){

        $this.bitmap.Dispose();

        $this.graphics.Dispose();
    }
}

One Response

  1. George says:

    You spew code as elegant as a chicken laying a golden egg.

Leave a Reply

Your email address will not be published. Required fields are marked *