Skip to content

Instantly share code, notes, and snippets.

@sleemanj
Created November 22, 2025 09:32
Show Gist options
  • Select an option

  • Save sleemanj/8904818bee556cb43613e84fe996239b to your computer and use it in GitHub Desktop.

Select an option

Save sleemanj/8904818bee556cb43613e84fe996239b to your computer and use it in GitHub Desktop.
Bash script (using gerbv and standard tools) to add two holes to a gerber paste mask and drill file for alignment purposes. Created mostly by Gemini 3 AI, a couple of manual fixes are commented with "Fix". To use, execute this from within the directory containing your gerber files.
#!/bin/bash
# =============================================================================
# PCB Tooling Hole Injector (Multi-Unit Support)
# Adds drill hits and paste mask openings to existing manufacturing files.
# Handles mixed Imperial/Metric units between Gerber and Drill files.
# =============================================================================
# Ensure standard decimal formatting
export LC_NUMERIC="C"
# Check dependencies
for cmd in gerbv bc awk sed grep; do
if ! command -v $cmd &> /dev/null; then
echo "Error: Required command '$cmd' not found. Please install it."
exit 1
fi
done
# --- 1. Locate Files ---
echo "Scanning directory for files..."
GKO_FILE=$(find . -maxdepth 1 -iname "*.gko" | head -n1)
DRILL_FILE=$(find . -maxdepth 1 -iname "*.txt" | head -n1)
GTP_FILE=$(find . -maxdepth 1 -iname "*.gtp" | head -n1)
GTL_FILE=$(find . -maxdepth 1 -iname "*.gtl" | head -n1)
GBL_FILE=$(find . -maxdepth 1 -iname "*.gbl" | head -n1)
if [[ -z "$GKO_FILE" || -z "$DRILL_FILE" || -z "$GTP_FILE" || -z "$GTL_FILE" || -z "$GBL_FILE" ]]; then
echo "Error: Could not find all required files (.gko, .txt, .gtp, .gtl, .gbl)."
exit 1
fi
echo "Found:"
echo " Outline: $GKO_FILE"
echo " Drill: $DRILL_FILE"
echo " Paste: $GTP_FILE"
echo " Top: $GTL_FILE"
echo " Bottom: $GBL_FILE"
echo ""
# --- 2. Analyze Units ---
# A. Gerber Units (from GKO)
UNITS_GKO=$(grep -a "^%MO" "$GKO_FILE" | head -n1)
if [[ "$UNITS_GKO" == *"%MOMM"* ]]; then
IS_GERBER_METRIC=1
echo "Gerber Units: Metric (mm)"
else
IS_GERBER_METRIC=0
echo "Gerber Units: Imperial (inches)"
fi
# B. Drill Units (from TXT)
# Look for "METRIC" string as requested
if grep -iq "METRIC" "$DRILL_FILE"; then
IS_DRILL_METRIC=1
echo "Drill Units: Metric (Detected 'METRIC' tag)"
else
IS_DRILL_METRIC=0
echo "Drill Units: Imperial (Defaulting, 'METRIC' tag not found)"
fi
# --- 3. Analyze Board Outline (GKO) ---
# Extract Format Specification for Gerbers (Coordinate parsing)
FORMAT_LINE=$(grep -a "^%FS" "$GKO_FILE" | head -n1)
GERBER_DECIMALS=4
if [[ $FORMAT_LINE =~ X[0-9]([0-9]) ]]; then
GERBER_DECIMALS=${BASH_REMATCH[1]}
fi
GERBER_DIVIDER=$(echo "10^$GERBER_DECIMALS" | bc)
echo "Gerber Format: $GERBER_DECIMALS decimals (Divisor: $GERBER_DIVIDER)"
# Parse GKO for Bounding Box
read min_x_raw min_y_raw max_x_raw max_y_raw <<< $(awk -v div="$GERBER_DIVIDER" '
BEGIN { minx=999999999; miny=999999999; maxx=-999999999; maxy=-999999999 }
/^X[0-9-]*Y[0-9-]*/ {
match($0, /X([0-9-]+)/, x)
match($0, /Y([0-9-]+)/, y)
val_x = x[1] + 0
val_y = y[1] + 0
if (val_x < minx) minx = val_x
if (val_x > maxx) maxx = val_x
if (val_y < miny) miny = val_y
if (val_y > maxy) maxy = val_y
}
END { print minx, miny, maxx, maxy }
' "$GKO_FILE")
# Convert Raw Gerber Integer Coords to Real Gerber Units
G_MIN_X=$(echo "scale=6; $min_x_raw / $GERBER_DIVIDER" | bc)
G_MIN_Y=$(echo "scale=6; $min_y_raw / $GERBER_DIVIDER" | bc)
G_MAX_X=$(echo "scale=6; $max_x_raw / $GERBER_DIVIDER" | bc)
G_MAX_Y=$(echo "scale=6; $max_y_raw / $GERBER_DIVIDER" | bc)
# Calculate Dimensions in Gerber Units
WIDTH_GU=$(echo "scale=6; $G_MAX_X - $G_MIN_X" | bc)
HEIGHT_GU=$(echo "scale=6; $G_MAX_Y - $G_MIN_Y" | bc)
# Display Dimensions to User (Always in mm)
if [ "$IS_GERBER_METRIC" -eq 0 ]; then
DISP_W=$(echo "scale=2; $WIDTH_GU * 25.4" | bc)
DISP_H=$(echo "scale=2; $HEIGHT_GU * 25.4" | bc)
# Determine Origin in MM for calculation base
ORIGIN_X_MM=$(echo "scale=6; $G_MIN_X * 25.4" | bc)
ORIGIN_Y_MM=$(echo "scale=6; $G_MIN_Y * 25.4" | bc)
else
DISP_W=$WIDTH_GU
DISP_H=$HEIGHT_GU
ORIGIN_X_MM=$G_MIN_X
ORIGIN_Y_MM=$G_MIN_Y
fi
echo "------------------------------------------------"
echo "Board Outline Dimensions: $DISP_W x $DISP_H mm"
echo "------------------------------------------------"
# --- 4. Input Loop ---
while true; do
echo "Enter offsets in Millimeters from Bottom-Left corner."
read -p "Hole 1 X offset (mm): " h1_x_off
read -p "Hole 1 Y offset (mm): " h1_y_off
read -p "Hole 2 X offset (mm): " h2_x_off
read -p "Hole 2 Y offset (mm): " h2_y_off
read -p "Hole Diameter (mm) [default 3]: " h_diam
h_diam=${h_diam:-3}
# --- 5. Calculate Absolute Coordinates in MM ---
# User Offset + Board Origin (in MM) = Absolute Position in MM
ABS_H1_X_MM=$(echo "scale=6; $ORIGIN_X_MM + $h1_x_off" | bc)
ABS_H1_Y_MM=$(echo "scale=6; $ORIGIN_Y_MM + $h1_y_off" | bc)
ABS_H2_X_MM=$(echo "scale=6; $ORIGIN_X_MM + $h2_x_off" | bc)
ABS_H2_Y_MM=$(echo "scale=6; $ORIGIN_Y_MM + $h2_y_off" | bc)
# --- 6. Calculate GERBER Coordinates ---
# Convert Absolute MM -> Gerber Units -> Integer Format
mm_to_gerber_int() {
local mm_val=$1
local result
if [ "$IS_GERBER_METRIC" -eq 0 ]; then
# MM to Inch
result=$(echo "scale=6; $mm_val / 25.4" | bc)
else
result=$mm_val
fi
# Float to Scaled Integer (Rounding handled by printf %.0f via awk)
echo "$result * $GERBER_DIVIDER" | bc | awk '{printf "%0.0f", $1}'
}
H1_X_GERBER=$(mm_to_gerber_int $ABS_H1_X_MM)
H1_Y_GERBER=$(mm_to_gerber_int $ABS_H1_Y_MM)
H2_X_GERBER=$(mm_to_gerber_int $ABS_H2_X_MM)
H2_Y_GERBER=$(mm_to_gerber_int $ABS_H2_Y_MM)
# Aperture size for Gerber
if [ "$IS_GERBER_METRIC" -eq 0 ]; then
APER_SIZE_GERBER=$(echo "scale=4; $h_diam / 25.4" | bc)
else
APER_SIZE_GERBER=$h_diam
fi
# --- 7. Calculate DRILL Coordinates ---
# Convert Absolute MM -> Drill Units -> Explicit Decimal Format
mm_to_drill_float() {
local mm_val=$1
if [ "$IS_DRILL_METRIC" -eq 0 ]; then
# MM to Inch
echo "scale=5; $mm_val / 25.4" | bc | awk '{printf "%.5f", $0}'
else
# MM is MM
echo "scale=3; $mm_val" | bc | awk '{printf "%.3f", $0}'
fi
}
H1_X_DRILL=$(mm_to_drill_float $ABS_H1_X_MM)
H1_Y_DRILL=$(mm_to_drill_float $ABS_H1_Y_MM)
H2_X_DRILL=$(mm_to_drill_float $ABS_H2_X_MM)
H2_Y_DRILL=$(mm_to_drill_float $ABS_H2_Y_MM)
# Tool size for Drill
# if [ "$IS_DRILL_METRIC" -eq 0 ]; then
# TOOL_SIZE_DRILL=$(echo "scale=4; $h_diam / 25.4" | bc)
# else
# TOOL_SIZE_DRILL=$h_diam
# fi
# Fix - James; If the tool size isn't correctly formatted with 2 dp
# gerbv doesn't interpret it correctly
TOOL_SIZE_DRILL=$(mm_to_drill_float $h_diam)
# --- 8. Generate Files ---
WORK_DIR=$(mktemp -d /tmp/pcb_mod_XXXXXX)
echo "Processing in $WORK_DIR..."
cp "$GKO_FILE" "$WORK_DIR/"
cp "$DRILL_FILE" "$WORK_DIR/"
cp "$GTP_FILE" "$WORK_DIR/"
cp "$GTL_FILE" "$WORK_DIR/"
cp "$GBL_FILE" "$WORK_DIR/"
TGT_GTP="$WORK_DIR/$(basename "$GTP_FILE")"
TGT_DRILL="$WORK_DIR/$(basename "$DRILL_FILE")"
# Modify Paste Mask (GERBER)
# Add Aperture (D199)
sed -i "/^%ADD/!b;n;a %ADD199C,$APER_SIZE_GERBER*%" "$TGT_GTP"
# Add Flashes
NEW_G_LINES="D199*\nX${H1_X_GERBER}Y${H1_Y_GERBER}D03*\nX${H2_X_GERBER}Y${H2_Y_GERBER}D03*"
sed -i "s/^M02/$NEW_G_LINES\nM02/" "$TGT_GTP"
# Modify Drill File (DRILL)
# Add Tool (T99)
# We use explicit decimals in the tool definition to be safe
sed -i "/^%/i T99C$TOOL_SIZE_DRILL" "$TGT_DRILL"
# Fix - James, the above also adds a line at the top of the file incorrectly
cat "$TGT_DRILL" | tail -n +2 >"$TGT_DRILL.2"
mv "$TGT_DRILL.2" "$TGT_DRILL"
# Add Hits using Explicit Decimals
DRILL_LINES="T99\nX${H1_X_DRILL}Y${H1_Y_DRILL}\nX${H2_X_DRILL}Y${H2_Y_DRILL}"
sed -i "s/^M30/$DRILL_LINES\nM30/" "$TGT_DRILL"
# --- 9. Visualize ---
echo "Opening gerbv..."
gerbv \
"$TGT_DRILL" \
"$TGT_GTP" \
"$WORK_DIR/$(basename "$GTL_FILE")" \
"$WORK_DIR/$(basename "$GBL_FILE")" \
"$WORK_DIR/$(basename "$GKO_FILE")" \
&
GERBV_PID=$!
# --- 10. Confirm ---
read -p "Is the placement acceptable? (y/N): " confirm
if ps -p $GERBV_PID > /dev/null; then
kill $GERBV_PID 2>/dev/null
fi
if [[ "$confirm" =~ ^[Yy]$ ]]; then
echo "Success! New files located at:"
echo "$WORK_DIR"
exit 0
else
echo "Retrying..."
rm -rf "$WORK_DIR"
echo ""
fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment