Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New profile action : add tag #230

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions src/main/java/org/karnak/backend/enums/ProfileItemType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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<? extends ProfileItem> profileClass;

Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/karnak/backend/model/action/Add.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
99 changes: 99 additions & 0 deletions src/main/java/org/karnak/backend/model/profiles/AddTag.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
35 changes: 23 additions & 12 deletions src/main/java/org/karnak/backend/service/profilepipe/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand All @@ -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()
Expand Down
183 changes: 183 additions & 0 deletions src/test/java/org/karnak/backend/model/profiles/AddTagTest.java
Original file line number Diff line number Diff line change
@@ -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<ProfileElementEntity> 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<ProfileElementEntity> 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<ProfileElementEntity> 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<ProfileElementEntity> 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<ProfileElementEntity> 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));
}
}