背景
講師をやっていて、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 いるかも・・
- 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"
}
仕組み
処理の流れはこんな感じ:
- PowerPoint COM APIで PPTX → PDF 変換
- iTextSharpで 4-in-1 レイアウト変換(有効時)
- 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...
あとがき
PowerShell の COM 連携と iTextSharp を組み合わせることで、Office 環境さえあれば追加ソフト不要で実現できた。
特に 4-in-1 レイアウト機能は、印刷コスト削減だけじゃなくてレビュー時の一覧性向上にも役立つはず
ライセンス面では、iTextSharp の AGPL が気になるポイント。個人や社内で使う分には問題ないけど、ツールを配布する場合は注意が必要。
ってことで、次回以降講師準備が少しは楽になるはず
ソースコード全文
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