Skip to content

Instantly share code, notes, and snippets.

@Archonic944
Last active September 13, 2025 17:13
Show Gist options
  • Select an option

  • Save Archonic944/45ea2658ea8d77822dd6d0b5e86f93e8 to your computer and use it in GitHub Desktop.

Select an option

Save Archonic944/45ea2658ea8d77822dd6d0b5e86f93e8 to your computer and use it in GitHub Desktop.
Apply an arbitrary Z offset to .gx file (Can be greater than 1 or -1, tested with FlashForge Finder)
#!/usr/bin/env python3
"""
apply_z_offset_safe.py
Binary-safe Z offset applier for FlashForge .gx / generic G-code files.
Designed for use with printers that have broken distance sensors or can otherwise not calibrate the z axis.
FlashPrint provides an option for z offset but it only goes from -1 to 1.
- Reads and writes bytes; preserves ALL non-G-code bytes, line endings, and comments.
- Only modifies Z values on lines whose first non-space char is 'G' or 'M'.
- Tracks absolute/relative mode via G90 / G91 in the code portion of those lines.
- If --offset 0, copies input to output unchanged (guaranteed identical bytes).
Usage:
python apply_z_offset_safe.py input.gx --offset -0.20 -o output.gx
python apply_z_offset_safe.py input.gx --offset -0.20 --inplace
python apply_z_offset_safe.py input.gx --offset -0.20 --apply-to-relative
"""
import argparse
import re
from decimal import Decimal, getcontext, ROUND_HALF_UP
import shutil
import sys
getcontext().prec = 12
getcontext().rounding = ROUND_HALF_UP
RE_G90 = re.compile(br'(?i)(?<!\d)G90(?!\d)')
RE_G91 = re.compile(br'(?i)(?<!\d)G91(?!\d)')
RE_ZTOK = re.compile(br'(?i)([Zz])([-+]?\d+(?:\.\d+)?)')
def process(in_bytes, offset_dec, apply_to_relative=False):
# Zero offset: return identical bytes
if offset_dec == 0:
return in_bytes
out = bytearray()
lines = in_bytes.splitlines(keepends=True)
current_abs = True # default to absolute until G91 seen
for line in lines:
stripped = line.lstrip(b' \t')
if not stripped:
out.extend(line)
continue
# Only modify lines that begin with G or M (after whitespace)
first = stripped[:1]
if first not in (b'G', b'g', b'M', b'm'):
out.extend(line)
continue
# Split code vs comment at first ';' and only change the code part
semicol = line.find(b';')
if semicol == -1:
code = line
comment = b''
else:
code = line[:semicol]
comment = line[semicol:]
# Track absolute/relative mode
if RE_G90.search(code):
current_abs = True
if RE_G91.search(code):
current_abs = False
def repl(m):
# Skip relative mode unless explicitly requested
if not current_abs and not apply_to_relative:
return m.group(0)
z_letter = m.group(1) # b'Z' or b'z'
num_bytes = m.group(2) # ASCII digits with optional sign/decimal
num_str = num_bytes.decode('ascii')
decimals = len(num_str.split('.', 1)[1]) if '.' in num_str else 0
orig = Decimal(num_str)
new = orig + Decimal(offset_dec)
if decimals > 0:
q = Decimal(1) / (10 ** decimals)
new = new.quantize(q)
num_out = (f"{new:.{decimals}f}").encode('ascii')
else:
num_out = str(int(new.to_integral_value(rounding=ROUND_HALF_UP))).encode('ascii')
return z_letter + num_out
new_code = RE_ZTOK.sub(repl, code)
out.extend(new_code)
out.extend(comment)
return bytes(out)
def main():
ap = argparse.ArgumentParser(description='Binary-safe Z-offset applier for .gx / G-code files.')
ap.add_argument('infile', help='Input .gx/.gcode')
ap.add_argument('--offset', required=True, help='Offset in mm (e.g. -0.20, 0.10)')
ap.add_argument('-o', '--outfile', help='Output path (default: input.zoffset.gx)')
ap.add_argument('--inplace', action='store_true', help='Modify input in place (creates .bak backup)')
ap.add_argument('--apply-to-relative', action='store_true', help='Also apply to relative Z moves (G91) (Not recommended)')
args = ap.parse_args()
try:
offset_dec = Decimal(args.offset)
except Exception:
print('Invalid --offset value:', args.offset, file=sys.stderr)
sys.exit(2)
src = args.infile
if args.outfile:
dst = args.outfile
else:
if args.inplace:
dst = src + '.tmp'
else:
base, dot, ext = src.rpartition('.')
dst = base + '.zoffset.' + ext if dot else src + '.zoffset'
if args.inplace:
bak = src + '.bak'
shutil.copy2(src, bak)
with open(src, 'rb') as f:
data = f.read()
out = process(data, offset_dec, apply_to_relative=args.apply_to_relative)
with open(dst, 'wb') as f:
f.write(out)
if args.inplace:
shutil.move(dst, src)
print('Updated in-place. Backup at', bak)
else:
print('Wrote', dst)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment