added UVtools patch

This commit is contained in:
cpu
2026-06-14 11:38:45 +02:00
parent f5eca9efa4
commit 9bcea25f78
14 changed files with 577 additions and 840 deletions

192
UVtools/Readme.md Normal file
View File

@@ -0,0 +1,192 @@
# UVtools PCB Exposure CLI — Build & Usage Guide
This guide covers building a wrapper for the`UVtoolsCmd`, for headless Gerber → `.pm4n` (or any slicer format) conversion on Ubuntu.
> **Note:** The UVtools maintainer is implementing an official equivalent via
> the generic `run` command and a new `FileArray` reflection property
> (`-p FileArray=file.gbr`). Once that ships in a release
> ([issue 1127](https://github.com/sn4k3/UVtools/issues/1127)), you can switch to
> `pcb-expose-official.sh` and skip the patch/compile steps entirely — see
> [Option B](#option-b-official-release-no-compiling-required) below.
---
## Option A: Patched local build (`pcb-expose` command)
### 1. Install .NET SDK
```bash
sudo snap install dotnet
export DOTNET_ROOT=/var/snap/dotnet/common/dotnet
dotnet --version
```
Add the `export` line to your `~/.bashrc` so it persists across shells:
```bash
echo 'export DOTNET_ROOT=/var/snap/dotnet/common/dotnet' >> ~/.bashrc
```
### 2. Clone the repo
```bash
cd ~/proj
git clone --depth 1 https://github.com/sn4k3/UVtools.git
cd UVtools
```
### 3. Apply the patch
Copy `pcb-expose.patch` into the repo root, then:
```bash
git apply ../kicad2msla/UVtools/pcb-expose.patch
```
This does two things:
- Adds `UVtools.Cmd/Symbols/PcbExposeCommand.cs` (new `pcb-expose` subcommand)
- Registers it in `UVtools.Cmd/Program.cs`
Verify it applied cleanly:
```bash
git add .
git status
# modified: UVtools.Cmd/Program.cs
# new file: UVtools.Cmd/Symbols/PcbExposeCommand.cs
```
### 4. Restore & build
```bash
dotnet restore
dotnet publish UVtools.Cmd/UVtools.Cmd.csproj \
-c Release \
-r linux-x64 \
--self-contained false \
-o ./publish/
```
### 5. Copy the OpenCV native library
The published output doesn't include `libcvextern.so` — copy it from the repo:
```bash
cp build/platforms/linux-x64/libcvextern.so ./publish/
```
### 6. Test it
```bash
./publish/UVtoolsCmd pcb-expose \
~/proj/kicad2msla/Dummy.pm4n \
~/proj/kicad2msla/gerbers/Flow_Controller_Panel-Front.gtl \
--invert \
--mirror \
--exposure 120 \
--output ~/proj/kicad2msla/Flow_Controller_Panel-Front.pm4n
```
### 7. Install the wrapper
Copy `pcb-expose-patched.sh` somewhere on your `$PATH`, e.g.:
```bash
# Copy the script
cp ~/proj/kicad2msla/UVtools/pcb-expose-patched.sh ~/.local/bin
# Make it executable
chmod +x ~/.local/bin/pcb-expose-patched.sh
# Create a symlink
ln -s ~/.local/bin/pcb-expose-patched.sh ~/.local/bin/pcb-expose.sh
```
By default the wrapper looks for the build at `~/proj/UVtools/publish/UVtoolsCmd`.
If yours is elsewhere, either edit the `UVTOOLSCMD` variable at the top of the
script, or set it as an environment variable each time:
```bash
export UVTOOLSCMD=~/proj/UVtools/publish/UVtoolsCmd
```
### 8. Usage
```bash
pcb-expose.sh ~/proj/kicad2msla/Dummy.pm4n \
~/proj/kicad2msla/gerbers/Flow_Controller_Panel-Front.gtl \
--invert --mirror --exposure 120 \
--output ~/proj/kicad2msla/Flow_Controller_Panel-Front.pm4n
```
Multiple files / per-file polarity inversion / merging into one layer:
```bash
pcb-expose.sh Dummy.pm4n \
Board-F.Cu.gtl Board-B.Cu.gtl \
--merge --invert-polarity --mirror --exposure 120 \
--output Board-combined.pm4n
```
---
## Option B: Official release (no compiling required)
Once the maintainer's `FileArray` property ships in an official UVtools
release:
### 1. Download & install the release
Grab the Linux release from the
[UVtools releases page](https://github.com/sn4k3/UVtools/releases) and
extract it, e.g. to `~/UVtools/`. `libcvextern.so` is bundled in official
releases, so no extra steps are needed there.
### 2. Install the wrapper
```bash
# Copy the script
cp ~/proj/kicad2msla/UVtools/pcb-expose-official.sh ~/.local/bin
# Make it executable
chmod +x ~/.local/bin/pcb-expose-official.sh
# Create a symlink
ln -s ~/.local/bin/pcb-expose-official.sh ~/.local/bin/pcb-expose.sh
```
By default it looks for `~/UVtools/UVtoolsCmd`. Override with:
```bash
export UVTOOLSCMD=~/UVtools/UVtoolsCmd
```
### 3. Usage
Same interface as Option A (minus `--invert-polarity`, which the official
`FileArray` property doesn't support per-file):
```bash
pcb-expose.sh ~/proj/kicad2msla/Dummy.pm4n \
~/proj/kicad2msla/gerbers/Flow_Controller_Panel-Front.gtl \
--invert --mirror --exposure 120 \
--output ~/proj/kicad2msla/Flow_Controller_Panel-Front.pm4n
```
Internally this runs:
```bash
UVtoolsCmd run Dummy.pm4n PCBExposure \
-p FileArray=Board-F.Cu.gtl \
-p InvertColor=true \
-p Mirror=true \
-p ExposureTime=120 \
--output Board-F.Cu.pm4n
```
---
## Files in this bundle
| File | Purpose |
|---|---|
| `pcb-expose.patch` | `git apply`-able patch adding the `pcb-expose` command (Option A) |
| `pcb-expose-patched.sh` | Wrapper for the patched local build |
| `pcb-expose-official.sh` | Wrapper for the official release using `run` + `FileArray` |

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env bash
#
# Wrapper for the OFFICIAL UVtoolsCmd release, using the generic `run` command
# with the new FileArray-capable PCBExposure operation.
#
# Mirrors the interface of pcb-expose-patched.sh, but maps everything onto:
#
# UVtoolsCmd run <dummy-file> PCBExposure \
# -p FileArray=<gerber1> -p FileArray=<gerber2> ... \
# -p InvertColor=true -p Mirror=true -p ExposureTime=120 ... \
# --output <output-file>
#
# Usage:
# pcb-expose-official.sh <dummy-file> <gerber-files...> [options]
#
# Example:
# pcb-expose-official.sh Dummy.pm4n \
# gerbers/Flow_Controller_Panel-Front.gtl \
# --invert --mirror --exposure 120 \
# --output Flow_Controller_Panel-Front.pm4n
#
# Options:
# -e, --exposure <seconds> Sets ExposureTime
# -i, --invert Sets InvertColor=true
# -m, --mirror Sets Mirror=true
# --merge Sets MergeFiles=true
# --aa Sets EnableAntiAliasing=true
# --offset-x <mm> Sets OffsetX
# --offset-y <mm> Sets OffsetY
# -o, --output <file> Output file (default: overwrite dummy file)
#
# NOTE: --invert-polarity (per-file InvertPolarity) is NOT supported by this
# wrapper, because the official FileArray property only accepts plain
# file paths, not the InvertPolarity flag per file. If you need that,
# use pcb-expose-patched.sh instead.
#
# Configure the path to your official build below, or override via:
# UVTOOLSCMD=/path/to/UVtoolsCmd pcb-expose-official.sh ...
set -euo pipefail
UVTOOLSCMD="${UVTOOLSCMD:-$HOME/UVtools/UVtoolsCmd}"
if [ ! -x "$UVTOOLSCMD" ]; then
echo "Error: UVtoolsCmd not found or not executable at: $UVTOOLSCMD" >&2
echo "Set UVTOOLSCMD=/path/to/UVtoolsCmd or edit this script." >&2
exit 1
fi
if [ $# -lt 2 ]; then
echo "Usage: $0 <dummy-file> <gerber-files...> [options]" >&2
exit 1
fi
DUMMY_FILE="$1"
shift
GERBERS=()
PROPS=() # FileArray entries + scalar properties, built below
OUTPUT_FILE=""
EXPOSURE=""
INVERT="false"
MIRROR="false"
MERGE="false"
AA="false"
OFFSET_X=""
OFFSET_Y=""
INVERT_POLARITY="false"
while [ $# -gt 0 ]; do
case "$1" in
-e|--exposure)
EXPOSURE="$2"; shift 2 ;;
-i|--invert)
INVERT="true"; shift ;;
-m|--mirror)
MIRROR="true"; shift ;;
--merge)
MERGE="true"; shift ;;
--aa)
AA="true"; shift ;;
--offset-x)
OFFSET_X="$2"; shift 2 ;;
--offset-y)
OFFSET_Y="$2"; shift 2 ;;
--invert-polarity)
INVERT_POLARITY="true"; shift ;;
-o|--output)
OUTPUT_FILE="$2"; shift 2 ;;
-*)
echo "Unknown option: $1" >&2
exit 1 ;;
*)
GERBERS+=("$1"); shift ;;
esac
done
if [ "$INVERT_POLARITY" = "true" ]; then
echo "Warning: --invert-polarity is not supported via the official FileArray" >&2
echo " property and will be ignored. Use pcb-expose-patched.sh instead." >&2
fi
if [ ${#GERBERS[@]} -eq 0 ]; then
echo "Error: specify at least one gerber file." >&2
exit 1
fi
for g in "${GERBERS[@]}"; do
if [ ! -f "$g" ]; then
echo "Error: gerber file not found: $g" >&2
exit 1
fi
done
# Build -p FileArray=... entries
for g in "${GERBERS[@]}"; do
PROPS+=(-p "FileArray=$g")
done
[ -n "$EXPOSURE" ] && PROPS+=(-p "ExposureTime=$EXPOSURE")
[ "$INVERT" = "true" ] && PROPS+=(-p "InvertColor=true")
[ "$MIRROR" = "true" ] && PROPS+=(-p "Mirror=true")
[ "$MERGE" = "true" ] && PROPS+=(-p "MergeFiles=true")
[ "$AA" = "true" ] && PROPS+=(-p "EnableAntiAliasing=true")
[ -n "$OFFSET_X" ] && PROPS+=(-p "OffsetX=$OFFSET_X")
[ -n "$OFFSET_Y" ] && PROPS+=(-p "OffsetY=$OFFSET_Y")
CMD=("$UVTOOLSCMD" run "$DUMMY_FILE" PCBExposure "${PROPS[@]}")
[ -n "$OUTPUT_FILE" ] && CMD+=(--output "$OUTPUT_FILE")
echo "+ ${CMD[*]}" >&2
exec "${CMD[@]}"

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
#
# Wrapper for a locally patched UVtoolsCmd build that provides the
# native `pcb-expose` command (see pcb-expose.patch).
#
# Usage:
# pcb-expose-patched.sh <dummy-file> <gerber-files...> [options]
#
# Example:
# pcb-expose-patched.sh Dummy.pm4n \
# Board-F.Cu.gtl \
# --invert --mirror --exposure 120 \
# --output Board-F.Cu.pm4n
#
# Configure the path to your patched build below, or override via:
# UVTOOLSCMD=/path/to/UVtoolsCmd pcb-expose-patched.sh ...
set -euo pipefail
UVTOOLSCMD="${UVTOOLSCMD:-$HOME/proj/UVtools/publish/UVtoolsCmd}"
if [ ! -x "$UVTOOLSCMD" ]; then
echo "Error: UVtoolsCmd not found or not executable at: $UVTOOLSCMD" >&2
echo "Set UVTOOLSCMD=/path/to/UVtoolsCmd or edit this script." >&2
exit 1
fi
exec "$UVTOOLSCMD" pcb-expose "$@"

173
UVtools/pcb-expose.patch Normal file
View File

@@ -0,0 +1,173 @@
diff --git a/UVtools.Cmd/Program.cs b/UVtools.Cmd/Program.cs
index 6d2d1ff..38da048 100644
--- a/UVtools.Cmd/Program.cs
+++ b/UVtools.Cmd/Program.cs
@@ -45,6 +45,7 @@ public static async Task<int> Main(params string[] args)
ExtractCommand.CreateCommand(),
CopyParametersCommand.CreateCommand(),
SetThumbnailCommand.CreateCommand(),
+ PcbExposeCommand.CreateCommand(),
CompareCommand.CreateCommand(),
diff --git a/UVtools.Cmd/Symbols/PcbExposeCommand.cs b/UVtools.Cmd/Symbols/PcbExposeCommand.cs
new file mode 100644
index 0000000..e67e876
--- /dev/null
+++ b/UVtools.Cmd/Symbols/PcbExposeCommand.cs
@@ -0,0 +1,155 @@
+/*
+ * GNU AFFERO GENERAL PUBLIC LICENSE
+ * Version 3, 19 November 2007
+ * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ * Everyone is permitted to copy and distribute verbatim copies
+ * of this license document, but changing it is not allowed.
+ */
+
+using System;
+using System.CommandLine;
+using System.IO;
+using UVtools.Core.FileFormats;
+using UVtools.Core.Operations;
+
+namespace UVtools.Cmd.Symbols;
+
+internal static class PcbExposeCommand
+{
+ internal static Command CreateCommand()
+ {
+ var gerberArgument = new Argument<FileInfo[]>("gerber-files")
+ {
+ Description = "One or more Gerber/drill files (.gbr .gtl .gbl .gts .gbs .drl etc.)",
+ };
+
+ var exposureOption = new Option<decimal>("-e", "--exposure")
+ {
+ Description = "Exposure time in seconds (0 = keep value from input file)",
+ DefaultValueFactory = _ => 0m,
+ };
+
+ var invertOption = new Option<bool>("-i", "--invert")
+ {
+ Description = "Invert image color",
+ };
+
+ var mirrorOption = new Option<bool>("-m", "--mirror")
+ {
+ Description = "Mirror the image horizontally",
+ };
+
+ var mergeOption = new Option<bool>("--merge")
+ {
+ Description = "Merge all gerber files into a single layer",
+ };
+
+ var invertPolarityOption = new Option<bool>("--invert-polarity")
+ {
+ Description = "Invert Gerber drawing polarity for all supplied files",
+ };
+
+ var antiAliasingOption = new Option<bool>("--aa")
+ {
+ Description = "Enable anti-aliasing",
+ };
+
+ var offsetXOption = new Option<decimal>("--offset-x")
+ {
+ Description = "X offset in mm",
+ DefaultValueFactory = _ => 0m,
+ };
+
+ var offsetYOption = new Option<decimal>("--offset-y")
+ {
+ Description = "Y offset in mm",
+ DefaultValueFactory = _ => 0m,
+ };
+
+ var command = new Command("pcb-expose", "Inject a Gerber file into a slicer file for PCB UV exposure")
+ {
+ GlobalArguments.InputFileArgument,
+ gerberArgument,
+
+ exposureOption,
+ invertOption,
+ mirrorOption,
+ mergeOption,
+ invertPolarityOption,
+ antiAliasingOption,
+ offsetXOption,
+ offsetYOption,
+ GlobalOptions.OutputFile,
+ };
+
+ command.SetAction(result =>
+ {
+ var inputFile = result.GetRequiredValue(GlobalArguments.InputFileArgument);
+ var gerbers = result.GetRequiredValue(gerberArgument);
+ var exposure = result.GetValue(exposureOption);
+ var invert = result.GetValue(invertOption);
+ var mirror = result.GetValue(mirrorOption);
+ var merge = result.GetValue(mergeOption);
+ var invertPolarity = result.GetValue(invertPolarityOption);
+ var aa = result.GetValue(antiAliasingOption);
+ var offsetX = result.GetValue(offsetXOption);
+ var offsetY = result.GetValue(offsetYOption);
+ var outputFile = result.GetValue(GlobalOptions.OutputFile);
+
+ if (gerbers.Length == 0)
+ {
+ Program.WriteLineError("Specify at least one gerber file.");
+ return;
+ }
+
+ foreach (var g in gerbers)
+ {
+ if (!g.Exists)
+ {
+ Program.WriteLineError($"Gerber file not found: {g.FullName}");
+ return;
+ }
+ }
+
+ var slicerFile = Program.OpenInputFile(inputFile);
+
+ var op = new OperationPCBExposure(slicerFile)
+ {
+ MergeFiles = merge,
+ Mirror = mirror,
+ InvertColor = invert,
+ EnableAntiAliasing = aa,
+ OffsetX = offsetX,
+ OffsetY = offsetY,
+ };
+
+ if (exposure > 0)
+ op.ExposureTime = exposure;
+
+ foreach (var g in gerbers)
+ op.Files.Add(new OperationPCBExposure.PCBExposureFile(g.FullName, invertPolarity));
+
+ var spawnError = op.ValidateSpawn();
+ if (!string.IsNullOrWhiteSpace(spawnError))
+ {
+ Program.WriteLineError(spawnError);
+ return;
+ }
+
+ var validationError = op.ValidateInternally();
+ if (!string.IsNullOrWhiteSpace(validationError))
+ {
+ Program.WriteLineError(validationError.TrimEnd());
+ return;
+ }
+
+ Program.ProgressBarWork(
+ $"PCB exposure: {string.Join(", ", Array.ConvertAll(gerbers, g => g.Name))}",
+ () => op.Execute(Program.Progress));
+
+ Program.SaveFile(slicerFile, outputFile);
+ });
+
+ return command;
+ }
+}