Source code for radioviz.services.xyz_format

#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""
XYZ image format helpers.

The XYZ binary format is defined as:

- First 4 bytes: header containing height and width as two 16-bit unsigned integers (little endian)
- Remaining bytes: pixel values as 16-bit unsigned integers (little endian)
"""

from __future__ import annotations

import numpy as np


[docs] def decode_xyz_bytes(data: bytes) -> np.ndarray: """ Decode XYZ binary data into a 2D numpy array. :param data: Raw XYZ file bytes :type data: bytes :return: Image array with shape (height, width) :rtype: numpy.ndarray The XYZ stream stores pixel rows and columns swapped, so the data is reshaped with (width, height) and returned in that orientation to preserve the instrument layout without an additional transpose. """ if len(data) < 4: raise ValueError('XYZ data too short to contain header') header = np.frombuffer(data[:4], dtype='<u2') height, width = int(header[0]), int(header[1]) expected_size = 4 + (height * width * 2) if len(data) != expected_size: raise ValueError(f'XYZ size mismatch: expected {expected_size} bytes for {width}x{height}, got {len(data)}') pixels = np.frombuffer(data[4:], dtype='<u2') image = pixels.reshape((width, height)) return image
[docs] def encode_xyz_array(arr: np.ndarray) -> bytes: """ Encode a 2D numpy array into XYZ binary data. :param arr: Image array to encode :type arr: numpy.ndarray :return: Encoded XYZ bytes :rtype: bytes When saving, the binary payload is emitted with the instrument's column-major orientation (width first), so the header keeps (height, width) while the C-ordered buffer reflects the swapped axes. """ array = np.asarray(arr) if array.ndim != 2: raise ValueError('XYZ encoder expects a 2D array') if array.size == 0: height, width = 0, 0 data_bytes = b'' else: if array.dtype.kind not in {'i', 'u'}: if array.dtype.kind != 'f': raise ValueError('XYZ encoder supports only numeric arrays') if not np.all(np.isfinite(array)): raise ValueError('XYZ encoder does not allow NaN or inf values') if not np.all(array == np.round(array)): raise ValueError('XYZ encoder requires integer pixel values') if array.min() < 0 or array.max() > 65535: raise ValueError('XYZ encoder supports only values in [0, 65535]') width, height = array.shape data_bytes = array.astype('<u2', copy=False).tobytes(order='C') header = np.array([height, width], dtype='<u2').tobytes() return header + data_bytes