0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

講師準備を楽に。PowerPointを4-in-1パスワード付きPDFに一発変換するPowerShellスクリプト

Posted at

背景

講師をやっていて、PowerPoint の資料を配布するとき、以下要望があった

  • パスワード付き PDF で配布したい
  • 印刷コスト削減のため 4 ページを 1 枚にまとめたい
  • でも Acrobat は入ってない

PowerPoint から直接パスワード付き PDF は出せないし、4-in-1 レイアウトも PowerPoint じゃ対応してない。市販ツール使えばできるけど、有償版となると使える人が限られる。

当初 python で作ってみたけど、動かす環境の問題が出たので、Power Shell にしてみた。

ってことで、勝手に必要な dll DL しながら完結するツールを用意した記録。

概要

  • インストール不要: PowerShell スクリプト 1 つで動作
  • 自動セットアップ: 必要な DLL は初回実行時に自動ダウンロード
  • 4-in-1 レイアウト: 4 ページを 1 ページにまとめて用紙節約(デフォルト有効)
  • AES 256bit 暗号化: パスワード保護
  • バッチ処理対応: フォルダ内一括変換も OK
  • ポータブル: フォルダごとコピーすれば他 PC でも使えるはず?

詳細

必要な環境

  • Windows 10/11
  • PowerShell 5.1 以上(7 いるかも・・ :sweat_smile:
  • Microsoft Office (M365) インストール済み。業務なんである全逓。

使い方

基本的な使い方

.\Convert-PptToPasswordPdf.ps1 -InputPath "presentation.pptx" -Password "secret123"

これだけ。初回実行時に iTextSharp DLL が自動ダウンロードされる。

出力先を指定

.\Convert-PptToPasswordPdf.ps1 `
    -InputPath "C:\Docs\slide.pptx" `
    -OutputPath "C:\Output\slide_protected.pdf" `
    -Password "mypass"

4-in-1 を無効化(1 ページ 1 スライド)

.\Convert-PptToPasswordPdf.ps1 `
    -InputPath "slide.pptx" `
    -Password "pass" `
    -FourInOne $false

4-in-1 レイアウト機能

デフォルトで有効。4 ページを 1 ページにまとめてくれる。

ページ配置順序

Z字順(デフォルト):     N字順:
┌────┬────┐          ┌────┬────┐
│ 1  │ 2  │          │ 1  │ 3  │
├────┼────┤          ├────┼────┤
│ 3  │ 4  │          │ 2  │ 4  │
└────┴────┘          └────┴────┘
-PageOrder "Z"       -PageOrder "N"

メリット

  • 用紙節約: 印刷時に 75%の用紙を削減
  • ファイルサイズ削減: PDF サイズが小さくなる
  • 一覧性向上: レビュー時に見やすい

バッチ処理(複数ファイル一括変換)

# フォルダ内の全PPTXファイルを一括変換
Get-ChildItem "C:\Presentations" -Filter *.pptx | ForEach-Object {
    .\Convert-PptToPasswordPdf.ps1 -InputPath $_.FullName -Password "commonpass"
}

仕組み

処理の流れはこんな感じ:

  1. PowerPoint COM APIで PPTX → PDF 変換
  2. iTextSharpで 4-in-1 レイアウト変換(有効時)
  3. iTextSharpで AES 256bit 暗号化&パスワード設定

iTextSharp DLL は初回実行時に NuGet から自動ダウンロードされる。プロキシ環境でも動作するように作ってある。

PDF 設定

項目 設定
暗号化方式 AES 256bit
印刷 許可
コピー 許可
編集 不可
注釈追加 不可

トラブルシューティング

"スクリプトの実行が無効"

# 現在のユーザーのみ許可
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

"PowerPoint の COM オブジェクトを作成できません"

  • Office (M365) がインストールされているか確認
  • PowerPoint を一度手動で起動して初期設定を完了

変換が遅い

PowerPoint のファイルサイズや画像数に応じて時間がかかる。
目安: 10 スライド ≈ 5-10 秒

セキュリティ上の注意

パスワードはコマンド履歴に残るので、機密性が高い場合は履歴削除を推奨。

# PowerShell履歴削除
Clear-History
Remove-Item (Get-PSReadlineOption).HistorySavePath

使用ライブラリとライセンス

このツールは初回実行時に以下の DLL を自動ダウンロードする。

ライブラリ バージョン ライセンス 用途
iTextSharp 5.5.13.3 AGPL v3 PDF 操作・暗号化
BouncyCastle 1.8.9 MIT 相当 暗号化ライブラリ(iTextSharp の依存)

iTextSharp(AGPL v3)について

AGPL ライセンスの注意点

iTextSharp は AGPL v3 ライセンス。以下の点に注意が必要:

  • 個人利用・社内利用(配布なし): ✅ 問題なし
  • ツールを社外に配布: ⚠️ ソースコード公開義務が発生
  • SaaS として提供: ⚠️ ソースコード公開義務が発生

社外配布や SaaS 利用の場合は、iText 商用ライセンスの購入を検討するか、別の PDF ライブラリを使用すること。

BouncyCastle(MIT 相当)について

BouncyCastle は MIT ライセンス相当の寛容なライセンス。商用利用も含めて自由に使える。

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction...

Bouncy Castle License

あとがき

PowerShell の COM 連携と iTextSharp を組み合わせることで、Office 環境さえあれば追加ソフト不要で実現できた。

特に 4-in-1 レイアウト機能は、印刷コスト削減だけじゃなくてレビュー時の一覧性向上にも役立つはず

ライセンス面では、iTextSharp の AGPL が気になるポイント。個人や社内で使う分には問題ないけど、ツールを配布する場合は注意が必要。

ってことで、次回以降講師準備が少しは楽になるはず

ソースコード全文

Convert-PptToPasswordPdf.ps1(クリックで展開)
Convert-PptToPasswordPdf.ps1
<#
.SYNOPSIS
    PowerPointファイルをパスワード付きPDFに変換するツール

.DESCRIPTION
    Microsoft Office (M365) のCOM APIを使用してPowerPointをPDFに変換し、
    iTextSharp DLLを使用してパスワード保護を適用します。
    インストール不要で動作するポータブルツールです。

.PARAMETER InputPath
    変換元のPowerPointファイルパス (.pptx, .ppt)

.PARAMETER OutputPath
    出力先のPDFファイルパス (省略時は入力ファイル名_protected.pdf)

.PARAMETER Password
    PDFに設定するパスワード

.PARAMETER OwnerPassword
    PDF所有者パスワード (省略時はユーザーパスワードと同じ)

.PARAMETER FourInOne
    4ページを1ページにまとめるか (デフォルト: $true)

.PARAMETER PageOrder
    4-in-1時のページ配置順序
    - "Z": Z字順 (左上→右上→左下→右下) デフォルト
    - "N": N字順 (左上→左下→右上→右下)

.EXAMPLE
    .\Convert-PptToPasswordPdf.ps1 -InputPath "presentation.pptx" -Password "secret123"

.NOTES
    使用ライブラリとライセンス:
    - iTextSharp 5.5.13.3 (AGPL v3) - https://www.nuget.org/packages/iTextSharp/
    - BouncyCastle 1.8.9 (MIT相当) - https://www.nuget.org/packages/BouncyCastle/
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidateScript({ Test-Path $_ -PathType Leaf })]
    [string]$InputPath,

    [Parameter(Mandatory = $false, Position = 1)]
    [string]$OutputPath,

    [Parameter(Mandatory = $true)]
    [string]$Password,

    [Parameter(Mandatory = $false)]
    [string]$OwnerPassword,

    [Parameter(Mandatory = $false)]
    [bool]$FourInOne = $true,

    [Parameter(Mandatory = $false)]
    [ValidateSet("Z", "N", "Horizontal", "Vertical")]
    [string]$PageOrder = "Z"
)

