powershell から pdftk を呼び出して便利に使う

印刷所からのゲラの確認や書き込みをしたゲラをスキャンして著者に送るなど、書籍編集の仕事をしていると本当に多くの PDF ファイルを扱います。
書籍全体の横断検索用として各章ごとに分割されたゲラを全章結合したり、 スキャン時に誤って上下逆になってしまったファイルを回転させたりといった処理に [pdftk](https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/) を使用しているのですが、powershell のパイプライン処理と組み合わせてもっと柔軟に使えないものかと各種コマンドレットを作ってみました。

# 事前準備

pdftk は [scoop](https://scoop.sh) で入手できます。非エンジニアで Windows しか触れたことない身としては神ツールだと思って感謝の気持ちで使用しています。

```
PS > scoop install pdftk
```

インストールが済んだら下記の各コマンドレットを `$profile` に書き込んでおくかドットソースで読み込んでおきます。なお、基本的に複数のファイルを一括処理するためのコマンドレットなので、パイプライン末尾でのフィルタ的な使い方のみ想定しています。

# 複数の PDF を結合する
`Get-ChildItem` で取得した作業ディレクトリ内の pdf ファイルを以下のコマンドレットにパイプすることで単一の pdf が生成されます。

## コード
```powershell
function Invoke-PdftkConc {
    <#
        .SYNOPSIS
        パイプライン経由で渡されたpdfファイルを結合する
        .DESCRIPTION
        ・pdftk.exe を使用
        ・pdfファイル以外は自動で無視する
        .PARAMETER outName
        出力するファイル名（拡張子不要）
        .EXAMPLE
        ls | Invoke-PdftkConc
    #>
    param(
        [string]$outName = "concatenated_output"
    )

    $fullpath = Join-Path -Path $PWD.Path -ChildPath ("{0}.pdf" -f $outName)
    if (Test-Path $fullpath) {
        Write-Host ("'{0}.pdf' already exists!" -f $outName) -ForegroundColor Magenta
        return
    }

    $procTarget = @()
    $targetPath = @()
    $input | Where-Object Extension -eq ".pdf" | ForEach-Object {
        $procTarget += $_
        $targetPath += "'{0}'" -f $_.Fullname
    }

    Write-Host "concatenating as " -NoNewline
    Write-Host ("{0}.pdf" -f $outName) -ForegroundColor Green -NoNewline
    Write-Host ":"
    $procTarget | ForEach-Object {
        Write-Host "  + " -NoNewline
        Write-Host $_.Name -ForegroundColor Cyan
    }
    try {
        Invoke-Expression -Command ("pdftk {0} cat output '{1}.pdf'" -f ($targetPath -join " "), $outName)
    }
    catch {
        Write-Host "ERROR: failed to concatanate pdfs!" -ForegroundColor Red
    }
}

# Alias
Set-Alias pdfConc Invoke-PdftkConc
```

どのファイルをどのように結合するかなど、処理内容を色付きで表示しようとして冗長な書き方になってしまいました。単純に結合処理のみ行う場合はもっとシンプルに簡略化できるはず。

## 使い方

```
PS > ls -file | pdfConc -outName "全体結合"

concatenating as 全体結合.pdf:
  + 01.pdf
  + 02.pdf
  + 03.pdf

# 逆順に結合
PS > ls -file | sort -descending | pdfConc -outName "全体結合_逆順"

concatenating as 全体結合_逆順.pdf:
  + 03.pdf
  + 02.pdf
  + 01.pdf

```

# 複数の PDF を回転させる
`Get-ChildItem` で取得した作業ディレクトリ内の pdf ファイルを以下のコマンドレットにパイプすることで、各ファイルを指定の向きに回転させて、それぞれ先頭に「rotated_」をつけて新規ファイルに書き出します。

## コード
```powershell
function Invoke-PdftkRotate {
    <#
        .SYNOPSIS
        pdftk.exe を使用してパイプライン経由で渡された pdf を回転させる
        .DESCRIPTION
        ・pdfファイル以外は自動で無視
        ・処理したファイルは先頭に "rotated_" を付けて保存
        .PARAMETER clockwise
        回転角度
        .EXAMPLE
        ls| Invoke-PdftkRotate
    #>
    param(
        [ValidateSet(90, 180, 270)]$clockwise = 180
    )
    $direction = switch -exact ($clockwise) {
        90 {"right"}
        270 {"left"}
        default {"down"}
    }

    $input | Where-Object Extension -eq ".pdf" | ForEach-Object {
        try {
            Invoke-Expression -Command ("pdftk '{0}' cat 1-end{1} output 'rotated_{2}'" -f $_.Fullname, $direction, $_.Name)
            Write-Host ("rotating '{0}' clockwise {1} degrees..." -f $_.Name, $clockwise)
        }
        catch {
            Write-Host ("ERROR: failed to rotate '{0}'!" -f $_.Name) -ForegroundColor Red
        }
    }
}

# Alias
Set-Alias pdfRotate Invoke-PdftkRotate
```

# PDF の特定ページを抽出する
ページの抽出に関してはパイプライン入力は想定していません（複数のファイルを一括処理するような場面はめったにないため）。

## コード
```powershell
function Invoke-PdftkExtract {
    <#
        .SYNOPSIS
        pdfファイルの指定範囲を抽出するコマンドレット
        .DESCRIPTION
        pdftk.exe を使用
        .PARAMETER path
        処理対象文書のパス
        .PARAMETER range
        抽出範囲
        未指定の場合はヘルプを表示
        .PARAMETER outName
        出力するファイル名（拡張子不要）
        .PARAMETER force
        同名のファイルがあった場合も上書きして保存する
        .EXAMPLE
        Invoke-PdftkExtract -path ./hoge.pdf -range "1-2" -outName hogehoge
        "hoge.pdf" の1～2ページを "hogehoge.pdf" に出力
        .EXAMPLE
        Invoke-PdftkExtract -path ./hoge.pdf -range "20-end" -outName hogehoge
        "hoge.pdf" の20～最終ページを "hogehoge.pdf" に出力
        .EXAMPLE
        Invoke-PdftkExtract -path ./hoge.pdf -range "2 3 45 69" -outName hogehoge
        "hoge.pdf" の2，3，45，69ページを "hogehoge.pdf" に出力
        .EXAMPLE
        Invoke-PdftkExtract -path ./hoge.pdf -range "2,5,10" -outName hogehoge
        range の区切りはカンマでもよい
    #>
    
    param (
        [string]$path,
        [string]$range,
        [string]$outName,
        [switch]$force
    )

    if (-not $range) {
        Write-Host "sample of parameter 'range':" -ForegroundColor White
        Write-Host " -range '1 4 5'" -ForegroundColor Cyan
        Write-Host "   => extract page 1, 4 and 5."
        Write-Host " -range '8 6 4'" -ForegroundColor Cyan
        Write-Host "   => extract page 8, 6 and 4."
        Write-Host " -range '1-5 9'" -ForegroundColor Cyan
        Write-Host "   => extract page 1 to 5 and 9."
        Write-Host " -range 5-end" -ForegroundColor Cyan
        Write-Host "   => extract page 5 to last page."
        Write-Host " -range 1-endodd" -ForegroundColor Cyan
        Write-Host "   => extract each odd page."
        Write-Host " -range 1-endeven" -ForegroundColor Cyan
        Write-Host "   => extract each even page."
        return
    }

    $fullpath = Join-Path -Path $PWD.Path -ChildPath "$outName.pdf"
    if (Test-Path $fullpath) {
        if (-not $force) {
            Write-Host ("'{0}.pdf' already exists!" -f $outName) -ForegroundColor Magenta
            return
        }
    }

    try {
        Write-Host "extracting " -NoNewline
        Write-Host ("{0}" -f $range) -NoNewline -ForegroundColor Cyan
        Write-Host " as " -NoNewline
        Write-Host ( "{0}.pdf" -f $outName) -ForegroundColor Green -NoNewline
        Write-Host "..."
        Invoke-Expression -Command ("pdftk '{0}' cat {1} output '{2}.pdf'" -f $path, $range, $outName)
    }
    catch {
        Write-Host ("ERROR: failed to extract page from '{0}'!" -f $path) -ForegroundColor Red
    }
}

# Alias
Set-Alias pdfExtract Invoke-PdftkExtract
```

本家の `range` の指定書式を毎回忘れてしまうので、パラメータを指定しなかった場合にヘルプを表示するようにしています。
複数のファイルから一括で特定のページを抽出するような場合は以下のようにします。

```
PS > ls | ? Extension -eq ".pdf" | % {pdfExtract -path $_.Fullname -range 2 -outName ("extracted_" + $_.Name)}
```