Functions/GenXdev.Windows/Save-DesktopScreenShot.cs

// ################################################################################
// Part of PowerShell module : GenXdev.Windows
// Original cmdlet filename : Save-DesktopScreenShot.cs
// Original author : René Vaessen / GenXdev
// Version : 1.302.2025
// ################################################################################
// Copyright (c) René Vaessen / GenXdev
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ################################################################################
 
 
 
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Windows.Forms;
 
namespace GenXdev.Windows
{
    /// <summary>
    /// <para type="synopsis">
    /// Captures a screenshot of the specified monitor and saves it to the specified path.
    /// </para>
    ///
    /// <para type="description">
    /// Takes a screenshot of the desktop from the specified monitor and saves it as an
    /// image file. Supports multiple monitors and automatically detects the correct
    /// screen bounds and scaling factors for high-DPI displays.
    /// </para>
    ///
    /// <para type="description">
    /// PARAMETERS
    /// </para>
    ///
    /// <para type="description">
    /// -Monitor &lt;Int32&gt;<br/>
    /// Monitor index to capture. 0 = primary monitor, 1+ = specific monitor (1-based),
    /// -1 = all monitors as a single image.<br/>
    /// - <b>Position</b>: 0<br/>
    /// - <b>Required</b>: true<br/>
    /// </para>
    ///
    /// <para type="description">
    /// -OutputPath &lt;String&gt;<br/>
    /// Full path where the screenshot image will be saved. File extension determines
    /// format (png, jpg, bmp, gif, tiff).<br/>
    /// - <b>Position</b>: 1<br/>
    /// - <b>Required</b>: true<br/>
    /// </para>
    ///
    /// <para type="description">
    /// -Quality &lt;Int32&gt;<br/>
    /// JPEG quality level (1-100). Only applies when saving as JPEG format.<br/>
    /// - <b>Default</b>: 90<br/>
    /// </para>
    ///
    /// <example>
    /// <para>Capture primary monitor screenshot</para>
    /// <para>Captures a screenshot of the primary monitor and saves it as PNG.</para>
    /// <code>
    /// Save-DesktopScreenShot -Monitor 0 -OutputPath "C:\Screenshots\primary.png"
    /// </code>
    /// </example>
    ///
    /// <example>
    /// <para>Capture second monitor screenshot as JPEG</para>
    /// <para>Captures a screenshot of the second monitor with high quality JPEG.</para>
    /// <code>
    /// Save-DesktopScreenShot -Monitor 1 -OutputPath "C:\Screenshots\monitor2.jpg" -Quality 95
    /// </code>
    /// </example>
    ///
    /// <example>
    /// <para>Capture all monitors as single image</para>
    /// <para>Captures all monitors combined into a single screenshot image.</para>
    /// <code>
    /// Save-DesktopScreenShot -Monitor -1 -OutputPath "C:\Screenshots\all_monitors.png"
    /// </code>
    /// </example>
    /// </summary>
    [Cmdlet("Save", "DesktopScreenShot")]
    [OutputType(typeof(FileInfo))]
    public class SaveDesktopScreenShotCommand : PSGenXdevCmdlet
    {
        // Windows API declarations for direct desktop capture
        [DllImport("user32.dll")]
        private static extern IntPtr GetDesktopWindow();
 
        // Use GetDC(IntPtr.Zero) to get a DC for the entire screen (virtual screen across monitors)
        [DllImport("user32.dll")]
        private static extern IntPtr GetDC(IntPtr hWnd);
 
        [DllImport("user32.dll")]
        private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
 
        [DllImport("user32.dll")]
        private static extern int GetSystemMetrics(int nIndex);
 
        [DllImport("gdi32.dll")]
        private static extern IntPtr CreateCompatibleDC(IntPtr hdc);
 
        [DllImport("gdi32.dll")]
        private static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int cx, int cy);
 
        [DllImport("gdi32.dll")]
        private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
 
        [DllImport("gdi32.dll")]
        private static extern bool BitBlt(IntPtr hdc, int x, int y, int cx, int cy, IntPtr hdcSrc, int x1, int y1, uint rop);
 
        [DllImport("gdi32.dll")]
        private static extern bool DeleteDC(IntPtr hdc);
 
        [DllImport("gdi32.dll")]
        private static extern bool DeleteObject(IntPtr hObject);
 
        // BitBlt operation constants
        private const uint SRCCOPY = 0x00CC0020;
 
        // System metrics for virtual screen
        private const int SM_XVIRTUALSCREEN = 76;
        private const int SM_YVIRTUALSCREEN = 77;
        private const int SM_CXVIRTUALSCREEN = 78;
        private const int SM_CYVIRTUALSCREEN = 79;
 