$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path

#region ヘルパー関数

# NuGetパッケージからDLLをダウンロードする共通関数
function Get-NuGetDll {
    param(
        [string]$PackageName,
        [string]$PackageVersion,
        [string]$DllName,
        [string]$DestinationPath
    )

    $packageUrl = "https://www.nuget.org/api/v2/package/$PackageName/$PackageVersion"
    $tempDir = Join-Path $env:TEMP "${PackageName}_download_$(Get-Random)"

    try {
        New-Item -ItemType Directory -Path $tempDir -Force | Out-Null

        # プロキシ対応ダウンロード
        $webClient = New-Object System.Net.WebClient
        $webClient.Proxy = [System.Net.WebRequest]::GetSystemWebProxy()
        $webClient.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials

        $packagePath = Join-Path $tempDir "$PackageName.nupkg"
        $webClient.DownloadFile($packageUrl, $packagePath)

        # 展開してDLL検索
        $extractPath = Join-Path $tempDir "extracted"
        Expand-Archive -Path $packagePath -DestinationPath $extractPath -Force

        $foundDll = Get-ChildItem -Path $extractPath -Filter $DllName -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($foundDll) {
            Copy-Item $foundDll.FullName $DestinationPath -Force
            Unblock-File $DestinationPath -ErrorAction SilentlyContinue
            return $true
        }
        return $false
    }
    finally {
        Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
    }
}

