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

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 

15 

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 

30 

31logger = logging.getLogger(__name__) 

32 

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 ) 

39 

40 

41CLIPBOARD_COPY_DPI = 150 

42 

43PRINT_RENDER_DPI = 200 

44 

45BATCH_EXPORT_FIGSIZE = (8, 6) 

46 

47 

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) 

58 

59 pixmap = QPixmap() 

60 pixmap.loadFromData(buf.read()) 

61 buf.close() 

62 

63 QApplication.clipboard().setPixmap(pixmap) 

64 

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() 

78 

79 dialog = CVDSimulationDialog(image, self) 

80 dialog.exec() 

81 

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 

88 

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) 

97 

98 pixmap = QPixmap() 

99 pixmap.loadFromData(buf.read()) 

100 buf.close() 

101 

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 

105 

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() 

120 

121 self.statusBar().showMessage("印刷を実行しました", 3000) 

122 

123 def _on_batch_export(self): 

124 """サブプロットごと、または複数のプロジェクトファイルをまとめて書き出す。別スレッドで1本の TaskRunner で行う。 

125 

126 rc_context はプロセス全体の rcParams を書き換えるので、並列にはしない。 

127 """ 

128 if self._batch_export_task_runner is not None: 

129 notify.information(self, "実行中", "別のバッチエクスポート処理が実行中です。完了までお待ちください。") 

130 return 

131 

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 

136 

137 options = dialog.get_common_options() 

138 if not options['output_dir']: 

139 notify.warning(self, "バッチエクスポート", "出力先フォルダを指定してください。") 

140 return 

141 

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 

160 

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) 

165 

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() 

173 

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 

179 

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}") 

184 

185 def _on_batch_export_succeeded(self, results, progress_dialog): 

186 """結果は [(出力ファイル名, エラーか None), ...]。キャンセルなら終わった分だけ。""" 

187 self._cleanup_batch_export_task_runner() 

188 progress_dialog.close() 

189 

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] 

192 

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件も終わらないうちにキャンセルされた 

195 

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) 

200 

201 def _save_figure_with_options(self, fig, out_path, options): 

202 """bbox_inches='tight' で保存する。rc の設定は export_rc_params(SVG の文字の扱い、PDF の TrueType 埋め込み)。 

203 

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 

214 

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) 

220 

221 def _batch_export_subplots(self, indices, options, report_progress=None, is_cancelled=None): 

222 """サブプロットを1枚ずつ書き出す。1x1 の一時キャンバスなので、subplot_target を 0 にした写しを渡す。 

223 

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 

254 

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 

282 

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 

296 

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' 

307 

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 

318 

319 self.statusBar().showMessage(f"Pythonスクリプトを書き出しました: {file_path}", 3000) 

320 

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) 

328 

329 dialog = CaptionGeneratorDialog(default_caption, default_label, self) 

330 dialog.exec() 

331 

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 "実験レポート" 

338 

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 

349 

350 methods_sections = collect_methods_sections(self.project) 

351 

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 

365 

366 self.statusBar().showMessage(f"実験レポートを書き出しました: {file_path}", 3000) 

367 

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 

371 

372 with mpl.rc_context(export_rc_params('pdf')), PdfPages(file_path) as pdf: 

373 pdf.savefig(self.canvas.fig, bbox_inches='tight') 

374 

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) 

387 

388 def _on_export_plot(self): 

389 dialog = ExportDialog(self) 

390 

391 dialog.preview_button.clicked.connect( 

392 lambda: self._generate_preview(dialog) 

393 ) 

394 

395 if dialog.exec() == QDialog.DialogCode.Accepted: 

396 options = dialog.get_options() 

397 

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 

407 

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 ) 

418 

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 

423 

424 # フル解像度なら間引かずに描き直してから保存する(finally で画面用に戻す) 

425 full_resolution = options.get('full_resolution', False) 

426 if full_resolution: 

427 self._update_plot(full_resolution=True) 

428 

429 original_size = self.canvas.fig.get_size_inches() 

430 

431 self.canvas.fig.set_size_inches(width_in, height_in) 

432 

433 try: 

434 file_ext = os.path.splitext(file_path)[1].lower() 

435 

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 

446 

447 save_kwargs = {'transparent': options.get('transparent', True)} 

448 save_kwargs['bbox_inches'] = 'tight' 

449 

450 if file_ext in ['.pdf', '.svg']: 

451 pass 

452 else: 

453 save_kwargs['dpi'] = options["dpi"] 

454 

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() 

466 

467 def _generate_preview(self, dialog): 

468 """今の軸だけを、書き出しの大きさの一時的な Figure に描いて見せる。""" 

469 

470 options = dialog.get_options() 

471 

472 width_in, height_in = self._calculate_size_in_inches(options) 

473 

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) 

483 

484 active_index = self.project.active_axis_index 

485 if active_index >= len(self.project.all_plot_settings): 

486 logger.warning("プレビュー生成時、アクティブな軸設定が見つかりません。") 

487 return 

488 

489 active_settings = self.project.all_plot_settings[active_index] 

490 

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) 

497 

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 

507 

508 try: 

509 fit_tight_layout(temp_fig) 

510 except ValueError: 

511 pass 

512 

513 buf = io.BytesIO() 

514 temp_fig.savefig(buf, format='png', dpi=dpi) 

515 buf.seek(0) 

516 

517 pixmap = QPixmap() 

518 pixmap.loadFromData(buf.read()) 

519 

520 dialog.preview_label.setPixmap( 

521 pixmap.scaled(dialog.preview_label.size(), 

522 Qt.AspectRatioMode.KeepAspectRatio, 

523 Qt.TransformationMode.SmoothTransformation) 

524 ) 

525 

526 buf.close() 

527 del temp_fig 

528 

529 def _calculate_size_in_inches(self, options): 

530 """ダイアログの幅と高さをインチにする。""" 

531 return size_in_inches(options["width"], options["height"], options["unit"], options["dpi"])