        /// <summary>
        /// Monitor index to capture (0=primary, 1+=specific monitor, -1=all monitors)
        /// </summary>
        [Parameter(
            Position = 0,
            Mandatory = true,
            ValueFromPipelineByPropertyName = true,
            HelpMessage = "Monitor index to capture (0=primary, 1+=specific monitor, -1=all monitors)"
        )]
        public int Monitor { get; set; }
 
        /// <summary>
        /// Full path where the screenshot will be saved
        /// </summary>
        [Parameter(
            Position = 1,
            Mandatory = true,
            ValueFromPipelineByPropertyName = true,
            HelpMessage = "Full path where the screenshot will be saved"
        )]
        [ValidateNotNullOrEmpty]
        public string OutputPath { get; set; }
 
        /// <summary>
        /// JPEG quality level (1-100)
        /// </summary>
        [Parameter(
            HelpMessage = "JPEG quality level (1-100), only applies to JPEG format"
        )]
        [ValidateRange(1, 100)]
        public int Quality { get; set; } = 90;
 
        /// <summary>
        /// Begin processing - initialization and validation
        /// </summary>
        protected override void BeginProcessing()
        {
            WriteVerbose("Starting desktop screenshot capture");
 
            // Validate output directory exists - use base class ExpandPath for proper path handling
            var expandedPath = ExpandPath(OutputPath, CreateDirectory: true, DeleteExistingFile: true);
            if (string.IsNullOrEmpty(expandedPath))
            {
                throw new ArgumentException($"Could not expand output path: {OutputPath}");
            }
 
            // Update OutputPath to use the expanded path
            OutputPath = expandedPath;
 
            // Validate monitor index
            var screenCount = Screen.AllScreens.Length;
            if (Monitor >= 0 && Monitor >= screenCount)
            {
                throw new ArgumentException($"Monitor index {Monitor} is invalid. Available monitors: 0-{screenCount - 1}");
            }
 
            WriteVerbose($"Available monitors: {screenCount}");
            WriteVerbose($"Target monitor: {Monitor}");
            WriteVerbose($"Output path: {OutputPath}");
        }
 
        /// <summary>
        /// Process record - main screenshot capture logic using Windows API
        /// </summary>
        protected override void ProcessRecord()
        {
            try
            {
                Rectangle captureArea;
 
                if (Monitor == -1)
                {
                    // Capture all monitors as single image
                    captureArea = GetVirtualScreenBounds();
                    WriteVerbose($"Capturing all monitors: {captureArea.Width}x{captureArea.Height} at ({captureArea.X}, {captureArea.Y})");
                }
                else
                {
                    // Capture specific monitor
                    var screens = Screen.AllScreens;
                    var targetScreen = Monitor == 0 ? Screen.PrimaryScreen : screens[Monitor];
                    captureArea = targetScreen.Bounds;
                    WriteVerbose($"Capturing monitor {Monitor}: {captureArea.Width}x{captureArea.Height} at ({captureArea.X}, {captureArea.Y})");
                }
 
                // Capture screenshot using Windows API with full virtual screen DC and offset handling
                Bitmap screenshot = CaptureDesktopUsingAPI(captureArea);
 
                // Save the screenshot
                SaveScreenshot(screenshot, OutputPath);
 
                WriteVerbose($"Screenshot saved successfully to: {OutputPath}");
 
                // Return FileInfo object of the saved file
                WriteObject(new FileInfo(OutputPath));
            }
            catch (Exception ex)
            {
                WriteError(new ErrorRecord(
                    ex,
                    "ScreenshotCaptureFailed",
                    ErrorCategory.InvalidOperation,
                    null));
            }
        }
 