# 必要なDLLを確認・ダウンロード
function Initialize-RequiredDlls {
    $dlls = @(
        @{ Name = "BouncyCastle"; Version = "1.8.9"; Dll = "BouncyCastle.Crypto.dll" },
        @{ Name = "iTextSharp"; Version = "5.5.13.3"; Dll = "itextsharp.dll" }
    )

    foreach ($pkg in $dlls) {
        $dllPath = Join-Path $scriptDir $pkg.Dll
        if (-not (Test-Path $dllPath)) {
            Write-Host "  $($pkg.Dll) をダウンロード中..." -ForegroundColor Yellow
            $result = Get-NuGetDll -PackageName $pkg.Name -PackageVersion $pkg.Version -DllName $pkg.Dll -DestinationPath $dllPath
            if ($result) {
                Write-Host "  ✓ $($pkg.Dll) ダウンロード完了" -ForegroundColor Green
            } else {
                throw "$($pkg.Dll) のダウンロードに失敗しました"
            }
        }
        Add-Type -Path $dllPath
    }
}

# PowerPointをPDFに変換
function Convert-PowerPointToPdf {
    param([string]$InputFile, [string]$OutputFile)

    $powerpoint = $null
    $presentation = $null

    try {
        $powerpoint = New-Object -ComObject PowerPoint.Application
        $powerpoint.DisplayAlerts = 2  # ppAlertsNone
        $presentation = $powerpoint.Presentations.Open($InputFile, $true, $true, $false)
        $presentation.SaveAs($OutputFile, 32)  # 32 = ppSaveAsPDF
    }
    finally {
        if ($presentation) { $presentation.Close() }
        if ($powerpoint) { $powerpoint.Quit() }
        if ($presentation) { [System.Runtime.Interopservices.Marshal]::ReleaseComObject($presentation) | Out-Null }
        if ($powerpoint) { [System.Runtime.Interopservices.Marshal]::ReleaseComObject($powerpoint) | Out-Null }
        [System.GC]::Collect()
        [System.GC]::WaitForPendingFinalizers()
    }
}

# 4-in-1レイアウト変換
function Convert-To4in1Pdf {
    param(
        [string]$SourcePdfPath,
        [string]$OutputPdfPath,
        [string]$PageOrder = "Z"
    )

    $reader = $document = $outputStream = $writer = $null

    try {
        $reader = New-Object iTextSharp.text.pdf.PdfReader($SourcePdfPath)
        $totalPages = $reader.NumberOfPages
        $pageSize = [iTextSharp.text.PageSize]::A4.Rotate()
        $document = New-Object iTextSharp.text.Document($pageSize)
        $outputStream = [System.IO.File]::Create($OutputPdfPath)
        $writer = [iTextSharp.text.pdf.PdfWriter]::GetInstance($document, $outputStream)

        $document.Open()
        $cb = $writer.DirectContent
        $pageWidth = $pageSize.Width / 2
        $pageHeight = $pageSize.Height / 2

        for ($i = 1; $i -le $totalPages; $i += 4) {
            $document.NewPage()

            # ページ配置順序の定義
            $positions = switch ($PageOrder) {
                "Z" { @(@{X=0;Y=$pageHeight;P=$i}, @{X=$pageWidth;Y=$pageHeight;P=$i+1}, @{X=0;Y=0;P=$i+2}, @{X=$pageWidth;Y=0;P=$i+3}) }
                "N" { @(@{X=0;Y=$pageHeight;P=$i}, @{X=0;Y=0;P=$i+1}, @{X=$pageWidth;Y=$pageHeight;P=$i+2}, @{X=$pageWidth;Y=0;P=$i+3}) }
                default { @(@{X=0;Y=$pageHeight;P=$i}, @{X=$pageWidth;Y=$pageHeight;P=$i+1}, @{X=0;Y=0;P=$i+2}, @{X=$pageWidth;Y=0;P=$i+3}) }
            }

            foreach ($pos in $positions) {
                if ($pos.P -le $totalPages) {
                    $importedPage = $writer.GetImportedPage($reader, $pos.P)
                    $originalPage = $reader.GetPageSize($pos.P)
                    $scale = [Math]::Min($pageWidth / $originalPage.Width, $pageHeight / $originalPage.Height)
                    $offsetX = ($pageWidth - $originalPage.Width * $scale) / 2
                    $offsetY = ($pageHeight - $originalPage.Height * $scale) / 2
                    $cb.AddTemplate($importedPage, $scale, 0, 0, $scale, $pos.X + $offsetX, $pos.Y + $offsetY)
                }
            }
        }
    }
    finally {
        if ($document) { $document.Close(); $document.Dispose() }
        if ($reader) { $reader.Close(); $reader.Dispose() }
        if ($outputStream) { $outputStream.Close(); $outputStream.Dispose() }
        if ($writer) { $writer.Dispose() }
        [System.GC]::Collect()
        [System.GC]::WaitForPendingFinalizers()
    }
}

