Public/Export-VellumPdfImage.ps1
|
function Export-VellumPdfImage { <# .SYNOPSIS Saves the images embedded in an existing PDF as files. .DESCRIPTION EXPERIMENTAL - built on VellumPdf.Reader, which is still a Preview package. Extracts every image XObject (and, by default, inline images and images in annotation appearances) and writes each to -DestinationPath. Images the reader can re-encode (Flate/raw samples, with their soft mask as alpha) are written as PNG. Compressed images keep their original data, which keeps them lossless: JPEG (.jpg) and JPEG 2000 (.jp2, .j2k) are written as they are; CCITT fax images are wrapped in a single-strip TIFF (.tif); JBIG2 images are joined with their shared "global" segments into a standalone sequential JBIG2 file (.jb2). A CCITT image TIFF cannot describe (Group 4 with byte-aligned rows, Group 3 without end-of-line codes unless it is one-dimensional and byte-aligned) and any other image that cannot be written as a PNG (for example CMYK or indexed samples) are skipped with a warning. Each file object carries Page, Encoding, Width, Height, BitsPerComponent, ColorSpace, ComponentCount, IsInline, IsStencilMask and HasSoftMask. Files are named <pdf name>-p<page>-i<n>.<ext> (<n> counts the images of that page in that PDF; -BaseName replaces <pdf name>). Names are unique within one call: when several PDFs would produce the same name (a/report.pdf and b/report.pdf, or any input with -BaseName) the later file gets a numeric suffix, for example report-p1-i1-2.png, and a file written earlier in the same call is never overwritten. A file that already existed before the call is not overwritten unless -Force is given. A failure for one PDF (not found, unreadable, malformed, wrong password, ...) is a non-terminating error for that file: the other PDFs are still processed. Use -ErrorAction Stop to stop at the first one. Inputs are read one at a time. .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 bind by name. .PARAMETER Bytes The PDF as a byte array (files are named 'pdf-p1-i1.png' ...; use -BaseName). .PARAMETER DestinationPath The folder to write into. Created if it does not exist. .PARAMETER BaseName Prefix of the file names. Default: the PDF's file name without extension. .PARAMETER Page 1-based pages to extract from. Default: all. .PARAMETER ExcludeInline Skip inline images (BI/ID/EI) and annotation-appearance images. .PARAMETER Password Password for an encrypted PDF (a SecureString). .PARAMETER AllowReconstruction Try to recover a damaged file (missing or broken cross-reference table) by scanning for objects. .PARAMETER Force Overwrite files that existed before the call. Files written earlier in the same call are never overwritten (see the naming rules above). .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 Export-VellumPdfImage ./brochure.pdf -DestinationPath ./images .OUTPUTS System.IO.FileInfo for each file written, with the image's details as note properties. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Path')] [OutputType([System.IO.FileInfo])] 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, [Parameter(Mandatory)] [string]$DestinationPath, [string]$BaseName, [ValidateRange(1, 1000000)] [int[]]$Page, [switch]$ExcludeInline, [securestring]$Password, [switch]$Force, [switch]$AllowReconstruction, # 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 } $destination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath) $usedNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # names given out by this call } catch { $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Export-VellumPdfImage')) } } process { try { foreach ($source in Get-VellumPdfInput -Path $Path -LiteralPath $LiteralPath -Bytes $Bytes -CommandName 'Export-VellumPdfImage') { try { Read-VellumPdfInput -Source $source $prefix = if ($BaseName) { $BaseName } elseif ($source.Name -eq '<bytes>') { 'pdf' } else { [System.IO.Path]::GetFileNameWithoutExtension($source.Name) } $reader = Open-VellumPdfReader -Limit $limits -Bytes $source.Data -Password $plain -AllowReconstruction:$AllowReconstruction ` -CommandName 'Export-VellumPdfImage' -Source $source.Name try { $options = [VellumPdf.Reader.PdfImageExtractionOptions]::new() if ($ExcludeInline) { $options.IncludeInlineImages = $false $options.IncludeAnnotationAppearances = $false } $result = $reader.ExtractImages($options) foreach ($d in $result.Diagnostics) { Write-Warning "Export-VellumPdfImage: $($d.Message)" } $counter = @{ } foreach ($image in $result.Images) { $pageNumber = $image.PageIndex + 1 if ($Page -and $pageNumber -notin $Page) { continue } $counter[$pageNumber] = 1 + [int]$counter[$pageNumber] [byte[]]$png = $null $data = $null $ext = $null if ($image.TryEncodePngWithAlpha([ref]$png) -or $image.TryEncodePng([ref]$png)) { $data = $png $ext = 'png' } else { switch ($image.Encoding) { 'Jpeg' { $data = $image.Data.ToArray(); $ext = 'jpg' } 'Jpx' { $data = $image.Data.ToArray(); $ext = $image.FileExtension.TrimStart('.') } 'CcittFax' { # A PDF stores bare CCITT data. Wrap it in a single-strip TIFF so that any viewer opens it. $fax = $image.CcittFax $decode = @($image.Decode) $inverted = $decode.Count -ge 2 -and $decode[0] -gt $decode[1] $columns = if ($fax.Columns -gt 0) { $fax.Columns } else { $image.Width } $data = [PSVellumPDF.Interop.ImageContainers]::BuildCcittTiff( $columns, $image.Height, $fax.K, ($fax.BlackIs1 -xor $inverted), $fax.EncodedByteAlign, $fax.EndOfLine, $image.Data.ToArray()) $ext = 'tif' if ($null -eq $data) { Write-Warning ("Export-VellumPdfImage: the CCITT image on page $pageNumber ($columns x $($image.Height), K=$($fax.K), " + "EndOfLine=$($fax.EndOfLine), EncodedByteAlign=$($fax.EncodedByteAlign)) has no TIFF equivalent " + '(Group 3 needs end-of-line codes, Group 4 cannot be byte-aligned, and the size must stay within 100,000 pixels); it was skipped.') } } 'Jbig2' { # A PDF stores JBIG2 segments without a file header and keeps shared ("global") segments in a # separate stream; join them into a standalone sequential file. $data = [PSVellumPDF.Interop.ImageContainers]::BuildJbig2File($image.Data.ToArray(), $image.Jbig2.Globals.ToArray()) $ext = 'jb2' if ($null -eq $data) { Write-Warning "Export-VellumPdfImage: the JBIG2 data of the image on page $pageNumber could not be parsed; it was skipped." } } default { Write-Warning ("Export-VellumPdfImage: the $($image.ColorSpace.Family) image on page $pageNumber ($($image.Width) x $($image.Height), " + "$($image.BitsPerComponent) bits per component) cannot be written as a PNG and has no file format of its own; it was skipped.") } } if ($null -eq $data) { $counter[$pageNumber] = $counter[$pageNumber] - 1; continue } } $stem = '{0}-p{1}-i{2}' -f $prefix, $pageNumber, $counter[$pageNumber] $extension = $ext.TrimStart('.') # Several inputs can share a base name (a/report.pdf, b/report.pdf) or -BaseName: later ones get -2, -3 ... $name = "$stem.$extension" for ($suffix = 2; -not $usedNames.Add($name); $suffix++) { $name = "$stem-$suffix.$extension" } $target = Join-Path $destination $name if ((Test-Path -LiteralPath $target) -and -not $Force) { # WriteError (not Write-Error -ErrorAction Continue) so the caller's -ErrorAction applies. $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("Export-VellumPdfImage: '$target' exists; use -Force to overwrite."), 'PSVellumPDF.FileExists', [System.Management.Automation.ErrorCategory]::ResourceExists, $target)) continue } if ($PSCmdlet.ShouldProcess($target, 'Write image')) { [void][System.IO.Directory]::CreateDirectory($destination) [System.IO.File]::WriteAllBytes($target, $data) $details = [ordered]@{ Page = $pageNumber Encoding = [string]$image.Encoding Width = $image.Width Height = $image.Height BitsPerComponent = $image.BitsPerComponent ColorSpace = [string]$image.ColorSpace.Family ComponentCount = $image.ColorSpace.ComponentCount IsInline = $image.IsInline IsStencilMask = $image.IsStencilMask HasSoftMask = $null -ne $image.SoftMask } $file = Get-Item -LiteralPath $target foreach ($key in $details.Keys) { $file | Add-Member -NotePropertyName $key -NotePropertyValue $details[$key] } $file } } } finally { $reader.Dispose() } } catch { if ($_.Exception -is [System.Management.Automation.ActionPreferenceStopException]) { throw } # -ErrorAction Stop on an image-level error Write-VellumPdfFileError -Cmdlet $PSCmdlet -ErrorRecord (ConvertTo-VellumPdfFileError -ErrorRecord $_ -Target $source.Name -CommandName 'Export-VellumPdfImage') -ErrorActionPreferenceValue $ErrorActionPreference } finally { $source.Data = $null } # one file in memory at a time } } catch { $PSCmdlet.ThrowTerminatingError((Resolve-VellumPdfErrorRecord -ErrorRecord $_ -CommandName 'Export-VellumPdfImage')) } } } |