Coverage for graphica/gui/mixins/export_mixin.py: 89.5%
369 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""画像・PDF・SVG への書き出し、印刷、クリップボード、書き出しのプレビュー。"""
2import copy
3import datetime
4import io
5import os
6import dataclasses
7import logging
8import matplotlib as mpl
9from PySide6.QtCore import Qt
10from PySide6.QtGui import QPixmap, QPainter, QImage
11from PySide6.QtWidgets import QApplication, QDialog, QProgressDialog
12from PySide6.QtPrintSupport import QPrinter, QPrintDialog
13from matplotlib.figure import Figure
14from matplotlib.backends.backend_pdf import PdfPages
16from graphica.gui import notify
17from graphica.gui import dialog_dirs
18from graphica.core.axis_settings import axis_setting
19from graphica.gui.dialogs import ExportDialog, BatchExportDialog, CaptionGeneratorDialog, CVDSimulationDialog
20from graphica.gui.canvas import _HeadlessRenderCanvas, fit_tight_layout
21from graphica.gui.export_settings import export_rc_params
22from graphica.gui.export_size import confirm_output_size, preview_dpi, preview_too_large_text, size_in_inches
23from graphica.gui.task_runner import TaskRunner
24from graphica.models.project import ProjectModel
25from graphica.core.plugin_api import get_plugin_api, get_registered_exporters
26from graphica.core.plugin_types import PluginExecutionError
27from graphica.core.script_export import generate_python_script
28from graphica.core.caption_export import sanitize_label
29from graphica.core.report_export import collect_methods_sections, generate_html_report
31logger = logging.getLogger(__name__)
33def _project_has_raster_gradient_fill(project):
34 """グラデーションの塗り(imshow で描くのでベクター形式でもラスタになる)を使う系列があるか。"""
35 return any(
36 ds.plot_type == 'Area' and ds.gradient_enabled and ds.gradient_target in ('fill', 'both')
37 for ds in project.datasets
38 )
41CLIPBOARD_COPY_DPI = 150
43PRINT_RENDER_DPI = 200
45BATCH_EXPORT_FIGSIZE = (8, 6)
48class ExportMixin:
49 def _on_copy_plot_to_clipboard(self):
50 buf = io.BytesIO()
51 try:
52 self.canvas.fig.savefig(buf, format='png', dpi=CLIPBOARD_COPY_DPI, bbox_inches='tight')
53 except Exception as e:
54 logger.exception("グラフのクリップボードコピーに失敗しました。")
55 notify.warning(self, "コピーエラー", f"グラフのコピー中にエラーが発生しました:\n{e}")
56 return
57 buf.seek(0)
59 pixmap = QPixmap()
60 pixmap.loadFromData(buf.read())
61 buf.close()
63 QApplication.clipboard().setPixmap(pixmap)
65 def _on_show_cvd_simulation(self):
66 """今のグラフを1回だけ PNG にし、ダイアログはその画像を変換して見せる(グラフは描き直さない)。"""
67 buf = io.BytesIO()
68 try:
69 self.canvas.fig.savefig(buf, format='png', dpi=CLIPBOARD_COPY_DPI, bbox_inches='tight')
70 except Exception as e:
71 logger.exception("色覚シミュレーションプレビュー用の画像生成に失敗しました。")
72 notify.warning(self, "プレビューエラー", f"プレビュー画像の生成中にエラーが発生しました:\n{e}")
73 return
74 buf.seek(0)
75 image = QImage()
76 image.loadFromData(buf.read())
77 buf.close()
79 dialog = CVDSimulationDialog(image, self)
80 dialog.exec()
82 def _on_print_plot(self):
83 """PNG に描いてから QPrinter に貼る(書き出しと同じ描画結果になる)。"""
84 printer = QPrinter(QPrinter.PrinterMode.HighResolution)
85 print_dialog = QPrintDialog(printer, self)
86 if print_dialog.exec() != QDialog.DialogCode.Accepted:
87 return
89 buf = io.BytesIO()
90 try:
91 self.canvas.fig.savefig(buf, format='png', dpi=PRINT_RENDER_DPI, bbox_inches='tight')
92 except Exception as e:
93 logger.exception("印刷用画像の生成に失敗しました。")
94 notify.warning(self, "印刷エラー", f"印刷用の画像生成中にエラーが発生しました:\n{e}")
95 return
96 buf.seek(0)
98 pixmap = QPixmap()
99 pixmap.loadFromData(buf.read())
100 buf.close()
102 if pixmap.isNull(): 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 notify.warning(self, "印刷エラー", "印刷用の画像生成に失敗しました。")
104 return
106 painter = QPainter()
107 if not painter.begin(printer): 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 notify.warning(self, "印刷エラー", "プリンターへの描画を開始できませんでした。")
109 return
110 try:
111 page_rect = printer.pageRect(QPrinter.Unit.DevicePixel).toRect()
112 scaled = pixmap.scaled(
113 page_rect.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation
114 )
115 x = page_rect.x() + (page_rect.width() - scaled.width()) // 2
116 y = page_rect.y() + (page_rect.height() - scaled.height()) // 2
117 painter.drawPixmap(x, y, scaled)
118 finally:
119 painter.end()
121 self.statusBar().showMessage("印刷を実行しました", 3000)
123 def _on_batch_export(self):
124 """サブプロットごと、または複数のプロジェクトファイルをまとめて書き出す。別スレッドで1本の TaskRunner で行う。
126 rc_context はプロセス全体の rcParams を書き換えるので、並列にはしない。
127 """
128 if self._batch_export_task_runner is not None:
129 notify.information(self, "実行中", "別のバッチエクスポート処理が実行中です。完了までお待ちください。")
130 return
132 extra_formats = [exp.format_name for exp in get_registered_exporters()]
133 dialog = BatchExportDialog(len(self.project.all_plot_settings), self, extra_formats=extra_formats)
134 if dialog.exec() != QDialog.DialogCode.Accepted:
135 return
137 options = dialog.get_common_options()
138 if not options['output_dir']:
139 notify.warning(self, "バッチエクスポート", "出力先フォルダを指定してください。")
140 return
142 mode = dialog.get_mode()
143 if mode == "subplots":
144 indices = dialog.get_selected_subplot_indices()
145 if not indices:
146 notify.warning(self, "バッチエクスポート", "書き出すサブプロットを選択してください。")
147 return
148 items = indices
149 worker_fn = self._batch_export_subplots
150 # 別スレッドで描くので、画面のマウスで変えた範囲はここで写しておく
151 options['view_overrides'] = (
152 copy.deepcopy(self.canvas.view_overrides) if dialog.visible_range_checkbox.isChecked() else {})
153 else:
154 paths = dialog.get_project_file_paths()
155 if not paths:
156 notify.warning(self, "バッチエクスポート", "プロジェクトファイルを追加してください。")
157 return
158 items = paths
159 worker_fn = self._batch_export_project_files
161 progress_dialog = QProgressDialog("バッチエクスポートを実行中...", "キャンセル", 0, len(items), self)
162 progress_dialog.setWindowModality(Qt.WindowModality.WindowModal)
163 progress_dialog.setMinimumDuration(0)
164 progress_dialog.setValue(0)
166 runner = TaskRunner(worker_fn, items, options)
167 runner.progress.connect(lambda done, total, message: progress_dialog.setValue(done))
168 progress_dialog.canceled.connect(runner.requestInterruption)
169 runner.succeeded.connect(lambda results: self._on_batch_export_succeeded(results, progress_dialog))
170 runner.failed.connect(lambda msg: self._on_batch_export_failed(msg, progress_dialog))
171 self._batch_export_task_runner = runner
172 runner.start()
174 def _cleanup_batch_export_task_runner(self):
175 if self._batch_export_task_runner is not None: 175 ↛ exitline 175 didn't return from function '_cleanup_batch_export_task_runner' because the condition on line 175 was always true
176 self._batch_export_task_runner.wait()
177 self._batch_export_task_runner.deleteLater()
178 self._batch_export_task_runner = None
180 def _on_batch_export_failed(self, error_message, progress_dialog):
181 self._cleanup_batch_export_task_runner()
182 progress_dialog.close()
183 notify.warning(self, "バッチエクスポート", f"バッチエクスポート処理に失敗しました:\n{error_message}")
185 def _on_batch_export_succeeded(self, results, progress_dialog):
186 """結果は [(出力ファイル名, エラーか None), ...]。キャンセルなら終わった分だけ。"""
187 self._cleanup_batch_export_task_runner()
188 progress_dialog.close()
190 succeeded = [name for name, error in results if error is None]
191 failed = [(name, error) for name, error in results if error is not None]
193 if not succeeded and not failed: 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true
194 return # 1件も終わらないうちにキャンセルされた
196 message = f"{len(succeeded)}件を書き出しました。"
197 if failed:
198 message += "\n\n失敗:\n" + "\n".join(f"{name}: {error}" for name, error in failed)
199 notify.information(self, "バッチエクスポート完了", message)
201 def _save_figure_with_options(self, fig, out_path, options):
202 """bbox_inches='tight' で保存する。rc の設定は export_rc_params(SVG の文字の扱い、PDF の TrueType 埋め込み)。
204 形式がプラグインの書き出しの名前なら、プラグインの writer を呼ぶ。
205 """
206 api = get_plugin_api()
207 exporter = api.get_exporter(options['format']) if api is not None else None
208 if exporter is not None:
209 try:
210 exporter.writer(fig, out_path)
211 except Exception as e:
212 raise PluginExecutionError(exporter.name, f"「{out_path}」への書き出しに失敗しました: {e}") from e
213 return
215 save_kwargs = {'transparent': options.get('transparent', True), 'bbox_inches': 'tight'}
216 if options['format'] not in ('pdf', 'svg'):
217 save_kwargs['dpi'] = options['dpi']
218 with mpl.rc_context(export_rc_params(options['format'], options.get('svg_text_as_path', False))):
219 fig.savefig(out_path, **save_kwargs)
221 def _batch_export_subplots(self, indices, options, report_progress=None, is_cancelled=None):
222 """サブプロットを1枚ずつ書き出す。1x1 の一時キャンバスなので、subplot_target を 0 にした写しを渡す。
224 別スレッドで呼ばれるので Qt には触れない(_HeadlessRenderCanvas は QWidget ではない)。
225 キャンセルは1件ごとにしか見ない(1件の描画と保存は中断できない)。
226 """
227 results = []
228 total = len(indices)
229 for i, idx in enumerate(indices): 229 ↛ 253line 229 didn't jump to line 253 because the loop on line 229 didn't complete
230 if is_cancelled is not None and is_cancelled():
231 break
232 if report_progress is not None: 232 ↛ 233line 232 didn't jump to line 233 because the condition on line 232 was never true
233 report_progress(i, total, f"P{idx + 1}")
234 out_name = f"{options['prefix']}_P{idx + 1}.{options['format']}"
235 out_path = os.path.join(options['output_dir'], out_name)
236 try:
237 settings = self.project.all_plot_settings[idx]
238 datasets_for_axis = [
239 dataclasses.replace(ds, subplot_target=0)
240 for ds in self.project.datasets if ds.subplot_target == idx
241 ]
242 temp_canvas = _HeadlessRenderCanvas(width=BATCH_EXPORT_FIGSIZE[0], height=BATCH_EXPORT_FIGSIZE[1], dpi=options['dpi'])
243 temp_canvas.dark_mode = self.canvas.dark_mode
244 visible_range = options.get('view_overrides', {}).get(idx)
245 if visible_range: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 temp_canvas.view_overrides = {0: dict(visible_range)}
247 temp_canvas.redraw_all(datasets_for_axis, 1, 1, [settings], full_resolution=options.get('full_resolution', False))
248 self._save_figure_with_options(temp_canvas.fig, out_path, options)
249 results.append((out_name, None))
250 except Exception as e:
251 logger.exception("バッチエクスポート(サブプロット)に失敗しました: %s", out_name)
252 results.append((out_name, str(e)))
253 return results
255 def _batch_export_project_files(self, paths, options, report_progress=None, is_cancelled=None):
256 """プロジェクトファイルを1つずつ読み、その図を書き出す。開いているプロジェクトには触れない。別スレッドで呼ばれる。"""
257 results = []
258 total = len(paths)
259 for i, path in enumerate(paths): 259 ↛ 295line 259 didn't jump to line 295 because the loop on line 259 didn't complete
260 if is_cancelled is not None and is_cancelled():
261 break
262 if report_progress is not None: 262 ↛ 263line 262 didn't jump to line 263 because the condition on line 262 was never true
263 report_progress(i, total, os.path.basename(path))
264 base_name = os.path.splitext(os.path.basename(path))[0]
265 out_name = f"{options['prefix']}_{base_name}.{options['format']}"
266 out_path = os.path.join(options['output_dir'], out_name)
267 try:
268 temp_project = ProjectModel()
269 temp_project.load_project(path)
270 layout_mode = getattr(temp_project, 'layout_mode', 'grid')
271 if layout_mode == 'free': 271 ↛ 273line 271 didn't jump to line 273 because the condition on line 271 was never true
272 # 自由配置には行×列が無いので、サブプロットの数に応じた大きさにする
273 if not temp_project.all_plot_settings:
274 raise ValueError("有効なプロット設定が見つかりません")
275 rows, cols = 0, 0
276 fig_width, fig_height = BATCH_EXPORT_FIGSIZE[0] * 2, BATCH_EXPORT_FIGSIZE[1] * 2
277 else:
278 rows, cols = temp_project.layout_rows, temp_project.layout_cols
279 if rows * cols == 0 or not temp_project.all_plot_settings: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true
280 raise ValueError("有効なプロット設定が見つかりません")
281 fig_width, fig_height = BATCH_EXPORT_FIGSIZE[0] * cols, BATCH_EXPORT_FIGSIZE[1] * rows
283 temp_canvas = _HeadlessRenderCanvas(width=fig_width, height=fig_height, dpi=options['dpi'])
284 temp_canvas.dark_mode = self.canvas.dark_mode
285 temp_canvas.redraw_all(
286 temp_project.datasets, rows, cols, temp_project.all_plot_settings, layout_mode=layout_mode,
287 panel_labels_enabled=temp_project.panel_labels_enabled,
288 full_resolution=options.get('full_resolution', False),
289 )
290 self._save_figure_with_options(temp_canvas.fig, out_path, options)
291 results.append((out_name, None))
292 except Exception as e:
293 logger.exception("バッチエクスポート(プロジェクトファイル)に失敗しました: %s", path)
294 results.append((os.path.basename(path), str(e)))
295 return results
297 def _on_export_python_script(self):
298 """matplotlib だけで図を再現するスクリプトを書き出す(生成は core/script_export.py)。"""
299 file_path, _ = dialog_dirs.get_save_file_name(
300 self, dialog_dirs.EXPORT, "Pythonスクリプトとしてエクスポート", dialog_dirs.project_stem(self),
301 "Python Files (*.py)", preferred_dir=dialog_dirs.output_dir(self)
302 )
303 if not file_path:
304 return
305 if not file_path.endswith('.py'):
306 file_path += '.py'
308 # サブプロットの行数と列数は画面にしか無いことがある(保存の前)。古い値のままだと 2 つ目以降の軸が抜ける
309 self._sync_project_from_ui()
310 try:
311 script_text = generate_python_script(self.project)
312 with open(file_path, 'w', encoding='utf-8') as f:
313 f.write(script_text)
314 except Exception as e:
315 notify.warning(self, "保存エラー", f"スクリプトの書き出し中にエラーが発生しました:\n{e}")
316 logger.exception("Pythonスクリプトの書き出し中にエラー")
317 return
319 self.statusBar().showMessage(f"Pythonスクリプトを書き出しました: {file_path}", 3000)
321 def _on_generate_caption(self):
322 """今の軸のタイトルを初期値にキャプションのダイアログを出す(生成は core/caption_export.py)。"""
323 settings = {}
324 if 0 <= self.project.active_axis_index < len(self.project.all_plot_settings): 324 ↛ 326line 324 didn't jump to line 326 because the condition on line 324 was always true
325 settings = self.project.all_plot_settings[self.project.active_axis_index]
326 default_caption = axis_setting(settings, 'title') or ''
327 default_label = sanitize_label(default_caption)
329 dialog = CaptionGeneratorDialog(default_caption, default_label, self)
330 dialog.exec()
332 def _on_generate_report(self):
333 """グラフの画像と、履歴のある全データセットの方法の文を1つのレポートにする。形式は拡張子(.html / .pdf)で決まる。"""
334 settings = {}
335 if 0 <= self.project.active_axis_index < len(self.project.all_plot_settings): 335 ↛ 337line 335 didn't jump to line 337 because the condition on line 335 was always true
336 settings = self.project.all_plot_settings[self.project.active_axis_index]
337 title = axis_setting(settings, 'title') or "実験レポート"
339 file_path, _ = dialog_dirs.get_save_file_name(
340 self, dialog_dirs.EXPORT, "実験レポートを生成", dialog_dirs.project_stem(self),
341 "HTML Files (*.html);;PDF Files (*.pdf)", preferred_dir=dialog_dirs.output_dir(self)
342 )
343 if not file_path:
344 return
345 file_ext = os.path.splitext(file_path)[1].lower()
346 if file_ext not in ('.html', '.pdf'):
347 file_ext = '.html'
348 file_path += file_ext
350 methods_sections = collect_methods_sections(self.project)
352 try:
353 if file_ext == '.pdf':
354 self._write_pdf_report(file_path, title, methods_sections)
355 else:
356 buf = io.BytesIO()
357 self.canvas.fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
358 html_text = generate_html_report(buf.getvalue(), methods_sections, title=title)
359 with open(file_path, 'w', encoding='utf-8') as f:
360 f.write(html_text)
361 except Exception as e:
362 logger.exception("実験レポートの生成中にエラーが発生しました。")
363 notify.warning(self, "保存エラー", f"実験レポートの生成中にエラーが発生しました:\n{e}")
364 return
366 self.statusBar().showMessage(f"実験レポートを書き出しました: {file_path}", 3000)
368 def _write_pdf_report(self, file_path, title, methods_sections):
369 """1ページ目にグラフ、2ページ目にタイトルと方法の文。"""
370 from graphica.gui.mathtext_preview import JP_CAPABLE_FONT_FAMILIES
372 with mpl.rc_context(export_rc_params('pdf')), PdfPages(file_path) as pdf:
373 pdf.savefig(self.canvas.fig, bbox_inches='tight')
375 text_fig = Figure(figsize=(8.27, 11.69)) # A4 縦
376 text_fig.text(0.08, 0.95, title, fontsize=16, fontweight='bold', va='top',
377 family=JP_CAPABLE_FONT_FAMILIES)
378 body_lines = [f"生成日時: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}", "", "方法:"]
379 if methods_sections: 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true
380 for name, text in methods_sections:
381 body_lines.append(f"・{name}: {text}")
382 else:
383 body_lines.append("(処理履歴を持つデータセットはありません)")
384 text_fig.text(0.08, 0.88, "\n".join(body_lines), fontsize=10, va='top', wrap=True,
385 family=JP_CAPABLE_FONT_FAMILIES)
386 pdf.savefig(text_fig)
388 def _on_export_plot(self):
389 dialog = ExportDialog(self)
391 dialog.preview_button.clicked.connect(
392 lambda: self._generate_preview(dialog)
393 )
395 if dialog.exec() == QDialog.DialogCode.Accepted:
396 options = dialog.get_options()
398 filter_parts = ["PNG (*.png)", "PDF (*.pdf)", "SVG (*.svg)"]
399 for exp in get_registered_exporters():
400 filter_parts.append(f"{exp.format_name} (*{exp.extension})")
401 file_path, _ = dialog_dirs.get_save_file_name(
402 self, dialog_dirs.EXPORT, "プロットを保存", dialog_dirs.project_stem(self), ";;".join(filter_parts),
403 preferred_dir=dialog_dirs.output_dir(self)
404 )
405 if not file_path:
406 return
408 # グラデーションの塗りはベクター形式でもラスタで埋め込まれる。書き出しは続け、知らせるだけ
409 export_ext = os.path.splitext(file_path)[1].lower()
410 if export_ext in ('.svg', '.pdf') and _project_has_raster_gradient_fill(self.project):
411 notify.warning(
412 self, "ベクター出力時の注意",
413 "グラデーション塗りが有効なデータセットが含まれています。\n"
414 "この部分は画像(ラスタ)として埋め込まれるため、拡大すると"
415 "他の要素のようにはくっきり表示されません。\n\n"
416 "エクスポートは続行します。"
417 )
419 width_in, height_in = self._calculate_size_in_inches(options)
420 if export_ext not in ('.pdf', '.svg') and not confirm_output_size(
421 self, width_in, height_in, options["dpi"]):
422 return
424 # フル解像度なら間引かずに描き直してから保存する(finally で画面用に戻す)
425 full_resolution = options.get('full_resolution', False)
426 if full_resolution:
427 self._update_plot(full_resolution=True)
429 original_size = self.canvas.fig.get_size_inches()
431 self.canvas.fig.set_size_inches(width_in, height_in)
433 try:
434 file_ext = os.path.splitext(file_path)[1].lower()
436 api = get_plugin_api()
437 exporter = api.get_exporter_for_extension(file_ext) if api is not None else None
438 if exporter is not None:
439 try:
440 exporter.writer(self.canvas.fig, file_path)
441 except Exception as e:
442 raise PluginExecutionError(
443 exporter.name, f"「{file_path}」への書き出しに失敗しました: {e}"
444 ) from e
445 return
447 save_kwargs = {'transparent': options.get('transparent', True)}
448 save_kwargs['bbox_inches'] = 'tight'
450 if file_ext in ['.pdf', '.svg']:
451 pass
452 else:
453 save_kwargs['dpi'] = options["dpi"]
455 with mpl.rc_context(export_rc_params(file_ext, options.get('svg_text_as_path', False))):
456 self.canvas.fig.savefig(file_path, **save_kwargs)
457 except Exception as e:
458 logger.exception("エクスポートに失敗しました")
459 notify.warning(self, "保存エラー", f"エクスポート中にエラーが発生しました:\n{e}")
460 finally:
461 # 失敗しても画面の大きさに戻す
462 self.canvas.fig.set_size_inches(original_size)
463 if full_resolution:
464 self._update_plot(full_resolution=False)
465 self.canvas.draw_idle()
467 def _generate_preview(self, dialog):
468 """今の軸だけを、書き出しの大きさの一時的な Figure に描いて見せる。"""
470 options = dialog.get_options()
472 width_in, height_in = self._calculate_size_in_inches(options)
474 # 書き出しの大きさのまま描くと、大きな値で固まる。欄に合う解像度で描く
475 dpi = preview_dpi(width_in, height_in, 100,
476 dialog.preview_label.width(), dialog.preview_label.height())
477 if dpi is None:
478 dialog.preview_label.setPixmap(QPixmap())
479 dialog.preview_label.setText(preview_too_large_text(width_in, height_in, options["dpi"]))
480 return
481 temp_fig = Figure(figsize=(width_in, height_in), dpi=dpi)
482 temp_ax = temp_fig.add_subplot(111)
484 active_index = self.project.active_axis_index
485 if active_index >= len(self.project.all_plot_settings):
486 logger.warning("プレビュー生成時、アクティブな軸設定が見つかりません。")
487 return
489 active_settings = self.project.all_plot_settings[active_index]
491 # _draw_data は第2Y軸をこの一時的な Figure に作って all_secondary_axes に入れる。
492 # 失敗しても必ず戻さないと、画面の第2Y軸がプレビューの軸を指したままになる。
493 original_secondary = self.canvas.all_secondary_axes.copy()
494 try:
495 while len(self.canvas.all_secondary_axes) <= active_index: 495 ↛ 496line 495 didn't jump to line 496 because the condition on line 495 was never true
496 self.canvas.all_secondary_axes.append(None)
498 self.canvas._draw_data(
499 temp_ax, active_index, self.project.datasets,
500 full_resolution=options.get('full_resolution', False),
501 )
502 self.canvas._apply_appearance(temp_ax, active_index, active_settings)
503 except Exception:
504 logger.exception("エクスポートのプレビューを描画できませんでした")
505 finally:
506 self.canvas.all_secondary_axes = original_secondary
508 try:
509 fit_tight_layout(temp_fig)
510 except ValueError:
511 pass
513 buf = io.BytesIO()
514 temp_fig.savefig(buf, format='png', dpi=dpi)
515 buf.seek(0)
517 pixmap = QPixmap()
518 pixmap.loadFromData(buf.read())
520 dialog.preview_label.setPixmap(
521 pixmap.scaled(dialog.preview_label.size(),
522 Qt.AspectRatioMode.KeepAspectRatio,
523 Qt.TransformationMode.SmoothTransformation)
524 )
526 buf.close()
527 del temp_fig
529 def _calculate_size_in_inches(self, options):
530 """ダイアログの幅と高さをインチにする。"""
531 return size_in_inches(options["width"], options["height"], options["unit"], options["dpi"])