From 42bf09610b46ced62f75cea762a1a0fbd4c00ac8 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Mon, 20 Jul 2026 15:39:34 +0200 Subject: [PATCH] Added binary file format --- BEdit/AGENTS.md | 3 + BEdit/README.md | 2 +- BEdit/pyproject.toml | 3 +- BEdit/src/bedit/core/libraries.py | 12 +- BEdit/src/bedit/core/serializer.py | 54 +++ BEdit/src/bedit/data/libraries/default.beb | Bin 0 -> 949 bytes BEdit/src/bedit/data/libraries/default.json | 435 ------------------ BEdit/src/bedit/gui/controllers/document.py | 6 +- BEdit/src/bedit/gui/dialogs/settings.py | 22 +- .../bedit/gui/generated/ui_settings_dialog.py | 2 +- BEdit/src/bedit/gui/main_window.py | 23 +- .../bedit/gui/models/library_repository.py | 3 +- BEdit/ui/settings_dialog.ui | 2 +- 13 files changed, 106 insertions(+), 461 deletions(-) create mode 100644 BEdit/src/bedit/data/libraries/default.beb delete mode 100644 BEdit/src/bedit/data/libraries/default.json diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index 930acc5..88a22af 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -162,6 +162,9 @@ runtime. A tiny generic prompt with one field and OK/Cancel may remain code-only ## Document and library files - Documents use the `bedit-document` JSON format. +- Documents can be stored as human-readable `.bedit.json`/`.json` through + `JsonDocumentSerializer`, or as compressed MessagePack `.beb` through + `BebDocumentSerializer`. UI document I/O dispatches via `DocumentSerializer`. - `test.bedit.json` is a useful manually created example during development. - Library documents use the same recursive document model. - Library parsing belongs in `core/libraries.py`; Qt change notifications belong diff --git a/BEdit/README.md b/BEdit/README.md index 794e7e8..41dc0b2 100644 --- a/BEdit/README.md +++ b/BEdit/README.md @@ -146,7 +146,7 @@ BEdit document used as a copy source. The built-in example defines A, B, and C. - File → Close Document removes the active document and returns to an empty workspace. An open graph uses a light gray grid whose visible and snapping spacing are configured separately. -- Edit → Settings → Libraries accepts document files or folders of JSON files. +- Edit → Settings → Libraries accepts JSON or `.beb` document files and folders. Every component owns its ports, declarative icon, properties, and child graph: diff --git a/BEdit/pyproject.toml b/BEdit/pyproject.toml index 58f9e45..43133fc 100644 --- a/BEdit/pyproject.toml +++ b/BEdit/pyproject.toml @@ -9,6 +9,7 @@ description = "A starter Qt desktop application" readme = "README.md" requires-python = ">=3.10" dependencies = [ + "msgpack>=1.0,<2", "PySide6>=6.7,<7", ] @@ -25,7 +26,7 @@ bedit = "bedit.gui.app:main" where = ["src"] [tool.setuptools.package-data] -bedit = ["data/libraries/*.json"] +bedit = ["data/libraries/*.json", "data/libraries/*.beb"] [tool.ruff] line-length = 100 diff --git a/BEdit/src/bedit/core/libraries.py b/BEdit/src/bedit/core/libraries.py index 0d3d51d..a5beb2f 100644 --- a/BEdit/src/bedit/core/libraries.py +++ b/BEdit/src/bedit/core/libraries.py @@ -1,8 +1,8 @@ -import json from dataclasses import dataclass from pathlib import Path from bedit.core.model import GraphDocument +from bedit.core.serializer import DocumentSerializer @dataclass(frozen=True) @@ -13,7 +13,7 @@ class LibraryDocument: def bundled_library_path() -> Path: - return Path(__file__).resolve().parents[1] / "data" / "libraries" / "default.json" + return Path(__file__).resolve().parents[1] / "data" / "libraries" / "default.beb" def default_library_paths() -> list[str]: @@ -21,12 +21,12 @@ def default_library_paths() -> list[str]: def load_library_file(path: Path) -> LibraryDocument: - with path.open(encoding="utf-8") as file: - data = json.load(file) - document = GraphDocument.from_dict(data) + document = DocumentSerializer.load(path) name = str(document.metadata.get("name") or path.stem) return LibraryDocument(name, document, str(path)) def library_candidates(path: Path) -> list[Path]: - return sorted(path.glob("*.json")) if path.is_dir() else [path] + if not path.is_dir(): + return [path] + return sorted((*path.glob("*.json"), *path.glob("*.beb"))) diff --git a/BEdit/src/bedit/core/serializer.py b/BEdit/src/bedit/core/serializer.py index e9e720a..fb592ac 100644 --- a/BEdit/src/bedit/core/serializer.py +++ b/BEdit/src/bedit/core/serializer.py @@ -1,6 +1,9 @@ import json +import zlib from pathlib import Path +import msgpack + from bedit.core.model import GraphDocument @@ -20,3 +23,54 @@ class JsonDocumentSerializer: json.dump(document.to_dict(), file, indent=2) file.write("\n") temporary_path.replace(path) + + +class BebDocumentSerializer: + """Compressed MessagePack serializer for the binary .beb format.""" + + MAGIC = b"BEB\x00" + VERSION = 1 + + @classmethod + def load(cls, path: Path) -> GraphDocument: + payload = path.read_bytes() + header = cls.MAGIC + bytes([cls.VERSION]) + if not payload.startswith(header): + raise ValueError("This is not a supported BEdit binary document") + try: + data = msgpack.unpackb(zlib.decompress(payload[len(header) :]), raw=False) + except (ValueError, zlib.error, msgpack.exceptions.MsgpackException) as error: + raise ValueError("The BEdit binary document is damaged") from error + if not isinstance(data, dict): + raise ValueError("The BEdit binary document has an invalid root value") + return GraphDocument.from_dict(data) + + @classmethod + def save(cls, document: GraphDocument, path: Path) -> None: + packed = msgpack.packb(document.to_dict(), use_bin_type=True) + payload = cls.MAGIC + bytes([cls.VERSION]) + zlib.compress(packed, level=9) + temporary_path = path.with_suffix(path.suffix + ".tmp") + temporary_path.write_bytes(payload) + temporary_path.replace(path) + + +class DocumentSerializer: + """Select the document serializer from its file extension.""" + + @staticmethod + def load(path: Path) -> GraphDocument: + serializer = ( + BebDocumentSerializer + if path.suffix.lower() == ".beb" + else JsonDocumentSerializer + ) + return serializer.load(path) + + @staticmethod + def save(document: GraphDocument, path: Path) -> None: + serializer = ( + BebDocumentSerializer + if path.suffix.lower() == ".beb" + else JsonDocumentSerializer + ) + serializer.save(document, path) diff --git a/BEdit/src/bedit/data/libraries/default.beb b/BEdit/src/bedit/data/libraries/default.beb new file mode 100644 index 0000000000000000000000000000000000000000..a467ebeb9ee3b72ca635bb8a24b96062396be27c GIT binary patch literal 949 zcmV;m14{ftMM3}pc-rk(%WfP+6ooBoK44HlBA|wT)bxXZ7>O(_MT#sOV1baR$E}%? zd#al1>Lkw2RumZ*Y0H(3pvSDE{qMnHMqX_#`GC%%!d=L z46fGamK`AtXpSjtjfBh!li$5%6FH^B?`K+NppAfh^MRmBt{&Lm zS)K2n4NJ3;!6+A9iPJy&MQTKr!WT+Ppp{qtc&rMufQdr_A&SEq(>=TXe4xJG2X*lo zL6udITf@AEGuYAAie)#2l8ZDQpW~={M?`4`dVDT$tG6_8-IQ0VpIqF4t>HDnOmDCA zeUow21JUi7`FsK?RGpIl@?#BVKS_nSvyXT>oP}Fm2sf+pA!NDHY8__Cc2XhXo7(;J zzos2gnbyok3d3Bbf<2&0OVD3pCW?Ho^LDfAry5y(unN@0BW0X{T3G0`-q_r&Olt)( z8sNzz(a)N?*9_Sz*M(#?%u%iiZ3(giKNR&^zH9rg^;v8E10gTm&#<;pEsOHIH*K5# zKA$_Ghf#p1y*gNLN~Qs)!>c$gVBj?z_k7}e#Pggu_G1U(#C32x^c*iiiN|OX20qjw z9x}l>XbVwb1E)-Oj}lC1%+C_p5ddHW(f?$B{mEXkkC{1<@BW$Q`_uP?v~7gbRzv*h z?7~~k)PDWuVzht0#)Vr%S|O`-Mxv$(q$!4)fB1 z+LcmCSje)paJ567^xmJhcMlwDmh{k}U%FgXLS;?oYunPB3t`l=8uUzZ#X}N9n`e5r?!Ra^b~U8MjGeMj(Ijcds7+s$P(3+DsIbpn2Kco8qFmVrwQ}}u zEq59)RA}0$)W5$SJfBJ}5PjuHwPxu%3+X$jL-fIiN44uE(fpl;-g(it*N<<-cP*}T XOIT{RyibJu=<=Z($ None: - self.document = JsonDocumentSerializer.load(path) + self.document = DocumentSerializer.load(path) self.active_component_id = next(iter(self.document.roots), None) self.file_path = path self.undo_stack.clear() @@ -124,7 +124,7 @@ class DocumentController(QObject): target = path or self.file_path if target is None: raise ValueError("No file path has been selected") - JsonDocumentSerializer.save(self.document, target) + DocumentSerializer.save(self.document, target) self.file_path = target self.undo_stack.setClean() self.filePathChanged.emit(target) diff --git a/BEdit/src/bedit/gui/dialogs/settings.py b/BEdit/src/bedit/gui/dialogs/settings.py index 201f4e2..9592ab1 100644 --- a/BEdit/src/bedit/gui/dialogs/settings.py +++ b/BEdit/src/bedit/gui/dialogs/settings.py @@ -3,7 +3,7 @@ from pathlib import Path from PySide6.QtCore import QSettings, Signal from PySide6.QtWidgets import QDialog, QFileDialog -from bedit.core.libraries import default_library_paths +from bedit.core.libraries import bundled_library_path, default_library_paths from bedit.gui.preferences import application_settings from bedit.gui.generated.ui_settings_dialog import Ui_SettingsDialog @@ -56,8 +56,22 @@ class SettingsDialog(QDialog): settings = settings if settings is not None else application_settings() value = settings.value("libraries/paths", default_library_paths()) if isinstance(value, str): - return [value] - return [str(path) for path in value] + paths = [value] + else: + paths = [str(path) for path in value] + bundled = bundled_library_path() + migrated = [ + str(bundled) + if not Path(path).exists() + and Path(path).parent == bundled.parent + and Path(path).suffix.lower() == ".json" + else path + for path in paths + ] + if migrated != paths: + settings.setValue("libraries/paths", migrated) + settings.sync() + return migrated @staticmethod def graph_grid_size(settings: QSettings | None = None) -> int: @@ -79,7 +93,7 @@ class SettingsDialog(QDialog): self, "Add library", "", - "BEdit libraries (*.json);;All files (*)", + "BEdit libraries (*.beb *.bedit.json *.json);;All files (*)", ) if path: self._append_unique_path(path) diff --git a/BEdit/src/bedit/gui/generated/ui_settings_dialog.py b/BEdit/src/bedit/gui/generated/ui_settings_dialog.py index 6c48978..9427d28 100644 --- a/BEdit/src/bedit/gui/generated/ui_settings_dialog.py +++ b/BEdit/src/bedit/gui/generated/ui_settings_dialog.py @@ -176,7 +176,7 @@ class Ui_SettingsDialog(object): self.iconGridLabel.setText(QCoreApplication.translate("SettingsDialog", u"Icon grid size:", None)) self.iconGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None)) self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None)) - self.libraryPathsLabel.setText(QCoreApplication.translate("SettingsDialog", u"Load library JSON files from these files or folders at startup:", None)) + self.libraryPathsLabel.setText(QCoreApplication.translate("SettingsDialog", u"Load library JSON or BEdit Binary files from these files or folders at startup:", None)) self.addLibraryFileButton.setText(QCoreApplication.translate("SettingsDialog", u"Add File\u2026", None)) self.addLibraryFolderButton.setText(QCoreApplication.translate("SettingsDialog", u"Add Folder\u2026", None)) self.removeLibraryPathButton.setText(QCoreApplication.translate("SettingsDialog", u"Remove", None)) diff --git a/BEdit/src/bedit/gui/main_window.py b/BEdit/src/bedit/gui/main_window.py index 9196206..844ef74 100644 --- a/BEdit/src/bedit/gui/main_window.py +++ b/BEdit/src/bedit/gui/main_window.py @@ -14,7 +14,7 @@ from PySide6.QtWidgets import ( from bedit.core.model import Component, Parameter from bedit.core.application_log import get_logger -from bedit.core.serializer import JsonDocumentSerializer +from bedit.core.serializer import DocumentSerializer from bedit.core.simulation import Simulation from bedit.gui.controllers.document import DocumentController from bedit.gui.dialogs.component_options import ComponentOptionsDialog @@ -407,7 +407,11 @@ class MainWindow(QMainWindow): if not self._resolve_source_edits() or not self._maybe_save(): return filename, _ = QFileDialog.getOpenFileName( - self, "Open graph", "", "BEdit graphs (*.bedit.json *.json);;All files (*)" + self, + "Open graph", + "", + "BEdit documents (*.bedit.json *.json *.beb);;" + "BEdit JSON (*.bedit.json *.json);;BEdit Binary (*.beb);;All files (*)", ) if not filename: return @@ -437,16 +441,21 @@ class MainWindow(QMainWindow): def save_document_as(self) -> bool: if self.document_controller.document is None: return False - filename, _ = QFileDialog.getSaveFileName( + filename, selected_filter = QFileDialog.getSaveFileName( self, "Save graph", - "untitled.bedit.json", - "BEdit graphs (*.bedit.json);;JSON files (*.json);;All files (*)", + "untitled.beb", + "BEdit Binary (*.beb);;BEdit JSON (*.bedit.json *.json);;All files (*)", ) if not filename: return False + path = Path(filename) + if path.suffix.lower() not in {".json", ".beb"}: + path = path.with_suffix( + ".beb" if "Binary" in selected_filter else ".bedit.json" + ) try: - path = self.document_controller.save(Path(filename)) + path = self.document_controller.save(path) self.log.info("Saved document %s", path) except OSError as error: self.log.error("Could not save document %s: %s", filename, error) @@ -557,7 +566,7 @@ class MainWindow(QMainWindow): try: if library is not None: library.document.validate() - JsonDocumentSerializer.save(library.document, Path(library.source_path)) + DocumentSerializer.save(library.document, Path(library.source_path)) except (OSError, ValueError) as error: component.inputs, component.outputs = old_inputs, old_outputs self.log.error("Could not change library ports: %s", error) diff --git a/BEdit/src/bedit/gui/models/library_repository.py b/BEdit/src/bedit/gui/models/library_repository.py index df37009..6abc5ad 100644 --- a/BEdit/src/bedit/gui/models/library_repository.py +++ b/BEdit/src/bedit/gui/models/library_repository.py @@ -1,4 +1,3 @@ -import json from pathlib import Path from PySide6.QtCore import QObject, Signal @@ -23,7 +22,7 @@ class LibraryRepository(QObject): for candidate in library_candidates(path): try: libraries.append(load_library_file(candidate)) - except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as error: + except (OSError, ValueError, KeyError, TypeError) as error: warnings.append(f"{candidate}: {error}") self.libraries = libraries self.load_warnings = warnings diff --git a/BEdit/ui/settings_dialog.ui b/BEdit/ui/settings_dialog.ui index ea38e60..1625988 100644 --- a/BEdit/ui/settings_dialog.ui +++ b/BEdit/ui/settings_dialog.ui @@ -94,7 +94,7 @@ - Load library JSON files from these files or folders at startup: + Load library JSON or BEdit Binary files from these files or folders at startup: true