diff --git a/components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java b/components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java
index cea3ee1c9bc..8f1c3816929 100644
--- a/components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java
+++ b/components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java
@@ -78,6 +78,7 @@
import loci.formats.meta.IPyramidStore;
import loci.formats.out.DicomWriter;
import loci.formats.ome.OMEPyramidStore;
+import loci.formats.out.IExtraMetadataWriter;
import loci.formats.out.TiffWriter;
import loci.formats.services.OMEXMLService;
import loci.formats.services.OMEXMLServiceImpl;
@@ -132,6 +133,8 @@ public final class ImageConverter {
private boolean precompressed = false;
private boolean tryPrecompressed = false;
+ private String extraMetadata = null;
+
private IFormatReader reader;
private MinMaxCalculator minMax;
private DimensionSwapper dimSwapper;
@@ -267,6 +270,9 @@ else if (args[i].equals("-fill")) {
// allow specifying 0-255
fillColor = (byte) Integer.parseInt(args[++i]);
}
+ else if (args[i].equals("-extra-metadata")) {
+ extraMetadata = args[++i];
+ }
else if (!args[i].equals(CommandLineTools.NO_UPGRADE_CHECK)) {
LOGGER.error("Found unknown command flag: {}; exiting.", args[i]);
return false;
@@ -671,6 +677,15 @@ else if (w instanceof DicomWriter) {
((DicomWriter) w).setBigTiff(bigtiff);
}
}
+ if (writer instanceof IExtraMetadataWriter) {
+ ((IExtraMetadataWriter) writer).setExtraMetadata(extraMetadata);
+ }
+ else if (writer instanceof ImageWriter) {
+ IFormatWriter w = ((ImageWriter) writer).getWriter(out);
+ if (w instanceof IExtraMetadataWriter) {
+ ((IExtraMetadataWriter) w).setExtraMetadata(extraMetadata);
+ }
+ }
String format = writer.getFormat();
LOGGER.info("[{}] -> {} [{}]",
diff --git a/components/formats-bsd/pom.xml b/components/formats-bsd/pom.xml
index 28842a8ae24..aee7ec9c5da 100644
--- a/components/formats-bsd/pom.xml
+++ b/components/formats-bsd/pom.xml
@@ -110,6 +110,13 @@
${jxrlib.version}
+
+ org.json
+ json
+ 20230227
+
+
+
xerces
diff --git a/components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java b/components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java
index acc3cf26009..e270a604597 100644
--- a/components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java
+++ b/components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java
@@ -787,6 +787,8 @@ public enum DicomAttribute {
private static final Map dict =
new HashMap();
+ private static final Map nameLookup =
+ new HashMap();
static {
try (InputStream stream = DicomAttribute.class.getResourceAsStream("dicom-dictionary.txt")) {
@@ -802,7 +804,9 @@ public enum DicomAttribute {
if (tokens.length > 2) {
tokens[2] = tokens[2].trim();
}
- dict.put((int) Long.parseLong(tokens[0], 16), tokens);
+ int key = (int) Long.parseLong(tokens[0], 16);
+ dict.put(key, tokens);
+ nameLookup.put(tokens[1].replaceAll("\\s", "").toLowerCase(), key);
}
}
catch (Exception e) {
@@ -872,6 +876,10 @@ public static String getDescription(int newTag) {
return null;
}
+ public static Integer getTag(String description) {
+ return nameLookup.get(description.toLowerCase());
+ }
+
/**
* Lookup the attribute for the given tag.
* May return null if the tag is not defined in our dictionary.
diff --git a/components/formats-bsd/src/loci/formats/dicom/DicomJSONProvider.java b/components/formats-bsd/src/loci/formats/dicom/DicomJSONProvider.java
new file mode 100644
index 00000000000..1dd6d4e962a
--- /dev/null
+++ b/components/formats-bsd/src/loci/formats/dicom/DicomJSONProvider.java
@@ -0,0 +1,211 @@
+/*
+ * #%L
+ * BSD implementations of Bio-Formats readers and writers
+ * %%
+ * Copyright (C) 2005 - 2023 Open Microscopy Environment:
+ * - Board of Regents of the University of Wisconsin-Madison
+ * - Glencoe Software, Inc.
+ * - University of Dundee
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+package loci.formats.dicom;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import loci.common.Constants;
+import loci.common.DataTools;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provide DICOM tags from a file containing JSON.
+ * Formal JSON schema yet to be determined, but the idea is to accept a hierarchy
+ * of tags of the form:
+ *
+ * {
+ * "BodyPartExamined": {
+ * "Value": "BRAIN",
+ * "VR": "CS",
+ * "Tag": "(0018,0015)"
+ * }
+ * "ContributingEquipmentSequence": {
+ * "VR": "SQ",
+ * "Tag": "(0018,a001)",
+ * "Sequence": {
+ * "Manufacturer": {
+ * "Value": "PixelMed",
+ * "VR": "LO",
+ * "Tag": "(0008,0070)"
+ * },
+ * "ContributionDateTime": {
+ * "Value": "20210710234601.105+0000",
+ * "VR": "DT",
+ * "Tag": "(0018,a002)"
+ * }
+ * }
+ * }
+ * }
+ *
+ * This is similar to JSON examples in https://github.com/QIICR/dcmqi/tree/master/doc/examples,
+ * but allows for the VR (type) and tag to be explicitly stated, in addition to
+ * the more human-readable description.
+ */
+public class DicomJSONProvider implements ITagProvider {
+
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(DicomJSONProvider.class);
+
+ private List tags = new ArrayList();
+
+ @Override
+ public void readTagSource(String location) throws IOException {
+ String rawJSON = DataTools.readFile(location);
+ try {
+ JSONObject root = new JSONObject(rawJSON);
+
+ for (String tagKey : root.keySet()) {
+ JSONObject tag = root.getJSONObject(tagKey);
+ String value = tag.has("Value") ? tag.getString("Value") : null;
+
+ Integer intTagCode = lookupTag(tagKey, tag);
+ DicomVR vrEnum = lookupVR(intTagCode, tag);
+
+ DicomTag dicomTag = new DicomTag(intTagCode, vrEnum);
+ dicomTag.value = value;
+ LOGGER.debug("Adding tag: {}, VR: {}, value: {}",
+ DicomAttribute.formatTag(intTagCode), vrEnum, value);
+ dicomTag.validateValue();
+
+ if (vrEnum == DicomVR.SQ && tag.has("Sequence")) {
+ readSequence(tag, dicomTag);
+ }
+
+ ResolutionStrategy rs = vrEnum == DicomVR.SQ ? ResolutionStrategy.APPEND : ResolutionStrategy.REPLACE;
+ if (tag.has("ResolutionStrategy")) {
+ String strategy = tag.getString("ResolutionStrategy");
+ rs = Enum.valueOf(ResolutionStrategy.class, strategy);
+ }
+ dicomTag.strategy = rs;
+
+ tags.add(dicomTag);
+ }
+ }
+ catch (JSONException e) {
+ throw new IOException("Could not parse JSON", e);
+ }
+ }
+
+ @Override
+ public List getTags() {
+ return tags;
+ }
+
+ private void readSequence(JSONObject rootTag, DicomTag parent) {
+ JSONObject sequence = rootTag.getJSONObject("Sequence");
+ for (String key : sequence.keySet()) {
+ JSONObject tag = sequence.getJSONObject(key);
+
+ Integer intTagCode = lookupTag(key, tag);
+ DicomVR vrEnum = lookupVR(intTagCode, tag);
+
+ DicomTag dicomTag = new DicomTag(intTagCode, vrEnum);
+
+ if (tag.has("Value")) {
+ dicomTag.value = tag.get("Value");
+ }
+
+ LOGGER.debug("Adding tag: {}, VR: {}, value: {}", intTagCode, vrEnum, dicomTag.value);
+ dicomTag.validateValue();
+
+ dicomTag.parent = parent;
+ parent.children.add(dicomTag);
+
+ if (vrEnum == DicomVR.SQ && tag.get("Sequence") != null) {
+ readSequence(tag, dicomTag);
+ }
+ }
+ parent.children.sort(null);
+ }
+
+ /**
+ * Get the tag code corresponding to the given JSON object.
+ * If "Tag" is not a defined attribute, lookup the default
+ * for the object name in the dictionary.
+ */
+ private Integer lookupTag(String tagKey, JSONObject tag) {
+ Integer intTagCode = null;
+
+ if (tag.has("Tag")) {
+ String[] tagCode = tag.getString("Tag").replaceAll("[()]", "").split(",");
+
+ int tagUpper = Integer.parseInt(tagCode[0], 16);
+ int tagLower = Integer.parseInt(tagCode[1], 16);
+
+ intTagCode = tagUpper << 16 | tagLower;
+ }
+ else {
+ intTagCode = DicomAttribute.getTag(tagKey);
+
+ if (intTagCode == null) {
+ throw new IllegalArgumentException(
+ "Tag not defined and could not be determined from description '" +
+ tagKey + "'");
+ }
+ }
+
+ return intTagCode;
+ }
+
+ /**
+ * Get the VR associated with the given tag code and JSON object.
+ * If "VR" is not a defined attribute, lookup the default for the
+ * given tag code in the dictionary.
+ */
+ private DicomVR lookupVR(Integer intTagCode, JSONObject tag) {
+ DicomVR vrEnum = DicomAttribute.getDefaultVR(intTagCode);
+ if (tag.has("VR")) {
+ DicomVR userEnum = DicomVR.valueOf(DicomVR.class, tag.getString("VR"));
+ if (!vrEnum.equals(userEnum)) {
+ LOGGER.warn("User-defined VR ({}) for {} does not match expected VR ({})",
+ userEnum, DicomAttribute.formatTag(intTagCode), vrEnum);
+ if (userEnum != null) {
+ vrEnum = userEnum;
+ }
+ }
+ }
+
+ return vrEnum;
+ }
+
+
+
+}
diff --git a/components/formats-bsd/src/loci/formats/dicom/DicomTag.java b/components/formats-bsd/src/loci/formats/dicom/DicomTag.java
index caec5c19e70..8e5a868c0ad 100644
--- a/components/formats-bsd/src/loci/formats/dicom/DicomTag.java
+++ b/components/formats-bsd/src/loci/formats/dicom/DicomTag.java
@@ -47,7 +47,7 @@
* Represents a complete DICOM tag, including the dictionary attribute,
* actual VR, value, and any "child" tags (in the case of a sequence).
*/
-public class DicomTag {
+public class DicomTag implements Comparable {
public DicomTag parent = null;
public List children = new ArrayList();
@@ -65,6 +65,10 @@ public class DicomTag {
private boolean bigEndianTransferSyntax = false;
private boolean oddLocations = false;
+ // optional indicator of how to handle this tag when merging
+ // into an existing tag list
+ public ResolutionStrategy strategy = null;
+
public DicomTag(DicomAttribute attribute) {
this(attribute, null);
}
@@ -80,6 +84,17 @@ public DicomTag(DicomAttribute attribute, DicomVR vr) {
this.tag = attribute.getTag();
}
+ public DicomTag(int tag, DicomVR vr) {
+ this.tag = tag;
+ this.attribute = DicomAttribute.get(tag);
+ if (vr != null) {
+ this.vr = vr;
+ }
+ else if (attribute != null) {
+ this.vr = attribute.getDefaultVR();
+ }
+ }
+
/**
* Read a complete tag and value from the given input stream.
*/
@@ -536,9 +551,174 @@ public DicomTag lookupChild(DicomAttribute attr) {
return null;
}
+ /**
+ * Check this tag against a list of existing tags.
+ * If this tag is a duplicate of an existing tag or otherwise deemed invalid,
+ * return false. Otherwise return true.
+ */
+ public boolean validate(List tags) {
+ for (DicomTag t : tags) {
+ if (this.tag == t.tag) {
+ return strategy != ResolutionStrategy.IGNORE;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Compare the value type to the VR.
+ * If the two types do not match, attempt to conver the value
+ * into the VR's type.
+ * Usually this means parsing numerical values from a String.
+ */
+ public void validateValue() {
+ if (value == null) {
+ return;
+ }
+ switch (vr) {
+ case AE:
+ case AS:
+ case CS:
+ case DA:
+ case DS:
+ case DT:
+ case IS:
+ case LO:
+ case LT:
+ case PN:
+ case SH:
+ case ST:
+ case TM:
+ case UC:
+ case UI:
+ case UR:
+ case UT:
+ value = value.toString();
+ break;
+ case AT:
+ case SS:
+ case US:
+ if (value instanceof Short) {
+ value = new short[] {(short) value};
+ }
+ else if (value instanceof String) {
+ String[] values = ((String) value).split(",");
+ short[] v = new short[values.length];
+ for (int i=0; i getTags();
+
+}
diff --git a/components/formats-bsd/src/loci/formats/dicom/ResolutionStrategy.java b/components/formats-bsd/src/loci/formats/dicom/ResolutionStrategy.java
new file mode 100644
index 00000000000..e4b8939b059
--- /dev/null
+++ b/components/formats-bsd/src/loci/formats/dicom/ResolutionStrategy.java
@@ -0,0 +1,41 @@
+/*
+ * #%L
+ * BSD implementations of Bio-Formats readers and writers
+ * %%
+ * Copyright (C) 2005 - 2023 Open Microscopy Environment:
+ * - Board of Regents of the University of Wisconsin-Madison
+ * - Glencoe Software, Inc.
+ * - University of Dundee
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+package loci.formats.dicom;
+
+/**
+ */
+public enum ResolutionStrategy {
+ IGNORE,
+ REPLACE,
+ APPEND;
+}
diff --git a/components/formats-bsd/src/loci/formats/dicom/dicom-dictionary.txt b/components/formats-bsd/src/loci/formats/dicom/dicom-dictionary.txt
index ed5ab38f832..3d29d03f7ba 100644
--- a/components/formats-bsd/src/loci/formats/dicom/dicom-dictionary.txt
+++ b/components/formats-bsd/src/loci/formats/dicom/dicom-dictionary.txt
@@ -123,7 +123,7 @@
00080124, "Mapping Resource Identification Sequence", SQ
00080201, "Timezone Offset from UTC", SH
00080220, "Responsible Group Code Sequence", SQ
-00080220, "Equipment Modality", CS
+00080221, "Equipment Modality", CS
00080222, "Manufacturer's Related Model Group", LO
00080300, "Private Data Element Characteristics Sequence", SQ
00080301, "Private Group Reference", US
@@ -285,7 +285,7 @@
00100221, "Genetic Modifications Sequence", SQ
00100222, "Genetic Modifications Description", UC
00100223, "Genetic Modifications Nomenclature", LO
-00100229, "Genetic Modifications Sequence", SQ
+00100229, "Genetic Modifications Code Sequence", SQ
00101000, "Other Patient IDs", LO
00101001, "Other Patient Names", PN
00101002, "Other Patient IDs Sequence", SQ
@@ -385,7 +385,7 @@
00142002, "Evaluator Sequence", SQ
00142004, "Evaluator Number", IS
00142006, "Evaluator Name", PN
-00142006, "Evaluator Attempt", IS
+00142008, "Evaluation Attempt", IS
00142012, "Indication Sequence", SQ
00142014, "Indication Number", IS
00142016, "Indication Label", SH
diff --git a/components/formats-bsd/src/loci/formats/out/DicomWriter.java b/components/formats-bsd/src/loci/formats/out/DicomWriter.java
index 274a9953cba..b3f25f64dd4 100644
--- a/components/formats-bsd/src/loci/formats/out/DicomWriter.java
+++ b/components/formats-bsd/src/loci/formats/out/DicomWriter.java
@@ -43,6 +43,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.List;
import loci.common.Constants;
import loci.common.DataTools;
@@ -58,6 +59,9 @@
import loci.formats.codec.JPEG2000Codec;
import loci.formats.codec.JPEG2000CodecOptions;
import loci.formats.codec.JPEGCodec;
+import loci.formats.dicom.DicomAttribute;
+import loci.formats.dicom.DicomJSONProvider;
+import loci.formats.dicom.ITagProvider;
import loci.formats.dicom.DicomTag;
import loci.formats.in.DynamicMetadataOptions;
import loci.formats.in.MetadataOptions;
@@ -82,7 +86,7 @@
* This is designed for whole slide images, and may not produce
* schema-compliant files for other modalities.
*/
-public class DicomWriter extends FormatWriter {
+public class DicomWriter extends FormatWriter implements IExtraMetadataWriter {
// -- Constants --
@@ -116,6 +120,7 @@ public class DicomWriter extends FormatWriter {
private String instanceUIDValue;
private String implementationUID;
+ private ArrayList tagProviders = new ArrayList();
private boolean bigTiff = false;
private TiffSaver tiffSaver;
@@ -132,6 +137,34 @@ public DicomWriter() {
};
}
+ // -- IExtraMetadataWriter API methods --
+
+ @Override
+ public void setExtraMetadata(String tagSource) {
+ FormatTools.assertId(currentId, false, 1);
+
+ // get the provider (parser) from the source name
+ // uses the file extension, this might need improvement
+
+ if (tagSource != null) {
+ ITagProvider provider = null;
+ if (checkSuffix(tagSource, "json")) {
+ provider = new DicomJSONProvider();
+ }
+ else {
+ throw new IllegalArgumentException("Unknown tag format: " + tagSource);
+ }
+
+ try {
+ provider.readTagSource(tagSource);
+ tagProviders.add(provider);
+ }
+ catch (IOException e) {
+ LOGGER.error("Could not parse extra metadata: " + tagSource, e);
+ }
+ }
+ }
+
/**
* Sets whether or not BigTIFF files should be written.
* This flag is not reset when close() is called.
@@ -1170,13 +1203,59 @@ public void setId(String id) throws FormatException, IOException {
tags.add(labelText);
}
+ // now add all supplementary tags from tag providers
+ for (ITagProvider provider : tagProviders) {
+ for (DicomTag t : provider.getTags()) {
+ boolean validTag = t.validate(tags);
+ if (validTag) {
+ padTagValues(t);
+
+ LOGGER.trace("handling supplemental tag ({}) with strategy {}", t, t.strategy);
+ switch (t.strategy) {
+ case APPEND:
+ if (t.vr == SQ) {
+ DicomTag existingSequence = lookupTag(tags, t);
+ if (existingSequence == null) {
+ tags.add(t);
+ }
+ else {
+ existingSequence.children.add(makeItem());
+ for (DicomTag child : t.children) {
+ existingSequence.children.add(child);
+ }
+ existingSequence.children.add(makeItemDelimitation());
+ }
+ }
+ else {
+ tags.add(t);
+ }
+ break;
+ case IGNORE:
+ // ignore current tag if a matching tag already exists
+ DicomTag existing = lookupTag(tags, t);
+ if (existing == null) {
+ tags.add(t);
+ }
+ break;
+ case REPLACE:
+ // replace existing tag with current tag
+ DicomTag replace = lookupTag(tags, t);
+ if (replace != null) {
+ tags.remove(replace);
+ }
+ tags.add(t);
+ break;
+ }
+ }
+ else {
+ LOGGER.warn("Ignoring tag {} from provider {}", t, provider);
+ }
+ }
+ }
+
// sort tags into ascending order, then write
- tags.sort(new Comparator() {
- public int compare(DicomTag a, DicomTag b) {
- return a.attribute.getTag() - b.attribute.getTag();
- }
- });
+ tags.sort(null);
for (DicomTag tag : tags) {
writeTag(tag);
@@ -1273,6 +1352,10 @@ public void close() throws IOException {
long fp = out.getFilePointer();
writeIFDs(resolutionIndex);
long length = out.getFilePointer() - fp;
+ if (length % 2 == 1) {
+ out.writeByte(0);
+ length++;
+ }
out.seek(fp - 4);
out.writeInt((int) length);
}
@@ -1292,6 +1375,8 @@ public void close() throws IOException {
tiffSaver = null;
validPixelCount = null;
+ tagProviders.clear();
+
// intentionally don't reset tile dimensions
}
@@ -1359,7 +1444,7 @@ private int getStoredLength(DicomTag tag) {
}
private void writeTag(DicomTag tag) throws IOException {
- int tagCode = tag.attribute.getTag();
+ int tagCode = tag.attribute == null ? tag.tag : tag.attribute.getTag();
out.writeShort((short) ((tagCode & 0xffff0000) >> 16));
out.writeShort((short) (tagCode & 0xffff));
@@ -1870,6 +1955,30 @@ private DicomTag makeItemDelimitation() {
return item;
}
+ private DicomTag lookupTag(List tags, DicomTag compare) {
+ for (DicomTag t : tags) {
+ if (t.tag == compare.tag) {
+ return t;
+ }
+ }
+ return null;
+ }
+
+ private void padTagValues(DicomTag t) {
+ if (t.value instanceof String) {
+ if (t.vr == UI) {
+ t.value = padUID((String) t.value);
+ }
+ else {
+ t.value = padString((String) t.value);
+ }
+ }
+
+ for (DicomTag child : t.children) {
+ padTagValues(child);
+ }
+ }
+
private short[] makeShortArray(int v) {
short[] s = new short[2];
s[0] = (short) ((v >> 16) & 0xffff);
diff --git a/components/formats-bsd/src/loci/formats/out/IExtraMetadataWriter.java b/components/formats-bsd/src/loci/formats/out/IExtraMetadataWriter.java
new file mode 100644
index 00000000000..7cc1da2170d
--- /dev/null
+++ b/components/formats-bsd/src/loci/formats/out/IExtraMetadataWriter.java
@@ -0,0 +1,53 @@
+/*
+ * #%L
+ * BSD implementations of Bio-Formats readers and writers
+ * %%
+ * Copyright (C) 2023 Open Microscopy Environment:
+ * - Board of Regents of the University of Wisconsin-Madison
+ * - Glencoe Software, Inc.
+ * - University of Dundee
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+package loci.formats.out;
+
+import java.io.IOException;
+
+public interface IExtraMetadataWriter {
+
+ /**
+ * Provide additional metadata that should be written
+ * as part of this dataset. Primarily intended for DicomWriter.
+ *
+ * Calling this method is optional. Multiple calls are allowed,
+ * e.g. if tags come from multiple external sources.
+ * If multiple calls to this method are made then the order matters,
+ * with earlier calls implying higher priority when resolving duplicate
+ * or conflicting tags.
+ *
+ * All calls to this method must occur before setId is called.
+ */
+ void setExtraMetadata(String metadataSource);
+
+}
diff --git a/components/formats-bsd/test/loci/formats/utests/dicom/ProvidedMetadataTest.java b/components/formats-bsd/test/loci/formats/utests/dicom/ProvidedMetadataTest.java
new file mode 100644
index 00000000000..e0e0cc63805
--- /dev/null
+++ b/components/formats-bsd/test/loci/formats/utests/dicom/ProvidedMetadataTest.java
@@ -0,0 +1,453 @@
+/*
+ * #%L
+ * BSD implementations of Bio-Formats readers and writers
+ * %%
+ * Copyright (C) 2023 Open Microscopy Environment:
+ * - Board of Regents of the University of Wisconsin-Madison
+ * - Glencoe Software, Inc.
+ * - University of Dundee
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+package loci.formats.utests.dicom;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import loci.common.Constants;
+import loci.formats.FormatException;
+import loci.formats.MetadataTools;
+import loci.formats.dicom.DicomAttribute;
+import loci.formats.dicom.DicomTag;
+import loci.formats.dicom.DicomVR;
+import loci.formats.in.DicomReader;
+import loci.formats.in.FakeReader;
+import loci.formats.meta.IMetadata;
+import loci.formats.out.DicomWriter;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/**
+ */
+public class ProvidedMetadataTest {
+
+ public Path doConversion(String jsonMetadata) throws FormatException, IOException {
+ Path jsonFile = Files.createTempFile("dicom", ".json");
+ Path dicomFile = Files.createTempFile("metadata-test", ".dcm");
+ FakeReader reader = new FakeReader();
+ DicomWriter writer = new DicomWriter();
+ try {
+ Files.write(jsonFile, jsonMetadata.getBytes(Constants.ENCODING));
+
+ IMetadata meta = MetadataTools.createOMEXMLMetadata();
+ reader.setMetadataStore(meta);
+ reader.setId("test.fake");
+
+ writer.setExtraMetadata(jsonFile.toString());
+ writer.setMetadataRetrieve(meta);
+ writer.setId(dicomFile.toString());
+ writer.saveBytes(0, reader.openBytes(0));
+ }
+ catch (Throwable e) {
+ Files.delete(dicomFile);
+ throw e;
+ }
+ finally {
+ reader.close();
+ writer.close();
+ Files.delete(jsonFile);
+ }
+
+ return dicomFile;
+ }
+
+ public List getTags(Path dicomFile) throws FormatException, IOException {
+ DicomReader reader = new DicomReader();
+ reader.setGroupFiles(false);
+ reader.setId(dicomFile.toString());
+ List tags = reader.getTags();
+ reader.close();
+ return tags;
+ }
+
+ public DicomTag lookup(List tags, DicomAttribute attr) {
+ for (DicomTag t : tags) {
+ if (attr.equals(t.attribute)) {
+ return t;
+ }
+ }
+ return null;
+ }
+
+ @Test
+ public void testSingleTagWithDefaults() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"BodyPartExamined\": {" +
+ "\"Value\": \"BRAIN\"" +
+ "}}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+ DicomTag found = lookup(tags, DicomAttribute.BODY_PART_EXAMINED);
+ assertNotNull(found);
+
+ // trailing space is not a typo
+ // the writer pads string values to an even number of characters
+ assertEquals(found.value, "BRAIN ");
+ assertEquals(found.vr, DicomVR.CS);
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+ @Test(expectedExceptions={ IllegalArgumentException.class })
+ public void testSingleInvalidTagName() throws FormatException, IOException {
+ String json = "{" +
+ "\"not a valid tag\": {" +
+ "\"Value\": \"x\"" +
+ "}}";
+
+ doConversion(json);
+ }
+
+ @Test(expectedExceptions={ IllegalArgumentException.class })
+ public void testSingleTagWeirdNameWithWhitespace() throws FormatException, IOException {
+ String json = "{" +
+ "\"bOdy PaRt examiNeD \": {" +
+ "\"Value\": \"x\"" +
+ "}}";
+
+ doConversion(json);
+ }
+
+ @Test
+ public void testSingleTagWeirdName() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"bOdyPaRtexamiNeD\": {" +
+ "\"Value\": \"BRAIN\"" +
+ "}}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+ DicomTag found = lookup(tags, DicomAttribute.BODY_PART_EXAMINED);
+ assertNotNull(found);
+
+ // trailing space is not a typo
+ // the writer pads string values to an even number of characters
+ assertEquals(found.value, "BRAIN ");
+ assertEquals(found.vr, DicomVR.CS);
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+ @Test
+ public void testSingleTagCustomVR() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"BodyPartExamined\": {" +
+ "\"Value\": \"0\"," +
+ "\"VR\": \"SH\"" +
+ "}}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+ DicomTag found = lookup(tags, DicomAttribute.BODY_PART_EXAMINED);
+ assertNotNull(found);
+
+ assertEquals(found.value, "0 ");
+ assertEquals(found.vr, DicomVR.SH);
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+ @Test
+ public void testReplaceExistingTag() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"SpecimenLabelInImage\": {" +
+ "\"Value\": \"NO\"" +
+ "}}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+ DicomTag found = lookup(tags, DicomAttribute.SPECIMEN_LABEL_IN_IMAGE);
+ assertNotNull(found);
+
+ assertEquals(found.value, "NO");
+ assertEquals(found.vr, DicomVR.CS);
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+ @Test
+ public void testIgnoreExistingTag() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"SpecimenLabelInImage\": {" +
+ "\"Value\": \"NO\"," +
+ "\"ResolutionStrategy\": \"IGNORE\"" +
+ "}}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+ DicomTag found = lookup(tags, DicomAttribute.SPECIMEN_LABEL_IN_IMAGE);
+ assertNotNull(found);
+
+ assertEquals(found.value, "YES ");
+ assertEquals(found.vr, DicomVR.CS);
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+ @Test
+ public void testReplaceOpticalPath() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"OpticalPathSequence\": {" +
+ "\"Sequence\": {" +
+ "\"IlluminationTypeCodeSequence\": {" +
+ "\"Sequence\": {" +
+ "\"CodeValue\": {" +
+ "\"VR\": \"SH\"," +
+ "\"Value\": \"111743\"" +
+ "}," +
+ "\"CodingSchemeDesignator\": {" +
+ "\"VR\": \"SH\"," +
+ "\"Value\": \"DCM\"" +
+ "}," +
+ "\"CodeMeaning\": {" +
+ "\"VR\": \"LO\"," +
+ "\"Tag\": \"(0008,0104)\"," +
+ "\"Value\": \"Epifluorescence illumination\"" +
+ "}}}," +
+ "\"IlluminationWaveLength\": {" +
+ "\"Value\": \"488.0\"" +
+ "}," +
+ "\"OpticalPathIdentifier\": {" +
+ "\"Value\": \"1\"" +
+ "}," +
+ "\"OpticalPathDescription\": {" +
+ "\"Value\": \"replacement channel\" " +
+ "}}," +
+ "\"ResolutionStrategy\": \"REPLACE\"" +
+ "}}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+
+ DicomTag found = lookup(tags, DicomAttribute.OPTICAL_PATH_SEQUENCE);
+ assertNotNull(found);
+
+ assertEquals(found.children.size(), 4);
+ assertEquals(found.vr, DicomVR.SQ);
+
+ assertEquals(found.children.get(0).attribute, DicomAttribute.ILLUMINATION_TYPE_CODE_SEQUENCE);
+
+ assertEquals(found.children.get(1).attribute, DicomAttribute.ILLUMINATION_WAVELENGTH);
+ assertEquals(found.children.get(1).value, 488f);
+
+ assertEquals(found.children.get(2).attribute, DicomAttribute.OPTICAL_PATH_ID);
+ assertEquals(found.children.get(2).value, "1 ");
+
+ assertEquals(found.children.get(3).attribute, DicomAttribute.OPTICAL_PATH_DESCRIPTION);
+ assertEquals(found.children.get(3).value, "replacement channel ");
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+ @Test
+ public void testAppendOpticalPath() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"OpticalPathSequence\": {" +
+ "\"Sequence\": {" +
+ "\"IlluminationTypeCodeSequence\": {" +
+ "\"Sequence\": {" +
+ "\"CodeValue\": {" +
+ "\"VR\": \"SH\"," +
+ "\"Value\": \"111743\"" +
+ "}," +
+ "\"CodingSchemeDesignator\": {" +
+ "\"VR\": \"SH\"," +
+ "\"Value\": \"DCM\"" +
+ "}," +
+ "\"CodeMeaning\": {" +
+ "\"VR\": \"LO\"," +
+ "\"Tag\": \"(0008,0104)\"," +
+ "\"Value\": \"Epifluorescence illumination\"" +
+ "}}}," +
+ "\"IlluminationWaveLength\": {" +
+ "\"Value\": \"488.0\"" +
+ "}," +
+ "\"OpticalPathIdentifier\": {" +
+ "\"Value\": \"1\"" +
+ "}," +
+ "\"OpticalPathDescription\": {" +
+ "\"Value\": \"replacement channel\" " +
+ "}}," +
+ "\"ResolutionStrategy\": \"APPEND\"" +
+ "}}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+
+ DicomTag found = lookup(tags, DicomAttribute.OPTICAL_PATH_SEQUENCE);
+ assertNotNull(found);
+
+ assertEquals(found.children.size(), 8);
+ assertEquals(found.vr, DicomVR.SQ);
+
+ assertEquals(found.children.get(0).attribute, DicomAttribute.ILLUMINATION_TYPE_CODE_SEQUENCE);
+
+ assertEquals(found.children.get(1).attribute, DicomAttribute.ILLUMINATION_WAVELENGTH);
+ assertEquals(found.children.get(1).value, 1f);
+
+ assertEquals(found.children.get(2).attribute, DicomAttribute.OPTICAL_PATH_ID);
+ assertEquals(found.children.get(2).value, "0 ");
+
+ assertEquals(found.children.get(3).attribute, DicomAttribute.OPTICAL_PATH_DESCRIPTION);
+ assertEquals(found.children.get(3).value, "");
+
+ assertEquals(found.children.get(4).attribute, DicomAttribute.ILLUMINATION_TYPE_CODE_SEQUENCE);
+
+ assertEquals(found.children.get(5).attribute, DicomAttribute.ILLUMINATION_WAVELENGTH);
+ assertEquals(found.children.get(5).value, 488f);
+
+ assertEquals(found.children.get(6).attribute, DicomAttribute.OPTICAL_PATH_ID);
+ assertEquals(found.children.get(6).value, "1 ");
+
+ assertEquals(found.children.get(7).attribute, DicomAttribute.OPTICAL_PATH_DESCRIPTION);
+ assertEquals(found.children.get(7).value, "replacement channel ");
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+ @Test
+ public void testIgnoreOpticalPath() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"OpticalPathSequence\": {" +
+ "\"Sequence\": {" +
+ "\"IlluminationTypeCodeSequence\": {" +
+ "\"Sequence\": {" +
+ "\"CodeValue\": {" +
+ "\"VR\": \"SH\"," +
+ "\"Value\": \"111743\"" +
+ "}," +
+ "\"CodingSchemeDesignator\": {" +
+ "\"VR\": \"SH\"," +
+ "\"Value\": \"DCM\"" +
+ "}," +
+ "\"CodeMeaning\": {" +
+ "\"VR\": \"LO\"," +
+ "\"Tag\": \"(0008,0104)\"," +
+ "\"Value\": \"Epifluorescence illumination\"" +
+ "}}}," +
+ "\"IlluminationWaveLength\": {" +
+ "\"Value\": \"488.0\"" +
+ "}," +
+ "\"OpticalPathIdentifier\": {" +
+ "\"Value\": \"1\"" +
+ "}," +
+ "\"OpticalPathDescription\": {" +
+ "\"Value\": \"replacement channel\" " +
+ "}}," +
+ "\"ResolutionStrategy\": \"IGNORE\"" +
+ "}}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+
+ DicomTag found = lookup(tags, DicomAttribute.OPTICAL_PATH_SEQUENCE);
+ assertNotNull(found);
+
+ assertEquals(found.children.size(), 4);
+ assertEquals(found.vr, DicomVR.SQ);
+
+ assertEquals(found.children.get(0).attribute, DicomAttribute.ILLUMINATION_TYPE_CODE_SEQUENCE);
+
+ assertEquals(found.children.get(1).attribute, DicomAttribute.ILLUMINATION_WAVELENGTH);
+ assertEquals(found.children.get(1).value, 1f);
+
+ assertEquals(found.children.get(2).attribute, DicomAttribute.OPTICAL_PATH_ID);
+ assertEquals(found.children.get(2).value, "0 ");
+
+ assertEquals(found.children.get(3).attribute, DicomAttribute.OPTICAL_PATH_DESCRIPTION);
+ assertEquals(found.children.get(3).value, "");
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+ @Test
+ public void testIgnoreInvalidJSON() throws FormatException, IOException {
+ Path dicomFile = null;
+ try {
+ String json = "{" +
+ "\"BodyPartExamined\": {" +
+ "\"Value\": \"BRAIN\"" +
+ "}";
+
+ dicomFile = doConversion(json);
+ List tags = getTags(dicomFile);
+ DicomTag found = lookup(tags, DicomAttribute.BODY_PART_EXAMINED);
+ assertEquals(found, null);
+ }
+ finally {
+ Files.delete(dicomFile);
+ }
+ }
+
+}
diff --git a/components/formats-bsd/test/loci/formats/utests/testng.xml b/components/formats-bsd/test/loci/formats/utests/testng.xml
index 1942d83acbf..95bec190a62 100644
--- a/components/formats-bsd/test/loci/formats/utests/testng.xml
+++ b/components/formats-bsd/test/loci/formats/utests/testng.xml
@@ -193,4 +193,10 @@
+
+
+
+
+
+