Public/New-VellumPdfPageDocument.ps1

function New-VellumPdfPageDocument {
    <#
    .SYNOPSIS
        Creates a page-based PDF document for precise placement: drawing, forms, links, outlines.
    .DESCRIPTION
        New-VellumPdfDocument builds flowing documents (text wraps and paginates
        for you). This cmdlet creates the other kind: a document made of explicit
        pages that you place content on by coordinates, using the VellumPdf.Kernel
        engine directly. Use it for what flow layout cannot express:

          Add-VellumPdfPage add pages (any size, rotation)
          Add-VellumPdfShape / -Text / -Gradient / -PageImage / -PageBarcode
                                     draw at x/y positions
          Invoke-VellumPdfCanvas full canvas access from a script block
          Add-VellumPdfFormField fillable AcroForm fields
          Add-VellumPdfLink link rectangles to URLs or other pages
          Add-VellumPdfOutlineEntry bookmarks, collapsed or expanded

        then Save-VellumPdfDocument. Coordinates are PDF points with the origin at
        the page's bottom-left (72 points = 1 inch). Set-VellumPdfDocumentInfo,
        Set-VellumPdfOutputIntent, Register-VellumPdfFont, Protect-VellumPdfDocument
        and Set-VellumPdfSignature work on page documents too.

        Tagging: with -Tagged (implied by -Conformance PdfA2a and PdfUA1) the document
        gets a structure tree. Text is tagged (P, H1-H6, Span, Caption via
        Add-VellumPdfText -Role), shapes and gradients are artifacts unless given
        -AltText, images and barcodes need alternate text (or -Decorative), and
        Invoke-VellumPdfCanvas drawing is an artifact. Links and form fields cannot be
        tagged by VellumPdf, so a PDF/UA document containing them is not fully
        conformant (the cmdlets warn). PDF/A and PDF/UA need embedded fonts and, for
        PDF/UA, a title (Set-VellumPdfDocumentInfo -Title) and -Language; base-14 text
        is embedded automatically with a metric-compatible substitute font.

        -Linearize writes a "fast web view" file (first page readable while the rest
        downloads). It cannot be combined with -UseObjectStreams or encryption, and
        signing ignores it. -DocumentId and -Timestamp fix the otherwise random
        document ID and creation date, which makes the output byte-for-byte
        reproducible.
    .PARAMETER Conformance
        None (default), PdfA2b, PdfA2u, PdfA2a or PdfUA1.
    .PARAMETER Tagged
        Write a structure tree (tagged PDF). Implied by PdfA2a and PdfUA1.
    .PARAMETER PageSize
        The default size of added pages: A0-A6, Ledger, Legal, Letter. Default A4.
    .PARAMETER PageWidthMm
        Custom default page width in millimetres (1..5080), with -PageHeightMm.
    .PARAMETER PageHeightMm
        Custom default page height in millimetres (1..5080).
    .PARAMETER Language
        BCP 47 document language written as /Lang (e.g. 'en-US').
    .PARAMETER Culture
        The culture used to turn non-string values into text: table cells, list items and pie-chart
        percentage labels (dates, numbers, and numbers read from strings). A culture name ('de-DE') or a
        [cultureinfo] (-Culture (Get-Culture) matches the console); an unknown name is an error. Default:
        the invariant culture, so the output is the same on every machine. Each of Add-VellumPdfTable,
        Add-VellumPdfList, Add-VellumPdfPieChart (and the page-document counterparts) has its own -Culture
        that overrides this default for one call. It is deliberately not derived from -Language: a language
        tag says nothing about how dates and numbers look.
    .PARAMETER InputCulture
        A culture name or [cultureinfo] used to READ numbers that are given as strings in table cells and
        list items (the document default; the table and list cmdlets have a per-call -InputCulture). With it,
        a string that consists only of a number is parsed strictly with that culture's decimal and grouping
        separators ('de-DE': '1.234,5' is 1234.5, '1.5' is not a number). Without it (the default) the
        separators are detected from the text. See Add-VellumPdfTable for the detection rules.
    .PARAMETER UseObjectStreams
        Compress the cross-reference data into object streams (smaller files).
    .PARAMETER Linearize
        Write a linearized ("fast web view") file.
    .PARAMETER DocumentId
        Exactly 16 bytes used as the document /ID. Default: derived from the content
        and the creation time.
    .PARAMETER Timestamp
        The creation/modification date written to the file. Default: now.
    .EXAMPLE
        $doc = New-VellumPdfPageDocument -Language en
        $doc | Add-VellumPdfPage | Add-VellumPdfText -Text 'Hello' -X 72 -Y 700 -FontSize 24 |
            Save-VellumPdfDocument -Path ./hello.pdf
    .EXAMPLE
        # Reproducible output
        New-VellumPdfPageDocument -DocumentId ([byte[]](1..16)) -Timestamp '2026-01-01T00:00:00Z' | ...
    .OUTPUTS
        VellumPdf.Document.PdfDocument
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Returns a new in-memory document object; performs no external/system state change.')]
    [CmdletBinding(DefaultParameterSetName = 'NamedPage')]
    [OutputType([VellumPdf.Document.PdfDocument])]
    param(
        [ValidateSet('None', 'PdfA2b', 'PdfA2u', 'PdfA2a', 'PdfUA1')]
        [string]$Conformance = 'None',

        # Write a structure tree (tagged PDF). PdfA2a and PdfUA1 imply it.
        [switch]$Tagged,

        [Parameter(ParameterSetName = 'NamedPage')]
        [ValidateSet('A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'Ledger', 'Legal', 'Letter')]
        [string]$PageSize = 'A4',

        [Parameter(Mandatory, ParameterSetName = 'CustomPage')]
        [ValidateRange(1.0, 5080.0)]
        [double]$PageWidthMm,

        [Parameter(Mandatory, ParameterSetName = 'CustomPage')]
        [ValidateRange(1.0, 5080.0)]
        [double]$PageHeightMm,

        [string]$Language,

        [switch]$UseObjectStreams,

        [switch]$Linearize,

        [byte[]]$DocumentId,

        [System.DateTimeOffset]$Timestamp,

        # Culture for turning values into text (tables, lists); a name or a [cultureinfo]. Default: invariant.
        [object]$Culture,

        # Culture used to read numbers given as strings. Default: detect the separators from the text.
        [object]$InputCulture
    )

    try {
        if ($Linearize -and $UseObjectStreams) {
            throw 'New-VellumPdfPageDocument: -Linearize cannot be combined with -UseObjectStreams.'
        }
        if ($PSBoundParameters.ContainsKey('DocumentId') -and $DocumentId.Length -ne 16) {
            throw "New-VellumPdfPageDocument: -DocumentId must be exactly 16 bytes; got $($DocumentId.Length)."
        }

        # Resolved first: an unknown culture name must fail before a document exists.
        $documentCulture = if ($PSBoundParameters.ContainsKey('Culture')) { ConvertTo-VellumPdfCulture -Value $Culture -CommandName 'New-VellumPdfPageDocument' } else { [cultureinfo]::InvariantCulture }
        $documentInputCulture = if ($PSBoundParameters.ContainsKey('InputCulture') -and $null -ne $InputCulture) { ConvertTo-VellumPdfCulture -Value $InputCulture -CommandName 'New-VellumPdfPageDocument' -ParameterName 'InputCulture' }
        $doc = [VellumPdf.Document.PdfDocument]::new()
        $doc.Conformance = [VellumPdf.Document.PdfConformance]::$Conformance
        if ($Tagged -or $Conformance -in 'PdfA2a', 'PdfUA1') { $doc.Tagged = $true }
        $doc.DefaultPageSize = if ($PSCmdlet.ParameterSetName -eq 'CustomPage') {
            [VellumPdf.Document.PageSize]::Mm($PageWidthMm, $PageHeightMm)
        } else {
            [VellumPdf.Document.PageSize]::$PageSize
        }
        Assert-VellumPdfLanguageTag -Value $Language -CommandName 'New-VellumPdfPageDocument'
            if ($Language) { $doc.Language = $Language }
        if ($UseObjectStreams) { $doc.UseObjectStreams = $true }
        if ($Linearize) { $doc.Linearize = $true }
        if ($PSBoundParameters.ContainsKey('DocumentId')) { $doc.DocumentId = $DocumentId }
        if ($PSBoundParameters.ContainsKey('Timestamp')) { $doc.Timestamp = $Timestamp }
        $doc.PSObject.Properties.Add([psnoteproperty]::new('PSVellumCulture', $documentCulture))
        $doc.PSObject.Properties.Add([psnoteproperty]::new('PSVellumInputCulture', $documentInputCulture))
        $doc
    }
    catch {
        $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'New-VellumPdfPageDocument'))
    }
}