<p>If your slicer does not have a mirror operation or a scale that allows negative values then mirroring in the G-code should be straightforward.</p>
<p>As long as your printer doesn't have certain specific tool change or homing or purge positions that are done in the G-code you can just transform it, otherwise you would want to skip these sections and just do the model data (it should be obvious looking at the code where model data starts).</p>
<p>In order to mirror it you just need to swap out the X coordinates in your G-code, If (0,0) is the center of your bed, as is often (but not always) the case for delta printers you will just want to negate the X, so <code>G1 X30 Y-3 Z2</code> becomes <code>G1 X-30 Y-3 Z2</code>.  If your coordinates have (0,0) in a corner  (often the case for orthogonal printers) then you want to subtract X from the maximum X value. for instance if your bed is 250 mm wide then in <code>G1 X30 Y10 Z3  X30</code> becomes <code>X(250-30)</code> or <code>G1 X220 Y10 Z3</code>.</p>
<p>There is only one caveat, some slicers will switch to relative movement using <code>G91</code> for certain operations and then back to absolute with <code>G90</code>, so you will want to look out for these. Between a <code>G91</code> and a <code>G90</code> you will want to negate the X, no matter where the origin of your printer is.</p>
<p>When writing your script, I'd keep track of the minimum and maximum values encountered and the new minimum and maximum values and print them at the end as a sanity check so you can see if anything is wonky.</p>

CC BY-SA 4.0
2021-07-03T03:47:10.247Z
2021-07-03T17:38:34.137Z
2021-07-03T17:38:34.137Z
John Meacham
<p>Based on @John Mecham's comprehensive answer, I whipped up a quick proof of concept. In the image below, the left arrow (top) is the original and the right arrow (bottom) is the reversed clone. Cura does generate a little relative offset code at the end, I think I handled it correctly.</p>
<p><a href="https://i.sstatic.net/ncnZP.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/ncnZP.jpg" alt="Left and right arrows" /></a></p>
<pre><code>import re

data_start = re.compile('^;LAYER_COUNT:[0-9]+')

ifilename = 'c:/Users/jentron128/Downloads/ThingVerse/Tools/arrow.gcode'
ofilename = 'c:/Users/jentron128/Downloads/ThingVerse/Tools/rev_arrow.gcode'

new_data =[]
BEDX = float(235)
h = BEDX # Absolute Positioning is default

mode='Skip'

with open(ifilename, 'r') as ifp:
    for d in ifp:
        new_d = ''
        tokens = d.split()
        if len(tokens) == 0:
            pass
        elif mode == 'Skip':
            new_d = d[0:-1]
            if data_start .match(d):
                mode = 'Go'
        else:
            if tokens[0] == 'G91': # Relative Positioning
                h = 0 
            elif tokens[0] == 'G90':# Absolute Positioning
                h = BEDX
                
            for t in tokens:
                if t.startswith('X'):
                    if len(t) &gt; 1:
                        x = h - float(t[1:])
                        t = 'X'+f'{x:.3f}'
                new_d += t+' '
        
        new_data.append(new_d)

with open(ofilename, 'w') as ofp:
    for d in new_data:
        ofp.write(d+'\n') # how does writelines not support a line separator?
    
</code></pre>

