Last active
June 24, 2026 06:05
-
-
Save NoNormalCreeper/37c62408b849b9003a1cf6b87bf28026 to your computer and use it in GitHub Desktop.
在 Windows 本地批量将 PPT/PPTX/PPTM 文件转换为 PDF,用于课件等文件的批量归档,传到只接受 PDF 的地方
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 使用方式:`powershell -ExecutionPolicy Bypass -File .\p2pb.ps1 -InputDir .\课件 -OutputDir .\课件`, | |
| # 如需覆盖已有 PDF 可加 `-Overwrite` 参数。 | |
| # 不加 -Flatten 保留原目录结构输出。 | |
| # 加 -Flatten,所有 PDF 都直接放进 OutputDir。 | |
| # 如果不同子目录里有同名 PPT,比如 ch4.pptx,会自动输出为 ch4.pdf、ch4 (1).pdf、ch4 (2).pdf。 | |
| # 其原理是通过 PowerShell 调用 Microsoft PowerPoint 的 COM 自动化接口,静默打开演示文稿, | |
| # 并使用 PowerPoint 自带的 `SaveAs(..., ppSaveAsPDF)` 导出 PDF,因此转换效果基本与手动用 PowerPoint 导出一致。 | |
| param( | |
| [string]$InputDir = ".", | |
| [string]$OutputDir = ".\pdf", | |
| [switch]$Overwrite, | |
| [switch]$Flatten | |
| ) | |
| $ErrorActionPreference = "Continue" | |
| $msoTrue = -1 | |
| $msoFalse = 0 | |
| $ppAlertsNone = 1 | |
| $ppWindowMinimized = 2 | |
| $ppSaveAsPDF = 32 | |
| function Get-PathKey { | |
| param([string]$Path) | |
| return [System.IO.Path]::GetFullPath($Path).ToLowerInvariant() | |
| } | |
| function Get-UniquePdfPath { | |
| param( | |
| [string]$Dir, | |
| [string]$BaseName, | |
| [hashtable]$UsedPaths, | |
| [bool]$AllowOverwriteBase | |
| ) | |
| $candidate = Join-Path $Dir ($BaseName + ".pdf") | |
| $key = Get-PathKey $candidate | |
| # 如果允许覆盖,则第一个同名文件可以直接使用 base.pdf; | |
| # 但同一轮转换里后续重复文件仍然会改名。 | |
| if (-not $UsedPaths.ContainsKey($key)) { | |
| if ($AllowOverwriteBase -or -not (Test-Path -LiteralPath $candidate)) { | |
| $UsedPaths[$key] = $true | |
| return $candidate | |
| } | |
| } | |
| $i = 1 | |
| while ($true) { | |
| $candidate = Join-Path $Dir ("{0} ({1}).pdf" -f $BaseName, $i) | |
| $key = Get-PathKey $candidate | |
| if (-not $UsedPaths.ContainsKey($key) -and -not (Test-Path -LiteralPath $candidate)) { | |
| $UsedPaths[$key] = $true | |
| return $candidate | |
| } | |
| $i++ | |
| } | |
| } | |
| $InputDir = (Resolve-Path -LiteralPath $InputDir).Path | |
| New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null | |
| $OutputDir = (Resolve-Path -LiteralPath $OutputDir).Path | |
| $logPath = Join-Path $OutputDir "convert-log.txt" | |
| "Start: $(Get-Date)" | Out-File -Encoding UTF8 $logPath | |
| $ppt = $null | |
| $usedPdfPaths = @{} | |
| try { | |
| $ppt = New-Object -ComObject PowerPoint.Application | |
| $ppt.DisplayAlerts = $ppAlertsNone | |
| try { | |
| $ppt.WindowState = $ppWindowMinimized | |
| } catch {} | |
| $files = Get-ChildItem -Path $InputDir -Recurse -File | | |
| Where-Object { | |
| $_.Extension.ToLowerInvariant() -in ".pptx", ".pptm", ".ppt" -and | |
| $_.Name -notlike "~$*" | |
| } | |
| foreach ($file in $files) { | |
| if ($Flatten) { | |
| $targetDir = $OutputDir | |
| $pdfPath = Get-UniquePdfPath ` | |
| -Dir $targetDir ` | |
| -BaseName $file.BaseName ` | |
| -UsedPaths $usedPdfPaths ` | |
| -AllowOverwriteBase $Overwrite.IsPresent | |
| } | |
| else { | |
| $relative = $file.FullName.Substring($InputDir.Length).TrimStart('\') | |
| $relativeDir = Split-Path $relative -Parent | |
| $targetDir = if ($relativeDir) { | |
| Join-Path $OutputDir $relativeDir | |
| } else { | |
| $OutputDir | |
| } | |
| New-Item -ItemType Directory -Force -Path $targetDir | Out-Null | |
| $pdfPath = Join-Path $targetDir ($file.BaseName + ".pdf") | |
| if ((Test-Path -LiteralPath $pdfPath) -and -not $Overwrite) { | |
| "[SKIP] $($file.FullName)" | Tee-Object -FilePath $logPath -Append | |
| continue | |
| } | |
| } | |
| New-Item -ItemType Directory -Force -Path $targetDir | Out-Null | |
| $pres = $null | |
| try { | |
| Write-Host "Converting: $($file.FullName)" | |
| Write-Host " -> $pdfPath" | |
| $pres = $ppt.Presentations.Open( | |
| $file.FullName, | |
| $msoTrue, | |
| $msoFalse, | |
| $msoFalse | |
| ) | |
| $pres.SaveAs($pdfPath, $ppSaveAsPDF) | |
| "[OK] $($file.FullName) -> $pdfPath" | | |
| Tee-Object -FilePath $logPath -Append | |
| } | |
| catch { | |
| "[FAIL] $($file.FullName) :: $($_.Exception.Message)" | | |
| Tee-Object -FilePath $logPath -Append | |
| } | |
| finally { | |
| if ($pres -ne $null) { | |
| try { $pres.Close() } catch {} | |
| [System.Runtime.InteropServices.Marshal]::ReleaseComObject($pres) | Out-Null | |
| } | |
| } | |
| } | |
| } | |
| finally { | |
| if ($ppt -ne $null) { | |
| try { $ppt.Quit() } catch {} | |
| [System.Runtime.InteropServices.Marshal]::ReleaseComObject($ppt) | Out-Null | |
| } | |
| [GC]::Collect() | |
| [GC]::WaitForPendingFinalizers() | |
| } | |
| "End: $(Get-Date)" | Out-File -Encoding UTF8 $logPath -Append | |
| Write-Host "Done. Log: $logPath" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment