Public/Add-VellumPdfText.ps1

function Add-VellumPdfText {
    <#
    .SYNOPSIS
        Places a line of text at x/y on a page of a page-based document.
    .DESCRIPTION
        Draws one line (no wrapping) with its baseline starting at (-X, -Y), in
        points from the page's bottom-left. -Align Center/Right positions the text
        about x instead. Use a base-14 font by name (-Font), or an embedded
        TrueType font (-FontHandle from Register-VellumPdfFont) for Unicode text.

        In a PDF/A document base-14 text is embedded automatically with a
        metric-compatible Liberation font (VellumPdf.Fonts.Standard14), as PDF/A
        forbids non-embedded fonts. A tab is drawn as four spaces (a tab has no glyph in a PDF font).
        For several lines of wrapped text use the
        flow-layout document (New-VellumPdfDocument).
    .PARAMETER Document
        A document from New-VellumPdfPageDocument.
    .PARAMETER Text
        The text. A single line; line breaks are not supported.
    .PARAMETER Page
        The 1-based page number. Default 1.
    .PARAMETER X
        Left edge (or centre/right edge with -Align) in points.
    .PARAMETER Y
        Baseline in points from the bottom of the page.
    .PARAMETER FontSize
        Size in points (default 12).
    .PARAMETER Font
        A base-14 font name (default Helvetica).
    .PARAMETER FontHandle
        An embedded TrueType font from Register-VellumPdfFont. Overrides -Font.
    .PARAMETER Color
        Text colour: RGB triple in 0..1, hex or name. Default black.
    .PARAMETER Align
        Left (default), Center or Right, about -X.
    .PARAMETER CharSpacing
        Extra space after every character, in points; negative tightens. Alignment accounts for it: the space after the
        last character is not counted, so right-aligned text ends exactly at -X and centred text is
        centred on its visible glyphs.
    .PARAMETER WordSpacing
        Extra space after every space character, in points.
    .PARAMETER Rise
        Baseline shift in points: positive for superscript, negative for subscript.
    .PARAMETER HorizontalScale
        Horizontal scaling in percent (100 is normal, 50 half width).
    .PARAMETER Rotation
        Rotates the text about its anchor point (-X, -Y), in degrees counter-clockwise. Alignment is
        applied along the rotated baseline.
    .PARAMETER Role
        In a tagged document (New-VellumPdfPageDocument -Tagged, PdfA2a or PdfUA1): the
        structure role of this text - P (default), H1..H6, Span or Caption.
    .PARAMETER Language
        BCP 47 language tag of this text, written on its structure element (tagged documents).
    .EXAMPLE
        $doc | Add-VellumPdfText 'Invoice 2026-001' -X 72 -Y 760 -FontSize 18 -Font HelveticaBold
    .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)]
        [string]$Text,

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

        [Parameter(Mandatory)] [double]$X,
        [Parameter(Mandatory)] [double]$Y,

        [ValidateRange(1.0, 1000.0)]
        [double]$FontSize = 12,

        [ValidateSet('Courier', 'CourierBold', 'CourierBoldOblique', 'CourierOblique',
            'Helvetica', 'HelveticaBold', 'HelveticaBoldOblique', 'HelveticaOblique',
            'Symbol', 'TimesBold', 'TimesBoldItalic', 'TimesItalic', 'TimesRoman', 'ZapfDingbats')]
        [string]$Font = 'Helvetica',

        [VellumPdf.Fonts.EmbeddedFontHandle]$FontHandle,

        [object]$Color,

        [ValidateSet('Left', 'Center', 'Right')]
        [string]$Align = 'Left',

        # Extra space after every character, in points (negative tightens).
        [ValidateRange(-100.0, 100.0)]
        [double]$CharSpacing,

        # Extra space after every space character, in points.
        [ValidateRange(-100.0, 100.0)]
        [double]$WordSpacing,

        # Baseline shift in points: positive raises (superscript), negative lowers.
        [ValidateRange(-1000.0, 1000.0)]
        [double]$Rise,

        # Horizontal scaling in percent (100 = normal).
        [ValidateRange(1.0, 1000.0)]
        [double]$HorizontalScale,

        # Rotation about the anchor (X, Y) in degrees, counter-clockwise.
        [double]$Rotation,

        # Structure role in a tagged document: P (default), H1-H6, Span or Caption.
        [ValidateSet('P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Span', 'Caption')]
        [string]$Role = 'P',

        # BCP 47 language of this text, for a tagged document.
        [ValidateNotNullOrEmpty()]
        [string]$Language
    )

    process {
        try {
            Assert-VellumPdfDocumentOpen -Document $Document -CommandName 'Add-VellumPdfText'
            $Page = Resolve-VellumPdfPageNumber -Document $Document -Page $(if ($PSBoundParameters.ContainsKey('Page')) { $PSBoundParameters['Page'] } else { 1 }) -Bound $PSBoundParameters.ContainsKey('Page') -CommandName 'Add-VellumPdfText'
            Assert-VellumPdfLanguageTag -Value $Language -CommandName 'Add-VellumPdfText'
            Assert-VellumPdfValidText -Text $Text -CommandName 'Add-VellumPdfText'
            $Text = Expand-VellumPdfTab -Text $Text
            $structType = Resolve-VellumPdfValidSetValue -Command $PSCmdlet.MyInvocation.MyCommand -Parameter 'Role' -Value $Role
            Assert-VellumPdfCoordinate $X 'X' 'Add-VellumPdfText'; Assert-VellumPdfCoordinate $Y 'Y' 'Add-VellumPdfText'
            if ($Text -match '[\r\n]') { throw 'Add-VellumPdfText: -Text must be a single line; use New-VellumPdfDocument for wrapped text.' }

            $resolved = Get-VellumPdfPageFont -Document $Document -FontHandle $FontHandle -Font $Font -CommandName 'Add-VellumPdfText'
            Write-VellumPdfMissingGlyphWarning -Handle $resolved.Handle -Text $Text -CommandName 'Add-VellumPdfText'
            if (-not $FontHandle -and ($Align -ne 'Left' -or $PSBoundParameters.ContainsKey('CharSpacing') -or $PSBoundParameters.ContainsKey('WordSpacing') -or $PSBoundParameters.ContainsKey('HorizontalScale'))) {
                Assert-VellumPdfMeasurableFont -Font $Font -CommandName 'Add-VellumPdfText' -Reason 'it aligns or spaces the text'
            }
            if (-not $resolved.Handle -and $Document.Conformance -eq [VellumPdf.Document.PdfConformance]::None) {
                Write-VellumPdfEncodingWarning -Text $Text -CommandName 'Add-VellumPdfText'
            }
            $canvas = Get-VellumPdfPageCanvas -Document $Document -Page $Page -CommandName 'Add-VellumPdfText'
            $pdfPage = $Document.Pages[$Page - 1]
            $bound = $PSBoundParameters
            if (-not $Document.Tagged -and ($bound.ContainsKey('Role') -or $bound.ContainsKey('Language'))) {
                Write-Verbose 'Add-VellumPdfText: -Role/-Language only matter in a tagged document (New-VellumPdfPageDocument -Tagged).'
            }

            # Alignment shifts the start by the text's width. The engine measures without
            # character/word spacing and scaling, so when those are set the width is computed
            # here and the text is placed left-aligned at the adjusted x.
            $rotated = $bound.ContainsKey('Rotation') -and $Rotation -ne 0
            if ($bound.ContainsKey('Rotation') -and -not [double]::IsFinite($Rotation)) { throw 'Add-VellumPdfText: -Rotation must be a finite number of degrees.' }
            $anchorX = $X; $anchorY = $Y
            # Rotated text is drawn at the origin of a rotated, moved coordinate system.
            $drawX = if ($rotated) { 0.0 } else { $X }
            $drawY = if ($rotated) { 0.0 } else { $Y }
            $hasSpacing = $bound.ContainsKey('CharSpacing') -or $bound.ContainsKey('WordSpacing') -or $bound.ContainsKey('HorizontalScale')
            $startX = $drawX
            $showAlignment = [VellumPdf.Canvas.TextAlignment]::$Align
            $tc = if ($bound.ContainsKey('CharSpacing')) { $CharSpacing } else { 0.0 }
            $tw = if ($bound.ContainsKey('WordSpacing')) { $WordSpacing } else { 0.0 }
            $th = if ($bound.ContainsKey('HorizontalScale')) { $HorizontalScale } else { 100.0 }
            if ($hasSpacing -and $Align -ne 'Left') {
                $width = Measure-VellumPdfTextWidth -Resolved $resolved -Text $Text -FontSize $FontSize -CharSpacing $tc -WordSpacing $tw -HorizontalScale $th -VisibleExtent
                $startX = if ($Align -eq 'Center') { $drawX - $width / 2 } else { $drawX - $width }
                $showAlignment = [VellumPdf.Canvas.TextAlignment]::Left
            }
            # Word spacing (Tw) only applies to the single-byte space code. An embedded font is drawn with
            # two-byte glyph ids, so viewers ignore it; place the words one by one instead.
            $placeWords = [bool]($resolved.Handle -and $bound.ContainsKey('WordSpacing'))

            [void]$canvas.SaveState()
            try {
                if ($rotated) {
                    $radians = $Rotation * [math]::PI / 180
                    $cos = [math]::Round([math]::Cos($radians), 8) + 0.0
                    $sin = [math]::Round([math]::Sin($radians), 8) + 0.0
                    [void]$canvas.Concat($cos, $sin, (0.0 - $sin), $cos, $anchorX, $anchorY)
                }
                if ($bound.ContainsKey('Color')) {
                    $c = ConvertTo-VellumColor $Color -Document $Document -CommandName 'Add-VellumPdfText'
                    [void]$canvas.SetFillColorRgb($c[0], $c[1], $c[2])
                }
                Invoke-VellumPdfTagged -Document $Document -Canvas $canvas -PdfPage $pdfPage -Kind Structure -StructType $structType -Language $Language -Draw {
                    [void]$canvas.BeginText()
                    try {
                    if ($bound.ContainsKey('CharSpacing')) { [void]$canvas.SetCharSpacing($CharSpacing) }
                    if ($bound.ContainsKey('WordSpacing') -and -not $placeWords) { [void]$canvas.SetWordSpacing($WordSpacing) }
                    if ($bound.ContainsKey('Rise')) { [void]$canvas.SetTextRise($Rise) }
                    if ($bound.ContainsKey('HorizontalScale')) { [void]$canvas.SetHorizScaling($HorizontalScale) }
                    if ($resolved.Handle) {
                        $Document.RegisterEmbeddedFontUsage($pdfPage, $resolved.Handle)
                        [void]$canvas.SetFontByName($resolved.Handle.ResourceName, $FontSize)
                        if ($placeWords) {
                            # Each word keeps its trailing space glyph, so extracted text still has its spaces.
                            $cursor = $startX
                            foreach ($m in [regex]::Matches($Text, '[^ ]*(?: |$)')) {
                                $piece = $m.Value
                                if ($piece.Length -eq 0) { continue }
                                [void][PSVellumPDF.Interop.CanvasHelpers]::ShowEmbeddedText($canvas, $resolved.Handle, $piece, $FontSize, $cursor, $drawY, [VellumPdf.Canvas.TextAlignment]::Left)
                                $trailing = if ($piece.EndsWith(' ')) { $tw } else { 0.0 }
                                $cursor += Measure-VellumPdfTextWidth -Resolved $resolved -Text $piece -FontSize $FontSize -CharSpacing $tc -WordSpacing $trailing -HorizontalScale $th
                            }
                        }
                        else {
                            [void][PSVellumPDF.Interop.CanvasHelpers]::ShowEmbeddedText($canvas, $resolved.Handle, $Text, $FontSize, $startX, $drawY, $showAlignment)
                        }
                    }
                    else {
                        [void]$canvas.SetFont($resolved.Resource, $FontSize)
                        [void]$canvas.ShowTextAligned($Text, $startX, $drawY, $showAlignment)
                    }
                    }
                    finally { [void]$canvas.EndText() }
                }
            }
            finally { [void]$canvas.RestoreState() }
            $Document
        }
        catch {
            $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Add-VellumPdfText'))
        }
    }
}