はじめに
-
背景
- 基盤更改などの案件では、現新比較テスト(差分がないこと)の確認が求められる
- 特に一部の帳票は、「差分がないこと」の厳密性が要件に含まれることが多い
- レイアウト位置、フォントの指定、文字サイズなど
-
課題
- 厳密な比較は、👀で確認は厳しい
- 確認結果の妥当性をステークスホルダに示すことも難しい
- PDFコンペアツールがお手頃なものがない
- 多数のPDFを一気に確認できたり、エビデンスを残せる機能がほしい
- 厳密な比較は、👀で確認は厳しい
やったこと
PythonのGUIアプリを自作しました。
ツールの操作方法
1.ディレクトリ選択
2つのディレクトリを選択します。
2.比較の実行
「比較開始」ボタンを押します。
すると、ディレクトリ内のすべてのPDFファイルを画像に変換され、ページごとの比較結果、差分の有無、差分の%が表示されます。スクロールで、全ページが確認できます。
3.比較レポートの出力
「結果の保存」ボタンを押します。
すると、比較結果レポートをHTMLファイルとして保存され、エビデンスとして残すことができます。
HTMLファイル
Ex.(オプション)パラメータ設定
「パラメータ設定」ボタンを押すと、ポップアップで設定画面が開きます。
比較の厳密さなどを変更できるようにしており、各種数値を調整できます。
3種類のプリセットからの選択も可能。設定のインポート、エクスポートも可能。
ヘルプから、各種パラメータの説明を確認できます。
処理内容
処理は大きく2つあります。
1.PDFを画像に変換して比較
2.HTMLレポートの保存
順に処理を解説します。
1.PDFを画像に変換して比較
行っていることは非常にシンプルで、大きく2つです。
- PyMuPDFを使ったPDFファイルの画像変換
- OpenCVを使った2画像の差分検知
# pdf_processor.py
import fitz # PyMuPDFライブラリをインポート
import os
import numpy as np
import cv2 # OpenCVライブラリをインポート
from pathlib import Path # パス操作のためのライブラリをインポート
class PDFProcessor:
def __init__(self, temp_dir):
self.temp_dir = temp_dir # 一時ディレクトリのパスを設定
def get_pdf_page_count(self, pdf_path):
"""PDFのページ数を取得"""
doc = fitz.open(pdf_path) # PDFを開く
page_count = len(doc) # ページ数を取得
doc.close() # PDFを閉じる
return page_count # ページ数を返す
def convert_pdf_to_images(self, pdf_path):
"""PDFの全ページを画像に変換"""
try:
doc = fitz.open(pdf_path) # PDFを開く
image_paths = [] # 画像パスのリストを初期化
for page_num in range(len(doc)): # 各ページを処理
page = doc[page_num] # ページを取得
pix = page.get_pixmap() # ページを画像に変換
img_path = os.path.join(
self.temp_dir,
f"temp_{os.path.basename(pdf_path)}_{page_num}.png"
) # 画像の保存パスを設定
pix.save(img_path) # 画像を保存
image_paths.append(img_path) # 画像パスをリストに追加
doc.close() # PDFを閉じる
return image_paths # 画像パスのリストを返す
except Exception as e:
raise Exception(f"PDF変換エラー: {str(e)}") # エラーをキャッチして例外を投げる
def compare_images(self, image_path1, image_path2, settings):
"""2つの画像を比較して差分を検出(高度な設定に対応)"""
try:
# 画像を読み込み
img1 = cv2.imread(image_path1)
img2 = cv2.imread(image_path2)
# 同じサイズにリサイズ
height = min(img1.shape[0], img2.shape[0])
width = min(img1.shape[1], img2.shape[1])
img1 = cv2.resize(img1, (width, height))
img2 = cv2.resize(img2, (width, height))
# ノイズ軽減処理
if settings["noise_reduction"] > 0:
kernel_size = 2 * settings["noise_reduction"] + 1
img1 = cv2.medianBlur(img1, kernel_size)
img2 = cv2.medianBlur(img2, kernel_size)
# ぼかし処理
if settings["blur_radius"] > 0:
kernel_size = 2 * settings["blur_radius"] + 1
img1 = cv2.GaussianBlur(img1, (kernel_size, kernel_size), 0)
img2 = cv2.GaussianBlur(img2, (kernel_size, kernel_size), 0)
# エッジ検出
if settings["edge_detection"]:
img1 = cv2.Canny(img1, 100, 200)
img2 = cv2.Canny(img2, 100, 200)
# 色差感度に基づく処理
if settings["color_sensitivity"] == "high":
color_space = cv2.COLOR_BGR2Lab
elif settings["color_sensitivity"] == "low":
color_space = cv2.COLOR_BGR2HSV
else: # medium
color_space = cv2.COLOR_BGR2RGB
if not settings["edge_detection"]:
img1 = cv2.cvtColor(img1, color_space)
img2 = cv2.cvtColor(img2, color_space)
# 差分計算
if settings["edge_detection"]:
diff = cv2.absdiff(img1, img2)
else:
diff = cv2.absdiff(img1, img2)
diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
# 閾値処理
_, thresh = cv2.threshold(
diff,
settings["pixel_threshold"],
255,
cv2.THRESH_BINARY
)
# 差分領域の強調表示
diff_img = cv2.imread(image_path1) # 元画像を再度読み込み
diff_img = cv2.resize(diff_img, (width, height))
diff_img[thresh > 0] = [0, 0, 255] # 差分箇所を赤色で表示
# 差分の程度を計算
diff_percentage = (np.count_nonzero(thresh) / thresh.size) * 100
# 結果を保存
diff_path = os.path.join(
self.temp_dir,
f"diff_{os.path.basename(image_path1)}"
)
cv2.imwrite(diff_path, diff_img)
return diff_path, diff_percentage # 差分画像のパスと差分率を返す
except Exception as e:
raise Exception(f"画像比較エラー: {str(e)}") # エラーをキャッチして例外を投げる
def get_pdf_files(self, directory):
"""ディレクトリ内のPDFファイルを取得"""
return sorted([f for f in Path(directory).glob("*.pdf")]) # PDFファイルを取得してソートして返す
def cleanup_temp_files(self):
"""一時ファイルの削除"""
for file in os.listdir(self.temp_dir): # 一時ディレクトリ内のファイルを取得
os.remove(os.path.join(self.temp_dir, file)) # ファイルを削除
2.HTMLレポートの保存
HTMLレポートのテンプレート定義をあらかじめ定義しておき、そのテンプレートにコンテンツを格納してHTMLを作成しています。
# report_generator.py
import os
import logging
from datetime import datetime
import base64
from PIL import Image
import traceback
from string import Template
from report_template import ReportTemplate
class ReportGenerator:
def __init__(self):
self.template = ReportTemplate() # レポートテンプレートのインスタンスを作成
self.setup_logging() # ロギングの設定を行う
self.logger = logging.getLogger('ReportGenerator') # ロガーを取得
def generate_report(self, comparison_results, settings, output_dir):
"""比較結果をHTMLレポートとして生成"""
try:
self.settings = settings # 設定を保存
self.logger.info("レポート生成開始")
self.logger.debug(f"出力ディレクトリ: {output_dir}")
self.logger.debug(f"設定内容: {settings}")
# タイムスタンプを生成してレポートディレクトリを作成
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
report_dir = os.path.join(output_dir, f"comparison_report_{timestamp}")
os.makedirs(report_dir, exist_ok=True)
# 画像ディレクトリを作成
images_dir = os.path.join(report_dir, "images")
os.makedirs(images_dir, exist_ok=True)
self.logger.info("コンテンツ生成開始")
content_parts = [] # コンテンツ部分を格納するリスト
# ヘッダー情報
self.logger.debug("ヘッダー生成")
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
content_parts.append(self.template.get_header(current_time, settings))
# サマリー情報
self.logger.debug("サマリー生成")
total_pages = len(comparison_results) # 総ページ数を取得
pages_with_diff = sum(1 for r in comparison_results if r['diff_percentage'] > settings['diff_threshold']) # 差分のあるページ数を計算
avg_diff = sum(r['diff_percentage'] for r in comparison_results) / total_pages if total_pages > 0 else 0 # 平均差分率を計算
content_parts.append(self.template.get_summary(total_pages, pages_with_diff, avg_diff))
# 比較結果
self.logger.info(f"比較結果の生成開始 (全{len(comparison_results)}件)")
current_files = None # 現在のファイルペアを初期化
for i, result in enumerate(comparison_results, 1):
try:
self.logger.debug(f"結果 {i}/{len(comparison_results)} の処理")
if current_files != (result['file1'], result['file2']):
if current_files is not None:
content_parts.append(self.template.get_file_comparison_end()) # ファイル比較終了テンプレートを追加
current_files = (result['file1'], result['file2'])
content_parts.append(self.template.get_file_comparison_start(result['file1'], result['file2'])) # ファイル比較開始テンプレートを追加
# 画像の処理
images = self._process_images(i-1, result, images_dir)
# 差分状態の決定
diff_class = "diff_high" if result['diff_percentage'] > settings['diff_threshold'] else "diff_low"
diff_status = "差分あり" if result['diff_percentage'] > settings['diff_threshold'] else "差分なし"
comparison_item = self.template.get_comparison_item(
index=i-1,
page=result['page'],
images=images,
diff_percentage=result['diff_percentage'],
diff_class=diff_class,
diff_status=diff_status
)
content_parts.append(comparison_item) # 比較アイテムテンプレートを追加
except Exception as e:
self.logger.error(f"比較結果 {i} の生成中にエラー: {str(e)}")
self.logger.debug(f"処理中の結果データ: {result}")
raise
if content_parts:
content_parts.append(self.template.get_file_comparison_end()) # 最後のファイル比較終了テンプレートを追加
# レポート生成
self.logger.info("HTMLファイルの生成開始")
try:
template = Template(self.template.get_html_template())
html_content = template.safe_substitute(content='\n'.join(content_parts)) # HTMLコンテンツを生成
# デバッグ用にHTMLの一部を出力
self.logger.debug("生成されたHTMLの最初の1000文字:")
self.logger.debug(html_content[:1000])
report_path = os.path.join(report_dir, "report.html")
with open(report_path, "w", encoding="utf-8") as f:
f.write(html_content) # HTMLファイルを書き込み
self.logger.info(f"レポート生成完了: {report_path}")
return report_path # レポートのパスを返す
except Exception as e:
self.logger.error("HTML生成中にエラー発生")
self.logger.error(f"エラー詳細: {str(e)}")
self.logger.debug(f"スタックトレース: {traceback.format_exc()}")
raise
except Exception as e:
self.logger.error(f"レポート生成中に重大なエラーが発生: {str(e)}")
self.logger.debug(f"スタックトレース: {traceback.format_exc()}")
raise
def _process_images(self, index, result, images_dir):
"""画像の処理とbase64エンコード"""
images = {}
for key in ['img1', 'img2', 'diff']:
try:
img_name = f"comparison_{index}_{key}.png"
img_path = os.path.join(images_dir, img_name)
self.logger.debug(f"画像処理: {img_path}")
img = Image.open(result[key]) # 画像を開く
img.save(img_path) # 画像を保存
with open(img_path, 'rb') as img_file:
img_data = base64.b64encode(img_file.read()).decode() # 画像をbase64エンコード
images[key] = f"data:image/png;base64,{img_data}" # エンコードしたデータを辞書に追加
except Exception as e:
self.logger.error(f"画像処理エラー ({key}): {str(e)}")
raise
return images # 画像データの辞書を返す
# report_template.py
import os
import logging
from string import Template
class ReportTemplate:
def __init__(self):
self.logger = logging.getLogger(__name__) # ロガーを取得
self.template_dir = self._get_template_dir() # テンプレートディレクトリのパスを取得
def _get_template_dir(self):
"""テンプレートディレクトリのパスを取得"""
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
def _load_resource(self, filename):
"""リソースファイルを読み込む"""
try:
file_path = os.path.join(self.template_dir, filename) # ファイルパスを生成
self.logger.debug(f"リソース読み込み: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
return f.read() # ファイル内容を読み込んで返す
except Exception as e:
self.logger.error(f"リソースファイルの読み込みエラー ({filename}): {str(e)}")
raise
def get_html_template(self):
"""メインのHTMLテンプレートを取得"""
try:
template_content = self._load_resource('report_template.html') # メインテンプレートを読み込む
css_content = self._load_resource('../styles/report_styles.css') # CSSを読み込む
js_content = self._load_resource('../scripts/report_scripts.js') # JavaScriptを読み込む
template = Template(template_content) # テンプレートを作成
return template.safe_substitute(
css=css_content,
javascript=js_content,
content='${content}' # 後で置換するためのプレースホルダー
)
except Exception as e:
self.logger.error(f"HTMLテンプレート生成エラー: {str(e)}")
raise
def get_header(self, timestamp, settings):
"""ヘッダー部分を生成"""
try:
template_content = self._load_resource('components/header.html') # ヘッダーテンプレートを読み込む
template = Template(template_content) # テンプレートを作成
noise_levels = ['なし', '弱', '中', '強'] # ノイズレベルのマッピング
sensitivity_map = {'low': '低', 'medium': '中', 'high': '高'} # 色感度のマッピング
return template.safe_substitute(
timestamp=timestamp,
diff_threshold=settings["diff_threshold"],
pixel_threshold=settings["pixel_threshold"],
noise_reduction=noise_levels[settings["noise_reduction"]],
blur_radius=settings["blur_radius"],
edge_detection='有効' if settings["edge_detection"] else '無効',
color_sensitivity=sensitivity_map.get(settings["color_sensitivity"], '中')
)
except Exception as e:
self.logger.error(f"ヘッダー生成エラー: {str(e)}")
raise
def get_summary(self, total_pages, pages_with_diff, avg_diff):
"""サマリー部分を生成"""
try:
template_content = self._load_resource('components/summary.html') # サマリーテンプレートを読み込む
template = Template(template_content) # テンプレートを作成
return template.safe_substitute(
total_pages=total_pages,
pages_with_diff=pages_with_diff,
avg_diff=f"{avg_diff:.2f}"
)
except Exception as e:
self.logger.error(f"サマリー生成エラー: {str(e)}")
raise
def get_file_comparison_start(self, file1, file2):
"""ファイル比較セクションの開始部分を生成"""
try:
template_content = self._load_resource('components/file_comparison.html') # ファイル比較テンプレートを読み込む
template = Template(template_content) # テンプレートを作成
return template.safe_substitute(
file1=file1,
file2=file2,
content='${content}'
)
except Exception as e:
self.logger.error(f"ファイル比較開始部分生成エラー: {str(e)}")
raise
def get_file_comparison_end(self):
"""ファイル比較セクションの終了部分を生成"""
return "</div>"
def get_comparison_item(self, index, page, images, diff_percentage, diff_class, diff_status):
"""比較アイテムを生成"""
try:
template_content = self._load_resource('components/comparison_item.html') # 比較アイテムテンプレートを読み込む
template = Template(template_content) # テンプレートを作成
return template.safe_substitute(
index=index,
page=page,
img1=images['img1'],
img2=images['img2'],
diff=images['diff'],
diff_percentage=f"{diff_percentage:.2f}",
diff_class=diff_class,
diff_status=diff_status
)
except Exception as e:
self.logger.error(f"比較アイテム生成エラー: {str(e)}")
raise
HTMLテンプレート
HTMLの各部、CSS,javascriptをそれぞれテンプレートとしてあらかじめ定義して、それらを組み合わせてレポートを生成しています。
全体のディレクトリは以下のようになります。
- main.py: メインのPythonスクリプト。GUIの設定や操作が含まれています。
- pdf_processor.py: PDF処理
- settings_dialog.py: 設定ダイアログ
- report_generator.py: レポート生成
- report_template.py: レポートのテンプレートに関するスクリプト
- scripts:
- report_scripts.js: レポートに関するJavaScriptファイル
- styles:
- report_styles.css: レポートのスタイルシート
- templates: テンプレートファイルが保存されるディレクトリ
- components/:
- comparison_item.html: 比較アイテムのテンプレート
- file_comparison.html: ファイル比較のテンプレート
- header.html: ヘッダーのテンプレート
- summary.html: サマリーのテンプレート
- report_template.html: レポートのメインテンプレート
GUI部分
GUIにはtkinterを使用し、シンプルなツール操作を実現しています。
# main.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
import tempfile
import os
from datetime import datetime
import webbrowser
from pdf_processor import PDFProcessor
from settings_dialog import SettingsDialog
from report_generator import ReportGenerator
class PDFComparisonGUI:
def __init__(self, root):
self.root = root
self.root.title("PDF比較ツール")
self.root.state('zoomed')
# 一時ディレクトリの作成
self.temp_dir = tempfile.mkdtemp()
# 各クラスのインスタンス化
self.pdf_processor = PDFProcessor(self.temp_dir)
self.report_generator = ReportGenerator()
# 変数の初期化
self.dir1_path = tk.StringVar()
self.dir2_path = tk.StringVar()
self.image_size = (400, 1) # 高さは自動計算
self.comparison_results = []
# 設定パラメータの初期化
self.settings = {
"diff_threshold": 1.0,
"pixel_threshold": 30,
"noise_reduction": 2,
"blur_radius": 1,
"edge_detection": False,
"color_sensitivity": "medium"
}
self.setup_ui()
def setup_ui(self):
# メインフレーム
main_frame = ttk.Frame(self.root)
main_frame.pack(fill='both', expand=True, padx=10, pady=5)
# 上部コントロールエリア
self.setup_control_area(main_frame)
# 中央の画像表示エリア(スクロール可能)
self.setup_scroll_area(main_frame)
# ステータスバー
self.status_bar = ttk.Label(self.root, text="準備完了", relief=tk.SUNKEN)
self.status_bar.pack(fill='x', side='bottom', padx=5, pady=2)
def setup_control_area(self, parent):
control_frame = ttk.LabelFrame(parent, text="コントロールパネル", padding=(5, 5, 5, 5))
control_frame.pack(fill='x', padx=5, pady=5)
# ディレクトリ選択部分
dir_frame = ttk.Frame(control_frame)
dir_frame.pack(fill='x')
# 左側のディレクトリ選択
left_frame = ttk.Frame(dir_frame)
left_frame.pack(side='left', padx=5)
ttk.Label(left_frame, text="ディレクトリ1:").pack(side='left')
ttk.Entry(left_frame, textvariable=self.dir1_path, width=50).pack(side='left', padx=5)
ttk.Button(left_frame, text="参照", command=lambda: self.select_directory(1)).pack(side='left')
# 右側のディレクトリ選択
right_frame = ttk.Frame(dir_frame)
right_frame.pack(side='left', padx=5)
ttk.Label(right_frame, text="ディレクトリ2:").pack(side='left')
ttk.Entry(right_frame, textvariable=self.dir2_path, width=50).pack(side='left', padx=5)
ttk.Button(right_frame, text="参照", command=lambda: self.select_directory(2)).pack(side='left')
# 設定とコントロールのフレーム
settings_frame = ttk.Frame(control_frame)
settings_frame.pack(fill='x', pady=5)
# 左側:操作ボタン
button_frame = ttk.Frame(settings_frame)
button_frame.pack(side='left', padx=5)
# 各種操作ボタン
ttk.Button(button_frame, text="比較開始", command=self.start_comparison).pack(side='left', padx=5)
ttk.Button(button_frame, text="画像サイズ調整", command=self.adjust_image_size).pack(side='left', padx=5)
ttk.Button(button_frame, text="パラメータ設定", command=self.show_settings_dialog).pack(side='left', padx=5)
ttk.Button(button_frame, text="結果を保存", command=self.save_results).pack(side='left', padx=5)
# 現在の設定値を表示
self.settings_label = ttk.Label(button_frame,
text=f"差分許容値: {self.settings['diff_threshold']}% / ピクセル閾値: {self.settings['pixel_threshold']}")
self.settings_label.pack(side='left', padx=20)
# 右側:プログレスバー
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(settings_frame, variable=self.progress_var, maximum=100)
self.progress_bar.pack(side='right', fill='x', expand=True, padx=5)
def setup_scroll_area(self, parent):
# スクロール可能なキャンバスの作成
self.canvas = tk.Canvas(parent)
scrollbar = ttk.Scrollbar(parent, orient="vertical", command=self.canvas.yview)
self.scrollable_frame = ttk.Frame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
self.canvas.configure(yscrollcommand=scrollbar.set)
# マウスホイールでスクロール
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
# パッキング
self.canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def _on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
def select_directory(self, dir_num):
directory = filedialog.askdirectory()
if directory:
if dir_num == 1:
self.dir1_path.set(directory)
else:
self.dir2_path.set(directory)
def adjust_image_size(self):
dialog = tk.Toplevel(self.root)
dialog.title("画像サイズ調整")
dialog.geometry("300x120")
dialog.transient(self.root)
dialog.grab_set()
ttk.Label(dialog, text="基準幅:").pack(pady=5)
width_entry = ttk.Entry(dialog)
width_entry.insert(0, str(self.image_size[0]))
width_entry.pack()
def apply_size():
try:
width = int(width_entry.get())
self.image_size = (width, 1) # 高さは自動計算
dialog.destroy()
if self.comparison_results:
self.display_results()
except ValueError:
messagebox.showerror("エラー", "有効な数値を入力してください")
ttk.Button(dialog, text="適用", command=apply_size).pack(pady=10)
def show_settings_dialog(self):
def on_settings_changed(new_settings):
self.settings.update(new_settings)
self.settings_label.config(
text=f"差分許容値: {self.settings['diff_threshold']}% / "
f"ピクセル閾値: {self.settings['pixel_threshold']} / "
f"詳細設定: 使用中"
)
if self.comparison_results:
if messagebox.askyesno("確認", "設定を変更しました。比較結果を更新しますか?"):
self.start_comparison()
SettingsDialog(
self.root,
self.settings,
on_settings_changed
)
def save_results(self):
"""比較結果をレポートとして保存"""
if not self.comparison_results:
messagebox.showwarning("警告", "保存する比較結果がありません")
return
# 保存先ディレクトリの選択
save_dir = filedialog.askdirectory(
title="レポートの保存先を選択"
)
if save_dir:
try:
# 進捗状態の更新
self.status_bar.config(text="レポート生成中...")
self.root.update()
# レポートの生成
report_path = self.report_generator.generate_report(
self.comparison_results,
self.settings,
save_dir
)
# 成功メッセージの表示
message = f"レポートを保存しました:\n{report_path}"
messagebox.showinfo("成功", message)
# レポートを自動で開く
if messagebox.askyesno("確認", "保存したレポートを開きますか?"):
webbrowser.open(f"file://{report_path}")
except Exception as e:
# エラーログの確認
log_file = os.path.join('logs', f'report_generator_{datetime.now().strftime("%Y%m%d")}.log')
error_message = f"レポートの保存中にエラーが発生しました:\n{str(e)}"
if os.path.exists('logs'):
error_message += f"\n\n詳細なエラー情報は以下のログファイルを確認してください:\n{log_file}"
messagebox.showerror("エラー", error_message)
finally:
self.status_bar.config(text="準備完了")
self.root.update()
def start_comparison(self):
try:
# 既存の結果をクリア
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
self.comparison_results = []
# PDFファイルの取得
pdf_files1 = self.pdf_processor.get_pdf_files(self.dir1_path.get())
pdf_files2 = self.pdf_processor.get_pdf_files(self.dir2_path.get())
if not pdf_files1 or not pdf_files2:
messagebox.showwarning("警告", "PDFファイルが見つかりません")
return
total_files = min(len(pdf_files1), len(pdf_files2))
# 全ファイルのページ数を計算
total_pages = 0
for i in range(total_files):
pages1 = self.pdf_processor.get_pdf_page_count(str(pdf_files1[i]))
pages2 = self.pdf_processor.get_pdf_page_count(str(pdf_files2[i]))
total_pages += min(pages1, pages2)
current_progress = 0
for i in range(total_files):
self.status_bar.config(text=f"ファイル処理中... {i+1}/{total_files}")
self.root.update()
# 各PDFの全ページを画像に変換
images1 = self.pdf_processor.convert_pdf_to_images(str(pdf_files1[i]))
images2 = self.pdf_processor.convert_pdf_to_images(str(pdf_files2[i]))
# 各ページを比較
pages_to_compare = min(len(images1), len(images2))
for page in range(pages_to_compare):
# プログレスバーの更新
current_progress += 1
self.progress_var.set((current_progress / total_pages) * 100)
self.root.update()
# 差分検出
diff_path, diff_percentage = self.pdf_processor.compare_images(
images1[page],
images2[page],
self.settings
)
self.comparison_results.append({
'img1': images1[page],
'img2': images2[page],
'diff': diff_path,
'diff_percentage': diff_percentage,
'file1': pdf_files1[i].name,
'file2': pdf_files2[i].name,
'page': page + 1
})
self.display_results()
self.progress_var.set(100)
self.status_bar.config(text="比較完了")
except Exception as e:
messagebox.showerror("エラー", str(e))
def display_results(self):
# 既存の結果をクリア
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
current_file = None
file_frame = None
for i, result in enumerate(self.comparison_results):
# ファイルが変わったら新しいフレームを作成
if current_file != (result['file1'], result['file2']):
current_file = (result['file1'], result['file2'])
file_frame = ttk.LabelFrame(
self.scrollable_frame,
text=f"ファイル比較: {result['file1']} vs {result['file2']}"
)
file_frame.pack(fill='x', padx=5, pady=5)
# ページごとの結果フレーム
result_frame = ttk.LabelFrame(
file_frame,
text=f"ページ {result['page']}"
)
result_frame.pack(fill='x', padx=5, pady=5)
# 画像を横に並べて表示するフレーム
image_frame = ttk.Frame(result_frame)
image_frame.pack(fill='x', padx=5, pady=5)
# 各画像のコンテナを作成(均等な幅で配置)
containers_frame = ttk.Frame(image_frame)
containers_frame.pack(fill='x', expand=True)
# 元の画像1のコンテナ
img1_container = ttk.Frame(containers_frame)
img1_container.pack(side='left', expand=True, fill='both', padx=5)
ttk.Label(img1_container, text="元画像1").pack()
img1_label = ttk.Label(img1_container)
img1_label.pack(expand=True)
self.display_image(result['img1'], img1_label)
# 元の画像2のコンテナ
img2_container = ttk.Frame(containers_frame)
img2_container.pack(side='left', expand=True, fill='both', padx=5)
ttk.Label(img2_container, text="元画像2").pack()
img2_label = ttk.Label(img2_container)
img2_label.pack(expand=True)
self.display_image(result['img2'], img2_label)
# 差分画像のコンテナ
diff_container = ttk.Frame(containers_frame)
diff_container.pack(side='left', expand=True, fill='both', padx=5)
ttk.Label(diff_container, text="差分").pack()
diff_label = ttk.Label(diff_container)
diff_label.pack(expand=True)
self.display_image(result['diff'], diff_label)
# 差分情報フレーム
diff_info_frame = ttk.Frame(result_frame)
diff_info_frame.pack(fill='x', padx=5, pady=5)
# 差分のパーセンテージ表示
diff_style = 'error' if result['diff_percentage'] > self.settings['diff_threshold'] else 'success'
diff_info = ttk.Label(
diff_info_frame,
text=f"差分: {result['diff_percentage']:.2f}%",
foreground='red' if diff_style == 'error' else 'green'
)
diff_info.pack(side='left')
# 差分の有無の表示
status_label = ttk.Label(
diff_info_frame,
text="差分あり" if diff_style == 'error' else "差分なし",
foreground='red' if diff_style == 'error' else 'green'
)
status_label.pack(side='left', padx=10)
def display_image(self, image_path, label):
"""画像をラベルに表示(アスペクト比を保持)"""
try:
img = Image.open(image_path)
# 元の画像のアスペクト比を計算
original_ratio = img.width / img.height
# 表示サイズの基準値
base_width = self.image_size[0]
# アスペクト比を維持しながら新しいサイズを計算
new_width = base_width
new_height = int(base_width / original_ratio)
# リサイズ(アスペクト比を保持)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(img)
label.configure(image=photo)
label.image = photo
except Exception as e:
error_msg = f"画像表示エラー: {str(e)}"
messagebox.showerror("エラー", error_msg)
def cleanup(self):
"""アプリケーション終了時のクリーンアップ"""
try:
self.pdf_processor.cleanup_temp_files()
os.rmdir(self.temp_dir)
except Exception as e:
print(f"クリーンアップエラー: {str(e)}")
def main():
root = tk.Tk()
app = PDFComparisonGUI(root)
root.protocol("WM_DELETE_WINDOW", lambda: (app.cleanup(), root.destroy()))
root.mainloop()
if __name__ == "__main__":
main()
さいごに
現新比較テストのためのPDFコンペアツールをつくりました。
ガンガン使っていきます。