        /// <summary>
        /// Captures desktop using Windows API that actually uses the desktop handle
        /// </summary>
        private Bitmap CaptureDesktopUsingAPI(Rectangle captureArea)
        {
            // Get the virtual screen offsets to map negative coordinates correctly
            int virtualLeft = GetSystemMetrics(SM_XVIRTUALSCREEN);
            int virtualTop = GetSystemMetrics(SM_YVIRTUALSCREEN);
 
            IntPtr screenDC = IntPtr.Zero; // DC for full virtual screen
            IntPtr memoryDC = IntPtr.Zero;
            IntPtr hBitmap = IntPtr.Zero;
            IntPtr hOld = IntPtr.Zero;
 
            try
            {
                // Get a DC for the entire screen (all monitors)
                screenDC = GetDC(IntPtr.Zero);
                if (screenDC == IntPtr.Zero)
                {
                    throw new InvalidOperationException("Failed to get screen device context");
                }
 
                WriteVerbose($"Virtual screen origin: ({virtualLeft}, {virtualTop})");
 
                // Create compatible memory DC
                memoryDC = CreateCompatibleDC(screenDC);
                if (memoryDC == IntPtr.Zero)
                {
                    throw new InvalidOperationException("Failed to create compatible device context");
                }
 
                // Create a bitmap sized to the capture area only (destination size)
                hBitmap = CreateCompatibleBitmap(screenDC, captureArea.Width, captureArea.Height);
                if (hBitmap == IntPtr.Zero)
                {
                    throw new InvalidOperationException("Failed to create compatible bitmap");
                }
 
                // Select bitmap into memory DC
                hOld = SelectObject(memoryDC, hBitmap);
                if (hOld == IntPtr.Zero)
                {
                    throw new InvalidOperationException("Failed to select bitmap into device context");
                }
 
                // Compute source coordinates in the screen DC (adjust for virtual origin)
                int srcX = captureArea.X - virtualLeft;
                int srcY = captureArea.Y - virtualTop;
 
                WriteVerbose($"BitBlt src=({srcX}, {srcY}) size={captureArea.Width}x{captureArea.Height}");
 
                // Perform the bit-block transfer from full screen DC to memory DC
                bool success = BitBlt(
                    memoryDC, 0, 0, captureArea.Width, captureArea.Height,
                    screenDC, srcX, srcY, SRCCOPY);
 
                if (!success)
                {
                    throw new InvalidOperationException("BitBlt operation failed");
                }
 
                // Convert HBITMAP to managed Bitmap
                Bitmap bitmap = Image.FromHbitmap(hBitmap);
 
                WriteVerbose($"Successfully created bitmap: {bitmap.Width}x{bitmap.Height}");
 
                return bitmap;
            }
            finally
            {
                // Restore and cleanup GDI objects
                if (hOld != IntPtr.Zero)
                    SelectObject(memoryDC, hOld);
 
                if (hBitmap != IntPtr.Zero)
                    DeleteObject(hBitmap);
 
                if (memoryDC != IntPtr.Zero)
                    DeleteDC(memoryDC);
 
                if (screenDC != IntPtr.Zero)
                    ReleaseDC(IntPtr.Zero, screenDC);
            }
        }
 
        /// <summary>
        /// Gets the bounds of the virtual screen (all monitors combined)
        /// </summary>
        private Rectangle GetVirtualScreenBounds()
        {
            int left = int.MaxValue;
            int top = int.MaxValue;
            int right = int.MinValue;
            int bottom = int.MinValue;
 
            foreach (var screen in Screen.AllScreens)
            {
                left = Math.Min(left, screen.Bounds.Left);
                top = Math.Min(top, screen.Bounds.Top);
                right = Math.Max(right, screen.Bounds.Right);
                bottom = Math.Max(bottom, screen.Bounds.Bottom);
            }
 
            return new Rectangle(left, top, right - left, bottom - top);
        }
 
        /// <summary>
        /// Saves the screenshot bitmap to the specified path with appropriate format
        /// </summary>
        private void SaveScreenshot(Bitmap bitmap, string path)
        {
            var extension = Path.GetExtension(path).ToLowerInvariant();
            ImageFormat format;
            EncoderParameters encoderParams = null;
 
            switch (extension)
            {
                case ".jpg":
                case ".jpeg":
                    format = ImageFormat.Jpeg;
                    encoderParams = GetJpegEncoderParams(Quality);
                    WriteVerbose($"Saving as JPEG with quality {Quality}");
                    break;
                case ".bmp":
                    format = ImageFormat.Bmp;
                    WriteVerbose("Saving as BMP");
                    break;
                case ".gif":
                    format = ImageFormat.Gif;
                    WriteVerbose("Saving as GIF");
                    break;
                case ".tiff":
                case ".tif":
                    format = ImageFormat.Tiff;
                    WriteVerbose("Saving as TIFF");
                    break;
                case ".png":
                default:
                    format = ImageFormat.Png;
                    WriteVerbose("Saving as PNG");
                    break;
            }
 
            if (encoderParams != null)
            {
                var encoder = GetEncoder(format);
                if (encoder != null)
                {
                    bitmap.Save(path, encoder, encoderParams);
                }
                else
                {
                    bitmap.Save(path, format);
                }
                encoderParams.Dispose();
            }
            else
            {
                bitmap.Save(path, format);
            }
 
            bitmap.Dispose();
        }
 
        /// <summary>
        /// Gets JPEG encoder parameters for quality setting
        /// </summary>
        private EncoderParameters GetJpegEncoderParams(int quality)
        {
            var encoderParams = new EncoderParameters(1);
            encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
            return encoderParams;
        }
 
        /// <summary>
        /// Gets the image codec encoder for the specified format
        /// </summary>
        private ImageCodecInfo GetEncoder(ImageFormat format)
        {
            var codecs = ImageCodecInfo.GetImageEncoders();
            foreach (var codec in codecs)
            {
                if (codec.FormatID == format.Guid)
                {
                    return codec;
                }
            }
            return null;
        }
 
        /// <summary>
        /// End processing - cleanup logic
        /// </summary>
        protected override void EndProcessing()
        {
            WriteVerbose("Desktop screenshot capture completed");
        }
    }
}