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
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
|
|
}
|
|
|