/*******************************************************************************
* Copyright (c) 2016 Weasis Team and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Nicolas Roduit - initial API and implementation
*******************************************************************************/
package org.weasis.dicom.explorer.pr;
import java.awt.Color;
import java.awt.Shape;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Sequence;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.VR;
import org.dcm4che3.io.DicomOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.weasis.core.api.gui.util.AppProperties;
import org.weasis.core.api.gui.util.GeomUtil;
import org.weasis.core.api.image.util.CIELab;
import org.weasis.core.api.util.GzipManager;
import org.weasis.core.ui.editor.image.dockable.MeasureTool;
import org.weasis.core.ui.model.GraphicModel;
import org.weasis.core.ui.model.ReferencedImage;
import org.weasis.core.ui.model.ReferencedSeries;
import org.weasis.core.ui.model.graphic.Graphic;
import org.weasis.core.ui.model.graphic.GraphicLabel;
import org.weasis.core.ui.model.graphic.imp.AnnotationGraphic;
import org.weasis.core.ui.model.graphic.imp.PointGraphic;
import org.weasis.core.ui.model.graphic.imp.area.EllipseGraphic;
import org.weasis.core.ui.model.graphic.imp.area.PolygonGraphic;
import org.weasis.core.ui.model.graphic.imp.area.RectangleGraphic;
import org.weasis.core.ui.model.graphic.imp.area.ThreePointsCircleGraphic;
import org.weasis.core.ui.model.graphic.imp.line.PolylineGraphic;
import org.weasis.core.ui.model.imp.XmlGraphicModel;
import org.weasis.core.ui.model.layer.GraphicLayer;
import org.weasis.dicom.codec.DcmMediaReader;
import org.weasis.dicom.codec.DicomImageElement;
import org.weasis.dicom.codec.PresentationStateReader;
import org.weasis.dicom.codec.utils.DicomMediaUtils;
public class PrSerializer {
private static final Logger LOGGER = LoggerFactory.getLogger(PrSerializer.class);
private static final String PIXEL = "PIXEL"; //$NON-NLS-1$
private PrSerializer() {
}
public static Attributes writePresentation(GraphicModel model, Attributes parentAttributes, File outputFile,
String seriesInstanceUID, String sopInstanceUID) {
return writePresentation(model, parentAttributes, outputFile, seriesInstanceUID, sopInstanceUID, null);
}
public static Attributes writePresentation(GraphicModel model, Attributes parentAttributes, File outputFile,
String seriesInstanceUID, String sopInstanceUID, Point2D offset) {
Objects.requireNonNull(model);
Objects.requireNonNull(outputFile);
if (parentAttributes != null) {
try {
GraphicModel m = getModelForSerialization(model, offset);
Attributes attributes =
DicomMediaUtils.createDicomPR(parentAttributes, seriesInstanceUID, sopInstanceUID);
writeCommonTags(attributes);
writeReferences(attributes, m, parentAttributes.getString(Tag.SOPClassUID));
writeGraphics(m, attributes);
writePrivateTags(m, attributes);
saveToFile(outputFile, attributes);
return attributes;
} catch (Exception e) {
LOGGER.error("Cannot write Presentation State : ", e); //$NON-NLS-1$
}
}
return null;
}
public static Attributes writePresentation(GraphicModel model, DicomImageElement img, File outputFile,
String seriesInstanceUID, String sopInstanceUID) {
Attributes imgAttributes = img.getMediaReader() instanceof DcmMediaReader
? ((DcmMediaReader) img.getMediaReader()).getDicomObject() : null;
return writePresentation(model, imgAttributes, outputFile, seriesInstanceUID, sopInstanceUID, null);
}
private static GraphicModel getModelForSerialization(GraphicModel model, Point2D offset) {
// Remove non serializable graphics
XmlGraphicModel xmlModel = new XmlGraphicModel();
xmlModel.setReferencedSeries(model.getReferencedSeries());
for (Graphic g : model.getModels()) {
if (g.getLayer().getSerializable() && !g.getPts().isEmpty()) {
if (offset != null) {
Graphic graphic = g.copy();
for (Point2D.Double p : graphic.getPts()) {
p.x -= offset.getX();
p.y -= offset.getY();
}
GraphicLabel label = g.getGraphicLabel();
graphic.buildShape();
if (label != null) {
graphic.setLabel(label);
}
xmlModel.addGraphic(graphic);
} else
xmlModel.addGraphic(g);
}
}
return xmlModel;
}
private static void writePrivateTags(GraphicModel model, Attributes attributes) {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(model.getClass());
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
jaxbMarshaller.marshal(model, outputStream);
// jaxbMarshaller.marshal(model, System.out);
attributes.setString(PresentationStateReader.PRIVATE_CREATOR_TAG, VR.LO,
PresentationStateReader.PR_MODEL_ID);
attributes.setBytes(PresentationStateReader.PR_MODEL_PRIVATE_TAG, VR.OB,
GzipManager.gzipCompressToByte(outputStream.toByteArray()));
} catch (Exception e) {
LOGGER.error("Cannot save xml: ", e); //$NON-NLS-1$
}
}
private static void writeCommonTags(Attributes attributes) {
String gsps = "GSPS"; //$NON-NLS-1$
attributes.setString(Tag.ContentCreatorName, VR.PN, AppProperties.WEASIS_USER);
attributes.setString(Tag.ContentLabel, VR.CS, gsps);
attributes.setString(Tag.ContentDescription, VR.LO, "Description"); //$NON-NLS-1$
attributes.setInt(Tag.SeriesNumber, VR.IS, 999);
try {
attributes.setString(Tag.StationName, VR.SH, InetAddress.getLocalHost().getHostName());
} catch (UnknownHostException e) {
LOGGER.error("Cannot get host name: ", e); //$NON-NLS-1$
}
attributes.setString(Tag.SoftwareVersions, VR.LO, AppProperties.WEASIS_VERSION);
attributes.setString(Tag.SeriesDescription, VR.LO, String.join(" ", AppProperties.WEASIS_NAME, gsps)); //$NON-NLS-1$
}
private static void writeReferences(Attributes attributes, GraphicModel model, String sopClassUID) {
Sequence seriesSeq = attributes.newSequence(Tag.ReferencedSeriesSequence, model.getReferencedSeries().size());
for (ReferencedSeries seriesRef : model.getReferencedSeries()) {
Attributes rfs = new Attributes(2);
rfs.setString(Tag.SeriesInstanceUID, VR.UI, seriesRef.getUuid());
Sequence imageSeq = rfs.newSequence(Tag.ReferencedImageSequence, seriesRef.getImages().size());
for (ReferencedImage imageRef : seriesRef.getImages()) {
Attributes rfi = new Attributes(2);
rfi.setString(Tag.ReferencedSOPClassUID, VR.UI, sopClassUID);
rfi.setString(Tag.ReferencedSOPInstanceUID, VR.UI, imageRef.getUuid());
List<Integer> frames = imageRef.getFrames();
if (frames != null && !frames.isEmpty()) {
int[] arrays = new int[frames.size()];
for (int i = 0; i < arrays.length; i++) {
arrays[i] = frames.get(i).intValue() + 1; // convert to DICOM frame
}
rfi.setInt(Tag.ReferencedFrameNumber, VR.IS, arrays);
}
imageSeq.add(rfi);
}
seriesSeq.add(rfs);
}
}
private static void writeGraphics(GraphicModel model, Attributes attributes) {
List<GraphicLayer> layers = model.getLayers();
Sequence annotationSeq = attributes.newSequence(Tag.GraphicAnnotationSequence, layers.size());
Sequence layerSeq = attributes.newSequence(Tag.GraphicLayerSequence, layers.size());
for (int i = 0; i < layers.size(); i++) {
GraphicLayer layer = layers.get(i);
if (layer.getSerializable()) {
String layerName = layer.getType().name();
List<Graphic> graphics = getGraphicsByLayer(model, layer.getUuid());
Attributes l = new Attributes(2);
l.setString(Tag.GraphicLayer, VR.CS, layerName);
l.setInt(Tag.GraphicLayerOrder, VR.IS, i);
float[] lab = PresentationStateReader
.colorToLAB(Optional.ofNullable(MeasureTool.viewSetting.getLineColor()).orElse(Color.YELLOW));
if (lab != null) {
l.setInt(Tag.GraphicLayerRecommendedDisplayCIELabValue, VR.US, CIELab.convertToDicomLab(lab));
}
l.setString(Tag.GraphicLayerDescription, VR.LO, layer.toString());
layerSeq.add(l);
Attributes a = new Attributes(2);
a.setString(Tag.GraphicLayer, VR.CS, layerName);
Sequence graphicSeq = a.newSequence(Tag.GraphicObjectSequence, graphics.size());
Sequence textSeq = a.newSequence(Tag.TextObjectSequence, graphics.size());
for (Graphic graphic : graphics) {
buildDicomGraphic(graphic, graphicSeq, textSeq);
}
annotationSeq.add(a);
}
}
}
private static List<Graphic> getGraphicsByLayer(GraphicModel model, String layerUid) {
return model.getModels().stream().filter(g -> layerUid.equals(g.getLayer().getUuid()))
.collect(Collectors.toList());
}
private static double[] getGraphicsPoints(List<Point2D.Double> pts) {
double[] list = new double[pts.size() * 2];
for (int i = 0; i < pts.size(); i++) {
Point2D.Double p = pts.get(i);
list[i * 2] = p.x;
list[i * 2 + 1] = p.y;
}
return list;
}
private static Attributes getBasicGraphic(Graphic graphic) {
Attributes dcm = new Attributes(5);
dcm.setString(Tag.GraphicAnnotationUnits, VR.CS, PIXEL);
dcm.setInt(Tag.GraphicDimensions, VR.US, 2);
dcm.setString(Tag.GraphicFilled, VR.CS, graphic.getFilled() ? "Y" : "N"); //$NON-NLS-1$ //$NON-NLS-2$
Sequence style = dcm.newSequence(Tag.LineStyleSequence, 1);
Attributes styles = new Attributes();
styles.setFloat(Tag.LineThickness, VR.FL, graphic.getLineThickness());
if (graphic.getColorPaint() instanceof Color) {
Color color = (Color) graphic.getColorPaint();
float[] rgb = PresentationStateReader.colorToLAB(color);
if (rgb != null) {
styles.setInt(Tag.PatternOnColorCIELabValue, VR.US, CIELab.convertToDicomLab(rgb));
}
}
style.add(styles);
return dcm;
}
private static void buildDicomGraphic(Graphic graphic, Sequence graphicSeq, Sequence textSeq) {
Attributes dcm = getBasicGraphic(graphic);
List<Point2D.Double> pts;
if (graphic instanceof RectangleGraphic) {
boolean ellipse = graphic instanceof EllipseGraphic;
dcm.setString(Tag.GraphicType, VR.CS, ellipse ? PrGraphicUtil.ELLIPSE : PrGraphicUtil.POLYLINE);
RectangleGraphic rg = (RectangleGraphic) graphic;
if (ellipse) {
Point2D.Double wPt = rg.getHandlePoint(RectangleGraphic.eHandlePoint.W.getIndex());
Point2D.Double ePt = rg.getHandlePoint(RectangleGraphic.eHandlePoint.E.getIndex());
Point2D.Double nPt = rg.getHandlePoint(RectangleGraphic.eHandlePoint.N.getIndex());
Point2D.Double sPt = rg.getHandlePoint(RectangleGraphic.eHandlePoint.S.getIndex());
pts = wPt.distance(ePt) > nPt.distance(sPt) ? Arrays.asList(wPt, ePt, nPt, sPt)
: Arrays.asList(nPt, sPt, wPt, ePt);
} else {
pts = Arrays.asList(rg.getHandlePoint(RectangleGraphic.eHandlePoint.NW.getIndex()),
rg.getHandlePoint(RectangleGraphic.eHandlePoint.NE.getIndex()),
rg.getHandlePoint(RectangleGraphic.eHandlePoint.SE.getIndex()),
rg.getHandlePoint(RectangleGraphic.eHandlePoint.SW.getIndex()),
rg.getHandlePoint(RectangleGraphic.eHandlePoint.NW.getIndex()));
}
} else if (graphic instanceof ThreePointsCircleGraphic) {
dcm.setString(Tag.GraphicType, VR.CS, PrGraphicUtil.CIRCLE);
Point2D.Double centerPt = GeomUtil.getCircleCenter(graphic.getPts());
pts = Arrays.asList(centerPt, graphic.getPts().get(0));
} else if (graphic instanceof PolygonGraphic) {
dcm.setString(Tag.GraphicType, VR.CS, PrGraphicUtil.POLYLINE);
pts = graphic.getPts();
pts.add(pts.get(0));
} else if (graphic instanceof PolylineGraphic) {
dcm.setString(Tag.GraphicType, VR.CS, PrGraphicUtil.POLYLINE);
pts = graphic.getPts();
} else if (graphic instanceof PointGraphic) {
dcm.setString(Tag.GraphicType, VR.CS, PrGraphicUtil.POINT);
pts = Arrays.asList(graphic.getPts().get(0));
} else if (graphic instanceof AnnotationGraphic) {
AnnotationGraphic g = (AnnotationGraphic) graphic;
Attributes attributes = bluildLabelAndAnchor(g);
textSeq.add(attributes);
return;
} else {
transformShapeToContour(graphic, graphicSeq);
bluildLabel(graphic, textSeq);
return;
}
dcm.setDouble(Tag.GraphicData, VR.FL, getGraphicsPoints(pts));
dcm.setInt(Tag.NumberOfGraphicPoints, VR.US, pts.size());
graphicSeq.add(dcm);
bluildLabel(graphic, textSeq);
}
private static void bluildLabel(Graphic graphic, Sequence textSeq) {
if (graphic.getLabelVisible()) {
GraphicLabel label = graphic.getGraphicLabel();
if (label != null) {
Rectangle2D bound = label.getTransformedBounds(null);
Attributes text =
bluildLabelAndBounds(bound, Arrays.stream(label.getLabels()).collect(Collectors.joining("\r\n"))); //$NON-NLS-1$
textSeq.add(text);
}
}
}
private static Attributes bluildLabelAndAnchor(AnnotationGraphic g) {
Rectangle2D bound = g.getLabelBounds();
Point2D anchor = g.getAnchorPoint();
String text = Arrays.stream(g.getLabels()).collect(Collectors.joining("\r\n")); //$NON-NLS-1$
Attributes attributes = new Attributes(7);
attributes.setString(Tag.BoundingBoxAnnotationUnits, VR.CS, PIXEL);
attributes.setFloat(Tag.AnchorPoint, VR.FL, (float) anchor.getX(), (float) anchor.getY());
attributes.setString(Tag.AnchorPointVisibility, VR.CS, "Y"); //$NON-NLS-1$
Sequence style = attributes.newSequence(Tag.LineStyleSequence, 1);
Attributes styles = new Attributes();
styles.setFloat(Tag.LineThickness, VR.FL, g.getLineThickness());
if (g.getColorPaint() instanceof Color) {
Color color = (Color) g.getColorPaint();
float[] rgb = PresentationStateReader.colorToLAB(color);
if (rgb != null) {
styles.setInt(Tag.PatternOnColorCIELabValue, VR.US, CIELab.convertToDicomLab(rgb));
}
}
style.add(styles);
attributes.setDouble(Tag.BoundingBoxTopLeftHandCorner, VR.FL,
new double[] { bound.getMinX(), bound.getMinY() });
attributes.setDouble(Tag.BoundingBoxBottomRightHandCorner, VR.FL,
new double[] { bound.getMaxX(), bound.getMaxY() });
// In text strings (value representation ST, LT, or UT) a new line shall be represented as CR LF.
// see http://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_6.html
attributes.setString(Tag.UnformattedTextValue, VR.ST, text);
return attributes;
}
private static Attributes bluildLabelAndBounds(Rectangle2D bound, String text) {
Attributes attributes = new Attributes(5);
attributes.setString(Tag.BoundingBoxAnnotationUnits, VR.CS, PIXEL);
attributes.setDouble(Tag.BoundingBoxTopLeftHandCorner, VR.FL,
new double[] { bound.getMinX(), bound.getMinY() });
attributes.setDouble(Tag.BoundingBoxBottomRightHandCorner, VR.FL,
new double[] { bound.getMaxX(), bound.getMaxY() });
// In text strings (value representation ST, LT, or UT) a new line shall be represented as CR LF.
// see http://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_6.html
attributes.setString(Tag.UnformattedTextValue, VR.ST, text);
return attributes;
}
public static void transformShapeToContour(Graphic graphic, Sequence graphicSeq) {
Shape shape = graphic.getShape();
Attributes dcm = null;
List<Point2D.Double> points = new ArrayList<>();
PathIterator iterator = new FlatteningPathIterator(shape.getPathIterator(null), 2);
double[] pts = new double[6];
while (!iterator.isDone()) {
int segType = iterator.currentSegment(pts);
switch (segType) {
case PathIterator.SEG_MOVETO:
addNewSubGraphic(dcm, graphicSeq, points);
dcm = getBasicGraphic(graphic);
points.add(new Point2D.Double(pts[0], pts[1]));
break;
case PathIterator.SEG_LINETO:
case PathIterator.SEG_CLOSE:
points.add(new Point2D.Double(pts[0], pts[1]));
break;
case PathIterator.SEG_CUBICTO:
case PathIterator.SEG_QUADTO:
default:
break; // should never append with FlatteningPathIterator
}
iterator.next();
}
addNewSubGraphic(dcm, graphicSeq, points);
}
private static void addNewSubGraphic(Attributes dcm, Sequence graphicSeq, List<Point2D.Double> points) {
if (dcm != null && dcm.getParent() == null) {
dcm.setString(Tag.GraphicType, VR.CS, PrGraphicUtil.POLYLINE);
dcm.setDouble(Tag.GraphicData, VR.FL, getGraphicsPoints(points));
dcm.setInt(Tag.NumberOfGraphicPoints, VR.US, points.size());
graphicSeq.add(dcm);
points.clear();
}
}
private static boolean saveToFile(File output, Attributes dcm) {
if (dcm != null) {
try (DicomOutputStream out = new DicomOutputStream(output)) {
out.writeDataset(dcm.createFileMetaInformation(UID.ImplicitVRLittleEndian), dcm);
return true;
} catch (IOException e) {
LOGGER.error("Cannot write dicom PR: {}", e); //$NON-NLS-1$
}
}
return false;
}
}