commit
3d08ba4d28
15 changed files with 2747 additions and 0 deletions
@ -0,0 +1,78 @@ |
|||
# Gradle |
|||
.gradle/ |
|||
build/ |
|||
!gradle/wrapper/gradle-wrapper.jar |
|||
|
|||
# Manual build artifacts (when Gradle is not available) |
|||
lib/ |
|||
!lib/README.md |
|||
|
|||
# Java |
|||
*.class |
|||
*.jar |
|||
!gradle/wrapper/gradle-wrapper.jar |
|||
# Allow our built JAR files in build/libs/ |
|||
!build/libs/pdf-memo-layers.jar |
|||
!build/libs/pdf-probe.jar |
|||
*.war |
|||
*.ear |
|||
*.nar |
|||
hs_err_pid* |
|||
|
|||
# IDE |
|||
.idea/ |
|||
*.iws |
|||
*.iml |
|||
*.ipr |
|||
.vscode/ |
|||
*.swp |
|||
*.swo |
|||
*~ |
|||
|
|||
# OS |
|||
.DS_Store |
|||
Thumbs.db |
|||
|
|||
# Logs |
|||
*.log |
|||
convert.log |
|||
convert-*.log |
|||
analysis-result*.txt |
|||
|
|||
# Sample files (user should provide their own) |
|||
samples/input.pdf |
|||
samples/output.pdf |
|||
samples/*.pdf |
|||
# Keep specific test files that were used during development |
|||
!samples/README.md |
|||
# But exclude any test files we generate |
|||
samples/test-*.pdf |
|||
|
|||
# PDF processing directories |
|||
pdfs/ |
|||
converted/ |
|||
test-output/ |
|||
# Keep README files for documentation |
|||
!pdfs/README.md |
|||
!converted/README.md |
|||
|
|||
# Generated PDFs in root directory |
|||
input.pdf |
|||
output.pdf |
|||
|
|||
# Temporary files |
|||
*.tmp |
|||
*.temp |
|||
temp_* |
|||
|
|||
# Test output files |
|||
test-output*.pdf |
|||
test-temp.pdf |
|||
test-*.pdf |
|||
*-test.pdf |
|||
|
|||
# PowerShell script generated files |
|||
create-*.ps1 |
|||
|
|||
# Build manifest files |
|||
build/MANIFEST*.MF |
|||
@ -0,0 +1,372 @@ |
|||
#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 |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
# Cursor プロジェクト説明(iText7 で PDF に「メモ用レイヤー」を 2 つ追加) |
|||
|
|||
## 目的 |
|||
|
|||
既存の PDF に **OCG(Optional Content Group=レイヤー)を 2 つ** 追加し、後からそのレイヤーにメモ(テキスト/図形/注釈)を書き込めるようにします。 |
|||
Acrobat の「レイヤー」パネルで表示・非表示を切り替えられる **「メモ1」「メモ2」** を作成します。 |
|||
|
|||
## ゴール/受け入れ基準 |
|||
|
|||
* 既存 PDF を入力に取り、出力 PDF のカタログに **OCProperties/OCGs** が作成されていること(2 つの OCG)。 |
|||
* Acrobat / PDF-XChange 等で **レイヤー「メモ1」「メモ2」** が確認でき、ON/OFF 切り替えが可能。 |
|||
* 追加サンプルとして、各レイヤーに 1 行テキストを描画(または四角形)できること。 |
|||
* 既存ページのコンテンツは変更しない(上に重ねるだけ)。 |
|||
|
|||
## 技術ポイント(PDF 構造) |
|||
|
|||
* iText7 では `PdfLayer`(= OCG)を生成し、`PdfCanvas.beginLayer(layer) ... endLayer()` の間で描画したコンテンツはそのレイヤーに属します。 |
|||
* 既存 PDF を開いてレイヤーを「追加」するだけなら、iText が **Catalog/OCProperties** を自動で拡張してくれます。 |
|||
* 初期表示状態(ON/OFF)は `layer.setOn(true/false)` で制御可能。 |
|||
|
|||
## 環境 |
|||
|
|||
* 言語:Java 17(推奨 11+) |
|||
* ビルド:Maven もしくは Gradle |
|||
* ライブラリ:iText7(kernel / layout) |
|||
|
|||
* Maven: |
|||
|
|||
```xml |
|||
<dependency> |
|||
<groupId>com.itextpdf</groupId> |
|||
<artifactId>kernel</artifactId> |
|||
<version>8.0.2</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.itextpdf</groupId> |
|||
<artifactId>layout</artifactId> |
|||
<version>8.0.2</version> |
|||
</dependency> |
|||
``` |
|||
* Gradle: |
|||
|
|||
```gradle |
|||
implementation "com.itextpdf:kernel:8.0.2" |
|||
implementation "com.itextpdf:layout:8.0.2" |
|||
``` |
|||
|
|||
## プロジェクト構成(推奨) |
|||
|
|||
``` |
|||
pdf-memo-layers/ |
|||
├─ README.md |
|||
├─ build.gradle or pom.xml |
|||
├─ src/ |
|||
│ └─ main/java/ |
|||
│ └─ co/jp/techsor/pdf/ |
|||
│ ├─ App.java // CLI エントリ |
|||
│ ├─ PdfMemoLayerService.java // レイヤー作成・描画の中核 |
|||
│ ├─ LayerDrawingOptions.java // 文字/図形の描画オプション |
|||
│ └─ util/ |
|||
│ └─ PdfUtils.java // 共通ユーティリティ |
|||
└─ samples/ |
|||
├─ input.pdf |
|||
└─ output.pdf (生成物) |
|||
``` |
|||
|
|||
## 主要ユースケース |
|||
|
|||
1. **レイヤーだけを追加**(後で編集者が注釈・図形を載せる) |
|||
2. **レイヤーにテキスト/図形を同時に配置**(例:右下に “Memo Layer”) |
|||
3. **初期状態の切替**(メモ1=ON、メモ2=OFF など) |
|||
|
|||
## CLI 仕様(例) |
|||
|
|||
``` |
|||
java -jar pdf-memo-layers.jar \ |
|||
--in samples/input.pdf \ |
|||
--out samples/output.pdf \ |
|||
--layer1 "メモ1" --layer2 "メモ2" \ |
|||
--draw "text:1,100,700,メモ1のサンプル" \ |
|||
--draw "rect:2,50,50,200,60" \ |
|||
--l2-on=false |
|||
``` |
|||
|
|||
* `--draw` は複数指定可。形式: |
|||
|
|||
* `text:<layerIndex(1|2)>,<x>,<y>,<content>` |
|||
* `rect:<layerIndex(1|2)>,<x>,<y>,<w>,<h>` |
|||
* `--l2-on=false` のように初期表示を切替。 |
|||
|
|||
## 実装概略(擬似コード) |
|||
|
|||
```java |
|||
try (PdfDocument pdf = |
|||
new PdfDocument(new PdfReader(inPath), new PdfWriter(outPath))) { |
|||
|
|||
// 1) レイヤー(OCG)作成 |
|||
PdfLayer memo1 = new PdfLayer("メモ1", pdf); |
|||
memo1.setOn(true); // 初期表示 ON |
|||
PdfLayer memo2 = new PdfLayer("メモ2", pdf); |
|||
memo2.setOn(false); // 初期表示 OFF(例) |
|||
|
|||
// 2) 必要に応じて描画(各ページの上に重ねる) |
|||
PdfPage page1 = pdf.getPage(1); |
|||
PdfCanvas canvas1 = new PdfCanvas(page1); |
|||
canvas1.beginLayer(memo1); |
|||
// テキスト描画(iText7 layout を使う場合は Canvas を併用) |
|||
try (Canvas c = new Canvas(canvas1, page1.getPageSize())) { |
|||
c.showTextAligned("メモ1のサンプル", 100, 700, TextAlignment.LEFT); |
|||
} |
|||
canvas1.endLayer(); |
|||
|
|||
PdfPage page2 = pdf.getPage(2); |
|||
PdfCanvas canvas2 = new PdfCanvas(page2); |
|||
canvas2.beginLayer(memo2); |
|||
canvas2.rectangle(50, 50, 200, 60); |
|||
canvas2.stroke(); |
|||
canvas2.endLayer(); |
|||
} |
|||
``` |
|||
|
|||
## テスト手順 |
|||
|
|||
1. `samples/input.pdf` を用意し、CLI で実行して `samples/output.pdf` を生成。 |
|||
2. Acrobat で `output.pdf` を開き、**表示 → ナビゲーションパネル → レイヤー** を表示。 |
|||
3. 「メモ1」「メモ2」の存在と ON/OFF の切り替えで描画が出入りすることを確認。 |
|||
|
|||
## エッジケースと対策 |
|||
|
|||
* **暗号化 PDF**:所有者パスワードが必要。`ReaderProperties`/`WriterProperties` で設定。 |
|||
* **透明度・ブレンド**:必要なら `PdfExtGState` を併用して半透明表現。 |
|||
* **注釈に載せたい**:注釈は OCG 直付けできないため、描画をレイヤーに、注釈は通常オブジェクトとして扱う(見た目同期が必要)。 |
|||
* **長文テキスト**:`layout` の `Paragraph` を使用し、行送りとフォント設定を行う。 |
|||
|
|||
## ライセンス注意 |
|||
|
|||
iText 7 は AGPL もしくは商用ライセンスです。社内/顧客配布要件に合わせてライセンス選定を行ってください。 |
|||
|
|||
@ -0,0 +1,386 @@ |
|||
# PDF メモレイヤー追加ツール |
|||
|
|||
iText7を使用してPDFファイルに2つのメモ用レイヤー(OCG: Optional Content Group)を追加するJavaアプリケーションです。 |
|||
|
|||
## 機能 |
|||
|
|||
- 既存PDFに「メモ1」「メモ2」レイヤーを追加 |
|||
- レイヤーの初期表示状態(ON/OFF)の設定 |
|||
- レイヤーにテキストや図形を描画 |
|||
- Adobe Acrobat等でレイヤーの表示・非表示切り替えが可能 |
|||
|
|||
## 必要な環境 |
|||
|
|||
- Java 17以上 |
|||
- Gradle 7.0以上(オプション) |
|||
- PowerShell 7.0以上(一括変換スクリプト使用時) |
|||
|
|||
## ビルド方法 |
|||
|
|||
### Gradle Wrapper使用時(推奨) |
|||
```bash |
|||
# プロジェクトのクローンまたはダウンロード後 |
|||
./gradlew build |
|||
|
|||
# 実行可能JARファイルの作成 |
|||
./gradlew fatJar |
|||
``` |
|||
|
|||
### 手動ビルド(Gradle未使用) |
|||
```bash |
|||
# 1. 必要なライブラリをダウンロード(lib/README.mdを参照) |
|||
mkdir lib |
|||
# iText7ライブラリなどを lib/ ディレクトリに配置 |
|||
|
|||
# 2. コンパイル |
|||
mkdir build/classes |
|||
javac -cp "lib/*" -d build/classes src/main/java/co/jp/techsor/pdf/*.java src/main/java/co/jp/techsor/pdf/util/*.java |
|||
``` |
|||
|
|||
## 使用方法 |
|||
|
|||
### 基本的な使用方法 |
|||
|
|||
```bash |
|||
# JARファイルを使用(推奨) |
|||
java -jar build/libs/pdf-memo-layers.jar --in samples/input.pdf --out samples/output.pdf |
|||
|
|||
# 直接クラスパスから実行(開発時) |
|||
java -cp "build/classes;lib/*" co.jp.techsor.pdf.App --in samples/input.pdf --out samples/output.pdf |
|||
|
|||
# カスタムレイヤー名で追加 |
|||
java -jar build/libs/pdf-memo-layers.jar \ |
|||
--in samples/input.pdf \ |
|||
--out samples/output.pdf \ |
|||
--layer1 "注釈レイヤー" \ |
|||
--layer2 "校正レイヤー" |
|||
``` |
|||
|
|||
### 描画オプション付きの使用方法 |
|||
|
|||
```bash |
|||
# テキストと図形を描画 |
|||
java -jar build/libs/pdf-memo-layers.jar \ |
|||
--in samples/input.pdf \ |
|||
--out samples/output.pdf \ |
|||
--layer1 "メモ1" \ |
|||
--layer2 "メモ2" \ |
|||
--draw "text:1,100,700,これはメモ1のテキストです" \ |
|||
--draw "rect:2,50,50,200,60" \ |
|||
--l2on false |
|||
``` |
|||
|
|||
### コマンドラインオプション |
|||
|
|||
| オプション | 説明 | 必須 | デフォルト値 | |
|||
|-----------|------|------|------------| |
|||
| `--in`, `--input` | 入力PDFファイルのパス | ✓ | - | |
|||
| `--out`, `--output` | 出力PDFファイルのパス | ✓ | - | |
|||
| `--layer1` | レイヤー1の名前 | | メモ1 | |
|||
| `--layer2` | レイヤー2の名前 | | メモ2 | |
|||
| `--l1on` | レイヤー1の初期表示状態 (true/false) | | true | |
|||
| `--l2on` | レイヤー2の初期表示状態 (true/false) | | true | |
|||
| `--draw` | 描画オプション(複数指定可能) | | なし | |
|||
| `-h`, `--help` | ヘルプの表示 | | - | |
|||
|
|||
### 描画オプションの書式 |
|||
|
|||
#### テキスト描画 |
|||
``` |
|||
text:<レイヤー番号>,<X座標>,<Y座標>,<テキスト内容> |
|||
``` |
|||
|
|||
例:`text:1,100,700,サンプルテキスト` |
|||
|
|||
#### 四角形描画 |
|||
``` |
|||
rect:<レイヤー番号>,<X座標>,<Y座標>,<幅>,<高さ> |
|||
``` |
|||
|
|||
例:`rect:2,50,50,200,60` |
|||
|
|||
## プロジェクト構成 |
|||
|
|||
``` |
|||
pdf-memo-layers/ |
|||
├─ README.md |
|||
├─ build.gradle |
|||
├─ Convert-Pdf.ps1 // 一括変換用PowerShellスクリプト |
|||
├─ lib/ |
|||
│ ├─ README.md // ライブラリダウンロード手順 |
|||
│ └─ *.jar // iText7等のライブラリ(手動配置) |
|||
├─ src/ |
|||
│ └─ main/java/ |
|||
│ └─ co/jp/techsor/pdf/ |
|||
│ ├─ App.java // CLIエントリポイント |
|||
│ ├─ PdfMemoLayerService.java // レイヤー作成・描画の中核 |
|||
│ ├─ LayerDrawingOptions.java // 描画オプション管理 |
|||
│ └─ util/ |
|||
│ └─ PdfUtils.java // 共通ユーティリティ |
|||
├─ samples/ |
|||
│ ├─ input.pdf // サンプル入力ファイル(要準備) |
|||
│ └─ output.pdf // 生成される出力ファイル |
|||
├─ pdfs/ // 一括変換用入力ディレクトリ |
|||
├─ converted/ // 一括変換結果出力ディレクトリ |
|||
└─ convert.log // 変換ログファイル |
|||
``` |
|||
|
|||
## 使用例 |
|||
|
|||
### 1. シンプルなレイヤー追加 |
|||
|
|||
```bash |
|||
java -jar build/libs/pdf-memo-layers.jar \ |
|||
--in samples/input.pdf \ |
|||
--out samples/output.pdf |
|||
``` |
|||
|
|||
### 2. カスタム設定でのレイヤー追加 |
|||
|
|||
```bash |
|||
java -jar build/libs/pdf-memo-layers.jar \ |
|||
--in samples/input.pdf \ |
|||
--out samples/output.pdf \ |
|||
--layer1 "校正用メモ" \ |
|||
--layer2 "レビューコメント" \ |
|||
--l1on true \ |
|||
--l2on false |
|||
``` |
|||
|
|||
### 3. 描画要素付きでのレイヤー追加 |
|||
|
|||
```bash |
|||
java -jar build/libs/pdf-memo-layers.jar \ |
|||
--in samples/input.pdf \ |
|||
--out samples/output.pdf \ |
|||
--draw "text:1,100,750,重要:この部分を確認してください" \ |
|||
--draw "text:1,100,720,作成者:田中太郎" \ |
|||
--draw "rect:2,50,600,300,100" \ |
|||
--draw "text:2,60,650,注意事項エリア" |
|||
``` |
|||
|
|||
## 結果の確認方法 |
|||
|
|||
1. 生成されたPDFをAdobe Acrobat、PDF-XChange Editor等で開く |
|||
2. レイヤーパネル(ナビゲーションパネル)を表示 |
|||
3. 「メモ1」「メモ2」レイヤーが表示されることを確認 |
|||
4. レイヤーのON/OFFで描画内容が表示・非表示されることを確認 |
|||
|
|||
## 注意事項 |
|||
|
|||
### 暗号化PDF |
|||
パスワード保護されたPDFの場合、現在のバージョンでは対応していません。事前にパスワードを解除してから処理してください。 |
|||
|
|||
### 座標系 |
|||
PDFの座標系は左下が原点(0,0)です。テキストや図形の配置時にご注意ください。 |
|||
|
|||
### ライセンス |
|||
このプロジェクトはiText 7を使用しています。iText 7はAGPLまたは商用ライセンスです。 |
|||
商用利用の場合は適切なライセンスを取得してください。 |
|||
|
|||
## トラブルシューティング |
|||
|
|||
### よくあるエラー |
|||
|
|||
1. **ファイルが見つからない** |
|||
- 入力ファイルのパスが正しいか確認してください |
|||
- 相対パスではなく絶対パスを使用してみてください |
|||
|
|||
2. **出力ディレクトリが存在しない** |
|||
- 出力先のディレクトリが存在することを確認してください |
|||
|
|||
3. **日本語ファイル名の問題** |
|||
- Windows環境で日本語ファイル名を使用する場合、文字エンコーディングの問題が発生することがあります |
|||
- **推奨解決策**: `Convert-Pdf.ps1`スクリプトを使用してください(日本語ファイル名に完全対応) |
|||
```powershell |
|||
.\Convert-Pdf.ps1 -RootDir ".\pdfs" -Converter 'java -cp "build\classes;lib\*" co.jp.techsor.pdf.App --in "{input}" --out "{output}"' -OutputRoot ".\converted" |
|||
``` |
|||
- 手動実行の場合:ファイル名を英語に変更するか、samplesディレクトリで実行してください |
|||
```bash |
|||
cd samples |
|||
java -cp "../build/classes;../lib/*" co.jp.techsor.pdf.App --in "input.pdf" --out "output.pdf" |
|||
``` |
|||
|
|||
4. **メモリ不足エラー** |
|||
- 大きなPDFファイルの場合、JVMのヒープサイズを増やしてください: |
|||
```bash |
|||
java -Xmx2g -jar build/libs/pdf-memo-layers.jar ... |
|||
``` |
|||
|
|||
## 開発者向け情報 |
|||
|
|||
### ビルドとテスト |
|||
|
|||
```bash |
|||
# 手動でのビルド(Gradle Wrapperが利用できない場合) |
|||
# 1. 必要なライブラリをダウンロード |
|||
mkdir lib |
|||
# iText7ライブラリなどを lib/ ディレクトリに配置 |
|||
|
|||
# 2. コンパイル |
|||
mkdir build/classes |
|||
javac -cp "lib/*" -d build/classes src/main/java/co/jp/techsor/pdf/*.java src/main/java/co/jp/techsor/pdf/util/*.java |
|||
|
|||
# 3. 実行 |
|||
java -cp "build/classes;lib/*" co.jp.techsor.pdf.App --in samples/input.pdf --out samples/output.pdf |
|||
|
|||
# Gradle Wrapperが利用可能な場合 |
|||
./gradlew build |
|||
./gradlew fatJar |
|||
./gradlew run --args="--in samples/input.pdf --out samples/output.pdf" |
|||
``` |
|||
|
|||
### APIの使用例 |
|||
|
|||
```java |
|||
import co.jp.techsor.pdf.PdfMemoLayerService; |
|||
import co.jp.techsor.pdf.LayerDrawingOptions; |
|||
|
|||
// サービスのインスタンス作成 |
|||
PdfMemoLayerService service = new PdfMemoLayerService(); |
|||
|
|||
// 描画オプションの作成 |
|||
List<LayerDrawingOptions> options = Arrays.asList( |
|||
LayerDrawingOptions.text(1, 100, 700, "サンプルテキスト"), |
|||
LayerDrawingOptions.rectangle(2, 50, 50, 200, 60) |
|||
); |
|||
|
|||
// レイヤーの追加と描画 |
|||
service.addMemoLayers("input.pdf", "output.pdf", |
|||
"メモ1", "メモ2", true, false, options); |
|||
``` |
|||
|
|||
## 一括変換用PowerShellスクリプト |
|||
|
|||
日本語ファイル名を含む複数のPDFファイルを一括で処理するためのPowerShellスクリプト `Convert-Pdf.ps1` が用意されています。 |
|||
|
|||
### 特徴 |
|||
|
|||
- **日本語ファイル名対応**: 変換ツールが日本語/マルチバイト名に対応していなくても安全に処理 |
|||
- **一時ファイル管理**: ASCII名の一時ファイルを作成し、処理後に自動削除 |
|||
- **並列処理**: 複数ファイルを同時に処理して効率化 |
|||
- **堅牢なエラーハンドリング**: 失敗したファイルがあっても処理を継続 |
|||
- **詳細ログ**: 処理状況をコンソールとファイルに記録 |
|||
|
|||
### 基本的な使用方法 |
|||
|
|||
```powershell |
|||
# 基本的な一括変換(クラスパス指定方式) |
|||
.\Convert-Pdf.ps1 ` |
|||
-RootDir ".\pdfs" ` |
|||
-Converter 'java -cp "build\classes;lib\*" co.jp.techsor.pdf.App --in "{input}" --out "{output}"' |
|||
|
|||
# 出力先指定と並列処理 |
|||
.\Convert-Pdf.ps1 ` |
|||
-RootDir ".\pdfs" ` |
|||
-Converter 'java -cp "build\classes;lib\*" co.jp.techsor.pdf.App --in "{input}" --out "{output}"' ` |
|||
-OutputRoot ".\converted" ` |
|||
-Concurrency 2 ` |
|||
-Log "convert.log" |
|||
|
|||
# DryRun(実行計画の確認) |
|||
.\Convert-Pdf.ps1 ` |
|||
-RootDir ".\pdfs" ` |
|||
-Converter 'java -cp "build\classes;lib\*" co.jp.techsor.pdf.App --in "{input}" --out "{output}"' ` |
|||
-DryRun |
|||
``` |
|||
|
|||
### パラメータ |
|||
|
|||
| パラメータ | 説明 | 必須 | デフォルト | |
|||
|-----------|------|------|-----------| |
|||
| `-RootDir` | 変換対象のルートフォルダ | ✓ | - | |
|||
| `-Converter` | 変換コマンドテンプレート | ✓ | - | |
|||
| `-OutputRoot` | 出力先ルートフォルダ | | 元ファイルと同じディレクトリ | |
|||
| `-Recurse` | サブフォルダも再帰処理 | | true | |
|||
| `-Concurrency` | 同時実行数 | | 1 | |
|||
| `-Log` | ログファイルパス | | コンソールのみ | |
|||
| `-DryRun` | 実行せず計画のみ表示 | | false | |
|||
| `-Force` | 既存出力ファイルを上書き | | false | |
|||
|
|||
### 実行例 |
|||
|
|||
```powershell |
|||
# 実際の動作確認済みコマンド |
|||
.\Convert-Pdf.ps1 ` |
|||
-RootDir ".\pdfs" ` |
|||
-Converter 'java -cp "build\classes;lib\*" co.jp.techsor.pdf.App --in "{input}" --out "{output}"' ` |
|||
-OutputRoot ".\converted" ` |
|||
-Concurrency 2 ` |
|||
-Log "convert.log" |
|||
``` |
|||
|
|||
**実行結果例**: |
|||
``` |
|||
[2025-09-12 14:28:53] [INFO] === PDF一括変換開始 === |
|||
[2025-09-12 14:28:53] [INFO] 対象ファイル数: 2 |
|||
[2025-09-12 14:28:54] [SUCCESS] 成功: テラル_ステンレス製立形多段ポンプSVM・SVMN型.pdf |
|||
[2025-09-12 14:28:55] [SUCCESS] 成功: 杉乃井ホテル_電力監視制御装置.pdf |
|||
[2025-09-12 14:28:55] [INFO] 処理済み: 2, 成功: 2, 失敗: 0 |
|||
``` |
|||
|
|||
### 注意事項 |
|||
|
|||
- **JARファイルが利用できない場合**: `java -cp "build\classes;lib\*"`形式を使用してください |
|||
- **ログファイル**: `-Log`パラメータでファイルパスを指定すると詳細ログが保存されます |
|||
- **出力先**: `-OutputRoot`を指定すると、すべてのファイルが指定ディレクトリに直接出力されます(サブフォルダは作成されません) |
|||
|
|||
## PDF解析ツール |
|||
|
|||
PDFファイルの詳細な解析を行うツール `PdfProbeApp` が追加されました。 |
|||
|
|||
### 機能 |
|||
|
|||
1. **メタデータ解析** |
|||
- Producer/Creator情報の取得 |
|||
- XMPメタデータの解析 |
|||
- Adobe Illustratorの検出 |
|||
|
|||
2. **OCG(レイヤー)解析** |
|||
- レイヤー一覧の取得 |
|||
- 初期表示設定(ON/OFF)の確認 |
|||
|
|||
3. **ページ内容解析** |
|||
- レイヤー使用状況の確認 |
|||
- BDC/EMCマーカーの検出 |
|||
|
|||
4. **注釈解析** |
|||
- 注釈のレイヤー紐付け状況 |
|||
- 手書き注釈を含む全注釈の分析 |
|||
|
|||
5. **Illustrator固有痕跡** |
|||
- PieceInfo の検出 |
|||
- AIPrivateData の痕跡確認 |
|||
|
|||
### 使用方法 |
|||
|
|||
```bash |
|||
# 基本的な解析 |
|||
java -jar build/libs/pdf-probe.jar -i input.pdf |
|||
|
|||
# 詳細解析 |
|||
java -jar build/libs/pdf-probe.jar -i input.pdf -v |
|||
|
|||
# 結果をファイルに出力 |
|||
java -jar build/libs/pdf-probe.jar -i input.pdf -v -o result.txt |
|||
|
|||
# JSON形式で出力(今後実装予定) |
|||
java -jar build/libs/pdf-probe.jar -i input.pdf -j -o result.json |
|||
``` |
|||
|
|||
### ビルド方法 |
|||
|
|||
```bash |
|||
# PDF解析ツールのJARファイルを作成 |
|||
./gradlew probeJar |
|||
|
|||
# 両方のツール(メモレイヤー追加+解析)を作成 |
|||
./gradlew allJars |
|||
``` |
|||
|
|||
### テスト実行 |
|||
|
|||
```powershell |
|||
# PDF解析ツールのテスト |
|||
.\test-probe.ps1 |
|||
``` |
|||
@ -0,0 +1,61 @@ |
|||
plugins { |
|||
id 'java' |
|||
id 'application' |
|||
} |
|||
|
|||
group = 'co.jp.techsor.pdf' |
|||
version = '1.0.0' |
|||
sourceCompatibility = '17' |
|||
|
|||
repositories { |
|||
mavenCentral() |
|||
} |
|||
|
|||
dependencies { |
|||
implementation 'com.itextpdf:kernel:8.0.2' |
|||
implementation 'com.itextpdf:layout:8.0.2' |
|||
implementation 'commons-cli:commons-cli:1.5.0' |
|||
|
|||
testImplementation 'junit:junit:4.13.2' |
|||
} |
|||
|
|||
application { |
|||
mainClass = 'co.jp.techsor.pdf.App' |
|||
} |
|||
|
|||
jar { |
|||
manifest { |
|||
attributes( |
|||
'Main-Class': 'co.jp.techsor.pdf.App' |
|||
) |
|||
} |
|||
from { |
|||
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } |
|||
} |
|||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE |
|||
} |
|||
|
|||
task fatJar(type: Jar) { |
|||
archiveBaseName = 'pdf-memo-layers' |
|||
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } |
|||
with jar |
|||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE |
|||
} |
|||
|
|||
// PDF解析ツール用のJAR |
|||
task probeJar(type: Jar) { |
|||
archiveBaseName = 'pdf-probe' |
|||
manifest { |
|||
attributes 'Main-Class': 'co.jp.techsor.pdf.PdfProbeApp' |
|||
} |
|||
from { |
|||
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } |
|||
} |
|||
with jar |
|||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE |
|||
} |
|||
|
|||
// 両方のJARを作成するタスク |
|||
task allJars { |
|||
dependsOn fatJar, probeJar |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
distributionBase=GRADLE_USER_HOME |
|||
distributionPath=wrapper/dists |
|||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip |
|||
networkTimeout=10000 |
|||
zipStoreBase=GRADLE_USER_HOME |
|||
zipStorePath=wrapper/dists |
|||
@ -0,0 +1,89 @@ |
|||
@rem |
|||
@rem Copyright 2015 the original author or authors. |
|||
@rem |
|||
@rem Licensed under the Apache License, Version 2.0 (the "License"); |
|||
@rem you may not use this file except in compliance with the License. |
|||
@rem You may obtain a copy of the License at |
|||
@rem |
|||
@rem https://www.apache.org/licenses/LICENSE-2.0 |
|||
@rem |
|||
@rem Unless required by applicable law or agreed to in writing, software |
|||
@rem distributed under the License is distributed on an "AS IS" BASIS, |
|||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
@rem See the License for the specific language governing permissions and |
|||
@rem limitations under the License. |
|||
@rem |
|||
|
|||
@if "%DEBUG%"=="" @echo off |
|||
@rem ########################################################################## |
|||
@rem |
|||
@rem Gradle startup script for Windows |
|||
@rem |
|||
@rem ########################################################################## |
|||
|
|||
@rem Set local scope for the variables with windows NT shell |
|||
if "%OS%"=="Windows_NT" setlocal |
|||
|
|||
set DIRNAME=%~dp0 |
|||
if "%DIRNAME%"=="" set DIRNAME=. |
|||
set APP_BASE_NAME=%~n0 |
|||
set APP_HOME=%DIRNAME% |
|||
|
|||
@rem Resolve any "." and ".." in APP_HOME to make it shorter. |
|||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi |
|||
|
|||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
|||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" |
|||
|
|||
@rem Find java.exe |
|||
if defined JAVA_HOME goto findJavaFromJavaHome |
|||
|
|||
set JAVA_EXE=java.exe |
|||
%JAVA_EXE% -version >NUL 2>&1 |
|||
if %ERRORLEVEL% equ 0 goto execute |
|||
|
|||
echo. |
|||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
|||
echo. |
|||
echo Please set the JAVA_HOME variable in your environment to match the |
|||
echo location of your Java installation. |
|||
|
|||
goto fail |
|||
|
|||
:findJavaFromJavaHome |
|||
set JAVA_HOME=%JAVA_HOME:"=% |
|||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe |
|||
|
|||
if exist "%JAVA_EXE%" goto execute |
|||
|
|||
echo. |
|||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% |
|||
echo. |
|||
echo Please set the JAVA_HOME variable in your environment to match the |
|||
echo location of your Java installation. |
|||
|
|||
goto fail |
|||
|
|||
:execute |
|||
@rem Setup the command line |
|||
|
|||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar |
|||
|
|||
|
|||
@rem Execute Gradle |
|||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* |
|||
|
|||
:end |
|||
@rem End local scope for the variables with windows NT shell |
|||
if %ERRORLEVEL% equ 0 goto mainEnd |
|||
|
|||
:fail |
|||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of |
|||
rem the _cmd_ return code. |
|||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 |
|||
exit /b 1 |
|||
|
|||
:mainEnd |
|||
if "%OS%"=="Windows_NT" endlocal |
|||
|
|||
:omega |
|||
@ -0,0 +1,49 @@ |
|||
# サンプルファイル |
|||
|
|||
このディレクトリには、PDF メモレイヤー追加ツールのサンプルファイルを配置します。 |
|||
|
|||
## ファイル構成 |
|||
|
|||
- `input.pdf` - 処理対象となる入力PDFファイル(ユーザーが準備) |
|||
- `output.pdf` - ツール実行後に生成される出力PDFファイル |
|||
|
|||
## 使用方法 |
|||
|
|||
1. 処理したいPDFファイルを `input.pdf` として配置してください |
|||
2. プロジェクトルートから以下のコマンドを実行: |
|||
|
|||
```bash |
|||
# 基本的な実行 |
|||
java -jar build/libs/pdf-memo-layers.jar \ |
|||
--in samples/input.pdf \ |
|||
--out samples/output.pdf |
|||
|
|||
# 描画要素付きの実行 |
|||
java -jar build/libs/pdf-memo-layers.jar \ |
|||
--in samples/input.pdf \ |
|||
--out samples/output.pdf \ |
|||
--draw "text:1,100,700,メモ1のサンプルテキスト" \ |
|||
--draw "rect:2,50,50,200,60" |
|||
``` |
|||
|
|||
3. 生成された `output.pdf` をPDFビューアで開いてレイヤーを確認 |
|||
|
|||
## テスト用PDFの準備 |
|||
|
|||
適当なPDFファイルがない場合は、以下の方法で簡単なテスト用PDFを作成できます: |
|||
|
|||
1. **LibreOffice Writer等を使用** |
|||
- 簡単な文書を作成 |
|||
- PDFとしてエクスポート |
|||
|
|||
2. **オンラインPDF生成ツール** |
|||
- 各種オンラインサービスでテキストからPDFを生成 |
|||
|
|||
3. **コマンドラインツール** |
|||
- `pandoc` などのツールでMarkdownからPDF生成 |
|||
|
|||
## 注意事項 |
|||
|
|||
- `input.pdf` ファイルはGit管理対象外です(`.gitignore`に含まれています) |
|||
- 実際のPDFファイルを使用してテストを行ってください |
|||
- 生成された `output.pdf` も適宜削除してクリーンな状態を保ってください |
|||
@ -0,0 +1,175 @@ |
|||
package co.jp.techsor.pdf; |
|||
|
|||
import org.apache.commons.cli.*; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* PDFメモレイヤー追加ツールのCLIエントリポイント |
|||
*/ |
|||
public class App { |
|||
|
|||
public static void main(String[] args) { |
|||
Options options = createOptions(); |
|||
|
|||
try { |
|||
CommandLineParser parser = new DefaultParser(); |
|||
CommandLine cmd = parser.parse(options, args); |
|||
|
|||
// 必須パラメータのチェック
|
|||
if (!cmd.hasOption("in") || !cmd.hasOption("out")) { |
|||
printHelp(options); |
|||
System.exit(1); |
|||
} |
|||
|
|||
String inputPath = cmd.getOptionValue("in"); |
|||
String outputPath = cmd.getOptionValue("out"); |
|||
String layer1Name = cmd.getOptionValue("layer1", "メモ1"); |
|||
String layer2Name = cmd.getOptionValue("layer2", "メモ2"); |
|||
boolean layer1On = Boolean.parseBoolean(cmd.getOptionValue("l1on", "true")); |
|||
boolean layer2On = Boolean.parseBoolean(cmd.getOptionValue("l2on", "true")); |
|||
|
|||
// 描画オプションの解析
|
|||
List<LayerDrawingOptions> drawingOptions = parseDrawingOptions(cmd.getOptionValues("draw")); |
|||
|
|||
// 注釈レイヤー紐付けオプション
|
|||
boolean assignAnnotations = cmd.hasOption("assign-annotations"); |
|||
int annotationLayer = Integer.parseInt(cmd.getOptionValue("annotation-layer", "1")); |
|||
|
|||
System.out.println("PDFメモレイヤー追加ツールを開始します..."); |
|||
System.out.println("入力ファイル: " + inputPath); |
|||
System.out.println("出力ファイル: " + outputPath); |
|||
System.out.println("レイヤー1: " + layer1Name + " (初期状態: " + (layer1On ? "ON" : "OFF") + ")"); |
|||
System.out.println("レイヤー2: " + layer2Name + " (初期状態: " + (layer2On ? "ON" : "OFF") + ")"); |
|||
|
|||
if (assignAnnotations) { |
|||
System.out.println("注釈のレイヤー紐付け: 有効 (デフォルトレイヤー: " + annotationLayer + ")"); |
|||
} |
|||
|
|||
// PDFメモレイヤーサービスを実行
|
|||
PdfMemoLayerService service = new PdfMemoLayerService(); |
|||
service.addMemoLayers(inputPath, outputPath, layer1Name, layer2Name, |
|||
layer1On, layer2On, drawingOptions, assignAnnotations, annotationLayer); |
|||
|
|||
System.out.println("処理が完了しました: " + outputPath); |
|||
|
|||
} catch (ParseException e) { |
|||
System.err.println("コマンドライン引数のパースエラー: " + e.getMessage()); |
|||
printHelp(options); |
|||
System.exit(1); |
|||
} catch (Exception e) { |
|||
System.err.println("処理中にエラーが発生しました: " + e.getMessage()); |
|||
e.printStackTrace(); |
|||
System.exit(1); |
|||
} |
|||
} |
|||
|
|||
private static Options createOptions() { |
|||
Options options = new Options(); |
|||
|
|||
options.addOption(Option.builder("in") |
|||
.longOpt("input") |
|||
.hasArg() |
|||
.required(true) |
|||
.desc("入力PDFファイルのパス") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("out") |
|||
.longOpt("output") |
|||
.hasArg() |
|||
.required(true) |
|||
.desc("出力PDFファイルのパス") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("layer1") |
|||
.hasArg() |
|||
.desc("レイヤー1の名前 (デフォルト: メモ1)") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("layer2") |
|||
.hasArg() |
|||
.desc("レイヤー2の名前 (デフォルト: メモ2)") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("l1on") |
|||
.hasArg() |
|||
.desc("レイヤー1の初期表示状態 (true/false, デフォルト: true)") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("l2on") |
|||
.hasArg() |
|||
.desc("レイヤー2の初期表示状態 (true/false, デフォルト: true)") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("draw") |
|||
.hasArgs() |
|||
.desc("描画オプション (複数指定可能): text:<layer>,<x>,<y>,<content> または rect:<layer>,<x>,<y>,<w>,<h>") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("h") |
|||
.longOpt("help") |
|||
.desc("ヘルプを表示") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder() |
|||
.longOpt("assign-annotations") |
|||
.desc("既存注釈をレイヤーに紐付ける") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder() |
|||
.longOpt("annotation-layer") |
|||
.hasArg() |
|||
.argName("LAYER") |
|||
.desc("注釈を紐付けるデフォルトレイヤー (1 or 2, デフォルト: 1)") |
|||
.build()); |
|||
|
|||
return options; |
|||
} |
|||
|
|||
private static List<LayerDrawingOptions> parseDrawingOptions(String[] drawOptions) { |
|||
List<LayerDrawingOptions> options = new ArrayList<>(); |
|||
|
|||
if (drawOptions == null) { |
|||
return options; |
|||
} |
|||
|
|||
for (String drawOption : drawOptions) { |
|||
try { |
|||
if (drawOption.startsWith("text:")) { |
|||
String[] parts = drawOption.substring(5).split(",", 4); |
|||
if (parts.length >= 4) { |
|||
int layerIndex = Integer.parseInt(parts[0]); |
|||
float x = Float.parseFloat(parts[1]); |
|||
float y = Float.parseFloat(parts[2]); |
|||
String content = parts[3]; |
|||
options.add(LayerDrawingOptions.text(layerIndex, x, y, content)); |
|||
} |
|||
} else if (drawOption.startsWith("rect:")) { |
|||
String[] parts = drawOption.substring(5).split(","); |
|||
if (parts.length >= 5) { |
|||
int layerIndex = Integer.parseInt(parts[0]); |
|||
float x = Float.parseFloat(parts[1]); |
|||
float y = Float.parseFloat(parts[2]); |
|||
float width = Float.parseFloat(parts[3]); |
|||
float height = Float.parseFloat(parts[4]); |
|||
options.add(LayerDrawingOptions.rectangle(layerIndex, x, y, width, height)); |
|||
} |
|||
} |
|||
} catch (NumberFormatException e) { |
|||
System.err.println("描画オプションの解析エラー: " + drawOption); |
|||
} |
|||
} |
|||
|
|||
return options; |
|||
} |
|||
|
|||
private static void printHelp(Options options) { |
|||
HelpFormatter formatter = new HelpFormatter(); |
|||
formatter.printHelp("pdf-memo-layers", "PDFにメモ用レイヤーを追加するツール", options, |
|||
"\n使用例:\n" + |
|||
" java -jar pdf-memo-layers.jar --in input.pdf --out output.pdf\n" + |
|||
" java -jar pdf-memo-layers.jar --in input.pdf --out output.pdf --layer1 \"メモ1\" --layer2 \"メモ2\" --draw \"text:1,100,700,サンプルテキスト\" --draw \"rect:2,50,50,200,60\" --l2-on false\n" + |
|||
" java -jar pdf-memo-layers.jar --in input.pdf --out output.pdf --assign-annotations --annotation-layer 1" |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
package co.jp.techsor.pdf; |
|||
|
|||
/** |
|||
* レイヤー描画オプションを管理するクラス |
|||
*/ |
|||
public class LayerDrawingOptions { |
|||
|
|||
public enum DrawingType { |
|||
TEXT, RECTANGLE |
|||
} |
|||
|
|||
private final DrawingType type; |
|||
private final int layerIndex; |
|||
private final float x; |
|||
private final float y; |
|||
private final float width; |
|||
private final float height; |
|||
private final String textContent; |
|||
private final float fontSize; |
|||
private final int pageNumber; |
|||
|
|||
// プライベートコンストラクタ
|
|||
private LayerDrawingOptions(Builder builder) { |
|||
this.type = builder.type; |
|||
this.layerIndex = builder.layerIndex; |
|||
this.x = builder.x; |
|||
this.y = builder.y; |
|||
this.width = builder.width; |
|||
this.height = builder.height; |
|||
this.textContent = builder.textContent; |
|||
this.fontSize = builder.fontSize; |
|||
this.pageNumber = builder.pageNumber; |
|||
} |
|||
|
|||
// ファクトリーメソッド:テキスト描画用
|
|||
public static LayerDrawingOptions text(int layerIndex, float x, float y, String content) { |
|||
return new Builder(DrawingType.TEXT, layerIndex) |
|||
.position(x, y) |
|||
.textContent(content) |
|||
.fontSize(12.0f) |
|||
.pageNumber(1) |
|||
.build(); |
|||
} |
|||
|
|||
// ファクトリーメソッド:テキスト描画用(フォントサイズ指定)
|
|||
public static LayerDrawingOptions text(int layerIndex, float x, float y, String content, float fontSize) { |
|||
return new Builder(DrawingType.TEXT, layerIndex) |
|||
.position(x, y) |
|||
.textContent(content) |
|||
.fontSize(fontSize) |
|||
.pageNumber(1) |
|||
.build(); |
|||
} |
|||
|
|||
// ファクトリーメソッド:四角形描画用
|
|||
public static LayerDrawingOptions rectangle(int layerIndex, float x, float y, float width, float height) { |
|||
return new Builder(DrawingType.RECTANGLE, layerIndex) |
|||
.position(x, y) |
|||
.size(width, height) |
|||
.pageNumber(1) |
|||
.build(); |
|||
} |
|||
|
|||
// Builderクラス
|
|||
public static class Builder { |
|||
private final DrawingType type; |
|||
private final int layerIndex; |
|||
private float x = 0; |
|||
private float y = 0; |
|||
private float width = 0; |
|||
private float height = 0; |
|||
private String textContent = ""; |
|||
private float fontSize = 12.0f; |
|||
private int pageNumber = 1; |
|||
|
|||
public Builder(DrawingType type, int layerIndex) { |
|||
this.type = type; |
|||
this.layerIndex = layerIndex; |
|||
} |
|||
|
|||
public Builder position(float x, float y) { |
|||
this.x = x; |
|||
this.y = y; |
|||
return this; |
|||
} |
|||
|
|||
public Builder size(float width, float height) { |
|||
this.width = width; |
|||
this.height = height; |
|||
return this; |
|||
} |
|||
|
|||
public Builder textContent(String content) { |
|||
this.textContent = content; |
|||
return this; |
|||
} |
|||
|
|||
public Builder fontSize(float fontSize) { |
|||
this.fontSize = fontSize; |
|||
return this; |
|||
} |
|||
|
|||
public Builder pageNumber(int pageNumber) { |
|||
this.pageNumber = Math.max(1, pageNumber); |
|||
return this; |
|||
} |
|||
|
|||
public LayerDrawingOptions build() { |
|||
return new LayerDrawingOptions(this); |
|||
} |
|||
} |
|||
|
|||
// Getterメソッド
|
|||
public DrawingType getType() { |
|||
return type; |
|||
} |
|||
|
|||
public int getLayerIndex() { |
|||
return layerIndex; |
|||
} |
|||
|
|||
public float getX() { |
|||
return x; |
|||
} |
|||
|
|||
public float getY() { |
|||
return y; |
|||
} |
|||
|
|||
public float getWidth() { |
|||
return width; |
|||
} |
|||
|
|||
public float getHeight() { |
|||
return height; |
|||
} |
|||
|
|||
public String getTextContent() { |
|||
return textContent; |
|||
} |
|||
|
|||
public float getFontSize() { |
|||
return fontSize; |
|||
} |
|||
|
|||
public int getPageNumber() { |
|||
return pageNumber; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "LayerDrawingOptions{" + |
|||
"type=" + type + |
|||
", layerIndex=" + layerIndex + |
|||
", x=" + x + |
|||
", y=" + y + |
|||
", width=" + width + |
|||
", height=" + height + |
|||
", textContent='" + textContent + '\'' + |
|||
", fontSize=" + fontSize + |
|||
", pageNumber=" + pageNumber + |
|||
'}'; |
|||
} |
|||
} |
|||
@ -0,0 +1,270 @@ |
|||
package co.jp.techsor.pdf; |
|||
|
|||
import com.itextpdf.kernel.pdf.*; |
|||
import com.itextpdf.kernel.pdf.layer.PdfLayer; |
|||
import com.itextpdf.kernel.pdf.canvas.PdfCanvas; |
|||
import com.itextpdf.kernel.pdf.annot.PdfAnnotation; |
|||
import com.itextpdf.layout.Canvas; |
|||
import com.itextpdf.layout.element.Paragraph; |
|||
import com.itextpdf.layout.properties.TextAlignment; |
|||
import com.itextpdf.kernel.geom.Rectangle; |
|||
import com.itextpdf.kernel.colors.ColorConstants; |
|||
import co.jp.techsor.pdf.util.PdfUtils; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* PDFにメモ用レイヤーを追加するサービスクラス |
|||
*/ |
|||
public class PdfMemoLayerService { |
|||
|
|||
/** |
|||
* PDFにメモ用レイヤーを2つ追加し、オプションで描画を行う |
|||
* |
|||
* @param inputPath 入力PDFファイルのパス |
|||
* @param outputPath 出力PDFファイルのパス |
|||
* @param layer1Name レイヤー1の名前 |
|||
* @param layer2Name レイヤー2の名前 |
|||
* @param layer1On レイヤー1の初期表示状態 |
|||
* @param layer2On レイヤー2の初期表示状態 |
|||
* @param drawingOptions 描画オプションのリスト |
|||
* @throws IOException ファイルI/Oエラー |
|||
*/ |
|||
public void addMemoLayers(String inputPath, String outputPath, |
|||
String layer1Name, String layer2Name, |
|||
boolean layer1On, boolean layer2On, |
|||
List<LayerDrawingOptions> drawingOptions) throws IOException { |
|||
addMemoLayers(inputPath, outputPath, layer1Name, layer2Name, |
|||
layer1On, layer2On, drawingOptions, false, 1); |
|||
} |
|||
|
|||
/** |
|||
* PDFにメモ用レイヤーを2つ追加し、オプションで描画を行う(注釈のレイヤー紐付け対応) |
|||
* |
|||
* @param inputPath 入力PDFファイルのパス |
|||
* @param outputPath 出力PDFファイルのパス |
|||
* @param layer1Name レイヤー1の名前 |
|||
* @param layer2Name レイヤー2の名前 |
|||
* @param layer1On レイヤー1の初期表示状態 |
|||
* @param layer2On レイヤー2の初期表示状態 |
|||
* @param drawingOptions 描画オプションのリスト |
|||
* @param assignAnnotationsToLayers 既存注釈をレイヤーに紐付けるかどうか |
|||
* @param defaultAnnotationLayer 注釈を紐付けるデフォルトレイヤー(1 or 2) |
|||
* @throws IOException ファイルI/Oエラー |
|||
*/ |
|||
public void addMemoLayers(String inputPath, String outputPath, |
|||
String layer1Name, String layer2Name, |
|||
boolean layer1On, boolean layer2On, |
|||
List<LayerDrawingOptions> drawingOptions, |
|||
boolean assignAnnotationsToLayers, |
|||
int defaultAnnotationLayer) throws IOException { |
|||
|
|||
// 入力ファイルの存在確認
|
|||
if (!PdfUtils.fileExists(inputPath)) { |
|||
throw new IOException("入力ファイルが見つかりません: " + inputPath); |
|||
} |
|||
|
|||
// ReaderPropertiesとWriterPropertiesの設定(暗号化PDF対応)
|
|||
ReaderProperties readerProps = new ReaderProperties(); |
|||
WriterProperties writerProps = new WriterProperties(); |
|||
|
|||
try (PdfDocument pdf = new PdfDocument(new PdfReader(inputPath, readerProps), |
|||
new PdfWriter(outputPath, writerProps))) { |
|||
|
|||
// レイヤー(OCG)の作成
|
|||
PdfLayer memoLayer1 = new PdfLayer(layer1Name, pdf); |
|||
memoLayer1.setOn(layer1On); |
|||
|
|||
PdfLayer memoLayer2 = new PdfLayer(layer2Name, pdf); |
|||
memoLayer2.setOn(layer2On); |
|||
|
|||
System.out.println("レイヤーを作成しました:"); |
|||
System.out.println(" - " + layer1Name + " (初期状態: " + (layer1On ? "ON" : "OFF") + ")"); |
|||
System.out.println(" - " + layer2Name + " (初期状態: " + (layer2On ? "ON" : "OFF") + ")"); |
|||
|
|||
// 既存注釈をレイヤーに紐付け
|
|||
if (assignAnnotationsToLayers) { |
|||
assignAnnotationsToLayers(pdf, memoLayer1, memoLayer2, defaultAnnotationLayer); |
|||
} |
|||
|
|||
// 描画オプションがある場合は描画を実行
|
|||
if (drawingOptions != null && !drawingOptions.isEmpty()) { |
|||
performDrawing(pdf, memoLayer1, memoLayer2, drawingOptions); |
|||
} |
|||
|
|||
System.out.println("PDFの処理が完了しました。ページ数: " + pdf.getNumberOfPages()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 指定された描画オプションに基づいて各レイヤーに描画を行う |
|||
*/ |
|||
private void performDrawing(PdfDocument pdf, PdfLayer layer1, PdfLayer layer2, |
|||
List<LayerDrawingOptions> drawingOptions) { |
|||
|
|||
for (LayerDrawingOptions option : drawingOptions) { |
|||
// 描画対象のページを決定(1ページ目をデフォルトとする)
|
|||
int pageNum = Math.min(option.getPageNumber(), pdf.getNumberOfPages()); |
|||
PdfPage page = pdf.getPage(pageNum); |
|||
PdfCanvas canvas = new PdfCanvas(page); |
|||
|
|||
// レイヤーの選択
|
|||
PdfLayer targetLayer = (option.getLayerIndex() == 1) ? layer1 : layer2; |
|||
|
|||
canvas.beginLayer(targetLayer); |
|||
|
|||
try { |
|||
switch (option.getType()) { |
|||
case TEXT: |
|||
drawText(canvas, page, option); |
|||
break; |
|||
case RECTANGLE: |
|||
drawRectangle(canvas, option); |
|||
break; |
|||
default: |
|||
System.err.println("未対応の描画タイプ: " + option.getType()); |
|||
break; |
|||
} |
|||
|
|||
System.out.println("描画完了: " + option.getType() + " on Layer " + option.getLayerIndex()); |
|||
|
|||
} catch (Exception e) { |
|||
System.err.println("描画エラー: " + e.getMessage()); |
|||
} finally { |
|||
canvas.endLayer(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* テキストを描画する |
|||
*/ |
|||
private void drawText(PdfCanvas canvas, PdfPage page, LayerDrawingOptions option) { |
|||
try (Canvas layoutCanvas = new Canvas(canvas, page.getPageSize())) { |
|||
Paragraph paragraph = new Paragraph(option.getTextContent()) |
|||
.setFontSize(option.getFontSize()) |
|||
.setFontColor(ColorConstants.BLACK); |
|||
|
|||
layoutCanvas.showTextAligned(paragraph, option.getX(), option.getY(), |
|||
TextAlignment.LEFT); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 四角形を描画する |
|||
*/ |
|||
private void drawRectangle(PdfCanvas canvas, LayerDrawingOptions option) { |
|||
canvas.setStrokeColor(ColorConstants.RED) |
|||
.setLineWidth(1.5f) |
|||
.rectangle(option.getX(), option.getY(), option.getWidth(), option.getHeight()) |
|||
.stroke(); |
|||
} |
|||
|
|||
/** |
|||
* 既存の注釈を指定されたレイヤーに紐付ける |
|||
* |
|||
* @param pdf PDFドキュメント |
|||
* @param layer1 レイヤー1 |
|||
* @param layer2 レイヤー2 |
|||
* @param defaultLayer デフォルトで紐付けるレイヤー(1 or 2) |
|||
*/ |
|||
private void assignAnnotationsToLayers(PdfDocument pdf, PdfLayer layer1, PdfLayer layer2, int defaultLayer) { |
|||
int totalAnnotations = 0; |
|||
int assignedAnnotations = 0; |
|||
|
|||
System.out.println("既存注釈のレイヤー紐付けを開始します..."); |
|||
|
|||
// 全ページをスキャンして注釈を処理
|
|||
for (int pageNum = 1; pageNum <= pdf.getNumberOfPages(); pageNum++) { |
|||
PdfPage page = pdf.getPage(pageNum); |
|||
List<PdfAnnotation> annotations = page.getAnnotations(); |
|||
|
|||
if (annotations != null && !annotations.isEmpty()) { |
|||
System.out.println("ページ " + pageNum + ": " + annotations.size() + "個の注釈を発見"); |
|||
|
|||
for (PdfAnnotation annotation : annotations) { |
|||
totalAnnotations++; |
|||
|
|||
try { |
|||
// 既存の/OCがあるかチェック
|
|||
PdfDictionary annotDict = annotation.getPdfObject(); |
|||
PdfDictionary existingOC = annotDict.getAsDictionary(PdfName.OC); |
|||
|
|||
if (existingOC != null) { |
|||
System.out.println(" 注釈 " + getAnnotationType(annotation) + " は既にレイヤーに紐付け済み"); |
|||
continue; |
|||
} |
|||
|
|||
// デフォルトレイヤーまたは注釈タイプに基づいてレイヤーを決定
|
|||
PdfLayer targetLayer = determineTargetLayer(annotation, layer1, layer2, defaultLayer); |
|||
|
|||
// /OCを注釈辞書に追加
|
|||
annotDict.put(PdfName.OC, targetLayer.getPdfObject()); |
|||
annotDict.setModified(); |
|||
|
|||
assignedAnnotations++; |
|||
System.out.println(" 注釈 " + getAnnotationType(annotation) + " を " + |
|||
(targetLayer == layer1 ? layer1.getPdfObject().getAsString(PdfName.Name).getValue() : |
|||
layer2.getPdfObject().getAsString(PdfName.Name).getValue()) + " レイヤーに紐付けしました"); |
|||
|
|||
} catch (Exception e) { |
|||
System.err.println(" 注釈の処理中にエラーが発生しました: " + e.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
System.out.println("注釈のレイヤー紐付け完了:"); |
|||
System.out.println(" 総注釈数: " + totalAnnotations); |
|||
System.out.println(" 紐付け済み: " + assignedAnnotations); |
|||
System.out.println(" スキップ: " + (totalAnnotations - assignedAnnotations)); |
|||
} |
|||
|
|||
/** |
|||
* 注釈の種類を取得する |
|||
*/ |
|||
private String getAnnotationType(PdfAnnotation annotation) { |
|||
PdfName subtype = annotation.getSubtype(); |
|||
return subtype != null ? subtype.getValue() : "Unknown"; |
|||
} |
|||
|
|||
/** |
|||
* 注釈に対する適切なレイヤーを決定する |
|||
* |
|||
* @param annotation 注釈 |
|||
* @param layer1 レイヤー1 |
|||
* @param layer2 レイヤー2 |
|||
* @param defaultLayer デフォルトレイヤー |
|||
* @return 対象レイヤー |
|||
*/ |
|||
private PdfLayer determineTargetLayer(PdfAnnotation annotation, PdfLayer layer1, PdfLayer layer2, int defaultLayer) { |
|||
// 注釈タイプに基づく自動判定ロジック(カスタマイズ可能)
|
|||
PdfName subtype = annotation.getSubtype(); |
|||
|
|||
if (subtype != null) { |
|||
String type = subtype.getValue(); |
|||
|
|||
// 手書き系の注釈はレイヤー1、テキスト系はレイヤー2など
|
|||
switch (type) { |
|||
case "Ink": // 手書き
|
|||
case "FreeText": // フリーテキスト
|
|||
case "Highlight": // ハイライト
|
|||
return layer1; |
|||
|
|||
case "Text": // テキスト注釈
|
|||
case "Popup": // ポップアップ
|
|||
case "Square": // 四角形
|
|||
case "Circle": // 円形
|
|||
return layer2; |
|||
|
|||
default: |
|||
// デフォルトレイヤーを使用
|
|||
return (defaultLayer == 1) ? layer1 : layer2; |
|||
} |
|||
} |
|||
|
|||
// サブタイプが不明な場合はデフォルトレイヤーを使用
|
|||
return (defaultLayer == 1) ? layer1 : layer2; |
|||
} |
|||
} |
|||
@ -0,0 +1,572 @@ |
|||
package co.jp.techsor.pdf; |
|||
|
|||
import com.itextpdf.kernel.pdf.*; |
|||
import com.itextpdf.kernel.pdf.annot.PdfAnnotation; |
|||
import com.itextpdf.kernel.xmp.XMPMetaFactory; |
|||
import com.itextpdf.kernel.xmp.XMPMeta; |
|||
import com.itextpdf.kernel.xmp.XMPException; |
|||
import org.apache.commons.cli.*; |
|||
|
|||
import java.io.File; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.*; |
|||
import java.util.regex.Pattern; |
|||
|
|||
/** |
|||
* PDF解析ツール - レイヤー、メタデータ、Illustrator痕跡の詳細分析 |
|||
*/ |
|||
public class PdfProbeApp { |
|||
|
|||
private static final String VERSION = "1.0"; |
|||
private static final Pattern ILLUSTRATOR_PATTERN = Pattern.compile("(?i)illustrator|adobe\\s+illustrator"); |
|||
|
|||
public static void main(String[] args) { |
|||
Options options = createOptions(); |
|||
CommandLineParser parser = new DefaultParser(); |
|||
|
|||
try { |
|||
CommandLine cmd = parser.parse(options, args); |
|||
|
|||
if (cmd.hasOption("help")) { |
|||
printHelp(options); |
|||
return; |
|||
} |
|||
|
|||
if (cmd.hasOption("version")) { |
|||
System.out.println("PDF Probe Tool v" + VERSION); |
|||
return; |
|||
} |
|||
|
|||
String inputFile = cmd.getOptionValue("input"); |
|||
if (inputFile == null) { |
|||
System.err.println("エラー: 入力ファイルを指定してください"); |
|||
printHelp(options); |
|||
System.exit(1); |
|||
} |
|||
|
|||
boolean verbose = cmd.hasOption("verbose"); |
|||
boolean jsonOutput = cmd.hasOption("json"); |
|||
String outputFile = cmd.getOptionValue("output"); |
|||
|
|||
PdfProbeApp app = new PdfProbeApp(); |
|||
app.analyzePdf(inputFile, verbose, jsonOutput, outputFile); |
|||
|
|||
} catch (ParseException e) { |
|||
System.err.println("コマンドラインパースエラー: " + e.getMessage()); |
|||
printHelp(options); |
|||
System.exit(1); |
|||
} catch (Exception e) { |
|||
System.err.println("処理中にエラーが発生しました: " + e.getMessage()); |
|||
if (args.length > 0 && Arrays.asList(args).contains("-v")) { |
|||
e.printStackTrace(); |
|||
} |
|||
System.exit(1); |
|||
} |
|||
} |
|||
|
|||
private static Options createOptions() { |
|||
Options options = new Options(); |
|||
|
|||
options.addOption(Option.builder("i") |
|||
.longOpt("input") |
|||
.hasArg() |
|||
.argName("FILE") |
|||
.desc("入力PDFファイルのパス") |
|||
.required() |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("o") |
|||
.longOpt("output") |
|||
.hasArg() |
|||
.argName("FILE") |
|||
.desc("結果を出力するファイル(省略時は標準出力)") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("v") |
|||
.longOpt("verbose") |
|||
.desc("詳細な情報を表示") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("j") |
|||
.longOpt("json") |
|||
.desc("JSON形式で出力") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder("h") |
|||
.longOpt("help") |
|||
.desc("ヘルプを表示") |
|||
.build()); |
|||
|
|||
options.addOption(Option.builder() |
|||
.longOpt("version") |
|||
.desc("バージョン情報を表示") |
|||
.build()); |
|||
|
|||
return options; |
|||
} |
|||
|
|||
private static void printHelp(Options options) { |
|||
HelpFormatter formatter = new HelpFormatter(); |
|||
formatter.printHelp("pdf-probe", "PDF解析ツール - レイヤー、メタデータ、Illustrator痕跡の詳細分析", options, |
|||
"\n使用例:\n" + |
|||
" java -jar pdf-probe.jar -i document.pdf\n" + |
|||
" java -jar pdf-probe.jar -i document.pdf -v -j -o result.json\n", true); |
|||
} |
|||
|
|||
public void analyzePdf(String inputFile, boolean verbose, boolean jsonOutput, String outputFile) throws Exception { |
|||
File file = new File(inputFile); |
|||
if (!file.exists()) { |
|||
throw new RuntimeException("ファイルが見つかりません: " + inputFile); |
|||
} |
|||
|
|||
PdfAnalysisResult result = new PdfAnalysisResult(); |
|||
result.fileName = file.getName(); |
|||
result.fileSize = file.length(); |
|||
|
|||
try (PdfDocument pdf = new PdfDocument(new PdfReader(inputFile))) { |
|||
System.out.println("=== PDF解析開始: " + file.getName() + " ===\n"); |
|||
|
|||
// 1. メタデータ解析
|
|||
analyzeMetadata(pdf, result, verbose); |
|||
|
|||
// 2. OCG(レイヤー)解析
|
|||
analyzeOCG(pdf, result, verbose); |
|||
|
|||
// 3. ページ内容とレイヤー使用状況解析
|
|||
analyzePageContent(pdf, result, verbose); |
|||
|
|||
// 4. 注釈のレイヤー紐付け解析
|
|||
analyzeAnnotations(pdf, result, verbose); |
|||
|
|||
// 5. Illustrator固有の痕跡解析
|
|||
analyzeIllustratorTraces(pdf, result, verbose); |
|||
|
|||
// 結果出力
|
|||
if (jsonOutput) { |
|||
outputJson(result, outputFile); |
|||
} else { |
|||
outputText(result, outputFile, verbose); |
|||
} |
|||
|
|||
System.out.println("\n=== 解析完了 ==="); |
|||
} |
|||
} |
|||
|
|||
private void analyzeMetadata(PdfDocument pdf, PdfAnalysisResult result, boolean verbose) { |
|||
System.out.println("1. メタデータ解析"); |
|||
System.out.println("=================="); |
|||
|
|||
PdfDocumentInfo info = pdf.getDocumentInfo(); |
|||
result.producer = info.getProducer(); |
|||
result.creator = info.getCreator(); |
|||
result.title = info.getTitle(); |
|||
result.author = info.getAuthor(); |
|||
result.subject = info.getSubject(); |
|||
|
|||
System.out.println("Producer: " + (result.producer != null ? result.producer : "(なし)")); |
|||
System.out.println("Creator : " + (result.creator != null ? result.creator : "(なし)")); |
|||
|
|||
if (verbose) { |
|||
System.out.println("Title : " + (result.title != null ? result.title : "(なし)")); |
|||
System.out.println("Author : " + (result.author != null ? result.author : "(なし)")); |
|||
System.out.println("Subject : " + (result.subject != null ? result.subject : "(なし)")); |
|||
} |
|||
|
|||
// Illustratorの検出
|
|||
result.hasIllustratorInProducer = detectIllustrator(result.producer); |
|||
result.hasIllustratorInCreator = detectIllustrator(result.creator); |
|||
|
|||
if (result.hasIllustratorInProducer || result.hasIllustratorInCreator) { |
|||
System.out.println("✓ Adobe Illustratorが検出されました"); |
|||
} |
|||
|
|||
// XMPメタデータ解析
|
|||
analyzeXMP(pdf, result, verbose); |
|||
System.out.println(); |
|||
} |
|||
|
|||
private void analyzeXMP(PdfDocument pdf, PdfAnalysisResult result, boolean verbose) { |
|||
PdfDictionary catalog = pdf.getCatalog().getPdfObject(); |
|||
PdfStream metadata = catalog.getAsStream(PdfName.Metadata); |
|||
|
|||
if (metadata != null) { |
|||
try { |
|||
String xmpString = new String(metadata.getBytes(), StandardCharsets.UTF_8); |
|||
result.xmpMetadata = xmpString; |
|||
result.hasIllustratorInXMP = detectIllustrator(xmpString); |
|||
|
|||
System.out.println("XMP Metadata: あり"); |
|||
if (result.hasIllustratorInXMP) { |
|||
System.out.println("✓ XMP内にAdobe Illustratorが検出されました"); |
|||
} |
|||
|
|||
if (verbose) { |
|||
System.out.println("--- XMP内容 (最初の1000文字) ---"); |
|||
System.out.println(xmpString.substring(0, Math.min(1000, xmpString.length()))); |
|||
if (xmpString.length() > 1000) { |
|||
System.out.println("... (省略)"); |
|||
} |
|||
} |
|||
|
|||
// XMPの詳細解析
|
|||
try { |
|||
XMPMeta xmpMeta = XMPMetaFactory.parseFromString(xmpString); |
|||
result.xmpCreatorTool = getXMPProperty(xmpMeta, "http://ns.adobe.com/xap/1.0/", "CreatorTool"); |
|||
if (result.xmpCreatorTool != null) { |
|||
System.out.println("XMP CreatorTool: " + result.xmpCreatorTool); |
|||
if (!result.hasIllustratorInXMP) { |
|||
result.hasIllustratorInXMP = detectIllustrator(result.xmpCreatorTool); |
|||
} |
|||
} |
|||
} catch (XMPException e) { |
|||
if (verbose) { |
|||
System.out.println("XMP解析エラー: " + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
System.out.println("XMP読み込みエラー: " + e.getMessage()); |
|||
} |
|||
} else { |
|||
System.out.println("XMP Metadata: なし"); |
|||
} |
|||
} |
|||
|
|||
private String getXMPProperty(XMPMeta xmpMeta, String namespace, String property) { |
|||
try { |
|||
return xmpMeta.getPropertyString(namespace, property); |
|||
} catch (XMPException e) { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private void analyzeOCG(PdfDocument pdf, PdfAnalysisResult result, boolean verbose) { |
|||
System.out.println("2. OCG(レイヤー)解析"); |
|||
System.out.println("===================="); |
|||
|
|||
PdfDictionary catalog = pdf.getCatalog().getPdfObject(); |
|||
PdfDictionary ocProperties = catalog.getAsDictionary(PdfName.OCProperties); |
|||
|
|||
if (ocProperties != null) { |
|||
result.hasOCG = true; |
|||
PdfArray ocgs = ocProperties.getAsArray(PdfName.OCGs); |
|||
|
|||
if (ocgs != null) { |
|||
result.ocgCount = ocgs.size(); |
|||
System.out.println("レイヤー数: " + result.ocgCount); |
|||
System.out.println("レイヤー一覧:"); |
|||
|
|||
for (int i = 0; i < ocgs.size(); i++) { |
|||
PdfDictionary ocg = ocgs.getAsDictionary(i); |
|||
if (ocg != null) { |
|||
String name = getStringValue(ocg.getAsString(PdfName.Name)); |
|||
int objNum = ocg.getIndirectReference().getObjNumber(); |
|||
|
|||
OCGInfo ocgInfo = new OCGInfo(); |
|||
ocgInfo.name = name; |
|||
ocgInfo.objectNumber = objNum; |
|||
result.ocgList.add(ocgInfo); |
|||
|
|||
System.out.printf(" - %s (オブジェクト番号: %d)\n", name, objNum); |
|||
|
|||
if (verbose) { |
|||
// OCGの詳細情報
|
|||
PdfName intent = ocg.getAsName(PdfName.Intent); |
|||
if (intent != null) { |
|||
System.out.printf(" Intent: %s\n", intent); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 初期表示設定
|
|||
PdfDictionary defaultConfig = ocProperties.getAsDictionary(PdfName.D); |
|||
if (defaultConfig != null) { |
|||
System.out.println("\n初期表示設定:"); |
|||
|
|||
PdfArray onArray = defaultConfig.getAsArray(PdfName.ON); |
|||
PdfArray offArray = defaultConfig.getAsArray(PdfName.OFF); |
|||
|
|||
if (onArray != null) { |
|||
System.out.print(" 初期ON : "); |
|||
for (int i = 0; i < onArray.size(); i++) { |
|||
PdfDictionary ocg = onArray.getAsDictionary(i); |
|||
if (ocg != null) { |
|||
String name = getStringValue(ocg.getAsString(PdfName.Name)); |
|||
System.out.print(name + " "); |
|||
result.initialOnLayers.add(name); |
|||
} |
|||
} |
|||
System.out.println(); |
|||
} |
|||
|
|||
if (offArray != null) { |
|||
System.out.print(" 初期OFF: "); |
|||
for (int i = 0; i < offArray.size(); i++) { |
|||
PdfDictionary ocg = offArray.getAsDictionary(i); |
|||
if (ocg != null) { |
|||
String name = getStringValue(ocg.getAsString(PdfName.Name)); |
|||
System.out.print(name + " "); |
|||
result.initialOffLayers.add(name); |
|||
} |
|||
} |
|||
System.out.println(); |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
System.out.println("OCProperties なし(レイヤー情報なし)"); |
|||
result.hasOCG = false; |
|||
} |
|||
System.out.println(); |
|||
} |
|||
|
|||
private void analyzePageContent(PdfDocument pdf, PdfAnalysisResult result, boolean verbose) { |
|||
System.out.println("3. ページ内容のレイヤー使用状況"); |
|||
System.out.println("=============================="); |
|||
|
|||
result.pageCount = pdf.getNumberOfPages(); |
|||
System.out.println("総ページ数: " + result.pageCount); |
|||
|
|||
for (int pageNum = 1; pageNum <= pdf.getNumberOfPages(); pageNum++) { |
|||
PdfPage page = pdf.getPage(pageNum); |
|||
PageAnalysisInfo pageInfo = new PageAnalysisInfo(); |
|||
pageInfo.pageNumber = pageNum; |
|||
|
|||
// コンテンツストリームの解析
|
|||
try { |
|||
byte[] contentBytes = page.getContentBytes(); |
|||
String content = new String(contentBytes, StandardCharsets.UTF_8); |
|||
|
|||
// BDC/EMCマーカーの検出
|
|||
boolean hasBDC = content.contains("BDC"); |
|||
boolean hasEMC = content.contains("EMC"); |
|||
boolean hasOCMarker = content.contains("/OC"); |
|||
|
|||
pageInfo.hasBDCEMC = hasBDC && hasEMC; |
|||
pageInfo.hasOCMarker = hasOCMarker; |
|||
|
|||
if (pageInfo.hasBDCEMC || pageInfo.hasOCMarker) { |
|||
result.pagesWithLayerContent++; |
|||
} |
|||
|
|||
if (verbose || pageInfo.hasBDCEMC || pageInfo.hasOCMarker) { |
|||
System.out.printf("ページ %d: BDC/EMC=%s, /OC=%s\n", |
|||
pageNum, pageInfo.hasBDCEMC ? "あり" : "なし", |
|||
pageInfo.hasOCMarker ? "あり" : "なし"); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
if (verbose) { |
|||
System.out.printf("ページ %d: コンテンツ解析エラー - %s\n", pageNum, e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
result.pageAnalysis.add(pageInfo); |
|||
} |
|||
|
|||
System.out.println("レイヤーコンテンツを含むページ数: " + result.pagesWithLayerContent); |
|||
System.out.println(); |
|||
} |
|||
|
|||
private void analyzeAnnotations(PdfDocument pdf, PdfAnalysisResult result, boolean verbose) { |
|||
System.out.println("4. 注釈のレイヤー紐付け解析"); |
|||
System.out.println("=========================="); |
|||
|
|||
int totalAnnotations = 0; |
|||
int annotationsWithOC = 0; |
|||
|
|||
for (int pageNum = 1; pageNum <= pdf.getNumberOfPages(); pageNum++) { |
|||
PdfPage page = pdf.getPage(pageNum); |
|||
List<PdfAnnotation> annotations = page.getAnnotations(); |
|||
|
|||
if (annotations != null && !annotations.isEmpty()) { |
|||
if (verbose) { |
|||
System.out.printf("-- ページ %d --\n", pageNum); |
|||
} |
|||
|
|||
for (PdfAnnotation annotation : annotations) { |
|||
totalAnnotations++; |
|||
PdfDictionary annotDict = annotation.getPdfObject(); |
|||
PdfName subtype = annotDict.getAsName(PdfName.Subtype); |
|||
PdfDictionary ocDict = annotDict.getAsDictionary(PdfName.OC); |
|||
|
|||
AnnotationInfo annotInfo = new AnnotationInfo(); |
|||
annotInfo.pageNumber = pageNum; |
|||
annotInfo.subtype = subtype != null ? subtype.getValue() : "Unknown"; |
|||
annotInfo.hasOC = (ocDict != null); |
|||
|
|||
if (ocDict != null) { |
|||
annotationsWithOC++; |
|||
annotInfo.ocLayerName = getStringValue(ocDict.getAsString(PdfName.Name)); |
|||
} |
|||
|
|||
result.annotations.add(annotInfo); |
|||
|
|||
if (verbose) { |
|||
System.out.printf(" 注釈 %s: /OC=%s\n", |
|||
annotInfo.subtype, |
|||
annotInfo.hasOC ? annotInfo.ocLayerName : "なし"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
result.totalAnnotations = totalAnnotations; |
|||
result.annotationsWithOC = annotationsWithOC; |
|||
|
|||
System.out.println("総注釈数: " + totalAnnotations); |
|||
System.out.println("レイヤー紐付け注釈数: " + annotationsWithOC); |
|||
if (totalAnnotations > 0) { |
|||
System.out.printf("レイヤー紐付け率: %.1f%%\n", |
|||
(annotationsWithOC * 100.0 / totalAnnotations)); |
|||
} |
|||
System.out.println(); |
|||
} |
|||
|
|||
private void analyzeIllustratorTraces(PdfDocument pdf, PdfAnalysisResult result, boolean verbose) { |
|||
System.out.println("5. Illustrator固有の痕跡解析"); |
|||
System.out.println("==========================="); |
|||
|
|||
// カタログレベルの確認
|
|||
PdfDictionary catalog = pdf.getCatalog().getPdfObject(); |
|||
PdfDictionary pieceInfo = catalog.getAsDictionary(PdfName.PieceInfo); |
|||
if (pieceInfo != null) { |
|||
result.hasCatalogPieceInfo = true; |
|||
System.out.println("✓ カタログに PieceInfo が存在"); |
|||
} |
|||
|
|||
// ページレベルの確認
|
|||
for (int pageNum = 1; pageNum <= pdf.getNumberOfPages(); pageNum++) { |
|||
PdfPage page = pdf.getPage(pageNum); |
|||
PdfDictionary pageDict = page.getPdfObject(); |
|||
|
|||
PdfDictionary pagePieceInfo = pageDict.getAsDictionary(PdfName.PieceInfo); |
|||
if (pagePieceInfo != null) { |
|||
result.pagesWithPieceInfo++; |
|||
if (verbose) { |
|||
System.out.printf("ページ %d: PieceInfo あり\n", pageNum); |
|||
} |
|||
} |
|||
|
|||
// AIPrivateDataの検索(リソース内)
|
|||
PdfDictionary resources = pageDict.getAsDictionary(PdfName.Resources); |
|||
if (resources != null && searchForAIPrivateData(resources)) { |
|||
result.pagesWithAIPrivateData++; |
|||
if (verbose) { |
|||
System.out.printf("ページ %d: AIPrivateData の痕跡あり\n", pageNum); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 全体的なIllustrator判定
|
|||
result.isLikelyFromIllustrator = |
|||
result.hasIllustratorInProducer || |
|||
result.hasIllustratorInCreator || |
|||
result.hasIllustratorInXMP || |
|||
result.hasCatalogPieceInfo || |
|||
result.pagesWithPieceInfo > 0; |
|||
|
|||
System.out.println("PieceInfo付きページ数: " + result.pagesWithPieceInfo); |
|||
System.out.println("AIPrivateData痕跡ページ数: " + result.pagesWithAIPrivateData); |
|||
|
|||
if (result.isLikelyFromIllustrator) { |
|||
System.out.println("✓ Adobe Illustratorで作成された可能性が高い"); |
|||
} else { |
|||
System.out.println("Adobe Illustratorの痕跡は検出されませんでした"); |
|||
} |
|||
System.out.println(); |
|||
} |
|||
|
|||
private boolean detectIllustrator(String text) { |
|||
return text != null && ILLUSTRATOR_PATTERN.matcher(text).find(); |
|||
} |
|||
|
|||
private boolean searchForAIPrivateData(PdfDictionary dict) { |
|||
// 簡易的な検索(実際はより詳細な検索が必要)
|
|||
for (PdfName key : dict.keySet()) { |
|||
if (key.getValue().contains("AI") || key.getValue().contains("Adobe")) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private String getStringValue(PdfString pdfString) { |
|||
return pdfString != null ? pdfString.getValue() : null; |
|||
} |
|||
|
|||
private void outputText(PdfAnalysisResult result, String outputFile, boolean verbose) { |
|||
// テキスト形式での結果出力(現在のコンソール出力がこれに相当)
|
|||
System.out.println("=== 解析サマリー ==="); |
|||
System.out.printf("ファイル: %s (%.1f KB)\n", result.fileName, result.fileSize / 1024.0); |
|||
System.out.printf("Adobe Illustrator: %s\n", result.isLikelyFromIllustrator ? "検出" : "未検出"); |
|||
System.out.printf("レイヤー数: %d\n", result.ocgCount); |
|||
System.out.printf("総ページ数: %d\n", result.pageCount); |
|||
System.out.printf("総注釈数: %d\n", result.totalAnnotations); |
|||
} |
|||
|
|||
private void outputJson(PdfAnalysisResult result, String outputFile) { |
|||
// JSON出力の実装(簡易版)
|
|||
System.out.println("JSON出力機能は今後実装予定です。"); |
|||
} |
|||
|
|||
// データクラス群
|
|||
static class PdfAnalysisResult { |
|||
String fileName; |
|||
long fileSize; |
|||
|
|||
// メタデータ
|
|||
String producer; |
|||
String creator; |
|||
String title; |
|||
String author; |
|||
String subject; |
|||
String xmpMetadata; |
|||
String xmpCreatorTool; |
|||
boolean hasIllustratorInProducer; |
|||
boolean hasIllustratorInCreator; |
|||
boolean hasIllustratorInXMP; |
|||
|
|||
// OCG
|
|||
boolean hasOCG; |
|||
int ocgCount; |
|||
List<OCGInfo> ocgList = new ArrayList<>(); |
|||
List<String> initialOnLayers = new ArrayList<>(); |
|||
List<String> initialOffLayers = new ArrayList<>(); |
|||
|
|||
// ページ
|
|||
int pageCount; |
|||
int pagesWithLayerContent; |
|||
List<PageAnalysisInfo> pageAnalysis = new ArrayList<>(); |
|||
|
|||
// 注釈
|
|||
int totalAnnotations; |
|||
int annotationsWithOC; |
|||
List<AnnotationInfo> annotations = new ArrayList<>(); |
|||
|
|||
// Illustrator痕跡
|
|||
boolean hasCatalogPieceInfo; |
|||
int pagesWithPieceInfo; |
|||
int pagesWithAIPrivateData; |
|||
boolean isLikelyFromIllustrator; |
|||
} |
|||
|
|||
static class OCGInfo { |
|||
String name; |
|||
int objectNumber; |
|||
} |
|||
|
|||
static class PageAnalysisInfo { |
|||
int pageNumber; |
|||
boolean hasBDCEMC; |
|||
boolean hasOCMarker; |
|||
} |
|||
|
|||
static class AnnotationInfo { |
|||
int pageNumber; |
|||
String subtype; |
|||
boolean hasOC; |
|||
String ocLayerName; |
|||
} |
|||
} |
|||
@ -0,0 +1,154 @@ |
|||
package co.jp.techsor.pdf.util; |
|||
|
|||
import java.io.File; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.nio.file.Paths; |
|||
|
|||
/** |
|||
* PDF処理に関する共通ユーティリティクラス |
|||
*/ |
|||
public class PdfUtils { |
|||
|
|||
/** |
|||
* ファイルが存在するかチェックする |
|||
* |
|||
* @param filePath ファイルパス |
|||
* @return ファイルが存在する場合true |
|||
*/ |
|||
public static boolean fileExists(String filePath) { |
|||
if (filePath == null || filePath.trim().isEmpty()) { |
|||
return false; |
|||
} |
|||
return Files.exists(Paths.get(filePath)); |
|||
} |
|||
|
|||
/** |
|||
* ディレクトリが存在するかチェックし、存在しない場合は作成する |
|||
* |
|||
* @param dirPath ディレクトリパス |
|||
* @return 作成成功またはすでに存在する場合true |
|||
*/ |
|||
public static boolean ensureDirectoryExists(String dirPath) { |
|||
if (dirPath == null || dirPath.trim().isEmpty()) { |
|||
return false; |
|||
} |
|||
|
|||
try { |
|||
Path path = Paths.get(dirPath); |
|||
if (!Files.exists(path)) { |
|||
Files.createDirectories(path); |
|||
} |
|||
return Files.isDirectory(path); |
|||
} catch (Exception e) { |
|||
System.err.println("ディレクトリ作成エラー: " + e.getMessage()); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 出力ファイルのディレクトリを確保する |
|||
* |
|||
* @param outputPath 出力ファイルのパス |
|||
* @return 成功した場合true |
|||
*/ |
|||
public static boolean ensureOutputDirectory(String outputPath) { |
|||
if (outputPath == null || outputPath.trim().isEmpty()) { |
|||
return false; |
|||
} |
|||
|
|||
File file = new File(outputPath); |
|||
File parentDir = file.getParentFile(); |
|||
|
|||
if (parentDir != null) { |
|||
return ensureDirectoryExists(parentDir.getAbsolutePath()); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* ファイル拡張子を取得する |
|||
* |
|||
* @param filePath ファイルパス |
|||
* @return ファイル拡張子(ドット含む)、拡張子がない場合は空文字列 |
|||
*/ |
|||
public static String getFileExtension(String filePath) { |
|||
if (filePath == null || filePath.trim().isEmpty()) { |
|||
return ""; |
|||
} |
|||
|
|||
int lastDotIndex = filePath.lastIndexOf('.'); |
|||
if (lastDotIndex > 0 && lastDotIndex < filePath.length() - 1) { |
|||
return filePath.substring(lastDotIndex); |
|||
} |
|||
|
|||
return ""; |
|||
} |
|||
|
|||
/** |
|||
* PDFファイルかどうかを拡張子で判定する |
|||
* |
|||
* @param filePath ファイルパス |
|||
* @return PDFファイルの場合true |
|||
*/ |
|||
public static boolean isPdfFile(String filePath) { |
|||
String extension = getFileExtension(filePath).toLowerCase(); |
|||
return ".pdf".equals(extension); |
|||
} |
|||
|
|||
/** |
|||
* ファイルサイズを取得する(バイト単位) |
|||
* |
|||
* @param filePath ファイルパス |
|||
* @return ファイルサイズ、取得できない場合は-1 |
|||
*/ |
|||
public static long getFileSize(String filePath) { |
|||
try { |
|||
if (fileExists(filePath)) { |
|||
return Files.size(Paths.get(filePath)); |
|||
} |
|||
} catch (Exception e) { |
|||
System.err.println("ファイルサイズ取得エラー: " + e.getMessage()); |
|||
} |
|||
return -1; |
|||
} |
|||
|
|||
/** |
|||
* ファイルサイズを人間が読みやすい形式で取得する |
|||
* |
|||
* @param filePath ファイルパス |
|||
* @return ファイルサイズの文字列表現 |
|||
*/ |
|||
public static String getReadableFileSize(String filePath) { |
|||
long bytes = getFileSize(filePath); |
|||
if (bytes < 0) { |
|||
return "不明"; |
|||
} |
|||
|
|||
if (bytes < 1024) { |
|||
return bytes + " B"; |
|||
} else if (bytes < 1024 * 1024) { |
|||
return String.format("%.1f KB", bytes / 1024.0); |
|||
} else if (bytes < 1024 * 1024 * 1024) { |
|||
return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); |
|||
} else { |
|||
return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 安全なファイル名を生成する(無効な文字を除去) |
|||
* |
|||
* @param fileName 元のファイル名 |
|||
* @return 安全なファイル名 |
|||
*/ |
|||
public static String sanitizeFileName(String fileName) { |
|||
if (fileName == null || fileName.trim().isEmpty()) { |
|||
return "unnamed"; |
|||
} |
|||
|
|||
// Windows/Linuxで無効な文字を置換
|
|||
return fileName.replaceAll("[<>:\"/\\\\|?*]", "_").trim(); |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
# テスト用スクリプト - Convert-Pdf.ps1のテスト実行 |
|||
|
|||
# 現在のプロジェクトのJARファイルを使用したテスト |
|||
$jarPath = "build\libs\pdf-memo-layers.jar" |
|||
$samplesDir = "samples" |
|||
|
|||
Write-Host "=== Convert-Pdf.ps1 テスト実行 ===" -ForegroundColor Green |
|||
|
|||
# JARファイルの存在確認 |
|||
if (-not (Test-Path $jarPath)) { |
|||
Write-Host "エラー: JARファイルが見つかりません: $jarPath" -ForegroundColor Red |
|||
Write-Host "まず 'gradlew build' または 'gradlew fatJar' を実行してください" -ForegroundColor Yellow |
|||
exit 1 |
|||
} |
|||
|
|||
# サンプルディレクトリの存在確認 |
|||
if (-not (Test-Path $samplesDir)) { |
|||
Write-Host "エラー: samplesディレクトリが見つかりません" -ForegroundColor Red |
|||
exit 1 |
|||
} |
|||
|
|||
# 1. DryRunテスト |
|||
Write-Host "`n1. DryRunテスト実行中..." -ForegroundColor Cyan |
|||
.\Convert-Pdf.ps1 ` |
|||
-RootDir $samplesDir ` |
|||
-Converter "java -jar `"$jarPath`" --in `"{input}`" --out `"{output}`"" ` |
|||
-DryRun |
|||
|
|||
# 2. 実際の変換テスト(単一ファイル処理) |
|||
Write-Host "`n2. 実際の変換テスト(順次処理)..." -ForegroundColor Cyan |
|||
.\Convert-Pdf.ps1 ` |
|||
-RootDir $samplesDir ` |
|||
-Converter "java -jar `"$jarPath`" --in `"{input}`" --out `"{output}`"" ` |
|||
-Concurrency 1 ` |
|||
-Log "convert-test.log" |
|||
|
|||
# 3. 並列処理テスト |
|||
Write-Host "`n3. 並列処理テスト..." -ForegroundColor Cyan |
|||
.\Convert-Pdf.ps1 ` |
|||
-RootDir $samplesDir ` |
|||
-Converter "java -jar `"$jarPath`" --in `"{input}`" --out `"{output}`"" ` |
|||
-Concurrency 2 ` |
|||
-Log "convert-parallel-test.log" ` |
|||
-Force |
|||
|
|||
# 4. 出力先指定テスト |
|||
Write-Host "`n4. 出力先指定テスト..." -ForegroundColor Cyan |
|||
$outputDir = "test-output" |
|||
if (Test-Path $outputDir) { |
|||
Remove-Item $outputDir -Recurse -Force |
|||
} |
|||
|
|||
.\Convert-Pdf.ps1 ` |
|||
-RootDir $samplesDir ` |
|||
-Converter "java -jar `"$jarPath`" --in `"{input}`" --out `"{output}`"" ` |
|||
-OutputRoot $outputDir ` |
|||
-Log "convert-output-test.log" |
|||
|
|||
Write-Host "`n=== テスト完了 ===" -ForegroundColor Green |
|||
Write-Host "生成されたファイルを確認してください:" -ForegroundColor Yellow |
|||
Write-Host "- samplesディレクトリ内の *.converted.pdf ファイル" |
|||
Write-Host "- test-outputディレクトリ内のファイル" |
|||
Write-Host "- ログファイル: convert-*.log" |
|||
|
|||
|
|||
@ -0,0 +1,168 @@ |
|||
# PDF解析ツール(PdfProbeApp)のテスト用スクリプト |
|||
|
|||
Write-Host "=== PDF解析ツール テスト実行 ===" -ForegroundColor Green |
|||
|
|||
# JARファイルのパス |
|||
$probeJarPath = "build\libs\pdf-probe.jar" |
|||
$samplesDir = "samples" |
|||
|
|||
# JARファイルの存在確認 |
|||
if (-not (Test-Path $probeJarPath)) { |
|||
Write-Host "JARファイルが見つかりません。ビルドを実行します..." -ForegroundColor Yellow |
|||
|
|||
# Gradleビルドを実行してJARファイルを作成 |
|||
try { |
|||
Write-Host "Gradleビルド実行中..." -ForegroundColor Cyan |
|||
$env:JAVA_TOOL_OPTIONS = "-Dfile.encoding=UTF-8" |
|||
|
|||
# 既存のクラスファイルを使ってJARを作成 |
|||
Write-Host "JARファイル作成中..." -ForegroundColor Cyan |
|||
|
|||
# probeJarタスクを実行 |
|||
if (Test-Path "gradlew.bat") { |
|||
& .\gradlew.bat probeJar 2>$null |
|||
} else { |
|||
# 手動でJARファイルを作成 |
|||
$buildLibsDir = "build\libs" |
|||
if (-not (Test-Path $buildLibsDir)) { |
|||
New-Item -Path $buildLibsDir -ItemType Directory -Force | Out-Null |
|||
} |
|||
|
|||
# クラスファイルとライブラリからJARを作成 |
|||
Write-Host "手動でJARファイルを作成中..." -ForegroundColor Cyan |
|||
|
|||
# Manifestファイルを作成 |
|||
$manifestContent = @" |
|||
Manifest-Version: 1.0 |
|||
Main-Class: co.jp.techsor.pdf.PdfProbeApp |
|||
|
|||
"@ |
|||
$manifestPath = "build\MANIFEST.MF" |
|||
$manifestContent | Out-File -FilePath $manifestPath -Encoding UTF8 |
|||
|
|||
# PowerShellでJARファイルを作成(zipコマンドの代替) |
|||
Add-Type -AssemblyName System.IO.Compression.FileSystem |
|||
|
|||
if (Test-Path $probeJarPath) { |
|||
Remove-Item $probeJarPath -Force |
|||
} |
|||
|
|||
$archive = [System.IO.Compression.ZipFile]::Open($probeJarPath, [System.IO.Compression.ZipArchiveMode]::Create) |
|||
|
|||
# クラスファイルを追加 |
|||
$classesDir = "build\classes" |
|||
if (Test-Path $classesDir) { |
|||
Get-ChildItem -Path $classesDir -Recurse -File | ForEach-Object { |
|||
$relativePath = $_.FullName.Substring((Resolve-Path $classesDir).Path.Length + 1) |
|||
$relativePath = $relativePath.Replace('\', '/') |
|||
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($archive, $_.FullName, $relativePath) | Out-Null |
|||
} |
|||
} |
|||
|
|||
# ライブラリファイルを追加 |
|||
$libDir = "lib" |
|||
if (Test-Path $libDir) { |
|||
Get-ChildItem -Path $libDir -Filter "*.jar" | ForEach-Object { |
|||
$libArchive = [System.IO.Compression.ZipFile]::OpenRead($_.FullName) |
|||
foreach ($entry in $libArchive.Entries) { |
|||
if (-not $entry.FullName.EndsWith('/') -and -not $entry.FullName.StartsWith('META-INF/')) { |
|||
$newEntry = $archive.CreateEntry($entry.FullName) |
|||
$newEntry.LastWriteTime = $entry.LastWriteTime |
|||
$stream = $newEntry.Open() |
|||
$entryStream = $entry.Open() |
|||
$entryStream.CopyTo($stream) |
|||
$stream.Close() |
|||
$entryStream.Close() |
|||
} |
|||
} |
|||
$libArchive.Dispose() |
|||
} |
|||
} |
|||
|
|||
# Manifestを追加 |
|||
$manifestEntry = $archive.CreateEntry("META-INF/MANIFEST.MF") |
|||
$manifestStream = $manifestEntry.Open() |
|||
$manifestBytes = [System.Text.Encoding]::UTF8.GetBytes($manifestContent) |
|||
$manifestStream.Write($manifestBytes, 0, $manifestBytes.Length) |
|||
$manifestStream.Close() |
|||
|
|||
$archive.Dispose() |
|||
|
|||
Write-Host "JARファイルを手動で作成しました: $probeJarPath" -ForegroundColor Green |
|||
} |
|||
} |
|||
catch { |
|||
Write-Host "ビルドエラー: $($_.Exception.Message)" -ForegroundColor Red |
|||
Write-Host "手動でJARファイルを確認してください" -ForegroundColor Yellow |
|||
exit 1 |
|||
} |
|||
} |
|||
|
|||
# サンプルディレクトリの確認 |
|||
if (-not (Test-Path $samplesDir)) { |
|||
Write-Host "エラー: samplesディレクトリが見つかりません" -ForegroundColor Red |
|||
exit 1 |
|||
} |
|||
|
|||
# テスト対象のPDFファイルを取得 |
|||
$pdfFiles = Get-ChildItem -Path $samplesDir -Filter "*.pdf" | Select-Object -First 3 |
|||
|
|||
if ($pdfFiles.Count -eq 0) { |
|||
Write-Host "エラー: samplesディレクトリにPDFファイルが見つかりません" -ForegroundColor Red |
|||
exit 1 |
|||
} |
|||
|
|||
Write-Host "`n対象PDFファイル:" -ForegroundColor Cyan |
|||
$pdfFiles | ForEach-Object { Write-Host " - $($_.Name)" } |
|||
|
|||
# 各PDFファイルに対してテストを実行 |
|||
foreach ($pdfFile in $pdfFiles) { |
|||
Write-Host "`n" + ("=" * 60) -ForegroundColor Yellow |
|||
Write-Host "解析対象: $($pdfFile.Name)" -ForegroundColor Yellow |
|||
Write-Host ("=" * 60) -ForegroundColor Yellow |
|||
|
|||
$inputPath = $pdfFile.FullName |
|||
|
|||
try { |
|||
# 基本解析 |
|||
Write-Host "`n1. 基本解析:" -ForegroundColor Cyan |
|||
java -Dfile.encoding=UTF-8 -jar $probeJarPath -i "$inputPath" |
|||
|
|||
# 詳細解析 |
|||
Write-Host "`n2. 詳細解析:" -ForegroundColor Cyan |
|||
java -Dfile.encoding=UTF-8 -jar $probeJarPath -i "$inputPath" -v |
|||
|
|||
# 結果をファイルに保存 |
|||
$outputFile = "analysis-result-$($pdfFile.BaseName).txt" |
|||
Write-Host "`n3. ファイル出力テスト:" -ForegroundColor Cyan |
|||
java -Dfile.encoding=UTF-8 -jar $probeJarPath -i "$inputPath" -v -o $outputFile |
|||
|
|||
if (Test-Path $outputFile) { |
|||
Write-Host "✓ 結果ファイルが作成されました: $outputFile" -ForegroundColor Green |
|||
} else { |
|||
Write-Host "⚠ 結果ファイルの作成に失敗しました" -ForegroundColor Yellow |
|||
} |
|||
|
|||
} |
|||
catch { |
|||
Write-Host "エラー: $($_.Exception.Message)" -ForegroundColor Red |
|||
} |
|||
} |
|||
|
|||
# ヘルプの表示 |
|||
Write-Host "`n" + ("=" * 60) -ForegroundColor Yellow |
|||
Write-Host "ヘルプ表示テスト" -ForegroundColor Yellow |
|||
Write-Host ("=" * 60) -ForegroundColor Yellow |
|||
|
|||
try { |
|||
java -jar $probeJarPath --help |
|||
} |
|||
catch { |
|||
Write-Host "ヘルプ表示エラー: $($_.Exception.Message)" -ForegroundColor Red |
|||
} |
|||
|
|||
Write-Host "`n=== PDF解析ツール テスト完了 ===" -ForegroundColor Green |
|||
Write-Host "生成されたファイル:" -ForegroundColor Yellow |
|||
Get-ChildItem -Filter "analysis-result-*.txt" | ForEach-Object { |
|||
Write-Host " - $($_.Name)" -ForegroundColor White |
|||
} |
|||
Loading…
Reference in new issue