You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

372 lines
12 KiB

#Requires -Version 5.1
<#
.SYNOPSIS
PDF一括変換用PowerShellスクリプト - 日本語ファイル名対応
.DESCRIPTION
指定フォルダ配下のすべてのPDFファイルを一括変換します。
日本語/マルチバイトファイル名に対応していない変換ツールでも安全に動作するよう、
一時的にASCII名でコピーしてから変換処理を行います。
.PARAMETER RootDir
変換対象のルートフォルダ(必須)
.PARAMETER Converter
実行する変換コマンドのテンプレート(必須)
{input} と {output} プレースホルダを含むこと
例: 'java -jar "C:\tools\pdf-converter.jar" --in "{input}" --out "{output}"'
.PARAMETER OutputRoot
出力のルートフォルダ(省略時は各ファイルの元ディレクトリ)
.PARAMETER Recurse
再帰的にサブフォルダも処理する(既定: 有効)
.PARAMETER DryRun
実行せず計画のみ表示する
.PARAMETER Concurrency
同時並行実行数(既定: 1)
.PARAMETER Log
ログファイルパス(既定: コンソールのみ)
.PARAMETER Force
既存の出力ファイルを上書きする
.EXAMPLE
.\Convert-Pdf.ps1 -RootDir "D:\pdfs" -Converter 'java -jar "C:\tools\pdf-memo-layers.jar" --in "{input}" --out "{output}"'
基本的な使用例
.EXAMPLE
.\Convert-Pdf.ps1 -RootDir "D:\pdfs" -Converter 'java -jar "C:\tools\pdf-memo-layers.jar" --in "{input}" --out "{output}"' -OutputRoot "D:\converted" -Concurrency 2 -Log "D:\convert.log"
出力先指定、並列処理、ログ出力付きの例
.EXAMPLE
.\Convert-Pdf.ps1 -RootDir "D:\pdfs" -Converter 'powershell -NoProfile -Command Copy-Item -LiteralPath "{input}" -Destination "{output}" -Force' -DryRun
ダミー変換(コピー)でのドライラン実行例
.NOTES
Author: PowerShell Engineer
Version: 1.1
PowerShell Version: 5.1+
Note: PowerShell 7.0の並列処理機能は使用できません(順次処理のみ)
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$RootDir,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Converter,
[Parameter(Mandatory = $false)]
[string]$OutputRoot = "",
[Parameter(Mandatory = $false)]
[switch]$Recurse = $true,
[Parameter(Mandatory = $false)]
[switch]$DryRun,
[Parameter(Mandatory = $false)]
[ValidateRange(1, 20)]
[int]$Concurrency = 1,
[Parameter(Mandatory = $false)]
[string]$Log = "",
[Parameter(Mandatory = $false)]
[switch]$Force
)
# UTF-8出力設定
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($true)
# グローバル変数
$script:ProcessedCount = 0
$script:SuccessCount = 0
$script:FailureCount = 0
$script:SkippedCount = 0
$script:StartTime = Get-Date
# ログ関数
function Write-Log {
param(
[string]$Message,
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "[$timestamp] [$Level] $Message"
# コンソール出力
switch ($Level) {
"ERROR" { Write-Host $logMessage -ForegroundColor Red }
"WARN" { Write-Host $logMessage -ForegroundColor Yellow }
"SUCCESS" { Write-Host $logMessage -ForegroundColor Green }
default { Write-Host $logMessage }
}
# ファイル出力
if ($Log) {
try {
# ログファイルの親ディレクトリを確認・作成
$logDir = Split-Path $Log -Parent
if ($logDir -and -not (Test-Path $logDir)) {
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
$logMessage | Add-Content -Path $Log -Encoding UTF8 -ErrorAction Stop
}
catch {
Write-Warning "ログファイルへの書き込みに失敗しました: $_"
}
}
}
# ASCII一時名生成関数
function Get-AsciiTempName {
param(
[string]$OriginalPath,
[string]$TempDir
)
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($OriginalPath)
$extension = [System.IO.Path]::GetExtension($OriginalPath)
# Unicode正規化(NFKD)
$normalized = $baseName.Normalize([System.Text.NormalizationForm]::FormKD)
# 非ASCII文字を除去し、空白をアンダースコアに変換、括弧も除去
$ascii = $normalized -replace '[^\x00-\x7F]', '' -replace '\s+', '_' -replace '_+', '_' -replace '[()]', ''
# 空の場合はデフォルト名を使用
if ([string]::IsNullOrWhiteSpace($ascii)) {
$ascii = "converted_file"
}
# 先頭末尾のアンダースコアを削除
$ascii = $ascii.Trim('_')
# 一意性確保(temp_プレフィックス付き)
$counter = 0
$tempName = "temp_$ascii$extension"
$tempPath = Join-Path $TempDir $tempName
while (Test-Path $tempPath) {
$counter++
$tempName = "temp_$ascii-$counter$extension"
$tempPath = Join-Path $TempDir $tempName
}
return $tempPath
}
# 出力ファイル名生成関数
function Get-OutputFileName {
param(
[string]$OriginalPath,
[string]$OutputDir
)
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($OriginalPath)
$extension = ".pdf"
# 基本的な出力名
$outputName = "$baseName.converted$extension"
$outputPath = Join-Path $OutputDir $outputName
# 既存ファイルとの重複回避
$counter = 1
while ((Test-Path $outputPath) -and -not $Force) {
$outputName = "$baseName.converted($counter)$extension"
$outputPath = Join-Path $OutputDir $outputName
$counter++
}
return $outputPath
}
# 単一ファイル処理関数
function Process-PdfFile {
param(
[string]$InputPath,
[string]$OutputDir,
[string]$ConverterCommand,
[string]$RootDir
)
$fileName = [System.IO.Path]::GetFileName($InputPath)
$tempDir = $RootDir # RootDirを一時ディレクトリとして使用
$tempFile = $null
try {
# 出力ファイルパス決定
$outputPath = Get-OutputFileName -OriginalPath $InputPath -OutputDir $OutputDir
# 既存ファイルのスキップ判定
if ((Test-Path $outputPath) -and -not $Force) {
Write-Log "スキップ: $fileName (出力ファイルが既に存在)" "WARN"
$script:SkippedCount++
return $true
}
# 出力ディレクトリ作成
$outputDirPath = Split-Path $outputPath -Parent
if (-not (Test-Path $outputDirPath)) {
New-Item -Path $outputDirPath -ItemType Directory -Force | Out-Null
}
# ASCII一時名でコピー
$tempFile = Get-AsciiTempName -OriginalPath $InputPath -TempDir $tempDir
Copy-Item -LiteralPath $InputPath -Destination $tempFile -Force
# 一時ファイルの存在確認
if (-not (Test-Path $tempFile)) {
throw "一時ファイルの作成に失敗しました: $tempFile"
}
# コマンド実行(絶対パスに変換)
$absoluteTempFile = (Resolve-Path $tempFile).Path
$command = $ConverterCommand -replace '\{input\}', "`"$absoluteTempFile`"" -replace '\{output\}', "`"$outputPath`""
if ($DryRun) {
Write-Log "DRY-RUN: $fileName"
Write-Log " 入力: $InputPath"
Write-Log " 一時: $tempFile"
Write-Log " 出力: $outputPath"
Write-Log " コマンド: $command"
$script:ProcessedCount++
return $true
}
Write-Log "処理中: $fileName"
Write-Log " コマンド: $command"
# コマンド実行(Start-Processに戻す)
Write-Log "実行コマンド: $command"
$process = Start-Process -FilePath "cmd" -ArgumentList @("/c", $command) -Wait -PassThru -WindowStyle Hidden -WorkingDirectory (Get-Location).Path
if ($process.ExitCode -eq 0) {
if (Test-Path $outputPath) {
Write-Log "成功: $fileName -> $(Split-Path $outputPath -Leaf)" "SUCCESS"
$script:SuccessCount++
} else {
throw "出力ファイルが生成されませんでした"
}
} else {
throw "コマンドが終了コード $($process.ExitCode) で失敗しました"
}
$script:ProcessedCount++
return $true
}
catch {
Write-Log "エラー: $fileName - $($_.Exception.Message)" "ERROR"
$script:FailureCount++
$script:ProcessedCount++
return $false
}
finally {
# 一時ファイルクリーンアップ
if ($tempFile -and (Test-Path $tempFile)) {
try {
Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue
}
catch {
Write-Log "一時ファイル削除失敗: $tempFile - $($_.Exception.Message)" "WARN"
}
}
}
}
# メイン処理
function Main {
Write-Log "=== PDF一括変換開始 ==="
Write-Log "ルートディレクトリ: $RootDir"
Write-Log "変換コマンド: $Converter"
Write-Log "出力ルート: $(if ($OutputRoot) { $OutputRoot } else { '(各ファイルの元ディレクトリ)' })"
Write-Log "並行数: $Concurrency"
Write-Log "DryRun: $DryRun"
# 入力検証
if (-not (Test-Path $RootDir)) {
Write-Log "エラー: ルートディレクトリが存在しません: $RootDir" "ERROR"
return 1
}
if ($Converter -notmatch '\{input\}' -or $Converter -notmatch '\{output\}') {
Write-Log "エラー: 変換コマンドに{input}と{output}プレースホルダが必要です" "ERROR"
return 1
}
# PDFファイル検索
$searchParams = @{
Path = $RootDir
Filter = "*.pdf"
File = $true
}
if ($Recurse) {
$searchParams.Recurse = $true
}
$pdfFiles = Get-ChildItem @searchParams | Where-Object { $_.Extension -match '\.pdf$' }
if (-not $pdfFiles) {
Write-Log "処理対象のPDFファイルが見つかりません" "WARN"
return 0
}
Write-Log "対象ファイル数: $($pdfFiles.Count)"
# PowerShell 5.1では並列処理をサポートしないため、常に順次処理
if ($Concurrency -gt 1 -and $pdfFiles.Count -gt 1 -and -not $DryRun) {
Write-Log "PowerShell 5.1では並列処理をサポートしていません。順次処理で実行します。"
}
# 順次処理
foreach ($file in $pdfFiles) {
$outputDir = if ($OutputRoot) {
$OutputRoot
} else {
$file.DirectoryName
}
Process-PdfFile -InputPath $file.FullName -OutputDir $outputDir -ConverterCommand $Converter -RootDir $RootDir
}
# 結果サマリー
$endTime = Get-Date
$duration = $endTime - $script:StartTime
Write-Log "=== 処理完了 ==="
Write-Log "処理時間: $($duration.ToString('hh\:mm\:ss'))"
Write-Log "処理済み: $script:ProcessedCount"
Write-Log "成功: $script:SuccessCount"
Write-Log "失敗: $script:FailureCount"
Write-Log "スキップ: $script:SkippedCount"
if ($script:FailureCount -gt 0) {
Write-Log "一部のファイルで処理に失敗しました" "WARN"
return 1
}
return 0
}
# スクリプト実行
try {
$exitCode = Main
exit $exitCode
}
catch {
Write-Log "予期しないエラーが発生しました: $($_.Exception.Message)" "ERROR"
exit 1
}