# PDFにパスワード保護を適用
function Set-PdfPassword {
    param(
        [string]$SourcePdfPath,
        [string]$OutputPdfPath,
        [string]$UserPassword,
        [string]$OwnerPassword
    )

    $reader = $outputStream = $stamper = $null

    try {
        $reader = New-Object iTextSharp.text.pdf.PdfReader($SourcePdfPath)
        $outputStream = [System.IO.File]::Create($OutputPdfPath)
        $stamper = New-Object iTextSharp.text.pdf.PdfStamper($reader, $outputStream)

        $permissions = [iTextSharp.text.pdf.PdfWriter]::ALLOW_PRINTING -bor [iTextSharp.text.pdf.PdfWriter]::ALLOW_COPY
        $stamper.SetEncryption(
            [System.Text.Encoding]::UTF8.GetBytes($UserPassword),
            [System.Text.Encoding]::UTF8.GetBytes($OwnerPassword),
            $permissions,
            [iTextSharp.text.pdf.PdfWriter]::ENCRYPTION_AES_256
        )
    }
    finally {
        if ($stamper) { $stamper.Close(); $stamper.Dispose() }
        if ($reader) { $reader.Close(); $reader.Dispose() }
        if ($outputStream) { $outputStream.Close(); $outputStream.Dispose() }
        [System.GC]::Collect()
        [System.GC]::WaitForPendingFinalizers()
    }
}

#endregion

#region メイン処理

# パラメータ初期化
if (-not $OutputPath) {
    $inputFile = Get-Item $InputPath
    $OutputPath = Join-Path $inputFile.DirectoryName "$([System.IO.Path]::GetFileNameWithoutExtension($inputFile.Name))_protected.pdf"
}
if (-not $OwnerPassword) { $OwnerPassword = $Password }

$InputPath = Resolve-Path $InputPath
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
$tempPdfPath = [System.IO.Path]::GetTempFileName() + ".pdf"
$temp4in1Path = if ($FourInOne) { [System.IO.Path]::GetTempFileName() + ".pdf" } else { $null }

Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "PowerPoint → パスワード付きPDF 変換ツール" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "入力: $InputPath" -ForegroundColor Yellow
Write-Host "出力: $OutputPath" -ForegroundColor Yellow
if ($FourInOne) { Write-Host "4-in-1: 有効 ($PageOrder)" -ForegroundColor Cyan }
Write-Host ""

try {
    $steps = if ($FourInOne) { 4 } else { 3 }

    # Step 1: PowerPoint → PDF
    Write-Host "[1/$steps] PowerPoint → PDF 変換中..." -ForegroundColor Green
    Convert-PowerPointToPdf -InputFile $InputPath -OutputFile $tempPdfPath
    Write-Host "✓ 完了" -ForegroundColor Green

    # Step 2: DLL準備
    Write-Host "[2/$steps] ライブラリ準備中..." -ForegroundColor Green
    Initialize-RequiredDlls
    Write-Host "✓ 完了" -ForegroundColor Green

    # Step 3: 4-in-1変換(オプション)
    $sourcePdf = $tempPdfPath
    if ($FourInOne) {
        Write-Host "[3/$steps] 4-in-1 レイアウト変換中..." -ForegroundColor Green
        Convert-To4in1Pdf -SourcePdfPath $tempPdfPath -OutputPdfPath $temp4in1Path -PageOrder $PageOrder
        $sourcePdf = $temp4in1Path
        Write-Host "✓ 完了" -ForegroundColor Green
    }

    # Step Final: パスワード保護
    $currentStep = if ($FourInOne) { 4 } else { 3 }
    Write-Host "[$currentStep/$steps] パスワード保護適用中..." -ForegroundColor Green
    Set-PdfPassword -SourcePdfPath $sourcePdf -OutputPdfPath $OutputPath -UserPassword $Password -OwnerPassword $OwnerPassword
    Write-Host "✓ 完了" -ForegroundColor Green

    Write-Host ""
    Write-Host "========================================" -ForegroundColor Cyan
    Write-Host "✓ 変換成功!" -ForegroundColor Green
    Write-Host "========================================" -ForegroundColor Cyan
    Write-Host "出力: $OutputPath" -ForegroundColor Yellow
    Write-Host "サイズ: $([math]::Round((Get-Item $OutputPath).Length / 1KB, 2)) KB" -ForegroundColor Gray
}
catch {
    Write-Host ""
    Write-Host "✗ エラー: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host $_.ScriptStackTrace -ForegroundColor Gray
    exit 1
}
finally {
    # 一時ファイル削除
    @($tempPdfPath, $temp4in1Path) | Where-Object { $_ -and (Test-Path $_) } | ForEach-Object { Remove-Item $_ -Force -ErrorAction SilentlyContinue }
}

#endregion

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?