CC BY-SA 4.0
2022-10-07T17:15:21.563Z
```#!/usr/bin/env python3

import re
from sys import argv

data_start = re.compile('^;LAYER_COUNT:[0-9]+')

ifilename = argv[1];
ofilename = ifilename.replace('.gcode', '-reversed.gcode');
```
Onza
CC BY-SA 4.0
2022-10-07T17:16:32.103Z
I tried to comment to make it a reusable script that you can run against any file, but the comment got mangled; basically use sys to import argv and use the argv[1] for the filename :)
Onza
CC BY-SA 4.0
2022-10-07T17:25:45.910Z
It didn't work for me nevertheless, not with a file generated by prusa.
Onza
CC BY-SA 4.0
2022-10-07T17:52:22.557Z
At least on Windows I never run Python from a command line. If you have a (preferably small) prusa gcode file I will take a look to see if I can modify the code to work with it.
Ron Jensen
CC BY-SA 4.0
2022-10-07T18:07:10.410Z
I think I found the issue, you are looking for a header that seemed to be specific to Cura, I made a new version based on yours which is correctly working for the prusa slicer files; I took all the math you used overall, it seems to work correctly now; honestly I am not sure, I have never touched gcode until now, but I think the G1 is universal.
Onza
CC BY-SA 4.0
2022-10-07T18:20:37.183Z
Yes, my code has a state-machine built into it, and it seems the switch is based on a Cura specific comment. Anything before the first ";LAYER:0" comment is output as read because it's all printer setup, then the state switches to "Go" mode and processes ALL X directives after that, regardless of G code. It treats G90/G91 separately by setting h = 0 for RELATIVE positioning and h = bed width for ABSOLUTE positioning.
Ron Jensen
CC BY-SA 4.0
2022-10-07T18:27:51.847Z
The easiest way around this is just change mode='Skip' to mode='Go' and then the script will process all the lines.
Ron Jensen
CC BY-SA 4.0
2022-10-08T07:12:42.757Z
However we (or at least I) wasn't sure wether that was the correct assumption, I simply took all G1 token output and flipped that one, assuming those are the instructions; I wrote the code below as you can see.
Onza
CC BY-SA 4.0
2021-07-03T05:45:21.840Z
2021-07-03T05:45:21.840Z
Ron Jensen
<p>Usage: python3 [filename.gcode] [bed-width]</p>
<pre><code>#!/usr/bin/env python3

import re
from sys import argv

#first arg is the file, second arg is the bed x width
ifilename = argv[1];
ofilename = ifilename.replace('.gcode', '-reversed.gcode');

new_data =[]
BEDX = float(argv[2])
h = BEDX # Absolute Positioning is default

with open(ifilename, 'r') as ifp:
    for d in ifp:
        new_d = ''
        tokens = d.split()
        if len(tokens) == 0:
            pass
        else:
            if tokens[0] == 'G91': # Relative Positioning
                h = 0
                new_d += d
            elif tokens[0] == 'G90':# Absolute Positioning
                h = BEDX
                new_d += d
            elif tokens[0] == 'G1':
                for t in tokens:
                    if t.startswith('X'):
                        if len(t) &gt; 1:
                            x = h - float(t[1:])
                            t = 'X'+f'{x:.3f}'
                    new_d += t+' '
            else:
                new_d += d

        new_data.append(new_d)

with open(ofilename, 'w') as ofp:
    for d in new_data:
        ofp.write(d+'\n') # how does writelines not support a line separator?
</code></pre>
<p>For those that have a problem with the script above, I think this fixes it.</p>
<p>Also there seems to be a bug with not writting G90 or G91 whenever it finds it.</p>

CC BY-SA 4.0
2022-10-10T14:48:51.073Z
The way you generate `ofilename` is not very safe. If the filename doesn't end in `.gcode` (e.g. if it ends in `.GCODE` or `.gco`) the replace will be a no-op and it looks like you'll overwrite the input.
R.. GitHub STOP HELPING ICE
CC BY-SA 4.0
2022-10-20T07:05:29.540Z
You are right, make a third version or something; I hacked this quickly on the spot, maybe using argv[2] for output name.
Onza
CC BY-SA 4.0
2022-10-07T17:58:19.613Z
2022-10-07T17:58:19.613Z
Onza
<p>I've got some sliced models that represent the <strong>right</strong>-side arms and legs of a robot. I'm really happy with how they printed, so now I'd like to print the <strong>left</strong>-side arms and legs.</p>
<p>I was thinking it would be pretty trivial to parse the G-code file using Python and change the value of all the <code>Xn</code> commands from <code>n</code> to <code>2*h - n</code>, where <code>h</code> is in the middle of the bed, say 110 or 120 mm for an Ender 3.</p>
<p>Before I fire up my favorite IDE, are there any major gotchas I might encounter from such a naïve approach to mirroring the G-code like this? I originally sliced in Cura 4.9.1.</p>

