Public/Save-VellumPdfDocument.ps1

function Save-VellumPdfDocument {
    <#
    .SYNOPSIS
        Writes a VellumPdf document to a .pdf file and disposes it.
    .DESCRIPTION
        Wraps Document.Save(path) - or, when a signature has been staged with
        Set-VellumPdfSignature, SigningExtensions.Sign(document, stream,
        settings), which signs the document while writing it (PAdES).
        The document is IDisposable; this function
        disposes it after the save attempt (success or failure) because saving is
        the terminal step of a build pipeline, and marks it so later cmdlet calls
        against the stale document fail with a clear error. Use -KeepOpen to keep
        the document alive for further edits, in which case you are responsible
        for calling $doc.Dispose() yourself. With -WhatIf nothing is saved and the
        document is left open.

        If the pipeline is aborted BEFORE this cmdlet runs (for example by an
        error in an earlier Add-VellumPdf* call, or -WarningAction Stop turning
        the encoding warning into a terminating error), the document is never
        saved or disposed - dispose it yourself in your catch block.

        The write is atomic: the PDF is rendered (and signed) to a temporary
        file beside -Path and only moved into place once it is complete, so a
        render or signing failure leaves any existing file at -Path untouched.
        On success an existing file at -Path is overwritten.
    .PARAMETER Document
        The live VellumPdf document to save. Accepts pipeline input. After saving,
        the document is disposed and stamped so subsequent cmdlet calls against
        the stale instance fail with a clear error. Use -KeepOpen to suppress
        disposal.
    .PARAMETER Path
        File system path for the output PDF file. The parent directory must
        already exist; an existing file at this path is overwritten. Mandatory
        and positional (position 0).
    .PARAMETER KeepOpen
        When specified, the document is not disposed after saving. The caller is
        responsible for calling $doc.Dispose() when finished. Useful when the
        same document object must be inspected or further manipulated after the
        file is written.
    .EXAMPLE
        $doc | Save-VellumPdfDocument -Path ./out.pdf
    .OUTPUTS
        System.IO.FileInfo for the written file.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.IO.FileInfo])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [VellumPdf.Layout.Document]$Document,

        [Parameter(Mandatory, Position = 0)]
        [string]$Path,

        # Keep the document open after saving (caller must Dispose it).
        [switch]$KeepOpen
    )

    process {
        Assert-VellumPdfDocumentOpen -Document $Document -CommandName 'Save-VellumPdfDocument'

        $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)

        # A signature staged by Set-VellumPdfSignature makes signing the write
        # step: VellumPdf signs at serialization time, so Sign() replaces Save().
        $signature = $Document.PSObject.Properties['PSVellumSignature']
        $action = if ($signature) { 'Save signed PDF' } else { 'Save PDF' }

        $attempted = $false
        try {
            # Everything below is skipped under -WhatIf (ShouldProcess returns
            # $false): nothing is written and the document is left open. The
            # directory check lives here too so a -WhatIf dry run never throws
            # or disposes the document.
            if ($PSCmdlet.ShouldProcess($resolved, $action)) {
                $attempted = $true

                $parent = [System.IO.Path]::GetDirectoryName($resolved)
                if ($parent -and -not [System.IO.Directory]::Exists($parent)) {
                    throw "Save-VellumPdfDocument: directory not found: '$parent'. Create it first or pass a path in an existing directory."
                }

                # Render/sign to a temporary file beside the target, then move
                # it into place only once it is complete. File.Create and
                # Document.Save both open the destination for writing before any
                # content exists, so writing straight to -Path would truncate an
                # existing good file the moment a render/sign failure occurred.
                $temp = "$resolved.$([guid]::NewGuid().ToString('N')).tmp"
                try {
                    try {
                        if ($signature) {
                            $stream = [System.IO.File]::Create($temp)
                            try {
                                [VellumPdf.Signing.SigningExtensions]::Sign($Document, $stream, $signature.Value)
                            }
                            finally {
                                $stream.Dispose()
                            }
                        }
                        else {
                            $Document.Save($temp)
                        }
                    }
                    catch {
                        # Surface failures with context specific to the operation
                        # that failed instead of a bare library exception.
                        $reason = $_.Exception.Message
                        if ($_.Exception.InnerException) { $reason = $_.Exception.InnerException.Message }
                        if ($signature) {
                            throw ("Save-VellumPdfDocument: failed to sign '$resolved': $reason " +
                                'Check that the signing certificate is valid and still holds its private key.')
                        }
                        throw ("Save-VellumPdfDocument: failed to render '$resolved': $reason " +
                            'Check for extreme margin values or elements taller than the page.')
                    }

                    # The rendered file is known good; replace the target now.
                    try {
                        [System.IO.File]::Move($temp, $resolved, $true)
                    }
                    catch {
                        $reason = $_.Exception.Message
                        throw ("Save-VellumPdfDocument: rendered the PDF but could not write it to '$resolved': $reason " +
                            'Check that -Path is a writable file location (not a directory) and is not locked by another process.')
                    }
                }
                finally {
                    # Clean up the temp file if a failure left it behind; on
                    # success it has already been moved onto the target.
                    if (Test-Path -LiteralPath $temp) {
                        Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue
                    }
                }

                Get-Item -LiteralPath $resolved
            }
        }
        finally {
            # Dispose after any save attempt (success or failure), but leave the
            # document open under -WhatIf, where no attempt was made. The stamp
            # lets the other cmdlets reject stale use of this document (VellumPdf
            # itself accepts Add() on a disposed document without complaint).
            if ($attempted -and -not $KeepOpen) {
                $Document.Dispose()
                if (-not $Document.PSObject.Properties['PSVellumDisposed']) {
                    $Document.PSObject.Properties.Add([psnoteproperty]::new('PSVellumDisposed', $true))
                }
            }
        }
    }
}