diff --git a/src/main/java/org/karnak/backend/enums/ProfileItemType.java b/src/main/java/org/karnak/backend/enums/ProfileItemType.java index d6d5027ce..81c407d61 100644 --- a/src/main/java/org/karnak/backend/enums/ProfileItemType.java +++ b/src/main/java/org/karnak/backend/enums/ProfileItemType.java @@ -9,15 +9,7 @@ */ package org.karnak.backend.enums; -import org.karnak.backend.model.profiles.ActionDates; -import org.karnak.backend.model.profiles.ActionTags; -import org.karnak.backend.model.profiles.BasicProfile; -import org.karnak.backend.model.profiles.CleanPixelData; -import org.karnak.backend.model.profiles.Defacing; -import org.karnak.backend.model.profiles.Expression; -import org.karnak.backend.model.profiles.PrivateTags; -import org.karnak.backend.model.profiles.ProfileItem; -import org.karnak.backend.model.profiles.UpdateUIDsProfile; +import org.karnak.backend.model.profiles.*; public enum ProfileItemType { @@ -30,7 +22,8 @@ public enum ProfileItemType { ACTION_PRIVATETAGS(PrivateTags.class, "action.on.privatetags", "113111", "Retain Safe Private Option"), ACTION_DATES(ActionDates.class, "action.on.dates", "113107", "Retain Longitudinal Temporal Information Modified Dates Option"), - EXPRESSION_TAGS(Expression.class, "expression.on.tags", null, null); + EXPRESSION_TAGS(Expression.class, "expression.on.tags", null, null), + ADD_TAG(AddTag.class, "action.add.tag", null, null); private final Class profileClass; diff --git a/src/main/java/org/karnak/backend/model/action/Add.java b/src/main/java/org/karnak/backend/model/action/Add.java index 9d9d90f13..68c447780 100644 --- a/src/main/java/org/karnak/backend/model/action/Add.java +++ b/src/main/java/org/karnak/backend/model/action/Add.java @@ -31,6 +31,9 @@ public void execute(Attributes dcm, int tag, HMAC hmac) { tagValueIn, dummyValue); } + // If the DICOM object already contains the attribute, do nothing + if (dcm.contains(newTag)) return; + if (dummyValue != null) { dcm.setString(newTag, vr, dummyValue); } diff --git a/src/main/java/org/karnak/backend/model/profiles/AddTag.java b/src/main/java/org/karnak/backend/model/profiles/AddTag.java new file mode 100644 index 000000000..ec92e10ae --- /dev/null +++ b/src/main/java/org/karnak/backend/model/profiles/AddTag.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021 Karnak Team and other contributors. + * + * This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache + * License, Version 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package org.karnak.backend.model.profiles; + +import lombok.extern.slf4j.Slf4j; +import org.dcm4che3.data.Attributes; +import org.dcm4che3.data.VR; +import org.dcm4che3.util.TagUtils; +import org.karnak.backend.config.AppConfig; +import org.karnak.backend.data.entity.ArgumentEntity; +import org.karnak.backend.data.entity.IncludedTagEntity; +import org.karnak.backend.data.entity.ProfileElementEntity; +import org.karnak.backend.model.action.*; +import org.karnak.backend.model.expression.ExprCondition; +import org.karnak.backend.model.expression.ExpressionError; +import org.karnak.backend.model.expression.ExpressionResult; +import org.karnak.backend.model.profilepipe.HMAC; +import org.karnak.backend.model.profilepipe.TagActionMap; +import org.karnak.backend.model.standard.StandardDICOM; + +@Slf4j +public class AddTag extends AbstractProfileItem { + + private final TagActionMap tagsAction; + + private final TagActionMap exceptedTagsAction; + + private final ActionItem actionByDefault; + + private boolean tagAdded = false; + + private final StandardDICOM standardDICOM; + + public AddTag(ProfileElementEntity profileElementEntity) throws Exception { + super(profileElementEntity); + tagsAction = new TagActionMap(); + exceptedTagsAction = new TagActionMap(); + actionByDefault = new Keep("K"); + profileValidation(); + setActionHashMap(); + + standardDICOM = AppConfig.getInstance().getStandardDICOM(); + } + + private void setActionHashMap() throws Exception { + + if (tagEntities != null && tagEntities.size() > 0) { + for (IncludedTagEntity tag : tagEntities) { + tagsAction.put(tag.getTagValue(), actionByDefault); + } + } + } + + @Override + public ActionItem getAction(Attributes dcm, Attributes dcmCopy, int tag, HMAC hmac) { + if (!tagAdded) { + IncludedTagEntity t = tagEntities.getFirst(); + String tagValue = t.getTagValue().replaceAll("[(),]", ""); + + String value = ""; + VR vr = null; + for (ArgumentEntity ae : argumentEntities) { + if (ae.getArgumentKey().equals("value")) { + value = ae.getArgumentValue(); + } else if (ae.getArgumentKey().equals("vr")) { + vr = VR.valueOf(ae.getArgumentValue()); + } + } + if (vr == null) { + vr = VR.valueOf(standardDICOM.getAttributeDetail(tagValue).getValueRepresentation()); + } + tagAdded = true; + return new Add("A", TagUtils.intFromHexString(tagValue), vr, value); + } + return null; + } + + public void profileValidation() throws Exception { + if (argumentEntities == null || argumentEntities.isEmpty()) { + throw new Exception("Cannot build the profile " + codeName + ": Need to specify value argument"); + } + if (tagEntities != null && tagEntities.size() > 1) { + throw new Exception("Cannot build the profile " + codeName + ": Exactly one tag is required"); + } + + final ExpressionError expressionError = ExpressionResult.isValid(condition, new ExprCondition(new Attributes()), + Boolean.class); + if (condition != null && !expressionError.isValid()) { + throw new Exception(expressionError.getMsg()); + } + } +} diff --git a/src/main/java/org/karnak/backend/service/profilepipe/Profile.java b/src/main/java/org/karnak/backend/service/profilepipe/Profile.java index 7dbe77c5e..a162cb897 100644 --- a/src/main/java/org/karnak/backend/service/profilepipe/Profile.java +++ b/src/main/java/org/karnak/backend/service/profilepipe/Profile.java @@ -40,16 +40,14 @@ import org.karnak.backend.dicom.Defacer; import org.karnak.backend.enums.ProfileItemType; import org.karnak.backend.model.action.ActionItem; +import org.karnak.backend.model.action.Add; import org.karnak.backend.model.action.Remove; import org.karnak.backend.model.action.ReplaceNull; import org.karnak.backend.model.expression.ExprCondition; import org.karnak.backend.model.expression.ExpressionResult; import org.karnak.backend.model.profilepipe.HMAC; import org.karnak.backend.model.profilepipe.HashContext; -import org.karnak.backend.model.profiles.ActionTags; -import org.karnak.backend.model.profiles.CleanPixelData; -import org.karnak.backend.model.profiles.Defacing; -import org.karnak.backend.model.profiles.ProfileItem; +import org.karnak.backend.model.profiles.*; import org.slf4j.MDC; import org.slf4j.Marker; import org.slf4j.MarkerFactory; @@ -156,7 +154,15 @@ public void applyAction(Attributes dcm, Attributes dcmCopy, HMAC hmac, ProfileIt } if (currentAction != null) { - break; + if (currentAction instanceof Add) { + // When adding a new tag, the variable tag is irrelevant and should not be flagged as modified + // Set the current action to null after execution and do not break out of the loop if other + // profile elements should be applied to the tag + execute(currentAction, dcm, tag, hmac); + currentAction = null; + } else { + break; + } } if (profileEntity.equals(profilePassedInSequence)) { @@ -177,18 +183,23 @@ public void applyAction(Attributes dcm, Attributes dcmCopy, HMAC hmac, ProfileIt } else { if (currentAction != null) { - try { - currentAction.execute(dcm, tag, hmac); - } - catch (final Exception e) { - log.error("Cannot execute the currentAction {} for tag: {}", currentAction, - TagUtils.toString(tag), e); - } + execute(currentAction, dcm, tag, hmac); } } } } + private void execute(ActionItem currentAction, Attributes dcm, int tag, HMAC hmac) { + if (currentAction == null) return; + try { + currentAction.execute(dcm, tag, hmac); + } + catch (final Exception e) { + log.error("Cannot execute the currentAction {} for tag: {}", currentAction, + TagUtils.toString(tag), e); + } + } + public void applyCleanPixelData(Attributes dcmCopy, AttributeEditorContext context, ProfileEntity profileEntity) { Object pix = dcmCopy.getValue(Tag.PixelData); if ((pix instanceof BulkData || pix instanceof Fragments) && !profileEntity.getMaskEntities().isEmpty() diff --git a/src/test/java/org/karnak/backend/model/profiles/AddTagTest.java b/src/test/java/org/karnak/backend/model/profiles/AddTagTest.java new file mode 100644 index 000000000..d12a94c1a --- /dev/null +++ b/src/test/java/org/karnak/backend/model/profiles/AddTagTest.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2021 Karnak Team and other contributors. + * + * This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0, or the Apache + * License, Version 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package org.karnak.backend.model.profiles; + +import org.dcm4che3.data.Attributes; +import org.dcm4che3.data.Tag; +import org.dcm4che3.data.VR; +import org.junit.jupiter.api.Test; +import org.karnak.backend.data.entity.ArgumentEntity; +import org.karnak.backend.data.entity.IncludedTagEntity; +import org.karnak.backend.data.entity.ProfileElementEntity; +import org.karnak.backend.data.entity.ProfileEntity; +import org.karnak.backend.service.profilepipe.Profile; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +public class AddTagTest { + + @Test + void addTag() { + ProfileEntity profileEntity = new ProfileEntity(); + Attributes attributes = new Attributes(); + attributes.setString(Tag.Modality, VR.CS, "XA"); + attributes.setString(Tag.SOPClassUID, VR.UI, "1.2.840.10008.5.1.4.1.1.12.1"); + + Set profileElementEntities = new HashSet<>(); + ProfileElementEntity profileElementEntityAddBurnedAttr = new ProfileElementEntity(); + profileElementEntityAddBurnedAttr.setCodename("action.add.tag"); + profileElementEntityAddBurnedAttr.setName("Add tag BurnedInAnnotation"); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("value", "YES", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("vr", "CS", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addIncludedTag(new IncludedTagEntity("(0028,0301)", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.setPosition(1); + + profileElementEntities.add(profileElementEntityAddBurnedAttr); + profileEntity.setProfileElementEntities(profileElementEntities); + Profile profile = new Profile(profileEntity); + + // Apply the profile that adds the BurnedInAnnotation attribute to both objects + profile.applyAction(attributes, attributes, null, null, null, null); + + // The BurnedInAnnotation attribute is added and its value set to YES + assertEquals("YES", attributes.getString(Tag.BurnedInAnnotation)); + } + + @Test + void addTag_withoutVR() { + ProfileEntity profileEntity = new ProfileEntity(); + Attributes attributes = new Attributes(); + attributes.setString(Tag.Modality, VR.CS, "XA"); + attributes.setString(Tag.SOPClassUID, VR.UI, "1.2.840.10008.5.1.4.1.1.12.1"); + + Set profileElementEntities = new HashSet<>(); + ProfileElementEntity profileElementEntityAddBurnedAttr = new ProfileElementEntity(); + profileElementEntityAddBurnedAttr.setCodename("action.add.tag"); + profileElementEntityAddBurnedAttr.setName("Add tag BurnedInAnnotation"); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("value", "YES", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addIncludedTag(new IncludedTagEntity("(0028,0301)", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.setPosition(1); + + profileElementEntities.add(profileElementEntityAddBurnedAttr); + profileEntity.setProfileElementEntities(profileElementEntities); + Profile profile = new Profile(profileEntity); + + // Apply the profile that adds the BurnedInAnnotation attribute to both objects + profile.applyAction(attributes, attributes, null, null, null, null); + + // The BurnedInAnnotation attribute is added and its value set to YES + assertEquals("YES", attributes.getString(Tag.BurnedInAnnotation)); + assertEquals("CS", attributes.getVR(Tag.BurnedInAnnotation).toString()); + } + + @Test + void addTagThenIgnoreAction() { + ProfileEntity profileEntity = new ProfileEntity(); + Attributes attributes = new Attributes(); + attributes.setString(Tag.Modality, VR.CS, "XA"); + attributes.setString(Tag.SOPClassUID, VR.UI, "1.2.840.10008.5.1.4.1.1.12.1"); + + Set profileElementEntities = new HashSet<>(); + ProfileElementEntity profileElementEntityAddBurnedAttr = new ProfileElementEntity(); + profileElementEntityAddBurnedAttr.setCodename("action.add.tag"); + profileElementEntityAddBurnedAttr.setName("Add tag BurnedInAnnotation"); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("value", "YES", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("vr", "CS", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addIncludedTag(new IncludedTagEntity("(0028,0301)", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.setPosition(1); + + ProfileElementEntity profileElementEntitySetBurnedAttr = new ProfileElementEntity(); + profileElementEntitySetBurnedAttr.setCodename("expression.on.tags"); + profileElementEntitySetBurnedAttr.setName("Set tag BurnedInAnnotation to NO"); + profileElementEntitySetBurnedAttr.addArgument(new ArgumentEntity("expr", "Replace('NO')", profileElementEntityAddBurnedAttr)); + profileElementEntitySetBurnedAttr.addIncludedTag(new IncludedTagEntity("(0028,0301)", profileElementEntityAddBurnedAttr)); + profileElementEntitySetBurnedAttr.setPosition(2); + + profileElementEntities.add(profileElementEntityAddBurnedAttr); + profileElementEntities.add(profileElementEntitySetBurnedAttr); + profileEntity.setProfileElementEntities(profileElementEntities); + Profile profile = new Profile(profileEntity); + + // Apply the profile that adds the BurnedInAnnotation attribute to both objects + profile.applyAction(attributes, attributes, null, null, null, null); + + // The BurnedInAnnotation attribute is added and its value set to YES, the Replace is not applied + assertEquals("YES", attributes.getString(Tag.BurnedInAnnotation)); + } + + @Test + void addTag_ignoreTagAlreadyExisting() { + ProfileEntity profileEntity = new ProfileEntity(); + Attributes attributes = new Attributes(); + attributes.setString(Tag.Modality, VR.CS, "XA"); + attributes.setString(Tag.SOPClassUID, VR.UI, "1.2.840.10008.5.1.4.1.1.12.1"); + attributes.setString(Tag.BurnedInAnnotation, VR.CS, "NO"); + + Set profileElementEntities = new HashSet<>(); + ProfileElementEntity profileElementEntityAddBurnedAttr = new ProfileElementEntity(); + profileElementEntityAddBurnedAttr.setCodename("action.add.tag"); + profileElementEntityAddBurnedAttr.setName("Add tag BurnedInAnnotation"); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("value", "YES", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("vr", "CS", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addIncludedTag(new IncludedTagEntity("(0028,0301)", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.setPosition(1); + + profileElementEntities.add(profileElementEntityAddBurnedAttr); + profileEntity.setProfileElementEntities(profileElementEntities); + Profile profile = new Profile(profileEntity); + + // Apply the profile that adds the BurnedInAnnotation attribute to both objects + profile.applyAction(attributes, attributes, null, null, null, null); + + // The add action is ignored because it already exists and its value is NO + assertEquals("NO", attributes.getString(Tag.BurnedInAnnotation)); + } + + @Test + void addTag_ignoreTagAlreadyExistingThenModify() { + ProfileEntity profileEntity = new ProfileEntity(); + Attributes attributes = new Attributes(); + attributes.setString(Tag.Modality, VR.CS, "XA"); + attributes.setString(Tag.SOPClassUID, VR.UI, "1.2.840.10008.5.1.4.1.1.12.1"); + attributes.setString(Tag.BurnedInAnnotation, VR.CS, ""); + + Set profileElementEntities = new HashSet<>(); + ProfileElementEntity profileElementEntityAddBurnedAttr = new ProfileElementEntity(); + profileElementEntityAddBurnedAttr.setCodename("action.add.tag"); + profileElementEntityAddBurnedAttr.setName("Add tag BurnedInAnnotation"); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("value", "YES", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addArgument(new ArgumentEntity("vr", "CS", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.addIncludedTag(new IncludedTagEntity("(0028,0301)", profileElementEntityAddBurnedAttr)); + profileElementEntityAddBurnedAttr.setPosition(1); + + ProfileElementEntity profileElementEntitySetBurnedAttr = new ProfileElementEntity(); + profileElementEntitySetBurnedAttr.setCodename("expression.on.tags"); + profileElementEntitySetBurnedAttr.setName("Set tag BurnedInAnnotation to NO"); + profileElementEntitySetBurnedAttr.addArgument(new ArgumentEntity("expr", "Replace('NO')", profileElementEntityAddBurnedAttr)); + profileElementEntitySetBurnedAttr.addIncludedTag(new IncludedTagEntity("(0028,0301)", profileElementEntityAddBurnedAttr)); + profileElementEntitySetBurnedAttr.setPosition(2); + + profileElementEntities.add(profileElementEntityAddBurnedAttr); + profileElementEntities.add(profileElementEntitySetBurnedAttr); + profileEntity.setProfileElementEntities(profileElementEntities); + Profile profile = new Profile(profileEntity); + + // Apply the profile that adds the BurnedInAnnotation attribute to both objects + profile.applyAction(attributes, attributes, null, null, null, null); + + // The Add action is ignored, the Replace action sets the value to NO + assertEquals("NO", attributes.getString(Tag.BurnedInAnnotation)); + } +}