Skip to content

Commit d51c6f8

Browse files
authored
Merge pull request #4379 from melissalinkert/gh-4376
DICOM: fix width of values with DS VR
2 parents 4f37585 + 18fd8be commit d51c6f8

3 files changed

Lines changed: 238 additions & 6 deletions

File tree

components/formats-bsd/src/loci/formats/out/DicomWriter.java

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.lang.reflect.Array;
4141
import java.rmi.dgc.VMID;
4242
import java.rmi.server.UID;
43+
import java.text.DecimalFormat;
4344
import java.util.ArrayList;
4445
import java.util.Arrays;
4546
import java.util.Comparator;
@@ -1063,7 +1064,8 @@ public void setId(String id) throws FormatException, IOException {
10631064
DicomTag sliceSpace = new DicomTag(SLICE_SPACING, DS);
10641065
Length physicalZ = fixUnits(r.getPixelsPhysicalSizeZ(pyramid));
10651066
if (physicalZ != null) {
1066-
sliceThickness.value = padString(String.valueOf(physicalZ.value(UNITS.MM)));
1067+
double pz = physicalZ.value(UNITS.MM).doubleValue();
1068+
sliceThickness.value = padString(formatFixedWidth(pz, 16));
10671069
}
10681070
else {
10691071
// a value of 0 is not allowed, but we don't know the actual thickness or slice spacing
@@ -1076,9 +1078,11 @@ public void setId(String id) throws FormatException, IOException {
10761078
DicomTag pixelSpacing = new DicomTag(PIXEL_SPACING, DS);
10771079
Length physicalX = fixUnits(r.getPixelsPhysicalSizeX(pyramid));
10781080
Length physicalY = fixUnits(r.getPixelsPhysicalSizeY(pyramid));
1079-
String px = physicalX == null ? "1" : String.valueOf(physicalX.value(UNITS.MM));
1080-
String py = physicalY == null ? "1" : String.valueOf(physicalY.value(UNITS.MM));
1081-
pixelSpacing.value = padString(px + "\\" + py);
1081+
double px = physicalX == null ? 1.0 : physicalX.value(UNITS.MM).doubleValue();
1082+
double py = physicalY == null ? 1.0 : physicalY.value(UNITS.MM).doubleValue();
1083+
1084+
pixelSpacing.value =
1085+
padString(formatFixedWidth(px, 15) + "\\" + formatFixedWidth(py, 15));
10821086
pixelMeasuresSequence.children.add(pixelSpacing);
10831087

10841088
pixelMeasuresSequence.children.add(makeItemDelimitation());
@@ -1154,11 +1158,13 @@ public void setId(String id) throws FormatException, IOException {
11541158
plane.children.add(makeItem());
11551159

11561160
DicomTag offsetX = new DicomTag(X_OFFSET_IN_SLIDE, DS);
1157-
offsetX.value = padString(physicalX == null ? "0" : padString(String.valueOf(physicalX.value(UNITS.MM).floatValue() * width)));
1161+
double ox = physicalX.value(UNITS.MM).floatValue() * width;
1162+
offsetX.value = padString(physicalX == null ? "0" : padString(formatFixedWidth(ox, 16)));
11581163
plane.children.add(offsetX);
11591164

11601165
DicomTag offsetY = new DicomTag(Y_OFFSET_IN_SLIDE, DS);
1161-
offsetY.value = padString(physicalY == null ? "0" : padString(String.valueOf(physicalY.value(UNITS.MM).floatValue() * height)));
1166+
double oy = physicalY.value(UNITS.MM).floatValue() * height;
1167+
offsetY.value = padString(physicalY == null ? "0" : padString(formatFixedWidth(oy, 16)));
11621168
plane.children.add(offsetY);
11631169

11641170
DicomTag positionZ = new DicomTag(Z_OFFSET_IN_SLIDE, DS);
@@ -2214,6 +2220,77 @@ private void checkPixelCount(boolean warn) throws FormatException {
22142220
}
22152221
}
22162222

2223+
private static String getScientificNotationPattern(int intDigits, int signBytes, int width) {
2224+
int exponentDigits = String.valueOf(Math.abs(intDigits)).length();
2225+
int mantissaDigits = width - exponentDigits - signBytes - 2;
2226+
StringBuffer pattern = new StringBuffer(".");
2227+
for (int i=0; i<mantissaDigits; i++) {
2228+
pattern.append("#");
2229+
}
2230+
pattern.append("E");
2231+
for (int i=0; i<exponentDigits; i++) {
2232+
pattern.append("0");
2233+
}
2234+
return pattern.toString();
2235+
}
2236+
2237+
/**
2238+
* Format the given double as a string with no more than
2239+
* <code>width</code> characters.
2240+
*/
2241+
public static String formatFixedWidth(double v, int width) {
2242+
// use a smaller value than usual to test double equivalency
2243+
double epsilon = Double.MIN_VALUE;
2244+
if (Double.isNaN(v)) {
2245+
return "NaN";
2246+
}
2247+
else if (Double.isInfinite(v)) {
2248+
if (width >= 9) {
2249+
return v < 0 ? "-Infinity" : "+Infinity";
2250+
}
2251+
return "";
2252+
}
2253+
else if (Math.abs(v - 0) < epsilon) {
2254+
return "0";
2255+
}
2256+
// get a decimal formatter for the current default locale
2257+
DecimalFormat formatter = new DecimalFormat();
2258+
formatter.setGroupingUsed(false);
2259+
2260+
int integerDigitsNeeded = (int) Math.log10(Math.abs(v)) + 1;
2261+
int signBytes = v < 0 ? 1 : 0;
2262+
2263+
if (integerDigitsNeeded + signBytes > width) {
2264+
String pattern = getScientificNotationPattern(integerDigitsNeeded, signBytes, width);
2265+
LOGGER.debug("float formatting pattern = {}", pattern);
2266+
formatter.applyPattern(pattern);
2267+
}
2268+
else {
2269+
if (Math.round(v) == 0) {
2270+
integerDigitsNeeded--;
2271+
}
2272+
int fractionDigits = width - signBytes - integerDigitsNeeded;
2273+
if (fractionDigits == 0 || fractionDigits == 1) {
2274+
formatter.setMaximumFractionDigits(0);
2275+
}
2276+
else if (fractionDigits > 1 && integerDigitsNeeded >= -4) {
2277+
formatter.setMaximumIntegerDigits(Math.max(0, integerDigitsNeeded));
2278+
if (integerDigitsNeeded >= 0) {
2279+
formatter.setMaximumFractionDigits(fractionDigits - 1);
2280+
}
2281+
else {
2282+
formatter.setMaximumFractionDigits(width - signBytes - 1);
2283+
}
2284+
}
2285+
else {
2286+
String pattern = getScientificNotationPattern(integerDigitsNeeded, signBytes, width - 1);
2287+
LOGGER.debug("float formatting pattern = {}", pattern);
2288+
formatter.applyPattern(pattern);
2289+
}
2290+
}
2291+
return formatter.format(v);
2292+
}
2293+
22172294
protected Slf4JStopWatch stopWatch() {
22182295
return new Slf4JStopWatch(LOGGER, Slf4JStopWatch.DEBUG_LEVEL);
22192296
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* #%L
3+
* BSD implementations of Bio-Formats readers and writers
4+
* %%
5+
* Copyright (C) 2026 Open Microscopy Environment:
6+
* - Board of Regents of the University of Wisconsin-Madison
7+
* - Glencoe Software, Inc.
8+
* - University of Dundee
9+
* %%
10+
* Redistribution and use in source and binary forms, with or without
11+
* modification, are permitted provided that the following conditions are met:
12+
*
13+
* 1. Redistributions of source code must retain the above copyright notice,
14+
* this list of conditions and the following disclaimer.
15+
* 2. Redistributions in binary form must reproduce the above copyright notice,
16+
* this list of conditions and the following disclaimer in the documentation
17+
* and/or other materials provided with the distribution.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
23+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
* POSSIBILITY OF SUCH DAMAGE.
30+
* #L%
31+
*/
32+
33+
package loci.formats.utests.dicom;
34+
35+
import static org.testng.AssertJUnit.assertEquals;
36+
37+
import loci.formats.out.DicomWriter;
38+
39+
import org.testng.annotations.DataProvider;
40+
import org.testng.annotations.Test;
41+
42+
/**
43+
*/
44+
public class FloatFormatTest {
45+
46+
@DataProvider(name = "values")
47+
public Object[][] values() {
48+
double[] d = getDoubleValues();
49+
String[] s = getStringValues();
50+
Object[][] rtn = new Object[d.length][2];
51+
for (int i=0; i<rtn.length; i++) {
52+
rtn[i][0] = d[i];
53+
rtn[i][1] = s[i];
54+
}
55+
return rtn;
56+
}
57+
58+
private double[] getDoubleValues() {
59+
return new double[] {
60+
0,
61+
1.1,
62+
0.11,
63+
0.1133408781152648,
64+
-0.1133408781152648,
65+
0.01133408781152648,
66+
-0.01133408781152648,
67+
0.001133408781152648,
68+
-0.001133408781152648,
69+
0.0001133408781152648,
70+
-0.0001133408781152648,
71+
0.00001133408781152648,
72+
-0.00001133408781152648,
73+
0.000001133408781152648,
74+
-0.000001133408781152648,
75+
0.000000000001133408781152648,
76+
-0.000000000001133408781152648,
77+
113340878115264.8,
78+
-113340878115264.8,
79+
1133408781152648.0,
80+
-1133408781152648.0,
81+
.012624143592677,
82+
99999.999,
83+
-99999.999,
84+
99999.999999999999999,
85+
-99999.999999999999999,
86+
Double.NEGATIVE_INFINITY,
87+
Double.POSITIVE_INFINITY,
88+
Double.NaN,
89+
Double.MAX_VALUE,
90+
Double.MIN_VALUE,
91+
Float.MAX_VALUE,
92+
Float.MIN_VALUE,
93+
Long.MAX_VALUE,
94+
Long.MIN_VALUE,
95+
Integer.MAX_VALUE,
96+
Integer.MIN_VALUE,
97+
Short.MAX_VALUE,
98+
Short.MIN_VALUE,
99+
// see https://github.com/ome/bioformats/issues/4376
100+
0.45641259698767683
101+
};
102+
}
103+
104+
private String[] getStringValues() {
105+
return new String[] {
106+
"0",
107+
"1.1",
108+
".11",
109+
".113340878115265",
110+
"-.11334087811526",
111+
".011334087811526",
112+
"-.01133408781153",
113+
".001133408781153",
114+
"-.00113340878115",
115+
".000113340878115",
116+
"-.00011334087812",
117+
".000011334087812",
118+
"-.00001133408781",
119+
".113340878115E-5",
120+
"-.11334087812E-5",
121+
".11334087812E-11",
122+
"-.1133408781E-11",
123+
"113340878115265",
124+
"-113340878115265",
125+
"1133408781152648",
126+
"-.11334087812E16",
127+
".012624143592677",
128+
"99999.999",
129+
"-99999.999",
130+
"100000",
131+
"-100000",
132+
"-Infinity",
133+
"+Infinity",
134+
"NaN",
135+
".17976931349E309",
136+
".49E-323",
137+
".340282346639E39",
138+
".14012984643E-44",
139+
".922337203685E19",
140+
"-.92233720369E19",
141+
"2147483647",
142+
"-2147483648",
143+
"32767",
144+
"-32768",
145+
".456412596987677"
146+
};
147+
}
148+
149+
@Test(dataProvider = "values")
150+
public void testFormat(double v, String expected) {
151+
assertEquals(DicomWriter.formatFixedWidth(v, 16), expected);
152+
}
153+
154+
}

components/formats-bsd/test/loci/formats/utests/testng.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@
209209
<groups/>
210210
<classes>
211211
<class name="loci.formats.utests.dicom.ProvidedMetadataTest"/>
212+
<class name="loci.formats.utests.dicom.FloatFormatTest"/>
212213
</classes>
213214
</test>
214215
</suite>

0 commit comments

Comments
 (0)