/******************************************************************************* * 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.print; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferShort; import java.awt.image.DataBufferUShort; import java.awt.image.RenderedImage; import java.awt.image.WritableRaster; import java.io.IOException; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Executors; 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.net.ApplicationEntity; import org.dcm4che3.net.Association; import org.dcm4che3.net.Connection; import org.dcm4che3.net.Device; import org.dcm4che3.net.DimseRSP; import org.dcm4che3.net.pdu.AAssociateRQ; import org.dcm4che3.net.pdu.PresentationContext; import org.dcm4che3.util.UIDUtils; import org.weasis.core.api.gui.util.MathUtil; import org.weasis.core.api.image.LayoutConstraints; import org.weasis.core.api.image.ZoomOp; import org.weasis.core.api.media.data.ImageElement; import org.weasis.core.api.service.BundleTools; import org.weasis.core.ui.editor.image.ExportImage; import org.weasis.core.ui.util.ExportLayout; import org.weasis.core.ui.util.ImagePrint; import org.weasis.core.ui.util.PrintOptions; import org.weasis.dicom.explorer.pref.node.DicomPrintNode; import org.weasis.dicom.explorer.print.DicomPrintDialog.FilmSize; public class DicomPrint { private final DicomPrintNode dcmNode; private final DicomPrintOptions printOptions; private int interpolation; private double placeholderX; private double placeholderY; private int lastx; private double lastwx; private double[] lastwy; private double wx; public DicomPrint(DicomPrintNode dicomPrintNode, DicomPrintOptions printOptions) { if (dicomPrintNode == null) { throw new IllegalArgumentException(); } this.dcmNode = dicomPrintNode; this.printOptions = printOptions == null ? dicomPrintNode.getPrintOptions() : printOptions; } public BufferedImage printImage(ExportLayout<? extends ImageElement> layout) { if (layout == null) { return null; } BufferedImage bufferedImage = initialize(layout); Graphics2D g2d = (Graphics2D) bufferedImage.getGraphics(); if (g2d != null) { Color borderColor = "WHITE".equals(printOptions.getBorderDensity()) ? Color.WHITE : Color.BLACK; //$NON-NLS-1$ Color background = "WHITE".equals(printOptions.getEmptyDensity()) ? Color.WHITE : Color.BLACK; //$NON-NLS-1$ g2d.setBackground(background); if (!Color.BLACK.equals(background)) { // Change background color g2d.clearRect(0, 0, bufferedImage.getWidth(), bufferedImage.getHeight()); } final Map<LayoutConstraints, Component> elements = layout.getLayoutModel().getConstraints(); Iterator<Entry<LayoutConstraints, Component>> enumVal = elements.entrySet().iterator(); while (enumVal.hasNext()) { Entry<LayoutConstraints, Component> e = enumVal.next(); LayoutConstraints key = e.getKey(); Component value = e.getValue(); ExportImage<? extends ImageElement> image = null; Point2D.Double pad = new Point2D.Double(0.0, 0.0); if (value instanceof ExportImage) { image = (ExportImage) value; formatImage(image, key, pad); } if (key.gridx == 0) { wx = 0.0; } else if (lastx < key.gridx) { wx += lastwx; } double wy = lastwy[key.gridx]; double x = 5 + (placeholderX * wx) + (MathUtil.isEqualToZero(wx) ? 0 : key.gridx * 5) + pad.x; double y = 5 + (placeholderY * wy) + (MathUtil.isEqualToZero(wy) ? 0 : key.gridy * 5) + pad.y; lastx = key.gridx; lastwx = key.weightx; for (int i = key.gridx; i < key.gridx + key.gridwidth; i++) { lastwy[i] += key.weighty; } if (image != null) { boolean wasBuffered = ImagePrint.disableDoubleBuffering(image); // Set us to the upper left corner g2d.translate(x, y); g2d.setClip(image.getBounds()); image.draw(g2d); ImagePrint.restoreDoubleBuffering(image, wasBuffered); g2d.translate(-x, -y); if (!borderColor.equals(background)) { // Change background color g2d.setClip(null); g2d.setColor(borderColor); g2d.setStroke(new BasicStroke(2)); Dimension viewSize = image.getSize(); g2d.drawRect((int) x - 1, (int) y - 1, viewSize.width + 1, viewSize.height + 1); } } } } return bufferedImage; } private BufferedImage initialize(ExportLayout<? extends ImageElement> layout) { Dimension dimGrid = layout.getLayoutModel().getGridSize(); FilmSize filmSize = printOptions.getFilmSizeId(); PrintOptions.DotPerInches dpi = printOptions.getDpi(); int width = filmSize.getWidth(dpi); int height = filmSize.getHeight(dpi); if ("LANDSCAPE".equals(printOptions.getFilmOrientation())) { //$NON-NLS-1$ int tmp = width; width = height; height = tmp; } String mType = printOptions.getMagnificationType(); interpolation = 1; if ("REPLICATE".equals(mType)) { //$NON-NLS-1$ interpolation = 0; } else if ("CUBIC".equals(mType)) { //$NON-NLS-1$ interpolation = 2; } // Printable size placeholderX = width - (dimGrid.width + 1) * 5.0; placeholderY = height - (dimGrid.height + 1) * 5.0; lastx = 0; lastwx = 0.0; lastwy = new double[dimGrid.width]; wx = 0.0; if (printOptions.isColorPrint()) { return createRGBBufferedImage(width, height); } else { return createGrayBufferedImage(width, height); } } private void formatImage(ExportImage<? extends ImageElement> image, LayoutConstraints key, Point2D.Double pad) { if (!printOptions.isShowingAnnotations() && image.getInfoLayer().getVisible()) { image.getInfoLayer().setVisible(false); } Rectangle2D originSize = (Rectangle2D) image.getActionValue("origin.image.bound"); //$NON-NLS-1$ Point2D originCenter = (Point2D) image.getActionValue("origin.center"); //$NON-NLS-1$ Double originZoom = (Double) image.getActionValue("origin.zoom"); //$NON-NLS-1$ RenderedImage img = image.getSourceImage(); if (img != null && originCenter != null && originZoom != null) { boolean bestfit = originZoom <= 0.0; double canvasWidth; double canvasHeight; if (bestfit || originSize == null) { canvasWidth = img.getWidth() * image.getImage().getRescaleX(); canvasHeight = img.getHeight() * image.getImage().getRescaleY(); } else { canvasWidth = originSize.getWidth() / originZoom; canvasHeight = originSize.getHeight() / originZoom; } double scaleCanvas = Math.min(placeholderX * key.weightx / canvasWidth, placeholderY * key.weighty / canvasHeight); // Set the print area in pixel double cw = canvasWidth * scaleCanvas; double ch = canvasHeight * scaleCanvas; image.setSize((int) (cw + 0.5), (int) (ch + 0.5)); if (printOptions.isCenter()) { pad.x = (placeholderX * key.weightx - cw) * 0.5; pad.y = (placeholderY * key.weighty - ch) * 0.5; } else { pad.x = 0.0; pad.y = 0.0; } image.getDisplayOpManager().setParamValue(ZoomOp.OP_NAME, ZoomOp.P_INTERPOLATION, interpolation); double scaleFactor = Math.min(cw / canvasWidth, ch / canvasHeight); // Resize in best fit window image.zoom(scaleFactor); if (bestfit) { image.center(); } else { image.setCenter(originCenter.getX(), originCenter.getY()); } } } /** * Creates a BufferedImage with a custom color model that can be used to store 3 channel RGB data in a byte array * data buffer */ public static BufferedImage createRGBBufferedImage(int destWidth, int destHeight) { ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); ColorModel cm = new ComponentColorModel(cs, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); WritableRaster r = cm.createCompatibleWritableRaster(destWidth, destHeight); return new BufferedImage(cm, r, false, null); } public static BufferedImage createGrayBufferedImage(int destWidth, int destHeight) { ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); ColorModel cm = new ComponentColorModel(cs, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); WritableRaster r = cm.createCompatibleWritableRaster(destWidth, destHeight); return new BufferedImage(cm, r, false, null); } public void printImage(BufferedImage image) throws Exception { Attributes filmSessionAttrs = new Attributes(); Attributes filmBoxAttrs = new Attributes(); Attributes imageBoxAttrs = new Attributes(); Attributes dicomImage = new Attributes(); final String printManagementSOPClass = printOptions.isColorPrint() ? UID.BasicColorPrintManagementMetaSOPClass : UID.BasicGrayscalePrintManagementMetaSOPClass; final String imageBoxSOPClass = printOptions.isColorPrint() ? UID.BasicColorImageBoxSOPClass : UID.BasicGrayscaleImageBoxSOPClass; storeRasterInDicom(image, dicomImage, printOptions.isColorPrint()); // writeDICOM(new File("/tmp/print.dcm"), dicomImage); String weasisAet = BundleTools.SYSTEM_PREFERENCES.getProperty("weasis.aet", "WEASIS_AE"); //$NON-NLS-1$ //$NON-NLS-2$ Device device = new Device(weasisAet); ApplicationEntity ae = new ApplicationEntity(weasisAet); Connection conn = new Connection(); ApplicationEntity remoteAE = new ApplicationEntity(dcmNode.getAeTitle()); Connection remoteConn = new Connection(); ae.addConnection(conn); ae.setAssociationInitiator(true); ae.setAETitle(weasisAet); remoteConn.setPort(dcmNode.getPort()); remoteConn.setHostname(dcmNode.getHostname()); remoteConn.setSocketCloseDelay(90); remoteAE.setAssociationAcceptor(true); remoteAE.addConnection(remoteConn); device.addConnection(conn); device.addApplicationEntity(ae); ae.addConnection(conn); device.setExecutor(Executors.newSingleThreadExecutor()); device.setScheduledExecutor(Executors.newSingleThreadScheduledExecutor()); filmSessionAttrs.setInt(Tag.NumberOfCopies, VR.IS, printOptions.getNumOfCopies()); filmSessionAttrs.setString(Tag.PrintPriority, VR.CS, printOptions.getPriority()); filmSessionAttrs.setString(Tag.MediumType, VR.CS, printOptions.getMediumType()); filmSessionAttrs.setString(Tag.FilmDestination, VR.CS, printOptions.getFilmDestination()); filmBoxAttrs.setString(Tag.FilmSizeID, VR.CS, printOptions.getFilmSizeId().toString()); filmBoxAttrs.setString(Tag.FilmOrientation, VR.CS, printOptions.getFilmOrientation()); filmBoxAttrs.setString(Tag.MagnificationType, VR.CS, printOptions.getMagnificationType()); filmBoxAttrs.setString(Tag.SmoothingType, VR.CS, printOptions.getSmoothingType()); filmBoxAttrs.setString(Tag.Trim, VR.CS, printOptions.getTrim()); filmBoxAttrs.setString(Tag.BorderDensity, VR.CS, printOptions.getBorderDensity()); filmBoxAttrs.setInt(Tag.MinDensity, VR.US, printOptions.getMinDensity()); filmBoxAttrs.setInt(Tag.MaxDensity, VR.US, printOptions.getMaxDensity()); filmBoxAttrs.setString(Tag.ImageDisplayFormat, VR.ST, printOptions.getImageDisplayFormat()); imageBoxAttrs.setInt(Tag.ImageBoxPosition, VR.US, 1); Sequence seq = imageBoxAttrs.ensureSequence( printOptions.isColorPrint() ? Tag.BasicColorImageSequence : Tag.BasicGrayscaleImageSequence, 1); seq.add(dicomImage); final String filmSessionUID = UIDUtils.createUID(); final String filmBoxUID = UIDUtils.createUID(); Attributes filmSessionSequenceObject = new Attributes(); filmSessionSequenceObject.setString(Tag.ReferencedSOPClassUID, VR.UI, UID.BasicFilmSessionSOPClass); filmSessionSequenceObject.setString(Tag.ReferencedSOPInstanceUID, VR.UI, filmSessionUID); seq = filmBoxAttrs.ensureSequence(Tag.ReferencedFilmSessionSequence, 1); seq.add(filmSessionSequenceObject); AAssociateRQ rq = new AAssociateRQ(); rq.addPresentationContext(new PresentationContext(1, printManagementSOPClass, UID.ImplicitVRLittleEndian)); rq.setCallingAET(ae.getAETitle()); rq.setCalledAET(remoteAE.getAETitle()); Association as = ae.connect(remoteConn, rq); try { // Create a Basic Film Session dimseRSPHandler(as.ncreate(printManagementSOPClass, UID.BasicFilmSessionSOPClass, filmSessionUID, filmSessionAttrs, UID.ImplicitVRLittleEndian)); // Create a Basic Film Box. We need to get the Image Box UID from the response DimseRSP ncreateFilmBoxRSP = as.ncreate(printManagementSOPClass, UID.BasicFilmBoxSOPClass, filmBoxUID, filmBoxAttrs, UID.ImplicitVRLittleEndian); dimseRSPHandler(ncreateFilmBoxRSP); ncreateFilmBoxRSP.next(); Attributes imageBoxSequence = ncreateFilmBoxRSP.getDataset().getNestedDataset(Tag.ReferencedImageBoxSequence); // Send N-SET message with the Image Box dimseRSPHandler(as.nset(printManagementSOPClass, imageBoxSOPClass, imageBoxSequence.getString(Tag.ReferencedSOPInstanceUID), imageBoxAttrs, UID.ImplicitVRLittleEndian)); // Send N-ACTION message with the print action dimseRSPHandler(as.naction(printManagementSOPClass, UID.BasicFilmBoxSOPClass, filmBoxUID, 1, null, UID.ImplicitVRLittleEndian)); // The print action ends here. This will only delete the Film Box and Film Session as.ndelete(printManagementSOPClass, UID.BasicFilmBoxSOPClass, filmBoxUID); as.ndelete(printManagementSOPClass, UID.BasicFilmSessionSOPClass, filmSessionUID); } finally { if (as != null && as.isReadyForDataTransfer()) { as.waitForOutstandingRSP(); as.release(); } } } private void dimseRSPHandler(DimseRSP response) throws IOException, InterruptedException { response.next(); Attributes command = response.getCommand(); if (command.getInt(Tag.Status, 0) != 0) { throw new IOException("Unable to print the image. DICOM response status: " + command.getInt(Tag.Status, 0)); //$NON-NLS-1$ } } public static void storeRasterInDicom(BufferedImage image, Attributes dcmObj, Boolean printInColor) { byte[] bytesOut = null; if (dcmObj != null && image != null) { dcmObj.setInt(Tag.Columns, VR.US, image.getWidth()); dcmObj.setInt(Tag.Rows, VR.US, image.getHeight()); dcmObj.setInt(Tag.PixelRepresentation, VR.US, 0); dcmObj.setString(Tag.PhotometricInterpretation, VR.CS, printInColor ? "RGB" : "MONOCHROME2"); //$NON-NLS-1$ //$NON-NLS-2$ dcmObj.setInt(Tag.SamplesPerPixel, VR.US, printInColor ? 3 : 1); dcmObj.setInt(Tag.BitsAllocated, VR.US, 8); dcmObj.setInt(Tag.BitsStored, VR.US, 8); dcmObj.setInt(Tag.HighBit, VR.US, 7); // Assumed that the displayed image has always an 1/1 aspect ratio. dcmObj.setInt(Tag.PixelAspectRatio, VR.IS, 1, 1); // Issue with some PrintSCP servers // dcmObj.putString(Tag.TransferSyntaxUID, VR.UI, UID.ImplicitVRLittleEndian); DataBuffer dataBuffer; if (printInColor) { // Must be PixelInterleavedSampleModel dcmObj.setInt(Tag.PlanarConfiguration, VR.US, 0); dataBuffer = image.getRaster().getDataBuffer(); } else { dataBuffer = convertRGBImageToMonochrome(image).getRaster().getDataBuffer(); } if (dataBuffer instanceof DataBufferByte) { bytesOut = ((DataBufferByte) dataBuffer).getData(); } else if (dataBuffer instanceof DataBufferShort || dataBuffer instanceof DataBufferUShort) { short[] data = dataBuffer instanceof DataBufferShort ? ((DataBufferShort) dataBuffer).getData() : ((DataBufferUShort) dataBuffer).getData(); bytesOut = new byte[data.length * 2]; for (int i = 0; i < data.length; i++) { bytesOut[i * 2] = (byte) (data[i] & 0xFF); bytesOut[i * 2 + 1] = (byte) ((data[i] >>> 8) & 0xFF); } } dcmObj.setBytes(Tag.PixelData, VR.OW, bytesOut); } } private static BufferedImage convertRGBImageToMonochrome(BufferedImage colorImage) { if (colorImage.getType() == BufferedImage.TYPE_BYTE_GRAY) { return colorImage; } BufferedImage image = new BufferedImage(colorImage.getWidth(), colorImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY); Graphics g = image.getGraphics(); g.drawImage(colorImage, 0, 0, null); g.dispose(); return image; } }