CC BY-SA 4.0
2021-07-03T00:09:36.120Z
There are g-code visualizers which will allow you to open your newly modified file to observe obvious problems.  Is your reference to the middle of the bed valid? I've not examined a g-code file sufficiently enough to note if the origin is the bed center or a specific corner.
fred_dot_u
CC BY-SA 4.0
2021-07-03T01:41:49.793Z
@fred_dot_u good point, I suppose I could find the bounding box of the print while I'm parsing the file. At least on the Ender 3 the home position seems to be at (0,0,0).
Ron Jensen
CC BY-SA 4.0
2021-07-03T03:32:24.047Z
often times you can scale with a -1 value to mirror an axis if your slicer does not have an explicit mirror option.
John Meacham
<g-code><python>
Reverse the X-axis of a G-code file
Ron Jensen
# Reverse the X-axis of a G-code file

(Asked by: username_0 on 2021-07-02)

I've got some sliced models that represent the **right**-side arms and legs of a robot. I'm really happy with how they printed, so now I'd like to print the **left**-side arms and legs.

I was thinking it would be pretty trivial to parse the G-code file using Python and change the value of all the `Xn` commands from `n` to `2*h - n`, where `h` is in the middle of the bed, say 110 or 120 mm for an Ender 3.

Before I fire up my favorite IDE, are there any major gotchas I might encounter from such a naïve approach to mirroring the G-code like this? I originally sliced in Cura 4.9.1.

**Tags:** g-code, python
> **Comments:**
> **username_1**: There are g-code visualizers which will allow you to open your newly modified file to observe obvious problems.  Is your reference to the middle of the bed valid? I've not examined a g-code file sufficiently enough to note if the origin is the bed center or a specific corner.  
> **username_0**: @fred_dot_u good point, I suppose I could find the bounding box of the print while I'm parsing the file. At least on the Ender 3 the home position seems to be at (0,0,0).  
> **username_2**: often times you can scale with a -1 value to mirror an axis if your slicer does not have an explicit mirror option.  

---

## 3 answers

### Answer by username_2 on 2021-07-03. Score: 4 | Accepted answer

If your slicer does not have a mirror operation or a scale that allows negative values then mirroring in the G-code should be straightforward.

As long as your printer doesn't have certain specific tool change or homing or purge positions that are done in the G-code you can just transform it, otherwise you would want to skip these sections and just do the model data (it should be obvious looking at the code where model data starts).

In order to mirror it you just need to swap out the X coordinates in your G-code, If (0,0) is the center of your bed, as is often (but not always) the case for delta printers you will just want to negate the X, so `G1 X30 Y-3 Z2` becomes `G1 X-30 Y-3 Z2`. If your coordinates have (0,0) in a corner (often the case for orthogonal printers) then you want to subtract X from the maximum X value. for instance if your bed is 250 mm wide then in `G1 X30 Y10 Z3 X30` becomes `X(250-30)` or `G1 X220 Y10 Z3`.

There is only one caveat, some slicers will switch to relative movement using `G91` for certain operations and then back to absolute with `G90`, so you will want to look out for these. Between a `G91` and a `G90` you will want to negate the X, no matter where the origin of your printer is.

When writing your script, I'd keep track of the minimum and maximum values encountered and the new minimum and maximum values and print them at the end as a sanity check so you can see if anything is wonky.


---

### Answer by username_0 on 2021-07-03. Score: 3

Based on @John Mecham's comprehensive answer, I whipped up a quick proof of concept. In the image below, the left arrow (top) is the original and the right arrow (bottom) is the reversed clone. Cura does generate a little relative offset code at the end, I think I handled it correctly.

import re

data_start = re.compile('^;LAYER_COUNT:[0-9]+')

ifilename = 'c:/Users/jentron128/Downloads/ThingVerse/Tools/arrow.gcode'
ofilename = 'c:/Users/jentron128/Downloads/ThingVerse/Tools/rev_arrow.gcode'

new_data =[]
BEDX = float(235)
h = BEDX # Absolute Positioning is default

mode='Skip'

