File: System\Drawing\Printing\PrinterSettings.cs
Web Access
Project: src\src\System.Drawing.Common\src\System.Drawing.Common.csproj (System.Drawing.Common)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Win32.Graphics.Printing;
using Windows.Win32.UI.Controls.Dialogs;
 
namespace System.Drawing.Printing;
 
/// <summary>
///  Information about how a document should be printed, including which printer to print it on.
/// </summary>
public unsafe partial class PrinterSettings : ICloneable
{
    private string? _printerName; // default printer.
    private string _driverName = "";
    private ushort _extraBytes;
    private byte[]? _extraInfo;
 
    private short _copies = -1;
    private Duplex _duplex = Duplex.Default;
    private TriState _collate = TriState.Default;
    private readonly PageSettings _defaultPageSettings;
    private int _fromPage;
    private int _toPage;
    private int _maxPage = 9999;
    private int _minPage;
    private PrintRange _printRange;
 
    private ushort _devmodeBytes;
    private byte[]? _cachedDevmode;
 
    /// <summary>
    ///  Initializes a new instance of the <see cref='PrinterSettings'/> class.
    /// </summary>
    public PrinterSettings()
    {
        _defaultPageSettings = new PageSettings(this);
    }
 
    /// <summary>
    ///  Gets a value indicating whether the printer supports duplex (double-sided) printing.
    /// </summary>
    public bool CanDuplex => DeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_DUPLEX) == 1;
 
    /// <summary>
    ///  Gets or sets the number of copies to print.
    /// </summary>
    public short Copies
    {
        get => _copies != -1 ? _copies : GetModeField(ModeField.Copies, 1);
        set
        {
            if (value < 0)
            {
                throw new ArgumentException(SR.Format(SR.InvalidLowBoundArgumentEx, nameof(value), value, 0));
            }
 
            _copies = value;
        }
    }
 
    /// <summary>
    ///  Gets or sets a value indicating whether the print out is collated.
    /// </summary>
    public bool Collate
    {
        get => _collate.IsDefault
            ? GetModeField(ModeField.Collate, (short)DEVMODE_COLLATE.DMCOLLATE_FALSE) == (short)DEVMODE_COLLATE.DMCOLLATE_TRUE
            : (bool)_collate;
        set => _collate = value;
    }
 
    /// <summary>
    ///  Gets the default page settings for this printer.
    /// </summary>
    public PageSettings DefaultPageSettings => _defaultPageSettings;
 
    // As far as I can tell, Windows no longer pays attention to driver names and output ports.
    // But I'm leaving this code in place in case I'm wrong.
    internal string DriverName => _driverName;
 
    /// <summary>
    ///  Gets or sets the printer's duplex setting.
    /// </summary>
    public Duplex Duplex
    {
        get => _duplex != Duplex.Default ? _duplex : (Duplex)GetModeField(ModeField.Duplex, (short)DEVMODE_DUPLEX.DMDUP_SIMPLEX);
        set
        {
            if (value is < Duplex.Default or > Duplex.Horizontal)
            {
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(Duplex));
            }
 
            _duplex = value;
        }
    }
 
    /// <summary>
    ///  Gets or sets the first page to print.
    /// </summary>
    public int FromPage
    {
        get => _fromPage;
        set
        {
            if (value < 0)
            {
                throw new ArgumentException(SR.Format(SR.InvalidLowBoundArgumentEx, nameof(value), value, 0));
            }
 
            _fromPage = value;
        }
    }
 
    /// <summary>
    ///  Gets the names of all printers installed on the machine.
    /// </summary>
    public static StringCollection InstalledPrinters
    {
        get
        {
            // Note: The call to get the size of the buffer required for level 5 does not work properly on NT platforms.
            const uint Level = 4;
 
            uint bytesNeeded;
            uint count;
 
            bool success = PInvoke.EnumPrinters(
                PInvoke.PRINTER_ENUM_LOCAL | PInvoke.PRINTER_ENUM_CONNECTIONS,
                Name: null,
                Level,
                pPrinterEnum: null,
                0,
                &bytesNeeded,
                &count);
 
            if (!success)
            {
                WIN32_ERROR error = (WIN32_ERROR)Marshal.GetLastPInvokeError();
                if (error != WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER)
                {
                    throw new Win32Exception((int)error);
                }
            }
 
            using BufferScope<byte> buffer = new((int)bytesNeeded);
 
            fixed (byte* b = buffer)
            {
                success = PInvoke.EnumPrinters(
                    PInvoke.PRINTER_ENUM_LOCAL | PInvoke.PRINTER_ENUM_CONNECTIONS,
                    Name: null,
                    Level,
                    b,
                    (uint)buffer.Length,
                    &bytesNeeded,
                    &count);
 
                if (!success)
                {
                    throw new Win32Exception();
                }
 
                string[] array = new string[count];
 
                ReadOnlySpan<PRINTER_INFO_4W> info = new(b, (int)count);
 
                for (int i = 0; i < count; i++)
                {
                    array[i] = new string(info[i].pPrinterName);
                }
 
                return new StringCollection(array);
            }
        }
    }
 
    /// <summary>
    ///  Gets a value indicating whether the <see cref='PrinterName'/> property designates the default printer.
    /// </summary>
    public bool IsDefaultPrinter => _printerName is null || _printerName == GetDefaultPrinterName();
 
    /// <summary>
    ///  Gets a value indicating whether the printer is a plotter, as opposed to a raster printer.
    /// </summary>
    public bool IsPlotter => GetDeviceCaps(GET_DEVICE_CAPS_INDEX.TECHNOLOGY) == PInvoke.DT_PLOTTER;
 
    /// <summary>
    ///  Gets a value indicating whether the <see cref='PrinterName'/> property designates a valid printer.
    /// </summary>
    public bool IsValid => DeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_COPIES) != -1;
 
    /// <summary>
    ///  Gets the angle, in degrees, which the portrait orientation is rotated to produce the landscape orientation.
    /// </summary>
    public int LandscapeAngle => DeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_ORIENTATION, defaultValue: 0);
 
    /// <summary>
    ///  Gets the maximum number of copies allowed by the printer.
    /// </summary>
    public int MaximumCopies => DeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_COPIES, defaultValue: 1);
 
    /// <summary>
    ///  Gets or sets the highest <see cref='FromPage'/> or <see cref='ToPage'/> which may be selected in a print dialog box.
    /// </summary>
    public int MaximumPage
    {
        get => _maxPage;
        set
        {
            if (value < 0)
            {
                throw new ArgumentException(SR.Format(SR.InvalidLowBoundArgumentEx, nameof(value), value, 0));
            }
 
            _maxPage = value;
        }
    }
 
    /// <summary>
    /// Gets or sets the lowest <see cref='FromPage'/> or <see cref='ToPage'/> which may be selected in a print dialog box.
    /// </summary>
    public int MinimumPage
    {
        get => _minPage;
        set
        {
            if (value < 0)
            {
                throw new ArgumentException(SR.Format(SR.InvalidLowBoundArgumentEx, nameof(value), value, 0));
            }
 
            _minPage = value;
        }
    }
 
    internal string OutputPort { get; set; } = "";
 
    /// <summary>
    ///  Indicates the name of the printer file.
    /// </summary>
    public string PrintFileName
    {
        get => OutputPort;
        set
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentNullException(value);
            }
 
            OutputPort = value;
        }
    }
 
    /// <summary>
    ///  Gets the paper sizes supported by this printer.
    /// </summary>
    public PaperSizeCollection PaperSizes => new(Get_PaperSizes());
 
    /// <summary>
    ///  Gets the paper sources available on this printer.
    /// </summary>
    public PaperSourceCollection PaperSources => new(Get_PaperSources());
 
    /// <summary>
    ///  Whether the print dialog has been displayed. In SafePrinting mode, a print dialog is required to print.
    ///  After printing, this property is set to false if the program does not have AllPrinting; this guarantees
    ///  a document is only printed once each time the print dialog is shown.
    /// </summary>
    internal bool PrintDialogDisplayed { get; set; }
 
    /// <summary>
    ///  Gets or sets the pages the user has asked to print.
    /// </summary>
    public PrintRange PrintRange
    {
        get => _printRange;
        set
        {
            if (!Enum.IsDefined(value))
            {
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(PrintRange));
            }
 
            _printRange = value;
        }
    }
 
    /// <summary>
    ///  Indicates whether to print to a file instead of a port.
    /// </summary>
    public bool PrintToFile { get; set; }
 
    /// <summary>
    ///  Gets or sets the name of the printer.
    /// </summary>
    public string PrinterName
    {
        get => PrinterNameInternal;
        set => PrinterNameInternal = value;
    }
 
    private string PrinterNameInternal
    {
        get => _printerName ?? GetDefaultPrinterName();
        set
        {
            // Reset the DevMode and extra bytes.
            _cachedDevmode = null;
            _extraInfo = null;
            _printerName = value;
        }
    }
 
    /// <summary>
    ///  Gets the resolutions supported by this printer.
    /// </summary>
    public PrinterResolutionCollection PrinterResolutions => new(Get_PrinterResolutions());
 
    /// <summary>
    ///  If the image is a JPEG or a PNG (Image.RawFormat) and the printer returns true from
    ///  ExtEscape(CHECKJPEGFORMAT) or ExtEscape(CHECKPNGFORMAT) then this function returns true.
    /// </summary>
    public bool IsDirectPrintingSupported(ImageFormat imageFormat)
    {
        if (!imageFormat.Equals(ImageFormat.Jpeg) && !imageFormat.Equals(ImageFormat.Png))
        {
            return false;
        }
 
        using var hdc = CreateInformationContext(DefaultPageSettings);
        return IsDirectPrintingSupported(hdc, imageFormat, out _);
    }
 
    private static bool IsDirectPrintingSupported(HDC hdc, ImageFormat imageFormat, out int escapeFunction)
    {
        Debug.Assert(imageFormat == ImageFormat.Jpeg || imageFormat == ImageFormat.Png);
 
        escapeFunction = imageFormat.Equals(ImageFormat.Jpeg)
            ? (int)PInvoke.CHECKJPEGFORMAT
            : (int)PInvoke.CHECKPNGFORMAT;
 
        fixed (int* function = &escapeFunction)
        {
            int result = PInvoke.ExtEscape(
                hdc,
                (int)PInvoke.QUERYESCSUPPORT,
                sizeof(int),
                (PCSTR)(void*)&function,
                0,
                null);
 
            return result != 0;
        }
    }
 
    /// <summary>
    ///  This method utilizes the CHECKJPEGFORMAT/CHECKPNGFORMAT printer escape functions
    ///  to determine whether the printer can handle a JPEG image.
    ///
    ///  If the image is a JPEG or a PNG (Image.RawFormat) and the printer returns true
    ///  from ExtEscape(CHECKJPEGFORMAT) or ExtEscape(CHECKPNGFORMAT) then this function returns true.
    /// </summary>
    public bool IsDirectPrintingSupported(Image image)
    {
        ImageFormat imageFormat = image.RawFormat;
 
        if (!imageFormat.Equals(ImageFormat.Jpeg) && !imageFormat.Equals(ImageFormat.Png))
        {
            return false;
        }
 
        using var hdc = CreateInformationContext(DefaultPageSettings);
 
        if (!IsDirectPrintingSupported(hdc, imageFormat, out int escapeFunction))
        {
            return false;
        }
 
        using MemoryStream stream = new();
        image.Save(stream, image.RawFormat);
 
        byte[] pvImage = stream.ToArray();
 
        fixed (byte* b = pvImage)
        {
            uint driverReturnValue;
            int result = PInvoke.ExtEscape(
                hdc,
                escapeFunction,
                pvImage.Length,
                (PCSTR)b,
                sizeof(uint),
                (PSTR)(void*)&driverReturnValue);
 
            // -1 means some sort of failure
            Debug.Assert(result != -1);
            return result == 1;
        }
    }
 
    /// <summary>
    ///  Gets a value indicating whether the printer supports color printing.
    /// </summary>
    public bool SupportsColor => DeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_COLORDEVICE) == 1;
 
    /// <summary>
    ///  Gets or sets the last page to print.
    /// </summary>
    public int ToPage
    {
        get => _toPage;
        set
        {
            if (value < 0)
            {
                throw new ArgumentException(SR.Format(SR.InvalidLowBoundArgumentEx, nameof(value), value, 0));
            }
 
            _toPage = value;
        }
    }
 
    /// <summary>
    ///  Creates an identical copy of this object.
    /// </summary>
    public object Clone()
    {
        PrinterSettings clone = (PrinterSettings)MemberwiseClone();
        clone.PrintDialogDisplayed = false;
        return clone;
    }
 
    internal CreateDcScope CreateDeviceContext(PageSettings pageSettings)
    {
        HGLOBAL modeHandle = GetHdevmodeInternal();
 
        try
        {
            // Copy the PageSettings to the DEVMODE.
            pageSettings.CopyToHdevmode(modeHandle);
            return CreateDeviceContext(modeHandle);
        }
        finally
        {
            PInvokeCore.GlobalFree(modeHandle);
        }
    }
 
    internal CreateDcScope CreateDeviceContext(HGLOBAL hdevmode)
    {
        DEVMODEW* devmode = (DEVMODEW*)PInvokeCore.GlobalLock(hdevmode);
        CreateDcScope hdc = new(DriverName, PrinterNameInternal, devmode, informationOnly: false);
        PInvokeCore.GlobalUnlock(hdevmode);
        return hdc;
    }
 
    // A read-only DC, which is faster than CreateHdc
    internal CreateDcScope CreateInformationContext(PageSettings pageSettings)
    {
        HGLOBAL modeHandle = GetHdevmodeInternal();
 
        try
        {
            // Copy the PageSettings to the DEVMODE.
            pageSettings.CopyToHdevmode(modeHandle);
            return CreateInformationContext(modeHandle);
        }
        finally
        {
            PInvokeCore.GlobalFree(modeHandle);
        }
    }
 
    // A read-only DC, which is faster than CreateHdc
    internal unsafe CreateDcScope CreateInformationContext(HGLOBAL hdevmode)
    {
        void* modePointer = PInvokeCore.GlobalLock(hdevmode);
        CreateDcScope dc = new(DriverName, PrinterNameInternal, (DEVMODEW*)modePointer, informationOnly: true);
        PInvokeCore.GlobalUnlock(hdevmode);
        return dc;
    }
 
    public Graphics CreateMeasurementGraphics() => CreateMeasurementGraphics(DefaultPageSettings);
 
    // whatever the call stack calling HardMarginX and HardMarginY here is safe
    public Graphics CreateMeasurementGraphics(bool honorOriginAtMargins)
    {
        Graphics g = CreateMeasurementGraphics();
        if (honorOriginAtMargins)
        {
            g.TranslateTransform(-_defaultPageSettings.HardMarginX, -_defaultPageSettings.HardMarginY);
            g.TranslateTransform(_defaultPageSettings.Margins.Left, _defaultPageSettings.Margins.Top);
        }
 
        return g;
    }
 
    public Graphics CreateMeasurementGraphics(PageSettings pageSettings)
    {
        // returns the Graphics object for the printer
        var hdc = CreateDeviceContext(pageSettings);
        Graphics g = Graphics.FromHdcInternal(hdc);
        g.PrintingHelper = new HdcHandle(hdc); // Graphics will dispose of the DeviceContext.
        return g;
    }
 
    // whatever the call stack calling HardMarginX and HardMarginY here is safe
    public Graphics CreateMeasurementGraphics(PageSettings pageSettings, bool honorOriginAtMargins)
    {
        Graphics g = CreateMeasurementGraphics();
        if (honorOriginAtMargins)
        {
            g.TranslateTransform(-pageSettings.HardMarginX, -pageSettings.HardMarginY);
            g.TranslateTransform(pageSettings.Margins.Left, pageSettings.Margins.Top);
        }
 
        return g;
    }
 
    // Use FastDeviceCapabilities where possible -- computing PrinterName is quite slow
    private int DeviceCapabilities(PRINTER_DEVICE_CAPABILITIES capability, void* output = null, int defaultValue = -1)
        => FastDeviceCapabilities(capability, PrinterName, output, defaultValue);
 
    // We pass PrinterName in as a parameter rather than computing it ourselves because it's expensive to compute.
    // We need to pass IntPtr.Zero since passing HDevMode is non-performant.
    private static int FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES capability, string printerName, void* output = null, int defaultValue = -1)
    {
        fixed (char* pn = printerName)
        fixed (char* op = GetOutputPort())
        {
            int result = PInvoke.DeviceCapabilities(pn, op, capability, (PWSTR)output, null);
            return result == -1 ? defaultValue : result;
        }
    }
 
    private static string GetDefaultPrinterName() => GetDefaultName(1);
 
    private static string GetOutputPort() => GetDefaultName(2);
 
    private static string GetDefaultName(int slot)
    {
        PRINTDLGEXW dialogSettings = new()
        {
            lStructSize = (uint)sizeof(PRINTDLGEXW),
            Flags = PRINTDLGEX_FLAGS.PD_RETURNDEFAULT | PRINTDLGEX_FLAGS.PD_NOPAGENUMS,
            // PrintDlgEx requires a valid HWND
            hwndOwner = PInvokeCore.GetDesktopWindow(),
            nStartPage = PInvokeCore.START_PAGE_GENERAL
        };
 
        HRESULT status = PInvokeCore.PrintDlgEx(&dialogSettings);
        if (status.Failed)
        {
            return SR.NoDefaultPrinter;
        }
 
        HGLOBAL handle = dialogSettings.hDevNames;
        DEVNAMES* names = (DEVNAMES*)PInvokeCore.GlobalLock(handle);
        if (names is null)
        {
            throw new Win32Exception();
        }
 
        string name = slot switch
        {
            1 => new((char*)names + names->wDeviceOffset),
            2 => new((char*)names + names->wOutputOffset),
            _ => throw new InvalidOperationException()
        };
 
        PInvokeCore.GlobalUnlock(handle);
 
        // Windows allocates them, but we have to free them
        PInvokeCore.GlobalFree(dialogSettings.hDevNames);
        PInvokeCore.GlobalFree(dialogSettings.hDevMode);
 
        return name;
    }
 
    private int GetDeviceCaps(GET_DEVICE_CAPS_INDEX capability)
    {
        using var hdc = CreateInformationContext(DefaultPageSettings);
        return PInvokeCore.GetDeviceCaps(hdc, capability);
    }
 
    /// <summary>
    ///  Creates a handle to a DEVMODE structure which correspond too the printer settings.When you are done with the
    ///  handle, you must deallocate it yourself:
    ///    Kernel32.GlobalFree(handle);
    ///    Where "handle" is the return value from this method.
    /// </summary>
    public IntPtr GetHdevmode()
    {
        HGLOBAL modeHandle = GetHdevmodeInternal();
        _defaultPageSettings.CopyToHdevmode(modeHandle);
        return modeHandle;
    }
 
    internal unsafe HGLOBAL GetHdevmodeInternal()
    {
        // Getting the printer name is quite expensive if PrinterName is left default,
        // because it needs to figure out what the default printer is.
        fixed (char* n = PrinterNameInternal)
        {
            return GetHdevmodeInternal(n);
        }
    }
 
    private HGLOBAL GetHdevmodeInternal(char* printerName)
    {
        int result = -1;
 
        // Create DEVMODE
        result = PInvoke.DocumentProperties(
            default,
            default,
            printerName,
            null,
            (DEVMODEW*)null,
            0);
 
        if (result < 1)
        {
            throw new InvalidPrinterException(this);
        }
 
        HGLOBAL handle = PInvokeCore.GlobalAlloc(GLOBAL_ALLOC_FLAGS.GMEM_MOVEABLE, (uint)result);
        DEVMODEW* devmode = (DEVMODEW*)PInvokeCore.GlobalLock(handle);
 
        // Get the DevMode only if its not cached.
        if (_cachedDevmode is not null)
        {
            Marshal.Copy(_cachedDevmode, 0, (nint)devmode, _devmodeBytes);
        }
        else
        {
            result = PInvoke.DocumentProperties(
                default,
                default,
                printerName,
                devmode,
                (DEVMODEW*)null,
                (uint)DEVMODE_FIELD_FLAGS.DM_OUT_BUFFER);
 
            if (result < 0)
            {
                throw new Win32Exception();
            }
        }
 
        if (_extraInfo is not null)
        {
            // Guard against buffer overrun attacks (since design allows client to set a new printer name without
            // updating the devmode)/ by checking for a large enough buffer size before copying the extra info buffer.
            if (_extraBytes <= devmode->dmDriverExtra)
            {
                Marshal.Copy(_extraInfo, 0, (nint)((byte*)devmode + devmode->dmSize), _extraBytes);
            }
        }
 
        if (devmode->dmFields.HasFlag(DEVMODE_FIELD_FLAGS.DM_COPIES) && _copies != -1)
        {
            devmode->Anonymous1.Anonymous1.dmCopies = _copies;
        }
 
        if (devmode->dmFields.HasFlag(DEVMODE_FIELD_FLAGS.DM_DUPLEX) && (int)_duplex != -1)
        {
            devmode->dmDuplex = (DEVMODE_DUPLEX)_duplex;
        }
 
        if (devmode->dmFields.HasFlag(DEVMODE_FIELD_FLAGS.DM_COLLATE) && _collate.IsNotDefault)
        {
            devmode->dmCollate = _collate.IsTrue ? DEVMODE_COLLATE.DMCOLLATE_TRUE : DEVMODE_COLLATE.DMCOLLATE_FALSE;
        }
 
        result = PInvoke.DocumentProperties(
            default,
            default,
            printerName,
            devmode,
            devmode,
            (uint)(DEVMODE_FIELD_FLAGS.DM_IN_BUFFER | DEVMODE_FIELD_FLAGS.DM_OUT_BUFFER));
 
        if (result < 0)
        {
            PInvokeCore.GlobalFree(handle);
            PInvokeCore.GlobalUnlock(handle);
            return default;
        }
 
        PInvokeCore.GlobalUnlock(handle);
        return handle;
    }
 
    /// <summary>
    ///  Creates a handle to a DEVMODE structure which correspond to the printer and page settings.
    ///  When you are done with the handle, you must deallocate it yourself:
    ///    Kernel32.GlobalFree(handle);
    ///    Where "handle" is the return value from this method.
    /// </summary>
    public IntPtr GetHdevmode(PageSettings pageSettings)
    {
        IntPtr modeHandle = GetHdevmodeInternal();
        pageSettings.CopyToHdevmode(modeHandle);
 
        return modeHandle;
    }
 
    /// <summary>
    ///  Creates a handle to a DEVNAMES structure which correspond to the printer settings.
    ///  When you are done with the handle, you must deallocate it yourself:
    ///    Kernel32.GlobalFree(handle);
    ///    Where "handle" is the return value from this method.
    /// </summary>
    public unsafe IntPtr GetHdevnames()
    {
        string printerName = PrinterName;
        string driver = DriverName;
        string outPort = OutputPort;
 
        // Create DEVNAMES structure, offsets are in characters, not bytes
 
        // Add 4 for null terminators
        int namesChars = checked(4 + printerName.Length + driver.Length + outPort.Length);
        int offsetInChars = sizeof(DEVNAMES) / sizeof(char);
        int sizeInChars = checked(offsetInChars + namesChars);
 
        HGLOBAL handle = PInvokeCore.GlobalAlloc(
            GLOBAL_ALLOC_FLAGS.GMEM_MOVEABLE | GLOBAL_ALLOC_FLAGS.GMEM_ZEROINIT,
            (uint)(sizeof(char) * sizeInChars));
 
        DEVNAMES* devnames = (DEVNAMES*)PInvokeCore.GlobalLock(handle);
        Span<char> names = new((char*)devnames, sizeInChars);
 
        devnames->wDriverOffset = checked((ushort)offsetInChars);
        driver.AsSpan().CopyTo(names.Slice(offsetInChars, driver.Length));
        offsetInChars += (ushort)(driver.Length + 1);
 
        devnames->wDeviceOffset = checked((ushort)offsetInChars);
        printerName.AsSpan().CopyTo(names.Slice(offsetInChars, printerName.Length));
        offsetInChars += (ushort)(printerName.Length + 1);
 
        devnames->wOutputOffset = checked((ushort)offsetInChars);
        outPort.AsSpan().CopyTo(names.Slice(offsetInChars, outPort.Length));
        offsetInChars += (ushort)(outPort.Length + 1);
 
        devnames->wDefault = checked((ushort)offsetInChars);
 
        PInvokeCore.GlobalUnlock(handle);
        return handle;
    }
 
    // Handles creating then disposing a default DEVMODE
    internal short GetModeField(ModeField field, short defaultValue) => GetModeField(field, defaultValue, modeHandle: default);
 
    internal short GetModeField(ModeField field, short defaultValue, HGLOBAL modeHandle)
    {
        bool ownHandle = false;
        short result;
        try
        {
            if (modeHandle == 0)
            {
                try
                {
                    modeHandle = GetHdevmodeInternal();
                    ownHandle = true;
                }
                catch (InvalidPrinterException)
                {
                    return defaultValue;
                }
            }
 
            DEVMODEW* devmode = (DEVMODEW*)PInvokeCore.GlobalLock(modeHandle);
            switch (field)
            {
                case ModeField.Orientation:
                    result = devmode->dmOrientation;
                    break;
                case ModeField.PaperSize:
                    result = devmode->dmPaperSize;
                    break;
                case ModeField.PaperLength:
                    result = devmode->dmPaperLength;
                    break;
                case ModeField.PaperWidth:
                    result = devmode->dmPaperWidth;
                    break;
                case ModeField.Copies:
                    result = devmode->dmCopies;
                    break;
                case ModeField.DefaultSource:
                    result = devmode->dmDefaultSource;
                    break;
                case ModeField.PrintQuality:
                    result = devmode->dmPrintQuality;
                    break;
                case ModeField.Color:
                    result = (short)devmode->dmColor;
                    break;
                case ModeField.Duplex:
                    result = (short)devmode->dmDuplex;
                    break;
                case ModeField.YResolution:
                    result = devmode->dmYResolution;
                    break;
                case ModeField.TTOption:
                    result = (short)devmode->dmTTOption;
                    break;
                case ModeField.Collate:
                    result = (short)devmode->dmCollate;
                    break;
                default:
                    Debug.Fail("Invalid field in GetModeField");
                    result = defaultValue;
                    break;
            }
 
            PInvokeCore.GlobalUnlock(modeHandle);
        }
        finally
        {
            if (ownHandle)
            {
                PInvokeCore.GlobalFree(modeHandle);
            }
        }
 
        return result;
    }
 
    internal unsafe PaperSize[] Get_PaperSizes()
    {
        // Cache the name as the name will be computed on every call if the name is default
        string printerName = PrinterName;
 
        int result = FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_PAPERNAMES, printerName);
        if (result == -1)
        {
            return [];
        }
 
        int count = result;
 
        // DC_PAPERNAMES is an array of fixed 64 char buffers
        const int NameLength = 64;
        using BufferScope<char> names = new(NameLength * count);
        fixed (char* n = names)
        {
            result = FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_PAPERNAMES, printerName, n);
        }
 
        Debug.Assert(
            FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_PAPERS, printerName) == count,
            "Not the same number of paper kinds as paper names?");
 
        Span<ushort> kinds = stackalloc ushort[count];
        fixed (ushort* k = kinds)
        {
            result = FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_PAPERS, printerName, k);
        }
 
        Debug.Assert(
            FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_PAPERSIZE, printerName) == count,
            "Not the same number of paper sizes as paper names?");
 
        Span<Size> sizes = stackalloc Size[count];
        fixed (Size* s = sizes)
        {
            result = FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_PAPERSIZE, printerName, s);
        }
 
        PaperSize[] paperSizes = new PaperSize[count];
        for (int i = 0; i < count; i++)
        {
            paperSizes[i] = new PaperSize(
                (PaperKind)kinds[i],
                names.Slice(i * NameLength, NameLength).SliceAtFirstNull().ToString(),
                PrinterUnitConvert.Convert(sizes[i].Width, PrinterUnit.TenthsOfAMillimeter, PrinterUnit.Display),
                PrinterUnitConvert.Convert(sizes[i].Height, PrinterUnit.TenthsOfAMillimeter, PrinterUnit.Display));
        }
 
        return paperSizes;
    }
 
    internal unsafe PaperSource[] Get_PaperSources()
    {
        // Cache the name as the name will be computed on every call if the name is default
        string printerName = PrinterName;
 
        int result = FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_BINNAMES, printerName);
        if (result == -1)
        {
            return [];
        }
 
        int count = result;
 
        // Contrary to documentation, DeviceCapabilities returns char[count, 24],
        // not char[count][24]
        // DC_BINNAMES is an array of fixed 64 char buffers
        const int NameLength = 24;
        using BufferScope<char> names = new(NameLength * count);
        fixed (char* n = names)
        {
            result = FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_BINNAMES, printerName, n);
        }
 
        Debug.Assert(
            FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_BINS, printerName) == count,
            "Not the same number of bin kinds as bin names?");
 
        Span<ushort> kinds = stackalloc ushort[count];
        fixed (ushort* k = kinds)
        {
            FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_BINS, printerName, k);
        }
 
        PaperSource[] paperSources = new PaperSource[count];
        for (int i = 0; i < count; i++)
        {
            paperSources[i] = new PaperSource(
                (PaperSourceKind)kinds[i],
                names.Slice(i * NameLength, NameLength).SliceAtFirstNull().ToString());
        }
 
        return paperSources;
    }
 
    internal unsafe PrinterResolution[] Get_PrinterResolutions()
    {
        // Cache the name as the name will be computed on every call if the name is default
        string printerName = PrinterName;
        PrinterResolution[] result;
 
        int count = FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_ENUMRESOLUTIONS, printerName);
        if (count == -1)
        {
            // Just return the standard values if custom resolutions are absent.
            result =
            [
                new(PrinterResolutionKind.High, -4, -1),
                new(PrinterResolutionKind.Medium, -3, -1),
                new(PrinterResolutionKind.Low, -2, -1),
                new(PrinterResolutionKind.Draft, -1, -1),
            ];
 
            return result;
        }
 
        result = new PrinterResolution[count + 4];
        result[0] = new(PrinterResolutionKind.High, -4, -1);
        result[1] = new(PrinterResolutionKind.Medium, -3, -1);
        result[2] = new(PrinterResolutionKind.Low, -2, -1);
        result[3] = new(PrinterResolutionKind.Draft, -1, -1);
 
        Span<Point> resolutions = stackalloc Point[count];
 
        fixed (Point* r = resolutions)
        {
            FastDeviceCapabilities(PRINTER_DEVICE_CAPABILITIES.DC_ENUMRESOLUTIONS, printerName, r);
        }
 
        for (int i = 0; i < count; i++)
        {
            Point resolution = resolutions[i];
            result[i + 4] = new PrinterResolution(PrinterResolutionKind.Custom, resolution.X, resolution.Y);
        }
 
        return result;
    }
 
    /// <summary>
    ///  Copies the relevant information out of the handle and into the PrinterSettings.
    /// </summary>
    public void SetHdevmode(IntPtr hdevmode)
    {
        if (hdevmode == 0)
            throw new ArgumentException(SR.Format(SR.InvalidPrinterHandle, hdevmode));
 
        DEVMODEW* devmode = (DEVMODEW*)PInvokeCore.GlobalLock((HGLOBAL)hdevmode);
 
        // Copy entire public devmode as a byte array.
        _devmodeBytes = devmode->dmSize;
        if (_devmodeBytes > 0)
        {
            _cachedDevmode = new byte[_devmodeBytes];
            Marshal.Copy((nint)devmode, _cachedDevmode, 0, _devmodeBytes);
        }
 
        // Copy private devmode as a byte array.
        _extraBytes = devmode->dmDriverExtra;
        if (_extraBytes > 0)
        {
            _extraInfo = new byte[_extraBytes];
            Marshal.Copy((nint)((byte*)devmode + devmode->dmSize), _extraInfo, 0, _extraBytes);
        }
 
        if (devmode->dmFields.HasFlag(DEVMODE_FIELD_FLAGS.DM_COPIES))
        {
            _copies = devmode->dmCopies;
        }
 
        if (devmode->dmFields.HasFlag(DEVMODE_FIELD_FLAGS.DM_DUPLEX))
        {
            _duplex = (Duplex)devmode->dmDuplex;
        }
 
        if (devmode->dmFields.HasFlag(DEVMODE_FIELD_FLAGS.DM_COLLATE))
        {
            _collate = devmode->dmCollate == DEVMODE_COLLATE.DMCOLLATE_TRUE;
        }
 
        PInvokeCore.GlobalUnlock((HGLOBAL)hdevmode);
    }
 
    /// <summary>
    ///  Copies the relevant information out of the handle and into the PrinterSettings.
    /// </summary>
    public void SetHdevnames(IntPtr hdevnames)
    {
        if (hdevnames == 0)
        {
            throw new ArgumentException(SR.Format(SR.InvalidPrinterHandle, hdevnames));
        }
 
        DEVNAMES* names = (DEVNAMES*)PInvokeCore.GlobalLock((HGLOBAL)hdevnames);
 
        _driverName = new((char*)names + names->wDriverOffset);
        _printerName = new((char*)names + names->wDeviceOffset);
        OutputPort = new((char*)names + names->wOutputOffset);
 
        PrintDialogDisplayed = true;
 
        PInvokeCore.GlobalUnlock((HGLOBAL)hdevnames);
    }
 
    public override string ToString() =>
        $"[PrinterSettings {PrinterName} Copies={Copies} Collate={Collate} Duplex={Duplex} FromPage={FromPage} LandscapeAngle={LandscapeAngle} MaximumCopies={MaximumCopies} OutputPort={OutputPort} ToPage={ToPage}]";
}