Public/Add-VellumPdfImage.ps1

function Add-VellumPdfImage {
    <#
    .SYNOPSIS
        Embeds an image into a VellumPdf document, from a file or from memory.
    .DESCRIPTION
        Wraps Document.Add(LayoutImage). Reads the image from -Path (loader chosen
        by file extension) or from -ImageBytes with an explicit -Format, constructs
        a LayoutImage, and adds it to the document. Formats: JPEG, PNG, BMP, GIF,
        TIFF, JBIG2, JPEG 2000.

        Supported extensions: .jpg/.jpeg, .png, .bmp, .gif, .tif/.tiff,
        .jbig2/.jb2, and .jp2/.jpx/.j2k/.jpf (JPEG 2000). For -ImageBytes, pass
        -Format (Jpeg/Png/Bmp/Gif/Tiff/Jbig2/Jpeg2000) since there is no extension.

        Note for PDF/A: JPEG 2000 and JBIG2 images compose with PDF/A-2. The
        bundled engine (VellumPdf 1.5.4+) embeds the JP2 box metadata that
        PDF/A-2 clause 6.2.8.3 requires, and CI validates a PDF/A-2b document
        with each image type through veraPDF. The JPEG 2000 source must still
        satisfy PDF/A-2's own rules - 1, 3, or 4 colour channels, all sharing a
        single bit depth.

        Optional -Width and -Height (in points) constrain the rendered size; when
        omitted the image renders at its natural size. -Alignment positions the
        image horizontally on the page. -AltText supplies alternate text that aids
        tagged PDF and PDF-A accessibility readers.

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

        The document flows through the pipeline for chaining with other
        Add-VellumPdf* functions.
    .PARAMETER Document
        The live VellumPdf document flowing through the pipeline. The same
        instance is returned after the image is added, enabling chaining.
    .PARAMETER Path
        File system path to the image file (parameter set 'Path'). Supported
        extensions are .jpg, .jpeg, .png, .bmp, .gif, .tif, .tiff, .jbig2, .jb2,
        .jp2, .jpx, .j2k, and .jpf. The path is resolved relative to the current
        PowerShell provider location. Mandatory and positional (position 0).
    .PARAMETER ImageBytes
        Raw image bytes to embed (parameter set 'Bytes'), for images produced in
        memory rather than read from disk. Requires -Format.
    .PARAMETER Format
        The format of -ImageBytes: Jpeg, Png, Bmp, Gif, Tiff, Jbig2, or Jpeg2000.
        Required with -ImageBytes (there is no extension to infer it from).
    .PARAMETER Width
        Rendered width of the image in points, between 1 and 100000. When
        omitted the image is rendered at its natural width.
    .PARAMETER Height
        Rendered height of the image in points, between 1 and 100000. When
        omitted the image is rendered at its natural height.
    .PARAMETER Alignment
        Horizontal alignment of the image on the page. Accepts Left, Center,
        Right, or Justify. Defaults to Left.
    .PARAMETER AltText
        Alternate text description for the image. Stored on the LayoutImage
        element and included in tagged PDF structure for accessibility readers
        and PDF/A compliance.
    .PARAMETER MarginTop
        Extra spacing in points above the image element. Does not affect the
        left/right page margins.
    .PARAMETER MarginBottom
        Extra spacing in points below the image element. Does not affect the
        left/right page margins.
    .PARAMETER Columns
        Pixel width of raw CCITT data (-Format Ccitt only; the fax bits have no header).
    .PARAMETER Rows
        Pixel height of raw CCITT data (-Format Ccitt only).
    .PARAMETER CcittK
        CCITT encoding: -1 Group 4 (default), 0 Group 3 one-dimensional, >0 Group 3
        two-dimensional.
    .PARAMETER BlackIs1
        CCITT: a 1 bit means black (default: 0 is black).
    .PARAMETER EncodedByteAlign
        CCITT: each encoded row starts on a byte boundary.
    .PARAMETER EndOfLine
        CCITT: the data contains end-of-line codes.
    .PARAMETER ReduceToEightBit
        Downsample high-bit-depth sources to 8 bits per component. Smaller file,
        less precision. PNG, TIFF, JBIG2, JPEG 2000 and CCITT only.
    .PARAMETER DecodeToRaster
        Decode the source image into a raster rather than embedding its
        codestream with the matching PDF filter (the default keeps the original
        compression). Supported for JBIG2; VellumPdf 2.3 refuses JPEG 2000 and
        CCITT Group 4 ("not yet supported") and PNG/TIFF are rasters already.
    .PARAMETER MarginLeft
        Extra space in points to the left of the image.
    .PARAMETER MarginRight
        Extra space in points to the right of the image.
    .EXAMPLE
        New-VellumPdfDocument |
            Add-VellumPdfImage -Path ./logo.png |
            Save-VellumPdfDocument -Path ./report.pdf
    .EXAMPLE
        $doc | Add-VellumPdfImage -Path ./photo.jpg -Width 200 -Height 150 `
               -Alignment Center -AltText 'Company photo'
    .EXAMPLE
        # Embed an in-memory PNG (e.g. a chart) without a temp file
        $doc | Add-VellumPdfImage -ImageBytes $pngBytes -Format Png -Width 150
    .OUTPUTS
        VellumPdf.Layout.Document (the same instance, for chaining)
    #>

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

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'Path')]
        [string]$Path,

        # In-memory image bytes (e.g. a chart or QR rendered by another library).
        [Parameter(Mandatory, ParameterSetName = 'Bytes')]
        [byte[]]$ImageBytes,

        # Image format of -ImageBytes, since there is no file extension to infer it.
        [Parameter(Mandatory, ParameterSetName = 'Bytes')]
        [ValidateSet('Jpeg', 'Png', 'Bmp', 'Gif', 'Tiff', 'Jbig2', 'Jpeg2000', 'Ccitt')]
        [string]$Format,

        # --- CCITT Group 3/4 fax data (-Format Ccitt only): raw bits carry no header ---
        [Parameter(ParameterSetName = 'Bytes')]
        [ValidateRange(1, 100000)]
        [int]$Columns,

        [Parameter(ParameterSetName = 'Bytes')]
        [ValidateRange(1, 100000)]
        [int]$Rows,

        # CCITT K: -1 = Group 4 (default), 0 = Group 3 1-D, >0 = Group 3 2-D.
        [Parameter(ParameterSetName = 'Bytes')]
        [ValidateRange(-1, 1000)]
        [int]$CcittK = -1,

        [Parameter(ParameterSetName = 'Bytes')]
        [switch]$BlackIs1,

        [Parameter(ParameterSetName = 'Bytes')]
        [switch]$EncodedByteAlign,

        [Parameter(ParameterSetName = 'Bytes')]
        [switch]$EndOfLine,

        # Downsample 16-bit sources to 8 bits per component (smaller, less precise).
        # PNG, TIFF, JBIG2, JPEG 2000 and CCITT only.
        [switch]$ReduceToEightBit,

        # Decode the source into a raster instead of embedding the codestream as
        # a PDF-native filter. Works for JBIG2; JPEG 2000 and CCITT Group 4 are
        # refused by the engine.
        [switch]$DecodeToRaster,

        [ValidateRange(1.0, 100000.0)]
        [double]$Width,

        [ValidateRange(1.0, 100000.0)]
        [double]$Height,

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

        [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) }, ErrorMessage = '-AltText must not be empty or whitespace.')]

        [string]$AltText,

        [ValidateRange(0.0, 10000.0)]
        [double]$MarginTop,

        [ValidateRange(0.0, 10000.0)]
        [double]$MarginBottom,

        [ValidateRange(0.0, 10000.0)]
        [double]$MarginLeft,

        [ValidateRange(0.0, 10000.0)]
        [double]$MarginRight
    )

    process {
        try {
            Assert-VellumPdfDocumentOpen -Document $Document -CommandName 'Add-VellumPdfImage'

            # Resolve the bytes and a format key from either input set.
            if ($PSCmdlet.ParameterSetName -eq 'Bytes') {
                $bytes = $ImageBytes
                $formatKey = $Format.ToLowerInvariant()
                $source = 'the supplied image bytes'
            }
            else {
                $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
                if (-not [System.IO.File]::Exists($resolved)) {
                    throw (New-VellumPdfErrorRecord FileNotFound "Add-VellumPdfImage: file not found: '$resolved'. Verify the path and try again.")
                }
                $bytes = [System.IO.File]::ReadAllBytes($resolved)
                $source = "'$resolved'"
                $formatKey = Get-VellumPdfImageFormat -Extension ([System.IO.Path]::GetExtension($resolved)) -CommandName 'Add-VellumPdfImage'
            }

            # Load options (bit depth / decode mode) exist only on some loaders.
            $wantsOptions = $ReduceToEightBit -or $DecodeToRaster
            if ($wantsOptions -and $formatKey -notin @('png', 'tiff', 'jbig2', 'jpeg2000', 'ccitt')) {
                throw ("Add-VellumPdfImage: -ReduceToEightBit and -DecodeToRaster apply to PNG, TIFF, JBIG2, JPEG 2000 " +
                    "and CCITT images only, not $formatKey.")
            }
            $loadOptions = $null
            if ($wantsOptions) {
                $loadOptions = [VellumPdf.Images.ImageLoadOptions]::new()
                if ($ReduceToEightBit) { $loadOptions.BitDepth = [VellumPdf.Images.ImageBitDepth]::ReduceToEight }
                if ($DecodeToRaster)   { $loadOptions.DecodeMode = [VellumPdf.Images.ImageDecodeMode]::DecodeToRaster }
            }
            $ccitt = $null
            if ($formatKey -eq 'ccitt') {
                if (-not $PSBoundParameters.ContainsKey('Columns') -or -not $PSBoundParameters.ContainsKey('Rows')) {
                    throw 'Add-VellumPdfImage: -Format Ccitt needs -Columns and -Rows (raw fax data has no header).'
                }
                $ccitt = [VellumPdf.Images.CcittOptions]::new()
                $ccitt.K = $CcittK
                $ccitt.BlackIs1 = $BlackIs1.IsPresent
                $ccitt.EncodedByteAlign = $EncodedByteAlign.IsPresent
                $ccitt.EndOfLine = $EndOfLine.IsPresent
            }
            elseif ($PSBoundParameters.ContainsKey('Columns') -or $PSBoundParameters.ContainsKey('Rows') -or $PSBoundParameters.ContainsKey('CcittK') -or
                    $BlackIs1 -or $EncodedByteAlign -or $EndOfLine) {
                throw 'Add-VellumPdfImage: -Columns/-Rows/-CcittK/-BlackIs1/-EncodedByteAlign/-EndOfLine apply to -Format Ccitt only.'
            }

            # Loader errors ("Not a PNG file.") do not mention the source; Import-VellumPdfImage
            # rethrows them with it so batch scripts can identify the culprit.
            $xObject = Import-VellumPdfImage -Bytes $bytes -Format $formatKey -LoadOptions $loadOptions -Columns $Columns -Rows $Rows `
                -CcittOptions $ccitt -CommandName 'Add-VellumPdfImage' -Source $source

            Write-VellumPdfRgbIntentWarning -Document $Document -CommandName 'Add-VellumPdfImage' -Image $xObject
            $layoutImage = [VellumPdf.Layout.Elements.LayoutImage]::new($xObject)

            if ($PSBoundParameters.ContainsKey('Width')) {
                $layoutImage.Width = $Width
            }
            if ($PSBoundParameters.ContainsKey('Height')) {
                $layoutImage.Height = $Height
            }

            $layoutImage.Alignment = [VellumPdf.Layout.Core.HorizontalAlignment]::$Alignment

            if ($PSBoundParameters.ContainsKey('AltText')) {
                $layoutImage.AltText = $AltText
            }

            Set-VellumPdfElementMargin -Element $layoutImage -Top $MarginTop -Bottom $MarginBottom `
                -BoundParameters $PSBoundParameters -Document $Document
            if ($PSBoundParameters.ContainsKey('MarginLeft') -or $PSBoundParameters.ContainsKey('MarginRight')) {
                $cur = $layoutImage.Margins
                $layoutImage.Margins = [VellumPdf.Layout.Core.EdgeInsets]::new(
                    $cur.Top, $(if ($PSBoundParameters.ContainsKey('MarginRight')) { $MarginRight } else { $cur.Right }),
                    $cur.Bottom, $(if ($PSBoundParameters.ContainsKey('MarginLeft')) { $MarginLeft } else { $cur.Left }))
            }

            [void]$Document.Add($layoutImage)
            $Document
        }
        catch {
            $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Add-VellumPdfImage'))
        }
    }
}