with open(ifilename, 'r') as ifp:
    for d in ifp:
        new_d = ''
        tokens = d.split()
        if len(tokens) == 0:
            pass
        elif mode == 'Skip':
            new_d = d[0:-1]
            if data_start .match(d):
                mode = 'Go'
        else:
            if tokens[0] == 'G91': # Relative Positioning
                h = 0 
            elif tokens[0] == 'G90':# Absolute Positioning
                h = BEDX
                
            for t in tokens:
                if t.startswith('X'):
                    if len(t) > 1:
                        x = h - float(t[1:])
                        t = 'X'+f'{x:.3f}'
                new_d += t+' '
        
        new_data.append(new_d)

with open(ofilename, 'w') as ofp:
    for d in new_data:
        ofp.write(d+'\n') # how does writelines not support a line separator?

> **Comments:**
> **username_3**: ```#!/usr/bin/env python3  import re from sys import argv  data_start = re.compile('^;LAYER_COUNT:[0-9]+')  ifilename = argv[1]; ofilename = ifilename.replace('.gcode', '-reversed.gcode'); ```  
> **username_3**: I tried to comment to make it a reusable script that you can run against any file, but the comment got mangled; basically use sys to import argv and use the argv[1] for the filename :)  
> **username_3**: It didn't work for me nevertheless, not with a file generated by prusa.  
> **username_0**: At least on Windows I never run Python from a command line. If you have a (preferably small) prusa gcode file I will take a look to see if I can modify the code to work with it.  
> **username_3**: I think I found the issue, you are looking for a header that seemed to be specific to Cura, I made a new version based on yours which is correctly working for the prusa slicer files; I took all the math you used overall, it seems to work correctly now; honestly I am not sure, I have never touched gcode until now, but I think the G1 is universal.  
> **username_0**: Yes, my code has a state-machine built into it, and it seems the switch is based on a Cura specific comment. Anything before the first ";LAYER:0" comment is output as read because it's all printer setup, then the state switches to "Go" mode and processes ALL X directives after that, regardless of G code. It treats G90/G91 separately by setting h = 0 for RELATIVE positioning and h = bed width for ABSOLUTE positioning.  
> **username_0**: The easiest way around this is just change mode='Skip' to mode='Go' and then the script will process all the lines.  
> **username_3**: However we (or at least I) wasn't sure wether that was the correct assumption, I simply took all G1 token output and flipped that one, assuming those are the instructions; I wrote the code below as you can see.  

---

### Answer by username_3 on 2022-10-07. Score: 0

Usage: python3 [filename.gcode] [bed-width]
#!/usr/bin/env python3

import re
from sys import argv

#first arg is the file, second arg is the bed x width
ifilename = argv[1];
ofilename = ifilename.replace('.gcode', '-reversed.gcode');

new_data =[]
BEDX = float(argv[2])
h = BEDX # Absolute Positioning is default

with open(ifilename, 'r') as ifp:
    for d in ifp:
        new_d = ''
        tokens = d.split()
        if len(tokens) == 0:
            pass
        else:
            if tokens[0] == 'G91': # Relative Positioning
                h = 0
                new_d += d
            elif tokens[0] == 'G90':# Absolute Positioning
                h = BEDX
                new_d += d
            elif tokens[0] == 'G1':
                for t in tokens:
                    if t.startswith('X'):
                        if len(t) > 1:
                            x = h - float(t[1:])
                            t = 'X'+f'{x:.3f}'
                    new_d += t+' '
            else:
                new_d += d

        new_data.append(new_d)

with open(ofilename, 'w') as ofp:
    for d in new_data:
        ofp.write(d+'\n') # how does writelines not support a line separator?

For those that have a problem with the script above, I think this fixes it.
Also there seems to be a bug with not writting G90 or G91 whenever it finds it.

> **Comments:**
> **username_4**: The way you generate `ofilename` is not very safe. If the filename doesn't end in `.gcode` (e.g. if it ends in `.GCODE` or `.gco`) the replace will be a no-op and it looks like you'll overwrite the input.  
> **username_3**: You are right, make a third version or something; I hacked this quickly on the spot, maybe using argv[2] for output name.  

---

