From 639b8ef654cf67a5affed178fa7893b694835d9d Mon Sep 17 00:00:00 2001 From: brokkoli71 Date: Wed, 4 Mar 2026 14:37:29 +0100 Subject: [PATCH 1/8] Add ngff-rfc5 coordinate transformation examples as submodule at 8eb63f3 --- .github/workflows/ci.yml | 2 ++ .gitmodules | 3 +++ testdata/ome/v0.6/examples | 1 + 3 files changed, 6 insertions(+) create mode 100644 .gitmodules create mode 160000 testdata/ome/v0.6/examples diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f497ef31..f149c542 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,8 @@ jobs: steps: - uses: actions/checkout@v5 + with: + submodules: true - name: Set up JDK uses: actions/setup-java@v4 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..37c054ac --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "testdata/ome/v0.6/examples"] + path = testdata/ome/v0.6/examples + url = https://github.com/jo-mueller/ngff-rfc5-coordinate-transformation-examples diff --git a/testdata/ome/v0.6/examples b/testdata/ome/v0.6/examples new file mode 160000 index 00000000..8eb63f34 --- /dev/null +++ b/testdata/ome/v0.6/examples @@ -0,0 +1 @@ +Subproject commit 8eb63f3401eaa73633ad9150fef9a2e971f857e6 From 6b4877e0476a82e3f86cdf4bc4f70a55fd3e2749 Mon Sep 17 00:00:00 2001 From: brokkoli71 Date: Wed, 4 Mar 2026 14:37:56 +0100 Subject: [PATCH 2/8] add ome testdata for v0.5 and v0.4 --- testdata/ome/v0.4/.zattrs | 79 +++++++++++++++++++++++++++++ testdata/ome/v0.4/.zgroup | 3 ++ testdata/ome/v0.4/0/.zarray | 29 +++++++++++ testdata/ome/v0.4/0/.zattrs | 1 + testdata/ome/v0.4/0/0.0.0.0.0 | Bin 0 -> 7567 bytes testdata/ome/v0.4/0/0.1.0.0.0 | Bin 0 -> 7534 bytes testdata/ome/v0.4/1/.zarray | 29 +++++++++++ testdata/ome/v0.4/1/.zattrs | 1 + testdata/ome/v0.4/1/0.0.0.0.0 | Bin 0 -> 1515 bytes testdata/ome/v0.4/1/0.1.0.0.0 | Bin 0 -> 1496 bytes testdata/ome/v0.5/0/c/0/0/0/0/0 | Bin 0 -> 7355 bytes testdata/ome/v0.5/0/c/0/1/0/0/0 | Bin 0 -> 7349 bytes testdata/ome/v0.5/0/zarr.json | 48 ++++++++++++++++++ testdata/ome/v0.5/1/c/0/0/0/0/0 | Bin 0 -> 1045 bytes testdata/ome/v0.5/1/c/0/1/0/0/0 | Bin 0 -> 1058 bytes testdata/ome/v0.5/1/zarr.json | 48 ++++++++++++++++++ testdata/ome/v0.5/zarr.json | 85 ++++++++++++++++++++++++++++++++ 17 files changed, 323 insertions(+) create mode 100644 testdata/ome/v0.4/.zattrs create mode 100644 testdata/ome/v0.4/.zgroup create mode 100644 testdata/ome/v0.4/0/.zarray create mode 100644 testdata/ome/v0.4/0/.zattrs create mode 100644 testdata/ome/v0.4/0/0.0.0.0.0 create mode 100644 testdata/ome/v0.4/0/0.1.0.0.0 create mode 100644 testdata/ome/v0.4/1/.zarray create mode 100644 testdata/ome/v0.4/1/.zattrs create mode 100644 testdata/ome/v0.4/1/0.0.0.0.0 create mode 100644 testdata/ome/v0.4/1/0.1.0.0.0 create mode 100644 testdata/ome/v0.5/0/c/0/0/0/0/0 create mode 100644 testdata/ome/v0.5/0/c/0/1/0/0/0 create mode 100644 testdata/ome/v0.5/0/zarr.json create mode 100644 testdata/ome/v0.5/1/c/0/0/0/0/0 create mode 100644 testdata/ome/v0.5/1/c/0/1/0/0/0 create mode 100644 testdata/ome/v0.5/1/zarr.json create mode 100644 testdata/ome/v0.5/zarr.json diff --git a/testdata/ome/v0.4/.zattrs b/testdata/ome/v0.4/.zattrs new file mode 100644 index 00000000..60027f13 --- /dev/null +++ b/testdata/ome/v0.4/.zattrs @@ -0,0 +1,79 @@ +{ + "multiscales": [ + { + "version": "0.4", + "name": "test_image", + "axes": [ + { + "name": "t", + "type": "time", + "unit": "millisecond" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 0.5, + 0.5, + 0.5 + ] + } + ] + }, + { + "path": "1", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ], + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ], + "type": "gaussian" + } + ] +} \ No newline at end of file diff --git a/testdata/ome/v0.4/.zgroup b/testdata/ome/v0.4/.zgroup new file mode 100644 index 00000000..cab13da6 --- /dev/null +++ b/testdata/ome/v0.4/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/testdata/ome/v0.4/0/.zarray b/testdata/ome/v0.4/0/.zarray new file mode 100644 index 00000000..ce9240fe --- /dev/null +++ b/testdata/ome/v0.4/0/.zarray @@ -0,0 +1,29 @@ +{ + "shape": [ + 1, + 2, + 8, + 16, + 16 + ], + "chunks": [ + 1, + 1, + 8, + 16, + 16 + ], + "dtype": "CNh(Py zl_n&innDZG|4#q8&pgk$_nh;c?|r}bJ z;y7LW&G||XKQ-)L$5X5mAA8Ub(KM>ed(Cf`tyabK4Ra-hS6)#%ElIrRGsB-7gO#rE*jT9)~?c#8Ex%xfY8wrSPd=epjtE!Nh53fGvg zrA!ih6!s>zyD>dpS`L063?kk;q20n67qagG58UkKoXi+%i={d3Wtx2Un4DXvb*>YU zuqOB3*Y#oGp4eq2OBNLJu~+JE*Dx4dnJ_!aWd79T=6>a3{ev;Uy;{HEJ4=b_J$wG5 z2Y!3|ayi8?WBu4VA;W3dvA|h+L`lw$P8#j$g^qJ7MFVd*0nZc(?B!~5iXVTd@sa=L zdaD$Dp^pD1bXA6Q&+Xp-eWbA^QTNtu5gl3<;c*;O%cQ+tO+*QP6KKN~AInbF zB{7B~O?AC|_>W$n=u;dZuNJ@Lwbrj&`uw&>t+{5S;{A|MZ8+y;@PAYgRjD1;Iy04 z=8R#@Aur!S%1qa?`L;<@pC8#5KBSVS%LpSYQAE=AQqj8Q{y(PK8|fXr$JB8> zh9_eABrkc7X~vi8;|XP473;m_8IR}7d9`~|#_a<$ikFY3k1~RP=?|uMc#IzT>@n(8 z(BZk)Av~|sm(Oy5S$;H%*L|6s#<-@t{C6^Ue8-}brqmQDWTy=?bc@_AJ@CJTqc9jK z@bUJ`v)ek-w*kh!82_mT(&5XM^i4756B&txF1}6@r3JsV*J|jwm0z6V%`m*ZCBr7q zoiZ|B(#<}(e@fAef1L}`KU*Q{!#GdiJL5)&U(8>U-zIAt&R4>Nqzg0;)6X}wMa z3Ez1q$}7U~wLHE7hAZwKV5zMd!cj3|SzvofoNj zKhVRls|(&IR&+bffqBig@rMH48rpr{5((CJ8e++@=9Sr!iuCeogU%3qUgMjF7mRZ) zZs(YNYFG3<*q<&QZqV3mwG(u8+qVve>!prxNsL-x`z`C*Gc&Yf0=C;RlAV36E6xk! zG{)?{sW9)2wGRiX?X-x?px0elHVA3P?z*@Ub*5sSU?Z`b7^pJQ|E4GWetDw<)*&?X zPF<1yJpRk?lXuH1g(7N33{|W%Ql*P?7*@RI5rLGMM{PivbA_C)&Z>#Kd9{~m+tc)p z(J;ut$IS)4{s-0^*7SC7D|nRXSXTSSTw&qQ;fxSdrohVE#hpIwkkwx&X5HJ-F5P`# zXwiiCF{yw#J`T;kxUoB8Gx@#w5B!cUG!kz8f~H|fO2+W*A1z`GlSHzM?9;<7Vs8(t z z-T-Zf^W;%>e@ol$GE&$ZT9idHeOZGw`pl5i z%$vq9WrXt8h?%?nnnjJPr!$nI1kWfh`}Q{X4%d;|o&JkRozQn(KZ)lOd0w(|!cclr zwyl${dCYp4e@DO|CR5SB?uvP~n9TWbW*5n>ip`Z2(G|3#zKEeY5uizM?uZp;Xjn!C zJ$#^CD09CNvgxHc^pONksqDAsUh6O3bX|F)m|rJ&O|x%*b$=WoRyCu{T63r#=5IWMUKjl>@R@EtNv5HuM@|* zS~zRE`<(75-RtBIWb_B^_Vsaic(6Fy?sVbxEv93KS)-dB#*3ujNReF~Y9jTKx z?P9$HJdiJJX?cXpa$wx=^O~q9`)mF#6MyxEE;7bdd6A>4sp+>}bNjm+Em}fuM zs&mb=9i4wnhAnK^zfq}czveaiwBB=H6ONs`J72)-NMhUQ2^B+g*)N6sV{AzQpDsAG zV>W5D@R|HMa8D;XbLuVaXXKemr9F9~(%Cw*H?+qkQ@h%dN$D3{;ug*nca^HhB}$$2 ze9R}f;Jo{Q;~DD)z-I_<1jMhB`2E6;F78PwB<7nFe^#0HMd*sA@8R%@Ho0~vq#=iv zRhC51Ce&Zw1c(@erC^*q1X(eu5nA=!6bf2f?oMB7hCDV?86!|^@1 zq{-*0;})+1>xwDFBhSu9brSnC3(a0%BtFxK)|HDfiann%D;ltc*i=>RclmkmsoD|w zyLL2&YU_;>FR$g2(Fx4Fk)7Y{Gud}pVQyyY0!9i9hT=!!>!4st+G%yZ`I-H~Egsh&o+c6@yjpTNneg2^@WzAuERix^kwzKnz_N4xBt-(ZcSWWKn z{mb1dbsZ1)@0G*Wi^g14QdhApt!vm~-{!hb*Bfcc4hT(=(*^Vwi+okt_Z?r6 z|6%rh@}-A~#S5!a{sHLgRSnLxH`QO}l&cMpxG7)Sv$F z(7qt|Z1&o&w4@+cecfkOmCO5zT)rBaJ;#l%uW9H-94ShmZI}g$yVli;>JFR!88#|d zscpaWGSa4brfA#k%ZJu$y1IJB_cV`|U8o5v{9dtU|IBE5AUR&TWKC`zla{)Lx+qNc zQK7MakGSU+`Y58%(7c+v;OSY>*qal~A9v&ykP$i0Md8wUnHlvWE3cToa66vYN6B?( z+b1)t;!a(de{qbY-S2I!K0UzH$7|K&D*L(yzD(r#lkW|rCj`0ws%^LCe7Tj`GMzJs zxn;W$a=h}KiAI5q?ym~sxNhyxP1vpY_U)&ED3X>_?H+rKkJzStItfiL33h@LYiGfw z`o@Cct~*Hxf9|F6pEtSRIkLT()jAztd}FH2w6{**bg}I7g2zzZm)n|W`$zvWcNfBC zOXVW>+~Bjg`ZnQ?NgBdw#V+(3R|_M9J=!|XX?VA8)An$o-a1t*6_GxQB+frc zVVIO@3(4dK+eWb+%-gHnq-%OjC$74u#(z8~F>Q;r^A|s#6h|$s;#gjRD{}A1<*heY z1|{a29trMf%ehh35YUzM_O?{0imD%9xagjxnODwzValazLc@8No^kXX6Df#}@26AL z^1L%1#$m1>xj5XNT!e3LD46AZL~gR0vc9hpEpth&{+TA5Yqvws-MZtkAulv)hMH${ zKl8O-xPQT6KChv9vwdNKsl_$6Cki)AmOM>A{Yuc~XA_gP;yTy4DX--%&*Jkgjz60( z6Ng0YW$TkNcTFaq&lGQ~JagjDxWt&DXNBDnnI(IU#=r9oodq?~+FZTQ)IOdD+~~ zct5zG`)#fJV&Bi2kbofb&s}jnm;5Yt3rG7oy*$Xa#CKPvD56Qi`|Yur`W+>sb;b)< zdX~?&7W-zH?@{k~*ZFx_Pka8|G?$3;y^9%`u`g{yjB4+umW1)}M4w@A1>P;Kcl@rD zcRD)o4<1z~G7i->_S(@Vy4hEVB*v}NuIaOb7tT>5j#~};3q>v}$9N!pAJS zd#GSZf>YPgoA%dz_7ntO`zk3+3^rKd4G%ILx9VI@Ugz5zClPTjURb_q@WnQn`=ur) zMz~|uqnFEMKIEO?47|&{Yot+idF=6XZjSP-jgkvI`)=qqUoyF}C@~Zf^=LGMS1q;S z=QZ&)$)D5BIoB%Kvb3kAU;0{|ymFotEAcm9Nh`1rFEV1|R3Os+a3G#jQ7S!zr$R7V zvRPrc==KwrhRy7wj^Eg3=$)TGIPA{3S}E6bN!z6rh?(h{I_G?RoYUFo)viqq$PO2? zQ8Mm*X}#fo@tI!bxfeGFFL+*C7R%>wRTl1C5&fM?n3zV!Pmmh(hKQ6L$9H~i`-=6q zs6OOwI{1ywTY_~+vTuC)NblIiZ3N0%)mvBfR~wH)zRv22ihe(ose@NGIFV?5u%|S> zDD?e;zC*IeN4<}e+^lzio0eM`Z@n9}9eUZ5D5eHBSnSSO=r-POO?fIi!b1? zx64U+WmBEj6Nxi{>Q<&!9bYzdcw}fA;k3?#(c0kd3Z|{;XSS_NmqYNXQSXRu@;dh~ zn7d6nxDNys#c3!CxY;>;v$e5O`KEi)ihmvvln_Xnf3Ldbn-{*yOjrTWcT2$btkinb zyAl27lf8fDkJ=eb-K#ak>^e|DpxyRwObf!FB)v#3sIXkwExVg+>I^Y zD!=Jg+>Pts8sa-c{^-nSnP?RHE(1>lfq&o@%%j9=Uo>Z;;S4FC=|Y_+(mJbK&z4#lJ|t06Kb9;R6$;CITI6of^S9D=^?EIi z)3ATwT9t}We7AdYX|Edv#OzBa3dKAxu=#jDkEwlgrA_r6L--kWR{flYQi^(Pi6XR71UfSkx_W#d)r7 zakS^-G*>7t(xnJEN@#&k7{>t_g+4hWkp{L%Bi^_;b^8(VY3p>`h?w?6hT`^pY%;1_ z(Jv{Xr7!Ai#%5u5xV30W{s@m1o3Dm9ev^#H!z|rY$yX>r&SnZZwjG?6mGbf8jgkBe zNp!nIs@2nJ zLGDc!pXaeYtguIDMkFnUyL?=jOXrBvRFu5LB1ZG^zVEo@B(9je+Z8geZKNM5B#z2F zb>AH$CRG(CTsFS^T|D@%&<(tp4)12MEixBVZLag$yx}0Qx5r~0DE$7|Fi#+}9k)xV z#O>_=qw+bZXt(>0fD8HftG~6#mD#l5sCO>9-eWy_GI5fsE0_+5t4N!*cy+--Q&^Wi z5Kg?ZRc+@s_AguXm7nhnI~d1rztjGp5*|Ld57t$?Z6i&tFvFxOjFLj4^yMQgPFt;} zi5aa2)E`fBclT1{R~k{ZHIs=j>ZEb|HAHw^{>f>s>D7(A38@hK;n{yp^sgr;Zv4h@ zdZ@hopdN2SmHE+!oAzsdcyUfpQt<0oo`w#)f_{pewqPvdSAA1`zSJfsjF#FeH3aZ;+h>PQ?}!!m>xs4M zdKaN)rcbhD+eM{5u)k{^5;YA;;**c5axN}x6{q=F-b<|8hyli) z_8|QErdP!u#(b16PGfRczg~IzhR^D&J4a^dC5gn)6(5Z&`)HR_BYs-`S=gm-CM9KB z%~u!JCLTd|2AL^c*`l$?9c7Y{hh90=rD`KUTp}8=6js%W*Rn-^9Xxg<{1{)Fu;>6H z6x882#-%K0e8SLSMuMZ{x^nVP(_&W%Lb%7Corh$lseJ51bahT6Gr{Ag1}T!`;n-+9 zylorX>yfBlivZG)q6Slr6i~(RXv-Gh8cm3z^gUfZ+7FRj|%3 zA3GR!b$&zp)a{;}ddI;qB7iVNsBYdQB+~y>NlG{p`I=+XaOc(obM`Ar=eC+O^u-z{ zZ6Eqh7xin*TXsmIt+4I;I(HOn#QUQyE_sXEAOESyL);~mX63#Xdtao=q_iFoD|*To zbvA(S)qt}HP3AE-oAQ5$=78rt~xqnV%=4Qigg@w)G zf;2(srP0VJb}E{U35^1<3g8A1iT2?U&;Z010UG$B{7_7&;Q%TC2m)$=gaDKR)FCKL zGyshS1;wug7>9p=P#Bj1wgET+P8{`%Y(LP0DXWM0r;`NqG@4_SxHdItQw$M zphJNh;ZO$91|$*S2tXUaQ@~JIc>^0DI{?e|k7PV62si@96hH=p-~i13#gt;50U<^R zC?hZofY$RAKpY5BQ5Jas!w$%HfW3fOM^{2;0-!Tddd^`$5&>60&LVhy0x$qX0oXVIgC1p_0(5Cu z0tf>r0Oa$(UIt*%5~Jut(STT#9*i*HoB-p6V1%%M(y$d^5P+X0On8E@qxIYgSX3n_ ze`uO$uL!Dg>LE1ucm(qY0}OzR2rCYnfFWQw&t4W8l%3Wp*Ey700#Ad3rj692XGF7G#uM?s!O{=dM; zEH^?DfC3<$2&}PckEc{47*ye?I?hY63d3Uh!%sJsDb#$YM{F+f-X g@4*1d zTcQw!gjABgSHIsMbC>6yd(L^k&-FsWL`I*|d-V>31 z6?54EDn#b|yQ$t$ZNtl-sizM$gyiS8xS{bao!2HhIMDANCAnP=Y3+@MPr4Di8MDfz zW?tSIVD0&@&ezYs3gy&X`Weckjfr=Q(O+uDxg1u)7+jn}OH0zMUu7j#HS) zG9NSNd_R$SsT`OW6zN>d6~J~fX>0ViTBjr!^YG0uUhnMA^}F?FO|!_SEeRx$Mc3-d zt;}nf>de}pd^w8S4CcgSEk@6C$VmAlF{}cA-)u5dK%*YlS9ZfA?^sVkhY-%53_$jr zS7&SabB`%{d{B8LJtp32n6GAE`HXE6!J6`@vpjd4KwC{2*j&do1vEFYlRGn7S_hdk zlxH{{1tSHb{931-^RqLxDnjp!(tnN7;<&md)-%3&dUoC*ByMMXEw^Zg51e22;L8t| zd#z^QAZy%hAI&h;cS?m{Ejta66_=X(>>g2~4$WX3tq;~|*i#!cMEFW9lQTeSPyV7@ zk!}&a`AP&q=n`cPUo0Cmp!WFrNW_m}!B55%2P!iW|Opo*_v ziHFBB%mXykq-pdnN`}D9%M!txH~P@)WWqVJvRAIqq6sFQuINBXuVgYtx0_Puqpu4L}% zCax)6pLk?m#a?Cz2fy;kcr5>!q7;abr!r9!50b}4txN8&HN^p4P-!)guThbTwc zO?OUPR`8;mv4h*$2=Omj-lG7?7h30cMHvCu2FhznI?X=%k|_kNZRvp6g4$zpPYf+ zL^24n%1h4Ijl1PAO^eIEIaH8iEf%tU$u&18Z&R%jQ&*l;Zt>d<1$5@hW?qczf&l@u4z4)-l%|s7)Ggm|F4Ug3 z8>b|(Ut>RX&-f50Za*6kwN*NRoza_jh!hug^^|KWOCU)^iEEM1h%C%(P4N~!PcTAY=h z*y_0Tqe$R-ien9Vbu5`i@8sZTmNc%;bjPk?+o#fFw%dkgd zMLQXe_y>$pU0QF154M<6iq8^$C`yxmsdof9+J9Eldj`DEu6C{SxC&BI!MlbMA&ZAh z#%+X=qGKmsuUVVlaI48@v?%o!*`znbK5xqt&b}rzG-{7vIuT*tlNBP8a)UyWUyDL8IgHm7gxt*y=XWpUcrzLLzV%9M#R#RpMKU{lhQ?* zEKS*|3sZ#UX-nk@WAqwX&2PDR1_ru)0a2|qE^Pr1YLTn-JhIPn8D}`>1ZQiCg)7jeP++xbmTpNA$1-d&Q-@JZTv?_MrGJ`bWl|Y_j!O zSU#=~wF6d9?Xq7dlvlpk*J^_vr+2Q11kM3;UZi=eH6VI19xHB;(80hnx&eFN0L1TM z(<|qf^7y#k2CWSX8Le29Ybn>W-N0Mg{d!{SGyH7)&`k7TO|#O9yejsrtwz41!{wfz zRtXF<+`lX2mmlt98}(a!)_wNlI-lZ1X|A{n|G>vT6^V2|Gq#aHCjW!itQ#*cmHqu? z6&TRlI3|vf#*ivqj}OjW57oba;K?(c(>(Uga(`5L$PylZX;9fxrLfJ?TU&((0e>O| z@5rAVk-ed)JGrIPa<8&hOSalF(W9bi$clgKD$%Y!JS?yO#Luxd&K(@(Qv67zXNDTK zdY&(Bt9e33i|NOPlPsF|H;0r^3S-GDLHEV3LO;eWLWc{_wdux%{Xr)!NZo-w_+MU{}C|Qbb`0}>cA%RMd6?G z*B`vT==3*v^_W(*hd;WsBpR7~N<}dx0kRcA^*U0{$3EZl5#L3}X!8ACyj_;sC7;ez zE$sUEd!~}wcQ(C99`_BW$W$-K;7zdV;kA~xD z`r>Op`FO}qy>Qg~i}$}u2gJk(Rfep%Y51bTMs852Lp)xzWakCX+A4o`@AGd71-{Sb1G1R3CwB zL+nHOZ+`R+sMPHZWNM73v-|>oS=3dG_Z`pl{3}3<;c&wZU&Fe2>D=0EDP;AT6mo;Y zr4ya*V3PCvVnXbl*}H;SOlLKt zQou-Bq0>?K-$t!Xckf6_insLWMphaP)C~?eN}Y?jHfVb(t}4Ob%%H!m0UKaL?z;an zanpyH;}$HR%#2g{YBAjSV?h2)&ZSdXzsc`g7B^P+f6)H%=_c;+(F*r?#Z7^zyG;)I zl#6Pw4{{u&l7APCNgf}-u5iC}2~K?e=+!2)A~2BuqPG2)&b_T~2PQxEIYbh4w;DU!uvsU`oLUzvX3AZBi`#n`JGnM{W$tffQwY~k4}Y;h zNz>m_jSopxPFsA_=lNrzJyI0yvyK&Hz;$efwYv#jgNo}WHy#-#Ye6nTu~!NYHD8%o zo|bnLXZUSupY0j?^kh&iX1Y3is{cobO&alY3M zugOo-7D;BY82|J9ZK2yS&n60qyrWC@-oPh)n~ra}+8x64D5a|V9VYV!HSM|Pe>eqI zlihYX>hkcFq(=OB@2J~)`e)5`&)11<39i2qxcb@s;^qOPAU`LLyI$WQ2w& z!l%s5r1*UP!WTICxQz{;PFKg*xa}J8g5Z`f^-}pz9QT^)uL&>5`JETce-2;1eeHqF zbBWUuyIQ@d^~>16#-h3Cn| zaCV8Sb%*Qb@2G7P$I2f%-pJu>Cp`j<&OML9Utj#&Z1qfB#jBGRlJ)>s^LO3*l0j9& zIrKpKAuo|z{m*1484g@{yb)L1@M6@_^X_vUA@QT%ESE#~UtjssU0z@FxnoE9bB1qi z=n}KPejBfG!o7okW?ypbKev(P&{XKy@sVGU!q+devSj?F#_P(lmifQy{7-9~y|d`-x1Pnbo-#kmet*p| zOV^M(+j0C1+t!0(0_E|=qZ$1y*E}x3A6Q1)-$*lzRLRy|u2%d>jyLacwdzRCEo(bN zUhRDRv?l@=)8DJP7W8)Eea)H1##R3xM=}gF!Y4O4at;r%l+r%zzN?jLjwv5#W!n1& zAj{#4Tcfoe67*jV1aJvz^T9enw?++R2JWa0uFPfvr-wHVp!e&^mt6C|X=^EK7D*we z1RuaN_~aUMoI8I`F(g>uE{>H=-8g2S2=Q1c?s(N8#aj zTtsoecNE|9ouHg@V^%G!sh zI2T!R{Dxset%j_+x?@tGTB-+o%Kn4%AukEkn8aA0MPpIF_5INW8H8bOB1gA#hvv#S zlO;{?(ND1x7e+G;N1PhDB%T?ZW@41Rwt$gRa?5&Lx}Uaa)aGvL*T)94tZ##I(sigsl~Ymy(jP#`HR@#FDs9D zgJB0%G*9{m4cGqks;|)vydkz>$~H+i{xa&2L+!8hHc1|~_S)s+y?@}% z?3A@+SGWyLug9eN^a7o`d-nBQUCv z<|-HM)-w_oicKmM6^&31#?*}8CJK4P?b@w=YT~wtxaCVtee8~*omP~dHWl2Zu*4^n z##?&2lW79+RP5um*Dvg#tt5<8vaWE@cti*MA8Os#%J0yb^BPz5e6qSD8KISFJa#`$ zVX@Ovj7=K59M4Geoaf{v?=%pfqoS=|AGRAwOs?7|o9g2y**eKQ)yC_<+>!P@PF}`d zgRPh`@P+o(2%|&6Yk6^3Tq4A^na_myaBIG@{Fr(aPrq>0(g2lj8E^eW(Hqueb%qng zTpvW?pfIb;Y|q&ej!@_`8b&{smMG88JuvH8y025lZKgA4N^=ppi@7F2am-6}@abka z5aE*~8;!BJCrUfYZzjOq7JPz54rgTxSyRMhy~Ly8#`y=|0taWrqTQd|F!{qerYBlS z+^?o6Gi@7UtR1L$Fv5uJILWum#0cPs_h-_2f1)-+-zH}3@y7>iONrPECZAdRb2x9~ zV1kXKwhYNUaB@k?#M4BgxN%zEOo0}s(IGf4Vb=la61KlEQv``k=;v0Caf_rQhxKwW zANpc##PTeAFWzJ_@Y6L^(mbS2zHhNT@c>#|b1{oBa6Gk4;-vhGZ!yi-4FYAS4keL`??^-wy)!_$`L zC+6aOm_gUhxP_~?jwJpl+5m5kc{BZ8pMsXw$Fn5M4ecMGhvg@y={6eBdxpQFZB+FK zR$>Z_t=xiGY$USZctUaFgge##Jw4A@$B&+Vy3KsC$}wK`^7Or#EX! zuNaG=5bBqiWaOA`;%pF=#FRqRca$NCT^Re85?cloA!wgxuZu-|uyu+lp+M;GSEJ}TGp20%!sYudyKeOU=6X)6};Y)H6&e1(fz9CxI zF+zXV{XR81c#;3O_Wl>;ZQn{_Iga)UAQJqFyx5uFm{})Vg$+*E$9>RzC;5mN&OEfS zvydcJW_!l}kR7d&jzwWk(v}V3b&Y;oCw%@Qvr!LzNJ#L zjFDO+^BQ**zUC;swK6|3CTfsk?TWt@qax$!d)3#-sg&&X2`?KoSD3<`R#3-rUi_no zn0OfLO;s^PE@hEI@|@$F>APwEw`N_4Ced!rD{KOWao*P0OD4&$tM^3|}zDL(fX;b=XRrXy>3NA;kM9wCp%%_9~Vy)v&2~|q5 zS8=I9ypqFjlt{KU$i{wY}ToJ|xZ{ z9P&}|!_y0&Y;XaP2D_F(U@g0{;9)ykIgE~lU|g)v4%g+-O_Nsr!+M0#<}I!Jg*Rr{ zq*FyCOd0L`wDg9&HpGNfleLt+0(ci?hif#roVdf@+X{y>=U)k9h=EOfj1JPx18H<) zcFXc?_XN>M(XIcacz~$9zUF0hT|Ia7HyxPmYB-%y?{Pa_{Rvffg+Gc>uZI#%k$Yn8 zVUfTmwi&*g*`+F-!YZ_IPeIYMuI&8Ch5?!DJbe3&n;v6@D`w6nCPUV6rP&X^oyUDz zgoI}@WX)gXa-GGGuri)HH;z&fEqPomYdAgZ`zh`AQq@Z%yUr(WOpg56MhTQbgU4G! z|8sMfs2eRwf~iJ<=S?ht#HsM)WP;Mek8$jli~y|hkw`=$5jpIg|D8zxw}RXu50Ili za)AXB1kyeL*Z|M~5GP>702B;h6#y{<3<(c_5`d%uBO^c=10W1A1Tg@RRTuy%09*k2 z48ZB>V*soRxb@kAG2)Rx1U&jD0nh_DqP_(HasY6GHJC)wB@syaBz=7nazw(r0bc$0 z|2!cfRRP3;crXP~hy_tspBM@tF(fsjKL8Z~qynG~K+N7I2c(MRl?ea=V2Dfrk_Qm- zTmb+X0I>o`1eG4C2>25BLXqMCkcHq00PP2m8i+<56A&iM0Y+aH;7CMo0F5JvTp+6dH5C{Nbs0O%+oB-4V$O@pOAc!46!hoB=17JY_ArJ)s z#Ql#&6C^zo0LcO{24FOR^Z*nO0Y)H6(mMamnHyr zp*^+O02r}g1pp1f4hWYJ3Sa<(#@^o5o+9egI5IrAAn&1A<|<5@Gyi}0%-u@ zVVLXRHdhhgB_MU@0U#LwM7s!CcmP`hkOd$W01g8nx~Dxp6o^9N0Y;>V1cVf#9->Y@ zfD!?)0zjRFK?H=zXD_*=y#x@R5&Z)K3>F|H90Ric5I_=p5m2izhz39f1$zIXGK3h5 z0YH)ohrtPGPn5lT3W1P6qLH`u0SFC%9fY8eAd>FDuUAgQ_n!2VAJh?f>k<_1AB(my(mfy= zf>CG;0jZiJ!08jN0Z9S@X=0>N^#A37s1gw)qDthJeXrQ?5i~s>?1uRxX!3a3Tj+HPTpkplpMMyz+C{QXYwt~_QN8g6-4P%O&KZ02Zm$C0=4qt=df4;Cn3Qj$Es8n%u{fpzE$nA zX7ig)umYF$+xaNet5@UQqgEhtjPw(3@$Y2^^@ zio|t?V+$`JdWc&xM=Ztn5ii^b?|XB2^I620cD`RLRe2#kda%VTa5V2jqxWQV=^v#&X^Y07Re1cd2<-IO*Wub^$PAaUNj#*e@=e#^rp1CzzPIB)e z)n!ytyT2U1PCu<499Z%Jn55u5rlzKb0u7TsXqdl`@5ipr9c81&@k7+LhI;poxC&wZ z!(rx*xzye+oWI^oEghQO-YCr9>(3y&_vfz^W9i_mgCF(GW)C1vO6%ciNnQ_P!i2}@ z@Wz6}#$FQ7JB{b^j1tjJPTw{fCM3W&O*l@Fj@u&${mP4sJMP^j_v>J3_|9>_hJtDTmCvug936=qWvK z=Z;yrE?LnnH49P*g zj7O3Gq_LaF&6K~=?d3}BhFEqxi@5P~4T{b#AA0Rv|9Bf>_}rXrOCx@}hDa}@Cgx0h zxd+je)ta6Th!nEfZ71_=SGs?N_;0n@;yzK3*yz1hIbdJ2yD$#DW!c=K#2%j%6kW*+ zGOIh9DVG&T+S&SHyV;FfA={|z#ckU>qeIzPRAn=9gy{+HRQ54H*m zMiWLK2?>wZBYIz=A^6u}TrgY%the*4v!ni>!8$_*EW0E;A?RzRx37p+(cfSQ0XFa?4P05iE^`yq}JwkjU>8h%Q% zIwG8<(V4l76$uc5i3Az}UGcLS7HiA{SV)R_02P6;L7;FI_D3kiZI~40;CEwJ*eFlJ sFQiyUSkq4ktOdh_Eu$d7o&>@~go}290B=G>K?LeiPLb2Q%FKfP4^##Ic>n+a literal 0 HcmV?d00001 diff --git a/testdata/ome/v0.4/1/0.1.0.0.0 b/testdata/ome/v0.4/1/0.1.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..03e5ec15f2cd77a17670a6a71bb9531dfda81d32 GIT binary patch literal 1496 zcmZ9M4NMbf7{{Nx@hJjVC&dqnv?vHnECedk-dz>ka3EGEgQ^4ML)|8V%m&8L7B~z# z(hvm^oKmKaj)_)>ZYr>DI&=#f!(c-f7>ZLE2o;DFP}$R_CfhD|$?x9#-~V}^_n-GA z384*m0hpgoTY%Y!Dgt2C@f1T1s2y~pIAFQd2jxrRJR9_`PzLe*;!cxd>`Mv`UuusC z9@0gu0UmICLATcx_smBWa5BT3P4lX>A0jr-Ia}2^1hWwb=k+m$p_jKQa3vv~leEVs z396I(N`M=qOP`O_w1`nu*0UmM>toF+#4B$kYL+ha51~Lg?@3s3&ee7qa7XnH_vLvW zOTB<=6AP4!PMw*F(piI_iVRh{MON$hh1EupwjmO&8*)+&0g(|SDB3lrj6Xdiu@(^~ z<@HjR+j|jB`)q_d=<38O%U=%qXrt9~;GT8JjQa8(Ppo2U?NB{$& z)`q++y<8igiY6Pk^`x#>KWafdq~-0{b+7&%#KjJE_aZCW%McX-rrg}2Pt;hY@8>7# z4B*R2t|^6Fn%VYn(966a(|GNaTNXRt5`jmZa=>|a@+xZrP}G~);8;3cXpSJHW!NPk zd+lc!0rh~GtbNp_Fh{@~kvbJPPjq9!b5Y@yP`s;j2$&F&fb(bqP z*&IIEiYU<>@+&+s{4U~1bJJiadoJ}~2 z*gLs>yN%*)E#h~58G#Ok#0A6y*Ly1m^n2_O1OFr{qf;c~h#kR28X*~a)9Sr1VCB~v za<45z@AhPsGLQ9sfg&egzY{fOTl{g2cZTzHirBdZk}Iyb`&HB3{wPc`H0oAXEd?Y?KR{#OVEmJpnO9p8{$7?ATS1XJY8?Ah=6@ysEl+qnep zSbxk7L-=C9e|qLX17!~3dRWo!Gjqe_=pA{GeC-IQ@k9J^v^AqBjbjjtPLK2JmQ6jd zHuKa=(h;T#@5g3p5(}TL-ucltC`#*hOZUyv*C5tj416r@IKB$eIc;C9XH`p;R`tPb{#9r5NB1ASuAfA3RHes$X-NWkraHV^k zUBWUq^!~*q^!)`}WVrpf$=`LdwE7U-wA_jHLEpbfk%S}GMVFMnm9sb$k?*nj`25Ns zA!7M*fp}Z>p5=&nS=yCxPnD+srx0vOfn^w$#rstXE6guAXimWb_)Y=Eh{2tvU^mOq z42}rk0{$SS%}Sbe0TF;F*x?rQ00(Oi=!>6OrUc+~2uGq_F+xvpGyuhVgRK|{0XT{w znP$m>^i-Gcs}; TPMc;|$gm_s*vrWO9w7e!Al3UK literal 0 HcmV?d00001 diff --git a/testdata/ome/v0.5/0/c/0/0/0/0/0 b/testdata/ome/v0.5/0/c/0/0/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..ee0c69cb7c0ab52868404c1d0ced6361691eb336 GIT binary patch literal 7355 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 Date: Wed, 4 Mar 2026 15:21:26 +0100 Subject: [PATCH 3/8] add ome v0.4 and v0.5 implementation --- .../zarr/zarrjava/ome/MultiscaleImage.java | 75 ++++++++++++ .../ome/MultiscalesMetadataImage.java | 43 +++++++ .../zarrjava/ome/UnifiedMultiscaleNode.java | 21 ++++ .../zarrjava/ome/UnifiedSinglescaleNode.java | 17 +++ .../dev/zarr/zarrjava/ome/metadata/Axis.java | 41 +++++++ .../metadata/CoordinateTransformation.java | 45 +++++++ .../zarr/zarrjava/ome/metadata/Dataset.java | 23 ++++ .../ome/metadata/MultiscalesEntry.java | 58 +++++++++ .../zarrjava/ome/metadata/OmeMetadata.java | 22 ++++ .../zarrjava/ome/v0_4/MultiscaleImage.java | 115 ++++++++++++++++++ .../zarrjava/ome/v0_5/MultiscaleImage.java | 115 ++++++++++++++++++ 11 files changed, 575 insertions(+) create mode 100644 src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/UnifiedMultiscaleNode.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/UnifiedSinglescaleNode.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/Axis.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/CoordinateTransformation.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/Dataset.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/MultiscalesEntry.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java new file mode 100644 index 00000000..3bee244e --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java @@ -0,0 +1,75 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Node; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.utils.Utils; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Unified interface for reading OME-Zarr multiscale images across Zarr format versions. + */ +public interface MultiscaleImage { + + /** + * Returns the multiscale node descriptor at index {@code i}. + */ + UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException; + + /** + * Opens the scale level array at index {@code i} within the first multiscale entry. + */ + dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException; + + /** + * Returns the number of scale levels in the first multiscale entry. + */ + int getScaleLevelCount() throws ZarrException; + + /** + * Returns the axis names of the first multiscale entry. + */ + default List getAxisNames() throws ZarrException { + UnifiedMultiscaleNode node = getMultiscaleNode(0); + List names = new java.util.ArrayList<>(); + for (dev.zarr.zarrjava.ome.metadata.Axis axis : node.axes) { + names.add(axis.name); + } + return names; + } + + /** + * Opens an OME-Zarr multiscale image at the given store handle, auto-detecting the Zarr version. + * + *

