private/New-OTPQRCode.ps1

# New-OTPQRCode.ps1
<#
.SYNOPSIS
    Generates a QR code for OTP configuration.
.DESCRIPTION
    The New-OTPQRCode function generates a QR code image from an OTP configuration URI.
    It accepts input directly or through the pipeline from New-OTPSecret, allowing for easy
    creation of QR codes for TOTP/HOTP configurations. The generated QR code can be saved
    to a file, displayed directly, or both.
 
    Features:
    - Generates high-quality QR codes suitable for scanning
    - Supports both file output and byte array return
    - Validates URI format before generation
    - Customizable QR code size
    - Configurable error correction
    - Clean disposal of resources
    - Option to display the QR code immediately
 
    The generated QR codes are compatible with common authenticator apps:
    - Google Authenticator
    - Microsoft Authenticator
    - Authy
    - And other RFC-compliant authenticators
 
.PARAMETER Uri
    The OTP configuration URI to encode in the QR code. Must start with 'otpauth://'.
    This parameter accepts pipeline input from New-OTPSecret and other functions that output
    OTP configuration URIs.
.PARAMETER Size
    The size of the QR code image in pixels. Must be between 100 and 1000.
    Default value is 300 pixels.
.PARAMETER OutFile
    Optional path where the QR code image will be saved.
    If not specified, the QR code will be displayed in the default image viewer.
.PARAMETER Show
    If specified, displays the QR code in the default image viewer.
    This can be used with or without the OutFile parameter.
.EXAMPLE
    PS> New-OTPSecret | New-OTPQRCode -Show
    Generates a new OTP secret and displays the QR code.
.EXAMPLE
    PS> New-OTPSecret -Length 32 -Tag "MyApp" | New-OTPQRCode -Size 400 -OutFile "myapp_qr.png" -Show
    Creates a 32-character OTP secret with a tag, generates a 400x400 pixel QR code, saves it to myapp_qr.png, and displays it.
.EXAMPLE
    PS> $uri = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"
    PS> New-OTPQRCode -Uri $uri -Size 500 -Show
    Creates a 500x500 pixel QR code from a manually specified OTP URI and displays it.
.NOTES
    The function validates that the URI starts with 'otpauth://' to ensure compatibility
    with OTP authenticator apps. When using pipeline input from New-OTPSecret, the URI
    is automatically formatted correctly.
 
    Best practices:
    - Use PNG format for best quality
    - Size of 300-400 pixels works well on most devices
    - Ensure adequate contrast in the display environment
    - Test scanning with various authenticator apps
.LINK
    https://github.com/thorstenbutz/otp
    https://github.com/google/google-authenticator/wiki/Key-Uri-Format
#>

function New-OTPQRCode {
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Save')]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias('OTPUri')]
        [string]$Uri,

        [Parameter()]
        [ValidateRange(100, 1000)]
        [int]$Size = 300,

        [Parameter(ParameterSetName = 'Save')]
        [Parameter(ParameterSetName = 'Show', Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$OutFile,

        [Parameter(ParameterSetName = 'Save')]
        [Parameter(ParameterSetName = 'Show', Mandatory)]
        [switch]$Show
    )

    begin {
        Add-Type -AssemblyName System.Drawing
        $zxingPath = Join-Path -Path $PSScriptRoot -ChildPath '..\lib\zxing.dll'
        if (-not (Test-Path -Path $zxingPath)) {
            throw "Required ZXing.Net library not found at: $zxingPath"
        }
        Add-Type -Path $zxingPath
    }

    process {
        try {
            if (-not $Uri.StartsWith('otpauth://')) {
                throw 'Invalid OTP URI format. URI must start with ''otpauth://'''
            }

            Write-Verbose -Message "Generating QR code with size: ${Size}x${Size} pixels"
            Write-Verbose -Message "URI length: $($Uri.Length) characters"

            $barcodeWriter = [ZXing.BarcodeWriter]::new()
            $barcodeWriter.Format = [ZXing.BarcodeFormat]::QR_CODE
            
            $options = [ZXing.QrCode.QrCodeEncodingOptions]::new()
            $options.Height = $Size
            $options.Width = $Size
            $options.Margin = 2
            $options.ErrorCorrection = [ZXing.QrCode.Internal.ErrorCorrectionLevel]::M
            $options.CharacterSet = 'UTF-8'
            $options.DisableECI = $true
            $barcodeWriter.Options = $options

            $qrBitmap = $barcodeWriter.Write($Uri)

            # Save to file if OutFile is specified
            if ($OutFile) {
                ## Check if file exists and prompt for overwrite confirmation
                $fileExists = Test-Path -Path $OutFile
                
                if ($fileExists) {
                    ## Check if confirmation was explicitly disabled
                    $skipConfirmation = ($PSBoundParameters.ContainsKey('Confirm') -and $PSBoundParameters['Confirm'] -eq $false)
                    
                    if (-not $skipConfirmation) {
                        ## Prompt for overwrite confirmation
                        $shouldOverwrite = $PSCmdlet.ShouldContinue(
                            "The file '$OutFile' already exists. Do you want to overwrite it?",
                            'Confirm Overwrite'
                        )
                        
                        if (-not $shouldOverwrite) {
                            Write-Verbose -Message "File overwrite cancelled by user: $OutFile"
                            return
                        }
                    }
                    else {
                        Write-Verbose -Message "Skipping overwrite confirmation due to -Confirm:`$false"
                    }
                }
                
                ## Proceed with file operation using ShouldProcess
                $actionMessage = if ($fileExists) { 'Overwrite existing QR code image' } else { 'Save QR code image' }
                
                if ($PSCmdlet.ShouldProcess($OutFile, $actionMessage)) {
                    $directory = [System.IO.Path]::GetDirectoryName($OutFile)
                    if (-not [string]::IsNullOrEmpty($directory) -and -not (Test-Path -Path $directory)) {
                        New-Item -ItemType Directory -Path $directory -Force | Out-Null
                    }

                    $imageFormat = [System.Drawing.Imaging.ImageFormat]::Png
                    $encoderParams = [System.Drawing.Imaging.EncoderParameters]::new(1)
                    $encoderParams.Param[0] = [System.Drawing.Imaging.EncoderParameter]::new(
                        [System.Drawing.Imaging.Encoder]::Quality,
                        [long]100
                    )

                    $qrBitmap.Save($OutFile, $imageFormat)
                    
                    $verboseMessage = if ($fileExists) { "QR code overwritten at: $OutFile" } else { "QR code saved to: $OutFile" }
                    Write-Verbose -Message $verboseMessage

                    $outputFile = Get-Item -Path $OutFile
                    
                    # Display the QR code if Show is specified
                    if ($Show) {
                        Start-Process -FilePath $OutFile
                        Write-Verbose -Message "QR code displayed from: $OutFile"
                    }
                    
                    # Return the output file
                    $outputFile
                }
            }
        }
        catch {
            Write-Error -ErrorRecord $_
        }
        finally {
            if ($qrBitmap) {
                $qrBitmap.Dispose()
            }
            if ($encoderParams) {
                $encoderParams.Dispose()
            }
        }
    }
}