Public/Measure-VellumPdfText.ps1

function Measure-VellumPdfText {
    <#
    .SYNOPSIS
        Measures the width of a line of text in a PDF font.
    .DESCRIPTION
        Returns how wide the text is in points when drawn in a base-14 font (-Font) or an embedded
        TrueType font (-FontHandle from Register-VellumPdfFont), using the same metrics the drawing
        cmdlets use for alignment and wrapping. Use it to size a box, to centre something by hand or
        to check that a label fits before placing it. -CharSpacing, -WordSpacing and
        -HorizontalScale are added the way Add-VellumPdfText applies them. The width is the PDF advance
        width: with -CharSpacing it includes the space after the last character (a viewer adds it there
        too), whereas Add-VellumPdfText aligns on the visible extent without it. A tab is measured as
        four spaces, which is how the drawing cmdlets draw it.

        The result also lists characters the font cannot show: UnencodableCharacters for a base-14 font
        (outside Windows-1252, drawn as a question mark) and MissingGlyphs for an embedded font (drawn
        as an empty box). The text must be a single line.
    .PARAMETER Text
        The text to measure. A single line; accepts pipeline input.
    .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 CharSpacing
        Extra space after every character, in points.
    .PARAMETER WordSpacing
        Extra space after every space character, in points.
    .PARAMETER HorizontalScale
        Horizontal scaling in percent (100 is normal).
    .EXAMPLE
        Measure-VellumPdfText 'Invoice 2026-001' -FontSize 18 -Font HelveticaBold | Select-Object -ExpandProperty Width
    .EXAMPLE
        $width = (Measure-VellumPdfText $label -FontSize 10).Width
        $doc | Add-VellumPdfText $label -X (300 - $width / 2) -Y 700 -FontSize 10
    .OUTPUTS
        PSVellumPDF.TextMeasurement with Text, Font, FontSize, Width, UnencodableCharacters and MissingGlyphs.
    #>

    [CmdletBinding()]
    [OutputType('PSVellumPDF.TextMeasurement')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [AllowEmptyString()]
        [string]$Text,

        [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,

        [ValidateRange(-100.0, 100.0)]
        [double]$CharSpacing = 0,

        [ValidateRange(-100.0, 100.0)]
        [double]$WordSpacing = 0,

        [ValidateRange(1.0, 1000.0)]
        [double]$HorizontalScale = 100
    )

    process {
        try {
            if ($Text -match '[\r\n]') { throw 'Measure-VellumPdfText: -Text must be a single line.' }
            Assert-VellumPdfValidText -Text $Text -CommandName 'Measure-VellumPdfText'
            $measured = Expand-VellumPdfTab -Text $Text
            if (-not $FontHandle) { Assert-VellumPdfMeasurableFont -Font $Font -CommandName 'Measure-VellumPdfText' -Reason 'it measures text' }
            $resolved = $null
            $unencodable = [System.Collections.Generic.List[string]]::new()
            $missing = [System.Collections.Generic.List[string]]::new()
            if ($FontHandle) {
                $resolved = [pscustomobject]@{ Handle = $FontHandle; Resource = $null }
                foreach ($rune in $measured.EnumerateRunes()) {
                    if ($FontHandle.GetGlyphId($rune.Value) -eq 0 -and -not $missing.Contains($rune.ToString())) { $missing.Add($rune.ToString()) }
                }
                $fontName = 'embedded'
            }
            else {
                $standard = [VellumPdf.Fonts.Standard14]::$Font
                $resource = [pscustomobject]@{}
                $resource | Add-Member -MemberType ScriptMethod -Name MeasureString -Value {
                    param([string]$s, [double]$size)
                    [VellumPdf.Fonts.Standard14Metrics]::MeasureString($standard, $s, $size)
                }.GetNewClosure()
                $resolved = [pscustomobject]@{ Handle = $null; Resource = $resource }
                for ($i = 0; $i -lt $measured.Length; $i++) {
                    $code = [int]$measured[$i]
                    if ($code -lt 0x80 -or (Test-VellumPdfWinAnsiChar -CodeUnit $code)) { continue }
                    $shown = if ([char]::IsHighSurrogate($measured[$i]) -and $i + 1 -lt $measured.Length) { $measured.Substring($i, 2) } else { [string]$measured[$i] }
                    if ($shown.Length -eq 2) { $i++ }
                    if (-not $unencodable.Contains($shown)) { $unencodable.Add($shown) }
                }
                $fontName = $Font
            }
            $width = Measure-VellumPdfTextWidth -Resolved $resolved -Text $measured -FontSize $FontSize -CharSpacing $CharSpacing -WordSpacing $WordSpacing -HorizontalScale $HorizontalScale
            [pscustomobject]@{
                PSTypeName             = 'PSVellumPDF.TextMeasurement'
                Text                   = $Text
                Font                   = $fontName
                FontSize               = $FontSize
                Width                  = [math]::Round($width, 4)
                UnencodableCharacters  = $unencodable.ToArray()
                MissingGlyphs          = $missing.ToArray()
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Measure-VellumPdfText'))
        }
    }
}