Source code for GUIBRUSHR.core.functions.figure_pickle

"""Helpers to persist matplotlib figures for interactive reload in the GUI.

Heavy plots are rendered in headless background subprocesses that only write
PNG/PDF to disk, so the live matplotlib ``Figure`` never reaches the GUI process.
To restore zoom/pan, each subprocess dumps the figure next to its PNG with
``dump_figure``; ``MyFigure`` then loads it and embeds an interactive canvas
(falling back to the static PNG when the dump is absent or unreadable).

Kept dependency-light (only ``pickle`` / ``os``) so subprocesses can import it
without pulling in the Tk backend.
"""

import os
import pickle

# Suffix appended to a PNG path to locate its pickled figure. Must stay in sync
# with the loader in GUIBRUSHR/GUI/WIDGET/MyFigure.py.
FIGPKL_SUFFIX = ".figpkl"

# Suffix used to store a list of figures (the pages of a multi-page PDF) next to
# that PDF. Must stay in sync with the loader in GUI/WIDGET/MyMultiFigure.py.
FIGS_SUFFIX = ".figs.pkl"


[docs] def figpkl_path(png_path) -> str: """Return the figpkl path that sits next to ``png_path``.""" return str(png_path) + FIGPKL_SUFFIX
[docs] def figs_path(pdf_path) -> str: """Return the multi-figure pickle path for a PDF (``.pdf`` -> ``.figs.pkl``).""" text = str(pdf_path) if text.endswith(".pdf"): return text[: -len(".pdf")] + FIGS_SUFFIX return text + FIGS_SUFFIX
[docs] def dump_figure(fig, png_path) -> None: """Pickle a matplotlib Figure next to its PNG for interactive reload. Best-effort: any failure (an unpicklable figure, a read-only folder) is swallowed so it can never break the PNG/PDF generation that callers rely on. Args: fig: The matplotlib Figure to persist. png_path: Path of the PNG the figure was saved to; the pickle is written alongside it with the ``.figpkl`` suffix. """ try: with open(figpkl_path(png_path), "wb") as handle: pickle.dump(fig, handle) except Exception: pass
[docs] def load_figure(png_path): """Load a pickled figure for ``png_path`` if present, else return ``None``. Best-effort: returns ``None`` on a missing file or any unpickling error so the caller can fall back to the static image. """ path = figpkl_path(png_path) if not os.path.exists(path): return None try: with open(path, "rb") as handle: return pickle.load(handle) except Exception: return None
[docs] def save_figure_blobs(blobs, pdf_path) -> None: """Persist a list of already-pickled figure byte-blobs next to a PDF. The on-disk ``.figs.pkl`` format is a list of ``pickle.dumps(figure)`` blobs. Storing bytes (rather than live figures) is deliberate: unpickling a figure re-registers it with pyplot's figure manager, so a retained live copy would be wiped by a later ``plt.clf()`` in the page-producing code. Bytes captured at draw time are an immutable snapshot, immune to that. Best-effort: any failure is swallowed so it can never break the PDF. """ try: with open(figs_path(pdf_path), "wb") as handle: pickle.dump(list(blobs), handle) except Exception: pass
[docs] def dump_figures(figures, pdf_path) -> None: """Serialize each figure to bytes immediately, then persist the blob list. Convenience for callers holding stable live figures. Each figure is pickled to bytes right away (see ``save_figure_blobs`` for why bytes). """ blobs = [] for figure in figures: try: blobs.append(pickle.dumps(figure)) except Exception: pass save_figure_blobs(blobs, pdf_path)
[docs] def load_figures(pdf_path): """Load the figures for ``pdf_path`` (unpickling each blob), else ``None``. Best-effort: returns ``None`` on a missing file or any unpickling error so the caller can fall back to the static PDF viewer. """ path = figs_path(pdf_path) if not os.path.exists(path): return None try: with open(path, "rb") as handle: blobs = pickle.load(handle) except Exception: return None figures = [] for blob in blobs or []: try: figures.append(pickle.loads(blob) if isinstance(blob, (bytes, bytearray)) else blob) except Exception: pass return figures or None