Tries v0.5 (zarr.json with "ome" key) first, then v0.4 (.zattrs with "multiscales" key). + */ + static MultiscaleImage open(StoreHandle storeHandle) throws IOException, ZarrException { + // Try v0.5: zarr.json with "ome" key + StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON); + if (zarrJson.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper(); + byte[] bytes = Utils.toArray(zarrJson.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); + if (attrs != null && attrs.has("ome")) { + return dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage(storeHandle); + } + } + + // Try v0.4: .zattrs with "multiscales" key + StoreHandle zattrs = storeHandle.resolve(Node.ZATTRS); + if (zattrs.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v2.Node.makeObjectMapper(); + byte[] bytes = Utils.toArray(zattrs.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + if (root.has("multiscales")) { + return dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(storeHandle); + } + } + + throw new ZarrException("No OME-Zarr multiscale metadata found at " + storeHandle); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java new file mode 100644 index 00000000..9593eae5 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java @@ -0,0 +1,43 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Extension of {@link MultiscaleImage} that provides typed access to OME-Zarr multiscales metadata + * and supports creating new scale levels. + * + * @param the concrete multiscales entry type + */ +public interface MultiscalesMetadataImage extends MultiscaleImage { + + /** + * Returns the raw multiscales entry at index {@code i}. + */ + M getMultiscalesEntry(int i) throws ZarrException; + + /** + * Creates a new scale level array at {@code path} with the given metadata and coordinate transformations, + * then registers it in the multiscales metadata. + */ + void createScaleLevel( + String path, + dev.zarr.zarrjava.core.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException; + + @Override + default UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException { + M entry = getMultiscalesEntry(i); + List nodes = new ArrayList<>(); + for (dev.zarr.zarrjava.ome.metadata.Dataset dataset : entry.datasets) { + nodes.add(new UnifiedSinglescaleNode(dataset.path, dataset.coordinateTransformations)); + } + return new UnifiedMultiscaleNode(entry.name, entry.axes, nodes); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/UnifiedMultiscaleNode.java b/src/main/java/dev/zarr/zarrjava/ome/UnifiedMultiscaleNode.java new file mode 100644 index 00000000..2eb5f61c --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/UnifiedMultiscaleNode.java @@ -0,0 +1,21 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ome.metadata.Axis; + +import javax.annotation.Nullable; +import java.util.List; + +/** A multiscale image node, using v1.0 RFC-8 terminology ("nodes" not "datasets"). */ +public final class UnifiedMultiscaleNode { + + @Nullable + public final String name; + public final List axes; + public final List nodes; + + public UnifiedMultiscaleNode(@Nullable String name, List axes, List nodes) { + this.name = name; + this.axes = axes; + this.nodes = nodes; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/UnifiedSinglescaleNode.java b/src/main/java/dev/zarr/zarrjava/ome/UnifiedSinglescaleNode.java new file mode 100644 index 00000000..2450a03c --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/UnifiedSinglescaleNode.java @@ -0,0 +1,17 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; + +import java.util.List; + +/** A single scale level within a multiscale image, using v1.0 RFC-8 terminology. */ +public final class UnifiedSinglescaleNode { + + public final String path; + public final List coordinateTransformations; + + public UnifiedSinglescaleNode(String path, List coordinateTransformations) { + this.path = path; + this.coordinateTransformations = coordinateTransformations; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/Axis.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/Axis.java new file mode 100644 index 00000000..895ea972 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/Axis.java @@ -0,0 +1,41 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class Axis { + + public final String name; + @Nullable + public final String type; + @Nullable + public final String unit; + @Nullable + public final Boolean discrete; + @Nullable + @JsonProperty("long_name") + public final String longName; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Axis( + @JsonProperty(value = "name", required = true) String name, + @Nullable @JsonProperty("type") String type, + @Nullable @JsonProperty("unit") String unit, + @Nullable @JsonProperty("discrete") Boolean discrete, + @Nullable @JsonProperty("long_name") String longName + ) { + this.name = name; + this.type = type; + this.unit = unit; + this.discrete = discrete; + this.longName = longName; + } + + public Axis(String name, @Nullable String type, @Nullable String unit) { + this(name, type, unit, null, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/CoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/CoordinateTransformation.java new file mode 100644 index 00000000..72755ddf --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/CoordinateTransformation.java @@ -0,0 +1,45 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CoordinateTransformation { + + public final String type; + @Nullable + public final List scale; + @Nullable + public final List translation; + @Nullable + public final String path; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CoordinateTransformation( + @JsonProperty(value = "type", required = true) String type, + @Nullable @JsonProperty("scale") List scale, + @Nullable @JsonProperty("translation") List translation, + @Nullable @JsonProperty("path") String path + ) { + this.type = type; + this.scale = scale; + this.translation = translation; + this.path = path; + } + + public static CoordinateTransformation scale(List scale) { + return new CoordinateTransformation("scale", scale, null, null); + } + + public static CoordinateTransformation translation(List translation) { + return new CoordinateTransformation("translation", null, translation, null); + } + + public static CoordinateTransformation identity() { + return new CoordinateTransformation("identity", null, null, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/Dataset.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/Dataset.java new file mode 100644 index 00000000..71117687 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/Dataset.java @@ -0,0 +1,23 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public final class Dataset { + + public final String path; + @JsonProperty("coordinateTransformations") + public final List coordinateTransformations; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Dataset( + @JsonProperty(value = "path", required = true) String path, + @JsonProperty(value = "coordinateTransformations", required = true) + List coordinateTransformations + ) { + this.path = path; + this.coordinateTransformations = coordinateTransformations; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/MultiscalesEntry.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/MultiscalesEntry.java new file mode 100644 index 00000000..05ae05cb --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/MultiscalesEntry.java @@ -0,0 +1,58 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class MultiscalesEntry { + + public final List axes; + public final List datasets; + @Nullable + @JsonProperty("coordinateTransformations") + public final List coordinateTransformations; + @Nullable + public final String name; + @Nullable + public final String type; + @Nullable + public final Map metadata; + @Nullable + public final String version; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public MultiscalesEntry( + @JsonProperty(value = "axes", required = true) List axes, + @JsonProperty(value = "datasets", required = true) List datasets, + @Nullable @JsonProperty("coordinateTransformations") List coordinateTransformations, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("type") String type, + @Nullable @JsonProperty("metadata") Map metadata, + @Nullable @JsonProperty("version") String version + ) { + this.axes = axes; + this.datasets = datasets; + this.coordinateTransformations = coordinateTransformations; + this.name = name; + this.type = type; + this.metadata = metadata; + this.version = version; + } + + public MultiscalesEntry(List axes, List datasets) { + this(axes, datasets, null, null, null, null, null); + } + + /** Returns a new MultiscalesEntry with the given dataset appended. */ + public MultiscalesEntry withDataset(Dataset dataset) { + List updated = new ArrayList<>(this.datasets); + updated.add(dataset); + return new MultiscalesEntry(axes, updated, coordinateTransformations, name, type, metadata, version); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java new file mode 100644 index 00000000..3d494802 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java @@ -0,0 +1,22 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** OME-Zarr metadata stored under {@code attributes["ome"]} (v0.5). */ +public final class OmeMetadata { + + public final String version; + public final List multiscales; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeMetadata( + @JsonProperty(value = "version", required = true) String version, + @JsonProperty(value = "multiscales", required = true) List multiscales + ) { + this.version = version; + this.multiscales = multiscales; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java new file mode 100644 index 00000000..4345bb46 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java @@ -0,0 +1,115 @@ +package dev.zarr.zarrjava.ome.v0_4; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.Dataset; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v2.Array; +import dev.zarr.zarrjava.v2.Group; +import dev.zarr.zarrjava.v2.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static dev.zarr.zarrjava.v2.Node.makeObjectMapper; + +/** + * OME-Zarr v0.4 multiscale image backed by a Zarr v2 group. + */ +public final class MultiscaleImage extends Group implements MultiscalesMetadataImage { + + private List multiscales; + + private MultiscaleImage( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull List multiscales + ) { + super(storeHandle, groupMetadata); + this.multiscales = multiscales; + } + + /** + * Opens an existing OME-Zarr v0.4 multiscale image at the given store handle. + */ + public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("multiscales")) { + throw new ZarrException("No 'multiscales' key found in attributes at " + storeHandle); + } + List multiscales = mapper.convertValue( + attributes.get("multiscales"), + new TypeReference>() {} + ); + return new MultiscaleImage(storeHandle, group.metadata, multiscales); + } + + /** + * Creates a new OME-Zarr v0.4 multiscale image at the given store handle. + */ + public static MultiscaleImage create( + @Nonnull StoreHandle storeHandle, + @Nonnull MultiscalesEntry multiscalesEntry + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + List multiscales = Collections.singletonList(multiscalesEntry); + @SuppressWarnings("unchecked") + List multiscalesList = mapper.convertValue(multiscales, List.class); + Attributes attributes = new Attributes(); + attributes.put("multiscales", multiscalesList); + Group group = Group.create(storeHandle, attributes); + return new MultiscaleImage(storeHandle, group.metadata, multiscales); + } + + @Override + public MultiscalesEntry getMultiscalesEntry(int i) throws ZarrException { + return multiscales.get(i); + } + + @Override + public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { + String path = getMultiscalesEntry(0).datasets.get(i).path; + return Array.open(storeHandle.resolve(path)); + } + + @Override + public int getScaleLevelCount() throws ZarrException { + return getMultiscalesEntry(0).datasets.size(); + } + + @Override + public void createScaleLevel( + String path, + dev.zarr.zarrjava.core.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException { + if (!(arrayMetadata instanceof dev.zarr.zarrjava.v2.ArrayMetadata)) { + throw new ZarrException("Expected v2.ArrayMetadata for OME-Zarr v0.4, got " + arrayMetadata.getClass()); + } + Array.create(storeHandle.resolve(path), (dev.zarr.zarrjava.v2.ArrayMetadata) arrayMetadata); + + MultiscalesEntry current = multiscales.get(0); + MultiscalesEntry updated = current.withDataset(new Dataset(path, coordinateTransformations)); + List updatedList = new ArrayList<>(multiscales); + updatedList.set(0, updated); + multiscales = updatedList; + + ObjectMapper mapper = makeObjectMapper(); + @SuppressWarnings("unchecked") + List multiscalesList = mapper.convertValue(multiscales, List.class); + Attributes newAttributes = new Attributes(); + newAttributes.put("multiscales", multiscalesList); + setAttributes(newAttributes); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java new file mode 100644 index 00000000..e9c78c93 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java @@ -0,0 +1,115 @@ +package dev.zarr.zarrjava.ome.v0_5; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.Dataset; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.metadata.OmeMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; + +/** + * OME-Zarr v0.5 multiscale image backed by a Zarr v3 group. + */ +public final class MultiscaleImage extends Group implements MultiscalesMetadataImage { + + private OmeMetadata omeMetadata; + + private MultiscaleImage( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v0.5 multiscale image at the given store handle. + */ + public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("ome")) { + throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); + } + OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + if (!omeMetadata.version.startsWith("0.5")) { + throw new ZarrException( + "Expected OME-Zarr version '0.5', got '" + omeMetadata.version + "' at " + storeHandle); + } + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v0.5 multiscale image at the given store handle. + */ + public static MultiscaleImage create( + @Nonnull StoreHandle storeHandle, + @Nonnull MultiscalesEntry multiscalesEntry + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + OmeMetadata omeMetadata = new OmeMetadata("0.5", Collections.singletonList(multiscalesEntry)); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes attributes = new Attributes(); + attributes.put("ome", omeMap); + Group group = Group.create(storeHandle, attributes); + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + @Override + public MultiscalesEntry getMultiscalesEntry(int i) throws ZarrException { + return omeMetadata.multiscales.get(i); + } + + @Override + public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { + String path = getMultiscalesEntry(0).datasets.get(i).path; + return Array.open(storeHandle.resolve(path)); + } + + @Override + public int getScaleLevelCount() throws ZarrException { + return getMultiscalesEntry(0).datasets.size(); + } + + @Override + public void createScaleLevel( + String path, + dev.zarr.zarrjava.core.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException { + if (!(arrayMetadata instanceof dev.zarr.zarrjava.v3.ArrayMetadata)) { + throw new ZarrException("Expected v3.ArrayMetadata for OME-Zarr v0.5, got " + arrayMetadata.getClass()); + } + Array.create(storeHandle.resolve(path), (dev.zarr.zarrjava.v3.ArrayMetadata) arrayMetadata); + + MultiscalesEntry current = omeMetadata.multiscales.get(0); + MultiscalesEntry updated = current.withDataset(new Dataset(path, coordinateTransformations)); + List updatedList = new java.util.ArrayList<>(omeMetadata.multiscales); + updatedList.set(0, updated); + omeMetadata = new OmeMetadata(omeMetadata.version, updatedList); + + ObjectMapper mapper = makeObjectMapper(); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes newAttributes = new Attributes(); + newAttributes.put("ome", omeMap); + setAttributes(newAttributes); + } +} From 33f57b763e1f1165296c1828b72e4cd6f4487a07 Mon Sep 17 00:00:00 2001 From: brokkoli71 Date: Wed, 4 Mar 2026 15:26:07 +0100 Subject: [PATCH 4/8] add ome v0.4 and v0.5 tests --- .../java/dev/zarr/zarrjava/OmeZarrTest.java | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/test/java/dev/zarr/zarrjava/OmeZarrTest.java diff --git a/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java b/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java new file mode 100644 index 00000000..89d35b10 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java @@ -0,0 +1,205 @@ +package dev.zarr.zarrjava; + +import dev.zarr.zarrjava.ome.MultiscaleImage; +import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.ome.UnifiedMultiscaleNode; +import dev.zarr.zarrjava.ome.UnifiedSinglescaleNode; +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.DataType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrTest extends ZarrTest { + + private StoreHandle storeHandle(java.nio.file.Path path) throws Exception { + return new FilesystemStore(path).resolve(); + } + + // ── v0.5 read tests ────────────────────────────────────────────────────── + + @Test + void readV05_axesAndDatasets() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); + + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, image); + + UnifiedMultiscaleNode node = image.getMultiscaleNode(0); + assertEquals("test_image", node.name); + assertEquals(5, node.axes.size()); + assertEquals(2, node.nodes.size()); + + List axisNames = Arrays.asList("t", "c", "z", "y", "x"); + for (int i = 0; i < axisNames.size(); i++) { + assertEquals(axisNames.get(i), node.axes.get(i).name); + } + + List axisTypes = Arrays.asList("time", "channel", "space", "space", "space"); + for (int i = 0; i < axisTypes.size(); i++) { + assertEquals(axisTypes.get(i), node.axes.get(i).type); + } + + UnifiedSinglescaleNode scaleNode0 = node.nodes.get(0); + assertEquals("0", scaleNode0.path); + assertEquals("scale", scaleNode0.coordinateTransformations.get(0).type); + } + + @Test + void readV04_axesAndDatasets() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.4"))); + + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, image); + + UnifiedMultiscaleNode node = image.getMultiscaleNode(0); + assertEquals("test_image", node.name); + assertEquals(5, node.axes.size()); + assertEquals(2, node.nodes.size()); + + List axisNames = Arrays.asList("t", "c", "z", "y", "x"); + for (int i = 0; i < axisNames.size(); i++) { + assertEquals(axisNames.get(i), node.axes.get(i).name); + } + + List axisTypes = Arrays.asList("time", "channel", "space", "space", "space"); + for (int i = 0; i < axisTypes.size(); i++) { + assertEquals(axisTypes.get(i), node.axes.get(i).type); + } + + UnifiedSinglescaleNode scaleNode0 = node.nodes.get(0); + assertEquals("0", scaleNode0.path); + assertEquals("scale", scaleNode0.coordinateTransformations.get(0).type); + } + + @Test + void readV05_getAxisNames() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), image.getAxisNames()); + } + + @Test + void readV05_openScaleLevel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); + + dev.zarr.zarrjava.core.Array level0 = image.openScaleLevel(0); + assertArrayEquals(new long[]{1, 2, 8, 16, 16}, level0.metadata().shape); + + dev.zarr.zarrjava.core.Array level1 = image.openScaleLevel(1); + assertArrayEquals(new long[]{1, 2, 4, 8, 8}, level1.metadata().shape); + } + + @Test + void readV04_openScaleLevel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.4"))); + + dev.zarr.zarrjava.core.Array level0 = image.openScaleLevel(0); + assertArrayEquals(new long[]{1, 2, 8, 16, 16}, level0.metadata().shape); + + dev.zarr.zarrjava.core.Array level1 = image.openScaleLevel(1); + assertArrayEquals(new long[]{1, 2, 4, 8, 8}, level1.metadata().shape); + } + + @Test + void readV05_scaleLevelCount() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); + assertEquals(2, image.getScaleLevelCount()); + } + + // ── v0.5 write tests ───────────────────────────────────────────────────── + + @Test + void writeV05_createAndReopen() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer") + ); + MultiscalesEntry entry = new MultiscalesEntry(axes, Collections.emptyList()); + + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_create")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(handle, entry); + + dev.zarr.zarrjava.v3.ArrayMetadata arrayMetadata = Array.metadataBuilder() + .withShape(16, 16) + .withChunkShape(16, 16) + .withDataType(DataType.FLOAT32) + .build(); + List transforms = + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(0.5, 0.5))); + created.createScaleLevel("0", arrayMetadata, transforms); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, reopened); + assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); + assertEquals(1, reopened.getScaleLevelCount()); + assertEquals("0", reopened.getMultiscaleNode(0).nodes.get(0).path); + } + + // ── v0.4 write tests ───────────────────────────────────────────────────── + + @Test + void writeV04_createAndReopen() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer") + ); + MultiscalesEntry entry = new MultiscalesEntry(axes, Collections.emptyList()); + + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_create")); + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.create(handle, entry); + + dev.zarr.zarrjava.v2.ArrayMetadata arrayMetadata = new dev.zarr.zarrjava.v2.ArrayMetadata( + 2, + new long[]{16, 16}, + new int[]{16, 16}, + dev.zarr.zarrjava.v2.DataType.FLOAT32, + 0, + dev.zarr.zarrjava.v2.Order.C, + null, + null, + null + ); + List transforms = + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(0.5, 0.5))); + created.createScaleLevel("0", arrayMetadata, transforms); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, reopened); + assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); + assertEquals(1, reopened.getScaleLevelCount()); + assertEquals("0", reopened.getMultiscaleNode(0).nodes.get(0).path); + } + + // ── typed metadata tests ───────────────────────────────────────────────── + + @Test + void readV05_typedMetadata() throws Exception { + MultiscalesMetadataImage image = + (MultiscalesMetadataImage) MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); + + MultiscalesEntry entry = image.getMultiscalesEntry(0); + assertEquals("test_image", entry.name); + assertNull(entry.version); + + List expectedScale = Arrays.asList(1.0, 1.0, 0.5, 0.5, 0.5); + assertEquals(expectedScale, entry.datasets.get(0).coordinateTransformations.get(0).scale); + } + + @Test + void readV04_entryHasVersion() throws Exception { + MultiscalesMetadataImage image = + (MultiscalesMetadataImage) MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.4"))); + + MultiscalesEntry entry = image.getMultiscalesEntry(0); + assertEquals("0.4", entry.version); + } +} From 6f31a28c54fcba8fd5ccd8a0bf331e4f7d29df23 Mon Sep 17 00:00:00 2001 From: brokkoli71 Date: Thu, 5 Mar 2026 11:40:17 +0100 Subject: [PATCH 5/8] add omero, bioformats2raw, labels, hcs --- .../zarr/zarrjava/ome/MultiscaleImage.java | 58 ++++++++++++- .../java/dev/zarr/zarrjava/ome/Plate.java | 60 ++++++++++++++ src/main/java/dev/zarr/zarrjava/ome/Well.java | 60 ++++++++++++++ .../zarrjava/ome/metadata/Acquisition.java | 41 +++++++++ .../zarrjava/ome/metadata/NamedEntry.java | 19 +++++ .../zarrjava/ome/metadata/OmeMetadata.java | 28 ++++++- .../zarrjava/ome/metadata/OmeroMetadata.java | 28 +++++++ .../zarrjava/ome/metadata/PlateMetadata.java | 44 ++++++++++ .../zarr/zarrjava/ome/metadata/WellImage.java | 25 ++++++ .../zarrjava/ome/metadata/WellMetadata.java | 30 +++++++ .../zarr/zarrjava/ome/metadata/WellRef.java | 25 ++++++ .../zarrjava/ome/v0_4/MultiscaleImage.java | 59 ++++++++++++- .../dev/zarr/zarrjava/ome/v0_4/Plate.java | 77 +++++++++++++++++ .../java/dev/zarr/zarrjava/ome/v0_4/Well.java | 78 +++++++++++++++++ .../zarrjava/ome/v0_5/MultiscaleImage.java | 5 ++ .../dev/zarr/zarrjava/ome/v0_5/Plate.java | 82 ++++++++++++++++++ .../java/dev/zarr/zarrjava/ome/v0_5/Well.java | 83 +++++++++++++++++++ 17 files changed, 797 insertions(+), 5 deletions(-) create mode 100644 src/main/java/dev/zarr/zarrjava/ome/Plate.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/Well.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/Acquisition.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/NamedEntry.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/OmeroMetadata.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/PlateMetadata.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/WellImage.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/WellMetadata.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/metadata/WellRef.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java index 3bee244e..b8f9a589 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java @@ -7,6 +7,8 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -14,6 +16,11 @@ */ public interface MultiscaleImage { + /** + * Returns the store handle for this multiscale image node. + */ + StoreHandle getStoreHandle(); + /** * Returns the multiscale node descriptor at index {@code i}. */ @@ -34,13 +41,62 @@ public interface MultiscaleImage { */ default List getAxisNames() throws ZarrException { UnifiedMultiscaleNode node = getMultiscaleNode(0); - List names = new java.util.ArrayList<>(); + List names = new ArrayList<>(); for (dev.zarr.zarrjava.ome.metadata.Axis axis : node.axes) { names.add(axis.name); } return names; } + /** + * Returns all label names from the {@code labels/} sub-group, or an empty list if none exist. + */ + default List getLabels() throws IOException, ZarrException { + StoreHandle labelsHandle = getStoreHandle().resolve("labels"); + + // Try v0.5: labels/zarr.json with {"attributes": {"labels": [...]}} + StoreHandle zarrJson = labelsHandle.resolve(Node.ZARR_JSON); + if (zarrJson.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper(); + byte[] bytes = Utils.toArray(zarrJson.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); + if (attrs != null && attrs.has("labels")) { + com.fasterxml.jackson.databind.JsonNode labelsNode = attrs.get("labels"); + List result = new ArrayList<>(); + for (com.fasterxml.jackson.databind.JsonNode item : labelsNode) { + result.add(item.asText()); + } + return result; + } + } + + // Try v0.4: labels/.zattrs with {"labels": [...]} + StoreHandle zattrs = labelsHandle.resolve(Node.ZATTRS); + if (zattrs.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v2.Node.makeObjectMapper(); + byte[] bytes = Utils.toArray(zattrs.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + if (root.has("labels")) { + com.fasterxml.jackson.databind.JsonNode labelsNode = root.get("labels"); + List result = new ArrayList<>(); + for (com.fasterxml.jackson.databind.JsonNode item : labelsNode) { + result.add(item.asText()); + } + return result; + } + } + + return Collections.emptyList(); + } + + /** + * Opens the named label image from the {@code labels/} sub-group. + */ + default MultiscaleImage openLabel(String name) throws IOException, ZarrException { + return MultiscaleImage.open(getStoreHandle().resolve("labels").resolve(name)); + } + /** * Opens an OME-Zarr multiscale image at the given store handle, auto-detecting the Zarr version. * diff --git a/src/main/java/dev/zarr/zarrjava/ome/Plate.java b/src/main/java/dev/zarr/zarrjava/ome/Plate.java new file mode 100644 index 00000000..ea88d2dd --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/Plate.java @@ -0,0 +1,60 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Node; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.utils.Utils; + +import java.io.IOException; + +/** + * Unified interface for reading OME-Zarr HCS plates across Zarr format versions. + */ +public interface Plate { + + /** + * Returns the plate metadata. + */ + PlateMetadata getPlateMetadata() throws ZarrException; + + /** + * Opens the well at the given row/column path (e.g. {@code "A/1"}). + */ + Well openWell(String rowColPath) throws IOException, ZarrException; + + /** + * Returns the store handle for this plate node. + */ + StoreHandle getStoreHandle(); + + /** + * Opens an OME-Zarr plate at the given store handle, auto-detecting the Zarr version. + */ + static Plate open(StoreHandle storeHandle) throws IOException, ZarrException { + // Try v0.5: zarr.json with "ome" -> "plate" + StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON); + if (zarrJson.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper(); + byte[] bytes = Utils.toArray(zarrJson.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); + if (attrs != null && attrs.has("ome") && attrs.get("ome").has("plate")) { + return dev.zarr.zarrjava.ome.v0_5.Plate.openPlate(storeHandle); + } + } + + // Try v0.4: .zattrs with "plate" + StoreHandle zattrs = storeHandle.resolve(Node.ZATTRS); + if (zattrs.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v2.Node.makeObjectMapper(); + byte[] bytes = Utils.toArray(zattrs.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + if (root.has("plate")) { + return dev.zarr.zarrjava.ome.v0_4.Plate.openPlate(storeHandle); + } + } + + throw new ZarrException("No OME-Zarr plate metadata found at " + storeHandle); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/Well.java b/src/main/java/dev/zarr/zarrjava/ome/Well.java new file mode 100644 index 00000000..e39b859b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/Well.java @@ -0,0 +1,60 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Node; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.utils.Utils; + +import java.io.IOException; + +/** + * Unified interface for reading OME-Zarr HCS wells across Zarr format versions. + */ +public interface Well { + + /** + * Returns the well metadata. + */ + WellMetadata getWellMetadata() throws ZarrException; + + /** + * Opens the image at the given path within this well (e.g. {@code "0"}). + */ + MultiscaleImage openImage(String path) throws IOException, ZarrException; + + /** + * Returns the store handle for this well node. + */ + StoreHandle getStoreHandle(); + + /** + * Opens an OME-Zarr well at the given store handle, auto-detecting the Zarr version. + */ + static Well open(StoreHandle storeHandle) throws IOException, ZarrException { + // Try v0.5: zarr.json with "ome" -> "well" + StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON); + if (zarrJson.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper(); + byte[] bytes = Utils.toArray(zarrJson.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); + if (attrs != null && attrs.has("ome") && attrs.get("ome").has("well")) { + return dev.zarr.zarrjava.ome.v0_5.Well.openWell(storeHandle); + } + } + + // Try v0.4: .zattrs with "well" + StoreHandle zattrs = storeHandle.resolve(Node.ZATTRS); + if (zattrs.exists()) { + com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v2.Node.makeObjectMapper(); + byte[] bytes = Utils.toArray(zattrs.readNonNull()); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); + if (root.has("well")) { + return dev.zarr.zarrjava.ome.v0_4.Well.openWell(storeHandle); + } + } + + throw new ZarrException("No OME-Zarr well metadata found at " + storeHandle); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/Acquisition.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/Acquisition.java new file mode 100644 index 00000000..42f8de42 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/Acquisition.java @@ -0,0 +1,41 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** An HCS acquisition entry within a plate. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class Acquisition { + + public final int id; + @Nullable + public final String name; + @Nullable + public final Integer maximumfieldcount; + @Nullable + public final String description; + @Nullable + public final Long starttime; + @Nullable + public final Long endtime; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Acquisition( + @JsonProperty(value = "id", required = true) int id, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("maximumfieldcount") Integer maximumfieldcount, + @Nullable @JsonProperty("description") String description, + @Nullable @JsonProperty("starttime") Long starttime, + @Nullable @JsonProperty("endtime") Long endtime + ) { + this.id = id; + this.name = name; + this.maximumfieldcount = maximumfieldcount; + this.description = description; + this.starttime = starttime; + this.endtime = endtime; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/NamedEntry.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/NamedEntry.java new file mode 100644 index 00000000..ea3e45de --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/NamedEntry.java @@ -0,0 +1,19 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** A named entry used for plate rows/columns. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class NamedEntry { + + public final String name; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public NamedEntry( + @JsonProperty(value = "name", required = true) String name + ) { + this.name = name; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java index 3d494802..c324bf4e 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeMetadata.java @@ -1,22 +1,48 @@ package dev.zarr.zarrjava.ome.metadata; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nullable; import java.util.List; /** OME-Zarr metadata stored under {@code attributes["ome"]} (v0.5). */ +@JsonInclude(JsonInclude.Include.NON_NULL) public final class OmeMetadata { public final String version; + @Nullable public final List multiscales; + @Nullable + public final OmeroMetadata omero; + @Nullable + @JsonProperty("bioformats2raw.layout") + public final Integer bioformats2rawLayout; + @Nullable + public final PlateMetadata plate; + @Nullable + public final WellMetadata well; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public OmeMetadata( @JsonProperty(value = "version", required = true) String version, - @JsonProperty(value = "multiscales", required = true) List multiscales + @Nullable @JsonProperty("multiscales") List multiscales, + @Nullable @JsonProperty("omero") OmeroMetadata omero, + @Nullable @JsonProperty("bioformats2raw.layout") Integer bioformats2rawLayout, + @Nullable @JsonProperty("plate") PlateMetadata plate, + @Nullable @JsonProperty("well") WellMetadata well ) { this.version = version; this.multiscales = multiscales; + this.omero = omero; + this.bioformats2rawLayout = bioformats2rawLayout; + this.plate = plate; + this.well = well; + } + + /** Convenience constructor for multiscale images (omero/layout/plate/well all null). */ + public OmeMetadata(String version, List multiscales) { + this(version, multiscales, null, null, null, null); } } diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeroMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeroMetadata.java new file mode 100644 index 00000000..5b27b0c2 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/OmeroMetadata.java @@ -0,0 +1,28 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; + +/** Omero display metadata stored in OME-Zarr attributes. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeroMetadata { + + @Nullable + public final List> channels; + @Nullable + public final Map rdefs; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeroMetadata( + @Nullable @JsonProperty("channels") List> channels, + @Nullable @JsonProperty("rdefs") Map rdefs + ) { + this.channels = channels; + this.rdefs = rdefs; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/PlateMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/PlateMetadata.java new file mode 100644 index 00000000..17ec1dda --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/PlateMetadata.java @@ -0,0 +1,44 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +/** OME-Zarr HCS plate metadata. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class PlateMetadata { + + public final List columns; + public final List rows; + public final List wells; + @Nullable + public final List acquisitions; + @Nullable + public final Integer field_count; + @Nullable + public final String name; + @Nullable + public final String version; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public PlateMetadata( + @JsonProperty(value = "columns", required = true) List columns, + @JsonProperty(value = "rows", required = true) List rows, + @JsonProperty(value = "wells", required = true) List wells, + @Nullable @JsonProperty("acquisitions") List acquisitions, + @Nullable @JsonProperty("field_count") Integer field_count, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("version") String version + ) { + this.columns = columns; + this.rows = rows; + this.wells = wells; + this.acquisitions = acquisitions; + this.field_count = field_count; + this.name = name; + this.version = version; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/WellImage.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellImage.java new file mode 100644 index 00000000..7bd9fe31 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellImage.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** A reference to an image within a well. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class WellImage { + + public final String path; + @Nullable + public final Integer acquisition; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public WellImage( + @JsonProperty(value = "path", required = true) String path, + @Nullable @JsonProperty("acquisition") Integer acquisition + ) { + this.path = path; + this.acquisition = acquisition; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/WellMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellMetadata.java new file mode 100644 index 00000000..fec4a899 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellMetadata.java @@ -0,0 +1,30 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +/** OME-Zarr HCS well metadata. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class WellMetadata { + + public final List images; + @Nullable + public final String version; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public WellMetadata( + @JsonProperty(value = "images", required = true) List images, + @Nullable @JsonProperty("version") String version + ) { + this.images = images; + this.version = version; + } + + public WellMetadata(List images) { + this(images, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/metadata/WellRef.java b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellRef.java new file mode 100644 index 00000000..19cc713f --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/metadata/WellRef.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** A reference to a well within a plate, identified by path and row/column indices. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class WellRef { + + public final String path; + public final int rowIndex; + public final int columnIndex; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public WellRef( + @JsonProperty(value = "path", required = true) String path, + @JsonProperty(value = "rowIndex", required = true) int rowIndex, + @JsonProperty(value = "columnIndex", required = true) int columnIndex + ) { + this.path = path; + this.rowIndex = rowIndex; + this.columnIndex = columnIndex; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java index 4345bb46..dfdf46eb 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java @@ -8,12 +8,14 @@ import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; import dev.zarr.zarrjava.ome.metadata.Dataset; import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.metadata.OmeroMetadata; import dev.zarr.zarrjava.store.StoreHandle; import dev.zarr.zarrjava.v2.Array; import dev.zarr.zarrjava.v2.Group; import dev.zarr.zarrjava.v2.GroupMetadata; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -28,14 +30,22 @@ public final class MultiscaleImage extends Group implements MultiscalesMetadataImage { private List multiscales; + @Nullable + private OmeroMetadata omeroMetadata; + @Nullable + private Integer bioformats2rawLayout; private MultiscaleImage( @Nonnull StoreHandle storeHandle, @Nonnull GroupMetadata groupMetadata, - @Nonnull List multiscales + @Nonnull List multiscales, + @Nullable OmeroMetadata omeroMetadata, + @Nullable Integer bioformats2rawLayout ) { super(storeHandle, groupMetadata); this.multiscales = multiscales; + this.omeroMetadata = omeroMetadata; + this.bioformats2rawLayout = bioformats2rawLayout; } /** @@ -52,7 +62,18 @@ public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHand attributes.get("multiscales"), new TypeReference>() {} ); - return new MultiscaleImage(storeHandle, group.metadata, multiscales); + OmeroMetadata omeroMetadata = null; + if (attributes.containsKey("omero")) { + omeroMetadata = mapper.convertValue(attributes.get("omero"), OmeroMetadata.class); + } + Integer bioformats2rawLayout = null; + if (attributes.containsKey("bioformats2raw.layout")) { + Object raw = attributes.get("bioformats2raw.layout"); + if (raw instanceof Number) { + bioformats2rawLayout = ((Number) raw).intValue(); + } + } + return new MultiscaleImage(storeHandle, group.metadata, multiscales, omeroMetadata, bioformats2rawLayout); } /** @@ -69,7 +90,27 @@ public static MultiscaleImage create( Attributes attributes = new Attributes(); attributes.put("multiscales", multiscalesList); Group group = Group.create(storeHandle, attributes); - return new MultiscaleImage(storeHandle, group.metadata, multiscales); + return new MultiscaleImage(storeHandle, group.metadata, multiscales, null, null); + } + + @Override + public dev.zarr.zarrjava.store.StoreHandle getStoreHandle() { + return this.storeHandle; + } + + @Nullable + public OmeroMetadata getOmeroMetadata() { + return omeroMetadata; + } + + public void setOmeroMetadata(@Nullable OmeroMetadata omeroMetadata) throws IOException, ZarrException { + this.omeroMetadata = omeroMetadata; + persistAttributes(); + } + + @Nullable + public Integer getBioformats2rawLayout() { + return bioformats2rawLayout; } @Override @@ -105,11 +146,23 @@ public void createScaleLevel( updatedList.set(0, updated); multiscales = updatedList; + persistAttributes(); + } + + private void persistAttributes() throws IOException, ZarrException { ObjectMapper mapper = makeObjectMapper(); @SuppressWarnings("unchecked") List multiscalesList = mapper.convertValue(multiscales, List.class); Attributes newAttributes = new Attributes(); newAttributes.put("multiscales", multiscalesList); + if (omeroMetadata != null) { + @SuppressWarnings("unchecked") + Map omeroMap = mapper.convertValue(omeroMetadata, Map.class); + newAttributes.put("omero", omeroMap); + } + if (bioformats2rawLayout != null) { + newAttributes.put("bioformats2raw.layout", bioformats2rawLayout); + } setAttributes(newAttributes); } } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java new file mode 100644 index 00000000..51ad777b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java @@ -0,0 +1,77 @@ +package dev.zarr.zarrjava.ome.v0_4; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v2.Group; +import dev.zarr.zarrjava.v2.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Map; + +import static dev.zarr.zarrjava.v2.Node.makeObjectMapper; + +/** + * OME-Zarr v0.4 HCS plate backed by a Zarr v2 group. + */ +public final class Plate extends Group implements dev.zarr.zarrjava.ome.Plate { + + private PlateMetadata plateMetadata; + + private Plate( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull PlateMetadata plateMetadata + ) { + super(storeHandle, groupMetadata); + this.plateMetadata = plateMetadata; + } + + /** + * Opens an existing OME-Zarr v0.4 plate at the given store handle. + */ + public static Plate openPlate(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("plate")) { + throw new ZarrException("No 'plate' key found in attributes at " + storeHandle); + } + PlateMetadata plateMetadata = mapper.convertValue(attributes.get("plate"), PlateMetadata.class); + return new Plate(storeHandle, group.metadata, plateMetadata); + } + + /** + * Creates a new OME-Zarr v0.4 plate at the given store handle. + */ + public static Plate createPlate( + @Nonnull StoreHandle storeHandle, + @Nonnull PlateMetadata plateMetadata + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + @SuppressWarnings("unchecked") + Map plateMap = mapper.convertValue(plateMetadata, Map.class); + Attributes attributes = new Attributes(); + attributes.put("plate", plateMap); + Group group = Group.create(storeHandle, attributes); + return new Plate(storeHandle, group.metadata, plateMetadata); + } + + @Override + public PlateMetadata getPlateMetadata() throws ZarrException { + return plateMetadata; + } + + @Override + public dev.zarr.zarrjava.ome.Well openWell(String rowColPath) throws IOException, ZarrException { + return Well.openWell(storeHandle.resolve(rowColPath)); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java new file mode 100644 index 00000000..726ef1fa --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java @@ -0,0 +1,78 @@ +package dev.zarr.zarrjava.ome.v0_4; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.MultiscaleImage; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v2.Group; +import dev.zarr.zarrjava.v2.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Map; + +import static dev.zarr.zarrjava.v2.Node.makeObjectMapper; + +/** + * OME-Zarr v0.4 HCS well backed by a Zarr v2 group. + */ +public final class Well extends Group implements dev.zarr.zarrjava.ome.Well { + + private WellMetadata wellMetadata; + + private Well( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull WellMetadata wellMetadata + ) { + super(storeHandle, groupMetadata); + this.wellMetadata = wellMetadata; + } + + /** + * Opens an existing OME-Zarr v0.4 well at the given store handle. + */ + public static Well openWell(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("well")) { + throw new ZarrException("No 'well' key found in attributes at " + storeHandle); + } + WellMetadata wellMetadata = mapper.convertValue(attributes.get("well"), WellMetadata.class); + return new Well(storeHandle, group.metadata, wellMetadata); + } + + /** + * Creates a new OME-Zarr v0.4 well at the given store handle. + */ + public static Well createWell( + @Nonnull StoreHandle storeHandle, + @Nonnull WellMetadata wellMetadata + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + @SuppressWarnings("unchecked") + Map wellMap = mapper.convertValue(wellMetadata, Map.class); + Attributes attributes = new Attributes(); + attributes.put("well", wellMap); + Group group = Group.create(storeHandle, attributes); + return new Well(storeHandle, group.metadata, wellMetadata); + } + + @Override + public WellMetadata getWellMetadata() throws ZarrException { + return wellMetadata; + } + + @Override + public MultiscaleImage openImage(String path) throws IOException, ZarrException { + return MultiscaleImage.open(storeHandle.resolve(path)); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java index e9c78c93..ae61c82b 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java @@ -72,6 +72,11 @@ public static MultiscaleImage create( return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); } + @Override + public dev.zarr.zarrjava.store.StoreHandle getStoreHandle() { + return this.storeHandle; + } + @Override public MultiscalesEntry getMultiscalesEntry(int i) throws ZarrException { return omeMetadata.multiscales.get(i); diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java new file mode 100644 index 00000000..660171f5 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java @@ -0,0 +1,82 @@ +package dev.zarr.zarrjava.ome.v0_5; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.metadata.OmeMetadata; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Map; + +import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; + +/** + * OME-Zarr v0.5 HCS plate backed by a Zarr v3 group. + */ +public final class Plate extends Group implements dev.zarr.zarrjava.ome.Plate { + + private OmeMetadata omeMetadata; + + private Plate( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v0.5 plate at the given store handle. + */ + public static Plate openPlate(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("ome")) { + throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); + } + OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + if (omeMetadata.plate == null) { + throw new ZarrException("No 'plate' found in ome metadata at " + storeHandle); + } + return new Plate(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v0.5 plate at the given store handle. + */ + public static Plate createPlate( + @Nonnull StoreHandle storeHandle, + @Nonnull PlateMetadata plateMetadata + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + OmeMetadata omeMetadata = new OmeMetadata("0.5", null, null, null, plateMetadata, null); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes attributes = new Attributes(); + attributes.put("ome", omeMap); + Group group = Group.create(storeHandle, attributes); + return new Plate(storeHandle, group.metadata, omeMetadata); + } + + @Override + public PlateMetadata getPlateMetadata() throws ZarrException { + return omeMetadata.plate; + } + + @Override + public dev.zarr.zarrjava.ome.Well openWell(String rowColPath) throws IOException, ZarrException { + return Well.openWell(storeHandle.resolve(rowColPath)); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java new file mode 100644 index 00000000..171eb63e --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java @@ -0,0 +1,83 @@ +package dev.zarr.zarrjava.ome.v0_5; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.MultiscaleImage; +import dev.zarr.zarrjava.ome.metadata.OmeMetadata; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Map; + +import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; + +/** + * OME-Zarr v0.5 HCS well backed by a Zarr v3 group. + */ +public final class Well extends Group implements dev.zarr.zarrjava.ome.Well { + + private OmeMetadata omeMetadata; + + private Well( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v0.5 well at the given store handle. + */ + public static Well openWell(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("ome")) { + throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); + } + OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + if (omeMetadata.well == null) { + throw new ZarrException("No 'well' found in ome metadata at " + storeHandle); + } + return new Well(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v0.5 well at the given store handle. + */ + public static Well createWell( + @Nonnull StoreHandle storeHandle, + @Nonnull WellMetadata wellMetadata + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + OmeMetadata omeMetadata = new OmeMetadata("0.5", null, null, null, null, wellMetadata); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes attributes = new Attributes(); + attributes.put("ome", omeMap); + Group group = Group.create(storeHandle, attributes); + return new Well(storeHandle, group.metadata, omeMetadata); + } + + @Override + public WellMetadata getWellMetadata() throws ZarrException { + return omeMetadata.well; + } + + @Override + public MultiscaleImage openImage(String path) throws IOException, ZarrException { + return MultiscaleImage.open(storeHandle.resolve(path)); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } +} From f9ee1ed97a411fdcce2af16fd9b31e0cb0350a4e Mon Sep 17 00:00:00 2001 From: brokkoli71 Date: Thu, 5 Mar 2026 13:22:09 +0100 Subject: [PATCH 6/8] add tests for omero, bioformats2raw, labels, and hcs --- .../zarrjava/ome/v0_5/MultiscaleImage.java | 10 + .../java/dev/zarr/zarrjava/OmeZarrTest.java | 277 ++++++++++++++++++ testdata/ome/v0.4/.zattrs | 18 +- testdata/ome/v0.4/0/0.0.0.0.0 | Bin 7567 -> 7601 bytes testdata/ome/v0.4/0/0.1.0.0.0 | Bin 7534 -> 7549 bytes testdata/ome/v0.4/1/0.0.0.0.0 | Bin 1515 -> 1506 bytes testdata/ome/v0.4/1/0.1.0.0.0 | Bin 1496 -> 1515 bytes testdata/ome/v0.4/labels/.zattrs | 5 + testdata/ome/v0.4/labels/.zgroup | 3 + testdata/ome/v0.4/labels/nuclei/.zattrs | 40 +++ testdata/ome/v0.4/labels/nuclei/.zgroup | 3 + testdata/ome/v0.4/labels/nuclei/0/.zarray | 25 ++ testdata/ome/v0.4/labels/nuclei/0/.zattrs | 1 + testdata/ome/v0.4/labels/nuclei/0/0.0.0 | Bin 0 -> 1475 bytes testdata/ome/v0.4_hcs/.zattrs | 29 ++ testdata/ome/v0.4_hcs/.zgroup | 3 + testdata/ome/v0.4_hcs/A/1/.zattrs | 11 + testdata/ome/v0.4_hcs/A/1/.zgroup | 3 + testdata/ome/v0.4_hcs/A/1/0/.zattrs | 51 ++++ testdata/ome/v0.4_hcs/A/1/0/.zgroup | 3 + testdata/ome/v0.4_hcs/A/1/0/0/.zarray | 29 ++ testdata/ome/v0.4_hcs/A/1/0/0/.zattrs | 1 + testdata/ome/v0.4_hcs/A/1/0/0/0.0.0.0.0 | Bin 0 -> 1004 bytes testdata/ome/v0.4_hcs/A/1/0/0/0.1.0.0.0 | Bin 0 -> 1015 bytes testdata/ome/v0.5/0/c/0/0/0/0/0 | Bin 7355 -> 7339 bytes testdata/ome/v0.5/0/c/0/1/0/0/0 | Bin 7349 -> 7340 bytes testdata/ome/v0.5/1/c/0/0/0/0/0 | Bin 1045 -> 1057 bytes testdata/ome/v0.5/1/c/0/1/0/0/0 | Bin 1058 -> 1058 bytes testdata/ome/v0.5/labels/nuclei/0/c/0/0/0 | Bin 0 -> 1021 bytes testdata/ome/v0.5/labels/nuclei/0/zarr.json | 44 +++ testdata/ome/v0.5/labels/nuclei/zarr.json | 46 +++ testdata/ome/v0.5/labels/zarr.json | 9 + testdata/ome/v0.5/zarr.json | 18 +- testdata/ome/v0.5_hcs/A/1/0/0/c/0/0/0/0/0 | Bin 0 -> 969 bytes testdata/ome/v0.5_hcs/A/1/0/0/c/0/1/0/0/0 | Bin 0 -> 971 bytes testdata/ome/v0.5_hcs/A/1/0/0/zarr.json | 48 +++ testdata/ome/v0.5_hcs/A/1/0/zarr.json | 57 ++++ testdata/ome/v0.5_hcs/A/1/zarr.json | 17 ++ testdata/ome/v0.5_hcs/zarr.json | 35 +++ 39 files changed, 784 insertions(+), 2 deletions(-) create mode 100644 testdata/ome/v0.4/labels/.zattrs create mode 100644 testdata/ome/v0.4/labels/.zgroup create mode 100644 testdata/ome/v0.4/labels/nuclei/.zattrs create mode 100644 testdata/ome/v0.4/labels/nuclei/.zgroup create mode 100644 testdata/ome/v0.4/labels/nuclei/0/.zarray create mode 100644 testdata/ome/v0.4/labels/nuclei/0/.zattrs create mode 100644 testdata/ome/v0.4/labels/nuclei/0/0.0.0 create mode 100644 testdata/ome/v0.4_hcs/.zattrs create mode 100644 testdata/ome/v0.4_hcs/.zgroup create mode 100644 testdata/ome/v0.4_hcs/A/1/.zattrs create mode 100644 testdata/ome/v0.4_hcs/A/1/.zgroup create mode 100644 testdata/ome/v0.4_hcs/A/1/0/.zattrs create mode 100644 testdata/ome/v0.4_hcs/A/1/0/.zgroup create mode 100644 testdata/ome/v0.4_hcs/A/1/0/0/.zarray create mode 100644 testdata/ome/v0.4_hcs/A/1/0/0/.zattrs create mode 100644 testdata/ome/v0.4_hcs/A/1/0/0/0.0.0.0.0 create mode 100644 testdata/ome/v0.4_hcs/A/1/0/0/0.1.0.0.0 create mode 100644 testdata/ome/v0.5/labels/nuclei/0/c/0/0/0 create mode 100644 testdata/ome/v0.5/labels/nuclei/0/zarr.json create mode 100644 testdata/ome/v0.5/labels/nuclei/zarr.json create mode 100644 testdata/ome/v0.5/labels/zarr.json create mode 100644 testdata/ome/v0.5_hcs/A/1/0/0/c/0/0/0/0/0 create mode 100644 testdata/ome/v0.5_hcs/A/1/0/0/c/0/1/0/0/0 create mode 100644 testdata/ome/v0.5_hcs/A/1/0/0/zarr.json create mode 100644 testdata/ome/v0.5_hcs/A/1/0/zarr.json create mode 100644 testdata/ome/v0.5_hcs/A/1/zarr.json create mode 100644 testdata/ome/v0.5_hcs/zarr.json diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java index ae61c82b..42f71575 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java @@ -82,6 +82,16 @@ public MultiscalesEntry getMultiscalesEntry(int i) throws ZarrException { return omeMetadata.multiscales.get(i); } + @javax.annotation.Nullable + public dev.zarr.zarrjava.ome.metadata.OmeroMetadata getOmeroMetadata() { + return omeMetadata.omero; + } + + @javax.annotation.Nullable + public Integer getBioformats2rawLayout() { + return omeMetadata.bioformats2rawLayout; + } + @Override public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { String path = getMultiscalesEntry(0).datasets.get(i).path; diff --git a/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java b/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java index 89d35b10..328fcd8f 100644 --- a/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java +++ b/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java @@ -1,12 +1,21 @@ package dev.zarr.zarrjava; +import dev.zarr.zarrjava.core.Attributes; import dev.zarr.zarrjava.ome.MultiscaleImage; import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.ome.Plate; import dev.zarr.zarrjava.ome.UnifiedMultiscaleNode; import dev.zarr.zarrjava.ome.UnifiedSinglescaleNode; +import dev.zarr.zarrjava.ome.Well; import dev.zarr.zarrjava.ome.metadata.Axis; import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.metadata.NamedEntry; +import dev.zarr.zarrjava.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.ome.metadata.WellImage; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.ome.metadata.WellRef; import dev.zarr.zarrjava.store.FilesystemStore; import dev.zarr.zarrjava.store.StoreHandle; import dev.zarr.zarrjava.v3.Array; @@ -15,7 +24,9 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -202,4 +213,270 @@ void readV04_entryHasVersion() throws Exception { MultiscalesEntry entry = image.getMultiscalesEntry(0); assertEquals("0.4", entry.version); } + + // ── Omero + bioformats2raw.layout (read from testdata) ────────────────── + + @Test + void readV05_omero() throws Exception { + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage( + storeHandle(TESTDATA.resolve("ome/v0.5"))); + + OmeroMetadata omero = image.getOmeroMetadata(); + assertNotNull(omero); + assertEquals(2, omero.channels.size()); + assertEquals("DAPI", omero.channels.get(0).get("label")); + assertEquals("GFP", omero.channels.get(1).get("label")); + assertEquals("color", omero.rdefs.get("model")); + } + + @Test + void readV04_omero() throws Exception { + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage( + storeHandle(TESTDATA.resolve("ome/v0.4"))); + + OmeroMetadata omero = image.getOmeroMetadata(); + assertNotNull(omero); + assertEquals(2, omero.channels.size()); + assertEquals("DAPI", omero.channels.get(0).get("label")); + assertEquals("color", omero.rdefs.get("model")); + } + + @Test + void readV05_bioformats2rawLayout() throws Exception { + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage( + storeHandle(TESTDATA.resolve("ome/v0.5"))); + assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); + } + + @Test + void readV04_bioformats2rawLayout() throws Exception { + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage( + storeHandle(TESTDATA.resolve("ome/v0.4"))); + assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); + } + + // ── Labels (read from testdata) ────────────────────────────────────────── + + @Test + void readV05_labels() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); + + List labels = image.getLabels(); + assertEquals(Collections.singletonList("nuclei"), labels); + + MultiscaleImage nuclei = image.openLabel("nuclei"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, nuclei); + assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); + } + + @Test + void readV04_labels() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.4"))); + + List labels = image.getLabels(); + assertEquals(Collections.singletonList("nuclei"), labels); + + MultiscaleImage nuclei = image.openLabel("nuclei"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, nuclei); + assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); + } + + // ── HCS Plate (read from testdata) ─────────────────────────────────────── + + @Test + void readV05_plate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.Plate.class, plate); + + PlateMetadata meta = plate.getPlateMetadata(); + assertEquals(2, meta.columns.size()); + assertEquals(2, meta.rows.size()); + assertEquals("A", meta.rows.get(0).name); + assertEquals("1", meta.columns.get(0).name); + assertEquals("A/1", meta.wells.get(0).path); + } + + @Test + void readV04_plate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.Plate.class, plate); + + PlateMetadata meta = plate.getPlateMetadata(); + assertEquals(2, meta.columns.size()); + assertEquals("A", meta.rows.get(0).name); + assertEquals("A/1", meta.wells.get(0).path); + } + + @Test + void readV05_wellViaPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.Well.class, well); + + assertEquals(1, well.getWellMetadata().images.size()); + assertEquals("0", well.getWellMetadata().images.get(0).path); + assertEquals(Integer.valueOf(0), well.getWellMetadata().images.get(0).acquisition); + } + + @Test + void readV04_wellViaPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.Well.class, well); + + assertEquals(1, well.getWellMetadata().images.size()); + assertEquals("0", well.getWellMetadata().images.get(0).path); + } + + @Test + void readV05_hcsFullNavigation() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + Well well = plate.openWell("A/1"); + MultiscaleImage fov = well.openImage("0"); + + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, fov); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); + } + + @Test + void readV04_hcsFullNavigation() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + Well well = plate.openWell("A/1"); + MultiscaleImage fov = well.openImage("0"); + + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, fov); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); + } + + // ── Omero write round-trip (v0.4) ──────────────────────────────────────── + + @Test + void writeV04_omeroRoundTrip() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer") + ); + MultiscalesEntry entry = new MultiscalesEntry(axes, Collections.emptyList()); + + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_omero")); + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.create(handle, entry); + + dev.zarr.zarrjava.v2.ArrayMetadata arrayMetadata = new dev.zarr.zarrjava.v2.ArrayMetadata( + 2, new long[]{16, 16}, new int[]{16, 16}, + dev.zarr.zarrjava.v2.DataType.FLOAT32, 0, + dev.zarr.zarrjava.v2.Order.C, null, null, null); + created.createScaleLevel("0", arrayMetadata, + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + Map channelMap = new HashMap(); + channelMap.put("label", "DAPI"); + channelMap.put("color", "0000FF"); + Map rdefsMap = new HashMap(); + rdefsMap.put("model", "color"); + OmeroMetadata omero = new OmeroMetadata(Collections.singletonList(channelMap), rdefsMap); + created.setOmeroMetadata(omero); + + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage reopened = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(handle); + OmeroMetadata got = reopened.getOmeroMetadata(); + assertNotNull(got); + assertEquals("DAPI", got.channels.get(0).get("label")); + assertEquals("color", got.rdefs.get("model")); + } + + // ── Labels write round-trip (v0.5) ─────────────────────────────────────── + + @Test + void writeV05_labelsRoundTrip() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer") + ); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_labels")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage parent = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + + Attributes labelsAttrs = new Attributes(); + labelsAttrs.put("labels", Arrays.asList("nuclei")); + dev.zarr.zarrjava.v3.Group.create(handle.resolve("labels"), labelsAttrs); + + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage nuclei = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create( + handle.resolve("labels").resolve("nuclei"), + new MultiscalesEntry(axes, Collections.emptyList())); + nuclei.createScaleLevel("0", + Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.UINT8).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertEquals(Collections.singletonList("nuclei"), reopened.getLabels()); + assertEquals(Arrays.asList("z", "y"), reopened.openLabel("nuclei").getAxisNames()); + } + + // ── HCS write round-trips ──────────────────────────────────────────────── + + @Test + void writeV05_plateRoundTrip() throws Exception { + PlateMetadata plateMetadata = new PlateMetadata( + Arrays.asList(new NamedEntry("1"), new NamedEntry("2")), + Arrays.asList(new NamedEntry("A"), new NamedEntry("B")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null); + + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_plate")); + dev.zarr.zarrjava.ome.v0_5.Plate.createPlate(handle, plateMetadata); + + Plate reopened = Plate.open(handle); + assertEquals(2, reopened.getPlateMetadata().columns.size()); + assertEquals("A", reopened.getPlateMetadata().rows.get(0).name); + assertEquals("A/1", reopened.getPlateMetadata().wells.get(0).path); + } + + @Test + void writeV04_plateRoundTrip() throws Exception { + PlateMetadata plateMetadata = new PlateMetadata( + Arrays.asList(new NamedEntry("1"), new NamedEntry("2")), + Arrays.asList(new NamedEntry("A"), new NamedEntry("B")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null); + + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_plate")); + dev.zarr.zarrjava.ome.v0_4.Plate.createPlate(handle, plateMetadata); + + Plate reopened = Plate.open(handle); + assertEquals(2, reopened.getPlateMetadata().columns.size()); + assertEquals("A/1", reopened.getPlateMetadata().wells.get(0).path); + } + + @Test + void writeV05_hcsFullIntegration() throws Exception { + StoreHandle plateHandle = storeHandle(TESTOUTPUT.resolve("ome_v05_hcs_full")); + + dev.zarr.zarrjava.ome.v0_5.Plate.createPlate(plateHandle, new PlateMetadata( + Collections.singletonList(new NamedEntry("1")), + Collections.singletonList(new NamedEntry("A")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null)); + + dev.zarr.zarrjava.ome.v0_5.Well.createWell( + plateHandle.resolve("A/1"), + new WellMetadata(Collections.singletonList(new WellImage("0", null)))); + + List axes = Arrays.asList(new Axis("z", "space", "micrometer"), new Axis("y", "space", "micrometer")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage fov = dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create( + plateHandle.resolve("A/1").resolve("0"), + new MultiscalesEntry(axes, Collections.emptyList())); + fov.createScaleLevel("0", + Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.FLOAT32).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + MultiscaleImage image = Plate.open(plateHandle).openWell("A/1").openImage("0"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, image); + assertEquals(Arrays.asList("z", "y"), image.getAxisNames()); + } } diff --git a/testdata/ome/v0.4/.zattrs b/testdata/ome/v0.4/.zattrs index 60027f13..552fc1aa 100644 --- a/testdata/ome/v0.4/.zattrs +++ b/testdata/ome/v0.4/.zattrs @@ -75,5 +75,21 @@ ], "type": "gaussian" } - ] + ], + "omero": { + "channels": [ + { + "label": "DAPI", + "color": "0000FF" + }, + { + "label": "GFP", + "color": "00FF00" + } + ], + "rdefs": { + "model": "color" + } + }, + "bioformats2raw.layout": 3 } \ No newline at end of file diff --git a/testdata/ome/v0.4/0/0.0.0.0.0 b/testdata/ome/v0.4/0/0.0.0.0.0 index 853ae779c2800a9b8e005b49f928b4a9e3218abb..08381c74d1145f1f1b3932231cec4f5da1b133fb 100644 GIT binary patch literal 7601 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 literal 7567 zcmX9@2{=^W8~*Oh*tfASSq78FnthwaWS2c;t*jBUWhbCNh(Py zl_n&innDZG|4#q8&pgk$_nh;c?|r}bJ z;y7LW&G||XKQ-)L$5X5mAA8Ub(KM>ed(Cf`tyabK4Ra-hS6)#%ElIrRGsB-7gO#rE*jT9)~?c#8Ex%xfY8wrSPd=epjtE!Nh53fGvg zrA!ih6!s>zyD>dpS`L063?kk;q20n67qagG58UkKoXi+%i={d3Wtx2Un4DXvb*>YU zuqOB3*Y#oGp4eq2OBNLJu~+JE*Dx4dnJ_!aWd79T=6>a3{ev;Uy;{HEJ4=b_J$wG5 z2Y!3|ayi8?WBu4VA;W3dvA|h+L`lw$P8#j$g^qJ7MFVd*0nZc(?B!~5iXVTd@sa=L zdaD$Dp^pD1bXA6Q&+Xp-eWbA^QTNtu5gl3<;c*;O%cQ+tO+*QP6KKN~AInbF zB{7B~O?AC|_>W$n=u;dZuNJ@Lwbrj&`uw&>t+{5S;{A|MZ8+y;@PAYgRjD1;Iy04 z=8R#@Aur!S%1qa?`L;<@pC8#5KBSVS%LpSYQAE=AQqj8Q{y(PK8|fXr$JB8> zh9_eABrkc7X~vi8;|XP473;m_8IR}7d9`~|#_a<$ikFY3k1~RP=?|uMc#IzT>@n(8 z(BZk)Av~|sm(Oy5S$;H%*L|6s#<-@t{C6^Ue8-}brqmQDWTy=?bc@_AJ@CJTqc9jK z@bUJ`v)ek-w*kh!82_mT(&5XM^i4756B&txF1}6@r3JsV*J|jwm0z6V%`m*ZCBr7q zoiZ|B(#<}(e@fAef1L}`KU*Q{!#GdiJL5)&U(8>U-zIAt&R4>Nqzg0;)6X}wMa z3Ez1q$}7U~wLHE7hAZwKV5zMd!cj3|SzvofoNj zKhVRls|(&IR&+bffqBig@rMH48rpr{5((CJ8e++@=9Sr!iuCeogU%3qUgMjF7mRZ) zZs(YNYFG3<*q<&QZqV3mwG(u8+qVve>!prxNsL-x`z`C*Gc&Yf0=C;RlAV36E6xk! zG{)?{sW9)2wGRiX?X-x?px0elHVA3P?z*@Ub*5sSU?Z`b7^pJQ|E4GWetDw<)*&?X zPF<1yJpRk?lXuH1g(7N33{|W%Ql*P?7*@RI5rLGMM{PivbA_C)&Z>#Kd9{~m+tc)p z(J;ut$IS)4{s-0^*7SC7D|nRXSXTSSTw&qQ;fxSdrohVE#hpIwkkwx&X5HJ-F5P`# zXwiiCF{yw#J`T;kxUoB8Gx@#w5B!cUG!kz8f~H|fO2+W*A1z`GlSHzM?9;<7Vs8(t z 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 LMaX8z+T#8PRsQAK literal 7534 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 diff --git a/testdata/ome/v0.4/1/0.1.0.0.0 b/testdata/ome/v0.4/1/0.1.0.0.0 index 03e5ec15f2cd77a17670a6a71bb9531dfda81d32..c42baa14a0c7f04b0f4734b651d819ad5b966ff4 100644 GIT binary patch literal 1515 zcmY+Edr%Ws6vodkiGUJ!LwPDlA}AI?gFGwjZYlwSMNQEbMo>|aiVy`M^?_iN1#t{k z!B)qrrT8dStkD4<=nN@RYAe<_z9_YbN;|a0M+1TiYR{(e4{s(jzq#|B@0@$Tn~;QD z1Tp~O`;QpF1+jzx=yNQWDIRVw9-W9A*7bP!Mz=O;K@|nN;4i;gO3^auI~uC=Ny?ti z9n?yY0)PKi?D|lR>jN5im;1j=HecVm0?{xs^QSt^x*dooO;^&)%d^kYK#`#3Q>A-) z0fl*%}V!?`r*)UBo2je&zk&}r+Rdp`?4ZwCIt?D<-K1ocb-eEV8bZ|EIf zk9Mc;L_ch6oME7$6awa~Usac$Z*|V$-7Zx}dQ2|WsRI=DSbTwxM&j zJf4~2q?&4gTvJbAb$Z@sm!wEFhOX_3!aja0al*SNAZ)(h8LV<_yUW=EL2U2Q1<>Dl!l zFl}|A5JT3~IisEL?Hz?N3`B&cg;mVW7h+(RnB40EwuNHP;Zo6~?j!rpBYvsv?DZ}W z@4f_G3HSiF&Cp^=a*W|7JJQdxsbMLK4&VLk+Te+E4v16JvpNP$kE0L^ zm8%O9t~6di9OxgB*Ka6Vjo5Nia{cmz78PPe{!m|QjDD`o`v=#Zm1WFv!a|ELIM~kY4l3xrU9&*3hT9!#$5ScS!@72XMw_<3D zHu1`ywU-s7_4PQq?tb;uweuiaPjLE%+}u!DigHV1_L+nymU)QXmdFlYiMI(cVMS5o znvS+bh|>P|jomRN#}Q{%?)INqwoiiAjtP5elB@G%cD4+1{?A~OHG@?5V|Mr4y8eBW zL5A1ynuQIYiiHd)k*|6d?QFyh$PV4l2?sU|2^j?Vj;Qe$hZkWNrzD_GCWLIgv;Wq$8wWV)J}q3lAGfT#3}KlN9-p~ zeTW6QyMn6x>~uJGVjFjER?7i=#yg`%NXKN>3ePxTf&1Bw%bId*))DPqcfN?6BSC9o z{Pu=(Sdb#MaWDoMO0agxhU#Eg&<=adu5gNQmi)!yG!6BDi8!Vkd~LX0&8mSs8oux~=( zka2^f;B(+`F({xRk7XE+;aCZfGMLM?{Lf9Qr5;2!;YI5vKAJtg_@B(?p1O z0DJ}hENcWA6D=piG$CghIVq+g6Gz2il|Zwv!5)2R(v8so%mR=SR6k@nkN^f8hY;bP yF%1q+2m(lZ8DYY+FgO|`^a=&SjPoc#%Jczv0gymw$WS&q8atp6bqeC`xBMT&uIy0& literal 1496 zcmZ9M4NMbf7{{Nx@hJjVC&dqnv?vHnECedk-dz>ka3EGEgQ^4ML)|8V%m&8L7B~z# z(hvm^oKmKaj)_)>ZYr>DI&=#f!(c-f7>ZLE2o;DFP}$R_CfhD|$?x9#-~V}^_n-GA z384*m0hpgoTY%Y!Dgt2C@f1T1s2y~pIAFQd2jxrRJR9_`PzLe*;!cxd>`Mv`UuusC z9@0gu0UmICLATcx_smBWa5BT3P4lX>A0jr-Ia}2^1hWwb=k+m$p_jKQa3vv~leEVs z396I(N`M=qOP`O_w1`nu*0UmM>toF+#4B$kYL+ha51~Lg?@3s3&ee7qa7XnH_vLvW zOTB<=6AP4!PMw*F(piI_iVRh{MON$hh1EupwjmO&8*)+&0g(|SDB3lrj6Xdiu@(^~ z<@HjR+j|jB`)q_d=<38O%U=%qXrt9~;GT8JjQa8(Ppo2U?NB{$& z)`q++y<8igiY6Pk^`x#>KWafdq~-0{b+7&%#KjJE_aZCW%McX-rrg}2Pt;hY@8>7# z4B*R2t|^6Fn%VYn(966a(|GNaTNXRt5`jmZa=>|a@+xZrP}G~);8;3cXpSJHW!NPk zd+lc!0rh~GtbNp_Fh{@~kvbJPPjq9!b5Y@yP`s;j2$&F&fb(bqP z*&IIEiYU<>@+&+s{4U~1bJJiadoJ}~2 z*gLs>yN%*)E#h~58G#Ok#0A6y*Ly1m^n2_O1OFr{qf;c~h#kR28X*~a)9Sr1VCB~v za<45z@AhPsGLQ9sfg&egzY{fOTl{g2cZTzHirBdZk}Iyb`&HB3{wPc`H0oAXEd?Y?KR{#OVEmJpnO9p8{$7?ATS1XJY8?Ah=6@ysEl+qnep zSbxk7L-=C9e|qLX17!~3dRWo!Gjqe_=pA{GeC-IQ@k9J^v^AqBjbjjtPLK2JmQ6jd zHuKa=(h;T#@5g3p5(}TL-ucltC`#*hOZUyv*C5tj416r@IKB$eIc;C9XH`p;R`tPb{#9r5NB1ASuAfA3RHes$X-NWkraHV^k zUBWUq^!~*q^!)`}WVrpf$=`LdwE7U-wA_jHLEpbfk%S}GMVFMnm9sb$k?*nj`25Ns zA!7M*fp}Z>p5=&nS=yCxPnD+srx0vOfn^w$#rstXE6guAXimWb_)Y=Eh{2tvU^mOq z42}rk0{$SS%}Sbe0TF;F*x?rQ00(Oi=!>6OrUc+~2uGq_F+xvpGyuhVgRK|{0XT{w znP$m>^i-Gcs}; TPMc;|$gm_s*vrWO9w7e!Al3UK diff --git a/testdata/ome/v0.4/labels/.zattrs b/testdata/ome/v0.4/labels/.zattrs new file mode 100644 index 00000000..f30f48a5 --- /dev/null +++ b/testdata/ome/v0.4/labels/.zattrs @@ -0,0 +1,5 @@ +{ + "labels": [ + "nuclei" + ] +} \ No newline at end of file diff --git a/testdata/ome/v0.4/labels/.zgroup b/testdata/ome/v0.4/labels/.zgroup new file mode 100644 index 00000000..cab13da6 --- /dev/null +++ b/testdata/ome/v0.4/labels/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/testdata/ome/v0.4/labels/nuclei/.zattrs b/testdata/ome/v0.4/labels/nuclei/.zattrs new file mode 100644 index 00000000..497c245c --- /dev/null +++ b/testdata/ome/v0.4/labels/nuclei/.zattrs @@ -0,0 +1,40 @@ +{ + "multiscales": [ + { + "version": "0.4", + "name": "nuclei", + "axes": [ + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 0.5, + 0.5, + 0.5 + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/ome/v0.4/labels/nuclei/.zgroup b/testdata/ome/v0.4/labels/nuclei/.zgroup new file mode 100644 index 00000000..cab13da6 --- /dev/null +++ b/testdata/ome/v0.4/labels/nuclei/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/testdata/ome/v0.4/labels/nuclei/0/.zarray b/testdata/ome/v0.4/labels/nuclei/0/.zarray new file mode 100644 index 00000000..667b5370 --- /dev/null +++ b/testdata/ome/v0.4/labels/nuclei/0/.zarray @@ -0,0 +1,25 @@ +{ + "shape": [ + 8, + 16, + 16 + ], + "chunks": [ + 8, + 16, + 16 + ], + "dtype": "`8(FJx)V6@@RQu9CE}7eAC1 zh$|3GOl9_>y-^V1Bn-k3DI^OGq`*ZVB8c_D@Pkmtb@vzaJUsV(U)Oz}*Lj@haU4&m zv5W@Dzpu+-j33we{#8gIlUjAUn!w}xPc~V9K4R^;y?q8 z5`4>OuB9friM=!u=bY@LJG^}l4B?kdg|}fJR5ZMJ} zz4ntjpjMs&OA87XqZP!9a0)$)mJ=@ip>(#Wv|hA}U&!saWHqrLwMc#NyO;(O6+OWs zr8%etfkujNiuP+P3M)LRRx*7ri)Vz9oD4lhPQM_YNPG|HgXcp-!MxxD zc$Vmp?uBmjnv6|Na4TGt8`KQ32o5Q=p>=4I3`R%5F;kSAGQ5#+5>|j)GtHjl7H~`( zT!P2oEtwJ3t9-j>m<>Q{mJsdMTb8p0gSBHGn}qGwr|`p8Ya`4~xbz8hMp^@hVKy32 z45IPDA4Q2-#l$9|J)*AA|X$7-of*!7*hX4da!}Aj9O8Cyc^qVZ-1cnK3csHEOXps>pH|Th__P3B|!(ZRuaZC=< z90w}2mrdKsv_+Bc#+!K$7>X}By=u9T<8LILA@?J}An7>Un+IWg$}XIIuqgh*y#poZ z?pzRwIC4OS!Rz@cr8&3O1nkvNYhc)aCt(>wXF<{?a2QPfEVrtMW3pdURB-y9;PAtN z+QzRhG$$HdG`)~ueazFj!$bDazGF`JuWfrL|2TO;=3AA^m9}XqHaotg?lEavEhC%t zbX8E|nRkDV6y1~9y4EBGbqK{Dj{SI;JsOkeY&L)}v{3NJjVZB3fIKy+!4>C-uTGMVl^o@R0| z)I?=_glWmzki;GTJ(us~UNpggGpRoyneXEw0k7h9e>+-ai;eeZr+8H_ba}k~x_*?A zfc1_pucZOGjO*42cV&rxi)Qn^ynWdU-vx$zd^-+GI^J3tY;mvS*G(HxI!I7i7;;N) z1)H?{M@yx$C!sIZSR<}aKIO!A?`IJ!N1o!5XNB*+?kTqnnFsD0U^Wl83aiLY`` zvAoOW&b=i5XwiM~ELKUCW8WC#7Q{zY3ETfQ&Qu8V4TwsgCR1;~^j<`Nm(#Q92@`XE zKkRAOstk%>N<3nH&elsi*Xjuhzy4^I?$vtZ zd(DkahK?uS)y8?-^j6v=XEY^gyjsy!yQ@`QYgMF(%Cg999vY7tKU(k47t_h=THY?g zk&x}`t(I2UV6P{ZEbQc|AmMHv><}m!^Fi4> Wiwq3X3?G<5ZUq_*B!I2~v1|ZGf{2;` literal 0 HcmV?d00001 diff --git a/testdata/ome/v0.4_hcs/A/1/0/0/0.1.0.0.0 b/testdata/ome/v0.4_hcs/A/1/0/0/0.1.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..d94cca93dfb9560827da34439b359d8ef67721c3 GIT binary patch literal 1015 zcmZQ#RAgabVPF8^@5~GgB0wesBM@Ji<54$l!=+sYi3f_DeD)+{Y=5n=%CV6{Z;J+p zaglT`lSKBy3kw(v3=Y`5IQK+j1%s^y8&62axx_O%mv>Cr^G7ddR)@w+kBDardZPC= zlx%0;c!)*GVD8Hoo0ew@M08k8S;3OE?7*H?dl%kwv*_`tc_CZ0n&E)#Ufl%;lI>(J zt+7iBSeWhn<->`KQOg1rE_U z^~GVc+7gDg&6j`Z=rA}md~k43b-W*!lTmSF4Qs!`0zQq0`2sQ#Yt|T)?3wfQfS#6v zpu}sJzcV)Of5G9$v~6v@*1p-sdJj|N7T#FinM@pa`i>Y0OkJ$R*D6zQ_h$X! znMdDb?-Pp1*m=^$r@;Qk+Mlb~Rs{%Z@!Mz}n)E?Zy6CeSSKCf6=?OfZR~mx8I_mD7 z$`e|%M%A}*r^1x9)_JijmOiN3^~I(2nTyW61Go0g^O!Epy_q-fzWe))PqSyIyqh*r z{9UqZ-ll1KHo_@B&-++U9q!$r@bkv!YdV|6#S_F{l=}8KY{;$FUzBilMMAx7NXUJY zIcL6TTsE|Rvh1K(ex(H0;i<`&eYO@9`5i$B9tM*Vlci^O;tbM3lQY-{9l%vrQrCPb782J;(O6!lMw-Bz|HJa>VwGT-6* zQN|*o37Xl=&p&Rt|B<~=@XysSg&G-F1NG}TV&?RPWfy&o4iUa3ea-4awJ zC1YP|w*ISKvq0+K^2pc~%sNeWh8OP5^{uIQ-Q-@C+Hr3GvXeZ05wGgI4=D#*FSsEc z`yh$?-fY$X=9ijpGHE`ss;sp9 zS>c10jlG?%ot>S%oxQy+kg&6}1u^WwOrRK$0TQqUiGfr>q=6djlo6=XK&Na!0N%kP|3jP&A_mdfx(tR$=*hg Vfgyn51FH>C7ua23>%q>j0|43DhhYE! literal 0 HcmV?d00001 diff --git a/testdata/ome/v0.5/0/c/0/0/0/0/0 b/testdata/ome/v0.5/0/c/0/0/0/0/0 index ee0c69cb7c0ab52868404c1d0ced6361691eb336..991d0eac4940ed204153428bc84a3da1c39d0a69 100644 GIT binary patch literal 7339 zcmV;c98}{dwJ-f(03QwI015zdaxxGQ?-|BFjR9Qnj%8RpiEsg|ev0HICMt3VRRV)X z*oUN6M;T=)*?WtulD?rlZnCli6$cmx9tSre(kCV%z%oiCu@51JY=g{`Bf(nn9X>7t zk@Nfss5mElq32kvf9@UEOQ$l8{Y4j>zYtlve-hg`%{J1JEC{^;pV`mei2lTtgag_E zy#ooU=dfDkNUJ(5fK`(L%kZqn$?k2lVqWBc_PG%-4CzODrP#GJ z4Nr8nX+jh-jikcVlL`Es#)B6w6Xe8&vgFQR2zvQ}vc6kL2BlRYboxk4d$|}{e!ZIR zlr)#maFu6krpejg-ZZ!g4p{6aWY%kTWO-C*)6=f({6ZJ6zX&Yz0ZUMABn{y$AV=H{ zuQcCPqretY!l#dB_Z&nUWxp+0gAqP$_eN+BBP_{Kb#Sc8Bzo-^qz^&ax*-`Qe!|4% zYem{CCVqO!98U*Xa%vA)SvN!A*-_AOJJ}nAGl+rZP9jrh7&7&5h456M$M4O7_*|IM z+sGJqW&0K{5tb?we$HOTk{*X~I%|g6x@aDHWCnHI{#}#b4vD>B3OS#G^@G2}J^3_= z?pi6DSO7Fj-k}o77E9Krn}CsF*uL$H(zISZJ@UTCna6u$rhHF{(CHAB{wJcB3)$-T z(gN8*7uyq;G<#&eZ~x8lG2rVM<*o0bT~XhoX5xG=Uy=I5-W%Hf``Wq$hy5rX#hV3 zP;?pGRm+HgXFo4ur{QGGT_}E@s?O}f34qN{zNDTe8udUCm)9^A;u>NtpEW=p)7Wyy zK?}cUAVfrq(HE2f7q{De`+RqEHE1CW{0z5IknsfEQ1bil7 zb=0!g&nui6bpaLB41igi8wHT=?m&V)o8AcX$>MY%SVN;qt@z-cl^s~&ZMh6kW<1_Qj6JJi-TVeuE_X_ws#gJ;aV$K#3x*Hd6ZD8- zry{VgG6KR&3Q#(ZncKC@w4Tgsi)Y*Hj^;ID%LMlGcWnr)M@xh;5P1C(5!IvCx*SB& zo|9P8ovcf$L6FT|%q)iS+~F{hlf|=m`eWAB4_8c|i?#nH5mdg+Qkq42{W#Dwpg) z(B=cLtgcNn{E8q~o^bW|3_>ROwFcM0Pz*WQYvViU7`&W*+UrJ-ZOf&J(gI}JTZCZ! zgfPY9R5G1K6y*>ByxAfatu|3_Poz2z8MVu6L2= z%#R0v{Eo>N@Qmq2)NL>f0My*324A-`Wb#A^BaCKCiUBa<>ATLT*NF7AoFgu*na9O; zc98N7~1++J(Cu_KXt<03bFN2435qLRnV;jBm2{pxZAbvaGx+8 zr@(0W3R8k6Ai>dfb>wsn6n%EWg1u6daX)ax=mTK1u&W@VH<^I1#d%`@=FGX+4lV8z z!`}*%@-;EB_b_AiZG~3N`doWBCRMv_c7BA{)=m!6E`jLuUYtiBgrhVhlg{(uL10l8 zk1s*v!QqSwSXB+@M>G+7nh-xT*`wo*dGPtLv>}f`X7&+Ve7^b-XgQ~O57g573Wj_B zl;g>LcCU`&l;}S}ByYh>0UIe@SjkVEDoVLO7*HCEj=zVEpm8(=1*~`;dq8AMvj)s^sQamX@I8q(HOb=xte4+xT1sc$Z|7_&VFYG zf#Ym)d3AHjDF``qwFt5|3z@Gk0VK{(l)$@?keruKOMCUGvE8jGXD+0*a;T6%CvhV4 zMX+tAJVeDcPGRpB3TGck5^wbN7SFU-10>*=ZBJ@-6`~*3FzNEC{VOd;UfuJ3KLVe*YP@awe$m9*v#AX%)Ouc}{qWyr;;aC%aMkV86 z$viSH)Rn;dIe~JeoYxy@v0MR?dl|9>d?Ho*5(mT>ja!kas^)y!X7z@eqPHq!vY9Z) zPqmXBqJ9fJY(5i5 z@?UYlP9%YaUr!`jN}J~hSr#8iTE=rMBabC^rHfM?s%;|7T4*mYF!3mWbGkdBi-HwkS9 zTlEeWK-?~E$n2S`DN}zBa-_-3Y{4@)qs6CbF-wh(>@}e-y zJW&d~PdP&PQ#br@&y^dADjh>-+${xNbR#9}_D0dFJ50H}br&+feX4rgSeqZfUb&8F zk=N1Tax=RmAAwEqOLuB_sO6QwR6iiA&%t^yxQEybx_4Y*4aa1V@eF~xSA&^mt<-Q5 zJLEis>fXUP4V(@M>N{N4xq-|A|3ZiMg>SI@&48d+aGSa={1@bx>ZwzTiXu-9w z6GGPbTNj=mvX;05Dm)M2%HhfxIdc_M8#B$%CAwNFhudW#Gy=gm-$}5?gh(h|0}YU^p2xGBPmtu-*OSR4IPnpMKN;U0u*xH_%XZ}juubCsbAYlvk*KPw_xVrdQfLq?3MD0 zH;P9o;J9-Tvu+>}^K3+OzMV~RKL|u8Ae-mOA{w@8Q(Dgh-UV$W(V^0U&0<1h;Srl}aut4p3 zpCw+`3F*d8vFqo_s@u$#e#VW&(?wx?hA&oTlpC~1gq(9M)&~Bx@?|FvaRvnA^;2y` zd<_h$IXQtGhNbFd06KK0$5@M%iCse}6%!R&btqcmx20xY6^ngSf)a;expXrx1HE8Q z#|alqI>H{TDLIIm(vaGd%w#Twd2fU4;>H|~w*ve-2Mllz;kDv?vUT6iaCBWOIc z)lOOb6Xm3fVtm@didk3iOmZqO1#W~Uc|oI8-M1A_U*I%$vNA?|qYL5X^zn5WUTO{q zc=t9SP_8(E!P)GXOoxx)7s&pw=_UKyk*B)|i#|xB>w}&>ytH}wvI}VriY39`=LUYQug7~?581u`K;yw{nbeQ=KI z3n19wJfdm7d=UE%7djoNjP-U?ws%2f^c6buF5`~fKlCBBr1r}(q)KlA$g0iY(hTZh z%BmEyH~`GVZ!cN6ixJWfXd83q77k-}r7$3t=^F@;=QT2PSSvxFW2eknl>nNZAOzmS z6`5vyv1-LKWS79j?-ni_EoMu`*}QD~_YIh?f<0WEy zoCF-%C6zV(SB)3{1BKx;b+&hI4eU)t^7s)Z8t)efPuH}J?t5Q+{3vS5Ul5(}mk)Mk zcH(HS2hU7D@&NUkD!R+Cvps_C+piSKtcXI;Eq+v4#*6J$D7kW3DyL@$Wn3@>t1mzo z`#L&d{vpqW=V?WJ>v!Si|NHH#W~$q9!bX2X2?`-Y7T`ahlPl$F@z(t*1B5zvJ(rUm!rDiG*?O|pb}^;WsjHQGUGokQe4J3?e(lg zxonaoeX#NGA(Sj%0u3)$E1~Kk3heP28^Z&hYPSoroLwz{VDFdV(DpLMqM3GV+6jrK zi)P^L4R&z3!BY}*_>=XqEGak9rQ|$#47rd9ud4?8Xh9Cp4%k)2h@`8ILgMS5QRNn0 zrYr@M>t3!#$2%0mfHE!b0%^^LZ-1Bi!)GW|gm#hR(>vT&94U#Hm#C{UA2QIdso6S{ z6c)olWi^^YS(fr6Zp-ILl?hR54MaFi+B&}50$ScX83rz*i}OtjuQJE|N> zBUXc(DOx*t5b8B=ss2llXELOzmc3fx$tVwgIAM_G3Vpd1iOaz{gPd;-mRtBr`85N0 z4vhnPt4$A=)sgdd4!+#DF0dC(Nv0_aZ2iO9k{3Oga}Nce#$zOQ#3nSChnVRR6cXSX zgH~?{Jg_$lZP7*Q?C$BGr{CZB;FZw_T7kF-1~9dg(V62=8FCmM3Qqv7?`w}PE+}M- zoxH)F!UQ``wc>hBF<0%xk@ZflezpLY<;&!WVaFJ{2PTMS!~%B~Oro!#r|@w=CKyMn z*tx9uT>&YBCq5DV46i%>!^Y6@`bc`3KK6UW8k{lb6tIIH$D` zqfQNEzO+Y?=E<5v)!cfP!c{_#u19!!s~Bb{d|cu?QE;B@0k(bg z)V>MHkm1rq`U;ey>%;N#UX~iV3xYZKRQAaQj2Jpo>(GY$Qk@6Nb{8RxT*!?TkFri) z!ar4)LZSL?f&U760tgz8L5i%8I9;8^(u>;(mibzwqpe)v`q2d(w*wK!YHBF(47^R_ z(8KWxd(eC;#Dnv($vupMnav~`9?kGuKaHzLLRIjt6r1MpB+v4E{ z{T&qshJjXk2x6MoR8#9ftwHZwvE{NkRymA~%)c4sF_}OezioQIEXdr1W00*8)#ud^ z1bG%1>&GM&oWsGKYgNqwYZP5M1LV2W)yiEn)Lw7^sy$Cpubv|8T>$uK!3J3u5qZXR zhAjPW%guOS|0YLzCAt}J0`p=O8>TKM7S3bcae159=QW~)&8J)xxKPXG59vZ}$;iWd z4rq@@#d$zgwL^eTB;|A|Xo0mPA=)ierO#~AZ_%18a9jd{LcgIr zH*8^bGd5`#(FVtmLo7{&Me>MDAnuz03~$)MjMw}8AG7RoD|h}VN#nx^Y3+4l&W?0VV;(nf?Wi*U^*8yn&@oRs`m-P? zPe;&TM`Z6x4-D@-LhiCD^|B%!E+B!bQ%RUThqoke%RBN4LjrBe zX6RU6P2Qwu;u6XT+%5nsF9E3UCB8D4Mvhqfh$7`qNxR&tcys32$HkXn4PHTn4o_Sq zVM4N8USrxb5ynz?@ZvOJpqfW86?4Z3fPG+^t@mZUN?^(`tTBERJhKfjB7o?{A6hHSWkA`#tzQE zWw<;EGirYO5c;M$0v|l4INW!(Lwy!1PrD?<~hoN%3{!0nXJE$F*$rzp8%#FRn zr4+YX;Cdn|=uJ`w_Jemyu9Y#uTu?VpRHftsQn3085ZIi7DW(Uv;F!uS^B90Q+VjYh z)fDObom!WRjmdC1FitE7a&jB^cy<<467C2LE~6TwDYH=CEGo?xCJ4HKCWjUPO2MDB zcsa-vx67o6?W#ahbrCGC0#8!vv>jEn+psfbM+}$0odJK;+4l|+I1;YYtz&0L%p<_P zBH-x>T);UUbyk47SCWu z<+E=5d6pj`Mv?&3uL;ObhGFkzt*}nR$I(vu4nM(+g!>uV;VCzqCetNqLvwqBSI@YP)W^jCY&^MyCamj}C@)v0^=%b0FM-1LXJde_iO9!8Xu;-LBRb!v z=i|1J1M4av7!TN(dEd5_ce>hPDx_@pa{P0@Ofa9zs`S(k)004TH0*#ed!f_#n=1Ga zRfQe`k%elyJ6zFldh=%?jUW(&wxaT%Nn4=7U)NoN15kDW=F2 z7Bj72n{`1WwU3D0bfD7CtK|XgXNWqnhB;bmsT7%$3)V-jo!)Z9WmUzc%Z0cvfVabB z@bHeLq=oH-Sy}cg){ly3FWOP)%c~+^)5@B8z@ohhx$Tt%%X}b|1UG?ZrYArcwGm1s z9}4hlDUFU!Jh43)oPs0O8Q@8Jtc=-(@;@7K6s%c?GCK3RJ_lR?#Mv>NHo4vrk!wuI z^2Pjft6c1@F@V--zHl82GS(SNL;t|4?sQhrc#J`$d&B^>m;-0mOcJtdFmQLiz6}Oc z+`NJsR2hwg*~duAd;>xUmJ@xwUuDkTf%A1MC~Ea+k0mahxK$6}0qY}7qwE=V`nsn< z8xgYPcW>_73o(9#rR52N0P>eZLvAPa;id{M`YKC}?<`s28&a&?0079e)*9Z8@{F$3 zX3uA)lukvb%MB*xbUd&g!U)V2O%i7}R6x(v=*23mJTVeFNe>jXX(k4pMkA}{LM|}8 z0xEAlL(r$OV9t3B#1yCE65xnSnU~F?_A5@c?_g%o^8z1u5|H#9NViNW)#xIxNvtF# z$~Qh|7*J;K94m`YAqK;Rt|XnS#gf^tDYvMs2R)|jS8_dnup;e%FUB+po5MlFTv)khoXp2nus zWJXhXi)O6BM7T00)R{dHj^0KkPv!&I_yJOdhcQUuP%8p>113CMtl34QIV5#x?AW8a za2~Mx5)mtJ25XzVI7yg48XB`3Rzb2unQgJIC%hI*<2RwE!B~EZReP~#ZeuvP{>Ub(R$bcGH2nU%-2fr zUDUHc)8V1^19rSV!)o3&QZG&F6wh*#a220B-XW9deqN3~@P-PTu8Q!RtEL-u zm0F7qwzrM;xKB-ATQ|6~#{kjg1iEPX-{b?MN>Mcfy{Xp#L-q_%xDx%LzEn2yAO+L4gV)Bd@BK9Prw0>CN$hGJka1}X$F5-%1B(TLxVIX4GSV9+0 zPv*(GK>CNAFKgfudI()%tc8O01=0YmW|7eK@+L2r$g1;Ukvsw z0GY>7TeDXSbL9a{OOEkp@lr->USQ+#D%!+dLveww)N8$5jKg23Nl%{G9z%!1)9E?B z42qIxc`90KFv-I@(`=FGS#)Tcpf&j{zf}=s8{4Nn-jk}!L=f9WfR4GciPf#V5ONw> z4JXhC%qD=)*~l2YGazH-+PTZS+mQ`l8qs7YMq-v)3Vd7z3^#)Xz*`)g-OA0pBOWO9 zK2ydwI6Qix974WB<;j~MLELZb$%%nc#^H5pB2It58<6*%b ze~?(@Nja=;N9gAuS02rfm{{~|9?91UO5Q>tJ72KCfd9_$en1NW7t8FI3j@FWXHfeG Rpbai&#>t%{B-2#4Gyo^06-X*%<`LAemrU**TF?W-64TC@Uvc7zcFv zJscS-CnlV;^~X~3wur-5p|UJtzi0s)2OkF$2Uu6s?fGUTIRU{LCaz-`QR533w-$oL=u6TokCB1yY7-_ri`47RK_TTsPB4wA_IL$QAijVNr;Xfb zJ)r|CL(v>N6{V^%I52gc8ikHHXLwzR$lEN#_hkh+0tR4aFagyz()|5yh#FJrqvA#f z>^WVO$g)I4FHnF6JK1x31BHM3Lmc_(S{z;G3|3aZ$ zMhD5egky{4VWj=h87mJ94egD z0x5aVgWegbnD_!1tZ%`E#(!9xSk2L_pYWFK2`J}zU{HR-C&z;!?w;Y(fE#8jYsUz$ zchyPud~4Tj_9z}~00&MrK+jRMX!EQ=1n$;m>_~DauHO;eU(^wCE&$M+aEqKjpo4Qs z1ZJ-{LXCf<>Ur6TH@{mO^XsM6W7uhVhtaBoxna815f(QI458zer25cX2q8NyaTA}^Qih9+M)3zS|LxTM$3)5GifRs zvS!gncTI;h9){@%6Vgz|UA!Gez46o&35G>koOM5gW3IQ^Mco~e{@<0Fhh z9@PhV8H3gymAW;kB(Njhu`)8Z?rZSov&qdzHMuh>1-+|HL$IK4Oaun{lnPpfhzB zu0ZK^A&Brzbt|3+WP}ABw(%xw)sv)-JZw)6gIK#mL!;T~tSjY1W!u|eF~Oij_?$zK zt6R{6<~e6;PNP< zX1wf9nyaWY{9RF)TPQ|&4hKl_4zadRxbkR!maYL^liwhw$6bJ4 zxF4d{Q+A4O7#)<)TO9jZYSh!>5*^3I@Q#5)y{=BSU%SKbK|{EY7_GZK3_W^58`}{$ z_v zNl@4i4#{(*E!LO@XwT^UzG7db0M^vKbi=bqN zTN8Sz6L(h9MFlnkg~m6?L}CFDcyc|hEaxD(Wh-6UJjzJXM+_RdL%a~L8$rVV1`%r# zu)Sq#vW6@mctuZ}Y=`OBuJZUuoY&>hWAdUha$y--z{sYW^^cLzw>L&&n>5lm_4Q#<VU;X}&&+SC);Klu$~=x3-e_^oSgP90h12mSdJ(w|h{4;jQSuHnIlhKM z$1k+l@IVW4@A7bX?`X+B1)er{%mz-cN@h(2^z_;dYCMcRroYQP@Fp5;e!ykYW$qfk zNH*bpn3X5800Mi*$JT}DPM<X ztN|yD9->J3LeV%s%kz7Y2*vw@^6g?ktPJ*N<;yz5`vZyDDYUtsNko_{p*6aTnd0%L zrhbCWi$zT8J&Xw24YlL+o4mGTTgWj3)CFE5Hg>zqE7m3%dRwQ^Rdz)0bfofl2qyhX zNb3URD&ORW%1C%B95*L>3pFkok5HpCt!!OU5QfRfko1lmj5cCm^IHD>-Ku6bL*o={ z0F~|SVPX3it97oC4edpEiayYe%Bg@b@aqG~+c`+!Yn6)k%=NUG7C4TKa_S)$FdgLr z(VcKtU#kN31_elH9R?9OG|-tvM?bFt@|-!g^MwEnZ(!tRKxH6c^AM2cI(}dmi^-|mu-Q3= z9zuLKXY72DQal$Yd(FJESS)6zuE53WEau1Qa}Ity>R+MEEY_hDyW5ife ztBgVo*Rk5jZWkR(S4}1A;T|d6K8{2N?ZA`P?&awm(3bc_jm4-%27p zg+ENr!bI{Azz&_m0w-siu-!Kdji#!z^gb{xXPZ_pxB13M_MTX4ikyS|$bRt(hSMDy zbP;w@ZXcQUUUjlJ%MeW0A++WNTv#23ob99dgj^zhY1lVX((FX@^iV1e-7<0zKh+xf zz&HY)_LkrjsFpk@%H59y9na*&#Yi>WfEk3Jfa2sVTu)rC3(9A#Vh z@wfvl*nUjUk#X?wJc1%RE})%LZ#W?Aa#03(4Ng^GIE|FryYOf_OP)hF&PnqRopUZ2R`~;3d@ri2b0rybY#PG|woOl?%_K1P z#Y{9mvIKIX(hr^@cw`o9wm;lr`re!!EFdOCRx%a6ohYbnWD+qFL4v#(R(g$i`8(p8 zCZB@}dzR#wE3MKoSmKmBTf91OhN7FOd0hn!H!o1AgZrdOgS|4L@nlh$yurB~#2)~T zR`kVTj7(uiU9FCUg37%}a6PPU=_3sNCs0xRg_0^Ba{|s`94X))u(EUoNERM~P1Wa~ zMqR;F)MbI5=M%KSu7Oyui0Nm-F_X`Rt++E(w+3_e=`gK@Ug(C=75q5efSV9zE!Enw z$rXdqiM)ja9^6PbY?vd#acKI}(>iztg@Cs#EK3`)Lc ztZT~p_5_ltUjpSbni+>4^GD2hShjZ|$K!x!SDZ%<&R(Fx3p@zH)l2q;%DLB{*oMjX#h(64uUC@pj@W+~AbKd^d6 z1gTrtd-ISkX4mjD%~k+~nDc-sZ(x{t4W*Vg4bf()G*)jyLvTMJ2|gH^;7>wW4Oq%* zECEf%h>anE?9_a9hS{i)}V&m+y#U-Gnmuf zLWQVzVG80U?r8WN6Y9nGCapvD=v7xreH9}03WP+uOAfi)Nx@-2JJ_D4Hg&92Ggs;| zdM+hs*c&po{(^;gMmPanOH0C2pi*i*WE3wcmd9_1gL=7GRSk$xn@7hiISS#ELr_xn zFE=_b@|hW-%>D{zZfXg-e?h2D0 z;L8Ma%pYDUY+$0c1o@T>A*9MC*VafqEf z8E~r6rpL5GPowjJMQ9TFPc>K!hAQ7_u)}rMV80eR=w5GpF1Q4WVE_>{XbT}<0cXyy zuCTcS0A3i&W$VfpR%lT48V`s^_9_ZyFT(=iW1y!#5UBHtX>R-gG0lNmE`wPG^Nm*n z_M%1Z<}i@vFFE2KSLSBbFj0TuvE^8XC>%oBA+#b@8o%dc;3FWF{HVFZVgkLqRK?0` z1}UvIMRf)<&o@QzyLbxt^Pe-RkMd-?6W}lwMJ12Xb24d>k_qQhJ*P^8FJDLh)Ijud z!QpAfE|xFUV&-gs@=mtc_blF)C8HD`LE7KNJ}G^Nl=Ev*WiDi8^}b9vKj|^(B8E7f zBkT2P8qQn|N_(HEDRt%nM3)Ylr6YaXc~pW9=gKhSZD}`|sL@NMi0(=f(%yC?4P1kb5yoMDkC*i4P5fhw@V36D?WbtvksXhKv z#dbY>a(-z7jhk3~@&%7?2UMjq6GcTgC?fhgzc6=E1nY5!SckB^@ToN^uHz!;tP7hT zR80B;9Cv;wCCNp^T=FXbW}fk+y-h`29(7pV!OQFsGTfK3A#ur&;k^IYELm9NcYPX8 z^<;|;>3sIc$EP$CUv^i*d|yi^18s7!s5qzKr{}f=tR8~2h|3^_d4U;Te}NbGK{OZE z5z6WVYFxhIhV)xWoLDYNs#V3*?5O3;Xaa1{_>}dgJUe`aCDcK#R6LK7&z4P?rV_XM zq}Q{%klSD!nP)c-rPZU59NlYju9-cg1L}b}BAeeS=)rNh2U$NI$~vH{*}+O6@#39Q zU&46Aa;UNW(E)8m=Ivn|s=RrYWw7lnMlPsDdlaFXFH_U>r3#k^9OLIZCTs3s^zl+yRDPfen2X(rvK0_` zyoHdW0evYRY7*3G(p3He1SfXvl69z78vfGTyD~gg4`GOwdlB)xgH&)i3jwb4UB;mW z%FuWQuMTd*^yqG5Qzx6zYRL|xmjMRCXOmQ&gce0RRtYtYLVB;mG3lyKL1#jxFak(? z{S*qjSKtayF_=73q|=MwVps~y=lSR+e9Vr-r`TLL(;k$^_=#OGt&K}J;oQJJOOGOx z_8Fp|MuOJ*EvTt?dHONvR22)VqjwwEV>fnIEp;Z#OX`$-aD^bxaYb@yk-PgU0Fn!M z3j0#glS{a<du;D31mY5+ z?mP|Y>N=Sy^$IsgA6$bnl0~p94I%rMmpx9SmEt0{1aMt3F^~7T^%@p6yhf18Wpl&o zM@M9Svf1boYfgWJ80Z(Qx_He6Gp})4#d=`uKr{8z_`lGy+!92`F_OCs25Vjc4ATkR zojBnR1CFH9b2X*RC*UB$mTX&`Cd}XIF2%TsrXIs#{Fbnai~Na?&4+ZJVU9gaeZ;3H?O9_%BR^dSAgx~H~^>` zG=bK`84|MEmez}gl>L-qn7z=c-l)wLLnbb;D^u0CN<`fd$;GoSweDnd4brGWW*c(`Gba_u#py_u|?2MwrsuPi>%qQ&b z{teRpW_Y~|F@R5O+H)Ex`fiA&?O8mo*Tf&uryPX-3l8GF#1wi+iO#2qIlbTs4OfmC zq2=Ttv||jgOMy{3+vVd)OuZW+W(6MsXLY-%*t&Fr(g$yBxgO~No|B+G1T1vE;55tU zw8qTzrFgQgA(zuSw44okj@T-PmBVf=WzNz~^qxGAs4E!Fa0GNf|H6O>|3DYygMT1z0(3a9kjXz= zUQqjk#wPY7SA;D^)w<5q=`JQ}FZe|0su49>Q;q1$S`}O>sED^=z}ip10b(3Ug|?Fd zf}yksH6RXthw%pIq*EnWQ$1QEI23iDy(!<*V)3gXgd6JlaSlpsxQn8l&)`GDY84Wh z5MBU|5rxZ!syJRjrsSQ_pnfKay&0i_SAhw1XCkWzJ=I$%a1hUj{3F5L)hCS3p# zM=Nm6-l~o%mJrqHv2e(F-esQG=2(p-i1cm^O( zIC6ujnK->2M2I8zOc$<;g^uXw)EN0k+mkCIa4H-a_)sR}9fe^JH zNYHr1DXo_f9g_V7+H&O$)uWhT=Tl)yKL;1;NwZ0F>M`??Et^+Tfp-*Uy6zq@LL(3} zG!h`&!>MTUzz<8;V8nngRXx7Y%=Ie(bvajF3tQ&#^0FsfoFz`45o;hihykLWfoOWe zG(z98WHo{iuRIf^&rVM z24KwZS_mZi$d(to;mBb=JhuNT!QzQ#TK`}s!{N%HT1kYS(~_87ticrzITLv`$R0Pz zY4RZ2cs>cq&yj$r$6Mv&Og6-eJ>1Ye1+l@^3%LjwB|SXg)AWWkU89KU7CUjx|QLKDd8mJqn0nG-V!5_z>h7M@@+ z$Oj>#&cuX>s|tvIN%QP(1+cnPn6LD-WgLo_4pU|IYCf1;>y4aE44OUz*aH)qB5?*( zsJOr8jB#vDuSTQjFHXxY!S(V5+&s8a9<*qZt&JD6 zjN&Z4fY%>3qH!ukhhM%*S#+Lsx* z-7Hy}nO?^ZgYxr-r(dq1Cy!5zf%vvhE^b1GkNGfizD47ezwu>WK&sM{5F2uVv}wGT z#MYT_l02v^jh%$cqgV2-YmTsgxTJIzuT>*CXXg7%q=drS&`b=o&?E)Y*)0ubFvMc=;~5V?kh zw$FMxU?O24TrCXV$HKf_29g7=iPkW`O)WWv0HilGs&GQIG=mkP`ijxZIQmGw>ID;v zNz-P_#X?u}YSKJ7kFHvb$@?9kHJ`j97kQxkAs;dpV}|h~rjW4)wKZ?yNb0XIisxGe zv?$Th1K~VeZu8F-7#eiTQ8j8>l@nNV<+=;n4>IUsCwxqwR8@JkGNljk`R8tWKDe@u zr=u<4lprHoP=uI+(=khc9 hUzQFm+Jfy*D4Mwha6$YAvcwJ-f(03Q|Q015zhax@SS&>6-9iSi8sRt#V&r637=40A(dpc0NMkx|%s zJsMiyje>%7r{qn)bH2P&kiV%@>kb+R7zYvumpbj!D%z|39hWC(`}4YLa;RO=ZiGLW z@)-~7WlI$!J|TcJ6Ud5rbxYJ2Y?^oo#49G?amC#xAo1rP6EBQY=!iV>coGlIh(3i} z36Gc;wPo5#pU)@i0DW7Q8h5=&{a+}dyY1oZ%B>SFIc;$6pwvXd)Wc ztaibO``nQFfw%4!pQFIRfxm<{0wPm`>3FSFAUhs<wg@ zl<6HLGUPKph)&&P^E^T-FNEUaGCY@@ur=&@NH?r?dUP2s$@?7|VLV_?%>lLZ6WsQ` z7zc)*XiVJ%q~K@lpt=S`aUNwla1&9WJ}yhw-B8HU8+q4o&aCCZ#6f9Shf#b_& zcwRv|wm&3$`WA(iYe8|E1PZ=qtE^a$FzYm20=df_tC6gcx(}blht2WxEdszZ`O)48 zfg8KEL4HeF=_it&wo&@!QZ4MRMP<9w6s^k`O0?-1x9g=9^=2Scc@bF_IQ31GGZm7$ zfg)-i*BQb9yg0nw9pS}6h-j%4g4#B1D$TXjXDVQakLyJIDa5(fArr==8gmb9R$fsS zokwf)d|l|B=kVRRRn9z)DIvPL59@mA*tS0c0(W<$W&Ptx)t8`n@sY|&I{*u{6F4<4 zsF}$7y)bbUO-_!W4iYP{8}%cf?D>vlIt7R?ACP0p7W!_yZ33K!M&a#D~DMVI{O}u7#3t^(K{m9-1yh|wHIbCmHA}@Z(}CbM$Oa{HG#9=xThQ|N1QwFq&h5>UZN3h-=jmH*bF9~3)smJsjAl=mwIGSRlu@Kr z0iSGR(V>l)K;%gXppURUyo3a&Z;%1ahKW$WVQbBMIIEt8BXnzmuiIwF_GTTb3^;(= z&BBnqv4WO6ix{nxTlqv6MIOk+?{jyKtfVx@|7|6CsXlX=u<-7Kz>jlFrlZ@q3^E0{Cl_&UV-mtwqU~ zTXB%{!xTx^Q!?-kEWCJh&6_tlDbapO^Ssj@mSaHlG94L79$iBB0*SC(h8@UrAeFQw z1HWs7gyypkO85xm4%axMcfa9tD=yV9kWzEPKwExh2gWz}z`GOZj5iHX<%X$}e%o96 zW#hMS!f?L@eCOiw=pU+g41~fG|5!3=0|yBH3;_;9qKZDRfT0V-m}0LGsIO3n!drw= z@S*^q--?iOyEwE9N`83Smmh1nsd~-F<=vv{_(_JWr=Va%tE5lXMlNVifs^oLQNli< z^vFqrT(5wN#7;`wxLn*&UTy$NU)4bRMqbu*-0VJKDqU(Q^%~ylOl2)=Qm-19;H1%I zP6f;+6N#a8slG4l%sW&9{s=^kOZD+{F*7Nqs;WGcn!EF@0-Cgs&t=;9{X#KaD+OvU zk#P^(6X%0Ks<^WcoQa%n+UxK7twD+38cc2_0cz7ky!%58&QnO@{A+Q`M}#2or#LkZ zM&QmzBQ!0B)P#@7F*Tzcmw6~UWHMlmmTFz|wmV3(0$Mx>Bm_811g>x8KD}Y@@g<@p z91KLMFYpk2nU$=MrQv<=3dbIXY_F6fgP#yu;~*3wSTruuYc0MU*~QXV5Tf4phxI}* zGVbWv)U6{%?^q0)<`G$VG8%zb(PZWoMpl1Yf}2X#_iPLz22u-A24&f=$ZY<$WWjARr zsbc>sv~t5eNe9F#xCKQ^o_1mF6pHNdDJLRt9-*1Y;L{=Wpz#k|yPwFA#vinJ9qTQP z_c(?bPKT(oIJCJEM-ugcq>4Y9@^i0!G>>Go;YzG&tcZXq zm-^Cp;f#rVoD2%ywg|l$i3|@c6XYBa%rRG=r?ZH9ycj8+pV=t0Uj*#kOe=j#xyBL> zSUuPF${O?#eFBFdwgF08awxllGeS(JLCmH>jJ(M%^Cr4|{~&_TPOKbom?~*r2P5%= zQ-^-VYHCz2P?sL2nnhiz0WCSZT4*?4JYYJ#nSJunlhqIELGcHKB7Y~A;97O+el?-R z?ffh`o1mu~r8cn}7PW_%^X1v?n{OS!;S~5ul$L-ATUH8n|0&CCB&8V0h;u5fb_iZogSdAMYL|s}3ccblML(Yr(4FA8}G1fE4m}rApp|3F=D|lkw#QSFJeQ8H8-{Ah zY*k8sNQZ*gsDN6sQ1k?1l-Wb7gI?^?`6ePlCz{vLY9**RToBky{Y& zs*~(nJVaMPlcu)}(R3UvO?5$?Rc zM>ptsQBgiaiP*K&ocV%m(``ccXhj2FmqEC~YXGPIHl@TW7ODBn2|b6narr?TkSDN! z%^h>5XJb&c=7usOV#?#~qP{D*X8M33N8@1v^a(`010FgZX;b?{B}A*TaD0^L_GXP$ z+R5_qS9}b6Utj)pygQ?DLer#>rs9i?lnb%Pmd7YXGNYHUA7%byLmw- zg|wh|dRIpXy63IIJK2$%i|`oxml!DnG2(c8SVg`>p%L%l81WE=1002rIyaD_mkGcb z;R4cV@R2E`y#NY%Al(_e5pw5!heY3C5bR@I5M1%anPbuEId+lBiE&YGg@WZqcwv7l zVrDXr1@0Ma=Ux30GZ-yh9)&{gdZZtI__q3~2p$e-r{t}ba3 zS%CN$5*mFt1AC^q54IzaYDFHZOl1P{XnJ=(4btT-7~69>{qPPy?FGc#9(x(+b!(Cg z#%T02lAK(x^~q5HQW=Vzkq1&Ka}X#{kHBt<%*vdj_1LhppBSHhV@0&03V=2w%eo0I z4X(QUy5;yRrBqGp#)RXIg2VJ!r=z!EbNDU{eI5wF<#llCbq8-UkPWH3K9SP&Q5;V1 z0uBNHyV2#YDd6f*L7%6iqu_%EhJ0&5)}5= zBF9-hU^)-BMsuxR0ke#=Fk!ud($PMWBhGS(i#Ic6auk?TH;EYKQ9L2^6Bo)QknnO0 zm8~ZP^u|!IS@&W==W{?(&xZNwMKjNPn9H;S4t6$8@?#u{tygP0^bo2K+=hjeFO+$_ zJhV{yk6@N9)KPU63~2wt6Ez;}rPpjTA9g6_f+V1h1SOH_q$M3|u!xTo9y$s(9gHS` zK!;$1@J>yl9&P(|Ga=|bYDbc5AsBGEF@vro;fj^CSZQ08T<;JmkJpL-*w0YmbQ2S8 zujJB{KglGxgAhIXcqVUWT&0)p;7s>s)RTtz_(BHD%dN6H3Iept83yn_BN@LS3F0iU z4EaK4=MfE{;v86f7~7)I1SUs3>VcOdMzQneq3Z?Amj3O`^(EqBJjGn8wufidZh2fr zB1rF8g3{bX!>#jBkh+Mnh!#YK^rjgC_az3Qe<2y=J{0tqS3Lsff-7f9^I&44xXzFr zH$+k9OtmZRDdyFsSM>a+=J^m$)L4;{^K3o_pT?*)noJ*GIx_R84O6~rpl3=RqTll( zc`eo2t?12sO;~^TM!}Qj!q^6$(K7*}^Q&fYr}pT=^}!uaX~AI((z<{yHj5h4{G5p= z+C&LZzim+VyBJ^>fe6Q&BEqZ(O4QxMDs`aKH75cS=v9e`9BT$;Cagyf+xmWnUKj-o ztoLL9)P}ZYKZx1jBlPYbC_>P;BsExw0E=%hMrjyMsGfpY=n;mH|6oDSjWfEK2-@Y- zq=bDB!;uG-nSA1$Ik)06WHE@eP9?_6K}->S0V^3uAlD@mm%SdA+#LidwVDW|pYnsf zo>y>A0T}0t0a&>}6a^jViQBix6dr32iLo#(G3OCE)HL&of+C&0;dQ@4a}F2Cb}`X2 z?a`m(4GkdT)H%F|n4I$z98vag0Q(Sx${cA=nQ7qA@C!d8J=tN$wUA|wCT8cwCW~hy z@%R>?Fn(CT#zDZqauP|67F`Sd2_TEUV5{p!aHBV>TXQZ54}R^^!bqH=TnKgj;t@L+ z5M9=$c1Ab=6j*-M+UGAoh$UD}KJE$gUM!9`6t^!n@wk%Gp>KxodM^&$zP7CC=m~Tg zmt8(~7?9m(d%Acx_%Q_ zc$VLjCxI&7F3IUHmIfYlsO8gBDWd_z=h#FsR`YtYm!L&^z#HMgZKS`9rTPJ)uYYT2 z%;&MFod!c$VZB&RRQ{|6m%q`WeQe0%3$z(>6=Pg@L?S_xaC5y^AOye1wB?M7biBnX z!4rrjJpmvjSq}!+UjXB>2`{+My@@#qQ8U-#vEneBc^-8k%RdY;;0?48IT{fk%?A+E z=mGi-QFN9wnE6G&A&!GJf{Y4>EI`{1L-|el8KFFHVdlVH#6e>nk84l7-0-(kL$kncyrZx9yvI7L1KP&W8~NntZigqXmGDOUyx za~2okQ6fj;S;7lXnpC~`aFl+k}EBYd3`n+u6q=pl~s48n2MD<%s)tj60t)j|0g za8q7_>%+4a^KQVznd9XT| z*U1<5Wk)P+z#>jzaG7<;~-;N=6;zW;~6emSeIz+@+E`XN&qT zrah|8kgHjd4+|^>e^TPRVdt3bkfNGP<}cVus8`vtE;W(bH@0@+QnZUZcfzuO@J&vnG${ z3j7%=3(=`)zF4x0-<#N^xn)e1R;rV|*HH&Q06trp81)V%n?}Gi>PRz^KIGVXFB&qw z7o^afVCZfiJ3W_aN3TUhh8?w#p0Ks+ZBAl*r^nXC+PHqsi<+1CXgU%W;K{x=PuCbm zizcnoQh1d8=#Lr$IUv$dNc6rXaSPUD#D1e-Tjx}%keA|u7%WFy|&Y_CN%}9_v&(Tf$!SMQ{yvY+*A@Ty7I{qg*=OTrVT1gSvFVPwOMWGpQ zFbZe4uC3>EaQvSkGe=O?cDW}!hH_H%SE|>Iq9W94{7kXdw$!P5_*{YI%*Pt%cPIjH z*IKE=c05_p@bVez#CR2A>978Pv7oT5-{H;OG&(3R85zBYX)xhE)v3QRiXEv5)X~xa z9s-rlTbaS|j3Ru`k~4az3}Z%(bF~v9N7^AxsLoOV=fkd4-D+mfoFp_{Hd3}%Yqol_ zH%V4u=Ij?T(4AUc{{X<6S?5mgLX4^f@S;7`jblWE@;r}e#tV%eZ-{B-RE10i;dSF3 zdQci$m-Q`HICnDu%5Rr!SFvT!R7?uJ%Z8CZNjc&vM|>75%svHG>aaW4KaG%}{c7CW zXan|qNu-`7EPERb3)W-jbpvS9vls)iuHyypT%dQhlc{AtJ<~Tx^Ya33g7`u9@1~Iu z`MNbhEtUw5+A4$e?)IR_qg12*7boGOAmFH#pyPUR9F-o!pCm(;o}Mqno=?!D<{Fxy z2D|CSqaxKx4rMQcj+GYkZEG+FoHu~x$`=*bcp2Y?%SOu9ElP0R66krb*)N#I6eUx_NN(QbB#f{N!vN;@u%z?XwQvGF6e*w!Oqk+O=HDh~^fGEus zps2c0pAu^b@_48p4Q>;r$UjwWIhNBHTcQ=-sw0{PfB?iB=3zY>3#&&7!aIxBBX>au z;)`5J#xqCqiz1>p1|FU1bUC?To0^MUHm}&Op2XLgTX@28CAS*>bU)Ap1;>*9sYOmSih zC^{d*k;Dc0=v~t;Q#(M!cNtz%908vdwu@5W63FyCSfzuLffk<002)8*6J;(;kXX)y zLzm!Uax6;qdZ{%7_Y=^i70|e2ya!vqsp4{n4xpDB6UBL|u%5sPJU@){@)&Hk_zxM* zAJ7~%qA7PQ#Zc@n1VNqvm`1k-z;z21_Pjs=21Wzt&J93odj}10z1EEA8=+Lb%1Xww z?g(6|1l0{2S$zQMn*$~h{>2Z)i)lvrRTDoKiU& z@1!NreHi_Ggx`$Q0kZI*C*MEZvAHocIEe>WrZd1BLkV%nDLDJixXAKW2@>klGp9!L z;_1eH!TL_5&YEw4$r{j)K8o&aZ?=K|G1p~L1hJK#383>gdH-P8Tn3!jtR1v|9m`g{Hguxdf81RrM&wJ+u;`>qoua!gb SUMEbgXNe!*rP+R)86^Po`oFpW literal 7349 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 bhmq0yQvu@d(ZS)*q2t!kJs4dyFU@%Z4h|Cj literal 1045 zcmV+w1nT=JwJ-f(03Tf-0CxZEIWS(&8VBpi$|#~$sXGN}Bo5@xC&wA93nLG`8#7v3 z!y{GyiK_i++98rsGXez>Z31dxH6r_$F$>)Q+W_1ErnA+whc8OI?cte+l@JeM!#&yL zmInp-@vYvabxfm=-zE*b&5rXCB$K!-j);GSRz1So!el@_eKi@iS_rx~U2RX-h-j)p z#&_{AdY~mNf2l+EcCvN;z)00l3 zFC969m%7<-6Ty5R;nE%su4~WwJ{6{W5@e4jdeY-zDAqWyu;>pgvEDRspT?jxo;Qzo z;8gWtW8?!UVEo;cxwBoC`Fv}riMQCzYxRs+&jZ3Yt;_UGMn|u10)A_gEo9pzuNy#S z7KGDJ6A{>HjL{;3wCiAM3+rPYU|+_};oG-g_2buYFA$8g6Hs!sGkVB%aT!RX-MeM_{Va6PhLV&w!Jv%Q6GYZGn!K8rf^e;HA8D*+su`};PZ3NK zcj6K1)&PLJm#jh?uYlOTtK#jn6WA%O#~%wAU3cz=a<0N;}GTywBRF%AoV1nsb8;9)RS`BU;V&s zOdjSddAaXEHM0Z9Jf6Ebwv~8rH;}%?y*asW$|~2{S@La^6Q7#bex_OX`n*4;;|;Rn zyj2zRVG!K5o>BHsgG)PrSi~-HwxE&+eGNwCQ9YUb)&xB%s&s zAOXiR8Qn|DR?{^eI1`kcm!-{5c}CD3qa6MSFPp7sb+I#eh1umh!yWw;+lNhn2jgz} zS|8JOt^@WI>Gxmo^k4zuS0A-|>KFH^t{$~!Al|QJVH>Q4)<4)`Zkn{#oOGzC2Cq?fmqv5_v|IP)4{4JoL%ZM;@(GI~32~DI^JeSIpbdI!AG6!CRnog(+q-FWg%`N zIA*`#Ma;hZ(SC^R_#8>drzzvh4iWp>bXOs@XC(SPo|b1&W${wavdukLKgQj}pa+wdcR2yhalUgm!WP|t<96?m z!1Xh9-<~v5aW=>%n?{w~w_T{5%!!g8LeBPb`t5FB_&!{QALsq-Csxcakf}PBn)gd> z7`}E@_y%IHN12UxCXeO@Q|V)4;ZpeOA6#~bl&@beKkRZ^vp%te`T;lQhp2($U8&6{ zr3iAG8rkG1iJNYbazoi-wX>w%+ENSURm{5$u89T&v>a@iBIyxYcJuZ5^VFbTQQBqf|`lKWU5uO&e?Lp5f1^e7>rs z>ttLDdx~YfIr|5_yhg`e}mCIj4q9O0yY2>hX4Qo literal 1058 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 literal 0 HcmV?d00001 diff --git a/testdata/ome/v0.5_hcs/A/1/0/0/zarr.json b/testdata/ome/v0.5_hcs/A/1/0/0/zarr.json new file mode 100644 index 00000000..ace0e82b --- /dev/null +++ b/testdata/ome/v0.5_hcs/A/1/0/0/zarr.json @@ -0,0 +1,48 @@ +{ + "shape": [ + 1, + 2, + 4, + 8, + 8 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 1, + 1, + 4, + 8, + 8 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/testdata/ome/v0.5_hcs/A/1/0/zarr.json b/testdata/ome/v0.5_hcs/A/1/0/zarr.json new file mode 100644 index 00000000..e37ce091 --- /dev/null +++ b/testdata/ome/v0.5_hcs/A/1/0/zarr.json @@ -0,0 +1,57 @@ +{ + "attributes": { + "ome": { + "version": "0.5", + "multiscales": [ + { + "name": "fov_0", + "axes": [ + { + "name": "t", + "type": "time", + "unit": "millisecond" + }, + { + "name": "c", + "type": "channel" + }, + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } + ], + "datasets": [ + { + "path": "0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + ] + } + ] + } + ] + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/testdata/ome/v0.5_hcs/A/1/zarr.json b/testdata/ome/v0.5_hcs/A/1/zarr.json new file mode 100644 index 00000000..2966be43 --- /dev/null +++ b/testdata/ome/v0.5_hcs/A/1/zarr.json @@ -0,0 +1,17 @@ +{ + "attributes": { + "ome": { + "version": "0.5", + "well": { + "images": [ + { + "path": "0", + "acquisition": 0 + } + ] + } + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/testdata/ome/v0.5_hcs/zarr.json b/testdata/ome/v0.5_hcs/zarr.json new file mode 100644 index 00000000..04310192 --- /dev/null +++ b/testdata/ome/v0.5_hcs/zarr.json @@ -0,0 +1,35 @@ +{ + "attributes": { + "ome": { + "version": "0.5", + "plate": { + "columns": [ + { + "name": "1" + }, + { + "name": "2" + } + ], + "rows": [ + { + "name": "A" + }, + { + "name": "B" + } + ], + "wells": [ + { + "path": "A/1", + "rowIndex": 0, + "columnIndex": 0 + } + ], + "name": "test_plate" + } + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file From 215de0f7341c3e2f2599248f635cd674859dc35b Mon Sep 17 00:00:00 2001 From: brokkoli71 Date: Thu, 5 Mar 2026 14:27:49 +0100 Subject: [PATCH 7/8] add ome v1.0 and v0.6 --- .../zarr/zarrjava/ome/MultiscaleImage.java | 11 + .../ome/MultiscalesMetadataImage.java | 16 +- .../zarrjava/ome/v0_6/MultiscaleImage.java | 151 ++++++ .../ome/v0_6/metadata/CoordinateSystem.java | 24 + .../metadata/CoordinateTransformation.java | 67 +++ .../zarrjava/ome/v0_6/metadata/Dataset.java | 25 + .../ome/v0_6/metadata/MultiscalesEntry.java | 50 ++ .../ome/v0_6/metadata/OmeMetadata.java | 25 + .../zarr/zarrjava/ome/v1_0/Collection.java | 100 ++++ .../zarrjava/ome/v1_0/MultiscaleImage.java | 139 +++++ .../ome/v1_0/metadata/CollectionMetadata.java | 25 + .../zarrjava/ome/v1_0/metadata/Level.java | 26 + .../ome/v1_0/metadata/MultiscaleMetadata.java | 45 ++ .../zarrjava/ome/v1_0/metadata/NodeRef.java | 31 ++ .../ome/v1_0/metadata/OmeMetadata.java | 37 ++ .../java/dev/zarr/zarrjava/OmeZarrTest.java | 482 ------------------ .../zarr/zarrjava/ome/OmeZarrBaseTest.java | 86 ++++ .../dev/zarr/zarrjava/ome/OmeZarrV04Test.java | 184 +++++++ .../dev/zarr/zarrjava/ome/OmeZarrV05Test.java | 223 ++++++++ .../dev/zarr/zarrjava/ome/OmeZarrV06Test.java | 124 +++++ .../dev/zarr/zarrjava/ome/OmeZarrV10Test.java | 119 +++++ testdata/ome/v1.0/image/s0/c/0/0/0 | Bin 0 -> 7352 bytes testdata/ome/v1.0/image/s0/zarr.json | 44 ++ testdata/ome/v1.0/image/zarr.json | 51 ++ testdata/ome/v1.0/zarr.json | 18 + 25 files changed, 1616 insertions(+), 487 deletions(-) create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateSystem.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateTransformation.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/Dataset.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/MultiscalesEntry.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/OmeMetadata.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/CollectionMetadata.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/Level.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/MultiscaleMetadata.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/NodeRef.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/OmeMetadata.java delete mode 100644 src/test/java/dev/zarr/zarrjava/OmeZarrTest.java create mode 100644 src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java create mode 100644 src/test/java/dev/zarr/zarrjava/ome/OmeZarrV04Test.java create mode 100644 src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java create mode 100644 src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java create mode 100644 src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java create mode 100644 testdata/ome/v1.0/image/s0/c/0/0/0 create mode 100644 testdata/ome/v1.0/image/s0/zarr.json create mode 100644 testdata/ome/v1.0/image/zarr.json create mode 100644 testdata/ome/v1.0/zarr.json diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java index b8f9a589..5410c2f3 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java @@ -111,6 +111,17 @@ static MultiscaleImage open(StoreHandle storeHandle) throws IOException, ZarrExc com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); if (attrs != null && attrs.has("ome")) { + com.fasterxml.jackson.databind.JsonNode omeNode = attrs.get("ome"); + String version = omeNode.has("version") ? omeNode.get("version").asText() : ""; + if (version.startsWith("1.")) { + if (omeNode.has("multiscale")) { + return dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.openMultiscaleImage(storeHandle); + } + throw new ZarrException("v1.0 store at " + storeHandle + " is a Collection, not a MultiscaleImage. Use v1_0.Collection.openCollection() instead."); + } + if (version.startsWith("0.6")) { + return dev.zarr.zarrjava.ome.v0_6.MultiscaleImage.openMultiscaleImage(storeHandle); + } return dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage(storeHandle); } } diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java index 9593eae5..2e22b4fa 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java @@ -12,9 +12,9 @@ * Extension of {@link MultiscaleImage} that provides typed access to OME-Zarr multiscales metadata * and supports creating new scale levels. * - * @param the concrete multiscales entry type + * @param the concrete multiscales entry type (may be {@link MultiscalesEntry} or a version-specific subtype) */ -public interface MultiscalesMetadataImage extends MultiscaleImage { +public interface MultiscalesMetadataImage extends MultiscaleImage { /** * Returns the raw multiscales entry at index {@code i}. @@ -33,11 +33,17 @@ void createScaleLevel( @Override default UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException { - M entry = getMultiscalesEntry(i); + Object entry = getMultiscalesEntry(i); + if (!(entry instanceof MultiscalesEntry)) { + throw new ZarrException( + "getMultiscaleNode() not supported for entry type " + entry.getClass().getName() + + "; override getMultiscaleNode() in your MultiscalesMetadataImage implementation."); + } + MultiscalesEntry mse = (MultiscalesEntry) entry; List nodes = new ArrayList<>(); - for (dev.zarr.zarrjava.ome.metadata.Dataset dataset : entry.datasets) { + for (dev.zarr.zarrjava.ome.metadata.Dataset dataset : mse.datasets) { nodes.add(new UnifiedSinglescaleNode(dataset.path, dataset.coordinateTransformations)); } - return new UnifiedMultiscaleNode(entry.name, entry.axes, nodes); + return new UnifiedMultiscaleNode(mse.name, mse.axes, nodes); } } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java new file mode 100644 index 00000000..cebb216b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java @@ -0,0 +1,151 @@ +package dev.zarr.zarrjava.ome.v0_6; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; +import dev.zarr.zarrjava.ome.UnifiedMultiscaleNode; +import dev.zarr.zarrjava.ome.UnifiedSinglescaleNode; +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.ome.v0_6.metadata.Dataset; +import dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.v0_6.metadata.OmeMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; + +/** + * OME-Zarr v0.6 (RFC-5) multiscale image backed by a Zarr v3 group. + */ +public final class MultiscaleImage extends Group implements MultiscalesMetadataImage { + + private OmeMetadata omeMetadata; + + private MultiscaleImage( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v0.6 multiscale image at the given store handle. + */ + public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("ome")) { + throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); + } + OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + if (!omeMetadata.version.startsWith("0.6")) { + throw new ZarrException( + "Expected OME-Zarr version '0.6', got '" + omeMetadata.version + "' at " + storeHandle); + } + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v0.6 multiscale image at the given store handle. + */ + public static MultiscaleImage create( + @Nonnull StoreHandle storeHandle, + @Nonnull MultiscalesEntry multiscalesEntry + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + OmeMetadata omeMetadata = new OmeMetadata("0.6", Collections.singletonList(multiscalesEntry)); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes attributes = new Attributes(); + attributes.put("ome", omeMap); + Group group = Group.create(storeHandle, attributes); + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } + + @Override + public MultiscalesEntry getMultiscalesEntry(int i) throws ZarrException { + return omeMetadata.multiscales.get(i); + } + + @Override + public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { + String path = getMultiscalesEntry(0).datasets.get(i).path; + return Array.open(storeHandle.resolve(path)); + } + + @Override + public int getScaleLevelCount() throws ZarrException { + return getMultiscalesEntry(0).datasets.size(); + } + + @Override + public void createScaleLevel( + String path, + dev.zarr.zarrjava.core.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException { + if (!(arrayMetadata instanceof dev.zarr.zarrjava.v3.ArrayMetadata)) { + throw new ZarrException("Expected v3.ArrayMetadata for OME-Zarr v0.6, got " + arrayMetadata.getClass()); + } + Array.create(storeHandle.resolve(path), (dev.zarr.zarrjava.v3.ArrayMetadata) arrayMetadata); + + // Convert ome.metadata.CoordinateTransformation to v0.6 CoordinateTransformation + List v06Transforms = new ArrayList<>(); + for (CoordinateTransformation ct : coordinateTransformations) { + v06Transforms.add(new dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation( + ct.type, null, null, null, ct.scale, ct.translation, ct.path, null, null, null, null)); + } + + MultiscalesEntry current = omeMetadata.multiscales.get(0); + MultiscalesEntry updated = current.withDataset(new Dataset(path, v06Transforms)); + List updatedList = new ArrayList<>(omeMetadata.multiscales); + updatedList.set(0, updated); + omeMetadata = new OmeMetadata(omeMetadata.version, updatedList); + + ObjectMapper mapper = makeObjectMapper(); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes newAttributes = new Attributes(); + newAttributes.put("ome", omeMap); + setAttributes(newAttributes); + } + + @Override + public UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException { + MultiscalesEntry entry = getMultiscalesEntry(i); + List nodes = new ArrayList<>(); + for (Dataset ds : entry.datasets) { + List mapped = new ArrayList<>(); + for (dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation ct : ds.coordinateTransformations) { + mapped.add(new CoordinateTransformation(ct.type, ct.scale, ct.translation, ct.path)); + } + nodes.add(new UnifiedSinglescaleNode(ds.path, mapped)); + } + // Axes: prefer entry.axes; fall back to first coordinateSystem's axes + List axes = entry.axes; + if ((axes == null || axes.isEmpty()) && entry.coordinateSystems != null && !entry.coordinateSystems.isEmpty()) { + axes = entry.coordinateSystems.get(0).axes; + } + return new UnifiedMultiscaleNode(entry.name, axes != null ? axes : Collections.emptyList(), nodes); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateSystem.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateSystem.java new file mode 100644 index 00000000..2b022601 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateSystem.java @@ -0,0 +1,24 @@ +package dev.zarr.zarrjava.ome.v0_6.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ome.metadata.Axis; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CoordinateSystem { + + public final String name; + public final List axes; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CoordinateSystem( + @JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "axes", required = true) List axes + ) { + this.name = name; + this.axes = axes; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateTransformation.java new file mode 100644 index 00000000..a2341613 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/CoordinateTransformation.java @@ -0,0 +1,67 @@ +package dev.zarr.zarrjava.ome.v0_6.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CoordinateTransformation { + + public final String type; + @Nullable public final String input; + @Nullable public final String output; + @Nullable public final String name; + @Nullable public final List scale; + @Nullable public final List translation; + @Nullable public final String path; + @Nullable public final List transformations; + @Nullable public final List mapAxis; + @Nullable public final List affine; + @Nullable public final CoordinateTransformation transformation; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CoordinateTransformation( + @JsonProperty(value = "type", required = true) String type, + @Nullable @JsonProperty("input") String input, + @Nullable @JsonProperty("output") String output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("scale") List scale, + @Nullable @JsonProperty("translation") List translation, + @Nullable @JsonProperty("path") String path, + @Nullable @JsonProperty("transformations") List transformations, + @Nullable @JsonProperty("mapAxis") List mapAxis, + @Nullable @JsonProperty("affine") List affine, + @Nullable @JsonProperty("transformation") CoordinateTransformation transformation + ) { + this.type = type; + this.input = input; + this.output = output; + this.name = name; + this.scale = scale; + this.translation = translation; + this.path = path; + this.transformations = transformations; + this.mapAxis = mapAxis; + this.affine = affine; + this.transformation = transformation; + } + + public static CoordinateTransformation scale(List scale, String input, String output) { + return new CoordinateTransformation("scale", input, output, null, scale, null, null, null, null, null, null); + } + + public static CoordinateTransformation translation(List translation, String input, String output) { + return new CoordinateTransformation("translation", input, output, null, null, translation, null, null, null, null, null); + } + + public static CoordinateTransformation identity(String input, String output) { + return new CoordinateTransformation("identity", input, output, null, null, null, null, null, null, null, null); + } + + public static CoordinateTransformation sequence(List transformations, String input, String output) { + return new CoordinateTransformation("sequence", input, output, null, null, null, null, transformations, null, null, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/Dataset.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/Dataset.java new file mode 100644 index 00000000..5d1b4823 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/Dataset.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.ome.v0_6.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class Dataset { + + public final String path; + @JsonProperty("coordinateTransformations") + public final List coordinateTransformations; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Dataset( + @JsonProperty(value = "path", required = true) String path, + @JsonProperty(value = "coordinateTransformations", required = true) + List coordinateTransformations + ) { + this.path = path; + this.coordinateTransformations = coordinateTransformations; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/MultiscalesEntry.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/MultiscalesEntry.java new file mode 100644 index 00000000..b2e30afd --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/MultiscalesEntry.java @@ -0,0 +1,50 @@ +package dev.zarr.zarrjava.ome.v0_6.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ome.metadata.Axis; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class MultiscalesEntry { + + @Nullable public final List axes; + public final List datasets; + @Nullable public final List coordinateSystems; + @Nullable public final String name; + @Nullable public final String type; + @Nullable public final Map metadata; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public MultiscalesEntry( + @Nullable @JsonProperty("axes") List axes, + @JsonProperty(value = "datasets", required = true) List datasets, + @Nullable @JsonProperty("coordinateSystems") List coordinateSystems, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("type") String type, + @Nullable @JsonProperty("metadata") Map metadata + ) { + this.axes = axes; + this.datasets = datasets; + this.coordinateSystems = coordinateSystems; + this.name = name; + this.type = type; + this.metadata = metadata; + } + + public MultiscalesEntry(List datasets, List coordinateSystems, String name) { + this(null, datasets, coordinateSystems, name, null, null); + } + + /** Returns a new MultiscalesEntry with the given dataset appended. */ + public MultiscalesEntry withDataset(Dataset dataset) { + List updated = new ArrayList<>(this.datasets); + updated.add(dataset); + return new MultiscalesEntry(axes, updated, coordinateSystems, name, type, metadata); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/OmeMetadata.java new file mode 100644 index 00000000..5fdbcab2 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/metadata/OmeMetadata.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.ome.v0_6.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +/** OME-Zarr v0.6 metadata stored under {@code attributes["ome"]}. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeMetadata { + + public final String version; + @Nullable public final List multiscales; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeMetadata( + @JsonProperty(value = "version", required = true) String version, + @Nullable @JsonProperty("multiscales") List multiscales + ) { + this.version = version; + this.multiscales = multiscales; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java new file mode 100644 index 00000000..64034606 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java @@ -0,0 +1,100 @@ +package dev.zarr.zarrjava.ome.v1_0; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.v1_0.metadata.CollectionMetadata; +import dev.zarr.zarrjava.ome.v1_0.metadata.OmeMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Map; + +import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; + +/** + * OME-Zarr v1.0 (RFC-8) collection backed by a Zarr v3 group. + */ +public final class Collection extends Group { + + private OmeMetadata omeMetadata; + + private Collection( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v1.0 collection at the given store handle. + */ + public static Collection openCollection(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("ome")) { + throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); + } + OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + if (omeMetadata.collection == null) { + throw new ZarrException("v1.0 store at " + storeHandle + " has no 'collection' — is it a MultiscaleImage?"); + } + return new Collection(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v1.0 collection at the given store handle. + */ + public static Collection createCollection( + @Nonnull StoreHandle storeHandle, + @Nonnull CollectionMetadata collectionMetadata + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + OmeMetadata omeMetadata = new OmeMetadata("1.0-dev", collectionMetadata); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes attributes = new Attributes(); + attributes.put("ome", omeMap); + Group group = Group.create(storeHandle, attributes); + return new Collection(storeHandle, group.metadata, omeMetadata); + } + + /** + * Returns the v1.0 collection metadata. + */ + public CollectionMetadata getCollectionMetadata() { + return omeMetadata.collection; + } + + public StoreHandle getStoreHandle() { + return this.storeHandle; + } + + /** + * Opens the child node at the given path, returning either a {@link MultiscaleImage} or a + * {@link Collection} depending on the metadata present. + */ + public Object openNode(String path) throws IOException, ZarrException { + StoreHandle child = storeHandle.resolve(path); + Group group = Group.open(child); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("ome")) { + throw new ZarrException("No 'ome' key found in child node at " + child); + } + OmeMetadata childOme = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + if (childOme.multiscale != null) { + return MultiscaleImage.openMultiscaleImage(child); + } + if (childOme.collection != null) { + return Collection.openCollection(child); + } + throw new ZarrException("Child node at " + child + " has neither 'multiscale' nor 'collection' in ome metadata"); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java new file mode 100644 index 00000000..5ea74cfd --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java @@ -0,0 +1,139 @@ +package dev.zarr.zarrjava.ome.v1_0; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.UnifiedMultiscaleNode; +import dev.zarr.zarrjava.ome.UnifiedSinglescaleNode; +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.v1_0.metadata.Level; +import dev.zarr.zarrjava.ome.v1_0.metadata.MultiscaleMetadata; +import dev.zarr.zarrjava.ome.v1_0.metadata.OmeMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; + +/** + * OME-Zarr v1.0 (RFC-8) multiscale image backed by a Zarr v3 group. + */ +public final class MultiscaleImage extends Group implements dev.zarr.zarrjava.ome.MultiscaleImage { + + private OmeMetadata omeMetadata; + + private MultiscaleImage( + @Nonnull StoreHandle storeHandle, + @Nonnull GroupMetadata groupMetadata, + @Nonnull OmeMetadata omeMetadata + ) throws IOException { + super(storeHandle, groupMetadata); + this.omeMetadata = omeMetadata; + } + + /** + * Opens an existing OME-Zarr v1.0 multiscale image at the given store handle. + */ + public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { + Group group = Group.open(storeHandle); + ObjectMapper mapper = makeObjectMapper(); + Attributes attributes = group.metadata.attributes; + if (attributes == null || !attributes.containsKey("ome")) { + throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); + } + OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + if (omeMetadata.multiscale == null) { + throw new ZarrException("v1.0 store at " + storeHandle + " has no 'multiscale' — is it a Collection?"); + } + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + /** + * Creates a new OME-Zarr v1.0 multiscale image at the given store handle. + */ + public static MultiscaleImage create( + @Nonnull StoreHandle storeHandle, + @Nonnull MultiscaleMetadata multiscaleMetadata + ) throws IOException, ZarrException { + ObjectMapper mapper = makeObjectMapper(); + OmeMetadata omeMetadata = new OmeMetadata("1.0-dev", multiscaleMetadata); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes attributes = new Attributes(); + attributes.put("ome", omeMap); + Group group = Group.create(storeHandle, attributes); + return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); + } + + /** + * Returns the v1.0-specific multiscale metadata. + */ + public MultiscaleMetadata getMultiscaleMetadata() { + return omeMetadata.multiscale; + } + + @Override + public StoreHandle getStoreHandle() { + return this.storeHandle; + } + + @Override + public UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException { + if (i != 0) { + throw new ZarrException("v1.0 has a single multiscale per group; index must be 0, got " + i); + } + MultiscaleMetadata m = omeMetadata.multiscale; + List nodes = new ArrayList<>(); + for (Level level : m.levels) { + List mapped = new ArrayList<>(); + for (dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation ct : level.coordinateTransformations) { + mapped.add(new CoordinateTransformation(ct.type, ct.scale, ct.translation, ct.path)); + } + nodes.add(new UnifiedSinglescaleNode(level.path, mapped)); + } + List axes = m.axes; + if ((axes == null || axes.isEmpty()) && m.coordinateSystems != null && !m.coordinateSystems.isEmpty()) { + axes = m.coordinateSystems.get(0).axes; + } + return new UnifiedMultiscaleNode(m.name, axes != null ? axes : Collections.emptyList(), nodes); + } + + @Override + public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { + return Array.open(storeHandle.resolve(omeMetadata.multiscale.levels.get(i).path)); + } + + @Override + public int getScaleLevelCount() throws ZarrException { + return omeMetadata.multiscale.levels.size(); + } + + /** + * Creates an array at the given path and appends a {@link Level} to this multiscale's metadata. + */ + public void createLevel( + String path, + dev.zarr.zarrjava.v3.ArrayMetadata arrayMetadata, + List coordinateTransformations + ) throws IOException, ZarrException { + Array.create(storeHandle.resolve(path), arrayMetadata); + MultiscaleMetadata updated = omeMetadata.multiscale.withLevel(new Level(path, coordinateTransformations)); + omeMetadata = new OmeMetadata(omeMetadata.version, updated); + + ObjectMapper mapper = makeObjectMapper(); + @SuppressWarnings("unchecked") + Map omeMap = mapper.convertValue(omeMetadata, Map.class); + Attributes newAttributes = new Attributes(); + newAttributes.put("ome", omeMap); + setAttributes(newAttributes); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/CollectionMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/CollectionMetadata.java new file mode 100644 index 00000000..5b7bbba2 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/CollectionMetadata.java @@ -0,0 +1,25 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +/** OME-Zarr v1.0 collection metadata stored under {@code attributes["ome"]["collection"]}. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CollectionMetadata { + + @Nullable public final String name; + public final List nodes; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CollectionMetadata( + @Nullable @JsonProperty("name") String name, + @JsonProperty(value = "nodes", required = true) List nodes + ) { + this.name = name; + this.nodes = nodes; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/Level.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/Level.java new file mode 100644 index 00000000..3cb7f2e9 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/Level.java @@ -0,0 +1,26 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation; + +import java.util.List; + +/** A single resolution level within a v1.0 multiscale image (replaces v0.6 Dataset). */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class Level { + + public final String path; + public final List coordinateTransformations; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Level( + @JsonProperty(value = "path", required = true) String path, + @JsonProperty(value = "coordinateTransformations", required = true) + List coordinateTransformations + ) { + this.path = path; + this.coordinateTransformations = coordinateTransformations; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/MultiscaleMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/MultiscaleMetadata.java new file mode 100644 index 00000000..ede1bfeb --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/MultiscaleMetadata.java @@ -0,0 +1,45 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +/** OME-Zarr v1.0 multiscale metadata stored under {@code attributes["ome"]["multiscale"]} (singular). */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class MultiscaleMetadata { + + @Nullable public final String name; + public final List levels; + @Nullable public final List coordinateSystems; + @Nullable public final List axes; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public MultiscaleMetadata( + @Nullable @JsonProperty("name") String name, + @JsonProperty(value = "levels", required = true) List levels, + @Nullable @JsonProperty("coordinateSystems") List coordinateSystems, + @Nullable @JsonProperty("axes") List axes + ) { + this.name = name; + this.levels = levels; + this.coordinateSystems = coordinateSystems; + this.axes = axes; + } + + public MultiscaleMetadata(String name, List levels, List coordinateSystems) { + this(name, levels, coordinateSystems, null); + } + + /** Returns a new MultiscaleMetadata with the given level appended. */ + public MultiscaleMetadata withLevel(Level level) { + List updated = new ArrayList<>(this.levels); + updated.add(level); + return new MultiscaleMetadata(name, updated, coordinateSystems, axes); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/NodeRef.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/NodeRef.java new file mode 100644 index 00000000..17209b85 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/NodeRef.java @@ -0,0 +1,31 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** A reference to a child node within a v1.0 collection. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class NodeRef { + + public final String type; // "multiscale" | "collection" + public final String path; + @Nullable public final String name; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public NodeRef( + @JsonProperty(value = "type", required = true) String type, + @JsonProperty(value = "path", required = true) String path, + @Nullable @JsonProperty("name") String name + ) { + this.type = type; + this.path = path; + this.name = name; + } + + public NodeRef(String type, String path) { + this(type, path, null); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/OmeMetadata.java new file mode 100644 index 00000000..f601521b --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/metadata/OmeMetadata.java @@ -0,0 +1,37 @@ +package dev.zarr.zarrjava.ome.v1_0.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +/** OME-Zarr v1.0 top-level wrapper stored under {@code attributes["ome"]}. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OmeMetadata { + + public final String version; + /** Present when this node is a multiscale image. */ + @Nullable public final MultiscaleMetadata multiscale; + /** Present when this node is a collection. */ + @Nullable public final CollectionMetadata collection; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public OmeMetadata( + @JsonProperty(value = "version", required = true) String version, + @Nullable @JsonProperty("multiscale") MultiscaleMetadata multiscale, + @Nullable @JsonProperty("collection") CollectionMetadata collection + ) { + this.version = version; + this.multiscale = multiscale; + this.collection = collection; + } + + public OmeMetadata(String version, MultiscaleMetadata multiscale) { + this(version, multiscale, null); + } + + public OmeMetadata(String version, CollectionMetadata collection) { + this(version, null, collection); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java b/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java deleted file mode 100644 index 328fcd8f..00000000 --- a/src/test/java/dev/zarr/zarrjava/OmeZarrTest.java +++ /dev/null @@ -1,482 +0,0 @@ -package dev.zarr.zarrjava; - -import dev.zarr.zarrjava.core.Attributes; -import dev.zarr.zarrjava.ome.MultiscaleImage; -import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; -import dev.zarr.zarrjava.ome.Plate; -import dev.zarr.zarrjava.ome.UnifiedMultiscaleNode; -import dev.zarr.zarrjava.ome.UnifiedSinglescaleNode; -import dev.zarr.zarrjava.ome.Well; -import dev.zarr.zarrjava.ome.metadata.Axis; -import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; -import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; -import dev.zarr.zarrjava.ome.metadata.NamedEntry; -import dev.zarr.zarrjava.ome.metadata.OmeroMetadata; -import dev.zarr.zarrjava.ome.metadata.PlateMetadata; -import dev.zarr.zarrjava.ome.metadata.WellImage; -import dev.zarr.zarrjava.ome.metadata.WellMetadata; -import dev.zarr.zarrjava.ome.metadata.WellRef; -import dev.zarr.zarrjava.store.FilesystemStore; -import dev.zarr.zarrjava.store.StoreHandle; -import dev.zarr.zarrjava.v3.Array; -import dev.zarr.zarrjava.v3.DataType; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -public class OmeZarrTest extends ZarrTest { - - private StoreHandle storeHandle(java.nio.file.Path path) throws Exception { - return new FilesystemStore(path).resolve(); - } - - // ── v0.5 read tests ────────────────────────────────────────────────────── - - @Test - void readV05_axesAndDatasets() throws Exception { - MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); - - assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, image); - - UnifiedMultiscaleNode node = image.getMultiscaleNode(0); - assertEquals("test_image", node.name); - assertEquals(5, node.axes.size()); - assertEquals(2, node.nodes.size()); - - List axisNames = Arrays.asList("t", "c", "z", "y", "x"); - for (int i = 0; i < axisNames.size(); i++) { - assertEquals(axisNames.get(i), node.axes.get(i).name); - } - - List axisTypes = Arrays.asList("time", "channel", "space", "space", "space"); - for (int i = 0; i < axisTypes.size(); i++) { - assertEquals(axisTypes.get(i), node.axes.get(i).type); - } - - UnifiedSinglescaleNode scaleNode0 = node.nodes.get(0); - assertEquals("0", scaleNode0.path); - assertEquals("scale", scaleNode0.coordinateTransformations.get(0).type); - } - - @Test - void readV04_axesAndDatasets() throws Exception { - MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.4"))); - - assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, image); - - UnifiedMultiscaleNode node = image.getMultiscaleNode(0); - assertEquals("test_image", node.name); - assertEquals(5, node.axes.size()); - assertEquals(2, node.nodes.size()); - - List axisNames = Arrays.asList("t", "c", "z", "y", "x"); - for (int i = 0; i < axisNames.size(); i++) { - assertEquals(axisNames.get(i), node.axes.get(i).name); - } - - List axisTypes = Arrays.asList("time", "channel", "space", "space", "space"); - for (int i = 0; i < axisTypes.size(); i++) { - assertEquals(axisTypes.get(i), node.axes.get(i).type); - } - - UnifiedSinglescaleNode scaleNode0 = node.nodes.get(0); - assertEquals("0", scaleNode0.path); - assertEquals("scale", scaleNode0.coordinateTransformations.get(0).type); - } - - @Test - void readV05_getAxisNames() throws Exception { - MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); - assertEquals(Arrays.asList("t", "c", "z", "y", "x"), image.getAxisNames()); - } - - @Test - void readV05_openScaleLevel() throws Exception { - MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); - - dev.zarr.zarrjava.core.Array level0 = image.openScaleLevel(0); - assertArrayEquals(new long[]{1, 2, 8, 16, 16}, level0.metadata().shape); - - dev.zarr.zarrjava.core.Array level1 = image.openScaleLevel(1); - assertArrayEquals(new long[]{1, 2, 4, 8, 8}, level1.metadata().shape); - } - - @Test - void readV04_openScaleLevel() throws Exception { - MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.4"))); - - dev.zarr.zarrjava.core.Array level0 = image.openScaleLevel(0); - assertArrayEquals(new long[]{1, 2, 8, 16, 16}, level0.metadata().shape); - - dev.zarr.zarrjava.core.Array level1 = image.openScaleLevel(1); - assertArrayEquals(new long[]{1, 2, 4, 8, 8}, level1.metadata().shape); - } - - @Test - void readV05_scaleLevelCount() throws Exception { - MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); - assertEquals(2, image.getScaleLevelCount()); - } - - // ── v0.5 write tests ───────────────────────────────────────────────────── - - @Test - void writeV05_createAndReopen() throws Exception { - List axes = Arrays.asList( - new Axis("z", "space", "micrometer"), - new Axis("y", "space", "micrometer") - ); - MultiscalesEntry entry = new MultiscalesEntry(axes, Collections.emptyList()); - - StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_create")); - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage created = - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(handle, entry); - - dev.zarr.zarrjava.v3.ArrayMetadata arrayMetadata = Array.metadataBuilder() - .withShape(16, 16) - .withChunkShape(16, 16) - .withDataType(DataType.FLOAT32) - .build(); - List transforms = - Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(0.5, 0.5))); - created.createScaleLevel("0", arrayMetadata, transforms); - - MultiscaleImage reopened = MultiscaleImage.open(handle); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, reopened); - assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); - assertEquals(1, reopened.getScaleLevelCount()); - assertEquals("0", reopened.getMultiscaleNode(0).nodes.get(0).path); - } - - // ── v0.4 write tests ───────────────────────────────────────────────────── - - @Test - void writeV04_createAndReopen() throws Exception { - List axes = Arrays.asList( - new Axis("z", "space", "micrometer"), - new Axis("y", "space", "micrometer") - ); - MultiscalesEntry entry = new MultiscalesEntry(axes, Collections.emptyList()); - - StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_create")); - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage created = - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.create(handle, entry); - - dev.zarr.zarrjava.v2.ArrayMetadata arrayMetadata = new dev.zarr.zarrjava.v2.ArrayMetadata( - 2, - new long[]{16, 16}, - new int[]{16, 16}, - dev.zarr.zarrjava.v2.DataType.FLOAT32, - 0, - dev.zarr.zarrjava.v2.Order.C, - null, - null, - null - ); - List transforms = - Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(0.5, 0.5))); - created.createScaleLevel("0", arrayMetadata, transforms); - - MultiscaleImage reopened = MultiscaleImage.open(handle); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, reopened); - assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); - assertEquals(1, reopened.getScaleLevelCount()); - assertEquals("0", reopened.getMultiscaleNode(0).nodes.get(0).path); - } - - // ── typed metadata tests ───────────────────────────────────────────────── - - @Test - void readV05_typedMetadata() throws Exception { - MultiscalesMetadataImage image = - (MultiscalesMetadataImage) MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); - - MultiscalesEntry entry = image.getMultiscalesEntry(0); - assertEquals("test_image", entry.name); - assertNull(entry.version); - - List expectedScale = Arrays.asList(1.0, 1.0, 0.5, 0.5, 0.5); - assertEquals(expectedScale, entry.datasets.get(0).coordinateTransformations.get(0).scale); - } - - @Test - void readV04_entryHasVersion() throws Exception { - MultiscalesMetadataImage image = - (MultiscalesMetadataImage) MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.4"))); - - MultiscalesEntry entry = image.getMultiscalesEntry(0); - assertEquals("0.4", entry.version); - } - - // ── Omero + bioformats2raw.layout (read from testdata) ────────────────── - - @Test - void readV05_omero() throws Exception { - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage image = - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage( - storeHandle(TESTDATA.resolve("ome/v0.5"))); - - OmeroMetadata omero = image.getOmeroMetadata(); - assertNotNull(omero); - assertEquals(2, omero.channels.size()); - assertEquals("DAPI", omero.channels.get(0).get("label")); - assertEquals("GFP", omero.channels.get(1).get("label")); - assertEquals("color", omero.rdefs.get("model")); - } - - @Test - void readV04_omero() throws Exception { - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage image = - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage( - storeHandle(TESTDATA.resolve("ome/v0.4"))); - - OmeroMetadata omero = image.getOmeroMetadata(); - assertNotNull(omero); - assertEquals(2, omero.channels.size()); - assertEquals("DAPI", omero.channels.get(0).get("label")); - assertEquals("color", omero.rdefs.get("model")); - } - - @Test - void readV05_bioformats2rawLayout() throws Exception { - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage image = - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage( - storeHandle(TESTDATA.resolve("ome/v0.5"))); - assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); - } - - @Test - void readV04_bioformats2rawLayout() throws Exception { - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage image = - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage( - storeHandle(TESTDATA.resolve("ome/v0.4"))); - assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); - } - - // ── Labels (read from testdata) ────────────────────────────────────────── - - @Test - void readV05_labels() throws Exception { - MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.5"))); - - List labels = image.getLabels(); - assertEquals(Collections.singletonList("nuclei"), labels); - - MultiscaleImage nuclei = image.openLabel("nuclei"); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, nuclei); - assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); - } - - @Test - void readV04_labels() throws Exception { - MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v0.4"))); - - List labels = image.getLabels(); - assertEquals(Collections.singletonList("nuclei"), labels); - - MultiscaleImage nuclei = image.openLabel("nuclei"); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, nuclei); - assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); - } - - // ── HCS Plate (read from testdata) ─────────────────────────────────────── - - @Test - void readV05_plate() throws Exception { - Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.Plate.class, plate); - - PlateMetadata meta = plate.getPlateMetadata(); - assertEquals(2, meta.columns.size()); - assertEquals(2, meta.rows.size()); - assertEquals("A", meta.rows.get(0).name); - assertEquals("1", meta.columns.get(0).name); - assertEquals("A/1", meta.wells.get(0).path); - } - - @Test - void readV04_plate() throws Exception { - Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.Plate.class, plate); - - PlateMetadata meta = plate.getPlateMetadata(); - assertEquals(2, meta.columns.size()); - assertEquals("A", meta.rows.get(0).name); - assertEquals("A/1", meta.wells.get(0).path); - } - - @Test - void readV05_wellViaPlate() throws Exception { - Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); - Well well = plate.openWell("A/1"); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.Well.class, well); - - assertEquals(1, well.getWellMetadata().images.size()); - assertEquals("0", well.getWellMetadata().images.get(0).path); - assertEquals(Integer.valueOf(0), well.getWellMetadata().images.get(0).acquisition); - } - - @Test - void readV04_wellViaPlate() throws Exception { - Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); - Well well = plate.openWell("A/1"); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.Well.class, well); - - assertEquals(1, well.getWellMetadata().images.size()); - assertEquals("0", well.getWellMetadata().images.get(0).path); - } - - @Test - void readV05_hcsFullNavigation() throws Exception { - Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); - Well well = plate.openWell("A/1"); - MultiscaleImage fov = well.openImage("0"); - - assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, fov); - assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); - } - - @Test - void readV04_hcsFullNavigation() throws Exception { - Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); - Well well = plate.openWell("A/1"); - MultiscaleImage fov = well.openImage("0"); - - assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, fov); - assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); - } - - // ── Omero write round-trip (v0.4) ──────────────────────────────────────── - - @Test - void writeV04_omeroRoundTrip() throws Exception { - List axes = Arrays.asList( - new Axis("z", "space", "micrometer"), - new Axis("y", "space", "micrometer") - ); - MultiscalesEntry entry = new MultiscalesEntry(axes, Collections.emptyList()); - - StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_omero")); - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage created = - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.create(handle, entry); - - dev.zarr.zarrjava.v2.ArrayMetadata arrayMetadata = new dev.zarr.zarrjava.v2.ArrayMetadata( - 2, new long[]{16, 16}, new int[]{16, 16}, - dev.zarr.zarrjava.v2.DataType.FLOAT32, 0, - dev.zarr.zarrjava.v2.Order.C, null, null, null); - created.createScaleLevel("0", arrayMetadata, - Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); - - Map channelMap = new HashMap(); - channelMap.put("label", "DAPI"); - channelMap.put("color", "0000FF"); - Map rdefsMap = new HashMap(); - rdefsMap.put("model", "color"); - OmeroMetadata omero = new OmeroMetadata(Collections.singletonList(channelMap), rdefsMap); - created.setOmeroMetadata(omero); - - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage reopened = - dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(handle); - OmeroMetadata got = reopened.getOmeroMetadata(); - assertNotNull(got); - assertEquals("DAPI", got.channels.get(0).get("label")); - assertEquals("color", got.rdefs.get("model")); - } - - // ── Labels write round-trip (v0.5) ─────────────────────────────────────── - - @Test - void writeV05_labelsRoundTrip() throws Exception { - List axes = Arrays.asList( - new Axis("z", "space", "micrometer"), - new Axis("y", "space", "micrometer") - ); - StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_labels")); - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage parent = - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); - - Attributes labelsAttrs = new Attributes(); - labelsAttrs.put("labels", Arrays.asList("nuclei")); - dev.zarr.zarrjava.v3.Group.create(handle.resolve("labels"), labelsAttrs); - - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage nuclei = - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create( - handle.resolve("labels").resolve("nuclei"), - new MultiscalesEntry(axes, Collections.emptyList())); - nuclei.createScaleLevel("0", - Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.UINT8).build(), - Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); - - MultiscaleImage reopened = MultiscaleImage.open(handle); - assertEquals(Collections.singletonList("nuclei"), reopened.getLabels()); - assertEquals(Arrays.asList("z", "y"), reopened.openLabel("nuclei").getAxisNames()); - } - - // ── HCS write round-trips ──────────────────────────────────────────────── - - @Test - void writeV05_plateRoundTrip() throws Exception { - PlateMetadata plateMetadata = new PlateMetadata( - Arrays.asList(new NamedEntry("1"), new NamedEntry("2")), - Arrays.asList(new NamedEntry("A"), new NamedEntry("B")), - Collections.singletonList(new WellRef("A/1", 0, 0)), - null, null, null, null); - - StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_plate")); - dev.zarr.zarrjava.ome.v0_5.Plate.createPlate(handle, plateMetadata); - - Plate reopened = Plate.open(handle); - assertEquals(2, reopened.getPlateMetadata().columns.size()); - assertEquals("A", reopened.getPlateMetadata().rows.get(0).name); - assertEquals("A/1", reopened.getPlateMetadata().wells.get(0).path); - } - - @Test - void writeV04_plateRoundTrip() throws Exception { - PlateMetadata plateMetadata = new PlateMetadata( - Arrays.asList(new NamedEntry("1"), new NamedEntry("2")), - Arrays.asList(new NamedEntry("A"), new NamedEntry("B")), - Collections.singletonList(new WellRef("A/1", 0, 0)), - null, null, null, null); - - StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_plate")); - dev.zarr.zarrjava.ome.v0_4.Plate.createPlate(handle, plateMetadata); - - Plate reopened = Plate.open(handle); - assertEquals(2, reopened.getPlateMetadata().columns.size()); - assertEquals("A/1", reopened.getPlateMetadata().wells.get(0).path); - } - - @Test - void writeV05_hcsFullIntegration() throws Exception { - StoreHandle plateHandle = storeHandle(TESTOUTPUT.resolve("ome_v05_hcs_full")); - - dev.zarr.zarrjava.ome.v0_5.Plate.createPlate(plateHandle, new PlateMetadata( - Collections.singletonList(new NamedEntry("1")), - Collections.singletonList(new NamedEntry("A")), - Collections.singletonList(new WellRef("A/1", 0, 0)), - null, null, null, null)); - - dev.zarr.zarrjava.ome.v0_5.Well.createWell( - plateHandle.resolve("A/1"), - new WellMetadata(Collections.singletonList(new WellImage("0", null)))); - - List axes = Arrays.asList(new Axis("z", "space", "micrometer"), new Axis("y", "space", "micrometer")); - dev.zarr.zarrjava.ome.v0_5.MultiscaleImage fov = dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create( - plateHandle.resolve("A/1").resolve("0"), - new MultiscalesEntry(axes, Collections.emptyList())); - fov.createScaleLevel("0", - Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.FLOAT32).build(), - Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); - - MultiscaleImage image = Plate.open(plateHandle).openWell("A/1").openImage("0"); - assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, image); - assertEquals(Arrays.asList("z", "y"), image.getAxisNames()); - } -} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java new file mode 100644 index 00000000..abd016d6 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java @@ -0,0 +1,86 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Abstract base for OME-Zarr multiscale image tests. + * + *

Exercises the unified {@link MultiscaleImage} interface contract that all versions + * (v0.4, v0.5, v0.6) must satisfy. Version-specific tests live in the concrete subclasses. + */ +public abstract class OmeZarrBaseTest extends ZarrTest { + + /** Returns the store handle for a representative multiscale image of this version. */ + abstract StoreHandle imageStoreHandle() throws Exception; + + /** Expected concrete implementation class. */ + abstract Class expectedConcreteClass(); + + /** Expected number of scale levels in the test image. */ + abstract int expectedScaleLevelCount(); + + /** Expected shape of scale level 0. */ + abstract long[] expectedLevel0Shape(); + + /** Expected axis names (from the unified interface). */ + abstract List expectedAxisNames(); + + // ── helpers ────────────────────────────────────────────────────────────── + + protected StoreHandle storeHandle(Path path) throws Exception { + return new FilesystemStore(path).resolve(); + } + + // ── unified interface contract tests ───────────────────────────────────── + + @Test + void open_returnsCorrectConcreteType() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertInstanceOf(expectedConcreteClass(), image); + } + + @Test + void getMultiscaleNode_hasExpectedAxes() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + UnifiedMultiscaleNode node = image.getMultiscaleNode(0); + assertNotNull(node); + assertEquals(expectedAxisNames().size(), node.axes.size()); + for (int i = 0; i < expectedAxisNames().size(); i++) { + assertEquals(expectedAxisNames().get(i), node.axes.get(i).name); + } + } + + @Test + void getMultiscaleNode_hasExpectedLevelCount() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + UnifiedMultiscaleNode node = image.getMultiscaleNode(0); + assertEquals(expectedScaleLevelCount(), node.nodes.size()); + } + + @Test + void getAxisNames_returnsExpected() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(expectedAxisNames(), image.getAxisNames()); + } + + @Test + void getScaleLevelCount_returnsExpected() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(expectedScaleLevelCount(), image.getScaleLevelCount()); + } + + @Test + void openScaleLevel_level0HasExpectedShape() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + dev.zarr.zarrjava.core.Array array = image.openScaleLevel(0); + assertArrayEquals(expectedLevel0Shape(), array.metadata().shape); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV04Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV04Test.java new file mode 100644 index 00000000..02a2212c --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV04Test.java @@ -0,0 +1,184 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.ome.metadata.NamedEntry; +import dev.zarr.zarrjava.ome.metadata.WellRef; +import dev.zarr.zarrjava.ome.metadata.WellImage; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrV04Test extends OmeZarrBaseTest { + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(TESTDATA.resolve("ome/v0.4")); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class; + } + + @Override + int expectedScaleLevelCount() { return 2; } + + @Override + long[] expectedLevel0Shape() { return new long[]{1, 2, 8, 16, 16}; } + + @Override + List expectedAxisNames() { + return Arrays.asList("t", "c", "z", "y", "x"); + } + + // ── typed metadata ─────────────────────────────────────────────────────── + + @Test + void typedEntry_hasVersion() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + assertEquals("0.4", entry.version); + } + + @Test + void typedEntry_level0ScaleValues() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + List expected = Arrays.asList(1.0, 1.0, 0.5, 0.5, 0.5); + assertEquals(expected, entry.datasets.get(0).coordinateTransformations.get(0).scale); + } + + // ── omero + bioformats2raw ─────────────────────────────────────────────── + + @Test + void omero_channels() throws Exception { + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + OmeroMetadata omero = image.getOmeroMetadata(); + assertNotNull(omero); + assertEquals(2, omero.channels.size()); + assertEquals("DAPI", omero.channels.get(0).get("label")); + assertEquals("color", omero.rdefs.get("model")); + } + + @Test + void bioformats2rawLayout_value() throws Exception { + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); + } + + // ── labels ─────────────────────────────────────────────────────────────── + + @Test + void labels_list() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(Collections.singletonList("nuclei"), image.getLabels()); + } + + @Test + void labels_openLabel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscaleImage nuclei = image.openLabel("nuclei"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, nuclei); + assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); + } + + // ── HCS ────────────────────────────────────────────────────────────────── + + @Test + void hcs_plate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.Plate.class, plate); + PlateMetadata meta = plate.getPlateMetadata(); + assertEquals(2, meta.columns.size()); + assertEquals("A", meta.rows.get(0).name); + assertEquals("A/1", meta.wells.get(0).path); + } + + @Test + void hcs_wellViaPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.Well.class, well); + assertEquals(1, well.getWellMetadata().images.size()); + assertEquals("0", well.getWellMetadata().images.get(0).path); + } + + @Test + void hcs_fullNavigation() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.4_hcs"))); + MultiscaleImage fov = plate.openWell("A/1").openImage("0"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, fov); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); + } + + // ── write round-trips ──────────────────────────────────────────────────── + + @Test + void write_createAndReopen() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_create")); + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + created.createScaleLevel("0", + new dev.zarr.zarrjava.v2.ArrayMetadata(2, new long[]{16, 16}, new int[]{16, 16}, + dev.zarr.zarrjava.v2.DataType.FLOAT32, 0, dev.zarr.zarrjava.v2.Order.C, null, null, null), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(0.5, 0.5)))); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.class, reopened); + assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); + assertEquals(1, reopened.getScaleLevelCount()); + } + + @Test + void write_omeroRoundTrip() throws Exception { + List axes = Arrays.asList(new Axis("z", "space", "micrometer"), new Axis("y", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_omero")); + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + created.createScaleLevel("0", + new dev.zarr.zarrjava.v2.ArrayMetadata(2, new long[]{16, 16}, new int[]{16, 16}, + dev.zarr.zarrjava.v2.DataType.FLOAT32, 0, dev.zarr.zarrjava.v2.Order.C, null, null, null), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + java.util.Map ch = new java.util.HashMap(); + ch.put("label", "DAPI"); + java.util.Map rd = new java.util.HashMap(); + rd.put("model", "color"); + created.setOmeroMetadata(new OmeroMetadata(Collections.singletonList(ch), rd)); + + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage reopened = + dev.zarr.zarrjava.ome.v0_4.MultiscaleImage.openMultiscaleImage(handle); + OmeroMetadata got = reopened.getOmeroMetadata(); + assertNotNull(got); + assertEquals("DAPI", got.channels.get(0).get("label")); + } + + @Test + void write_plateRoundTrip() throws Exception { + PlateMetadata meta = new PlateMetadata( + Arrays.asList(new NamedEntry("1"), new NamedEntry("2")), + Arrays.asList(new NamedEntry("A"), new NamedEntry("B")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v04_plate")); + dev.zarr.zarrjava.ome.v0_4.Plate.createPlate(handle, meta); + Plate reopened = Plate.open(handle); + assertEquals(2, reopened.getPlateMetadata().columns.size()); + assertEquals("A/1", reopened.getPlateMetadata().wells.get(0).path); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java new file mode 100644 index 00000000..b4d5c476 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java @@ -0,0 +1,223 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.metadata.Axis; +import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.ome.metadata.NamedEntry; +import dev.zarr.zarrjava.ome.metadata.OmeroMetadata; +import dev.zarr.zarrjava.ome.metadata.PlateMetadata; +import dev.zarr.zarrjava.ome.metadata.WellImage; +import dev.zarr.zarrjava.ome.metadata.WellMetadata; +import dev.zarr.zarrjava.ome.metadata.WellRef; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Array; +import dev.zarr.zarrjava.v3.DataType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrV05Test extends OmeZarrBaseTest { + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(TESTDATA.resolve("ome/v0.5")); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class; + } + + @Override + int expectedScaleLevelCount() { return 2; } + + @Override + long[] expectedLevel0Shape() { return new long[]{1, 2, 8, 16, 16}; } + + @Override + List expectedAxisNames() { + return Arrays.asList("t", "c", "z", "y", "x"); + } + + // ── typed metadata ─────────────────────────────────────────────────────── + + @Test + void typedEntry_noVersion() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + assertEquals("test_image", entry.name); + assertNull(entry.version); + } + + @Test + void typedEntry_level0ScaleValues() throws Exception { + MultiscalesMetadataImage image = (MultiscalesMetadataImage) MultiscaleImage.open(imageStoreHandle()); + MultiscalesEntry entry = (MultiscalesEntry) image.getMultiscalesEntry(0); + List expected = Arrays.asList(1.0, 1.0, 0.5, 0.5, 0.5); + assertEquals(expected, entry.datasets.get(0).coordinateTransformations.get(0).scale); + } + + @Test + void openScaleLevel_level1HasExpectedShape() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + dev.zarr.zarrjava.core.Array level1 = image.openScaleLevel(1); + assertArrayEquals(new long[]{1, 2, 4, 8, 8}, level1.metadata().shape); + } + + // ── omero + bioformats2raw ─────────────────────────────────────────────── + + @Test + void omero_channels() throws Exception { + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + OmeroMetadata omero = image.getOmeroMetadata(); + assertNotNull(omero); + assertEquals(2, omero.channels.size()); + assertEquals("DAPI", omero.channels.get(0).get("label")); + assertEquals("GFP", omero.channels.get(1).get("label")); + assertEquals("color", omero.rdefs.get("model")); + } + + @Test + void bioformats2rawLayout_value() throws Exception { + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.openMultiscaleImage(imageStoreHandle()); + assertEquals(Integer.valueOf(3), image.getBioformats2rawLayout()); + } + + // ── labels ─────────────────────────────────────────────────────────────── + + @Test + void labels_list() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + assertEquals(Collections.singletonList("nuclei"), image.getLabels()); + } + + @Test + void labels_openLabel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscaleImage nuclei = image.openLabel("nuclei"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, nuclei); + assertEquals(Arrays.asList("z", "y", "x"), nuclei.getAxisNames()); + } + + // ── HCS ────────────────────────────────────────────────────────────────── + + @Test + void hcs_plate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.Plate.class, plate); + PlateMetadata meta = plate.getPlateMetadata(); + assertEquals(2, meta.columns.size()); + assertEquals(2, meta.rows.size()); + assertEquals("A", meta.rows.get(0).name); + assertEquals("1", meta.columns.get(0).name); + assertEquals("A/1", meta.wells.get(0).path); + } + + @Test + void hcs_wellViaPlate() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + Well well = plate.openWell("A/1"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.Well.class, well); + assertEquals(1, well.getWellMetadata().images.size()); + assertEquals("0", well.getWellMetadata().images.get(0).path); + assertEquals(Integer.valueOf(0), well.getWellMetadata().images.get(0).acquisition); + } + + @Test + void hcs_fullNavigation() throws Exception { + Plate plate = Plate.open(storeHandle(TESTDATA.resolve("ome/v0.5_hcs"))); + MultiscaleImage fov = plate.openWell("A/1").openImage("0"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, fov); + assertEquals(Arrays.asList("t", "c", "z", "y", "x"), fov.getAxisNames()); + } + + // ── write round-trips ──────────────────────────────────────────────────── + + @Test + void write_createAndReopen() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_create")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage created = + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + created.createScaleLevel("0", + Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.FLOAT32).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(0.5, 0.5)))); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, reopened); + assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); + assertEquals(1, reopened.getScaleLevelCount()); + assertEquals("0", reopened.getMultiscaleNode(0).nodes.get(0).path); + } + + @Test + void write_labelsRoundTrip() throws Exception { + List axes = Arrays.asList( + new Axis("z", "space", "micrometer"), + new Axis("y", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_labels")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + + Attributes labelsAttrs = new Attributes(); + labelsAttrs.put("labels", Arrays.asList("nuclei")); + dev.zarr.zarrjava.v3.Group.create(handle.resolve("labels"), labelsAttrs); + + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage nuclei = dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create( + handle.resolve("labels").resolve("nuclei"), new MultiscalesEntry(axes, Collections.emptyList())); + nuclei.createScaleLevel("0", + Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.UINT8).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + MultiscaleImage reopened = MultiscaleImage.open(handle); + assertEquals(Collections.singletonList("nuclei"), reopened.getLabels()); + assertEquals(Arrays.asList("z", "y"), reopened.openLabel("nuclei").getAxisNames()); + } + + @Test + void write_plateRoundTrip() throws Exception { + PlateMetadata meta = new PlateMetadata( + Arrays.asList(new NamedEntry("1"), new NamedEntry("2")), + Arrays.asList(new NamedEntry("A"), new NamedEntry("B")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_plate")); + dev.zarr.zarrjava.ome.v0_5.Plate.createPlate(handle, meta); + Plate reopened = Plate.open(handle); + assertEquals(2, reopened.getPlateMetadata().columns.size()); + assertEquals("A", reopened.getPlateMetadata().rows.get(0).name); + assertEquals("A/1", reopened.getPlateMetadata().wells.get(0).path); + } + + @Test + void write_hcsFullIntegration() throws Exception { + StoreHandle plateHandle = storeHandle(TESTOUTPUT.resolve("ome_v05_hcs_full")); + dev.zarr.zarrjava.ome.v0_5.Plate.createPlate(plateHandle, new PlateMetadata( + Collections.singletonList(new NamedEntry("1")), + Collections.singletonList(new NamedEntry("A")), + Collections.singletonList(new WellRef("A/1", 0, 0)), + null, null, null, null)); + dev.zarr.zarrjava.ome.v0_5.Well.createWell( + plateHandle.resolve("A/1"), + new WellMetadata(Collections.singletonList(new WellImage("0", null)))); + + List axes = Arrays.asList(new Axis("z", "space", "micrometer"), new Axis("y", "space", "micrometer")); + dev.zarr.zarrjava.ome.v0_5.MultiscaleImage fov = dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.create( + plateHandle.resolve("A/1").resolve("0"), new MultiscalesEntry(axes, Collections.emptyList())); + fov.createScaleLevel("0", + Array.metadataBuilder().withShape(16, 16).withChunkShape(16, 16).withDataType(DataType.FLOAT32).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + MultiscaleImage image = Plate.open(plateHandle).openWell("A/1").openImage("0"); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, image); + assertEquals(Arrays.asList("z", "y"), image.getAxisNames()); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java new file mode 100644 index 00000000..41867ec9 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java @@ -0,0 +1,124 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class OmeZarrV06Test extends OmeZarrBaseTest { + + private static final java.nio.file.Path V06_2D = + TESTDATA.resolve("ome/v0.6/examples/2d/basic/scale_multiscale.zarr"); + private static final java.nio.file.Path V06_3D = + TESTDATA.resolve("ome/v0.6/examples/3d/basic/scale_multiscale.zarr"); + + @Override + StoreHandle imageStoreHandle() throws Exception { + return storeHandle(V06_2D); + } + + @Override + Class expectedConcreteClass() { + return dev.zarr.zarrjava.ome.v0_6.MultiscaleImage.class; + } + + @Override + int expectedScaleLevelCount() { return 3; } + + @Override + long[] expectedLevel0Shape() { return new long[]{576, 720}; } + + @Override + List expectedAxisNames() { + return Arrays.asList("y", "x"); + } + + // ── v0.6-specific: coordinate systems ──────────────────────────────────── + + @Test + @SuppressWarnings("unchecked") + void coordinateSystems_presentInEntry() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscalesMetadataImage mmi = (MultiscalesMetadataImage) image; + MultiscalesEntry entry = (MultiscalesEntry) mmi.getMultiscalesEntry(0); + + assertNotNull(entry.coordinateSystems); + assertEquals(1, entry.coordinateSystems.size()); + + CoordinateSystem cs = entry.coordinateSystems.get(0); + assertEquals("physical", cs.name); + assertNotNull(cs.axes); + assertEquals(2, cs.axes.size()); + assertEquals("y", cs.axes.get(0).name); + assertEquals("x", cs.axes.get(1).name); + } + + @Test + @SuppressWarnings("unchecked") + void datasets_pathsAndTransformations() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + MultiscalesMetadataImage mmi = (MultiscalesMetadataImage) image; + MultiscalesEntry entry = (MultiscalesEntry) mmi.getMultiscalesEntry(0); + + assertNotNull(entry.datasets); + assertEquals(3, entry.datasets.size()); + assertEquals("s0", entry.datasets.get(0).path); + assertEquals("s1", entry.datasets.get(1).path); + assertEquals("s2", entry.datasets.get(2).path); + + dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation ct = + entry.datasets.get(0).coordinateTransformations.get(0); + assertEquals("scale", ct.type); + assertEquals("s0", ct.input); + assertEquals("physical", ct.output); + assertNotNull(ct.scale); + assertEquals(2, ct.scale.size()); + assertEquals(6.0, ct.scale.get(0), 1e-9); + assertEquals(4.0, ct.scale.get(1), 1e-9); + } + + @Test + void unifiedInterface_nodesAndPaths() throws Exception { + MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); + UnifiedMultiscaleNode node = image.getMultiscaleNode(0); + + assertEquals("multiscales", node.name); + assertEquals(3, node.nodes.size()); + assertEquals("s0", node.nodes.get(0).path); + assertEquals("scale", node.nodes.get(0).coordinateTransformations.get(0).type); + } + + // ── 3D example ─────────────────────────────────────────────────────────── + + @Test + @SuppressWarnings("unchecked") + void read3d_axesFromCoordinateSystems() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(V06_3D)); + assertInstanceOf(dev.zarr.zarrjava.ome.v0_6.MultiscaleImage.class, image); + + MultiscalesMetadataImage mmi = (MultiscalesMetadataImage) image; + MultiscalesEntry entry = (MultiscalesEntry) mmi.getMultiscalesEntry(0); + + assertEquals(3, entry.datasets.size()); + assertNotNull(entry.coordinateSystems); + assertFalse(entry.coordinateSystems.isEmpty()); + + List axes = entry.coordinateSystems.get(0).axes; + assertEquals(3, axes.size()); + assertEquals("z", axes.get(0).name); + assertEquals("y", axes.get(1).name); + assertEquals("x", axes.get(2).name); + } + + @Test + void read3d_unifiedAxisNames() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(V06_3D)); + List axisNames = image.getAxisNames(); + assertEquals(Arrays.asList("z", "y", "x"), axisNames); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java new file mode 100644 index 00000000..767b4885 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java @@ -0,0 +1,119 @@ +package dev.zarr.zarrjava.ome; + +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.v1_0.Collection; +import dev.zarr.zarrjava.ome.v1_0.metadata.CollectionMetadata; +import dev.zarr.zarrjava.ome.v1_0.metadata.Level; +import dev.zarr.zarrjava.ome.v1_0.metadata.MultiscaleMetadata; +import dev.zarr.zarrjava.ome.v1_0.metadata.NodeRef; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.ArrayMetadata; +import dev.zarr.zarrjava.v3.DataType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for v1.0 (RFC-8). Standalone — Collection does not share the unified + * MultiscaleImage base class contract (different metadata model). + */ +public class OmeZarrV10Test extends ZarrTest { + + private StoreHandle storeHandle(java.nio.file.Path path) throws Exception { + return new FilesystemStore(path).resolve(); + } + + @Test + void readMultiscaleImage_concreteType() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v1.0/image"))); + assertInstanceOf(dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.class, image); + } + + @Test + void readMultiscaleImage_unifiedInterface() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v1.0/image"))); + UnifiedMultiscaleNode node = image.getMultiscaleNode(0); + + assertEquals("test_image", node.name); + assertNotNull(node.axes); + assertFalse(node.axes.isEmpty()); + assertFalse(node.nodes.isEmpty()); + assertEquals("s0", node.nodes.get(0).path); + } + + @Test + void readMultiscaleImage_openScaleLevel() throws Exception { + MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v1.0/image"))); + dev.zarr.zarrjava.core.Array array = image.openScaleLevel(0); + assertArrayEquals(new long[]{8, 16, 16}, array.metadata().shape); + } + + @Test + void readCollection_metadata() throws Exception { + Collection collection = Collection.openCollection(storeHandle(TESTDATA.resolve("ome/v1.0"))); + CollectionMetadata meta = collection.getCollectionMetadata(); + assertNotNull(meta); + assertEquals("test_collection", meta.name); + assertEquals(1, meta.nodes.size()); + assertEquals("multiscale", meta.nodes.get(0).type); + assertEquals("image", meta.nodes.get(0).path); + } + + @Test + void readCollection_openNodeReturnsMultiscaleImage() throws Exception { + Collection collection = Collection.openCollection(storeHandle(TESTDATA.resolve("ome/v1.0"))); + Object node = collection.openNode("image"); + assertInstanceOf(dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.class, node); + } + + @Test + void write_roundTrip() throws Exception { + StoreHandle collectionHandle = new FilesystemStore(TESTOUTPUT.resolve("v10_collection")).resolve(); + StoreHandle imageHandle = collectionHandle.resolve("image"); + + CoordinateSystem cs = new CoordinateSystem("physical", + Arrays.asList( + new dev.zarr.zarrjava.ome.metadata.Axis("z", "space", "micrometer", null, null), + new dev.zarr.zarrjava.ome.metadata.Axis("y", "space", "micrometer", null, null), + new dev.zarr.zarrjava.ome.metadata.Axis("x", "space", "micrometer", null, null))); + + MultiscaleMetadata msm = new MultiscaleMetadata( + "written_image", + Collections.emptyList(), + Collections.singletonList(cs)); + dev.zarr.zarrjava.ome.v1_0.MultiscaleImage image = + dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.create(imageHandle, msm); + + ArrayMetadata arrayMetadata = dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(4, 8, 8) + .withChunkShape(4, 8, 8) + .withDataType(DataType.FLOAT32) + .build(); + image.createLevel("s0", arrayMetadata, + Collections.singletonList( + CoordinateTransformation.scale(Arrays.asList(1.0, 1.0, 1.0), "s0", "physical"))); + + CollectionMetadata cm = new CollectionMetadata( + "written_collection", + Collections.singletonList(new NodeRef("multiscale", "image"))); + Collection.createCollection(collectionHandle, cm); + + Collection readCollection = Collection.openCollection(collectionHandle); + assertEquals("written_collection", readCollection.getCollectionMetadata().name); + assertEquals(1, readCollection.getCollectionMetadata().nodes.size()); + + Object node = readCollection.openNode("image"); + assertInstanceOf(dev.zarr.zarrjava.ome.v1_0.MultiscaleImage.class, node); + dev.zarr.zarrjava.ome.v1_0.MultiscaleImage readImage = + (dev.zarr.zarrjava.ome.v1_0.MultiscaleImage) node; + assertEquals("written_image", readImage.getMultiscaleMetadata().name); + assertEquals(1, readImage.getScaleLevelCount()); + assertEquals("s0", readImage.getMultiscaleMetadata().levels.get(0).path); + } +} diff --git a/testdata/ome/v1.0/image/s0/c/0/0/0 b/testdata/ome/v1.0/image/s0/c/0/0/0 new file mode 100644 index 0000000000000000000000000000000000000000..bc977586227027a77976b60d9156142d4aed89b6 GIT binary patch literal 7352 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 eEdr literal 0 HcmV?d00001 diff --git a/testdata/ome/v1.0/image/s0/zarr.json b/testdata/ome/v1.0/image/s0/zarr.json new file mode 100644 index 00000000..cd859e9a --- /dev/null +++ b/testdata/ome/v1.0/image/s0/zarr.json @@ -0,0 +1,44 @@ +{ + "shape": [ + 8, + 16, + 16 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 8, + 16, + 16 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/testdata/ome/v1.0/image/zarr.json b/testdata/ome/v1.0/image/zarr.json new file mode 100644 index 00000000..ccc2c123 --- /dev/null +++ b/testdata/ome/v1.0/image/zarr.json @@ -0,0 +1,51 @@ +{ + "attributes": { + "ome": { + "version": "1.0-dev", + "multiscale": { + "name": "test_image", + "coordinateSystems": [ + { + "name": "physical", + "axes": [ + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } + ] + } + ], + "levels": [ + { + "path": "s0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0, + 1.0 + ], + "input": "s0", + "output": "physical" + } + ] + } + ] + } + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/testdata/ome/v1.0/zarr.json b/testdata/ome/v1.0/zarr.json new file mode 100644 index 00000000..17624283 --- /dev/null +++ b/testdata/ome/v1.0/zarr.json @@ -0,0 +1,18 @@ +{ + "attributes": { + "ome": { + "version": "1.0-dev", + "collection": { + "name": "test_collection", + "nodes": [ + { + "type": "multiscale", + "path": "image" + } + ] + } + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file From c1eb28728bdfb27f5260b94c0dd6fe19ced09bf5 Mon Sep 17 00:00:00 2001 From: brokkoli71 Date: Thu, 5 Mar 2026 15:18:35 +0100 Subject: [PATCH 8/8] reduce code duplication and better unified interface between versions (via MultiscalesEntry) --- .../zarr/zarrjava/ome/MultiscaleImage.java | 13 ++-- .../ome/MultiscalesMetadataImage.java | 23 ++++--- .../dev/zarr/zarrjava/ome/OmeV2Group.java | 62 +++++++++++++++++++ .../dev/zarr/zarrjava/ome/OmeV3Group.java | 48 ++++++++++++++ .../zarrjava/ome/UnifiedMultiscaleNode.java | 21 ------- .../zarrjava/ome/UnifiedSinglescaleNode.java | 17 ----- .../zarrjava/ome/v0_4/MultiscaleImage.java | 41 +++--------- .../dev/zarr/zarrjava/ome/v0_4/Plate.java | 23 ++----- .../java/dev/zarr/zarrjava/ome/v0_4/Well.java | 23 ++----- .../zarrjava/ome/v0_5/MultiscaleImage.java | 30 ++------- .../dev/zarr/zarrjava/ome/v0_5/Plate.java | 23 ++----- .../java/dev/zarr/zarrjava/ome/v0_5/Well.java | 23 ++----- .../zarrjava/ome/v0_6/MultiscaleImage.java | 49 +++++---------- .../zarr/zarrjava/ome/v1_0/Collection.java | 31 +++------- .../zarrjava/ome/v1_0/MultiscaleImage.java | 44 ++++--------- .../zarr/zarrjava/ome/OmeZarrBaseTest.java | 13 ++-- .../dev/zarr/zarrjava/ome/OmeZarrV05Test.java | 2 +- .../dev/zarr/zarrjava/ome/OmeZarrV06Test.java | 27 ++++---- .../dev/zarr/zarrjava/ome/OmeZarrV10Test.java | 13 ++-- 19 files changed, 227 insertions(+), 299 deletions(-) create mode 100644 src/main/java/dev/zarr/zarrjava/ome/OmeV2Group.java create mode 100644 src/main/java/dev/zarr/zarrjava/ome/OmeV3Group.java delete mode 100644 src/main/java/dev/zarr/zarrjava/ome/UnifiedMultiscaleNode.java delete mode 100644 src/main/java/dev/zarr/zarrjava/ome/UnifiedSinglescaleNode.java diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java index 5410c2f3..8ad8d6e1 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscaleImage.java @@ -2,11 +2,11 @@ import dev.zarr.zarrjava.ZarrException; import dev.zarr.zarrjava.core.Node; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; import dev.zarr.zarrjava.store.StoreHandle; import dev.zarr.zarrjava.utils.Utils; import java.io.IOException; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -22,9 +22,10 @@ public interface MultiscaleImage { StoreHandle getStoreHandle(); /** - * Returns the multiscale node descriptor at index {@code i}. + * Returns a {@link MultiscalesEntry} view of multiscale {@code i}, normalized to the shared + * metadata type. All axis and dataset information is accessible from the returned entry. */ - UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException; + MultiscalesEntry getMultiscaleNode(int i) throws ZarrException; /** * Opens the scale level array at index {@code i} within the first multiscale entry. @@ -40,9 +41,9 @@ public interface MultiscaleImage { * Returns the axis names of the first multiscale entry. */ default List getAxisNames() throws ZarrException { - UnifiedMultiscaleNode node = getMultiscaleNode(0); + MultiscalesEntry entry = getMultiscaleNode(0); List names = new ArrayList<>(); - for (dev.zarr.zarrjava.ome.metadata.Axis axis : node.axes) { + for (dev.zarr.zarrjava.ome.metadata.Axis axis : entry.axes) { names.add(axis.name); } return names; @@ -103,7 +104,7 @@ default MultiscaleImage openLabel(String name) throws IOException, ZarrException *

Tries v0.5 (zarr.json with "ome" key) first, then v0.4 (.zattrs with "multiscales" key). */ static MultiscaleImage open(StoreHandle storeHandle) throws IOException, ZarrException { - // Try v0.5: zarr.json with "ome" key + // Try version>= 0.5: zarr.json with "ome" key StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON); if (zarrJson.exists()) { com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper(); diff --git a/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java b/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java index 2e22b4fa..d804f813 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/MultiscalesMetadataImage.java @@ -5,7 +5,6 @@ import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; import java.io.IOException; -import java.util.ArrayList; import java.util.List; /** @@ -17,13 +16,13 @@ public interface MultiscalesMetadataImage extends MultiscaleImage { /** - * Returns the raw multiscales entry at index {@code i}. + * Returns the raw multiscales entry at index {@code i} — the version-specific type. */ M getMultiscalesEntry(int i) throws ZarrException; /** - * Creates a new scale level array at {@code path} with the given metadata and coordinate transformations, - * then registers it in the multiscales metadata. + * Creates a new scale level array at {@code path} with the given metadata and coordinate + * transformations, then registers it in the multiscales metadata. */ void createScaleLevel( String path, @@ -31,19 +30,19 @@ void createScaleLevel( List coordinateTransformations ) throws IOException, ZarrException; + /** + * Default implementation: casts the version-specific entry to the shared {@link MultiscalesEntry}. + * Versions whose entry type does not extend {@link MultiscalesEntry} (e.g., v0.6, v1.0) must + * override {@link #getMultiscaleNode(int)} directly. + */ @Override - default UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException { + default MultiscalesEntry getMultiscaleNode(int i) throws ZarrException { Object entry = getMultiscalesEntry(i); if (!(entry instanceof MultiscalesEntry)) { throw new ZarrException( "getMultiscaleNode() not supported for entry type " + entry.getClass().getName() - + "; override getMultiscaleNode() in your MultiscalesMetadataImage implementation."); - } - MultiscalesEntry mse = (MultiscalesEntry) entry; - List nodes = new ArrayList<>(); - for (dev.zarr.zarrjava.ome.metadata.Dataset dataset : mse.datasets) { - nodes.add(new UnifiedSinglescaleNode(dataset.path, dataset.coordinateTransformations)); + + "; override getMultiscaleNode() in your implementation."); } - return new UnifiedMultiscaleNode(mse.name, mse.axes, nodes); + return (MultiscalesEntry) entry; } } diff --git a/src/main/java/dev/zarr/zarrjava/ome/OmeV2Group.java b/src/main/java/dev/zarr/zarrjava/ome/OmeV2Group.java new file mode 100644 index 00000000..d95706a2 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/OmeV2Group.java @@ -0,0 +1,62 @@ +package dev.zarr.zarrjava.ome; + +import com.fasterxml.jackson.core.type.TypeReference; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v2.Group; +import dev.zarr.zarrjava.v2.GroupMetadata; + +import javax.annotation.Nonnull; + +/** + * Base class for all OME-Zarr nodes backed by a Zarr v2 group. + * + *

Provides {@code protected static} helpers for reading attributes and building + * {@link Attributes} for writing. The actual byte serialization is performed by + * {@link dev.zarr.zarrjava.v2.Node#makeObjectWriter()} inside {@code Group.create()} and + * {@code Group.setAttributes()}. + */ +public abstract class OmeV2Group extends Group { + + protected OmeV2Group(@Nonnull StoreHandle storeHandle, @Nonnull GroupMetadata groupMetadata) { + super(storeHandle, groupMetadata); + } + + /** Reads and converts a named attribute value from the given v2 group's attributes. */ + protected static T readAttribute( + Attributes attributes, StoreHandle storeHandle, String key, Class cls) + throws ZarrException { + if (attributes == null || !attributes.containsKey(key)) { + throw new ZarrException("No '" + key + "' key found in attributes at " + storeHandle); + } + return dev.zarr.zarrjava.v2.Node.makeObjectMapper().convertValue(attributes.get(key), cls); + } + + /** Reads and converts a named attribute using a {@link TypeReference} (e.g. for {@code List}). */ + protected static T readTypedAttribute( + Attributes attributes, StoreHandle storeHandle, String key, TypeReference typeRef) + throws ZarrException { + if (attributes == null || !attributes.containsKey(key)) { + throw new ZarrException("No '" + key + "' key found in attributes at " + storeHandle); + } + return dev.zarr.zarrjava.v2.Node.makeObjectMapper().convertValue(attributes.get(key), typeRef); + } + + /** + * Builds {@link Attributes} containing {@code {key: }}, ready to + * pass to {@code Group.create()} or {@code Group.setAttributes()}. + */ + protected static Attributes buildAttributes(String key, Object value) { + Object serialized = dev.zarr.zarrjava.v2.Node.makeObjectMapper() + .convertValue(value, Object.class); + Attributes attrs = new Attributes(); + attrs.put(key, serialized); + return attrs; + } + + /** Serializes {@code value} via the v2 mapper to a plain Java object (Map/List/primitive). */ + protected static Object serialize(Object value) { + return dev.zarr.zarrjava.v2.Node.makeObjectMapper().convertValue(value, Object.class); + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/OmeV3Group.java b/src/main/java/dev/zarr/zarrjava/ome/OmeV3Group.java new file mode 100644 index 00000000..17cabf93 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/ome/OmeV3Group.java @@ -0,0 +1,48 @@ +package dev.zarr.zarrjava.ome; + +import com.fasterxml.jackson.core.type.TypeReference; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.store.StoreHandle; +import dev.zarr.zarrjava.v3.Group; +import dev.zarr.zarrjava.v3.GroupMetadata; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * Base class for all OME-Zarr nodes backed by a Zarr v3 group. + * + *

Provides {@code protected static} helpers for reading OME attributes and building + * {@link Attributes} for writing. The actual byte serialization is performed by + * {@link dev.zarr.zarrjava.v3.Node#makeObjectWriter()} inside {@code Group.create()} and + * {@code Group.setAttributes()}. + */ +public abstract class OmeV3Group extends Group { + + protected OmeV3Group(@Nonnull StoreHandle storeHandle, @Nonnull GroupMetadata groupMetadata) + throws IOException { + super(storeHandle, groupMetadata); + } + + /** Reads and converts the {@code "ome"} attribute value from the given group's attributes. */ + protected static T readOmeAttribute( + Attributes attributes, StoreHandle storeHandle, Class cls) throws ZarrException { + if (attributes == null || !attributes.containsKey("ome")) { + throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); + } + return dev.zarr.zarrjava.v3.Node.makeObjectMapper().convertValue(attributes.get("ome"), cls); + } + + /** + * Builds {@link Attributes} containing {@code {"ome": }}, ready to + * pass to {@code Group.create()} or {@code Group.setAttributes()}. + */ + protected static Attributes omeAttributes(Object omeMetadata) { + Object serialized = dev.zarr.zarrjava.v3.Node.makeObjectMapper() + .convertValue(omeMetadata, Object.class); + Attributes attrs = new Attributes(); + attrs.put("ome", serialized); + return attrs; + } +} diff --git a/src/main/java/dev/zarr/zarrjava/ome/UnifiedMultiscaleNode.java b/src/main/java/dev/zarr/zarrjava/ome/UnifiedMultiscaleNode.java deleted file mode 100644 index 2eb5f61c..00000000 --- a/src/main/java/dev/zarr/zarrjava/ome/UnifiedMultiscaleNode.java +++ /dev/null @@ -1,21 +0,0 @@ -package dev.zarr.zarrjava.ome; - -import dev.zarr.zarrjava.ome.metadata.Axis; - -import javax.annotation.Nullable; -import java.util.List; - -/** A multiscale image node, using v1.0 RFC-8 terminology ("nodes" not "datasets"). */ -public final class UnifiedMultiscaleNode { - - @Nullable - public final String name; - public final List axes; - public final List nodes; - - public UnifiedMultiscaleNode(@Nullable String name, List axes, List nodes) { - this.name = name; - this.axes = axes; - this.nodes = nodes; - } -} diff --git a/src/main/java/dev/zarr/zarrjava/ome/UnifiedSinglescaleNode.java b/src/main/java/dev/zarr/zarrjava/ome/UnifiedSinglescaleNode.java deleted file mode 100644 index 2450a03c..00000000 --- a/src/main/java/dev/zarr/zarrjava/ome/UnifiedSinglescaleNode.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.zarr.zarrjava.ome; - -import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; - -import java.util.List; - -/** A single scale level within a multiscale image, using v1.0 RFC-8 terminology. */ -public final class UnifiedSinglescaleNode { - - public final String path; - public final List coordinateTransformations; - - public UnifiedSinglescaleNode(String path, List coordinateTransformations) { - this.path = path; - this.coordinateTransformations = coordinateTransformations; - } -} diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java index dfdf46eb..9ba70dfc 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/MultiscaleImage.java @@ -1,9 +1,9 @@ package dev.zarr.zarrjava.ome.v0_4; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.OmeV2Group; import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; import dev.zarr.zarrjava.ome.metadata.Dataset; @@ -20,14 +20,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; - -import static dev.zarr.zarrjava.v2.Node.makeObjectMapper; /** * OME-Zarr v0.4 multiscale image backed by a Zarr v2 group. */ -public final class MultiscaleImage extends Group implements MultiscalesMetadataImage { +public final class MultiscaleImage extends OmeV2Group implements MultiscalesMetadataImage { private List multiscales; @Nullable @@ -53,19 +50,12 @@ private MultiscaleImage( */ public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("multiscales")) { - throw new ZarrException("No 'multiscales' key found in attributes at " + storeHandle); - } - List multiscales = mapper.convertValue( - attributes.get("multiscales"), - new TypeReference>() {} - ); - OmeroMetadata omeroMetadata = null; - if (attributes.containsKey("omero")) { - omeroMetadata = mapper.convertValue(attributes.get("omero"), OmeroMetadata.class); - } + List multiscales = readTypedAttribute( + attributes, storeHandle, "multiscales", new TypeReference>() {}); + OmeroMetadata omeroMetadata = attributes.containsKey("omero") + ? readAttribute(attributes, storeHandle, "omero", OmeroMetadata.class) + : null; Integer bioformats2rawLayout = null; if (attributes.containsKey("bioformats2raw.layout")) { Object raw = attributes.get("bioformats2raw.layout"); @@ -83,13 +73,8 @@ public static MultiscaleImage create( @Nonnull StoreHandle storeHandle, @Nonnull MultiscalesEntry multiscalesEntry ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); List multiscales = Collections.singletonList(multiscalesEntry); - @SuppressWarnings("unchecked") - List multiscalesList = mapper.convertValue(multiscales, List.class); - Attributes attributes = new Attributes(); - attributes.put("multiscales", multiscalesList); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, buildAttributes("multiscales", multiscales)); return new MultiscaleImage(storeHandle, group.metadata, multiscales, null, null); } @@ -150,15 +135,9 @@ public void createScaleLevel( } private void persistAttributes() throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); - @SuppressWarnings("unchecked") - List multiscalesList = mapper.convertValue(multiscales, List.class); - Attributes newAttributes = new Attributes(); - newAttributes.put("multiscales", multiscalesList); + Attributes newAttributes = buildAttributes("multiscales", multiscales); if (omeroMetadata != null) { - @SuppressWarnings("unchecked") - Map omeroMap = mapper.convertValue(omeroMetadata, Map.class); - newAttributes.put("omero", omeroMap); + newAttributes.put("omero", serialize(omeroMetadata)); } if (bioformats2rawLayout != null) { newAttributes.put("bioformats2raw.layout", bioformats2rawLayout); diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java index 51ad777b..69bcf80b 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Plate.java @@ -1,8 +1,7 @@ package dev.zarr.zarrjava.ome.v0_4; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; -import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.OmeV2Group; import dev.zarr.zarrjava.ome.metadata.PlateMetadata; import dev.zarr.zarrjava.store.StoreHandle; import dev.zarr.zarrjava.v2.Group; @@ -10,14 +9,11 @@ import javax.annotation.Nonnull; import java.io.IOException; -import java.util.Map; - -import static dev.zarr.zarrjava.v2.Node.makeObjectMapper; /** * OME-Zarr v0.4 HCS plate backed by a Zarr v2 group. */ -public final class Plate extends Group implements dev.zarr.zarrjava.ome.Plate { +public final class Plate extends OmeV2Group implements dev.zarr.zarrjava.ome.Plate { private PlateMetadata plateMetadata; @@ -35,12 +31,8 @@ private Plate( */ public static Plate openPlate(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("plate")) { - throw new ZarrException("No 'plate' key found in attributes at " + storeHandle); - } - PlateMetadata plateMetadata = mapper.convertValue(attributes.get("plate"), PlateMetadata.class); + PlateMetadata plateMetadata = readAttribute( + group.metadata.attributes, storeHandle, "plate", PlateMetadata.class); return new Plate(storeHandle, group.metadata, plateMetadata); } @@ -51,12 +43,7 @@ public static Plate createPlate( @Nonnull StoreHandle storeHandle, @Nonnull PlateMetadata plateMetadata ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); - @SuppressWarnings("unchecked") - Map plateMap = mapper.convertValue(plateMetadata, Map.class); - Attributes attributes = new Attributes(); - attributes.put("plate", plateMap); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, buildAttributes("plate", plateMetadata)); return new Plate(storeHandle, group.metadata, plateMetadata); } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java index 726ef1fa..309b7650 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_4/Well.java @@ -1,9 +1,8 @@ package dev.zarr.zarrjava.ome.v0_4; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; -import dev.zarr.zarrjava.core.Attributes; import dev.zarr.zarrjava.ome.MultiscaleImage; +import dev.zarr.zarrjava.ome.OmeV2Group; import dev.zarr.zarrjava.ome.metadata.WellMetadata; import dev.zarr.zarrjava.store.StoreHandle; import dev.zarr.zarrjava.v2.Group; @@ -11,14 +10,11 @@ import javax.annotation.Nonnull; import java.io.IOException; -import java.util.Map; - -import static dev.zarr.zarrjava.v2.Node.makeObjectMapper; /** * OME-Zarr v0.4 HCS well backed by a Zarr v2 group. */ -public final class Well extends Group implements dev.zarr.zarrjava.ome.Well { +public final class Well extends OmeV2Group implements dev.zarr.zarrjava.ome.Well { private WellMetadata wellMetadata; @@ -36,12 +32,8 @@ private Well( */ public static Well openWell(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("well")) { - throw new ZarrException("No 'well' key found in attributes at " + storeHandle); - } - WellMetadata wellMetadata = mapper.convertValue(attributes.get("well"), WellMetadata.class); + WellMetadata wellMetadata = readAttribute( + group.metadata.attributes, storeHandle, "well", WellMetadata.class); return new Well(storeHandle, group.metadata, wellMetadata); } @@ -52,12 +44,7 @@ public static Well createWell( @Nonnull StoreHandle storeHandle, @Nonnull WellMetadata wellMetadata ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); - @SuppressWarnings("unchecked") - Map wellMap = mapper.convertValue(wellMetadata, Map.class); - Attributes attributes = new Attributes(); - attributes.put("well", wellMap); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, buildAttributes("well", wellMetadata)); return new Well(storeHandle, group.metadata, wellMetadata); } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java index 42f71575..a6aa82d9 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/MultiscaleImage.java @@ -1,8 +1,7 @@ package dev.zarr.zarrjava.ome.v0_5; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; -import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.OmeV3Group; import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; import dev.zarr.zarrjava.ome.metadata.Dataset; @@ -17,14 +16,11 @@ import java.io.IOException; import java.util.Collections; import java.util.List; -import java.util.Map; - -import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; /** * OME-Zarr v0.5 multiscale image backed by a Zarr v3 group. */ -public final class MultiscaleImage extends Group implements MultiscalesMetadataImage { +public final class MultiscaleImage extends OmeV3Group implements MultiscalesMetadataImage { private OmeMetadata omeMetadata; @@ -42,12 +38,7 @@ private MultiscaleImage( */ public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("ome")) { - throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); - } - OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + OmeMetadata omeMetadata = readOmeAttribute(group.metadata.attributes, storeHandle, OmeMetadata.class); if (!omeMetadata.version.startsWith("0.5")) { throw new ZarrException( "Expected OME-Zarr version '0.5', got '" + omeMetadata.version + "' at " + storeHandle); @@ -62,13 +53,8 @@ public static MultiscaleImage create( @Nonnull StoreHandle storeHandle, @Nonnull MultiscalesEntry multiscalesEntry ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); OmeMetadata omeMetadata = new OmeMetadata("0.5", Collections.singletonList(multiscalesEntry)); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes attributes = new Attributes(); - attributes.put("ome", omeMap); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); } @@ -119,12 +105,6 @@ public void createScaleLevel( List updatedList = new java.util.ArrayList<>(omeMetadata.multiscales); updatedList.set(0, updated); omeMetadata = new OmeMetadata(omeMetadata.version, updatedList); - - ObjectMapper mapper = makeObjectMapper(); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes newAttributes = new Attributes(); - newAttributes.put("ome", omeMap); - setAttributes(newAttributes); + setAttributes(omeAttributes(omeMetadata)); } } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java index 660171f5..c7b803e9 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Plate.java @@ -1,8 +1,7 @@ package dev.zarr.zarrjava.ome.v0_5; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; -import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.OmeV3Group; import dev.zarr.zarrjava.ome.metadata.OmeMetadata; import dev.zarr.zarrjava.ome.metadata.PlateMetadata; import dev.zarr.zarrjava.store.StoreHandle; @@ -11,14 +10,11 @@ import javax.annotation.Nonnull; import java.io.IOException; -import java.util.Map; - -import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; /** * OME-Zarr v0.5 HCS plate backed by a Zarr v3 group. */ -public final class Plate extends Group implements dev.zarr.zarrjava.ome.Plate { +public final class Plate extends OmeV3Group implements dev.zarr.zarrjava.ome.Plate { private OmeMetadata omeMetadata; @@ -36,12 +32,8 @@ private Plate( */ public static Plate openPlate(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("ome")) { - throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); - } - OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); if (omeMetadata.plate == null) { throw new ZarrException("No 'plate' found in ome metadata at " + storeHandle); } @@ -55,13 +47,8 @@ public static Plate createPlate( @Nonnull StoreHandle storeHandle, @Nonnull PlateMetadata plateMetadata ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); OmeMetadata omeMetadata = new OmeMetadata("0.5", null, null, null, plateMetadata, null); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes attributes = new Attributes(); - attributes.put("ome", omeMap); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); return new Plate(storeHandle, group.metadata, omeMetadata); } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java index 171eb63e..349bf4aa 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_5/Well.java @@ -1,9 +1,8 @@ package dev.zarr.zarrjava.ome.v0_5; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; -import dev.zarr.zarrjava.core.Attributes; import dev.zarr.zarrjava.ome.MultiscaleImage; +import dev.zarr.zarrjava.ome.OmeV3Group; import dev.zarr.zarrjava.ome.metadata.OmeMetadata; import dev.zarr.zarrjava.ome.metadata.WellMetadata; import dev.zarr.zarrjava.store.StoreHandle; @@ -12,14 +11,11 @@ import javax.annotation.Nonnull; import java.io.IOException; -import java.util.Map; - -import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; /** * OME-Zarr v0.5 HCS well backed by a Zarr v3 group. */ -public final class Well extends Group implements dev.zarr.zarrjava.ome.Well { +public final class Well extends OmeV3Group implements dev.zarr.zarrjava.ome.Well { private OmeMetadata omeMetadata; @@ -37,12 +33,8 @@ private Well( */ public static Well openWell(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("ome")) { - throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); - } - OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); if (omeMetadata.well == null) { throw new ZarrException("No 'well' found in ome metadata at " + storeHandle); } @@ -56,13 +48,8 @@ public static Well createWell( @Nonnull StoreHandle storeHandle, @Nonnull WellMetadata wellMetadata ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); OmeMetadata omeMetadata = new OmeMetadata("0.5", null, null, null, null, wellMetadata); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes attributes = new Attributes(); - attributes.put("ome", omeMap); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); return new Well(storeHandle, group.metadata, omeMetadata); } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java index cebb216b..5c3aeb7a 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v0_6/MultiscaleImage.java @@ -1,15 +1,11 @@ package dev.zarr.zarrjava.ome.v0_6; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; -import dev.zarr.zarrjava.core.Attributes; import dev.zarr.zarrjava.ome.MultiscalesMetadataImage; -import dev.zarr.zarrjava.ome.UnifiedMultiscaleNode; -import dev.zarr.zarrjava.ome.UnifiedSinglescaleNode; +import dev.zarr.zarrjava.ome.OmeV3Group; import dev.zarr.zarrjava.ome.metadata.Axis; import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; -import dev.zarr.zarrjava.ome.v0_6.metadata.Dataset; import dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry; import dev.zarr.zarrjava.ome.v0_6.metadata.OmeMetadata; import dev.zarr.zarrjava.store.StoreHandle; @@ -22,14 +18,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; - -import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; /** * OME-Zarr v0.6 (RFC-5) multiscale image backed by a Zarr v3 group. */ -public final class MultiscaleImage extends Group implements MultiscalesMetadataImage { +public final class MultiscaleImage extends OmeV3Group implements MultiscalesMetadataImage { private OmeMetadata omeMetadata; @@ -47,12 +40,8 @@ private MultiscaleImage( */ public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("ome")) { - throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); - } - OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); if (!omeMetadata.version.startsWith("0.6")) { throw new ZarrException( "Expected OME-Zarr version '0.6', got '" + omeMetadata.version + "' at " + storeHandle); @@ -67,13 +56,8 @@ public static MultiscaleImage create( @Nonnull StoreHandle storeHandle, @Nonnull MultiscalesEntry multiscalesEntry ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); OmeMetadata omeMetadata = new OmeMetadata("0.6", Collections.singletonList(multiscalesEntry)); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes attributes = new Attributes(); - attributes.put("ome", omeMap); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); } @@ -117,35 +101,30 @@ public void createScaleLevel( } MultiscalesEntry current = omeMetadata.multiscales.get(0); - MultiscalesEntry updated = current.withDataset(new Dataset(path, v06Transforms)); + MultiscalesEntry updated = current.withDataset(new dev.zarr.zarrjava.ome.v0_6.metadata.Dataset(path, v06Transforms)); List updatedList = new ArrayList<>(omeMetadata.multiscales); updatedList.set(0, updated); omeMetadata = new OmeMetadata(omeMetadata.version, updatedList); - - ObjectMapper mapper = makeObjectMapper(); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes newAttributes = new Attributes(); - newAttributes.put("ome", omeMap); - setAttributes(newAttributes); + setAttributes(omeAttributes(omeMetadata)); } @Override - public UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException { + public dev.zarr.zarrjava.ome.metadata.MultiscalesEntry getMultiscaleNode(int i) throws ZarrException { MultiscalesEntry entry = getMultiscalesEntry(i); - List nodes = new ArrayList<>(); - for (Dataset ds : entry.datasets) { + List mappedDatasets = new ArrayList<>(); + for (dev.zarr.zarrjava.ome.v0_6.metadata.Dataset ds : entry.datasets) { List mapped = new ArrayList<>(); for (dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation ct : ds.coordinateTransformations) { mapped.add(new CoordinateTransformation(ct.type, ct.scale, ct.translation, ct.path)); } - nodes.add(new UnifiedSinglescaleNode(ds.path, mapped)); + mappedDatasets.add(new dev.zarr.zarrjava.ome.metadata.Dataset(ds.path, mapped)); } - // Axes: prefer entry.axes; fall back to first coordinateSystem's axes List axes = entry.axes; if ((axes == null || axes.isEmpty()) && entry.coordinateSystems != null && !entry.coordinateSystems.isEmpty()) { axes = entry.coordinateSystems.get(0).axes; } - return new UnifiedMultiscaleNode(entry.name, axes != null ? axes : Collections.emptyList(), nodes); + return new dev.zarr.zarrjava.ome.metadata.MultiscalesEntry( + axes != null ? axes : Collections.emptyList(), + mappedDatasets, null, entry.name, null, null, null); } } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java index 64034606..a5723ef6 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/Collection.java @@ -1,8 +1,7 @@ package dev.zarr.zarrjava.ome.v1_0; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; -import dev.zarr.zarrjava.core.Attributes; +import dev.zarr.zarrjava.ome.OmeV3Group; import dev.zarr.zarrjava.ome.v1_0.metadata.CollectionMetadata; import dev.zarr.zarrjava.ome.v1_0.metadata.OmeMetadata; import dev.zarr.zarrjava.store.StoreHandle; @@ -11,14 +10,11 @@ import javax.annotation.Nonnull; import java.io.IOException; -import java.util.Map; - -import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; /** * OME-Zarr v1.0 (RFC-8) collection backed by a Zarr v3 group. */ -public final class Collection extends Group { +public final class Collection extends OmeV3Group { private OmeMetadata omeMetadata; @@ -36,12 +32,8 @@ private Collection( */ public static Collection openCollection(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("ome")) { - throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); - } - OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); if (omeMetadata.collection == null) { throw new ZarrException("v1.0 store at " + storeHandle + " has no 'collection' — is it a MultiscaleImage?"); } @@ -55,13 +47,8 @@ public static Collection createCollection( @Nonnull StoreHandle storeHandle, @Nonnull CollectionMetadata collectionMetadata ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); OmeMetadata omeMetadata = new OmeMetadata("1.0-dev", collectionMetadata); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes attributes = new Attributes(); - attributes.put("ome", omeMap); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); return new Collection(storeHandle, group.metadata, omeMetadata); } @@ -83,12 +70,8 @@ public StoreHandle getStoreHandle() { public Object openNode(String path) throws IOException, ZarrException { StoreHandle child = storeHandle.resolve(path); Group group = Group.open(child); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("ome")) { - throw new ZarrException("No 'ome' key found in child node at " + child); - } - OmeMetadata childOme = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + OmeMetadata childOme = readOmeAttribute( + group.metadata.attributes, child, OmeMetadata.class); if (childOme.multiscale != null) { return MultiscaleImage.openMultiscaleImage(child); } diff --git a/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java index 5ea74cfd..b191398c 100644 --- a/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/ome/v1_0/MultiscaleImage.java @@ -1,12 +1,10 @@ package dev.zarr.zarrjava.ome.v1_0; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.zarr.zarrjava.ZarrException; -import dev.zarr.zarrjava.core.Attributes; -import dev.zarr.zarrjava.ome.UnifiedMultiscaleNode; -import dev.zarr.zarrjava.ome.UnifiedSinglescaleNode; +import dev.zarr.zarrjava.ome.OmeV3Group; import dev.zarr.zarrjava.ome.metadata.Axis; import dev.zarr.zarrjava.ome.metadata.CoordinateTransformation; +import dev.zarr.zarrjava.ome.metadata.Dataset; import dev.zarr.zarrjava.ome.v1_0.metadata.Level; import dev.zarr.zarrjava.ome.v1_0.metadata.MultiscaleMetadata; import dev.zarr.zarrjava.ome.v1_0.metadata.OmeMetadata; @@ -20,14 +18,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; - -import static dev.zarr.zarrjava.v3.Node.makeObjectMapper; /** * OME-Zarr v1.0 (RFC-8) multiscale image backed by a Zarr v3 group. */ -public final class MultiscaleImage extends Group implements dev.zarr.zarrjava.ome.MultiscaleImage { +public final class MultiscaleImage extends OmeV3Group implements dev.zarr.zarrjava.ome.MultiscaleImage { private OmeMetadata omeMetadata; @@ -45,12 +40,8 @@ private MultiscaleImage( */ public static MultiscaleImage openMultiscaleImage(@Nonnull StoreHandle storeHandle) throws IOException, ZarrException { Group group = Group.open(storeHandle); - ObjectMapper mapper = makeObjectMapper(); - Attributes attributes = group.metadata.attributes; - if (attributes == null || !attributes.containsKey("ome")) { - throw new ZarrException("No 'ome' key found in attributes at " + storeHandle); - } - OmeMetadata omeMetadata = mapper.convertValue(attributes.get("ome"), OmeMetadata.class); + OmeMetadata omeMetadata = readOmeAttribute( + group.metadata.attributes, storeHandle, OmeMetadata.class); if (omeMetadata.multiscale == null) { throw new ZarrException("v1.0 store at " + storeHandle + " has no 'multiscale' — is it a Collection?"); } @@ -64,13 +55,8 @@ public static MultiscaleImage create( @Nonnull StoreHandle storeHandle, @Nonnull MultiscaleMetadata multiscaleMetadata ) throws IOException, ZarrException { - ObjectMapper mapper = makeObjectMapper(); OmeMetadata omeMetadata = new OmeMetadata("1.0-dev", multiscaleMetadata); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes attributes = new Attributes(); - attributes.put("ome", omeMap); - Group group = Group.create(storeHandle, attributes); + Group group = Group.create(storeHandle, omeAttributes(omeMetadata)); return new MultiscaleImage(storeHandle, group.metadata, omeMetadata); } @@ -87,24 +73,26 @@ public StoreHandle getStoreHandle() { } @Override - public UnifiedMultiscaleNode getMultiscaleNode(int i) throws ZarrException { + public dev.zarr.zarrjava.ome.metadata.MultiscalesEntry getMultiscaleNode(int i) throws ZarrException { if (i != 0) { throw new ZarrException("v1.0 has a single multiscale per group; index must be 0, got " + i); } MultiscaleMetadata m = omeMetadata.multiscale; - List nodes = new ArrayList<>(); + List datasets = new ArrayList<>(); for (Level level : m.levels) { List mapped = new ArrayList<>(); for (dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation ct : level.coordinateTransformations) { mapped.add(new CoordinateTransformation(ct.type, ct.scale, ct.translation, ct.path)); } - nodes.add(new UnifiedSinglescaleNode(level.path, mapped)); + datasets.add(new Dataset(level.path, mapped)); } List axes = m.axes; if ((axes == null || axes.isEmpty()) && m.coordinateSystems != null && !m.coordinateSystems.isEmpty()) { axes = m.coordinateSystems.get(0).axes; } - return new UnifiedMultiscaleNode(m.name, axes != null ? axes : Collections.emptyList(), nodes); + return new dev.zarr.zarrjava.ome.metadata.MultiscalesEntry( + axes != null ? axes : Collections.emptyList(), + datasets, null, m.name, null, null, null); } @Override @@ -128,12 +116,6 @@ public void createLevel( Array.create(storeHandle.resolve(path), arrayMetadata); MultiscaleMetadata updated = omeMetadata.multiscale.withLevel(new Level(path, coordinateTransformations)); omeMetadata = new OmeMetadata(omeMetadata.version, updated); - - ObjectMapper mapper = makeObjectMapper(); - @SuppressWarnings("unchecked") - Map omeMap = mapper.convertValue(omeMetadata, Map.class); - Attributes newAttributes = new Attributes(); - newAttributes.put("ome", omeMap); - setAttributes(newAttributes); + setAttributes(omeAttributes(omeMetadata)); } } diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java index abd016d6..e06ef1ff 100644 --- a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrBaseTest.java @@ -1,6 +1,7 @@ package dev.zarr.zarrjava.ome; import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; import dev.zarr.zarrjava.store.FilesystemStore; import dev.zarr.zarrjava.store.StoreHandle; import org.junit.jupiter.api.Test; @@ -50,19 +51,19 @@ void open_returnsCorrectConcreteType() throws Exception { @Test void getMultiscaleNode_hasExpectedAxes() throws Exception { MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); - UnifiedMultiscaleNode node = image.getMultiscaleNode(0); - assertNotNull(node); - assertEquals(expectedAxisNames().size(), node.axes.size()); + MultiscalesEntry entry = image.getMultiscaleNode(0); + assertNotNull(entry); + assertEquals(expectedAxisNames().size(), entry.axes.size()); for (int i = 0; i < expectedAxisNames().size(); i++) { - assertEquals(expectedAxisNames().get(i), node.axes.get(i).name); + assertEquals(expectedAxisNames().get(i), entry.axes.get(i).name); } } @Test void getMultiscaleNode_hasExpectedLevelCount() throws Exception { MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); - UnifiedMultiscaleNode node = image.getMultiscaleNode(0); - assertEquals(expectedScaleLevelCount(), node.nodes.size()); + MultiscalesEntry entry = image.getMultiscaleNode(0); + assertEquals(expectedScaleLevelCount(), entry.datasets.size()); } @Test diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java index b4d5c476..46638070 100644 --- a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV05Test.java @@ -156,7 +156,7 @@ void write_createAndReopen() throws Exception { assertInstanceOf(dev.zarr.zarrjava.ome.v0_5.MultiscaleImage.class, reopened); assertEquals(Arrays.asList("z", "y"), reopened.getAxisNames()); assertEquals(1, reopened.getScaleLevelCount()); - assertEquals("0", reopened.getMultiscaleNode(0).nodes.get(0).path); + assertEquals("0", reopened.getMultiscaleNode(0).datasets.get(0).path); } @Test diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java index 41867ec9..a02952cf 100644 --- a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV06Test.java @@ -1,7 +1,7 @@ package dev.zarr.zarrjava.ome; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; -import dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry; import dev.zarr.zarrjava.store.StoreHandle; import org.junit.jupiter.api.Test; @@ -44,8 +44,9 @@ List expectedAxisNames() { @SuppressWarnings("unchecked") void coordinateSystems_presentInEntry() throws Exception { MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); - MultiscalesMetadataImage mmi = (MultiscalesMetadataImage) image; - MultiscalesEntry entry = (MultiscalesEntry) mmi.getMultiscalesEntry(0); + dev.zarr.zarrjava.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry entry = v06Image.getMultiscalesEntry(0); assertNotNull(entry.coordinateSystems); assertEquals(1, entry.coordinateSystems.size()); @@ -62,8 +63,9 @@ void coordinateSystems_presentInEntry() throws Exception { @SuppressWarnings("unchecked") void datasets_pathsAndTransformations() throws Exception { MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); - MultiscalesMetadataImage mmi = (MultiscalesMetadataImage) image; - MultiscalesEntry entry = (MultiscalesEntry) mmi.getMultiscalesEntry(0); + dev.zarr.zarrjava.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry entry = v06Image.getMultiscalesEntry(0); assertNotNull(entry.datasets); assertEquals(3, entry.datasets.size()); @@ -85,12 +87,12 @@ void datasets_pathsAndTransformations() throws Exception { @Test void unifiedInterface_nodesAndPaths() throws Exception { MultiscaleImage image = MultiscaleImage.open(imageStoreHandle()); - UnifiedMultiscaleNode node = image.getMultiscaleNode(0); + MultiscalesEntry entry = image.getMultiscaleNode(0); - assertEquals("multiscales", node.name); - assertEquals(3, node.nodes.size()); - assertEquals("s0", node.nodes.get(0).path); - assertEquals("scale", node.nodes.get(0).coordinateTransformations.get(0).type); + assertEquals("multiscales", entry.name); + assertEquals(3, entry.datasets.size()); + assertEquals("s0", entry.datasets.get(0).path); + assertEquals("scale", entry.datasets.get(0).coordinateTransformations.get(0).type); } // ── 3D example ─────────────────────────────────────────────────────────── @@ -101,8 +103,9 @@ void read3d_axesFromCoordinateSystems() throws Exception { MultiscaleImage image = MultiscaleImage.open(storeHandle(V06_3D)); assertInstanceOf(dev.zarr.zarrjava.ome.v0_6.MultiscaleImage.class, image); - MultiscalesMetadataImage mmi = (MultiscalesMetadataImage) image; - MultiscalesEntry entry = (MultiscalesEntry) mmi.getMultiscalesEntry(0); + dev.zarr.zarrjava.ome.v0_6.MultiscaleImage v06Image = + (dev.zarr.zarrjava.ome.v0_6.MultiscaleImage) image; + dev.zarr.zarrjava.ome.v0_6.metadata.MultiscalesEntry entry = v06Image.getMultiscalesEntry(0); assertEquals(3, entry.datasets.size()); assertNotNull(entry.coordinateSystems); diff --git a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java index 767b4885..4296dc8e 100644 --- a/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java +++ b/src/test/java/dev/zarr/zarrjava/ome/OmeZarrV10Test.java @@ -1,6 +1,7 @@ package dev.zarr.zarrjava.ome; import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.ome.metadata.MultiscalesEntry; import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateSystem; import dev.zarr.zarrjava.ome.v0_6.metadata.CoordinateTransformation; import dev.zarr.zarrjava.ome.v1_0.Collection; @@ -38,13 +39,13 @@ void readMultiscaleImage_concreteType() throws Exception { @Test void readMultiscaleImage_unifiedInterface() throws Exception { MultiscaleImage image = MultiscaleImage.open(storeHandle(TESTDATA.resolve("ome/v1.0/image"))); - UnifiedMultiscaleNode node = image.getMultiscaleNode(0); + MultiscalesEntry entry = image.getMultiscaleNode(0); - assertEquals("test_image", node.name); - assertNotNull(node.axes); - assertFalse(node.axes.isEmpty()); - assertFalse(node.nodes.isEmpty()); - assertEquals("s0", node.nodes.get(0).path); + assertEquals("test_image", entry.name); + assertNotNull(entry.axes); + assertFalse(entry.axes.isEmpty()); + assertFalse(entry.datasets.isEmpty()); + assertEquals("s0", entry.datasets.get(0).path); } @Test