Last active
September 13, 2025 17:13
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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