Public/Add-VellumPdfTable.ps1

function Add-VellumPdfTable {
    <#
    .SYNOPSIS
        Adds a table to a VellumPdf document.
    .DESCRIPTION
        Wraps Document.Add(TableElement). Builds a TableElement from row data, an
        optional header row, optional column widths, border and row styling, and a
        default cell text style. The document flows through the pipeline for
        chaining with other Add-VellumPdf* functions.

        -Row accepts two shapes (the first row decides which):
          - Records: PSCustomObject rows straight from Import-Csv or Select-Object,
            or one hashtable per row. Columns are the property/key names and a
            header is derived from them when -Header is omitted.
          - Cell arrays: one array per row. Each element is a scalar (added as
            text) or a rich-cell hashtable for that one cell:
              @{ Text = 'Total'; ColSpan = 2; Alignment = 'Right';
                 Background = '#eeeeee'; Font = 'HelveticaBold'; FontSize = 11;
                 Color = 'navy' }
            Text is required; the rest are optional.

        Colour parameters (-BorderColor, -HeaderBackground, -AlternateRowBackground,
        and a cell's Background/Color) accept an R,G,B triple in 0..1, a hex string
        ('#3366cc'/'#36c'), or a colour name.

        NOTE: with cell-array rows, a SINGLE row needs the unary comma operator so
        PowerShell does not flatten the outer array: -Row @(,@('Cell1','Cell2')).
        A flat array like -Row @('a','b') is treated as two one-cell rows. Records
        do not need this.

        -MarginTop and -MarginBottom apply spacing above and below the table
        without affecting the left/right margins already set on the element.

        Import-Csv example:
            Import-Csv data.csv | ForEach-Object { $rows += $_ }
            Add-VellumPdfTable -Row (Import-Csv data.csv)
    .PARAMETER Document
        The live VellumPdf document flowing through the pipeline. The same
        instance is returned after the table is added, enabling chaining.
    .PARAMETER Header
        An optional string array of column header labels. When supplied, a
        styled header row is prepended to the table via AddHeaderRow(). The
        count of header cells determines the expected column count for
        -ColumnWidth mismatch warnings.
    .PARAMETER Row
        The table rows. Either PSCustomObject/hashtable records (columns are the
        property/key names; header derived when -Header is omitted), or one array
        per row whose elements are scalars or rich-cell hashtables (see the
        description). For a single cell-array row, use the unary comma operator to
        prevent PowerShell from flattening the outer array: -Row @(,@('a','b')).
    .PARAMETER ColumnWidth
        Column widths in points, each between 0.01 and 100000. The count should
        match the number of columns determined by the -Header or first -Row; a
        mismatch emits a warning and extra widths are ignored.
    .PARAMETER BorderWidth
        Border line width in points applied to all cell borders, between 0 and
        100. When omitted the VellumPdf library default is used.
    .PARAMETER BorderColor
        Border line colour: an R,G,B triple in 0..1, a hex string ('#3366cc'),
        or a colour name. When omitted the library default border colour is used.
    .PARAMETER HeaderBackground
        Background fill colour for the header row (R,G,B triple, hex, or name).
        Applied only when a header is present (supplied via -Header or derived
        from record columns).
    .PARAMETER AlternateRowBackground
        Background fill colour applied to every second data row (zebra striping):
        an R,G,B triple in 0..1, a hex string, or a colour name.
    .PARAMETER ColumnAlignment
        Per-column horizontal alignment by column index (Left/Center/Right/
        Justify), overriding -Alignment for those columns. A cell's own
        Alignment key still wins over this.
    .PARAMETER Font
        A base-14 font name applied as the default cell style for all data
        cells. When omitted the document default font is used.
    .PARAMETER FontSize
        Font size in points for all data cells, between 1 and 1000. When
        omitted the document default size is used.
    .PARAMETER Alignment
        Horizontal text alignment for all cells (header and data). Accepts
        Left, Center, Right, or Justify. Defaults to Left.
    .PARAMETER MarginTop
        Extra spacing in points above the table element. Does not affect the
        left/right page margins.
    .PARAMETER MarginBottom
        Extra spacing in points below the table element. Does not affect the
        left/right page margins.
    .EXAMPLE
        $headers = @('Name', 'Score', 'Grade')
        $rows = @(
            @('Alice', '95', 'A'),
            @('Bob', '82', 'B')
        )
        New-VellumPdfDocument |
            Add-VellumPdfTable -Header $headers -Row $rows -BorderWidth 0.5 |
            Save-VellumPdfDocument -Path ./report.pdf
    .EXAMPLE
        $doc | Add-VellumPdfTable -Row @(@('Cell1','Cell2')) `
               -ColumnWidth @(100, 200) -Font Helvetica -FontSize 10 -Alignment Center
    .OUTPUTS
        VellumPdf.Layout.Document (the same instance, for chaining)
    #>

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

        [string[]]$Header,

        [Parameter(Mandatory)]
        [object[]]$Row,

        [ValidateRange(0.01, 100000)]
        [double[]]$ColumnWidth,

        [ValidateRange(0, 100)]
        [double]$BorderWidth,

        # RGB triple (0..1), hex ('#3366cc'/'#36c'), or a colour name.
        [object]$BorderColor,

        # Background for the header row. RGB triple, hex, or a colour name.
        [object]$HeaderBackground,

        # Background applied to every second data row (zebra striping). RGB
        # triple, hex, or a colour name.
        [object]$AlternateRowBackground,

        # Per-column horizontal alignment, by column index, overriding
        # -Alignment for that column. Shorter than the column count leaves the
        # remaining columns on -Alignment.
        [ValidateSet('Left', 'Center', 'Right', 'Justify')]
        [string[]]$ColumnAlignment,

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

        [ValidateRange(1, 1000)]
        [double]$FontSize,

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

        [ValidateRange(0, 10000)]
        [double]$MarginTop,

        [ValidateRange(0, 10000)]
        [double]$MarginBottom
    )

    process {
        Assert-VellumPdfDocumentOpen -Document $Document -CommandName 'Add-VellumPdfTable'

        # Two row shapes are accepted:
        # - records: PSCustomObject (e.g. from Import-Csv) or a hashtable per
        # row. Columns are the property/key names; values are read by name.
        # - cell arrays: an array per row, each element a scalar or a rich-cell
        # hashtable (@{ Text=...; ColSpan=...; Background=...; Alignment=... }).
        # The first row decides the mode.
        $recordMode = $Row.Count -gt 0 -and (
            ($Row[0] -is [System.Management.Automation.PSCustomObject]) -or
            ($Row[0] -is [System.Collections.IDictionary]))

        # Resolve a single cell value to a spec hashtable carrying at least Text.
        $toSpec = {
            param($value)
            if ($value -is [System.Collections.IDictionary]) {
                if (-not $value.Contains('Text')) {
                    throw "Add-VellumPdfTable: a rich-cell hashtable must include a 'Text' key."
                }
                return $value
            }
            return @{ Text = [string]$value }
        }

        # Build the rows as arrays of cell specs, and the effective header labels.
        $headerLabels = $Header
        $specRows = [System.Collections.Generic.List[object]]::new()
        if ($recordMode) {
            $columns = if ($Row[0] -is [System.Collections.IDictionary]) {
                @($Row[0].Keys)
            }
            else {
                @($Row[0].PSObject.Properties.Name)
            }
            if (-not $headerLabels) { $headerLabels = [string[]]$columns }
            foreach ($rec in $Row) {
                $cells = foreach ($col in $columns) {
                    $cellValue = if ($rec -is [System.Collections.IDictionary]) { $rec[$col] } else { $rec.$col }
                    & $toSpec $cellValue
                }
                $specRows.Add(@($cells))
            }
        }
        else {
            foreach ($r in $Row) {
                $cells = foreach ($v in @($r)) { & $toSpec $v }
                $specRows.Add(@($cells))
            }
        }

        # Encoding warning over every header label and cell's text.
        $cellText = @($headerLabels) + @($specRows | ForEach-Object { $_ | ForEach-Object { [string]$_['Text'] } })
        Write-VellumPdfEncodingWarning -Text $cellText -CommandName 'Add-VellumPdfTable'

        $table = [VellumPdf.Layout.Elements.Table.TableElement]::new()

        # Apply default cell style when font or size is requested. Gaps are
        # filled from the document defaults: a style without a font renders in
        # the library-global Helvetica, not the document default.
        $wantsStyle = [bool]$Font -or $PSBoundParameters.ContainsKey('FontSize')
        if ($wantsStyle) {
            $default = Resolve-VellumPdfDefault -Document $Document
            $effFont = if ($Font) { $Font } else { $default.Font }
            $effSize = if ($PSBoundParameters.ContainsKey('FontSize')) { $FontSize } else { $default.FontSize }
            $table.DefaultCellStyle = New-VellumTextStyle -Font $effFont -FontSize $effSize
        }

        # Apply border width.
        if ($PSBoundParameters.ContainsKey('BorderWidth')) {
            $table.BorderWidth = $BorderWidth
        }

        # Resolve flexible colours once (RGB triple / hex / name -> ColorRgb).
        $toRgb = {
            param($value)
            $c = ConvertTo-VellumColor $value
            [VellumPdf.Layout.Core.ColorRgb]::new($c[0], $c[1], $c[2])
        }
        if ($PSBoundParameters.ContainsKey('BorderColor')) { $table.BorderColor = & $toRgb $BorderColor }
        $headerBg = if ($PSBoundParameters.ContainsKey('HeaderBackground')) { & $toRgb $HeaderBackground }
        $altBg    = if ($PSBoundParameters.ContainsKey('AlternateRowBackground')) { & $toRgb $AlternateRowBackground }

        # Builds a styled Cell from a spec hashtable at a column index.
        $buildCell = {
            param($spec, $colIndex)
            $cell = [VellumPdf.Layout.Elements.Table.Cell]::new([string]$spec['Text'])
            $align = if ($spec['Alignment']) { [string]$spec['Alignment'] }
                elseif ($ColumnAlignment -and $colIndex -lt $ColumnAlignment.Count) { $ColumnAlignment[$colIndex] }
                else { $Alignment }
            $cell.Alignment = [VellumPdf.Layout.Core.HorizontalAlignment]::$align
            if ($spec['ColSpan']) { $cell.ColSpan = [int]$spec['ColSpan'] }
            if ($spec['RowSpan']) { $cell.RowSpan = [int]$spec['RowSpan'] }
            if ($spec['Background']) { $cell.Background = & $toRgb $spec['Background'] }
            if ($spec['Font'] -or $spec['FontSize'] -or $spec['Color']) {
                $cellStyle = @{}
                if ($spec['Font'])     { $cellStyle['Font']     = [string]$spec['Font'] }
                if ($spec['FontSize']) { $cellStyle['FontSize'] = [double]$spec['FontSize'] }
                if ($spec['Color'])    { $cellStyle['Color']    = ConvertTo-VellumColor $spec['Color'] }
                $cell.Style = New-VellumTextStyle @cellStyle
            }
            $cell
        }

        # Apply column widths.
        if ($ColumnWidth) {
            $columnCount = if ($headerLabels) { $headerLabels.Count } elseif ($specRows.Count) { $specRows[0].Count } else { 0 }
            if ($ColumnWidth.Count -ne $columnCount) {
                Write-Warning ("Add-VellumPdfTable: -ColumnWidth has $($ColumnWidth.Count) value(s) " +
                    "but the table has $columnCount column(s); extra widths are ignored and " +
                    'missing ones fall back to the library default.')
            }
            [void]$table.SetColumnWidths($ColumnWidth)
        }

        # Add optional header row.
        if ($headerLabels) {
            $headerRow = $table.AddHeaderRow()
            if ($headerBg) { $headerRow.Background = $headerBg }
            $hi = 0
            foreach ($text in $headerLabels) {
                [void]$headerRow.AddCell((& $buildCell @{ Text = [string]$text } $hi))
                $hi++
            }
        }

        # Add data rows, applying zebra striping to every second row.
        $rowIndex = 0
        foreach ($specRow in $specRows) {
            $tableRow = $table.AddRow($false)
            if ($altBg -and ($rowIndex % 2 -eq 1)) { $tableRow.Background = $altBg }
            $colIndex = 0
            foreach ($spec in $specRow) {
                [void]$tableRow.AddCell((& $buildCell $spec $colIndex))
                $colIndex++
            }
            $rowIndex++
        }

        Set-VellumPdfElementMargin -Element $table -Top $MarginTop -Bottom $MarginBottom `
            -BoundParameters $PSBoundParameters

        [void]$Document.Add($table)
        $Document
    }
}