org.dcm4che2.tool.fixjpegls.FixJpegLS.java Source code

Java tutorial

Introduction

Here is the source code for org.dcm4che2.tool.fixjpegls.FixJpegLS.java

Source

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
 * Java(TM), hosted at http://sourceforge.net/projects/dcm4che.
 *
 * The Initial Developer of the Original Code is
 * Agfa HealthCare.
 * Portions created by the Initial Developer are Copyright (C) 2010
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 * See listed authors below.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.dcm4che2.tool.fixjpegls;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.dcm4che2.data.DicomObject;
import org.dcm4che2.data.Tag;
import org.dcm4che2.data.UID;
import org.dcm4che2.data.UIDDictionary;
import org.dcm4che2.io.DicomInputHandler;
import org.dcm4che2.io.DicomInputStream;
import org.dcm4che2.util.ByteUtils;

/**
 * @author Gunter Zeilinger <gunterze@gmail.com>
 * @version $Rev$ $Date:: 0000-00-00 $
 * @since Oct 22, 2010
 */
public class FixJpegLS {

    private static final String USAGE = "fixjpegls [Options] SOURCE DEST";
    private static final String DESCRIPTION = "    or fixjpegls [Options] " + "SOURCE... DIRECTORY\n.\n"
            + "Patch faulty DICOM JPEG-LS images compressed by JAI-IMAGEIO "
            + "JPEG-LS encoder by inserting a LSE marker segment with "
            + "encoder parameter values T1, T2 and T3 actually used by " + "JAI-IMAGEIO JPEG-LS encoder.\n.\n"
            + "Options:";
    private static final String NO_CHECK_IMPL_CUID = "fix also DICOM files "
            + "with different Implementation Class UID than specified by " + "option --check-impl-cuid";
    private static final String CHECK_IMPL_CUID = "Implementation Class UID "
            + "of files to fix; default: `1.2.40.0.13.1.1'";
    private static final String NO_NEW_IMPL_CUID = "do not replace "
            + "Implementation Class UID in fixed files with UID specified by " + "option --new-impl-cuid";
    private static final String NEW_IMPL_CUID = "Implementation Class UID "
            + "inserted in fixed files; default: `1.2.40.0.13.1.1.1'";
    private static final String EXAMPLE = null;

    private static final int SOI = 0xffd8;
    private static final int SOF55 = 0xfff7;
    private static final int LSE = 0xfff8;
    private static final int SOS = 0xffda;
    private static final byte[] LSE_13 = { (byte) 0xff, (byte) 0xf8, (byte) 0x00, (byte) 0x0D, (byte) 0x01,
            (byte) 0x1f, (byte) 0xff, (byte) 0x00, (byte) 0x22, // T1 = 34
            (byte) 0x00, (byte) 0x83, // T2 = 131
            (byte) 0x02, (byte) 0x24, // T3 = 548
            (byte) 0x00, (byte) 0x40 };
    private static final byte[] LSE_14 = { (byte) 0xff, (byte) 0xf8, (byte) 0x00, (byte) 0x0D, (byte) 0x01,
            (byte) 0x3f, (byte) 0xff, (byte) 0x00, (byte) 0x42, // T1 = 66
            (byte) 0x01, (byte) 0x03, // T2 = 259
            (byte) 0x04, (byte) 0x44, // T3 = 1092
            (byte) 0x00, (byte) 0x40 };
    private static final byte[] LSE_15 = { (byte) 0xff, (byte) 0xf8, (byte) 0x00, (byte) 0x0D, (byte) 0x01,
            (byte) 0x7f, (byte) 0xff, (byte) 0x00, (byte) 0x82, // T1 = 130
            (byte) 0x02, (byte) 0x03, // T2 = 515
            (byte) 0x08, (byte) 0x84, // T3 = 2180
            (byte) 0x00, (byte) 0x40 };
    private static final byte[] LSE_16 = { (byte) 0xff, (byte) 0xf8, (byte) 0x00, (byte) 0x0D, (byte) 0x01,
            (byte) 0xff, (byte) 0xff, (byte) 0x01, (byte) 0x02, // T1 = 258
            (byte) 0x04, (byte) 0x03, // T2 = 1027
            (byte) 0x11, (byte) 0x04, // T3 = 4356
            (byte) 0x00, (byte) 0x40 };
    private static final byte[] PADDING_BYTE = { 0 };

