Private/ConvertTo-VellumPdfPathOp.ps1
|
function ConvertTo-VellumPdfPathOp { <# .SYNOPSIS Parses path data or a point list into validated drawing operations. .DESCRIPTION -Data is a small subset of SVG path syntax in PDF points: absolute commands only, M x y (move), L x y (line), C x1 y1 x2 y2 x y (cubic Bezier) and Z (close). A command's numbers may repeat (L 1 2 3 4 draws two lines). -Point is a polyline / polygon given as x,y pairs; -Close closes it. Returns objects with Op (M, L, C or Z) and Args (doubles). #> [CmdletBinding()] param( [string]$Data, [object[]]$Point, [switch]$Close, [Parameter(Mandatory)] [string]$CommandName ) $ops = [System.Collections.Generic.List[object]]::new() $number = '[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?' if ($PSBoundParameters.ContainsKey('Point')) { if ($Point.Count -lt 2) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: -Point needs at least two points.") } $first = $true foreach ($p in $Point) { $pair = @($p) if ($pair.Count -ne 2) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: each -Point must be an x,y pair.") } foreach ($v in $pair) { if ($v -isnot [ValueType]) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: -Point coordinates must be numbers.") } Assert-VellumPdfCoordinate $v 'Point' $CommandName } $ops.Add([pscustomobject]@{ Op = $(if ($first) { 'M' } else { 'L' }); Args = [double[]]@($pair[0], $pair[1]) }) $first = $false } if ($Close) { $ops.Add([pscustomobject]@{ Op = 'Z'; Args = [double[]]@() }) } return , $ops.ToArray() } $tokens = [regex]::Matches($Data, "[A-Za-z]|$number|[^\s,]") $arity = @{ M = 2; L = 2; C = 6; Z = 0 } $i = 0 $cmd = $null $sawMove = $false while ($i -lt $tokens.Count) { $t = $tokens[$i].Value if ($t -match '^[A-Za-z]$') { $cmd = $t if ($cmd -cnotmatch '^[MLCZ]$') { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: unsupported path command '$t'. Use absolute M, L, C and Z only.") } $i++ if ($cmd -eq 'Z') { if (-not $sawMove) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: path data must start with M.") } $ops.Add([pscustomobject]@{ Op = 'Z'; Args = [double[]]@() }) $cmd = $null continue } } elseif ($null -eq $cmd) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: path data must start with a command (M), found '$t'.") } $n = $arity[$cmd] if ($null -eq $n) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: unsupported path command '$cmd'. Use absolute M, L, C and Z only.") } if ($i + $n -gt $tokens.Count) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: '$cmd' needs $n numbers.") } $nums = [double[]]::new($n) for ($k = 0; $k -lt $n; $k++) { $tv = $tokens[$i + $k].Value if ($tv -notmatch "^$number$") { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: expected a number for '$cmd' but found '$tv'.") } $nums[$k] = [double]::Parse($tv, [cultureinfo]::InvariantCulture) Assert-VellumPdfCoordinate $nums[$k] 'Data' $CommandName } $i += $n if ($cmd -ne 'M' -and -not $sawMove) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: path data must start with M.") } if ($cmd -eq 'M') { $sawMove = $true $ops.Add([pscustomobject]@{ Op = 'M'; Args = $nums }) $cmd = 'L' # further pairs after M are implicit line-tos, as in SVG } else { $ops.Add([pscustomobject]@{ Op = $cmd; Args = $nums }) } } if ($ops.Count -lt 2) { throw (New-VellumPdfErrorRecord InvalidArgument "${CommandName}: the path needs at least a move and one segment.") } , $ops.ToArray() } |