Public/Use-VellumPdfTransform.ps1

function Use-VellumPdfTransform {
    <#
    .SYNOPSIS
        Runs a script block whose page drawing is rotated, scaled and/or moved.
    .DESCRIPTION
        Everything drawn on the page by the script block (Add-VellumPdfShape, -Path, -Text, -TextBox,
        -PageImage, -PageBarcode, -Gradient, Invoke-VellumPdfCanvas ...) is transformed; the
        transformation ends with the block, even if it throws. Scopes can be nested.

        The transformation is applied in this order: scale about the origin, rotate about the origin,
        then move. The origin is -OriginX/-OriginY (default the page's bottom-left corner), so to
        rotate a stamp about its own centre give that centre as the origin. Rotation is in degrees,
        counter-clockwise.

        Coordinates inside the block are the ones you would have used without the transform; only
        the result moves. A block that draws on another page than -Page is not transformed there
        (a warning says so).

        Links and form fields (Add-VellumPdfLink, Add-VellumPdfFormField) are annotations placed in page
        coordinates and are not transformed, nor clipped by Use-VellumPdfClip.

        The block runs in the caller's scope with the document as $_. Its own output is discarded;
        the cmdlet writes the document to the pipeline, so the call chains.
    .PARAMETER Document
        A document from New-VellumPdfPageDocument.
    .PARAMETER ScriptBlock
        The drawing to transform.
    .PARAMETER Page
        The 1-based page number. Default 1.
    .PARAMETER Rotate
        Rotation in degrees, counter-clockwise (negative is clockwise).
    .PARAMETER Scale
        One factor for both directions, or two (horizontal, vertical). Negative values mirror. The size of
        a factor is limited to 0.000001 ... 1,000,000.
    .PARAMETER OriginX
        x of the point the scale and rotation are about, in points (default 0).
    .PARAMETER OriginY
        y of the point the scale and rotation are about, in points (default 0).
    .PARAMETER TranslateX
        Horizontal move applied last, in points.
    .PARAMETER TranslateY
        Vertical move applied last, in points.
    .EXAMPLE
        # A diagonal "DRAFT" stamp, rotated 45 degrees about the page centre.
        $doc | Use-VellumPdfTransform -Rotate 45 -OriginX 297 -OriginY 421 -ScriptBlock {
            $_ | Add-VellumPdfText 'DRAFT' -X 297 -Y 421 -FontSize 80 -Font HelveticaBold -Align Center -Color '#cccccc'
        }
    .OUTPUTS
        VellumPdf.Document.PdfDocument (the same instance, for chaining)
    #>

    [CmdletBinding()]
    [OutputType([VellumPdf.Document.PdfDocument])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [VellumPdf.Document.PdfDocument]$Document,

        [Parameter(Mandatory, Position = 0)]
        [scriptblock]$ScriptBlock,

        [ValidateRange(1, 1000000)]
        [int]$Page = 1,

        [double]$Rotate = 0,

        [ValidateCount(1, 2)]
        [double[]]$Scale = @(1.0),

        [double]$OriginX = 0,
        [double]$OriginY = 0,
        [double]$TranslateX = 0,
        [double]$TranslateY = 0
    )

    process {
        try {
            Assert-VellumPdfDocumentOpen -Document $Document -CommandName 'Use-VellumPdfTransform'
            $Page = Resolve-VellumPdfPageNumber -Document $Document -Page $(if ($PSBoundParameters.ContainsKey('Page')) { $PSBoundParameters['Page'] } else { 1 }) -Bound $PSBoundParameters.ContainsKey('Page') -CommandName 'Use-VellumPdfTransform'
            if (-not [double]::IsFinite($Rotate)) { throw 'Use-VellumPdfTransform: -Rotate must be a finite number of degrees.' }
            foreach ($factor in $Scale) {
                if (-not [double]::IsFinite($factor) -or $factor -eq 0 -or [Math]::Abs($factor) -lt 1e-6 -or [Math]::Abs($factor) -gt 1e6) {
                    throw 'Use-VellumPdfTransform: -Scale must be finite, not zero, and between 0.000001 and 1,000,000 in size (a smaller factor collapses the drawing; a larger one overflows the numbers PDF allows).'
                }
            }
            foreach ($pair in @('OriginX', $OriginX), @('OriginY', $OriginY), @('TranslateX', $TranslateX), @('TranslateY', $TranslateY)) {
                Assert-VellumPdfCoordinate $pair[1] $pair[0] 'Use-VellumPdfTransform'
            }
            $sx = $Scale[0]
            $sy = if ($Scale.Count -gt 1) { $Scale[1] } else { $Scale[0] }
            $radians = $Rotate * [math]::PI / 180
            $cos = [math]::Cos($radians)
            $sin = [math]::Sin($radians)
            # p' = O + R * S * (p - O) + T, written as the PDF matrix [a b c d e f].
            $a = $sx * $cos; $b = $sx * $sin; $c = -$sy * $sin; $d = $sy * $cos
            $e = $OriginX - ($a * $OriginX + $c * $OriginY) + $TranslateX
            $f = $OriginY - ($b * $OriginX + $d * $OriginY) + $TranslateY
            # Rounding (and + 0.0) keeps floating-point dust and "-0" out of the content stream.
            $a, $b, $c, $d, $e, $f = $a, $b, $c, $d, $e, $f | ForEach-Object { [math]::Round($_, 8) + 0.0 }
            if (($a, $b, $c, $d | Where-Object { $_ -ne 0 }).Count -eq 0) { throw 'Use-VellumPdfTransform: the transformation collapses everything to a point.' }
            $canvas = Get-VellumPdfPageCanvas -Document $Document -Page $Page -CommandName 'Use-VellumPdfTransform'
            [void]$canvas.SaveState()
            Push-VellumPdfScopePage -Document $Document -Page $Page
            try {
                [void]$canvas.Concat($a, $b, $c, $d, $e, $f)
                $null = $Document | ForEach-Object -Process $ScriptBlock
            }
            finally { Pop-VellumPdfScopePage -Document $Document; [void]$canvas.RestoreState() }
            $Document
        }
        catch {
            $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Use-VellumPdfTransform'))
        }
    }
}