    private static final UIDDictionary DICT = UIDDictionary.getDictionary();

    private String implClassUID = "1.2.40.0.13.1.1";
    private String newImplClassUID = "1.2.40.0.13.1.1.1";

    @SuppressWarnings("serial")
    private static final class NoFixException extends IOException {

        public NoFixException(String message) {
            super(message);
        }
    }

    private static final class Replacement {
        final long pos;
        final int len;
        final byte[] val;

        Replacement(long pos, int len, byte[] val) {
            super();
            this.pos = pos;
            this.len = len;
            this.val = val;
        }
    }

    private final class Replacements implements DicomInputHandler {

        boolean pixelData;
        boolean fmi = true;
        List<Replacement> replacements;
        int numItems;
        int bitsStored;

        public boolean readValue(DicomInputStream in) throws IOException {
            int tag = in.tag();
            int len = in.valueLength();
            long pos = in.getStreamPosition();
            DicomObject attrs = in.getDicomObject();
            String uid;
            if (fmi && tag >= 0x00080000) {
                if (replacements == null)
                    throw new NoFixException("File Meta Information (0002,eeee) is missing");
                if (FixJpegLS.this.implClassUID != null) {
                    uid = attrs.getString(Tag.ImplementationClassUID);
                    if (!FixJpegLS.this.implClassUID.equals(uid))
                        throw new NoFixException("Implementation Class UID (0002,0012) = " + uid);
                }
                fmi = false;
            }
            switch (tag) {
            case Tag.FileMetaInformationGroupLength:
            case Tag.ItemDelimitationItem:
            case Tag.SequenceDelimitationItem:
                return in.readValue(in);
            case Tag.TransferSyntaxUID:
                in.readValue(in);
                uid = attrs.getString(Tag.TransferSyntaxUID);
                if (!UID.JPEGLSLossless.equals(uid))
                    throw new NoFixException("Transfer Syntax UID (0002,0010) = " + DICT.prompt(uid));
                replacements = new ArrayList<Replacement>();
                return true;
            case Tag.ImplementationClassUID:
                if (replacements == null)
                    throw new NoFixException("File Meta Information (0002,eeee) is missing");

                in.readValue(in);
                if (FixJpegLS.this.newImplClassUID != null)
                    addImplClassUIDReplacements(pos, len, (int) in.getEndOfFileMetaInfoPosition());
                return true;
            case Tag.PixelData:
                if (in.level() == 0) {
                    if (len != -1)
                        throw new NoFixException("Pixel Data is not encapsulated into Data Fragments");

                    pixelData = true;
                }
                return in.readValue(in);
            case Tag.Item:
                if (pixelData) {
                    if (len == 0)
                        return true;
                    byte[] jpegheader = new byte[17];
                    in.readFully(jpegheader);
                    byte[] lse = selectLSE(jpegheader);
                    in.skipFully(len - 18);
                    addItemReplacements(pos, len, lse, in.read() == 0);
                    numItems++;
                    return true;
                }
            }
            pixelData = false;
            if (len == -1)
                return in.readValue(in);
            in.skipFully(len);
            return true;
        }

        @SuppressWarnings("deprecation")
        private void addImplClassUIDReplacements(long pos, int len, int eoffmipos) {
            int uidlen = FixJpegLS.this.newImplClassUID.length();
            int newlen = (uidlen + 1) & ~1;
            if (eoffmipos > 0) {
                byte[] newfmilen = new byte[4];
                ByteUtils.int2bytesLE(eoffmipos - 144 - len + newlen, newfmilen, 0);
                replacements.add(new Replacement(140, 4, newfmilen));
            }
            byte[] newval = new byte[newlen + 2];
            ByteUtils.ushort2bytesLE(newlen, newval, 0);
            FixJpegLS.this.newImplClassUID.getBytes(0, uidlen, newval, 2);
            replacements.add(new Replacement(pos - 2, len + 2, newval));
        }

