"""WordCloud画像生成モジュール."""
import logging
import random
from pathlib import Path
from typing import Optional
import matplotlib
import matplotlib.pyplot as plt
from wordcloud import WordCloud
# matplotlibのバックエンドをAggに設定(GUI不要)
matplotlib.use("Agg")
logger = logging.getLogger(__name__)
class WordCloudGenerator:
"""WordCloud画像生成を行うクラス.
日本語対応のWordCloudを生成し、美しいカラースキームを適用します。
"""
def __init__(self):
"""初期化."""
self.default_font = self._get_japanese_font()
self.color_schemes = {"accenture": self._accenture_color_func, "default": None}
def generate(
self,
text_or_frequencies: str | dict[str, int],
output_path: Optional[str] = None,
color_scheme: str = "accenture",
width: int = 1200,
height: int = 600,
) -> str:
"""WordCloudを生成.
Args:
text_or_frequencies: WordCloud生成元のテキストまたは頻度辞書
output_path: 画像出力パス(省略時は一時ファイル)
color_scheme: カラースキーム('accenture' or 'default')
width: 画像幅
height: 画像高さ
Returns:
str: 生成された画像のパス
Raises:
ValueError: データが空の場合
"""
# 入力チェック
if isinstance(text_or_frequencies, dict):
if not text_or_frequencies:
raise ValueError("頻度辞書が空です")
logger.info(f"WordCloud生成開始: {len(text_or_frequencies)}語の頻度辞書")
elif isinstance(text_or_frequencies, str):
if not text_or_frequencies or not text_or_frequencies.strip():
raise ValueError("テキストが空です")
logger.info(f"WordCloud生成開始: {len(text_or_frequencies)}文字")
else:
raise ValueError("テキストまたは頻度辞書を指定してください")
# カラー関数取得
color_func = self.color_schemes.get(color_scheme, None)
# WordCloud生成
wordcloud = WordCloud(
width=width,
height=height,
background_color="#F8F8F8" if color_scheme == "accenture" else "white",
font_path=self.default_font,
color_func=color_func,
prefer_horizontal=0.7,
relative_scaling=0.5,
min_font_size=10,
)
# 頻度辞書またはテキストから生成
if isinstance(text_or_frequencies, dict):
wordcloud.generate_from_frequencies(text_or_frequencies)
else:
wordcloud.generate(text_or_frequencies)
# 画像保存
if output_path is None:
output_path = "wordcloud.png"
output_file = Path(output_path)
output_file.parent.mkdir(parents=True, exist_ok=True)
plt.figure(figsize=(width / 100, height / 100), facecolor="#F8F8F8" if color_scheme == "accenture" else "white")
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.tight_layout(pad=0)
plt.savefig(
output_file, format="png", dpi=100, bbox_inches="tight", facecolor="#F8F8F8" if color_scheme == "accenture" else "white"
)
plt.close()
logger.info(f"WordCloud生成完了: {output_file}")
return str(output_file)
def _get_japanese_font(self) -> Optional[str]:
"""日本語フォントパスを取得.
Returns:
Optional[str]: フォントパス(見つからない場合はNone)
"""
# Windows環境での日本語フォント候補
font_candidates = [
"C:\\Windows\\Fonts\\msgothic.ttc", # MSゴシック
"C:\\Windows\\Fonts\\meiryo.ttc", # メイリオ
"C:\\Windows\\Fonts\\yugothm.ttc", # 游ゴシック
# Linux環境
"/usr/share/fonts/truetype/fonts-japanese-gothic.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
# macOS環境
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
"/Library/Fonts/Hiragino Sans GB.ttc",
]
for font_path in font_candidates:
if Path(font_path).exists():
logger.info(f"日本語フォント検出: {font_path}")
return font_path
logger.warning("日本語フォントが見つかりません。デフォルトフォントを使用します。")
return None
def _accenture_color_func(self, word=None, font_size=None, position=None, orientation=None, font_path=None, random_state=None):
"""パープルグラデーションカラー関数.
パープルグラデーション(#A100FF → #6B00B6)のカラースキームを生成します。
Returns:
str: RGB文字列
"""
# パープルグラデーションの色リスト
purple_colors = [
(161, 0, 255), # #A100FF
(139, 0, 230),
(107, 0, 182), # #6B00B6
(201, 102, 255), # #C966FF (lighter)
(180, 50, 255),
]
# random_stateを使用して決定的に色を選択
# 単語のハッシュ値から色を決定(再現性を持たせる)
if word:
idx = hash(word) % len(purple_colors)
elif random_state is not None:
# numpy.random.RandomStateオブジェクトの場合
if hasattr(random_state, 'randint'):
idx = random_state.randint(0, len(purple_colors))
# 念のため範囲チェック
idx = min(idx, len(purple_colors) - 1)
else:
# 整数などの場合
idx = hash(random_state) % len(purple_colors)
else:
# random_stateがない場合はランダムに選択
idx = random.randint(0, len(purple_colors) - 1)
color = purple_colors[idx]
return f"rgb({color[0]}, {color[1]}, {color[2]})"