001/* ***** BEGIN LICENSE BLOCK *****
002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
003 *
004 * The contents of this file are subject to the Mozilla Public License Version
005 * 1.1 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 * http://www.mozilla.org/MPL/
008 *
009 * Software distributed under the License is distributed on an "AS IS" basis,
010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
011 * for the specific language governing rights and limitations under the
012 * License.
013 *
014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
015 * Java(TM), hosted at http://sourceforge.net/projects/dcm4che.
016 *
017 * The Initial Developer of the Original Code is
018 * Gunter Zeilinger, Huetteldorferstr. 24/10, 1150 Vienna/Austria/Europe.
019 * Portions created by the Initial Developer are Copyright (C) 2017
020 * the Initial Developer. All Rights Reserved.
021 *
022 * Contributor(s):
023 * Gunter Zeilinger <gunterze@gmail.com>
024 *
025 * Alternatively, the contents of this file may be used under the terms of
026 * either the GNU General Public License Version 2 or later (the "GPL"), or
027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
028 * in which case the provisions of the GPL or the LGPL are applicable instead
029 * of those above. If you wish to allow use of your version of this file only
030 * under the terms of either the GPL or the LGPL, and not to allow others to
031 * use your version of this file under the terms of the MPL, indicate your
032 * decision by deleting the provisions above and replace them with the notice
033 * and other provisions required by the GPL or the LGPL. If you do not delete
034 * the provisions above, a recipient may use your version of this file under
035 * the terms of any one of the MPL, the GPL or the LGPL.
036 *
037 * ***** END LICENSE BLOCK ***** */
038
039package org.dcm4che3.tool.jpg2dcm;
040
041import java.io.File;
042import java.io.BufferedInputStream;
043import java.io.FileInputStream;
044import java.io.IOException;
045import java.util.Arrays;
046import java.util.Date;
047import java.util.ResourceBundle;
048
049import com.sun.imageio.plugins.jpeg.JPEG;
050import org.apache.commons.cli.CommandLine;
051import org.apache.commons.cli.Options;
052import org.apache.commons.cli.OptionBuilder;
053import org.apache.commons.cli.MissingArgumentException;
054import org.apache.commons.cli.ParseException;
055
056import org.dcm4che3.data.Attributes;
057import org.dcm4che3.data.Tag;
058import org.dcm4che3.data.UID;
059import org.dcm4che3.data.VR;
060import org.dcm4che3.imageio.codec.jpeg.JPEGHeader;
061import org.dcm4che3.imageio.codec.mpeg.MPEGHeader;
062import org.dcm4che3.io.DicomOutputStream;
063import org.dcm4che3.io.SAXReader;
064import org.dcm4che3.tool.common.CLIUtils;
065import org.dcm4che3.util.StreamUtils;
066import org.dcm4che3.util.UIDUtils;
067import org.slf4j.Logger;
068import org.slf4j.LoggerFactory;
069import org.xml.sax.SAXException;
070
071import javax.xml.parsers.ParserConfigurationException;
072
073/**
074 * @author gunter zeilinger<gunterze@gmail.com>
075 * @author Hesham Elbadawi <bsdreko@gmail.com>
076 * @author Vrinda Nayak <vrinda.nayak@j4care.com>
077 * @since May 2017
078 */
079public class Jpg2Dcm {
080
081    static final Logger LOG = LoggerFactory.getLogger(Jpg2Dcm.class);
082
083    private String charset = "ISO_IR 100";
084
085    private int fileLen;
086
087    private boolean noAPPn;
088
089    private static Options opts;
090    private static ResourceBundle rb = ResourceBundle
091            .getBundle("org.dcm4che3.tool.jpg2dcm.messages");
092    private Attributes keys;
093    private String metadataFile;
094    private JPEGHeader jpegHeader;
095    private File pixelDataFile;
096    private byte[] pixelData;
097
098    public Jpg2Dcm() {
099    }
100
101    public void convert(Attributes metadata, Jpg2Dcm jpg2Dcm, File dcmFile)
102            throws Exception {
103        File pixelDataFile = jpg2Dcm.pixelDataFile;
104        fileLen = (int) pixelDataFile.length();
105        DicomOutputStream dos = null;
106        try {
107            dos = new DicomOutputStream(dcmFile);
108            Date now = new Date();
109            metadata.setDate(Tag.InstanceCreationDate, VR.DA, now);
110            metadata.setDate(Tag.InstanceCreationTime, VR.TM, now);
111            Attributes fmi = metadata.createFileMetaInformation(metadata.getString(Tag.TransferSyntaxUID));
112            dos.writeDataset(fmi, metadata);
113            dos.writeHeader(Tag.PixelData, VR.OB, -1);
114            dos.writeHeader(Tag.Item, null, 0);
115            if (noAPPn && jpg2Dcm.jpegHeader != null) {
116                int off = jpg2Dcm.jpegHeader.offsetAfterAPP();
117                dos.writeHeader(Tag.Item, null, fileLen - off + 3);
118                dos.write((byte) -1);
119                dos.write((byte) JPEG.SOI);
120                dos.write((byte) -1);
121                dos.write(jpg2Dcm.pixelData);
122            } else {
123                dos.writeHeader(Tag.Item, null, (fileLen + 1) & ~1);
124                dos.write(jpg2Dcm.pixelData);
125            }
126            if ((fileLen & 1) != 0) {
127                dos.write(0);
128            }
129            dos.writeHeader(Tag.SequenceDelimitationItem, null, 0);
130        } finally {
131            if (dos != null)
132                dos.close();
133        }
134    }
135
136    public static void main(String[] args) {
137        try {
138            CommandLine cl = parseCommandLine(args);
139            Jpg2Dcm jpg2Dcm = new Jpg2Dcm();
140            jpg2Dcm.keys = configureKeys(jpg2Dcm, cl);
141            LOG.info("added keys for coercion: \n" + jpg2Dcm.keys.toString());
142            jpg2Dcm.metadataFile = cl.getOptionValue("f");
143            if (cl.getArgs().length < 2)
144                throw new MissingArgumentException("Either input pixel data file or output binary file is missing. See example in jpg2dcm help.");
145            jpg2Dcm.pixelDataFile = new File(cl.getArgs()[0]);
146            Extension ext = getExt(jpg2Dcm.pixelDataFile.getName());
147            jpg2Dcm.noAPPn = Boolean.valueOf(cl.getOptionValue("na"));
148            Attributes metadata = getMetadata(jpg2Dcm, ext);
149
150            @SuppressWarnings("rawtypes")
151            File dcmFile = new File(cl.getArgs()[1]);
152            long start = System.currentTimeMillis();
153            jpg2Dcm.convert(metadata, jpg2Dcm, dcmFile);
154            long fin = System.currentTimeMillis();
155            LOG.info("Encapsulated " + jpg2Dcm.pixelDataFile + " to " + dcmFile.getPath() + " in "
156                    + (fin - start) + "ms.");
157        } catch (ParseException e) {
158            System.err.println("jpg2dcm: " + e.getMessage());
159            System.err.println(rb.getString("try"));
160            System.exit(2);
161        } catch (Exception e) {
162            LOG.error("Error: \n", e);
163            e.printStackTrace();
164            System.exit(2);
165        }
166    }
167
168    private static CommandLine parseCommandLine(String[] args)
169            throws ParseException {
170        opts = new Options();
171        opts.addOption(OptionBuilder.hasArgs(2).withArgName("[seq/]attr=value")
172                .withValueSeparator().withDescription(rb.getString("metadata"))
173                .create("m"));
174        opts.addOption("f", "file", true, rb.getString("file"));
175        opts.addOption("na","no-appn", true, rb.getString("no-appn"));
176        CLIUtils.addCommonOptions(opts);
177        return CLIUtils.parseComandLine(args, opts, rb, Jpg2Dcm.class);
178    }
179
180    private static Attributes configureKeys(Jpg2Dcm main, CommandLine cl) {
181        Attributes temp = new Attributes();
182        CLIUtils.addAttributes(temp, cl.getOptionValues("m"));
183        return temp;
184    }
185
186    private static Attributes getMetadata(Jpg2Dcm jpg2Dcm, Extension ext)
187            throws Exception {
188        Attributes metadata = defaultMetadata();
189        String metadataFile = jpg2Dcm.metadataFile;
190        if (metadataFile != null) {
191            String metadataFileExt = metadataFile.substring(metadataFile.lastIndexOf(".")+1).toLowerCase();
192            if (!metadataFileExt.equals("xml"))
193                throw new IllegalArgumentException("Metadata file extension not supported. Read -f option in jpg2dcm help");
194            File filePath = new File(metadataFile);
195            metadata = SAXReader.parse(filePath.toString());
196        }
197        coerceAttributes(metadata, jpg2Dcm);
198        switch (ext) {
199            case jpg:
200            case jpeg:
201                metadata.setString(Tag.SOPClassUID, VR.UI, metadata.getString(Tag.SOPClassUID, UID.SecondaryCaptureImageStorage));
202                metadata.setString(Tag.TransferSyntaxUID, VR.UI, metadata.getString(Tag.TransferSyntaxUID, UID.JPEGBaseline1));
203                readPixelHeader(jpg2Dcm, metadata, jpg2Dcm.pixelDataFile, false);
204                break;
205            case mpg:
206            case mpeg:
207            case mpg2:
208                metadata.setString(Tag.SOPClassUID, VR.UI, metadata.getString(Tag.SOPClassUID, UID.VideoPhotographicImageStorage));
209                metadata.setString(Tag.TransferSyntaxUID, VR.UI, metadata.getString(Tag.TransferSyntaxUID, UID.MPEG2));
210                readPixelHeader(jpg2Dcm, metadata, jpg2Dcm.pixelDataFile, true);
211                break;
212        }
213        return metadata;
214    }
215
216    private static Attributes defaultMetadata() throws ParserConfigurationException, SAXException, IOException {
217        LOG.info("Always first set default metadata in the event of required attributes not set/sent by user.");
218        Attributes metadata = new Attributes();
219        metadata.setString(Tag.PatientName, VR.PN, "JPG2DCM-PatientName");
220        metadata.setString(Tag.StudyInstanceUID, VR.UI, UIDUtils.createUID());
221        metadata.setString(Tag.SeriesInstanceUID, VR.UI, UIDUtils.createUID());
222        metadata.setString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID());
223        return metadata;
224    }
225
226    private static void coerceAttributes(Attributes metadata, Jpg2Dcm jpg2Dcm) {
227        if (jpg2Dcm.keys.tags().length > 0)
228            LOG.info("Coercing the following keys from specified attributes to metadata:");
229        metadata.addAll(jpg2Dcm.keys);
230        metadata.setString(Tag.SpecificCharacterSet, VR.CS, metadata.getString(Tag.SpecificCharacterSet, jpg2Dcm.charset));
231        LOG.info(jpg2Dcm.keys.toString());
232    }
233
234    enum Extension {
235        mpg, mpg2, mpeg, jpeg, jpg
236    }
237    private static Extension getExt(String file) {
238        String fileExt = file.substring(file.lastIndexOf(".")+1).toLowerCase();
239        for (Extension ext : Extension.values())
240            if (ext.name().equals(fileExt))
241                return ext;
242        throw new IllegalArgumentException("Specified file for conversion is not Pixel Data Type. Read jpg2dcm help for supported extension types.");
243    }
244
245    private static void readPixelHeader(Jpg2Dcm jpg2Dcm, Attributes metadata, File pixelDataFile, boolean isMpeg)
246            throws Exception {
247        int fileLen = (int) pixelDataFile.length();
248        FileInputStream fis = null;
249        BufferedInputStream bis = null;
250        try {
251            fis = new FileInputStream(pixelDataFile);
252            bis = new BufferedInputStream(fis);
253            byte[] b16384 = new byte[16384];
254            StreamUtils.readFully(bis, b16384, 0, 16384);
255            jpg2Dcm.pixelData = new byte[fileLen];
256            if (isMpeg) {
257                MPEGHeader mpegHeader = new MPEGHeader(b16384);
258                mpegHeader.toAttributes(metadata, pixelDataFile.length());
259                jpg2Dcm.pixelData = Arrays.copyOf(b16384, fileLen);
260                StreamUtils.readFully(bis, jpg2Dcm.pixelData, 16384, fileLen-16384);
261            } else {
262                jpg2Dcm.jpegHeader = new JPEGHeader(b16384, JPEG.SOS);
263                jpg2Dcm.jpegHeader.toAttributes(metadata);
264                if (jpg2Dcm.noAPPn) {
265                    int off = jpg2Dcm.jpegHeader.offsetAfterAPP();
266                    byte[] bTemp = Arrays.copyOf(b16384, fileLen);
267                    StreamUtils.readFully(bis, bTemp, 16384, fileLen - 16384);
268                    jpg2Dcm.pixelData = Arrays.copyOfRange(bTemp, off, fileLen);
269                } else {
270                    jpg2Dcm.pixelData = Arrays.copyOf(b16384, fileLen);
271                    StreamUtils.readFully(bis, jpg2Dcm.pixelData, 16384, fileLen - 16384);
272                }
273            }
274        } finally {
275            if (bis != null)
276                bis.close();
277            if (fis != null)
278                fis.close();
279        }
280    }
281
282}