        private void addItemReplacements(long pos, int len, byte[] lse, boolean padded) {
            int newlen = len + (padded ? 14 : 15);
            replacements.add(new Replacement(pos - 4, 4, ByteUtils.int2bytesLE((newlen + 1) & ~1, new byte[4], 0)));
            replacements.add(new Replacement(pos + 15, 0, lse));
            boolean newPadded = (newlen & 1) != 0;
            if (newPadded != padded)
                replacements.add(newPadded ? new Replacement(pos + len, 0, PADDING_BYTE)
                        : new Replacement(pos + len - 1, 1, null));
        }

        public void applyTo(File source, File target) throws IOException {
            boolean failed = true;
            FileInputStream fin = new FileInputStream(source);
            try {
                FileChannel in = fin.getChannel();
                FileOutputStream fos = new FileOutputStream(target);
                try {
                    FileChannel out = fos.getChannel();
                    long pos = 0L;
                    for (Replacement replacement : replacements) {
                        in.transferTo(pos, replacement.pos - pos, out);
                        if (replacement.val != null)
                            out.write(ByteBuffer.wrap(replacement.val));
                        pos = replacement.pos + replacement.len;
                    }
                    in.transferTo(pos, source.length() - pos, out);
                    failed = false;
                } finally {
                    fos.close();
                    if (failed == true)
                        target.delete();
                }
            } finally {
                fin.close();
            }
        }

        private byte[] selectLSE(byte[] jpegheader) throws NoFixException {
            if (ByteUtils.bytesBE2ushort(jpegheader, 0) != SOI)
                throw new NoFixException("SOI marker is missing");
            if (ByteUtils.bytesBE2ushort(jpegheader, 2) != SOF55)
                throw new NoFixException("SOI marker is not followed by JPEG-LS SOF marker");
            if (ByteUtils.bytesBE2ushort(jpegheader, 4) != 11)
                throw new NoFixException("unexpected length of JPEG-LS SOF marker segment");
            int marker = ByteUtils.bytesBE2ushort(jpegheader, 15);
            if (marker != SOS) {
                throw new NoFixException(marker == LSE ? "contains already LSE marker segment"
                        : "JPEG-LS SOF marker segment is not followed by SOS marker");
            }
            switch (bitsStored = jpegheader[6]) {
            case 13:
                return LSE_13;
            case 14:
                return LSE_14;
            case 15:
                return LSE_15;
            case 16:
                return LSE_16;
            }
            throw new NoFixException("JPEG-LS " + bitsStored + "-bit");
        }

    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        try {
            CommandLine cl = parseCommandLine(args);
            if (cl == null)
                return;
            List<String> argList = cl.getArgList();
            File target = removeTarget(argList);
            FixJpegLS fixJpegLS = new FixJpegLS();
            if (cl.hasOption("no-check-impl-cuid"))
                fixJpegLS.setImplClassUID(null);
            else if (cl.hasOption("check-impl-cuid"))
                fixJpegLS.setImplClassUID(cl.getOptionValue("check-impl-cuid"));
            if (cl.hasOption("no-new-impl-cuid"))
                fixJpegLS.setNewImplClassUID(null);
            else if (cl.hasOption("new-impl-cuid"))
                fixJpegLS.setNewImplClassUID(cl.getOptionValue("new-impl-cuid"));
            int[] counts = new int[2];
            long start = System.currentTimeMillis();
            for (String arg : argList)
                fixJpegLS.fix(new File(arg), target, counts);
            long end = System.currentTimeMillis();
            System.out.println();
            System.out.println("Fix " + counts[1] + " of " + counts[0] + " scanned files in "
                    + ((end - start) / 1000.f) + " s.");
        } catch (ParseException e) {
            System.err.println("fixjpegls: " + e.getMessage());
            System.err.println("Try `fixjpegls --help' for more information.");
            System.exit(1);
        } catch (Exception e) {
            System.err.println("fixjpegls: " + e.getMessage());
            System.exit(1);
        }

    }

