From f530cb5c7d5e0d0e68332df4b8491ea31a9fd38b Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Sat, 24 Oct 2026 08:55:10 +0200 Subject: [PATCH 1/3] fix(h5): keep the previous workspace file when saving fails Saving a workspace opened the destination file in write mode, which truncated it, then serialized the panels and the provenance: an error during serialization, a full disk or an interruption destroyed the last valid version of the file (the workspace stayed marked as modified, but the file on disk was already lost). Workspaces (and capsules, which save one) and .dlhist files are now written to a temporary file of the same directory, which replaces the destination only once completely written and closed (atomic_native_writer). On failure, the temporary file is removed and the previous file is left untouched. The permissions of an existing file are kept. Assisted-by: Claude Opus 5.5 --- datalab/gui/h5io.py | 13 +- datalab/h5/history.py | 4 +- datalab/h5/native.py | 57 +++++++ .../features/hdf5/atomic_save_unit_test.py | 160 ++++++++++++++++++ 4 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 datalab/tests/features/hdf5/atomic_save_unit_test.py diff --git a/datalab/gui/h5io.py b/datalab/gui/h5io.py index 473b37dc2..dee5129ed 100644 --- a/datalab/gui/h5io.py +++ b/datalab/gui/h5io.py @@ -27,7 +27,7 @@ from datalab.env import execenv from datalab.gui.panel.base import H5ImportBatch from datalab.h5 import H5Importer -from datalab.h5.native import NativeH5Reader, NativeH5Writer +from datalab.h5.native import NativeH5Reader, atomic_native_writer from datalab.utils.qthelpers import create_progress_bar, qt_try_loadsave_file if TYPE_CHECKING: @@ -80,14 +80,15 @@ def __progbartitle(fname: str) -> str: return _("Loading data from %s...") % osp.basename(fname) def save_file(self, filename: str) -> None: - """Save all signals and images from DataLab model into a HDF5 file""" - writer = NativeH5Writer(filename) - try: + """Save all signals and images from DataLab model into a HDF5 file + + The file is replaced only once completely written: if saving fails, the + previous file (if any) is left untouched. + """ + with atomic_native_writer(filename) as writer: for panel in self.mainwindow.panels: panel.serialize_to_hdf5(writer) self.mainwindow.provenance.save(writer.h5) - finally: - writer.close() @staticmethod def __read_provenance( diff --git a/datalab/h5/history.py b/datalab/h5/history.py index 7eb9c1d59..5d3d66603 100644 --- a/datalab/h5/history.py +++ b/datalab/h5/history.py @@ -13,7 +13,7 @@ from datalab.config import Conf, _ from datalab.gui.panel.base import H5ImportBatch -from datalab.h5.native import NativeH5Reader, NativeH5Writer +from datalab.h5.native import NativeH5Reader, NativeH5Writer, atomic_native_writer from datalab.history import HistorySession from datalab.objectmodel import get_uuid from datalab.utils.qthelpers import qt_try_loadsave_file, save_restore_stds @@ -53,7 +53,7 @@ def save_to_dlhist_file(panel: HistoryPanel, filename: str | None = None) -> boo filename += ".dlhist" with qt_try_loadsave_file(panel.parentWidget(), filename, "save"): Conf.base_dir.set(filename) - with NativeH5Writer(filename) as writer: + with atomic_native_writer(filename) as writer: # Make the .dlhist file panel-contained: store the signal and # image panel objects (all of them) alongside the history, so # that reopening restores both the data objects and the history diff --git a/datalab/h5/native.py b/datalab/h5/native.py index 5004c5774..0c51b6e8c 100644 --- a/datalab/h5/native.py +++ b/datalab/h5/native.py @@ -8,7 +8,13 @@ from __future__ import annotations +import contextlib import importlib +import os +import os.path as osp +import stat +import tempfile +from collections.abc import Generator from typing import Any, Callable from guidata.io import HDF5Reader, HDF5Writer @@ -73,6 +79,57 @@ def write(self, val: Any, group_name: str | None = None) -> None: raise +def _set_final_permissions(tmpname: str, filename: str) -> None: + """Give the temporary file the permissions the final file must have. + + Args: + tmpname: Temporary file name (created with owner-only permissions). + filename: Final file name: its permissions are kept if it exists, + otherwise those of a newly created file are applied. + """ + if osp.exists(filename): + mode = stat.S_IMODE(os.stat(filename).st_mode) + else: + umask = os.umask(0) + os.umask(umask) + mode = 0o666 & ~umask + os.chmod(tmpname, mode) + + +@contextlib.contextmanager +def atomic_native_writer(filename: str) -> Generator[NativeH5Writer, None, None]: + """Write a native DataLab HDF5 file without losing the previous one on failure. + + The file is written to a temporary file of the same directory, which replaces + ``filename`` only once it has been completely written and closed. If writing + fails (serialization error, full disk, interruption...), the temporary file is + removed and the previous file, if any, is left untouched. + + Args: + filename: HDF5 file name + + Yields: + Writer of the temporary file + """ + filename = osp.abspath(filename) + fd, tmpname = tempfile.mkstemp( + prefix=f".{osp.basename(filename)}.", suffix=".tmp", dir=osp.dirname(filename) + ) + os.close(fd) + try: + writer = NativeH5Writer(tmpname) + try: + yield writer + finally: + writer.close() + _set_final_permissions(tmpname, filename) + os.replace(tmpname, filename) + except BaseException: + with contextlib.suppress(OSError): + os.remove(tmpname) + raise + + class NativeH5Reader(HDF5Reader): """DataLab signal/image objects HDF5 guidata dataset Writer class diff --git a/datalab/tests/features/hdf5/atomic_save_unit_test.py b/datalab/tests/features/hdf5/atomic_save_unit_test.py new file mode 100644 index 000000000..fd366d167 --- /dev/null +++ b/datalab/tests/features/hdf5/atomic_save_unit_test.py @@ -0,0 +1,160 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +""" +Atomic HDF5 save unit tests +--------------------------- + +A failed save must never destroy the previous version of a workspace (or +``.dlhist``) file: the file is written to a temporary file of the same directory, +which replaces the previous file only once completely written. +""" + +# guitest: show + +from __future__ import annotations + +import os +import os.path as osp +from types import SimpleNamespace + +import h5py +import numpy as np +import pytest +from sigima.tests.data import create_paracetamol_signal + +from datalab.gui.h5io import H5InputOutput +from datalab.h5.native import DATALAB_VERSION_NAME, atomic_native_writer +from datalab.tests import datalab_test_app_context, helpers + + +def _write_previous_file(filename: str) -> bytes: + """Write a previous version of a file and return its contents.""" + with h5py.File(filename, "w") as h5file: + h5file["original"] = np.arange(5.0) + with open(filename, "rb") as fdesc: + return fdesc.read() + + +def _read_bytes(filename: str) -> bytes: + """Return the contents of a file.""" + with open(filename, "rb") as fdesc: + return fdesc.read() + + +def _assert_no_temporary_file(dirname: str, filename: str) -> None: + """Check that no temporary file was left next to the saved file.""" + assert sorted(os.listdir(dirname)) == [osp.basename(filename)] + + +def test_atomic_writer_replaces_file_once_written(tmp_path) -> None: + """A successful write replaces the previous file.""" + filename = str(tmp_path / "workspace.h5") + _write_previous_file(filename) + with atomic_native_writer(filename) as writer: + writer.h5["partial"] = np.arange(3.0) + assert not osp.samefile(writer.filename, filename) + with h5py.File(filename, "r") as h5file: + assert set(h5file) == {DATALAB_VERSION_NAME, "partial"} + _assert_no_temporary_file(str(tmp_path), filename) + + +def test_atomic_writer_creates_new_file(tmp_path) -> None: + """A new file is created with the permissions of a newly created file.""" + filename = str(tmp_path / "new.h5") + reference = str(tmp_path / "reference") + with open(reference, "wb"): + pass + with atomic_native_writer(filename) as writer: + writer.h5["data"] = np.arange(3.0) + assert os.stat(filename).st_mode == os.stat(reference).st_mode + os.remove(reference) + _assert_no_temporary_file(str(tmp_path), filename) + + +def test_atomic_writer_keeps_previous_file_on_failure(tmp_path) -> None: + """A failed write leaves the previous file untouched and no temporary file.""" + filename = str(tmp_path / "workspace.h5") + previous = _write_previous_file(filename) + with pytest.raises(OSError, match="disk full"): + with atomic_native_writer(filename) as writer: + writer.h5["partial"] = np.arange(3.0) + raise OSError("disk full") + assert _read_bytes(filename) == previous + _assert_no_temporary_file(str(tmp_path), filename) + + +class _Panel: + """Panel double writing one dataset, then optionally failing.""" + + def __init__(self, name: str, fail: bool = False) -> None: + self.name = name + self.fail = fail + + def serialize_to_hdf5(self, writer) -> None: + """Serialize the panel, like ``BaseDataPanel.serialize_to_hdf5``.""" + writer.h5[self.name] = np.arange(3.0) + if self.fail: + raise OSError("write error") + + +class _Provenance: + """Provenance double, optionally failing.""" + + def __init__(self, fail: bool = False) -> None: + self.fail = fail + + def save(self, h5file: h5py.File) -> None: + """Save the provenance block, like ``ProvenanceService.save``.""" + h5file["provenance"] = np.arange(2.0) + if self.fail: + raise ValueError("provenance error") + + +@pytest.mark.parametrize( + "panels, provenance", + [ + ([_Panel("signals"), _Panel("images", fail=True)], _Provenance()), + ([_Panel("signals"), _Panel("images")], _Provenance(fail=True)), + ], + ids=["panel", "provenance"], +) +def test_save_file_keeps_previous_workspace_on_failure( + tmp_path, panels: list[_Panel], provenance: _Provenance +) -> None: + """Regression: saving truncated the previous workspace file before writing, + so that an error in a panel or in the provenance destroyed it.""" + filename = str(tmp_path / "workspace.h5") + previous = _write_previous_file(filename) + h5io = H5InputOutput(SimpleNamespace(panels=panels, provenance=provenance)) + with pytest.raises((OSError, ValueError)): + h5io.save_file(filename) + assert _read_bytes(filename) == previous + _assert_no_temporary_file(str(tmp_path), filename) + + +def test_failed_save_keeps_workspace_modified(monkeypatch) -> None: + """After a failed save, the previous file is intact and the workspace is still + marked as modified.""" + with helpers.WorkdirRestoringTempDir() as tmpdir: + filename = osp.join(tmpdir, "workspace.h5") + previous = _write_previous_file(filename) + with datalab_test_app_context(console=False) as win: + win.signalpanel.add_object(create_paracetamol_signal()) + assert win.is_modified() + + def fail(_writer) -> None: + raise OSError("write error") + + monkeypatch.setattr(win.imagepanel, "serialize_to_hdf5", fail) + with pytest.raises(OSError, match="write error"): + win.save_h5_workspace(filename) + assert win.is_modified() + assert _read_bytes(filename) == previous + monkeypatch.undo() + win.save_h5_workspace(filename) + assert not win.is_modified() + assert not [name for name in os.listdir(tmpdir) if name.endswith(".tmp")] + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 34ad31c99ed4aca6bcb9a4c3cf658dfa233d0b33 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Sat, 24 Oct 2026 08:56:47 +0200 Subject: [PATCH 2/3] fix(webapi): keep non-uniform image coordinates in NPZ transfers The NPZ format of the Web API stored the pixels of an image and its x0, y0, dx and dy, but not non-uniform coordinates: an image with non-uniform axes uploaded to or downloaded from DataLab was rebuilt with default uniform coordinates, so that remote processing and measurements used another geometry than the original. Image archives now record is_uniform_coords and, for non-uniform images, the pixel center coordinates in xcoords.npy and ycoords.npy. Their lengths are checked against the image shape. Archives written by earlier versions (without the flag) are still read with uniform coordinates, and earlier readers ignore the new entries. The format is documented in the module and in the Web API guide. Assisted-by: Claude Opus 5.5 --- datalab/tests/webapi_extra_test.py | 93 ++++++++++++++++++++++++++++++ datalab/webapi/serialization.py | 33 +++++++++-- doc/features/advanced/webapi.rst | 5 +- 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/datalab/tests/webapi_extra_test.py b/datalab/tests/webapi_extra_test.py index 1390964f3..42553a183 100644 --- a/datalab/tests/webapi_extra_test.py +++ b/datalab/tests/webapi_extra_test.py @@ -363,6 +363,76 @@ def test_image_obj_metadata_with_numpy_origin(self) -> None: assert result.x0 == pytest.approx(1.5) assert result.dx == pytest.approx(0.1) + def test_image_non_uniform_coords_round_trip(self) -> None: + """Non-uniform image coordinates survive a round trip. + + Regression: they were lost, the image being rebuilt with uniform + coordinates (its pixels survived, but not their physical positions). + """ + obj = _make_image(shape=(3, 4)) + obj.xunit, obj.yunit = "mm", "s" + obj.set_coords(np.array([1.0, 2.0, 4.0, 8.0]), np.array([10.0, 11.0, 15.0])) + result = deserialize_object_from_npz(serialize_object_to_npz(obj)) + assert not result.is_uniform_coords + np.testing.assert_array_equal(result.xcoords, obj.xcoords) + np.testing.assert_array_equal(result.ycoords, obj.ycoords) + np.testing.assert_array_equal(result.data, obj.data) + assert (result.xunit, result.yunit) == ("mm", "s") + + def test_image_uniform_coords_round_trip(self) -> None: + """Uniform images are stored without coordinate arrays.""" + obj = _make_image(shape=(3, 4)) + obj.set_uniform_coords(0.5, 2.0, -1.0, 3.0) + data = serialize_object_to_npz(obj) + with zipfile.ZipFile(io.BytesIO(data)) as zf: + assert sorted(zf.namelist()) == ["data.npy", "metadata.json"] + assert json.loads(zf.read("metadata.json"))["is_uniform_coords"] is True + result = deserialize_object_from_npz(data) + assert result.is_uniform_coords + assert (result.dx, result.dy, result.x0, result.y0) == (0.5, 2.0, -1.0, 3.0) + + def test_image_archive_without_coords_flag_is_uniform(self) -> None: + """Archives written by earlier versions (no ``is_uniform_coords``) are read + with uniform coordinates.""" + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + arr = io.BytesIO() + np.save(arr, np.zeros((3, 4))) + zf.writestr("data.npy", arr.getvalue()) + metadata = {"type": "image", "x0": 1.0, "y0": 2.0, "dx": 0.5, "dy": 0.25} + zf.writestr("metadata.json", json.dumps(metadata)) + result = deserialize_object_from_npz(buf.getvalue()) + assert result.is_uniform_coords + assert (result.dx, result.dy, result.x0, result.y0) == (0.5, 0.25, 1.0, 2.0) + + @pytest.mark.parametrize( + "xcoords, ycoords, match", + [ + (None, np.arange(3.0), "missing xcoords.npy"), + (np.arange(3.0), np.arange(3.0), "do not match image shape"), + (np.arange(4.0), np.arange(4.0), "do not match image shape"), + ], + ) + def test_image_invalid_non_uniform_coords_raise( + self, xcoords, ycoords, match: str + ) -> None: + """Missing or mismatched coordinate arrays are rejected.""" + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + for name, array in ( + ("data.npy", np.zeros((3, 4))), + ("xcoords.npy", xcoords), + ("ycoords.npy", ycoords), + ): + if array is not None: + arr = io.BytesIO() + np.save(arr, array) + zf.writestr(name, arr.getvalue()) + metadata = {"type": "image", "is_uniform_coords": False} + zf.writestr("metadata.json", json.dumps(metadata)) + with pytest.raises(ValueError, match=match): + deserialize_object_from_npz(buf.getvalue()) + # ============================================================================= # Routes integration tests @@ -638,6 +708,29 @@ def test_get_data_missing_object(self, api_client) -> None: response = client.get("/api/v1/objects/nope/data", headers=_auth(token)) assert response.status_code == 404 + def test_non_uniform_image_round_trip_through_endpoints(self, api_client) -> None: + """An image with non-uniform coordinates keeps them when uploaded with + ``PUT data`` and downloaded with ``GET data``.""" + client, token, adapter = api_client + obj = _make_image("NU", shape=(3, 4)) + xcoords, ycoords = np.array([1.0, 2.0, 4.0, 8.0]), np.array([10.0, 11.0, 15.0]) + obj.set_coords(xcoords, ycoords) + response = client.put( + "/api/v1/objects/NU/data", + content=serialize_object_to_npz(obj), + headers={**_auth(token), "Content-Type": "application/x-npz"}, + ) + assert response.status_code == 201 + stored = adapter.get_object("NU") + assert not stored.is_uniform_coords + np.testing.assert_array_equal(stored.xcoords, xcoords) + response = client.get("/api/v1/objects/NU/data", headers=_auth(token)) + assert response.status_code == 200 + result = deserialize_object_from_npz(response.content) + assert not result.is_uniform_coords + np.testing.assert_array_equal(result.xcoords, xcoords) + np.testing.assert_array_equal(result.ycoords, ycoords) + def test_put_creates_object(self, api_client) -> None: """``PUT data`` creates a new object from an NPZ payload.""" client, token, adapter = api_client diff --git a/datalab/webapi/serialization.py b/datalab/webapi/serialization.py index e0531012f..b593f21dd 100644 --- a/datalab/webapi/serialization.py +++ b/datalab/webapi/serialization.py @@ -24,12 +24,17 @@ For ImageObj: - ``data.npy``: Image data (2D array, preserves dtype) + - ``xcoords.npy``, ``ycoords.npy``: Pixel center coordinates along X and Y + (1D float arrays, only for non-uniform coordinates) - ``metadata.json``: JSON-encoded metadata Metadata JSON includes: - ``type``: "signal" or "image" - ``title``, ``xlabel``, ``ylabel``, etc. - ``x0``, ``y0``, ``dx``, ``dy`` (for images) + - ``is_uniform_coords`` (for images): False when the coordinates are given by + ``xcoords.npy`` and ``ycoords.npy``. Archives without this key (written by + earlier versions) have uniform coordinates. """ from __future__ import annotations @@ -214,6 +219,7 @@ def _serialize_image(obj, buffer: io.BytesIO, *, compress: bool = True) -> None: "y0": _to_python_scalar(obj.y0), "dx": _to_python_scalar(obj.dx), "dy": _to_python_scalar(obj.dy), + "is_uniform_coords": bool(obj.is_uniform_coords), } # Include object metadata (contains Geometry_*, Table_* results) obj_metadata = obj.metadata @@ -224,6 +230,9 @@ def _serialize_image(obj, buffer: io.BytesIO, *, compress: bool = True) -> None: compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED with zipfile.ZipFile(buffer, "w", compression) as zf: _write_array_to_zip(zf, "data.npy", obj.data) + if not obj.is_uniform_coords: + _write_array_to_zip(zf, "xcoords.npy", obj.xcoords) + _write_array_to_zip(zf, "ycoords.npy", obj.ycoords) zf.writestr("metadata.json", json.dumps(metadata, ensure_ascii=False)) @@ -330,10 +339,26 @@ def _deserialize_image(zf: zipfile.ZipFile, metadata: dict) -> DataObject: obj.zunit = metadata["zunit"] # Set coordinate info - obj.x0 = metadata.get("x0", 0.0) - obj.y0 = metadata.get("y0", 0.0) - obj.dx = metadata.get("dx", 1.0) - obj.dy = metadata.get("dy", 1.0) + if metadata.get("is_uniform_coords", True): + obj.x0 = metadata.get("x0", 0.0) + obj.y0 = metadata.get("y0", 0.0) + obj.dx = metadata.get("dx", 1.0) + obj.dy = metadata.get("dy", 1.0) + else: + xcoords = _read_array_from_zip(zf, "xcoords.npy") + ycoords = _read_array_from_zip(zf, "ycoords.npy") + if xcoords is None or ycoords is None: + raise ValueError( + "Invalid image NPZ: missing xcoords.npy or ycoords.npy " + "for non-uniform coordinates" + ) + nrows, ncols = data.shape + if xcoords.shape != (ncols,) or ycoords.shape != (nrows,): + raise ValueError( + f"Invalid image NPZ: coordinates of shape {xcoords.shape} (X) and " + f"{ycoords.shape} (Y) do not match image shape {data.shape}" + ) + obj.set_coords(xcoords, ycoords) # Restore object metadata (Geometry_*, Table_* results) if metadata.get("obj_metadata"): diff --git a/doc/features/advanced/webapi.rst b/doc/features/advanced/webapi.rst index f06ba9a8c..7560da38a 100644 --- a/doc/features/advanced/webapi.rst +++ b/doc/features/advanced/webapi.rst @@ -241,7 +241,10 @@ ImageObj NPZ Structure my_image.npz ├── data.npy # 2D image array (preserves dtype) - └── metadata.json # Labels, units, coordinates (x0, y0, dx, dy) + ├── xcoords.npy # X pixel centers (only for non-uniform coordinates) + ├── ycoords.npy # Y pixel centers (only for non-uniform coordinates) + └── metadata.json # Labels, units, coordinates + # (x0, y0, dx, dy, is_uniform_coords) Authentication -------------- From 70170f2689ad72e749d05b0b15d733b4b800ec70 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Sat, 24 Oct 2026 08:58:24 +0200 Subject: [PATCH 3/3] docs: release notes for the review fixes Describe the user-visible corrections of this release cycle coming from the review: atomic workspace saving, non-uniform image coordinates in Web API transfers, HDF5 link cycles (SigimaX) and the processing fixes of Sigima 1.4.0 (resampling grid, real/complex signal operations, complex images, independent geometric results, ROI extraction and rotation of non-uniform images, product uncertainty). Document the resampling bounds as pixel centers (and fix the x_min formula markup) and update the resampling test scenario accordingly. French translations updated. Assisted-by: Claude Opus 5.5 --- datalab/tests/scenarios/common.py | 4 +-- doc/features/image/menu_processing.rst | 10 +++--- .../features/image/menu_processing.po | 18 +++++----- .../LC_MESSAGES/release_notes/release_1.04.po | 33 +++++++++++++++++++ doc/release_notes/release_1.04.md | 13 ++++++++ 5 files changed, 62 insertions(+), 16 deletions(-) diff --git a/datalab/tests/scenarios/common.py b/datalab/tests/scenarios/common.py index 72df26fa8..5803d95de 100644 --- a/datalab/tests/scenarios/common.py +++ b/datalab/tests/scenarios/common.py @@ -328,9 +328,9 @@ def run_image_computations( width=width_param, height=height_param, xmin=ima1.x0, - xmax=ima1.x0 + ima1.width, + xmax=ima1.x0 + ima1.width - ima1.dx, ymin=ima1.y0, - ymax=ima1.y0 + ima1.height, + ymax=ima1.y0 + ima1.height - ima1.dy, ) panel.processor.run_feature("resampling", param) diff --git a/doc/features/image/menu_processing.rst b/doc/features/image/menu_processing.rst index 84831be8a..5cd9d0fb3 100644 --- a/doc/features/image/menu_processing.rst +++ b/doc/features/image/menu_processing.rst @@ -547,14 +547,14 @@ The following parameters are available: * - Parameter - Description - * - math:`x_{min}` - - Minimum x-coordinate of the output image + * - :math:`x_{min}` + - X-coordinate of the center of the first output pixel * - :math:`x_{max}` - - Maximum x-coordinate of the output image + - X-coordinate of the center of the last output pixel * - :math:`y_{min}` - - Minimum y-coordinate of the output image + - Y-coordinate of the center of the first output pixel * - :math:`y_{max}` - - Maximum y-coordinate of the output image + - Y-coordinate of the center of the last output pixel * - Mode - Image size definition mode: 'Pixel size' or 'Output shape'. The 'Pixel size' mode allows to define the pixel size of the new image, while the 'Output shape' mode diff --git a/doc/locale/fr/LC_MESSAGES/features/image/menu_processing.po b/doc/locale/fr/LC_MESSAGES/features/image/menu_processing.po index 450535593..df9f410e8 100644 --- a/doc/locale/fr/LC_MESSAGES/features/image/menu_processing.po +++ b/doc/locale/fr/LC_MESSAGES/features/image/menu_processing.po @@ -791,32 +791,32 @@ msgid "Generate new images by resampling each selected image. The following para msgstr "Génère de nouvelles images en rééchantillonnant chaque image sélectionnée. Les paramètres suivants sont disponibles :" #, python-brace-format -msgid "math:`x_{min}`" +msgid ":math:`x_{min}`" msgstr "" -msgid "Minimum x-coordinate of the output image" -msgstr "Coordonnée x minimale de l'image de sortie" +msgid "X-coordinate of the center of the first output pixel" +msgstr "Coordonnée x du centre du premier pixel de sortie" #, python-brace-format msgid ":math:`x_{max}`" msgstr "" -msgid "Maximum x-coordinate of the output image" -msgstr "Coordonnée x maximale de l'image de sortie" +msgid "X-coordinate of the center of the last output pixel" +msgstr "Coordonnée x du centre du dernier pixel de sortie" #, python-brace-format msgid ":math:`y_{min}`" msgstr "" -msgid "Minimum y-coordinate of the output image" -msgstr "Coordonnée y minimale de l'image de sortie" +msgid "Y-coordinate of the center of the first output pixel" +msgstr "Coordonnée y du centre du premier pixel de sortie" #, python-brace-format msgid ":math:`y_{max}`" msgstr "" -msgid "Maximum y-coordinate of the output image" -msgstr "Coordonnée y maximale de l'image de sortie" +msgid "Y-coordinate of the center of the last output pixel" +msgstr "Coordonnée y du centre du dernier pixel de sortie" msgid "Mode" msgstr "Mode" diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index 21399400b..a617598fe 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po @@ -484,3 +484,36 @@ msgstr "**Sécurité :**" msgid "Opening a workspace received from someone else can no longer make DataLab load or run other code installed on the computer. The parameters stored with objects, analysis results and History actions name their class, and DataLab imported whatever module they named and called the object named there, which a modified file could use, for example, to make DataLab quit. Only parameter classes of DataLab, Sigima, guidata and installed plugins are now used; other stored parameters are reported as unreadable (fixes [Issue #407](https://github.com/DataLab-Platform/DataLab/issues/407))" msgstr "Ouvrir un espace de travail reçu d'une autre personne ne permet plus de faire charger ou exécuter par DataLab un autre code installé sur l'ordinateur. Les paramètres enregistrés avec les objets, les résultats d'analyse et les actions de l'historique nomment leur classe, et DataLab importait le module ainsi nommé puis appelait l'objet désigné, ce qu'un fichier modifié pouvait exploiter, par exemple pour faire quitter DataLab. Seules les classes de paramètres de DataLab, Sigima, guidata et des plugins installés sont désormais utilisées ; les autres paramètres enregistrés sont signalés comme illisibles (corrige [l'issue #407](https://github.com/DataLab-Platform/DataLab/issues/407))" + +msgid "Fixed HDF5 files containing a group linked into itself or into one of its subgroups failing to open in the HDF5 browser (the file was browsed endlessly until an error): such links back to a parent group are now skipped, while other aliases of a group or dataset are still shown at each of their paths" +msgstr "Correction des fichiers HDF5 contenant un groupe lié dans lui-même ou dans l'un de ses sous-groupes, qui ne pouvaient pas être ouverts dans l'explorateur HDF5 (le fichier était parcouru sans fin jusqu'à une erreur) : ces liens vers un groupe parent sont désormais ignorés, tandis que les autres alias d'un groupe ou d'un jeu de données restent affichés à chacun de leurs chemins" + +msgid "**Workspace saving:**" +msgstr "**Enregistrement de l'espace de travail :**" + +msgid "Fixed a failed save destroying the previous version of the workspace file: the file was emptied before the data was written, so that an error during the save, a full disk or an interruption left neither the previous nor the new workspace on disk. Workspaces, capsules and History files (`.dlhist`) are now written to a temporary file, which replaces the previous file only once completely written; if the save fails, the previous file is left untouched" +msgstr "Correction d'un enregistrement en échec qui détruisait la version précédente du fichier d'espace de travail : le fichier était vidé avant l'écriture des données, si bien qu'une erreur pendant l'enregistrement, un disque plein ou une interruption ne laissaient sur le disque ni l'ancien ni le nouvel espace de travail. Les espaces de travail, les capsules et les fichiers d'historique (`.dlhist`) sont désormais écrits dans un fichier temporaire, qui ne remplace le fichier précédent qu'une fois entièrement écrit ; en cas d'échec de l'enregistrement, le fichier précédent reste intact" + +msgid "Fixed **Processing > Resampling** of images shifting every value by half a pixel: resampling an image with the default parameters, i.e. onto its own grid, filled its last row and column with NaN. The X/Y min/max parameters are now the coordinates of the centers of the first and last output pixels (by default, those of the image), and the 'Pixel size' mode keeps the requested pixel size exactly" +msgstr "Correction du **Traitement > Rééchantillonnage** des images, qui décalait toutes les valeurs d'un demi-pixel : rééchantillonner une image avec les paramètres par défaut, c'est-à-dire sur sa propre grille, remplissait sa dernière ligne et sa dernière colonne de NaN. Les paramètres X/Y min/max sont désormais les coordonnées des centres du premier et du dernier pixel de sortie (par défaut, ceux de l'image), et le mode 'Taille de pixel' conserve exactement la taille de pixel demandée" + +msgid "Fixed the sum, average, product and standard deviation of several signals, and their combination into an image, depending on the order of the signals: when a real signal came first, the imaginary parts of the complex signals were discarded" +msgstr "Correction de la somme, de la moyenne, du produit et de l'écart-type de plusieurs signaux, ainsi que de leur combinaison en image, qui dépendaient de l'ordre des signaux : lorsqu'un signal réel venait en premier, les parties imaginaires des signaux complexes étaient perdues" + +msgid "The sum, average and product of several images now accept complex images, which raised an error" +msgstr "La somme, la moyenne et le produit de plusieurs images acceptent désormais les images complexes, qui provoquaient une erreur" + +msgid "Fixed modifying the result of a diagonal, horizontal or vertical flip or a 90° rotation of an image in place (e.g. from a macro or a plugin) also modifying its source image" +msgstr "Correction de la modification en place du résultat d'un retournement diagonal, horizontal ou vertical ou d'une rotation de 90° d'une image (par exemple depuis une macro ou un plugin), qui modifiait aussi l'image source" + +msgid "Fixed the extraction of several regions of interest into a single image giving wrong coordinates to images with non-uniform coordinates (the X coordinates were taken from the extracted rows and the Y coordinates from the extracted columns)" +msgstr "Correction de l'extraction de plusieurs régions d'intérêt dans une seule image, qui donnait des coordonnées erronées aux images à coordonnées non uniformes (les coordonnées X étaient prises sur les lignes extraites et les coordonnées Y sur les colonnes extraites)" + +msgid "The arbitrary rotation of images (**Processing > Geometry > Rotate by...**) now refuses images with non-uniform coordinates, like resizing and resampling, instead of producing an image whose coordinates did not match its pixels" +msgstr "La rotation arbitraire des images (**Traitement > Géométrie > Rotation de...**) refuse désormais les images à coordonnées non uniformes, comme le redimensionnement et le rééchantillonnage, au lieu de produire une image dont les coordonnées ne correspondaient pas à ses pixels" + +msgid "Fixed the uncertainty of the product of signals being lost where one of the signals is zero: it is now propagated there too" +msgstr "Correction de l'incertitude du produit de signaux, qui était perdue là où l'un des signaux est nul : elle y est désormais également propagée" + +msgid "Fixed images with non-uniform coordinates losing them when transferred through the Web API (`GET` and `PUT /objects/{name}/data`): they were rebuilt with default uniform coordinates. The NPZ data format now includes these coordinates; archives written by earlier versions remain readable. Clients with their own NPZ reader, such as DataLab-Kernel, must support the new entries to take advantage of this" +msgstr "Correction des images à coordonnées non uniformes qui perdaient celles-ci lors d'un transfert par l'API Web (`GET` et `PUT /objects/{name}/data`) : elles étaient reconstruites avec des coordonnées uniformes par défaut. Le format de données NPZ inclut désormais ces coordonnées ; les archives écrites par les versions antérieures restent lisibles. Les clients disposant de leur propre lecteur NPZ, comme DataLab-Kernel, doivent prendre en charge les nouvelles entrées pour en bénéficier" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 97bddb90f..a70bdf6ca 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -178,6 +178,11 @@ See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific ob * Fixed single-precision complex (`complex64`) 1D datasets losing their imaginary part when imported from an HDF5 file: they are now imported as complex signals. Integer signals beyond ±2⁵³ now also emit the precision warning instead of being rounded silently * Fixed the import of several datasets from HDF5 files stopping at the first dataset that could not be imported: the other datasets are now imported, and the datasets that could not be are listed in a single message at the end * Fixed HDF5 files containing broken links failing to open in the HDF5 browser, for example synchrotron master files whose detector files are not available (external links to a missing file): the broken links are now skipped and the rest of the file can be browsed and imported +* Fixed HDF5 files containing a group linked into itself or into one of its subgroups failing to open in the HDF5 browser (the file was browsed endlessly until an error): such links back to a parent group are now skipped, while other aliases of a group or dataset are still shown at each of their paths + +**Workspace saving:** + +* Fixed a failed save destroying the previous version of the workspace file: the file was emptied before the data was written, so that an error during the save, a full disk or an interruption left neither the previous nor the new workspace on disk. Workspaces, capsules and History files (`.dlhist`) are now written to a temporary file, which replaces the previous file only once completely written; if the save fails, the previous file is left untouched **Metadata:** @@ -188,6 +193,13 @@ See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific ob * Results of the extraction of several regions of interest into a single object can be processed again: the **Processing** tab showed "Unknown computing feature: extract_rois" with **Reapply** disabled, and replaying the History skipped the action * **Edit > Recompute** now also recomputes results of operations combining several objects (e.g., difference of two signals, average of several images), which were skipped +* Fixed **Processing > Resampling** of images shifting every value by half a pixel: resampling an image with the default parameters, i.e. onto its own grid, filled its last row and column with NaN. The X/Y min/max parameters are now the coordinates of the centers of the first and last output pixels (by default, those of the image), and the 'Pixel size' mode keeps the requested pixel size exactly +* Fixed the sum, average, product and standard deviation of several signals, and their combination into an image, depending on the order of the signals: when a real signal came first, the imaginary parts of the complex signals were discarded +* The sum, average and product of several images now accept complex images, which raised an error +* Fixed modifying the result of a diagonal, horizontal or vertical flip or a 90° rotation of an image in place (e.g. from a macro or a plugin) also modifying its source image +* Fixed the extraction of several regions of interest into a single image giving wrong coordinates to images with non-uniform coordinates (the X coordinates were taken from the extracted rows and the Y coordinates from the extracted columns) +* The arbitrary rotation of images (**Processing > Geometry > Rotate by...**) now refuses images with non-uniform coordinates, like resizing and resampling, instead of producing an image whose coordinates did not match its pixels +* Fixed the uncertainty of the product of signals being lost where one of the signals is zero: it is now propagated there too **Plugins:** @@ -218,6 +230,7 @@ See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific ob * Errors raised by remote commands, such as `calc` with an unknown computation function or `set_object` with an unknown object, are now reported to the client instead of being silently ignored * Fixed remote commands sometimes returning the result of the previous command instead of their own: for example, `get_current_object_uuid` could return a panel name instead of `None` when there was no current object * Fixed DataLab freezing when closed while a remote command was still running (e.g. when closing the main window with `call_method("close")`) +* Fixed images with non-uniform coordinates losing them when transferred through the Web API (`GET` and `PUT /objects/{name}/data`): they were rebuilt with default uniform coordinates. The NPZ data format now includes these coordinates; archives written by earlier versions remain readable. Clients with their own NPZ reader, such as DataLab-Kernel, must support the new entries to take advantage of this **Security:**