【詰め合わせ】コマンドラインで PDF 結合
環境:
> $PSVersionTable
Name Value
---- -----
PSVersion 7.1.3
PSEdition Core
GitCommitId 7.1.3
OS Microsoft Windows 10.0.18363
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
pdftk
pdftk *.pdf cat output out.pdf
手軽さはダントツですね。内部的には iText を使用しているとか。
Windows では Scoop で入手できます( Scoop install pdftk
)。
Python
PyPDF
「Python PDF 結合」で検索すると多くヒットするのがこちらの PyPDF 。
有名な PyPDF2 のほかに PyPDF4 も見つかりましたが、どちらも同じ方法で結合できます。
(無印から2と4が派生した経緯は こちら も参照)
import glob
import os
import PyPDF2
pdfs = glob.glob("*.pdf")
merger = PyPDF2.PdfFileMerger()
for p in pdfs:
merger.append(p)
merger.write("out.pdf")
merger.close()
import glob
import os
import PyPDF4
pdfs = glob.glob("*.pdf")
merger = PyPDF4.PdfFileMerger()
for p in pdfs:
merger.append(p)
merger.write("out.pdf")
merger.close()
後に紹介する iText 系に比べて PDF を厳密に扱うらしく、 Word や Excel から変換した PDF だと時々「形式がおかしいです」的なエラーを出して止まってしまいます。
pdfrw
import glob
import os
from pdfrw import PdfReader, PdfWriter
pdfs = glob.glob("*.pdf")
writer = PdfWriter()
for p in pdfs:
writer.addpages(PdfReader(p).pages)
writer.write("out.pdf")
rw と冠していますがどちらかというと read のほうが得意のようで、PDF 文書をイチから生成するには reportlab と組み合わせるような使い方が想定されているそうです。
公式では結合のほかにも豊富な サンプル が紹介されています。
上記の PyPDF 同様に PDF の型式が厳密に正確でないときに警告を出しますが、警告メッセージを表示するだけで動作上は問題なく動くようです。
iText
世間には C# としての記事が多いようです。が、個人的には普段づかいの PowerShell のコマンドレットに組み込んでターミナルから呼び出すのが気軽に使えて便利だと感じているので、ここではその方法で。
以下、 Get-Childitem
したファイルオブジェクト([System.IO.FileInfo]
型)をパイプで渡すフィルタ的な使い方を想定しています。
事前に Where-Object
でフィルタリングしたり Sort-Object
でソートしてから渡せるので自由度は高めです。
iTextSharp
上記の NuGet ページから dll をダウンロードして、スクリプトと同じ階層に作った lib
フォルダ内に itextsharp.dll
を置いて使います。
Add-Type -Path ($PSScriptRoot | Join-Path -ChildPath "lib\itextsharp.dll")
function Invoke-PdfConc {
<#
.EXAMPLE
ls | Invoke-PdfConc -outname hogehoge
#>
param(
[string]$outName = "out"
)
$fullpath = Join-Path -Path $PWD.Path -ChildPath "$($outName).pdf"
if (Test-Path $fullpath) {
"'{0}.pdf' はもう存在しているファイルです!" -f $outName | Write-Error
return
}
$pdfs = @($input | Where-Object Extension -eq ".pdf")
if ($pdfs.Count -le 1) {
return
}
$filestream = New-Object System.IO.FileStream($fullpath, [System.IO.FileMode]::Create)
$document = New-Object iTextSharp.text.Document
$pdfCopy = New-Object iTextSharp.text.pdf.PdfSmartCopy($document, $fileStream)
$document.Open()
"'{0}.pdf' として結合中:" -f $outName | Write-Host -ForegroundColor Cyan
$pdfs | ForEach-Object {
" + {0}" -f $_.Name | Write-Host -ForegroundColor Cyan
$reader = New-Object iTextSharp.text.pdf.PdfReader($_.Fullname)
$pdfCopy.AddDocument($reader)
$reader.Close()
}
$pdfCopy.Close()
$document.Close()
$filestream.Close()
}
. .\merge_itextsharp.ps1
としてドットソースで読み込んでおくとそのセッションでコマンドレットが有効化されます。 $Profile
で読み込んでおくのがオススメです。
iText7
iText7 も NuGet からダウンロードできます。
もともと機能別に複数の dll に分割されているようですが、さらに下記のライブラリも必要だそうです( 参考 )。
ライブラリをダウンロードして解凍すると、中の lib
フォルダ内に net●●
や netstandard●●
がありますが、どちらの中に入っている dll でも特に問題なく動作しました。
一連の dll を lib
フォルダ内に放り込んで function 宣言の前の Add-Type
部分を下記のようにして一通りロードしておきます。他は iTextSharp と同じ内容で動作します。
Get-ChildItem ($PSScriptRoot | Join-Path -ChildPath "lib") -Recurse -Filter "*.dll" | ForEach-Object {
Add-Type -Path $_.FullName > $null
}
PdfSharp
老舗のライブラリ。 NuGet からダウンロードできます(現在の最新は 1.51.5185-beta
の様子)。
こちらも同じく、スクリプトと同じ階層に lib
フォルダを作ってその中に PdfSharp.dll
を配置して読み込みます。
Add-Type -Path ($PSScriptRoot | Join-Path -ChildPath "lib\PdfSharp.dll")
function Invoke-PdfSharpConc {
param(
[string]$outName = "out"
)
$outPath = Join-Path -Path $PWD.Path -ChildPath "$($outName).pdf"
if (Test-Path $outPath) {
"'{0}.pdf' はもう存在しているファイルです!" -f $outName | Write-Error
return
}
$pdfs = @($input | Where-Object Extension -eq ".pdf")
if ($pdfs.Count -le 1) {
return
}
"'{0}.pdf' として結合中:" -f $outName | Write-Host -ForegroundColor Cyan
$outputFile = New-Object PdfSharp.Pdf.PdfDocument
$pdfs | ForEach-Object {
" + {0}" -f $_.Name | Write-Host -ForegroundColor Blue
$pdf = [PdfSharp.Pdf.IO.PdfReader]::Open($_.FullName, [PdfSharp.Pdf.IO.PdfDocumentOpenMode]::Import)
for ($i = 0; $i -lt $pdf.PageCount; $i++) {
$outputFile.AddPage($pdf.Pages[$i]) > $null
}
}
$outputFile.Save($outPath)
}
.Close()
を書かなくて言いぶん iText よりも少し記述が少なくて済みます。
Discussion