    @SuppressWarnings("static-access")
    private static CommandLine parseCommandLine(String[] args) throws ParseException {
        Options options = new Options();
        options.addOption(
                OptionBuilder.withLongOpt("no-check-impl-cuid").withDescription(NO_CHECK_IMPL_CUID).create());
        options.addOption(OptionBuilder.withLongOpt("check-impl-cuid").hasArg().withArgName("uid")
                .withDescription(CHECK_IMPL_CUID).create());
        options.addOption(OptionBuilder.withLongOpt("no-new-impl-cuid").withDescription(NO_NEW_IMPL_CUID).create());
        options.addOption(OptionBuilder.withLongOpt("new-impl-cuid").hasArg().withArgName("uid")
                .withDescription(NEW_IMPL_CUID).create());
        options.addOption(OptionBuilder.withLongOpt("help").withDescription("display this help and exit").create());
        options.addOption(OptionBuilder.withLongOpt("version")
                .withDescription("output version information and exit").create());
        CommandLineParser parser = new PosixParser();
        CommandLine cl = parser.parse(options, args);
        if (cl.hasOption("help")) {
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp(USAGE, DESCRIPTION, options, EXAMPLE);
            return null;
        }
        if (cl.hasOption("version")) {
            System.out.println("fixjpegls " + FixJpegLS.class.getPackage().getImplementationVersion());
            return null;
        }
        return cl;
    }

    private static File removeTarget(List<String> argList) throws ParseException, IOException {
        int size = argList.size();
        if (size < 2)
            throw new ParseException(size == 0 ? "missing file operand"
                    : "missing destination file operand after `" + argList.get(0) + "'");
        String targetName = argList.get(size - 1);
        File target = new File(targetName);
        if (size > 2 && !target.isDirectory()) {
            throw new IOException("target `" + targetName + "' is not a directory");
        }
        argList.remove(size - 1);
        return target;
    }

    public final String getImplClassUID() {
        return implClassUID;
    }

    public final void setImplClassUID(String implClassUID) {
        this.implClassUID = implClassUID;
    }

    public final String getNewImplClassUID() {
        return newImplClassUID;
    }

    public final void setNewImplClassUID(String newImplClassUID) {
        this.newImplClassUID = newImplClassUID;
    }

    public void fix(File source, File target, int[] counts) {
        if (target.isDirectory() && source.isFile())
            target = new File(target, source.getName());
        fixRecursive(source, target, counts);
    }

    private void fixRecursive(File source, File target, int[] counts) {
        if (!source.exists()) {
            System.err.println("no such file or directory `" + source + "'");
        } else if (source.isDirectory()) {
            if (!target.exists()) {
                if (!target.mkdir())
                    System.err.println("failed to create directory `" + target + "'");
            }
            if (target.isFile()) {
                System.err
                        .println("cannot overwrite non-directory `" + target + "' with directory `" + source + "'");
                return;
            }
            String[] ss = source.list();
            for (String s : ss) {
                fixRecursive(new File(source, s), new File(target, s), counts);
            }
        } else if (target.isDirectory()) {
            System.err.println("cannot overwrite directory `" + target + "' with non-directory `" + source + "'");
        } else {
            Replacements replacements = new Replacements();
            try {
                counts[0]++;
                DicomInputStream din = new DicomInputStream(source);
                try {
                    din.setHandler(replacements);
                    din.readDicomObject();
                } finally {
                    try {
                        din.close();
                    } catch (IOException ignore) {
                    }
                }
                if (replacements.numItems == 0)
                    throw new NoFixException("no Pixel Data Fragments");
                replacements.applyTo(source, target);
                counts[1]++;
                System.out.println(
                        "PATCHED " + source + ": JPEG-LS " + replacements.bitsStored + "-bit -> " + target);
            } catch (Exception e) {
                System.out.println("skipped " + source + ": " + e.getMessage());
            }
        }
    }

}