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;
+    }
+}
