Public/Get-VellumPdfInfo.ps1
|
function Get-VellumPdfInfo { <# .SYNOPSIS Reads structural information from an existing PDF: pages, encryption, signatures, diagnostics. .DESCRIPTION EXPERIMENTAL - built on VellumPdf.Reader, which is still a Preview package: its API and reach may change between releases. Opens a PDF (any PDF, not only ones this module wrote) and reports its page count and page boxes, whether and how it is encrypted, the digital signatures it contains, which PDF/A and PDF/UA profiles it claims, and any problems the reader noticed. It never modifies the file. Signatures are listed with what the reader exposes: the sub-filter, the claimed signing time and the signed byte range. Catalog reports whether the document has a language, structure tree (tagged), XMP metadata, outline, form, output intents and named destinations. The reader does not verify a signature, so this does not tell you whether one is valid. Several files can be given (or piped); they are opened one at a time. A failure for one file (not found, unreadable, malformed, wrong password, ...) is a non-terminating error with that file as TargetObject and the next file is still processed; use -ErrorAction Stop to stop at the first one. Invalid parameters terminate. .PARAMETER Path The PDF file(s). Accepts wildcards. Piped strings bind here, and so do piped objects with a Path property; piped FileInfo objects (Get-ChildItem) bind to -LiteralPath through their PSPath, so names containing [ ] work. .PARAMETER LiteralPath The PDF file(s), taken literally. Aliases PSPath and FullName, so objects with either property (FileInfo, Select-Object FullName) bind by name. .PARAMETER Bytes The PDF as a byte array. .PARAMETER Password Password for an encrypted PDF (a SecureString). An empty user password is tried automatically. .PARAMETER IncludeSignatureContents Add the raw CMS signature bytes (Contents) to every signature object, so that a script can verify it, for example with [System.Security.Cryptography.Pkcs.SignedCms] over the ranges in ByteRange. .PARAMETER AllowReconstruction Try to recover a damaged file (missing or broken cross-reference table) by scanning for objects. The result then has WasReconstructed = $true. .PARAMETER MaxDecodedStreamBytes Refuse to decode any stream larger than this many bytes (1 MiB to 512 MiB, the default ceiling). Lower it for untrusted files. .PARAMETER MaxDiagnostics Stop collecting reader diagnostics after this many (1 to 1000). .PARAMETER MaxFormXObjectDepth Deepest nesting of form XObjects followed while reading content (1 to 32). .PARAMETER ReconstructionBudgetMultiplier How many times the file size reconstruction may scan (1 to 8); used with -AllowReconstruction. .EXAMPLE Get-VellumPdfInfo ./report.pdf | Select-Object PageCount, IsEncrypted, ClaimedProfiles .EXAMPLE (Get-VellumPdfInfo ./contract.pdf).Signatures .OUTPUTS PSVellumPDF.PdfInfo #> [CmdletBinding(DefaultParameterSetName = 'Path')] [OutputType('PSVellumPDF.PdfInfo')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path')] [string[]]$Path, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPath')] [Alias('PSPath', 'FullName')] [string[]]$LiteralPath, [Parameter(Mandatory, ParameterSetName = 'Bytes')] [AllowEmptyCollection()] [byte[]]$Bytes, [securestring]$Password, [switch]$AllowReconstruction, [switch]$IncludeSignatureContents, # Resource limits for hostile or huge files. They can only tighten the reader's # built-in ceilings (512 MiB decoded, 1000 diagnostics, 32 form levels, budget 8). [ValidateRange(1MB, 512MB)] [long]$MaxDecodedStreamBytes, [ValidateRange(1, 1000)] [int]$MaxDiagnostics, [ValidateRange(1, 32)] [int]$MaxFormXObjectDepth, [ValidateRange(1, 8)] [int]$ReconstructionBudgetMultiplier ) begin { try { $limits = @{} foreach ($n in 'MaxDecodedStreamBytes', 'MaxDiagnostics', 'MaxFormXObjectDepth', 'ReconstructionBudgetMultiplier') { if ($PSBoundParameters.ContainsKey($n)) { $limits[$n] = $PSBoundParameters[$n] } } $plain = if ($Password) { [System.Net.NetworkCredential]::new('', $Password).Password } } catch { $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Get-VellumPdfInfo')) } } process { try { foreach ($source in Get-VellumPdfInput -Path $Path -LiteralPath $LiteralPath -Bytes $Bytes -CommandName 'Get-VellumPdfInfo') { try { Read-VellumPdfInput -Source $source $reader = Open-VellumPdfReader -Limit $limits -Bytes $source.Data -Password $plain -AllowReconstruction:$AllowReconstruction ` -CommandName 'Get-VellumPdfInfo' -Source $source.Name try { $pages = @(foreach ($p in $reader.Pages) { [pscustomobject]@{ PSTypeName = 'PSVellumPDF.PdfPageInfo' Page = $p.Index + 1 Width = $p.MediaBox.Width Height = $p.MediaBox.Height Rotate = $p.Rotate MediaBox = @($p.MediaBox.LlX, $p.MediaBox.LlY, $p.MediaBox.UrX, $p.MediaBox.UrY) CropBox = @($p.CropBox.LlX, $p.CropBox.LlY, $p.CropBox.UrX, $p.CropBox.UrY) } }) $encryption = if ($reader.Encryption) { $e = $reader.Encryption [pscustomobject]@{ PSTypeName = 'PSVellumPDF.PdfEncryptionInfo' Version = $e.V Revision = $e.R KeyLengthBits = $e.KeyLengthBits StreamCipher = [string]$e.StreamCipher StringCipher = [string]$e.StringCipher Permissions = [string]$e.Permissions IsOwnerAccess = $e.IsOwnerAccess EncryptMetadata = $e.EncryptMetadata } } $signatures = @(foreach ($sig in $reader.Signatures) { $range = $sig.ByteRange.ToArray() $info = [pscustomobject]@{ PSTypeName = 'PSVellumPDF.PdfSignatureInfo' SubFilter = if ($sig.SubFilter) { [string]$sig.SubFilter.Value } else { $null } SigningTime = $sig.SigningTime ByteRange = $range ContentsLength = $sig.Contents.Length } if ($IncludeSignatureContents) { $info | Add-Member -NotePropertyName Contents -NotePropertyValue $sig.Contents.ToArray() } $info }) $diagnostics = @(foreach ($d in $reader.Diagnostics) { [pscustomobject]@{ PSTypeName = 'PSVellumPDF.PdfReaderDiagnostic' Severity = [string]$d.Severity Code = [string]$d.Code Message = $d.Message Page = if ($null -ne $d.PageIndex) { $d.PageIndex + 1 } ObjectNumber = $d.ObjectNumber Generation = $d.Generation } }) # The catalog's entries cannot be enumerated through the public API, so report # the presence of the ones that matter for accessibility and navigation. $has = { param($key) $null -ne $reader.Catalog.Get([VellumPdf.Core.PdfName]::new($key)) } $features = [pscustomobject]@{ PSTypeName = 'PSVellumPDF.PdfCatalogInfo' HasLanguage = & $has 'Lang' IsTagged = & $has 'StructTreeRoot' HasMetadata = & $has 'Metadata' HasOutline = & $has 'Outlines' HasForm = & $has 'AcroForm' HasOutputIntents = & $has 'OutputIntents' HasNamedDests = & $has 'Names' } $claimed = @() try { $claimed = @([VellumPdf.Conformance.PdfPreflight]::DetectClaimedProfiles($source.Data, $plain) | ForEach-Object { switch ($_) { 'PdfA2B' { 'PdfA2b' } 'PdfA2U' { 'PdfA2u' } 'PdfA2A' { 'PdfA2a' } default { [string]$_ } } }) } catch { Write-Verbose "Could not detect claimed profiles: $($_.Exception.Message)" } [pscustomobject]@{ PSTypeName = 'PSVellumPDF.PdfInfo' Path = $source.Name SizeBytes = $source.Data.Length PageCount = $reader.PageCount Pages = $pages IsEncrypted = [bool]$reader.Encryption Encryption = $encryption Signatures = $signatures ClaimedProfiles = $claimed WasReconstructed = $reader.WasReconstructed Diagnostics = $diagnostics Catalog = $features } } finally { $reader.Dispose() } } catch { Write-VellumPdfFileError -Cmdlet $PSCmdlet -ErrorRecord (ConvertTo-VellumPdfFileError -ErrorRecord $_ -Target $source.Name -CommandName 'Get-VellumPdfInfo') -ErrorActionPreferenceValue $ErrorActionPreference } finally { $source.Data = $null } # one file in memory at a time } } catch { $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Get-VellumPdfInfo')) } } } |