@@ -363,6 +363,76 @@ def test_image_obj_metadata_with_numpy_origin(self) -> None:
363363 assert result .x0 == pytest .approx (1.5 )
364364 assert result .dx == pytest .approx (0.1 )
365365
366+ def test_image_non_uniform_coords_round_trip (self ) -> None :
367+ """Non-uniform image coordinates survive a round trip.
368+
369+ Regression: they were lost, the image being rebuilt with uniform
370+ coordinates (its pixels survived, but not their physical positions).
371+ """
372+ obj = _make_image (shape = (3 , 4 ))
373+ obj .xunit , obj .yunit = "mm" , "s"
374+ obj .set_coords (np .array ([1.0 , 2.0 , 4.0 , 8.0 ]), np .array ([10.0 , 11.0 , 15.0 ]))
375+ result = deserialize_object_from_npz (serialize_object_to_npz (obj ))
376+ assert not result .is_uniform_coords
377+ np .testing .assert_array_equal (result .xcoords , obj .xcoords )
378+ np .testing .assert_array_equal (result .ycoords , obj .ycoords )
379+ np .testing .assert_array_equal (result .data , obj .data )
380+ assert (result .xunit , result .yunit ) == ("mm" , "s" )
381+
382+ def test_image_uniform_coords_round_trip (self ) -> None :
383+ """Uniform images are stored without coordinate arrays."""
384+ obj = _make_image (shape = (3 , 4 ))
385+ obj .set_uniform_coords (0.5 , 2.0 , - 1.0 , 3.0 )
386+ data = serialize_object_to_npz (obj )
387+ with zipfile .ZipFile (io .BytesIO (data )) as zf :
388+ assert sorted (zf .namelist ()) == ["data.npy" , "metadata.json" ]
389+ assert json .loads (zf .read ("metadata.json" ))["is_uniform_coords" ] is True
390+ result = deserialize_object_from_npz (data )
391+ assert result .is_uniform_coords
392+ assert (result .dx , result .dy , result .x0 , result .y0 ) == (0.5 , 2.0 , - 1.0 , 3.0 )
393+
394+ def test_image_archive_without_coords_flag_is_uniform (self ) -> None :
395+ """Archives written by earlier versions (no ``is_uniform_coords``) are read
396+ with uniform coordinates."""
397+ buf = io .BytesIO ()
398+ with zipfile .ZipFile (buf , "w" ) as zf :
399+ arr = io .BytesIO ()
400+ np .save (arr , np .zeros ((3 , 4 )))
401+ zf .writestr ("data.npy" , arr .getvalue ())
402+ metadata = {"type" : "image" , "x0" : 1.0 , "y0" : 2.0 , "dx" : 0.5 , "dy" : 0.25 }
403+ zf .writestr ("metadata.json" , json .dumps (metadata ))
404+ result = deserialize_object_from_npz (buf .getvalue ())
405+ assert result .is_uniform_coords
406+ assert (result .dx , result .dy , result .x0 , result .y0 ) == (0.5 , 0.25 , 1.0 , 2.0 )
407+
408+ @pytest .mark .parametrize (
409+ "xcoords, ycoords, match" ,
410+ [
411+ (None , np .arange (3.0 ), "missing xcoords.npy" ),
412+ (np .arange (3.0 ), np .arange (3.0 ), "do not match image shape" ),
413+ (np .arange (4.0 ), np .arange (4.0 ), "do not match image shape" ),
414+ ],
415+ )
416+ def test_image_invalid_non_uniform_coords_raise (
417+ self , xcoords , ycoords , match : str
418+ ) -> None :
419+ """Missing or mismatched coordinate arrays are rejected."""
420+ buf = io .BytesIO ()
421+ with zipfile .ZipFile (buf , "w" ) as zf :
422+ for name , array in (
423+ ("data.npy" , np .zeros ((3 , 4 ))),
424+ ("xcoords.npy" , xcoords ),
425+ ("ycoords.npy" , ycoords ),
426+ ):
427+ if array is not None :
428+ arr = io .BytesIO ()
429+ np .save (arr , array )
430+ zf .writestr (name , arr .getvalue ())
431+ metadata = {"type" : "image" , "is_uniform_coords" : False }
432+ zf .writestr ("metadata.json" , json .dumps (metadata ))
433+ with pytest .raises (ValueError , match = match ):
434+ deserialize_object_from_npz (buf .getvalue ())
435+
366436
367437# =============================================================================
368438# Routes integration tests
@@ -638,6 +708,29 @@ def test_get_data_missing_object(self, api_client) -> None:
638708 response = client .get ("/api/v1/objects/nope/data" , headers = _auth (token ))
639709 assert response .status_code == 404
640710
711+ def test_non_uniform_image_round_trip_through_endpoints (self , api_client ) -> None :
712+ """An image with non-uniform coordinates keeps them when uploaded with
713+ ``PUT data`` and downloaded with ``GET data``."""
714+ client , token , adapter = api_client
715+ obj = _make_image ("NU" , shape = (3 , 4 ))
716+ xcoords , ycoords = np .array ([1.0 , 2.0 , 4.0 , 8.0 ]), np .array ([10.0 , 11.0 , 15.0 ])
717+ obj .set_coords (xcoords , ycoords )
718+ response = client .put (
719+ "/api/v1/objects/NU/data" ,
720+ content = serialize_object_to_npz (obj ),
721+ headers = {** _auth (token ), "Content-Type" : "application/x-npz" },
722+ )
723+ assert response .status_code == 201
724+ stored = adapter .get_object ("NU" )
725+ assert not stored .is_uniform_coords
726+ np .testing .assert_array_equal (stored .xcoords , xcoords )
727+ response = client .get ("/api/v1/objects/NU/data" , headers = _auth (token ))
728+ assert response .status_code == 200
729+ result = deserialize_object_from_npz (response .content )
730+ assert not result .is_uniform_coords
731+ np .testing .assert_array_equal (result .xcoords , xcoords )
732+ np .testing .assert_array_equal (result .ycoords , ycoords )
733+
641734 def test_put_creates_object (self , api_client ) -> None :
642735 """``PUT data`` creates a new object from an NPZ payload."""
643736 client , token , adapter = api_client
0 commit comments