public class Constants {

    public static final String APP_NAME = ApplicationAttributes.getInstance().getName();
    public static final String APP_VERSION = ApplicationAttributes.getInstance().getVersion();
    public static final String BUILD_DATE = ApplicationAttributes.getInstance().getBuildDate();
    public static final String DEVELOPERS = "Volker Oth, Miklos Juhasz";

    /** RED components of default DVD palette */
    public static final byte DEFAULT_PALETTE_RED[] = {
        (byte)0x00, (byte)0xf0, (byte)0xcc, (byte)0x99,
        (byte)0x33, (byte)0x11, (byte)0xfa, (byte)0xbb,
        (byte)0x33, (byte)0x11, (byte)0xfa, (byte)0xbb,
        (byte)0xfa, (byte)0xbb, (byte)0x33, (byte)0x11
    };

    /** GREEN components of default DVD palette */
    public static final byte DEFAULT_PALETTE_GREEN[] = {
        (byte)0x00, (byte)0xf0, (byte)0xcc, (byte)0x99,
        (byte)0x33, (byte)0x11, (byte)0x33, (byte)0x11,
        (byte)0xfa, (byte)0xbb, (byte)0xfa, (byte)0xbb,
        (byte)0x33, (byte)0x11, (byte)0xfa, (byte)0xbb
    };

    /** BLUE components of default DVD palette */
    public static final byte DEFAULT_PALETTE_BLUE[] = {
        (byte)0x00, (byte)0xf0, (byte)0xcc, (byte)0x99,
        (byte)0xfa, (byte)0xbb, (byte)0x33, (byte)0x11,
        (byte)0x33, (byte)0x11, (byte)0x33, (byte)0x11,
        (byte)0xfa, (byte)0xbb, (byte)0xfa, (byte)0xbb,
    };

    /** ALPHA components of default DVD palette */
    public static final byte DEFAULT_PALETTE_ALPHA[] = {
        (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
        (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
        (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
        (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
    };

    public static final Palette DEFAULT_DVD_PALETTE = new Palette(
            DEFAULT_PALETTE_RED,
            DEFAULT_PALETTE_GREEN,
            DEFAULT_PALETTE_BLUE,
            DEFAULT_PALETTE_ALPHA,
            true
    );

    public static final String[][] LANGUAGES = {
        {"English",      "en", "eng"},
        {"Abkhazian",    "ab", "abk"},
        {"Afar",         "aa", "aar"},
        {"Afrikaans",    "af", "afr"},
        {"Albanian",     "sq", "sqi"},
        {"Amharic",      "am", "amh"},
        {"Arabic",       "ar", "ara"},
        {"Aragonese",    "an", "arg"},
        {"Armenian",     "hy", "hye"},
        {"Assamese",     "as", "asm"},
        {"Avaric",       "av", "ava"},
        {"Avestan",      "ae", "ave"},
        {"Aymara",       "ay", "aym"},
        {"Azerbaijani",  "az", "aze"},
        {"Bambara",      "bm", "bam"},
        {"Bashkir",      "ba", "bak"},
        {"Basque",       "eu", "eus"},
        {"Belarusian",   "be", "bel"},
        {"Bengali",      "bn", "ben"},
        {"Bihari",       "bh", "bih"},
        {"Bislama",      "bi", "bis"},
        {"Bosnian",      "bs", "bos"},
        {"Breton",       "br", "bre"},
        {"Bulgarian",    "bg", "bul"},
        {"Burmese",      "my", "mya"},
        {"Cambodian",    "km", "khm"},
        {"Catalan",      "ca", "cat"},
        {"Chamorro",     "ch", "cha"},
        {"Chechen",      "ce", "che"},
        {"Chichewa",     "ny", "nya"},
        {"Chinese",      "zh", "zho"},
        {"Chuvash",      "cv", "chv"},
        {"Cornish",      "kw", "cor"},
        {"Corsican",     "co", "cos"},
        {"Cree",         "cr", "cre"},
        {"Croatian",     "hr", "hrv"},
        {"Czech",        "cs", "ces"},
        {"Danish",       "da", "dan"},
        {"Divehi",       "dv", "div"},
        {"Dzongkha",     "dz", "dzo"},
        {"Dutch",        "nl", "nld"},
        {"Esperanto",    "eo", "epo"},
        {"Estonian",     "et" ,"est"},
        {"Ewe",          "ee", "ewe"},
        {"Faroese",      "fo", "fao"},
        {"Fiji",         "fj", "fij"},
        {"Finnish",      "fi", "fin"},
        {"French",       "fr", "fra"},
        {"Frisian",      "fy", "fry"},
        {"Fulah",        "ff", "ful"},
        {"Gaelic",       "gd", "gla"},
        {"Galician",     "gl", "glg"},
        {"Ganda",        "lg", "lug"},
        {"Georgian",     "ka", "kat"},
        {"German",       "de", "deu"},
        {"Greek",        "el", "ell"},
        {"Greenlandic",  "kl", "kal"},
        {"Guarani",      "gn", "grn"},
        {"Gujarati",     "gu", "guj"},
        {"Haitian",      "ht", "hat"},
        {"Hausa",        "ha", "hau"},
        {"Hebrew",       "he", "heb"},
        {"Herero",       "hz", "her"},
        {"Hindi",        "hi", "hin"},
        {"Hiri Motu",    "ho", "hmo"},
        {"Hungarian",    "hu", "hun"},
        {"Icelandic",    "is", "isl"},
        {"Ido",          "io", "ido"},
        {"Igbo",         "ig", "ibo"},
        {"Indonesian",   "id", "ind"},
        {"Interlingua",  "ia", "ina"},
        {"Interlingue",  "ie", "ile"},
        {"Inupiaq",      "ik", "ipk"},
        {"Inuktitut",    "iu", "iku"},
        {"Irish",        "ga", "gle"},
        {"Italian",      "it", "ita"},
        {"Japanese",     "ja", "jpn"},
        {"Javanese",     "jv", "jav"},
        {"Kannada",      "kn", "kan"},
        {"Kanuri",       "kr", "kau"},
        {"Kashmiri",     "ks", "kas"},
        {"Kazakh",       "kk", "kaz"},
        {"Kikuyu",       "ki", "kik"},
        {"Kinyarwanda",  "rw", "kin"},
        {"Kirghiz",      "ky", "kir"},
        {"Komi",         "kv", "kom"},
        {"Kongo",        "kg", "kon"},
        {"Korean",       "ko", "kor"},
        {"Kuanyama",     "kj", "kua"},
        {"Kurdish",      "ku", "kur"},
        {"Lao",          "lo", "lao"},
        {"Latin",        "la", "lat"},
        {"Latvian",      "lv", "lav"},
        {"Limburgan",    "li", "lim"},
        {"Lingala",      "ln", "lin"},
        {"Lithuanian",   "lt", "lit"},
        {"Luba",         "lu", "lub"},
        {"Luxembourgish","lb","ltz"},
        {"Macedonian",   "mk", "mkd"},
        {"Malagasy",     "mg", "mlg"},
        {"Malay",        "ms", "msa"},
        {"Malayalam",    "ml", "mal"},
        {"Maltese",      "mt", "mlt"},
        {"Marshallese",  "mh", "mah"},
        {"Manx",         "gv", "glv"},
        {"Maori",        "mi", "mri"},
        {"Marathi",      "mr", "mar"},
        {"Mongolian",    "mn", "mon"},
        {"Nauru",        "na", "nau"},
        {"Navajo",       "nv", "nav"},
        {"Ndebele",      "nd", "nde"},
        {"Ndonga",       "ng", "ndo"},
        {"Nepali",       "ne", "nep"},
        {"Norwegian",    "no", "nor"},
        {"Occitan",      "oc", "oci"},
        {"Ojibwa",       "oj", "oji"},
        {"Oriya",        "or", "ori"},
        {"Oromo",        "om", "orm"},
        {"Ossetian",     "os", "oss"},
        {"Pali",         "pi", "pli"},
        {"Panjabi",      "pa", "pan"},
        {"Pashto",       "ps", "pus"},
        {"Persian",      "fa", "fas"},
        {"Polish",       "pl", "pol"},
        {"Portuguese",   "pt", "por"},
        {"Quechua",      "qu", "que"},
        {"Romansh",      "rm", "roh"},
        {"Romanian",     "ro", "ron"},
        {"Rundi",        "rn", "run"},
        {"Russian",      "ru", "rus"},
        {"Sami",         "se", "sme"},
        {"Samoan",       "sm", "smo"},
        {"Sango",        "sg", "sag"},
        {"Sanskrit",     "sa", "san"},
        {"Sardinian",    "sc", "srd"},
        {"Serbian",      "sr", "srp"},
        {"Shona",        "sn", "sna"},
        {"Sichuan Yi",   "ii", "iii"},
        {"Sindhi",       "sd", "snd"},
        {"Sinhalese",    "si", "sin"},
        {"Slavonic",     "cu", "chu"},
        {"Slovak",       "sk", "slk"},
        {"Slovenian",    "sl", "slv"},
        {"Somali",       "so", "som"},
        {"Sotho",        "st", "sot"},
        {"Spanish",      "es", "spa"},
        {"Sundanese",    "su", "sun"},
        {"Swahili",      "sw", "swa"},
        {"Swati",        "ss", "ssw"},
        {"Swedish",      "sv", "swe"},
        {"Tagalog",      "tl", "tgl"},
        {"Tahitian",     "ty", "tah"},
        {"Tajik",        "tg", "tgk"},
        {"Tamil",        "ta", "tam"},
        {"Tatar",        "tt", "tar"},
        {"Telugu",       "te", "tel"},
        {"Thai",         "th", "tha"},
        {"Tibetan",      "bo", "bod"},
        {"Tigrinya",     "ti", "tir"},
        {"Tonga",        "to", "ton"},
        {"Tsonga",       "ts", "tso"},
        {"Tswana",       "tn", "tsn"},
        {"Turkish",      "tr", "tur"},
        {"Turkmen",      "tk", "tuk"},
        {"Twi",          "tw", "twi"},
        {"Uighur",       "ug", "uig"},
        {"Ukrainian",    "uk", "ukr"},
        {"Urdu",         "ur", "urd"},
        {"Uzbek",        "uz", "uzb"},
        {"Venda",        "ve", "ven"},
        {"Vietnamese",   "vi", "vie"},
        {"Volapük",      "vo", "vol"},
        {"Welsh",        "cy", "cym"},
        {"Walloon",      "wa", "wln"},
        {"Wolof",        "wo", "wol"},
        {"Xhosa",        "xh", "xho"},
        {"Yiddish",      "yi", "yid"},
        {"Yoruba",       "yo", "yor"},
        {"Zhuang",       "za", "zha"},
        {"Zulu",         "zu", "zul"},
    };
} public final class SubtitleUtils {

    private SubtitleUtils() {
    }

    public static double getFps(String fps) {
        fps = fps.toLowerCase().trim();
        if (fps.equals("pal")  || fps.equals("25p") || fps.equals("25")) {
            return Framerate.PAL.getValue();
        }
        if (fps.equals("ntsc") || fps.equals("30p") || fps.equals("29.97") || fps.equals("29.970")) {
            return Framerate.NTSC.getValue();
        }
        if (fps.equals("24p")  || fps.equals("23.976")) {
            return Framerate.FPS_23_976.getValue();
        }
        if (fps.equals("23.975")) {
            return Framerate.FPS_23_975.getValue();
        }
        if (fps.equals("24")) {
            return Framerate.FPS_24.getValue();
        }
        if (fps.equals("50i")  || fps.equals("50")) {
            return Framerate.PAL_I.getValue();
        }
        if (fps.equals("60i")  || fps.equals("59.94")) {
            return Framerate.NTSC_I.getValue();
        }

        double d;
        try {
            d = Double.parseDouble(fps);
        } catch (NumberFormatException ex) {
            return -1.0;
        }
        if (Math.abs(d - Framerate.FPS_23_975.getValue()) < 0.001) {
            return Framerate.FPS_23_975.getValue();
        }
        if (Math.abs(d - Framerate.FPS_23_976.getValue()) < 0.001) {
            return Framerate.FPS_23_976.getValue();
        }
        if (Math.abs(d - Framerate.FPS_24.getValue()) < 0.001) {
            return Framerate.FPS_24.getValue();
        }
        if (Math.abs(d - Framerate.PAL.getValue()) < 0.001) {
            return Framerate.PAL.getValue();
        }
        if (Math.abs(d - Framerate.NTSC.getValue()) < 0.001) {
            return Framerate.NTSC.getValue();
        }
        if (Math.abs(d - Framerate.NTSC_I.getValue()) < 0.001) {
            return Framerate.NTSC_I.getValue();
        }
        if (Math.abs(d - Framerate.PAL_I.getValue()) < 0.001) {
            return Framerate.PAL_I.getValue();
        }
        return d;
    }

    /**
     * Synchronizes a time stamp in 90kHz resolution to the given frame rate.
     */
    public static long syncTimePTS(long timeStamp, double fps, double fpsTrg) {
        long retval;
        // correct time stamps to fit to frames
        if (fps == Framerate.NTSC.getValue() || fps == Framerate.PAL.getValue() || fps == Framerate.FPS_24.getValue()) {
            // NTSC: 90000/(30000/1001) = 3003
            // PAL:  90000/25 = 3600
            // 24Hz: 90000/24 = 3750
            int tpfi = (int)((90000+(fps/2))/fps); // target time per frame in 90kHz
            int tpfh = tpfi/2;
            retval = ((timeStamp + tpfh)/tpfi)*tpfi;
        } else if (fpsTrg == Framerate.FPS_23_976.getValue()) {
            // 90000/(24000/1001) = 3753.75 = 15015/4
            retval = ((((timeStamp + 1877)*4)/15015)*15015)/4;
        } else {
            double tpf = (90000/fpsTrg); // target time per frame in 90kHz
            retval = (long)((long)(timeStamp/tpf)*tpf+0.5);
        }
        return retval;
    }

    public static double getDefaultFramerateForResolution(Resolution resolution) {
        double fps;
        switch (resolution.getDimensions()[1]) {
            case 480:
                fps = Framerate.NTSC.getValue();
                break;
            case 576:
                fps = Framerate.PAL.getValue();
                break;
            default:
                fps = Framerate.FPS_23_976.getValue();
        }
        return fps;
    }

    public static Resolution getResolutionForDimension(int width, int height) {
        if (width <= Resolution.NTSC.getDimensions()[0] && height <= Resolution.NTSC.getDimensions()[1]) {
            return Resolution.NTSC;
        } else if (width <= Resolution.PAL.getDimensions()[0] && height <= Resolution.PAL.getDimensions()[1]) {
            return Resolution.PAL;
        } else if (width <= Resolution.HD_720.getDimensions()[0] && height <= Resolution.HD_720.getDimensions()[1]) {
            return Resolution.HD_720;
        } else if (width <= Resolution.HD_1440x1080.getDimensions()[0] && height <= Resolution.HD_1440x1080.getDimensions()[1]) {
            return Resolution.HD_1440x1080;
        } else {
            return Resolution.HD_1080;
        }
    }
} public class TimeUtils {

    private static final int PTS_FREQUENCY_IN_KHZ = 90;
    private static final Pattern TIME_PATTERN = Pattern.compile( "(\\d+):(\\d+):(\\d+)[:\\.](\\d+)" );

    public static String ptsToTimeStr(long pts) {
        int time[] = ptsToTime(pts);
        return leftZeroPad(time[0], 2) + ":" + leftZeroPad(time[1], 2) + ":" + leftZeroPad(time[2], 2) + "." + leftZeroPad(time[3], 3);
    }

    public static String ptsToTimeStrIdx(long pts) {
        int time[] = ptsToTime(pts);
        return leftZeroPad(time[0], 2) + ":" + leftZeroPad(time[1], 2) + ":" + leftZeroPad(time[2], 2) + ":" + leftZeroPad(time[3], 3);
    }

    public static String ptsToTimeStrXml(long pts, double fps) {
        int time[] = ptsToTime(pts);
        return leftZeroPad(time[0], 2) + ":" + leftZeroPad(time[1], 2) + ":" + leftZeroPad(time[2], 2) + ":" + leftZeroPad((int)(fps*time[3] / 1000.0 + 0.5), 2);
    }

    private static int[] ptsToTime(long pts) {
        return msToTime((pts + 45) / PTS_FREQUENCY_IN_KHZ);
    }

    private static int[] msToTime(long ms) {
        int time[] = new int[4];
        time[0] = (int)(ms / (60*60*1000)); // hours
        ms -= time[0] * 60*60*1000;
        time[1] = (int)(ms / (60*1000)); // minutes
        ms -= time[1] * 60*1000;
        time[2] = (int)(ms / 1000); // seconds
        ms -= time[2] *1000;
        time[3] = (int)ms; // milliseconds
        return time;
    }

    public static long timeStrToPTS(String s) {
        Matcher m = TIME_PATTERN.matcher(s);
        if (m.matches()) {
            long hour = Integer.parseInt(m.group(1));
            long min = Integer.parseInt(m.group(2));
            long sec = Integer.parseInt(m.group(3));
            long ms  = Integer.parseInt(m.group(4));

            long temp = hour * 60;
            temp += min;
            temp *= 60;
            temp += sec;
            temp *= 1000;
            return (temp+ms) * PTS_FREQUENCY_IN_KHZ;
        } else {
            return -1;
        }
    }

    public static long timeStrXmlToPTS(String s, double fps) {
        Matcher m = TIME_PATTERN.matcher(s);
        if (m.matches()) {
            long hour    = Integer.parseInt(m.group(1));
            long min     = Integer.parseInt(m.group(2));
            long sec     = Integer.parseInt(m.group(3));
            long frames  = Integer.parseInt(m.group(4));

            long temp = hour * 60;
            temp += min;
            temp *= 60;
            temp += sec;
            temp *= 1000;
            return (temp + (int)(frames / fps * 1000.0 + 0.5)) * PTS_FREQUENCY_IN_KHZ;
        } else {
            return -1;
        }
    }
} public class Bitmap {

    private static final Logger logger = Logger.getInstance();

    private final int width;
    private final int height;
    private final byte buffer[];

    public Bitmap(int width, int height) {
        this.width = width;
        this.height = height;
        this.buffer = new byte[width * height];
    }

    public Bitmap(int width, int height, byte fillerColorIndex) {
        this(width, height);
        fillWithColorIndexValue(fillerColorIndex);
    }

    public Bitmap(int width, int height, byte[] buffer) {
        this.width = width;
        this.height = height;
        this.buffer = buffer;
    }

    public Bitmap(Bitmap bitmap) {
        this.width = bitmap.width;
        this.height = bitmap.height;
        this.buffer = Arrays.copyOf(bitmap.buffer, bitmap.buffer.length);
    }

    private void fillWithColorIndexValue(byte colorIndex) {
        for (int i = 0; i < width * height; i++) {
            buffer[i] = colorIndex;
        }
    }

    public void fillRectangularWithColorIndex(int rectX, int rectY, int rectWidth, int rectHeight, byte colorIndex) {
        int xMax = rectX + rectWidth;
        if (xMax > width) {
            xMax = width;
        }

        int yMax = rectY+rectHeight;
        if (yMax > height) {
            yMax = height;
        }

        for (int y = rectY; y < yMax; y++) {
            int yOfs = y * width;
            for (int x = rectX; x < xMax; x++) {
                buffer[yOfs + x] = colorIndex;
            }
        }
    }

    public BufferedImage getImage(ColorModel colorModel) {
        DataBuffer dataBuffer = new DataBufferByte(buffer, width * height);
        SampleModel sampleModel = new SinglePixelPackedSampleModel(DataBuffer.TYPE_BYTE, width, height, new int[]{ 0xff });
        WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, null);
        return new BufferedImage(colorModel, raster, false, null);
    }

    public int getPrimaryColorIndex(byte[] alpha, int alphaThreshold, byte[] luma) {
        int colorIndexCount = alpha.length;
        int[] histogram = createHistogram(colorIndexCount);
        weightHistogramWithAlphaAndLumaValues(histogram, alpha, alphaThreshold, luma);
        return getColorIndexWithTheHighestValue(histogram);
    }

    private int[] createHistogram(int colorIndexCount) {
        int histogram[] = new int[colorIndexCount];
        Arrays.fill(histogram, 0);
        for (byte b : buffer) {
            histogram[(b & 0xff)]++;
        }
        return histogram;
    }

    private void weightHistogramWithAlphaAndLumaValues(int[] histogram, byte[] alphaValues, int alphaThreshold, byte[] lumaValues) {
        for (int i = 0; i < histogram.length; i++) {
            int alpha = alphaValues[i] & 0xff;
            if (alpha < alphaThreshold) {
                alpha = 0;
            }
            histogram[i] = (histogram[i] * alpha + 128) / 256; // prefer opaque

            int luma = lumaValues[i] & 0xff;
            histogram[i] = (histogram[i] * luma + 128) / 256; // prefer light
        }
    }

    private int getColorIndexWithTheHighestValue(int[] histogram) {
        int maxValue = 0;
        int colorIndex = 0;
        for (int i=0; i < histogram.length; i++) {
            if (histogram[i] > maxValue) {
                maxValue = histogram[i];
                colorIndex = i;
            }
        }
        return colorIndex;
    }

    public int getHighestVisibleColorIndex(byte[] alphaValues) {
        int maxColorIndex = 0;
        for (byte b : buffer) {
            int colorIndex = b & 0xff;
            if ((alphaValues[colorIndex] & 0xff) > 0) {
                if (colorIndex > maxColorIndex) {
                    maxColorIndex = colorIndex;
                    if (maxColorIndex == 255) {
                        break;
                    }
                }
            }
        }
        return maxColorIndex;
    }

    /**
     * Convert a palettized Bitmap (where each palette entry has individual alpha)
     * to a Bitmap with N color palette, where:
     * index0 = transparent (background), index1 = light color (pattern), ... , indexN-2 = dark color, indexN-1 = black.
     *
     * For N-1 luminances, N-2 thresholds are needed
     *                  lumaThreshold[0] is the threshold for the lightest color (-> index 1)
     *                  lumaThreshold[N-2] is the threshold for the darkest color (-> index N-1)
     */
    public Bitmap getBitmapWithNormalizedPalette(byte[] alphaValues, int alphaThreshold, byte[] lumaValues, int lumaThreshold[]) {
        Bitmap bm = new Bitmap(width, height);
        HashMap<Integer, Integer> p = new HashMap<Integer, Integer>();

        int newColorIndex;
        for (int i = 0; i < buffer.length; i++) {
            int colorIndex = buffer[i] & 0xff;
            int alpha = alphaValues[colorIndex] & 0xff;
            int luma = lumaValues[colorIndex] & 0xff;

            Integer existingColorIndex = p.get((alpha << 8) | luma);

            if (existingColorIndex != null) {
                newColorIndex = existingColorIndex;
            }
            else {
                if (alpha < alphaThreshold) {
                    newColorIndex = 0; // transparent color
                } else {
                    newColorIndex = 1; // default: lightest color
                    for (int threshold : lumaThreshold) {
                        if (luma > threshold) {
                            break;
                        }
                        newColorIndex++; // try next darker color
                    }
                }
                p.put((alpha << 8) | luma, newColorIndex);
            }
            bm.buffer[i] = (byte)newColorIndex;
        }
        return bm;
    }

    /**
     * Scales a palettized Bitmap (where each palette entry has individual alpha) using bilinear filtering
     * to a Bitmap with N color palette, where:
     * index0 = transparent, index1 = light color, ... , indexN-2 = dark color, indexN-1 = black.
     * @param sizeX		Target width
     * @param sizeY		Target height
     * @param pal 		Palette of the source Bitmap
     * @param alphaThr	Threshold for alpha (transparency), lower = more transparent
     * @param lumThr    Threshold for luminances. For N-1 luminances, N-2 thresholds are needed
     *                  lumThr[0] is the threshold for the lightest color (-> idx 1)
     *                  lumThr[N-2] is the threshold for the darkest color (-> idx N-1)
     * @return Scaled Bitmap which uses a fixed frame Palette.
     */
    public Bitmap scaleBilinearLm(final int sizeX, final int sizeY, final Palette pal, final int alphaThr, final int lumThr[]) {
        final byte[] cy = pal.getY();
        final byte[] a = pal.getAlpha();

        final double scaleX = (double)(width - 1) / (sizeX - 1);
        final double scaleY = (double)(height - 1) / (sizeY - 1);

        int lastCY = 0;
        int lastA  = 0;
        int lastColIdx = 0; // 0 is the transparent color

        Bitmap trg = new Bitmap(sizeX, sizeY);

        for (int yt = 0; yt < sizeY; yt++) {
            double ys = yt * scaleY; // source coordinate
            int ysi = (int)ys;
            double wy = (ys - ysi);
            double wy1 = 1.0 - wy;

            for (int xt = 0; xt < sizeX; xt++) {
                double xs = xt * scaleX; // source coordinate
                int xsi = (int)xs;
                double wx = (xs - xsi); // weight factor

                double wx1 = 1.0 - wx;

                // interpolate pixel

                // top left
                double w = wx1 * wy1;
                int idx = getPixel(xsi, ysi) & 0xff;
                double at = (a[idx] & 0xff) * w;
                double cyt = (cy[idx] & 0xff) * w;

                // top right
                if (xsi < width - 1) {
                    w = wx * wy1;
                    idx = getPixel(xsi + 1, ysi) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    cyt += (cy[idx] & 0xff) * w;
                }  // else assume transparent black

                // bottom left
                if (ysi < height-1) {
                    w = wx1 * wy;
                    idx = getPixel(xsi, ysi + 1) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    cyt += (cy[idx] & 0xff) * w;
                } // else assume transparent black

                // bottom right
                if ((ysi < height - 1) && (xsi < width - 1)) {
                    w = wx * wy;
                    idx = getPixel(xsi + 1, ysi + 1) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    cyt += (cy[idx] & 0xff) * w;
                } // else assume transparent black

                int ati = (int)at;
                int cyti = (int)cyt;

                // find color index in palette

                // already known ?
                int colIdx = lastColIdx;
                if (ati != lastA || cyti != lastCY ) {
                    // determine index in target
                    if (ati < alphaThr) {
                        colIdx = 0; // transparent color
                    } else {
                        colIdx = 1; // default: lightest color
                        for (int threshold : lumThr) {
                            if (cyti > threshold) {
                                break;
                            }
                            colIdx++; // try next darker color
                        }
                    }
                    // remember
                    lastA = ati;
                    lastCY = cyti;
                    lastColIdx = colIdx;
                }
                // write target pixel
                trg.setPixel(xt, yt, (byte)colIdx);
            }
        }
        return trg;
    }

    /**
     * Scales a palettized Bitmap (where each palette entry has individual alpha) using a given scaling filter
     * to a Bitmap with N color palette, where:
     * index0 = transparent, index1 = light color, ... , indexN-2 = dark color, indexN-1 = black
     * @param sizeX		Target width
     * @param sizeY		Target height
     * @param pal 		Palette of the source Bitmap
     * @param alphaThr	Threshold for alpha (transparency), lower = more transparent
     * @param lumThr    Threshold for luminances. For N-1 luminances, N-2 thresholds are needed
     *                  lumThr[0] is the threshold for the lightest color (-> idx 1)
     *                  lumThr[N-2] is the threshold for the darkest color (-> idx N-1)
     * @param f Filter for scaling
     * @return Scaled Bitmap which uses a fixed frame Palette.
     */
    public Bitmap scaleFilterLm(final int sizeX, final int sizeY, final Palette pal, final int alphaThr, final int lumThr[], final ResampleFilter f) {
        final int[] trg = new FilterOp(f, sizeX, sizeY).filter(this, pal);

        Bitmap bm = new Bitmap(sizeX, sizeY);

        // select nearest colors in existing palette
        HashMap<Integer, Integer> p = new HashMap<Integer, Integer>();

        for (int i = 0; i < trg.length; i++) {
            int color = trg[i];
            int colIdx;
            Integer idxEx = p.get(color);
            if (idxEx != null) {
                colIdx = idxEx;
            }
            else {
                int alpha = (color >> 24) & 0xff;
                int red   = (color >> 16) & 0xff;
                int green = (color >>  8) & 0xff;
                int blue  =  color        & 0xff;
                int cyp   = RGB2YCbCr(red, green, blue, false)[0];

                // determine index in target
                if (alpha < alphaThr) {
                    colIdx = 0; // transparent color
                } else {
                    colIdx = 1; // default: lightest color
                    for (int threshold : lumThr) {
                        if (cyp > threshold) {
                            break;
                        }
                        colIdx++; // try next darker color
                    }
                }
                p.put(color, colIdx);
            }
            // write target pixel
            bm.buffer[i] = (byte)colIdx;
        }
        return bm;
    }

    /** Scales a palettized Bitmap to a Bitmap with the same palette using bilinear filtering.
     * @param sizeX Target width
     * @param sizeY Target height
     * @param pal   Palette of the source Bitmap
     * @return Scaled Bitmap which uses the same Palette as the source Bitmap.
     */
    public Bitmap scaleBilinear(final int sizeX, final int sizeY, final Palette pal) {
        final byte[] r = pal.getR();
        final byte[] g = pal.getG();
        final byte[] b = pal.getB();
        final byte[] a = pal.getAlpha();

        final double scaleX = (double)(width - 1) / (sizeX - 1);
        final double scaleY = (double)(height - 1) / (sizeY - 1);

        int lastR = 0;
        int lastG = 0;
        int lastB = 0;
        int lastA = 0;
        int lastColIdx = pal.getIndexOfMostTransparentPaletteEntry();

        final Bitmap trg = new Bitmap(sizeX, sizeY);

        for (int yt = 0; yt < sizeY; yt++) {
            double ys = yt * scaleY; // source coordinate
            int ysi = (int)ys;
            double wy = (ys - ysi);
            double wy1 = 1.0 - wy;

            for (int xt = 0; xt < sizeX; xt++) {
                double xs = xt * scaleX; // source coordinate
                int xsi = (int)xs;
                double wx = (xs - xsi); // weight factor

                double wx1 = 1.0 - wx;

                // interpolate pixel

                // top left
                double w = wx1 * wy1;
                int idx = getPixel(xsi, ysi) & 0xff;
                double at = (a[idx] & 0xff) * w;
                double rt = (r[idx] & 0xff) * w;
                double gt = (g[idx] & 0xff) * w;
                double bt = (b[idx] & 0xff) * w;

                // top right
                if (xsi < width - 1) {
                    w = wx * wy1;
                    idx = getPixel(xsi + 1, ysi) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                }  // else assume transparent black

                // bottom left
                if (ysi < height - 1) {
                    w = wx1 * wy;
                    idx = getPixel(xsi, ysi + 1) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                } // else assume transparent black

                // bottom right
                if ((ysi < height - 1) && (xsi < width - 1)) {
                    w = wx * wy;
                    idx = getPixel(xsi + 1, ysi + 1) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;

                } // else assume transparent black

                int ati = (int)(at + 0.5);
                int rti = (int)(rt + 0.5);
                int gti = (int)(gt + 0.5);
                int bti = (int)(bt + 0.5);

                // find color index in palette

                // already known ?
                int colIdx = lastColIdx;
                if (ati != lastA || rti != lastR || gti != lastG || bti!=lastB) {
                    int minDistance = 0xffffff; // init > 0xff*0xff*4 = 0x03f804
                    for (idx = 0; idx < pal.getSize(); idx++) {
                        // distance vector (skip sqrt)
                        int ad = ati - (a[idx] & 0xff);
                        int rd = rti - (r[idx] & 0xff);
                        int gd = gti - (g[idx] & 0xff);
                        int bd = bti - (b[idx] & 0xff);
                        int distance = rd * rd + gd * gd + bd * bd + ad * ad;
                        // new minimum distance ?
                        if ( distance < minDistance) {
                            colIdx = idx;
                            minDistance = distance;
                            if (minDistance == 0) {
                                break;
                            }
                        }
                    }
                    // remember values
                    lastA = ati;
                    lastR = rti;
                    lastG = gti;
                    lastB = bti;
                    lastColIdx = colIdx;
                }
                // write target pixel
                trg.setPixel(xt, yt, (byte)colIdx);

            }
        }
        return trg;
    }

    /** Scales a palettized Bitmap to a Bitmap with a new quantized Palette using bilinear filtering.
     * @param sizeX  Target width
     * @param sizeY  Target height
     * @param pal    Palette of the source Bitmap
     * @param dither True: apply dithering
     * @return Scaled Bitmap and new Palette
     */
    public BitmapWithPalette scaleBilinear(final int sizeX, final int sizeY, final Palette pal, final boolean dither) {
        final byte[] r = pal.getR();
        final byte[] g = pal.getG();
        final byte[] b = pal.getB();
        final byte[] a = pal.getAlpha();

        final double scaleX = (double)(width - 1) / (sizeX - 1);
        final double scaleY = (double)(height - 1) / (sizeY - 1);

        final int[] trg = new int[sizeX * sizeY];

        for (int yt = 0; yt < sizeY; yt++) {
            double ys = yt * scaleY; // source coordinate
            int ysi = (int)ys;
            double wy = (ys - ysi);
            double wy1 = 1.0 - wy;
            int ofsY = yt * sizeX;

            for (int xt = 0; xt < sizeX; xt++) {
                double xs = xt * scaleX; // source coordinate
                int xsi = (int)xs;
                double wx = (xs - xsi); // weight factor

                int idx;

                // interpolate pixel
                double wx1 = 1.0 - wx;
                int x,y;

                // top left
                double w = wx1 * wy1;
                idx = getPixel(xsi, ysi) & 0xff;
                double at = (a[idx] & 0xff) * w;
                double rt = (r[idx] & 0xff) * w;
                double gt = (g[idx] & 0xff) * w;
                double bt = (b[idx] & 0xff) * w;

                // top right
                x = xsi + 1;
                if (x < width) {
                    w = wx * wy1;
                    idx = getPixel(x, ysi) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                }  // else assume transparent black

                // bottom left
                y = ysi + 1;
                if (y < height) {
                    w = wx1 * wy;
                    idx = getPixel(xsi, y) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                } // else assume transparent black

                // bottom right
                x = xsi + 1;
                y = ysi + 1;
                if ((x < width) && (y < height)) {
                    w = wx * wy;
                    idx = getPixel(x, y) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                } // else assume transparent black

                int ati = (int)(at);
                int rti = (int)(rt);
                int gti = (int)(gt);
                int bti = (int)(bt);

                trg[xt + ofsY] = ((ati << 24) | (rti << 16) | (gti << 8) | bti);
            }
        }
        // quantize image
        QuantizeFilter qf = new QuantizeFilter();
        final Bitmap bm = new Bitmap(sizeX, sizeY);
        int[] ct = qf.quantize(trg, bm.buffer, sizeX, sizeY, 255, dither, dither);
        int size = ct.length;
        if (size > 255) {
            size = 255;
            logger.warn("Quantizer failed.\n");
        }
        // create palette
        Palette trgPal = new Palette(256);
        for (int i = 0; i < size; i++) {
            trgPal.setARGB(i,ct[i]);
        }

        return new BitmapWithPalette(bm, trgPal);
    }

    /** Scales a palettized Bitmap to a Bitmap with the same Palette using a given scaling filter.
     * @param sizeX Target width
     * @param sizeY Target height
     * @param pal   Palette of the source Bitmap
     * @param f     Filter for scaling
     * @return Scaled Bitmap which uses the same Palette as the source Bitmap.
     */
    public Bitmap scaleFilter(final int sizeX, final int sizeY, final Palette pal, final ResampleFilter f) {
        final byte[] r = pal.getR();
        final byte[] g = pal.getG();
        final byte[] b = pal.getB();
        final byte[] a = pal.getAlpha();

        final int[] trg = new FilterOp(f, sizeX, sizeY).filter(this, pal);

        final Bitmap bm = new Bitmap(sizeX, sizeY);

        // select nearest colors in existing palette
        HashMap<Integer, Integer> p = new HashMap<Integer, Integer>();

        for (int i = 0; i < trg.length; i++) {
            int color = trg[i];
            int colIdx;
            Integer idxEx = p.get(color);
            if (idxEx != null) {
                colIdx = idxEx;
            } else {
                colIdx = 0;
                int minDistance = 0xffffff; // init > 0xff*0xff*4 = 0x03f804
                int alpha = (color >> 24) & 0xff;
                int red   = (color >> 16) & 0xff;
                int green = (color >>  8) & 0xff;
                int blue  =  color        & 0xff;
                for (int idx = 0; idx < pal.getSize(); idx++) {
                    // distance vector (skip sqrt)
                    int ad = alpha - (a[idx] & 0xff);
                    int rd = red   - (r[idx] & 0xff);
                    int gd = green - (g[idx] & 0xff);
                    int bd = blue  - (b[idx] & 0xff);
                    int distance = rd * rd + gd * gd + bd * bd + ad * ad;
                    // new minimum distance ?
                    if ( distance < minDistance) {
                        colIdx = idx;
                        minDistance = distance;
                        if (minDistance == 0) {
                            break;
                        }
                    }
                }
                p.put(color, colIdx);
            }
            // write target pixel
            bm.buffer[i] = (byte)colIdx;
        }
        return bm;
    }

    /** Scales a palettized Bitmap to a Bitmap with a new quantized Palette using a given scaling filter.
     * @param sizeX  Target width
     * @param sizeY  Target height
     * @param pal    Palette of the source Bitmap
     * @param f      Filter for scaling
     * @param dither True: apply dithering
     * @return Scaled Bitmap and new Palette
     */
    public BitmapWithPalette scaleFilter(final int sizeX, final int sizeY, final Palette pal, final ResampleFilter f, final boolean dither) {
        final int[] trg = new FilterOp(f, sizeX, sizeY).filter(this, pal);

        // quantize image
        QuantizeFilter qf = new QuantizeFilter();
        final Bitmap bm = new Bitmap(sizeX, sizeY);
        int[] ct = qf.quantize(trg, bm.buffer, sizeX, sizeY, 255, dither, dither);
        int size = ct.length;
        if (size > 255) {
            size = 255;
            logger.warn("Quantizer failed.\n");
        }
        // create palette
        Palette trgPal = new Palette(256);
        for (int i = 0; i < size; i++) {
            trgPal.setARGB(i, ct[i]);
        }

        return new BitmapWithPalette(bm, trgPal);
    }

    public int[] toARGB(Palette pal) {
        int[] argbValues = new int[buffer.length];
        for (int i = 0; i < argbValues.length; i++) {
            argbValues[i] = pal.getARGB(buffer[i] & 0xff);
        }
        return argbValues;
    }

    public Bitmap crop(int xOffset, int yOffset, int croppedBitmapWidth, int croppedBitmapHeight) {
        Bitmap bitmap = new Bitmap(croppedBitmapWidth, croppedBitmapHeight);

        int yOfsSrc = yOffset * width;
        int yOfsTrg = 0;
        for (int i = 0; i < croppedBitmapHeight; i++, yOfsSrc += width, yOfsTrg += croppedBitmapWidth) {
            for (int xTrg = 0; xTrg < croppedBitmapWidth; xTrg++) {
                bitmap.buffer[xTrg + yOfsTrg] = buffer[xOffset + xTrg + yOfsSrc];
            }
        }
        return bitmap;
    }

    public BitmapBounds getCroppingBounds(byte[] alpha, int alphaThreshold) {
        int xMin, xMax, yMin, yMax;

        // search lower bound
        yMax = height - 1;
        int yOfs = yMax * width;
        for (int y = height - 1; y > 0; y--, yOfs -= width) {
            yMax = y;
            if (isRowWithColorAboveAlphaThreshold(yOfs, alpha, alphaThreshold)) {
                break;
            }
        }

        // search upper bound
        yMin = 0;
        yOfs = 0;
        for (int y = 0; y < yMax; y++, yOfs += width) {
            yMin = y;
            if (isRowWithColorAboveAlphaThreshold(yOfs, alpha, alphaThreshold)) {
                break;
            }
        }

        // search right bound
        xMax = width-1;
        for (int x = width - 1; x > 0; x--) {
            xMax = x;
            if (isColumnWithColorAboveAlphaThreshold(x, yMin, yMax, alpha, alphaThreshold)) {
                break;
            }
        }

        // search left bound
        xMin = 0;
        for (int x = 0; x < xMax; x++) {
            xMin = x;
            if (isColumnWithColorAboveAlphaThreshold(x, yMin, yMax, alpha, alphaThreshold)) {
                break;
            }
        }

        return new BitmapBounds(xMin, xMax, yMin, yMax);
    }

    private boolean isRowWithColorAboveAlphaThreshold(int yOfs, byte[] alpha, int alphaThreshold) {
        for (int x = 0; x < width; x++) {
            if (isColorAboveAlphaThreshold(yOfs + x, alpha, alphaThreshold)) {
                return true;
            }
        }
        return false;
    }

    private boolean isColumnWithColorAboveAlphaThreshold(int x, int yMin, int yMax, byte[] alpha, int alphaThreshold) {
        int yOfs = yMin * width;
        for (int i = yMin; i < yMax; i++, yOfs += width) {
            if (isColorAboveAlphaThreshold(yOfs + x, alpha, alphaThreshold)) {
                return true;
            }
        }
        return false;
    }

    private boolean isColorAboveAlphaThreshold(int bufferIndex, byte[] alpha, int alphaThreshold) {
        int idx = buffer[bufferIndex] & 0xff;
        return (alpha[idx] & 0xff) >= alphaThreshold;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public byte[] getInternalBuffer() {
        return buffer;
    }

    private byte getPixel(int x, int y) {
        return buffer[x + width * y];
    }

    private void setPixel(int x, int y, byte color) {
        buffer[x + width * y] = color;
    }
} public class BitmapWithPalette {

    public final Bitmap bitmap;
    public final Palette palette;

    public BitmapWithPalette(Bitmap bitmap, Palette palette) {
        this.bitmap = bitmap;
        this.palette = palette;
    }
} public class ErasePatch {

    public final int x;
    public final int y;
    public final int width;
    public final int height;

    public ErasePatch(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
} public class Palette {
    /** Number of palette entries */
    private final int size;
    /** Byte buffer for RED info */
    private final byte[] r;
    /** Byte buffer for GREEN info */
    private final byte[] g;
    /** Byte buffer for BLUE info */
    private final byte[] b;
    /** Byte buffer for alpha info */
    private final byte[] a;
    /** Byte buffer for Y (luminance) info */
    private final byte[] y;
    /** Byte buffer for Cb (chrominance blue) info */
    private final byte[] cb;
    /** Byte buffer for Cr (chrominance red) info */
    private final byte[] cr;
    /** Use BT.601 color model instead of BT.709 */
    private final boolean useBT601;

    /**
     * Initializes palette with transparent black (RGBA: 0x00000000)
     * @param size Number of palette entries
     */
    public Palette(int size, boolean useBT601) {
        this.size = size;
        this.useBT601 = useBT601;
        r  = new byte[size];
        g  = new byte[size];
        b  = new byte[size];
        a  = new byte[size];
        y  = new byte[size];
        cb = new byte[size];
        cr = new byte[size];

        // set at least all alpha values to invisible
        int[] yCbCr = RGB2YCbCr(0, 0, 0, useBT601);
        for (int i=0; i < size; i++) {
            a[i]  = 0;
            r[i]  = 0;
            g[i]  = 0;
            b[i]  = 0;
            y[i]  = (byte)yCbCr[0];
            cb[i] = (byte)yCbCr[1];
            cr[i] = (byte)yCbCr[2];
        }
    }

    /**
     * Initializes a palette with transparent black (RGBA: 0x00000000)
     */
    public Palette(int size) {
        this(size, false);
    }

    public Palette(byte[] red, byte[] green, byte[] blue, byte[] alpha, boolean useBT601) {
        size = red.length;
        this.useBT601 = useBT601;
        r  = new byte[size];
        g  = new byte[size];
        b  = new byte[size];
        a  = new byte[size];
        y  = new byte[size];
        cb = new byte[size];
        cr = new byte[size];

        int yCbCr[];
        for (int i=0; i<size; i++) {
            a[i]  = alpha[i];
            r[i]  = red[i];
            g[i]  = green[i];
            b[i]  = blue[i];
            yCbCr = RGB2YCbCr(r[i]&0xff, g[i]&0xff, b[i]&0xff, useBT601);
            y[i]  = (byte)yCbCr[0];
            cb[i] = (byte)yCbCr[1];
            cr[i] = (byte)yCbCr[2];
        }
    }

    /**
     * Constructs a palette from red, green blue and alpha buffers using BT.709
     */
    public Palette(byte[] red, byte[] green, byte[] blue, byte[] alpha) {
        this(red, green, blue, alpha, false);
    }

    public Palette(Palette palette) {
        size = palette.getSize();
        useBT601 = palette.usesBT601();
        r  = new byte[size];
        g  = new byte[size];
        b  = new byte[size];
        a  = new byte[size];
        y  = new byte[size];
        cb = new byte[size];
        cr = new byte[size];

        for (int i = 0; i < size; i++) {
            a[i]  = palette.a[i];
            r[i]  = palette.r[i];
            g[i]  = palette.g[i];
            b[i]  = palette.b[i];
            y[i]  = palette.y[i];
            cb[i] = palette.cb[i];
            cr[i] = palette.cr[i];
        }
    }

    public ColorModel getColorModel() {
        return new IndexColorModel(8, size, r, g, b, a);
    }

    public void setColor(int index, Color c) {
        setRGB(index, c.getRed(), c.getGreen(), c.getBlue());
        setAlpha(index, c.getAlpha());
    }

    public void setARGB(int index, int c) {
        setColor(index, new Color(c, true));
    }

    public Color getColor(int index) {
        return new Color(r[index] & 0xff, g[index] & 0xff, b[index] & 0xff, a[index] & 0xff);
    }

    public int getARGB(int index) {
        return ((a[index] & 0xff) << 24) | ((r[index] & 0xff) << 16) | (( g[index] & 0xff) << 8) | (b[index] & 0xff) ;
    }

    public void setRGB(int index, int red, int green, int blue) {
        r[index] = (byte)red;
        g[index] = (byte)green;
        b[index] = (byte)blue;

        int yCbCr[] = RGB2YCbCr(red, green, blue, useBT601);
        y[index]  = (byte)yCbCr[0];
        cb[index] = (byte)yCbCr[1];
        cr[index] = (byte)yCbCr[2];
    }

    public void setYCbCr(int index, int yn, int cbn, int crn) {
        y[index]  = (byte)yn;
        cb[index] = (byte)cbn;
        cr[index] = (byte)crn;

        int rgb[] = ColorSpaceUtils.YCbCr2RGB(yn, cbn, crn, useBT601);
        r[index] = (byte)rgb[0];
        g[index] = (byte)rgb[1];
        b[index] = (byte)rgb[2];
    }

    public void setAlpha(int index, int alpha) {
        a[index] = (byte)alpha;
    }

    public int getAlpha(int index) {
        return a[index] & 0xff;
    }

    public byte[] getAlpha() {
        return Arrays.copyOf(a, a.length);
    }

    public int[] getRGB(int index) {
        int rgb[] = new int[3];
        rgb[0] = r[index] & 0xff;
        rgb[1] = g[index] & 0xff;
        rgb[2] = b[index] & 0xff;
        return rgb;
    }

    public int[] getYCbCr(final int index) {
        int yCbCr[] = new int[3];
        yCbCr[0] = y[index]  & 0xff;
        yCbCr[1] = cb[index] & 0xff;
        yCbCr[2] = cr[index] & 0xff;
        return yCbCr;
    }

    public byte[] getR() {
        return Arrays.copyOf(r, r.length);
    }

    public byte[] getG() {
        return Arrays.copyOf(g, g.length);
    }

    public byte[] getB() {
        return Arrays.copyOf(b, b.length);
    }

    public byte[] getY() {
        return Arrays.copyOf(y, y.length);
    }

    public byte[] getCb() {
        return Arrays.copyOf(cb, cb.length);
    }

    public byte[] getCr() {
        return Arrays.copyOf(cr, cr.length);
    }

    public int getSize() {
        return size;
    }

    public int getIndexOfMostTransparentPaletteEntry() {
        int transpIdx = 0;
        int minAlpha = 0x100;
        for (int i = 0; i < size; i++ ) {
            if ((a[i] & 0xff) < minAlpha) {
                minAlpha = a[i] & 0xff;
                transpIdx = i;
                if (minAlpha == 0) {
                    break;
                }
            }
        }
        return transpIdx;
    }

    public boolean usesBT601() {
        return useBT601;
    }
} public class Progress extends JDialog {

    private JPanel jContentPane;
    private JButton jButtonCancel;
    private JProgressBar jProgressBar;
    private JLabel jLabelProgress;

    private Timer timer;


    public Progress(Frame owner) {
        super(owner, true);
        initialize();
        centerRelativeToOwner(this);
    }

    private void initialize() {
        setMinimumSize(new Dimension(224, 139));
        setResizable(false);
        setBounds(new Rectangle(0, 0, 224, 139));
        setMaximumSize(new Dimension(224, 139));
        setContentPane(getJContentPane());
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                Core.cancel();
            }
        });
    }

    @Override
    public void setVisible(boolean b) {
        timer = new Timer();
        timer.schedule(new ProgressTimer(), 200, 200);
        super.setVisible(b);
    }

    public void setText(String s) {
        jLabelProgress.setText(s);
    }

    private JPanel getJContentPane() {
        if (jContentPane == null) {
            GridBagConstraints gridBagBtnCancel = new GridBagConstraints();
            gridBagBtnCancel.insets = new Insets(8, 8, 8, 8);
            gridBagBtnCancel.gridx = 0;
            gridBagBtnCancel.gridy = 2;
            GridBagConstraints gridBagProgressBar = new GridBagConstraints();
            gridBagProgressBar.gridy = 1;
            gridBagProgressBar.fill = GridBagConstraints.VERTICAL;
            gridBagProgressBar.insets = new Insets(8, 8, 8, 8);
            gridBagProgressBar.gridx = 0;
            GridBagConstraints gridBagConstraints = new GridBagConstraints();
            gridBagConstraints.gridx = 0;
            gridBagConstraints.fill = GridBagConstraints.VERTICAL;
            gridBagConstraints.insets = new Insets(8, 8, 8, 8);
            gridBagConstraints.gridy = 0;
            jLabelProgress = new JLabel();
            jLabelProgress.setText("Exporting SUB/IDX");
            jContentPane = new JPanel();
            jContentPane.setLayout(new GridBagLayout());
            jContentPane.add(jLabelProgress, gridBagConstraints);
            jContentPane.add(getJProgressBar(), gridBagProgressBar);
            jContentPane.add(getJButtonCancel(), gridBagBtnCancel);
        }
        return jContentPane;
    }

    private JButton getJButtonCancel() {
        if (jButtonCancel == null) {
            jButtonCancel = new JButton();
            jButtonCancel.setText("Cancel");
            jButtonCancel.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    Core.cancel();
                }
            });
        }
        return jButtonCancel;
    }

    private JProgressBar getJProgressBar() {
        if (jProgressBar == null) {
            jProgressBar = new JProgressBar();
            jProgressBar.setPreferredSize(new Dimension(200, 20));
            jProgressBar.setMinimumSize(new Dimension(200, 20));
            jProgressBar.setStringPainted(true);
        }
        return jProgressBar;
    }

    public void setProgress(final int val) {
        jProgressBar.setValue(val);
        jProgressBar.repaint();
    }

    private class ProgressTimer extends TimerTask {
        @Override
        public void run() {
            if (Core.getStatus() != CoreThreadState.ACTIVE) {
                timer.cancel();
                dispose();
            }
        }
    }
} public class SubPicture {

    /** width of subtitle image */
    private int imageWidth;
    /** height of subtitle image */
    private int imageHeight;
    /** upper left corner of subtitle x */
    private int xOffset;
    /** upper left corner of subtitle y */
    private int yOffset;

    /** screen width */
    private int width;
    /** screen height */
    private int height;
    /** start time in milliseconds */
    private long startTime;
    /** end time in milliseconds */
    private long endTime;
    /** if true, this is a forced subtitle */
    private boolean forced;
    /** composition number - increased at start and end PCS */
    private int compositionNumber;
    /** frame was already decoded */
    private boolean wasDecoded;

    /* the following fields are really only needed for editing */

    /** exclude from export? */
    private boolean excluded;
    /** list of erase patches */
    private List<ErasePatch> erasePatch = new ArrayList<ErasePatch>();

    public SubPicture() {
    }

    public SubPicture(SubPicture other) {
        /* Note that by using the getter functions
         * the internal values of a SubPictureBD are
         * copied into the plain members of the
         * SubPicture object.
         */
        this.imageWidth = other.getImageWidth();
        this.imageHeight = other.getImageHeight();
        this.xOffset = other.getXOffset();
        this.yOffset = other.getYOffset();
        this.imageWidth = other.imageWidth;
        this.imageHeight = other.imageHeight;
        this.xOffset = other.xOffset;
        this.yOffset = other.yOffset;

        this.width = other.width;
        this.height = other.height;
        this.startTime = other.startTime;
        this.endTime = other.endTime;
        this.forced = other.forced;
        this.compositionNumber = other.compositionNumber;
        this.wasDecoded = other.wasDecoded;
        this.excluded = other.excluded;
        if (other.erasePatch != null) {
            this.erasePatch = new ArrayList<ErasePatch>(other.erasePatch);
        }
    }

    public int getImageWidth() {
        return imageWidth;
    }

    public int getImageHeight() {
        return imageHeight;
    }

    public int getXOffset() {
        return xOffset;
    }

    public int getYOffset() {
        return yOffset;
    }

    public void setImageWidth(int w) {
        imageWidth = w;
    }

    public void setImageHeight(int h) {
        imageHeight = h;
    }

    public void setOfsX(int ofs) {
        xOffset = ofs;
    }

    public void setOfsY(int ofs) {
        yOffset = ofs;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public long getStartTime() {
        return startTime;
    }

    public void setStartTime(long startTime) {
        this.startTime = startTime;
    }

    public long getEndTime() {
        return endTime;
    }

    public void setEndTime(long endTime) {
        this.endTime = endTime;
    }

    public boolean isForced() {
        return forced;
    }

    public void setForced(boolean forced) {
        this.forced = forced;
    }

    public int getCompositionNumber() {
        return compositionNumber;
    }

    public void setCompositionNumber(int compositionNumber) {
        this.compositionNumber = compositionNumber;
    }

    public boolean isWasDecoded() {
        return wasDecoded;
    }

    public void setWasDecoded(boolean wasDecoded) {
        this.wasDecoded = wasDecoded;
    }

    public boolean isExcluded() {
        return excluded;
    }

    public void setExcluded(boolean excluded) {
        this.excluded = excluded;
    }

    public List<ErasePatch> getErasePatch() {
        return erasePatch;
    }

    public void setErasePatch(List<ErasePatch> erasePatch) {
        this.erasePatch = erasePatch;
    }
} public interface SubtitleStream {

    /**
     * Return the Palette of the current (last decoded) frame.
     * @return Palette of the current frame
     */
    Palette getPalette();

    /**
     * Return the Bitmap of the current (last decoded) frame.
     * @return Bitmap of the current frame
     */
    Bitmap getBitmap();

    /**
     * Return current (last decoded) frame
     * @return Current (last decoded) frame
     */
    BufferedImage getImage();

    /**
     * Return given Bitmap as BufferedImage (using current Palette).
     * @param bitmap Bitmap to convert.
     * @return BufferedImage of given bitmap
     */
    BufferedImage getImage(Bitmap bitmap);

    /**
     * Get index of most dominant opaque color (for DVD subtitle export).
     * @return Index of most dominant opaque color
     */
    int getPrimaryColorIndex();

    /**
     * Decode caption.
     * @param index Index of caption
     * @throws CoreException
     */
    void decode(int index) throws CoreException;

    /**
     * Get number of frames in the currently loaded subtitle stream.
     * @return Number of frames
     */
    int getFrameCount();

    /**
     * Get number of forced frames in the currently loaded subtitle stream.
     * @return Number of forced frames
     */
    int getForcedFrameCount();

    /**
     * Get forced flag of given frame.
     * @param index Index of caption
     * @return Forced flag of given frame
     */
    boolean isForced(int index);

    /**
     * Close input stream.
     */
    void close();

    /**
     * Get end time stamp of given frame.
     * @param index Index of caption
     * @return End time stamp of given frame in 90kHz resolution
     */
    long getEndTime(int index);

    /**
     * Get start time stamp of given frame.
     * @param index Index of caption
     * @return Start time stamp of given frame in 90kHz resolution
     */
    long getStartTime(int index);

    /**
     * Get start offset (in input stream) of given frame.
     * @param index Index of caption
     * @return Start offset of given frame in input stream
     */
    long getStartOffset(int index);

    /**
     * Get SubPicture of given frame.
     * @param index Index of caption
     * @return SubPicture of caption
     */
    SubPicture getSubPicture(int index);
} public class SupBD implements SubtitleStream {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger logger = Logger.getInstance();

    private FileBuffer buffer;
    private List<SubPictureBD> subPictures = new ArrayList<SubPictureBD>();
    private int forcedFrameCount;

    /** color palette of the last decoded caption  */
    private Palette palette;
    /** bitmap of the last decoded caption  */
    private Bitmap bitmap;
    /** index of dominant color for the current caption  */
    private int primaryColorIndex;

    public SupBD(String filename) throws CoreException {
        SupBDParser parser = new SupBDParser(filename);
        buffer = parser.getBuffer();
        subPictures = parser.getSubPictures();
        forcedFrameCount = parser.getForcedFrameCount();
    }

    /**
     * Decode caption from the input stream.
     * @param subPictureBD SubPicture object containing info about the caption
     * @param transparentColorIndex index of the transparent color
     * @return bitmap of the decoded caption
     * @throws CoreException
     */
    private Bitmap decodeImage(SubPictureBD subPictureBD, int transparentColorIndex) throws CoreException {
        int width = subPictureBD.getImageWidth();
        int height = subPictureBD.getImageHeight();
        // always decode image obj 0, start with first entry in fragment list
        ImageObjectFragment imageObjectFragment = subPictureBD.getImageObject().getFragmentList().get(0);
        long startOfs = imageObjectFragment.getImageBufferOfs();

        if (width > subPictureBD.getWidth() || height > subPictureBD.getHeight()) {
            throw new CoreException("Subpicture too large: " + width + "x" + height + " at offset " + ToolBox.toHexLeftZeroPadded(startOfs, 8));
        }

        Bitmap bm = new Bitmap(width, height, (byte)transparentColorIndex);

        int b;
        int index = 0;
        int ofs = 0;
        int size;
        int xpos = 0;

        try {
            // just for multi-packet support, copy all of the image data in one common buffer
            byte[] buffer = new byte[subPictureBD.getImageObject().getBufferSize()];
            index = 0;

            for (ImageObjectFragment fragment : subPictureBD.getImageObject().getFragmentList()) {
                // copy data of all packet to one common buffer
                imageObjectFragment = fragment;
                for (int i=0; i < imageObjectFragment.getImagePacketSize(); i++) {
                    buffer[index+i] = (byte) this.buffer.getByte(imageObjectFragment.getImageBufferOfs() + i);
                }
                index += imageObjectFragment.getImagePacketSize();
            }

            index = 0;

            do {
                b = buffer[index++] & 0xff;
                if (b == 0) {
                    b = buffer[index++] & 0xff;
                    if (b == 0) {
                        // next line
                        ofs = (ofs/width) * width;
                        if (xpos < width) {
                            ofs += width;
                        }
                        xpos = 0;
                    } else {
                        if ( (b & 0xC0) == 0x40) {
                            // 00 4x xx -> xxx zeroes
                            size = ((b - 0x40) << 8) + (buffer[index++] & 0xff);
                            for (int i=0; i < size; i++) {
                                bm.getInternalBuffer()[ofs++] = 0; /*(byte)b;*/
                            }
                            xpos += size;
                        } else if ((b & 0xC0) == 0x80) {
                            // 00 8x yy -> x times value y
                            size = (b-0x80);
                            b = buffer[index++]&0xff;
                            for (int i=0; i < size; i++) {
                                bm.getInternalBuffer()[ofs++] = (byte)b;
                            }
                            xpos += size;
                        } else if  ((b & 0xC0) != 0) {
                            // 00 cx yy zz -> xyy times value z
                            size = ((b - 0xC0) << 8) + (buffer[index++] & 0xff);
                            b = buffer[index++] & 0xff;
                            for (int i=0; i < size; i++) {
                                bm.getInternalBuffer()[ofs++] = (byte)b;
                            }
                            xpos += size;
                        }  else {
                            // 00 xx -> xx times 0
                            for (int i=0; i < b; i++) {
                                bm.getInternalBuffer()[ofs++] = 0;
                            }
                            xpos += b;
                        }
                    }
                } else {
                    bm.getInternalBuffer()[ofs++] = (byte)b;
                    xpos++;
                }
            } while (index < buffer.length);

            return bm;
        } catch (FileBufferException ex) {
            throw new CoreException(ex.getMessage());
        } catch (ArrayIndexOutOfBoundsException ex) {
            logger.warn("Problems during RLE decoding of picture OBJ at offset " + ToolBox.toHexLeftZeroPadded(startOfs + index, 8) + "\n");
            return bm;
        }
    }

    /**
     * decode palette from the input stream
     * @param pic SubPicture object containing info about the current caption
     * @return
     * @throws CoreException
     */
    private Palette decodePalette(SubPictureBD pic) throws CoreException {
        boolean fadeOut = false;
        int paletteIndex;
        List<PaletteInfo> paletteInfos = pic.getPalettes().get(pic.getImageObject().getPaletteID());
        if (paletteInfos == null) {
            throw new CoreException("Palette ID out of bounds.");
        }

        Palette palette = new Palette(256, Core.usesBT601());
        // by definition, index 0xff is always completely transparent
        // also all entries must be fully transparent after initialization

        try {
            for (PaletteInfo paletteInfo : paletteInfos) {
                int index = paletteInfo.getPaletteOffset();
                for (int i = 0; i < paletteInfo.getPaletteSize(); i++) {
                    // each palette entry consists of 5 bytes
                    paletteIndex = buffer.getByte(index);
                    int y = buffer.getByte(++index);
                    int cr, cb;
                    if (configuration.isSwapCrCb()) {
                        cb = buffer.getByte(++index);
                        cr = buffer.getByte(++index);
                    } else {
                        cr = buffer.getByte(++index);
                        cb = buffer.getByte(++index);
                    }
                    int alpha = buffer.getByte(++index);

                    int alphaOld = palette.getAlpha(paletteIndex);
                    // avoid fading out
                    if (alpha >= alphaOld) {
                        if (alpha < configuration.getAlphaCrop()) {// to not mess with scaling algorithms, make transparent color black
                            y = 16;
                            cr = 128;
                            cb = 128;
                        }
                        palette.setAlpha(paletteIndex, alpha);
                    } else {
                        fadeOut = true;
                    }

                    palette.setYCbCr(paletteIndex, y, cb, cr);
                    index++;
                }
            }
            if (fadeOut) {
                logger.warn("fade out detected -> patched palette\n");
            }
            return palette;
        } catch (FileBufferException ex) {
            throw new CoreException(ex.getMessage());
        }
    }

    /**
     * decode given picture
     * @param pic SubPicture object containing info about caption
     * @throws CoreException
     */
    private void decode(SubPictureBD pic)  throws CoreException {
        palette = decodePalette(pic);
        bitmap  = decodeImage(pic, palette.getIndexOfMostTransparentPaletteEntry());
        primaryColorIndex = bitmap.getPrimaryColorIndex(palette.getAlpha(), configuration.getAlphaThreshold(), palette.getY());
    }

    /* (non-Javadoc)
     * @see SubtitleStream#decode(int)
     */
    public void decode(int index) throws CoreException {
        if (index < subPictures.size()) {
            decode(subPictures.get(index));
        } else {
            throw new CoreException("Index "+index+" out of bounds\n");
        }
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getPalette()
     */
    public Palette getPalette() {
        return palette;
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getBitmap()
     */
    public Bitmap getBitmap() {
        return bitmap;
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getImage()
     */
    public BufferedImage getImage() {
        return bitmap.getImage(palette.getColorModel());
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getImage(Bitmap)
     */
    public BufferedImage getImage(Bitmap bm) {
        return bm.getImage(palette.getColorModel());
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getPrimaryColorIndex()
     */
    public int getPrimaryColorIndex() {
        return primaryColorIndex;
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getSubPicture(int)
     */
    public SubPicture getSubPicture(int index) {
        return subPictures.get(index);
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getNumFrames()
     */
    public int getFrameCount() {
        return subPictures.size();
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getForcedFrameCount()
     */
    public int getForcedFrameCount() {
        return forcedFrameCount;
    }

    /* (non-Javadoc)
     * @see SubtitleStream#close()
     */
    public void close() {
        if (buffer != null) {
            buffer.close();
        }
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getEndTime(int)
     */
    public long getEndTime(int index) {
        return subPictures.get(index).getEndTime();
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getStartTime(int)
     */
    public long getStartTime(int index) {
        return subPictures.get(index).getStartTime();
    }

    /* (non-Javadoc)
     * @see SubtitleStream#isForced(int)
     */
    public boolean isForced(int index) {
        return subPictures.get(index).isForced();
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getStartOffset(int)
     */
    public long getStartOffset(int index) {
        SubPictureBD pic = subPictures.get(index);
        return pic.getImageObject().getFragmentList().get(0).getImageBufferOfs();
    }

    /**
     * Get frame rate for given caption
     * @param index index of caption
     * @return frame rate
     */
    public double getFps(int index) {
        return Framerate.valueForId(subPictures.get(index).getType());
    }
} public final class SupBDWriter {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger logger = Logger.getInstance();

    private static final byte PACKET_HEADER[] = {
            0x50, 0x47,             // 0:  "PG"
            0x00, 0x00, 0x00, 0x00, // 2:  PTS - presentation time stamp
            0x00, 0x00, 0x00, 0x00, // 6:  DTS - decoding time stamp
            0x00,                   // 10: segment_type
            0x00, 0x00,             // 11: segment_length (bytes following till next PG)
    };

    private static final byte HEADER_PCS_START[] = {
            0x00, 0x00, 0x00, 0x00, // 0: video_width, video_height
            0x10,                   // 4: hi nibble: frame_rate (0x10=24p), lo nibble: reserved
            0x00, 0x00,             // 5: composition_number (increased by start and end header)
            (byte)0x80,             // 7: composition_state (0x80: epoch start)
            0x00,                   // 8: palette_update_flag (0x80), 7bit reserved
            0x00,                   // 9: palette_id_ref (0..7)
            0x01,                   // 10: number_of_composition_objects (0..2)
            0x00, 0x00,             // 11: 16bit object_id_ref
            0x00,                   // 13: window_id_ref (0..1)
            0x00,                   // 14: object_cropped_flag: 0x80, forced_on_flag = 0x040, 6bit reserved
            0x00, 0x00, 0x00, 0x00  // 15: composition_object_horizontal_position, composition_object_vertical_position
    };

    private static final byte HEADER_PCS_END[] = {
            0x00, 0x00, 0x00, 0x00, // 0: video_width, video_height
            0x10,                   // 4: hi nibble: frame_rate (0x10=24p), lo nibble: reserved
            0x00, 0x00,             // 5: composition_number (increased by start and end header)
            0x00,                   // 7: composition_state (0x00: normal)
            0x00,                   // 8: palette_update_flag (0x80), 7bit reserved
            0x00,                   // 9: palette_id_ref (0..7)
            0x00,                   // 10: number_of_composition_objects (0..2)
    };


    private static final byte HEADER_ODS_FIRST[] = {
            0x00, 0x00,             // 0: object_id
            0x00,                   // 2: object_version_number
            (byte)0xC0,             // 3: first_in_sequence (0x80), last_in_sequence (0x40), 6bits reserved
            0x00, 0x00, 0x00,       // 4: object_data_length - full RLE buffer length (including 4 bytes size info)
            0x00, 0x00, 0x00, 0x00, // 7: object_width, object_height
    };

    private static final byte HEADER_ODS_NEXT[] = {
            0x00, 0x00,             // 0: object_id
            0x00,                   // 2: object_version_number
            (byte)0x40,             // 3: first_in_sequence (0x80), last_in_sequence (0x40), 6bits reserved
    };

    private static final byte HEADER_WDS[] = {
            0x01,                   // 0 : number of windows (currently assumed 1, 0..2 is legal)
            0x00,                   // 1 : window id (0..1)
            0x00, 0x00, 0x00, 0x00, // 2 : x-ofs, y-ofs
            0x00, 0x00, 0x00, 0x00  // 6 : width, height
    };

    private SupBDWriter() {
    }

    /**
     * Create the binary stream representation of one caption
     * @param pic SubPicture object containing caption info
     * @param bm bitmap
     * @param pal palette
     * @return byte buffer containing the binary stream representation of one caption
     */
    public static byte[] createSupFrame(SubPicture pic, Bitmap bm, Palette pal) {
        // the last palette entry must be transparent
        if (pal.getSize() > 255 && pal.getAlpha(255) > 0) {
            // quantize image
            QuantizeFilter qf = new QuantizeFilter();
            Bitmap bmQ = new Bitmap(bm.getWidth(), bm.getHeight());
            int[] ct = qf.quantize(bm.toARGB(pal), bmQ.getInternalBuffer(), bm.getWidth(), bm.getHeight(), 255, false, false);
            int size = ct.length;
            if (size > 255) {
                size = 255;
                logger.trace("Palette had to be reduced from " + pal.getSize() + " to " + size + " entries.\n");
                logger.warn("Quantizer failed.\n");
            } else {
                logger.trace("Palette had to be reduced from " + pal.getSize() + " to " + size + " entries.\n");
            }
            // create palette
            pal = new Palette(size);
            for (int i=0; i < size; i++) {
                pal.setARGB(i,ct[i]);
            }
            // use new bitmap
            bm = bmQ;
        }

        byte[] rleBuf = encodeImage(bm);

        // for some obscure reason, a packet can be a maximum 0xfffc bytes
        // since 13 bytes are needed for the header("PG", PTS, DTS, ID, SIZE)
        // there are only 0xffef bytes available for the packet
        // since the first ODS packet needs an additional 11 bytes for info
        // and the following ODS packets need 4 additional bytes, the
        // first package can store only 0xffe4 RLE buffer bytes and the
        // following packets can store 0xffeb RLE buffer bytes
        int numAddPackets;
        if (rleBuf.length <= 0xffe4) {
            numAddPackets = 0; // no additional packets needed;
        } else {
            numAddPackets = 1 + (rleBuf.length - 0xffe4) / 0xffeb;
        }

        // a typical frame consists of 8 packets. It can be enlonged by additional
        // object frames
        int palSize = bm.getHighestVisibleColorIndex(pal.getAlpha()) + 1;
        int size = PACKET_HEADER.length * (8 + numAddPackets);
        size += HEADER_PCS_START.length + HEADER_PCS_END.length;
        size += 2* HEADER_WDS.length + HEADER_ODS_FIRST.length;
        size += numAddPackets * HEADER_ODS_NEXT.length;
        size += (2 + palSize * 5) /* PDS */;
        size += rleBuf.length;

        int yOfs = pic.getYOffset() - configuration.getCropOffsetY();
        if (yOfs < 0) {
            yOfs = 0;
        } else {
            int yMax = pic.getHeight() - pic.getImageHeight() - 2 * configuration.getCropOffsetY();
            if (yOfs > yMax) {
                yOfs = yMax;
            }
        }

        int h = pic.getHeight() -2 * configuration.getCropOffsetY();

        byte[] buf = new byte[size];
        int index = 0;

        int fpsId = Framerate.idForFramerate(configuration.getFpsTrg());

        /* time (in 90kHz resolution) needed to initialize (clear) the screen buffer
           based on the composition pixel rate of 256e6 bit/s - always rounded up */
        int frameInitTime = (pic.getWidth() * pic.getHeight() * 9 + 3199) / 3200; // better use default height here
        /* time (in 90kHz resolution) needed to initialize (clear) the window area
           based on the composition pixel rate of 256e6 bit/s - always rounded up
           Note: no cropping etc. -> window size == image size */
        int windowInitTime = (bm.getWidth() * bm.getHeight() * 9 + 3199) / 3200;
        /* time (in 90kHz resolution) needed to decode the image
           based on the decoding pixel rate of 128e6 bit/s - always rounded up  */
        int imageDecodeTime = (bm.getWidth() * bm.getHeight() * 9 + 1599) / 1600;
        // write PCS start
        PACKET_HEADER[10] = 0x16;                               // ID
        setDWord(PACKET_HEADER, 2, (int) pic.getStartTime());   // PTS
        setDWord(PACKET_HEADER, 6, 0);                          // DTS (0)
        setWord(PACKET_HEADER, 11, HEADER_PCS_START.length);    // size
        for (byte b : PACKET_HEADER) {
            buf[index++] = b;
        }
        setWord(HEADER_PCS_START,0, pic.getWidth());
        setWord(HEADER_PCS_START, 2, h);                        // cropped height
        setByte(HEADER_PCS_START, 4, fpsId);
        setWord(HEADER_PCS_START, 5, pic.getCompositionNumber());
        HEADER_PCS_START[14] = (pic.isForced() ? (byte)0x40 : 0);
        setWord(HEADER_PCS_START,15, pic.getXOffset());
        setWord(HEADER_PCS_START,17, yOfs);
        for (byte b : HEADER_PCS_START) {
            buf[index++] = b;
        }

        // write WDS
        PACKET_HEADER[10] = 0x17;                                   // ID
        int timeStamp = (int) pic.getStartTime() - windowInitTime;
        setDWord(PACKET_HEADER, 2, timeStamp);                      // PTS
        setDWord(PACKET_HEADER, 6, 0);                              // DTS (0)
        setWord(PACKET_HEADER, 11, HEADER_WDS.length);              // size
        for (byte b : PACKET_HEADER) {
            buf[index++] = b;
        }
        setWord(HEADER_WDS, 2, pic.getXOffset());
        setWord(HEADER_WDS, 4, yOfs);
        setWord(HEADER_WDS, 6, bm.getWidth());
        setWord(HEADER_WDS, 8, bm.getHeight());
        for (byte b : HEADER_WDS) {
            buf[index++] = b;
        }

        // write PDS
        PACKET_HEADER[10] = 0x14;                       // ID
        int dts = (int) pic.getStartTime() - (frameInitTime + windowInitTime);
        setDWord(PACKET_HEADER, 2, dts);                // PTS
        setDWord(PACKET_HEADER, 6, 0);                  // DTS (0)
        setWord(PACKET_HEADER, 11, (2 + palSize * 5));  // size
        for (byte b : PACKET_HEADER) {
            buf[index++] = b;
        }
        buf[index++] = 0;
        buf[index++] = 0;
        for (int i=0; i < palSize; i++) {
            buf[index++] = (byte)i;             // index
            buf[index++] = pal.getY()[i];       // Y
            buf[index++] = pal.getCr()[i];      // Cr
            buf[index++] = pal.getCb()[i];      // Cb
            buf[index++] = pal.getAlpha()[i];   // Alpha
        }

        // write first OBJ
        int bufSize = rleBuf.length;
        int rleIndex = 0;
        if (bufSize > 0xffe4) {
            bufSize = 0xffe4;
        }
        PACKET_HEADER[10] = 0x15;                                       // ID
        timeStamp = dts + imageDecodeTime;
        setDWord(PACKET_HEADER, 2, timeStamp);                          // PTS
        setDWord(PACKET_HEADER, 6, 0);                                  // DTS (0)
        setWord(PACKET_HEADER, 11, HEADER_ODS_FIRST.length + bufSize);  // size
        for (byte b : PACKET_HEADER) {
            buf[index++] = b;
        }
        int marker = ((numAddPackets == 0) ? 0xC0000000 : 0x80000000);
        setDWord(HEADER_ODS_FIRST, 3, marker | (rleBuf.length + 4));
        setWord(HEADER_ODS_FIRST, 7, bm.getWidth());
        setWord(HEADER_ODS_FIRST, 9, bm.getHeight());
        for (byte b : HEADER_ODS_FIRST) {
            buf[index++] = b;
        }
        for (int i=0; i < bufSize; i++) {
            buf[index++] = rleBuf[rleIndex++];
        }

        // write additional OBJ packets
        bufSize = rleBuf.length-bufSize; // remaining bytes to write
        for (int p=0; p < numAddPackets; p++) {
            int psize = bufSize;
            if (psize > 0xffeb) {
                psize = 0xffeb;
            }
            PACKET_HEADER[10] = 0x15;                                   // ID (keep DTS & PTS)
            setWord(PACKET_HEADER, 11, HEADER_ODS_NEXT.length + psize); // size
            for (byte b : PACKET_HEADER) {
                buf[index++] = b;
            }
            for (byte b : HEADER_ODS_NEXT) {
                buf[index++] = b;
            }
            for (int i=0; i < psize; i++) {
                buf[index++] = rleBuf[rleIndex++];
            }
            bufSize -= psize;
        }

        // write END
        PACKET_HEADER[10] = (byte)0x80;                 // ID
        setDWord(PACKET_HEADER, 2, timeStamp);          // PTS
        setDWord(PACKET_HEADER, 6, 0);                  // DTS (0)
        setWord(PACKET_HEADER, 11, 0);                  // size
        for (byte b : PACKET_HEADER) {
            buf[index++] = b;
        }

        // write PCS end
        PACKET_HEADER[10] = 0x16;                               // ID
        setDWord(PACKET_HEADER, 2, (int) pic.getEndTime());     // PTS
        setDWord(PACKET_HEADER, 6, 0);                          // DTS (0)
        setWord(PACKET_HEADER, 11, HEADER_PCS_END.length);      // size
        for (byte b : PACKET_HEADER) {
            buf[index++] = b;
        }
        setWord(HEADER_PCS_END,0, pic.getWidth());
        setWord(HEADER_PCS_END, 2, h);                          // cropped height
        setByte(HEADER_PCS_END, 4, fpsId);
        setWord(HEADER_PCS_END, 5, pic.getCompositionNumber() + 1);
        for (byte b : HEADER_PCS_END) {
            buf[index++] = b;
        }

        // write WDS
        PACKET_HEADER[10] = 0x17;                               // ID
        timeStamp = (int) pic.getEndTime() - windowInitTime;
        setDWord(PACKET_HEADER, 2, timeStamp);                  // PTS
        setDWord(PACKET_HEADER, 6, 0);
        setWord(PACKET_HEADER, 11, HEADER_WDS.length);          // size
        for (byte b : PACKET_HEADER) {
            buf[index++] = b;
        }
        setWord(HEADER_WDS, 2, pic.getXOffset());
        setWord(HEADER_WDS, 4, yOfs);
        setWord(HEADER_WDS, 6, bm.getWidth());
        setWord(HEADER_WDS, 8, bm.getHeight());
        for (byte b : HEADER_WDS) {
            buf[index++] = b;
        }

        // write END
        PACKET_HEADER[10] = (byte)0x80;             // ID
        setDWord(PACKET_HEADER, 2, timeStamp);      // PTS (PTS of end PCS)
        setDWord(PACKET_HEADER, 6, 0);              // DTS (0)
        setWord(PACKET_HEADER, 11, 0);              // size
        for (byte b : PACKET_HEADER) {
            buf[index++] = b;
        }

        return buf;
    }

    /**
     * Create RLE buffer from bitmap
     * @param bm bitmap to compress
     * @return RLE buffer
     */
    private static byte[] encodeImage(Bitmap bm) {
        ArrayList<Byte> bytes = new ArrayList<Byte>();
        byte color;
        int ofs;
        int len;
        //boolean eol;

        for (int y=0; y < bm.getHeight(); y++) {
            ofs = y * bm.getWidth();
            //eol = false;
            int x;
            for (x=0; x < bm.getWidth(); x+=len, ofs+=len) {
                color = bm.getInternalBuffer()[ofs];
                for (len=1; x+len < bm.getWidth(); len++) {
                    if (bm.getInternalBuffer()[ofs+len] != color) {
                        break;
                    }
                }
                if (len<=2 && color != 0) {
                    // only a single occurrence -> add color
                    bytes.add(color);
                    if (len==2) {
                        bytes.add(color);
                    }
                } else {
                    if (len > 0x3fff) {
                        len = 0x3fff;
                    }
                    bytes.add((byte)0); // rle id
                    // commented out due to bug in SupRip
                    /*if (color == 0 && x+len == bm.getWidth()) {
                        bytes.add((byte)0);
                        eol = true;
                    } else*/
                    if (color == 0 && len < 0x40){
                        // 00 xx -> xx times 0
                        bytes.add((byte)len);
                    } else if (color == 0){
                        // 00 4x xx -> xxx zeroes
                        bytes.add((byte)(0x40|(len>>8)) );
                        bytes.add((byte)len);
                    } else if(len < 0x40) {
                        // 00 8x cc -> x times value cc
                        bytes.add((byte)(0x80|len) );
                        bytes.add(color);
                    } else {
                        // 00 cx yy cc -> xyy times value cc
                        bytes.add((byte)(0xc0|(len>>8)) );
                        bytes.add((byte)len);
                        bytes.add(color);
                    }
                }
            }
            if (/*!eol &&*/ x == bm.getWidth()) {
                bytes.add((byte)0); // rle id
                bytes.add((byte)0);
            }
        }
        int size =  bytes.size();
        byte[] retval = new byte[size];
        Iterator<Byte> it = bytes.iterator();
        for (int i=0; i < size; i++) {
            retval[i] = it.next();
        }
        return retval;
    }
} public class SupXml implements SubtitleStream {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger logger = Logger.getInstance();

    /** ArrayList of captions contained in the current file */
    private List<SubPictureXml> subPictures = new ArrayList<SubPictureXml>();
    /** color palette of the last decoded caption  */
    private Palette palette;
    /** bitmap of the last decoded caption  */
    private Bitmap bitmap;
    /** index of dominant color for the current caption  */
    private int primaryColorIndex;
    /** number of forced captions in the current file  */
    private int numForcedFrames;

    /** path of the input stream */
    private String pathName;
    /** file name of XML file used as title */
    private String title;
    /** language id read from the xml */
    private String language = "eng";
    /** resolution read from the xml */
    private Resolution resolution = Resolution.HD_1080;
    /** frame rate read from the stream */
    private double fps = Framerate.FPS_23_976.getValue();
    /** converted xml frame rate read from the stream */
    private double fpsXml = XmlFps(fps);

    /**
     * Constructor (for reading)
     * @param filename file name of Xml file to read
     * @throws CoreException
     */
    public SupXml(String filename) throws CoreException {
        this.pathName = FilenameUtils.addSeparator(FilenameUtils.getParent(filename));
        this.title = FilenameUtils.removeExtension(FilenameUtils.getName(filename));

        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser saxParser;
        try {
            saxParser = factory.newSAXParser();
            DefaultHandler handler = new XmlHandler();
            saxParser.parse(new File(filename), handler);
        } catch (ParserConfigurationException e) {
            throw new CoreException(e.getMessage());
        } catch (SAXException e) {
            throw new CoreException(e.getMessage());
        } catch (IOException e) {
            throw new CoreException(e.getMessage());
        }

        logger.trace("\nDetected " + numForcedFrames + " forced captions.\n");
    }

    /**
     * Return an integer frame rate in BDN XML style
     * @param fps source frame rate
     * @return next integer frame rate (yet returned as double)
     */
    private static double XmlFps(double fps) {
        if (fps == Framerate.FPS_23_975.getValue()) {
            return Framerate.FPS_24.getValue();
        } else if (fps == Framerate.FPS_23_976.getValue()) {
            return Framerate.FPS_24.getValue();
        } else if (fps == Framerate.NTSC.getValue()) {
            return 30.0;
        } else if (fps == Framerate.NTSC_I.getValue()) {
            return 60.0;
        } else {
            return fps;
        }
    }

    /* (non-Javadoc)
     * @see deadbeef.SupTools.SubtitleStream#close()
     */
    @Override
    public void close() {
    }

    /* (non-Javadoc)
     * @see deadbeef.SupTools.SubtitleStream#decode(int)
     */
    @Override
    public void decode(int index) throws CoreException {
        try {
            File f = new File(subPictures.get(index).getFileName());
            if (!f.exists()) {
                throw new CoreException("file " + subPictures.get(index).getFileName() + " not found.");
            }
            BufferedImage img = ImageIO.read(f);
            int w = img.getWidth();
            int h = img.getHeight();

            this.palette = null;

            // first try to read image and palette directly from imported image
            if (img.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
                IndexColorModel icm = (IndexColorModel)img.getColorModel();
                if (icm.getMapSize() < 255 || (icm.hasAlpha() && icm.getAlpha(255) == 0)) {
                    // create palette
                    palette = new Palette(256);
                    for (int i=0; i < icm.getMapSize(); i++) {
                        int alpha = (icm.getRGB(i) >> 24) & 0xff;
                        if (alpha >= configuration.getAlphaCrop()) {
                            palette.setARGB(i, icm.getRGB(i));
                        } else {
                            palette.setARGB(i, 0);
                        }
                    }
                    // copy pixels
                    WritableRaster raster = img.getRaster();
                    bitmap = new Bitmap(img.getWidth(), img.getHeight(), (byte[])raster.getDataElements( 0, 0, img.getWidth(), img.getHeight(), null ));
                }
            }

            // if this failed, assume RGB image and quantize palette
            if (palette == null) {
                // grab int array (ARGB)
                int[] pixels = new int[w * h];
                img.getRGB(0, 0, w, h, pixels, 0, w);
                // quantize image
                QuantizeFilter qf = new QuantizeFilter();
                bitmap = new Bitmap(img.getWidth(), img.getHeight());
                int ct[] = qf.quantize(pixels, bitmap.getInternalBuffer(), w, h, 255, false, false);
                int size = ct.length;
                if (size > 255) {
                    logger.warn("Quantizer failed.\n");
                    size = 255;
                }
                // create palette
                palette = new Palette(256);
                for (int i=0; i < size; i++) {
                    int alpha = (ct[i] >> 24) & 0xff;
                    if (alpha >= configuration.getAlphaCrop()) {
                        palette.setARGB(i, ct[i]);
                    } else {
                        palette.setARGB(i, 0);
                    }
                }
            }
            primaryColorIndex = bitmap.getPrimaryColorIndex(palette.getAlpha(), configuration.getAlphaThreshold(), palette.getY());
            // crop
            BitmapBounds bounds = bitmap.getCroppingBounds(palette.getAlpha(), configuration.getAlphaCrop());
            if (bounds.yMin>0 || bounds.xMin > 0 || bounds.xMax<bitmap.getWidth()-1 || bounds.yMax<bitmap.getHeight()-1) {
                w = bounds.xMax - bounds.xMin + 1;
                h = bounds.yMax - bounds.yMin + 1;
                if (w < 2) {
                    w = 2;
                }
                if (h < 2) {
                    h = 2;
                }
                bitmap = bitmap.crop(bounds.xMin, bounds.yMin, w, h);
                // update picture
                SubPictureXml pic = subPictures.get(index);
                pic.setImageWidth(w);
                pic.setImageHeight(h);
                pic.setOfsX(pic.getOriginalXOffset() + bounds.xMin);
                pic.setOfsY(pic.getOriginalYOffset() + bounds.yMin);
            }
        } catch (IOException e) {
            throw new CoreException(e.getMessage());
        } catch (OutOfMemoryError e) {
            JOptionPane.showMessageDialog(null,"Out of heap! Use -Xmx256m to increase heap!","Error!", JOptionPane.WARNING_MESSAGE);
            throw new CoreException("Out of heap! Use -Xmx256m to increase heap!");
        }
    }

    /**
     * Create Xml file
     *
     * @param fname file name
     * @param pics Map of SubPictures and their original indexes which were used to generate the png file names
     * @throws CoreException
     */
    public static void writeXml(String fname, SortedMap<Integer, SubPicture> pics) throws CoreException {
        double fps = configuration.getFpsTrg();
        double fpsXml = XmlFps(fps);
        BufferedWriter out = null;
        String name = FilenameUtils.removeExtension(FilenameUtils.getName(fname));
        try {
            out = new BufferedWriter(new FileWriter(fname));
            out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            out.newLine();
            out.write("<BDN Version=\"0.93\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"BD-03-006-0093b BDN File Format.xsd\">");
            out.newLine();
            out.write("  <Description>");
            out.newLine();
            out.write("    <Name Title=\"" + name + "\" Content=\"\"/>");
            out.newLine();
            out.write("    <Language Code=\"" + LANGUAGES[configuration.getLanguageIdx()][2] + "\"/>");
            out.newLine();
            String res = configuration.getOutputResolution().getResolutionNameForXml();
            out.write("    <Format VideoFormat=\"" + res + "\" FrameRate=\"" + ToolBox.formatDouble(fps) + "\" DropFrame=\"False\"/>");
            out.newLine();
            long t = pics.get(pics.firstKey()).getStartTime();
            if (fps != fpsXml) {
                t = (t * 2000 + 1001) / 2002;
            }
            String ts = ptsToTimeStrXml(t,fpsXml);
            t = pics.get(pics.lastKey()).getEndTime();
            if (fps != fpsXml) {
                t = (t * 2000 + 1001) / 2002;
            }
            String te = ptsToTimeStrXml(t,fpsXml);
            out.write("    <Events Type=\"Graphic\" FirstEventInTC=\"" + ts + "\" LastEventOutTC=\"" + te + "\" NumberofEvents=\"" + pics.size() + "\"/>");
            out.newLine();
            out.write("  </Description>");
            out.newLine();
            out.write("  <Events>");
            out.newLine();

            for (int idx : pics.keySet()) {
                SubPicture p = pics.get(idx);
                t = p.getStartTime();
                if (fps != fpsXml) {
                    t = (t * 2000 + 1001) / 2002;
                }
                ts = ptsToTimeStrXml(t,fpsXml);
                t = p.getEndTime();
                if (fps != fpsXml) {
                    t = (t * 2000 + 1001) / 2002;
                }
                te = ptsToTimeStrXml(t, fpsXml);
                String forced = p.isForced() ? "True": "False";
                out.write("    <Event InTC=\"" + ts + "\" OutTC=\"" + te + "\" Forced=\"" + forced + "\">");
                out.newLine();

                String pname = getPNGname(name, idx+1);
                out.write("      <Graphic Width=\"" + p.getImageWidth() + "\" Height=\"" + p.getImageHeight()
                        + "\" X=\"" + p.getXOffset() + "\" Y=\"" + p.getYOffset() + "\">" + pname + "</Graphic>");
                out.newLine();
                out.write("    </Event>");
                out.newLine();
            }
            out.write("  </Events>");
            out.newLine();
            out.write("</BDN>");
            out.newLine();
        } catch (IOException ex) {
            throw new CoreException(ex.getMessage());
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ex) {
            }
        }
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getBitmap()
     */
    public Bitmap getBitmap() {
        return bitmap;
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getImage()
     */
    public BufferedImage getImage() {
        return bitmap.getImage(palette.getColorModel());
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getImage(Bitmap)
     */
    public BufferedImage getImage(final Bitmap bm) {
        return bm.getImage(palette.getColorModel());
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getForcedFrameCount()
     */
    public int getForcedFrameCount() {
        return numForcedFrames;
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getNumFrames()
     */
    public int getFrameCount() {
        return subPictures.size();
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getPalette()
     */
    public Palette getPalette() {
        return palette;
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getPrimaryColorIndex()
     */
    public int getPrimaryColorIndex() {
        return primaryColorIndex;
    }

    /* (non-Javadoc)
     * @see deadbeef.SupTools.SubtitleStream#getStartOffset(int)
     */
    public long getStartOffset(int index) {
        // dummy
        return 0;
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getSubPicture(int)
     */
    public SubPicture getSubPicture(int index) {
        return subPictures.get(index);
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getEndTime(int)
     */
    public long getEndTime(int index) {
        return subPictures.get(index).getEndTime();
    }

    /* (non-Javadoc)
     * @see SubtitleStream#getStartTime(int)
     */
    public long getStartTime(final int index) {
        return subPictures.get(index).getStartTime();
    }

    /* (non-Javadoc)
     * @see SubtitleStream#isForced(int)
     */
    public boolean isForced(int index) {
        return subPictures.get(index).isForced();
    }

    /**
     * Create PNG name from (xml) file name and index
     * @param fn file name
     * @param idx index
     * @return PNG name
     */
    public static String getPNGname(String fn, int idx) {
        return fn + "_" + ToolBox.leftZeroPad(idx, 4) + ".png";
    }

    /**
     * get language read from Xml
     * @return language as String
     */
    public String getLanguage() {
        return language;
    }

    /**
     * get fps read from Xml
     * @return frame rate as double
     */
    public double getFps() {
        return fps;
    }

    enum XmlState { BDN, DESCRIPT, NAME, LANGUAGE, FORMAT, EVENTS, EVENT, GRAPHIC, UNKNOWN}

    private static final String xmlStates[] = { "bdn", "description", "name", "language", "format", "events", "event", "graphic"};

    class XmlHandler extends DefaultHandler {

        XmlState state;
        StringBuffer txt;
        boolean valid;
        SubPictureXml pic;

        private XmlState findState(final String s) {
            for (XmlState x : XmlState.values()) {
                if (s.toLowerCase().equals(xmlStates[x.ordinal()])) {
                    return x;
                }
            }
            return XmlState.UNKNOWN;
        }

        @Override
        public void startElement(String namespaceURI, String localName, String qName, Attributes atts ) {
            state = findState(qName);
            String at;

            if (state != XmlState.BDN && !valid) {
                logger.error("BDN tag missing");
            }

            txt = null;

            switch (state) {
                case UNKNOWN:
                    logger.error("Unknown tag " + qName + "\n");
                    break;
                case BDN:
                    if (valid) {
                        logger.error("BDN must be used only once");
                    } else {
                        valid = true;
                    }
                    break;
                case NAME:
                    at = atts.getValue("Title");
                    if (at != null) {
                        title = at;
                        logger.trace("Title: " + title + "\n");
                    }
                    break;
                case LANGUAGE:
                    at = atts.getValue("Code");
                    if (at != null) {
                        language = at;
                        logger.trace("Language: " + language + "\n");
                    }
                    break;
                case FORMAT:
                    at = atts.getValue("FrameRate");
                    if (at != null) {
                        fps = SubtitleUtils.getFps(at);
                        fpsXml = XmlFps(fps);
                        logger.trace("fps: " + ToolBox.formatDouble(fps) + "\n");
                    }
                    at = atts.getValue("VideoFormat");
                    if (at != null) {
                        String res = at;
                        for (Resolution r : Resolution.values())  {
                            if (res.length() == 4 && res.charAt(0) != '7') { // hack to rename 480p/576p to 480i/576i
                                res = res.replace('p', 'i');
                            }
                            if (r.getResolutionNameForXml().equalsIgnoreCase(res)) {
                                resolution = r;
                                logger.trace("Language: " + r.getResolutionNameForXml() + "\n");
                                break;
                            }
                        }
                    }
                    break;
                case EVENTS:
                    at = atts.getValue("NumberofEvents");
                    if (at != null) {
                        int n = ToolBox.getInt(at);
                        if (n > 0) {
                            /* number of subtitles read from the xml */
                            Core.setProgressMax(n);
                        }
                    }
                    break;
                case EVENT:
                    pic = new SubPictureXml();
                    subPictures.add(pic);
                    int num  = subPictures.size();
                    logger.info("#" + num + "\n");
                    Core.setProgress(num);
                    at = atts.getValue("InTC");
                    if (at != null) {
                        pic.setStartTime(timeStrXmlToPTS(at, fpsXml));
                        if (pic.getStartTime() == -1) {
                            pic.setStartTime(0);
                            logger.warn("Invalid start time " + at + "\n");
                        }
                    }
                    at = atts.getValue("OutTC");
                    if (at != null) {
                        pic.setEndTime(timeStrXmlToPTS(at, fpsXml));
                        if (pic.getEndTime() == -1) {
                            pic.setEndTime(0);
                            logger.warn("Invalid end time " + at + "\n");
                        }
                    }
                    if (fps != fpsXml) {
                        pic.setStartTime((pic.getStartTime() * 1001 + 500) / 1000);
                        pic.setEndTime((pic.getEndTime() * 1001 + 500) / 1000);
                    }
                    at = atts.getValue("Forced");
                    pic.setForced(at != null && at.equalsIgnoreCase("true"));
                    if (pic.isForced()) {
                        numForcedFrames++;
                    }
                    int dim[] = resolution.getDimensions();
                    pic.setWidth(dim[0]);
                    pic.setHeight(dim[1]);
                    break;
                case GRAPHIC:
                    pic.setImageWidth(ToolBox.getInt(atts.getValue("Width")));
                    pic.setImageHeight(ToolBox.getInt(atts.getValue("Height")));
                    pic.setOfsX(ToolBox.getInt(atts.getValue("X")));
                    pic.setOfsY(ToolBox.getInt(atts.getValue("Y")));
                    pic.storeOriginalOffsets();
                    txt = new StringBuffer();
                    break;
            }
        }

        @Override
        public void endElement(String namespaceURI, String localName, String qName ) {
            XmlState endState = findState(qName);
            if (state == XmlState.GRAPHIC && endState == XmlState.GRAPHIC) {
                pic.setFileName(pathName + txt.toString().trim());
            }
        }

        @Override
        public void characters(char[] ch, int start, int length ) {
            if (txt != null) {
                txt.append(ch, start, length);
            }
        }
    }
} public interface DvdSubtitleStream extends SubtitleStream {

    /**
     * Get frame alpha values.
     * @param index Index of caption
     * @return Integer array with 4 entries representing the frame alpha values
     */
    int[] getFrameAlpha(int index);

    /**
     * Get frame Palette.
     * @param index Index of caption
     * @return Integer array with 4 entries representing the frame palette
     */
    int[] getFramePalette(int index);

    /**
     * Get original frame alpha values (as they were before editing).
     * @param index Index of caption
     * @return Integer array with 4 entries representing the original frame alpha values.
     */
    int[] getOriginalFrameAlpha(int index);

    /**
     * Get original frame Palette (as they were before editing).
     * @param index index of caption
     * @return Integer array with 4 entries representing the original frame palette.
     */
    int[] getOriginalFramePalette(int index);

    /**
     * Get imported 16 color DVD Palette.
     * @return Imported 16 color DVD Palette.
     */
    Palette getSrcPalette();

    /**
     * Replace imported 16 color DVD Palette with a new Palette.
     * @param pal New Palette
     */
    void setSrcPalette(Palette pal);

    /**
     * Get language index read from Idx.
     * @return language index.
     */
    int getLanguageIndex();
} public final class IfoWriter {

    private static final Configuration configuration = Configuration.getInstance();

    private IfoWriter() {
    }

    public static void writeIFO(String fname, int pictureHeight, Palette palette) throws CoreException {
        byte[] buf = new byte[0x1800];
        int index = 0;

        // video attributes
        int vidAttr = pictureHeight == 480 ? 0x4f01 : 0x5f01;

        // VTSI_MAT
        ToolBox.setString(buf, index, "DVDVIDEO-VTS");
        ByteUtils.setDWord(buf, index + 0x12, 0x00000004);    // last sector of title set
        ByteUtils.setDWord(buf, index + 0x1C, 0x00000004);    // last sector of IFO
        ByteUtils.setDWord(buf, index + 0x80, 0x000007ff);    // end byte address of VTS_MAT
        ByteUtils.setDWord(buf, index + 0xC8, 0x00000001);    // start sector of Title Vob (*2048 -> 0x0800) -> PTT_SRPTI
        ByteUtils.setDWord(buf, index + 0xCC, 0x00000002);    // start sector of Titles&Chapters table (*2048 -> 0x1000) -> VTS_PGCITI

        ByteUtils.setWord(buf, index + 0x100, vidAttr);    // video attributes
        ByteUtils.setWord(buf, index + 0x200, vidAttr);    // video attributes

        String l = Constants.LANGUAGES[configuration.getLanguageIdx()][1];
        ByteUtils.setWord(buf, index + 0x254, 1);            // number of subtitle streams
        ByteUtils.setByte(buf, index + 0x256, 1);            // subtitle attributes
        ByteUtils.setByte(buf, index + 0x258, (byte) l.charAt(0));
        ByteUtils.setByte(buf, index + 0x259, (byte) l.charAt(1));

        // PTT_SRPTI
        index = 0x0800;
        ByteUtils.setWord(buf, index, 0x0001);               // Number of TTUs
        ByteUtils.setWord(buf, index + 0x04, 0x000f);        // End byte of PTT_SRPT table
        ByteUtils.setDWord(buf, index + 0x04, 0x0000000C);   // TTU_1: starting byte
        ByteUtils.setWord(buf, index + 0x0C, 0x0001);        // PTT_1: program chain number PGCN
        ByteUtils.setWord(buf, index + 0x0e, 0x0001);        // PTT_1: program number PG

        // VTS_PGCITI/VTS_PTT_SRPT
        index = 0x1000;
        ByteUtils.setWord(buf, index, 0x0001);                // Number of VTS_PGCI_SRP (2 bytes, 2 bytes reserved)
        ByteUtils.setDWord(buf, index + 0x04, 0x00000119);    // end byte of VTS_PGCI_SRP table (281)
        ByteUtils.setDWord(buf, index + 0x08, 0x81000000);    // VTS_PGC_1_ category mask. entry PGC (0x80), title number 1 (0x01), Category 0,...
        ByteUtils.setDWord(buf, index + 0x0C, 0x00000010);    // VTS_PGCI start byte (16)

        // VTS_PGC_1
        index = 0x1010;
        ByteUtils.setByte(buf, index + 0x02, 0x01);        // Number of Programs
        ByteUtils.setByte(buf, index + 0x03, 0x01);        // Number of Cells
        for (int i = 0; i < 16; i++) {
            int ycbcr[] = palette.getYCbCr(i);
            ByteUtils.setByte(buf, index + 0xA4 + 4 * i + 1, ycbcr[0]);
            ByteUtils.setByte(buf, index + 0xA4 + 4 * i + 2, ycbcr[1]);
            ByteUtils.setByte(buf, index + 0xA4 + 4 * i + 3, ycbcr[2]);
        }

        BufferedOutputStream out = null;
        try {
            out = new BufferedOutputStream(new FileOutputStream(fname));
            out.write(buf);
        } catch (IOException ex) {
            throw new CoreException(ex.getMessage());
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ex) {
            }
        }
    }
} public class SubDvd implements DvdSubtitleStream {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger logger = Logger.getInstance();

    /** ArrayList of captions contained in the current file */
    private final List<SubPictureDVD> subPictures = new ArrayList<SubPictureDVD>();
    /** color palette read from idx file  */
    private Palette srcPalette = new Palette(DEFAULT_DVD_PALETTE);
    /** color palette created for last decoded caption  */
    private Palette palette;
    /** bitmap of the last decoded caption */
    private Bitmap bitmap;
    /** screen width of imported VobSub */
    private int screenWidth = 720;
    /** screen height of imported VobSub  */
    private int screenHeight = 576;
    /** global x offset  */
    private int globalXOffset;
    /** global y offset */
    private int globalYOffset;
    /** global delay  */
    private int globalDelay;
    /** index of language read from IDX */
    private int languageIndex;
    /** stream ID */
    private int streamID;
    /** FileBuffer for reading SUB */
    private final FileBuffer buffer;
    /** index of dominant color for the current caption */
    private int primaryColorIndex;
    /** number of forced captions in the current file  */
    private int forcedFrameCount;
    /** store last alpha values for invisible workaround */
    private static int[] lastAlpha = {0, 0xf, 0xf, 0xf};


    public SubDvd(String subFile, String idxFile) throws CoreException {
        readIdx(idxFile);
        Core.setProgressMax(subPictures.size());
        try {
            buffer = new FileBuffer(subFile);
        } catch (FileBufferException e) {
            throw new CoreException(e.getMessage());
        }
        for (int i=0; i < subPictures.size(); i++) {
            Core.setProgress(i);
            logger.info("# " + (i + 1) + "\n");
            logger.trace("Offset: " + ToolBox.toHexLeftZeroPadded(subPictures.get(i).getOffset(), 8) + "\n");
            long nextOfs;
            if (i < subPictures.size() - 1) {
                nextOfs = subPictures.get(i+1).getOffset();
            } else {
                nextOfs = buffer.getSize();
            }
            readSubFrame(subPictures.get(i), nextOfs, buffer);
        }
        logger.info("\nDetected " + forcedFrameCount + " forced captions.\n");
    }

    private void readIdx(String idxFile) throws CoreException {
        BufferedReader in = null;
        try {
            in = new BufferedReader(new FileReader(idxFile));
            String s;
            int v;
            int langIdx = 0;
            boolean ignore = false;
            while ((s = in.readLine()) != null) {
                s = s.trim();
                if (s.length() < 1 || s.charAt(0) == '#') {
                    continue;
                }
                int pos = s.indexOf(':');
                if (pos == -1 || s.length()-pos <= 1) {
                    logger.error("Illegal key: " + s + "\n");
                    continue;
                }
                String key = s.substring(0, pos).trim();
                String val = s.substring(pos+1).trim();

                // size (e.g. "size: 720x576")
                if (key.equalsIgnoreCase("size")) {
                    pos = val.indexOf('x');
                    if (pos == -1 || val.length()-pos <= 1) {
                        throw new CoreException("Illegal size: " + val);
                    }
                    v = ToolBox.getInt(val.substring(0,pos));
                    if (v < 2) {
                        throw new CoreException("Illegal screen width: " + v);
                    }
                    screenWidth = v;
                    v = ToolBox.getInt(val.substring(pos+1));
                    if (v < 2) {
                        throw new CoreException("Illegal screen height: " + v);
                    }
                    screenHeight = v;
                    continue;
                }

                // origin (e.g. "org: 0, 0")
                if (key.equalsIgnoreCase("org")) {
                    pos = val.indexOf(',');
                    if (pos == -1 || val.length()-pos <= 1) {
                        throw new CoreException("Illegal origin: " + val);
                    }
                    v = ToolBox.getInt(val.substring(0,pos));
                    if (v < 0) {
                        throw new CoreException("Illegal x origin: " + v);
                    }
                    globalXOffset = v;
                    v = ToolBox.getInt(val.substring(pos+1));
                    if (v < 0) {
                        throw new CoreException("Illegal y origin: " + v);
                    }
                    globalYOffset = v;
                    continue;
                }

                // scale (e.g. "scale: 100%, 100%")
                if (key.equalsIgnoreCase("scale")) {
                    // ignored for the moment
                    continue;
                }

                // alpha (e.g. "alpha: 100%")
                if (key.equalsIgnoreCase("alpha")) {
                    // ignored for the moment
                    continue;
                }

                // smoothing (e.g. "smooth: OFF")
                if (key.equalsIgnoreCase("smooth")) {
                    // ignored for the moment
                    continue;
                }

                // fading (e.g. "fadein/out: 0, 0");
                if (key.equalsIgnoreCase("fadein/out")) {
                    // ignored for the moment
                    continue;
                }

                // alignment (e.g. "align: OFF at LEFT TOP")
                if (key.equalsIgnoreCase("align")) {
                    // ignored for the moment
                    continue;
                }

                // time offset (e.g. "time offset: 0")
                if (key.equalsIgnoreCase("time offset")) {
                    v = ToolBox.getInt(val);
                    if (v < 0) {
                        v = (int)timeStrToPTS(val);
                    }
                    if (v < 0) {
                        throw new CoreException("Illegal time offset: " + v);
                    }
                    globalDelay = v * 90; // ms -> 90kHz
                    continue;
                }

                // forced subs (e.g. "forced subs: OFF")
                if (key.equalsIgnoreCase("align")) {
                    // ignored for the moment
                    continue;
                }

                // palette
                if (key.equalsIgnoreCase("palette")) {
                    String vals[] = val.split(",");
                    if (vals == null || vals.length < 1 || vals.length > 16) {
                        throw new CoreException("Illegal palette definition: " + val);
                    }
                    for (int i=0; i<vals.length; i++) {
                        int color = -1;
                        try {
                            color = Integer.parseInt(vals[i].trim(), 16);
                        } catch (NumberFormatException ex) {
                        }
                        if (color == -1) {
                            throw new CoreException("Illegal palette entry: " + vals[i]);
                        }
                        srcPalette.setARGB(i, color);
                    }
                    continue;
                }

                // custom colors (e.g. "custom colors: OFF, tridx: 1000, colors: 000000, 444444, 888888, cccccc")
                if (key.equalsIgnoreCase("custom colors")) {
                    // ignored for the moment
                    continue;
                }

                // language index (e.g. "langidx: 0")
                if (key.equalsIgnoreCase("langidx")) {
                    v = ToolBox.getInt(val);
                    if (v < 0) {
                        throw new CoreException("Illegal language idx: " + v);
                    }
                    langIdx = v; // ms -> 90kHz
                    // ignored for the moment
                    continue;
                }

                // language id (e.g. "id: de, index: 0")
                if (key.equalsIgnoreCase("id")) {
                    String id;
                    pos = val.indexOf(',');
                    if (pos > 0 ) {
                        id = val.substring(0, pos).trim();
                    } else {
                        id = val;
                    }
                    if (id.length() != 2) {
                        logger.warn("Illegal language id: " + id + "\n");
                        continue;
                    }
                    boolean found = false;
                    for (int i=0; i < LANGUAGES.length; i++) {
                        if (id.equalsIgnoreCase(LANGUAGES[i][1])) {
                            languageIndex = i;
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        logger.warn("Illegal language id: " + id + "\n");
                    }

                    pos = val.indexOf(':');
                    if (pos == -1 || s.length() - pos <= 1) {
                        logger.error("Missing index key: " + val + "\n");
                        continue;
                    }
                    key = val.substring(0, pos).trim();
                    val = val.substring(pos+1).trim();
                    if (key.equalsIgnoreCase("index"))  {
                        logger.error("Missing index key: " + s + "\n");
                        continue;
                    }
                    v = ToolBox.getInt(val);
                    if (v < 0) {
                        throw new CoreException("Illegal language index: " + v);
                    }

                    if (v != langIdx) {
                        ignore = true;
                        logger.warn("Language id " + id + "(index:" + v + ") inactive -> ignored\n");
                    } else {
                        streamID = v;
                        ignore = false;
                    }
                    continue;
                }

                if (!ignore) {
                    // timestamp: 00:00:14:160, filepos: 000000000
                    if (key.equalsIgnoreCase("timestamp")) {
                        String vs;
                        pos = val.indexOf(',');
                        if (pos == -1 || val.length()-pos <= 1) {
                            throw new CoreException("Illegal timestamp entry: " + val);
                        }
                        vs = val.substring(0,pos);
                        long t = timeStrToPTS(vs);
                        if (t < 0) {
                            throw new CoreException("Illegal timestamp: " + vs);
                        }
                        vs = val.substring(pos+1).toLowerCase();
                        pos = vs.indexOf("filepos:");
                        if (pos == -1 || vs.length()-pos <= 1) {
                            throw new CoreException("Missing filepos: " + val);
                        }
                        long l = Long.parseLong(vs.substring(pos+8).trim(), 16);
                        if (l == -1) {
                            throw new CoreException("Illegal filepos: " + vs.substring(pos+8));
                        }
                        SubPictureDVD pic = new SubPictureDVD();
                        pic.setOffset(l);
                        pic.setWidth(screenWidth);
                        pic.setHeight(screenHeight);
                        pic.setStartTime(t + globalDelay);
                        subPictures.add(pic);
                    }
                }
            }
        } catch (IOException ex) {
            throw new CoreException(ex.getMessage());
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
            }
        }
    }

    /**
     * Read one frame from SUB file
     * @param pic SubPicture object for this frame
     * @param endOfs end offset
     * @param buffer File Buffer to read from
     * @throws CoreException
     */
    private void readSubFrame(SubPictureDVD pic, long endOfs, FileBuffer buffer) throws CoreException  {
        long ofs = pic.getOffset();
        long ctrlOfs = -1;
        long nextOfs;
        int  ctrlOfsRel = 0;
        int rleSize = 0;
        int rleBufferFound = 0;
        int ctrlSize = -1;
        int ctrlHeaderCopied = 0;
        byte ctrlHeader[] = null;
        ImageObjectFragment rleFrag;
        int length;
        int packHeaderSize;
        boolean firstPackFound = false;

        try {
            do {
                // 4 bytes:  packet identifier 0x000001ba
                long startOfs = ofs;
                if (buffer.getDWord(ofs) != 0x000001ba) {
                    throw new CoreException("Missing packet identifier at ofs " + ToolBox.toHexLeftZeroPadded(ofs,8));
                }
                // 6 bytes:  system clock reference
                // 3 bytes:  multiplexer rate
                // 1 byte:   stuffing info
                int stuffOfs = buffer.getByte(ofs+=13) & 7;
                // 4 bytes:  sub packet ID 0x000001bd
                if (buffer.getDWord(ofs += (1+stuffOfs)) != 0x000001bd) {
                    throw new CoreException("Missing packet identifier at ofs " + ToolBox.toHexLeftZeroPadded(ofs,8));
                }
                // 2 bytes:  packet length (number of bytes after this entry)
                length = buffer.getWord(ofs+=4);
                nextOfs = ofs+2+length;
                // 2 bytes:  packet type
                ofs += 2;
                packHeaderSize = (int)(ofs-startOfs);
                boolean firstPack = ((buffer.getByte(++ofs) & 0x80) == 0x80);
                // 1 byte    pts length
                int ptsLength = buffer.getByte(ofs+=1);
                ofs += 1 + ptsLength; // skip PTS and stream ID
                int packetStreamID = buffer.getByte(ofs++) - 0x20;
                if (packetStreamID != streamID) {
                    // packet doesn't belong to stream -> skip
                    if (nextOfs % 0x800 != 0) {
                        ofs = (nextOfs/0x800 + 1)*0x800;
                        logger.warn("Offset to next fragment is invalid. Fixed to:" + ToolBox.toHexLeftZeroPadded(ofs, 8) + "\n");
                    } else {
                        ofs = nextOfs;
                    }
                    ctrlOfs += 0x800;
                    continue;
                }
                int headerSize = (int)(ofs-startOfs); // only valid for additional packets
                if (firstPack && ptsLength >= 5) {
                    int size = buffer.getWord(ofs);
                    ofs += 2;
                    ctrlOfsRel = buffer.getWord(ofs);
                    rleSize = ctrlOfsRel-2;             // calculate size of RLE buffer
                    ctrlSize = size-ctrlOfsRel-2;       // calculate size of control header
                    if (ctrlSize < 0) {
                        throw new CoreException("Invalid control buffer size");
                    }
                    ctrlHeader = new byte[ctrlSize];
                    ctrlOfs = ctrlOfsRel + ofs; // might have to be corrected for multiple packets
                    ofs += 2;
                    headerSize = (int)(ofs-startOfs);
                    pic.setRleFragments(new ArrayList<ImageObjectFragment>());
                    firstPackFound = true;
                } else {
                    if (firstPackFound) {
                        ctrlOfs += headerSize; // fix absolute offset by adding header bytes
                    } else {
                        logger.warn("Invalid fragment skipped at ofs " + ToolBox.toHexLeftZeroPadded(startOfs, 8) + "\n");
                    }
                }

                // check if control header is (partly) in this packet
                int diff = (int)(nextOfs - ctrlOfs - ctrlHeaderCopied);
                if (diff<0) {
                    diff = 0;
                }
                int copied = ctrlHeaderCopied;
                try {
                    for (int i=0; (i < diff) && (ctrlHeaderCopied<ctrlSize); i++) {
                        ctrlHeader[ctrlHeaderCopied] = (byte)buffer.getByte(ctrlOfs + i + copied);
                        ctrlHeaderCopied++;
                    }
                } catch (ArrayIndexOutOfBoundsException ex) {
                    throw new CoreException("Inconsistent control buffer access (" + ex.getMessage() + ")");
                }
                rleFrag = new ImageObjectFragment(ofs, length - headerSize - diff + packHeaderSize);
                pic.getRleFragments().add(rleFrag);

                rleBufferFound += rleFrag.getImagePacketSize();

                if (ctrlHeaderCopied != ctrlSize && (nextOfs % 0x800 != 0)) {
                    ofs = (nextOfs/0x800 + 1) * 0x800;
                    logger.warn("Offset to next fragment is invalid. Fixed to:" + ToolBox.toHexLeftZeroPadded(ofs, 8) + "\n");
                    rleBufferFound += ofs-nextOfs;
                } else {
                    ofs = nextOfs;
                }
            } while (ofs < endOfs && ctrlHeaderCopied < ctrlSize);

            if (ctrlHeaderCopied != ctrlSize) {
                logger.warn("Control buffer size inconsistent.\n");
                // fill rest of buffer with break command to avoid wrong detection of forced caption (0x00)
                for (int i=ctrlHeaderCopied; i<ctrlSize; i++) {
                    ctrlHeader[i] = (byte)0xff;
                }
            }

            if (rleBufferFound != rleSize) {
                logger.warn("RLE buffer size inconsistent.\n");
            }

            pic.setRleSize(rleBufferFound);
        } catch (FileBufferException ex) {
            throw new CoreException(ex.getMessage());
        }

        pic.setPal(new int[4]);
        pic.setAlpha(new int[4]);
        int alphaSum = 0;
        int alphaUpdate[] = new int[4];
        int alphaUpdateSum;
        int delay = -1;
        boolean ColAlphaUpdate = false;

        logger.trace("SP_DCSQT at ofs: " + ToolBox.toHexLeftZeroPadded(ctrlOfs, 8) + "\n");

        try {
            // parse control header
            int b;
            int index = 0;
            int endSeqOfs = getWord(ctrlHeader, index) - ctrlOfsRel - 2;
            if (endSeqOfs < 0 || endSeqOfs > ctrlSize) {
                logger.warn("Invalid end sequence offset -> no end time\n");
                endSeqOfs = ctrlSize;
            }
            index += 2;
            parse_ctrl:
            while (index < endSeqOfs) {
                int cmd = getByte(ctrlHeader, index++);
                switch (cmd) {
                    case 0: // forced (?)
                        pic.setForced(true);
                        forcedFrameCount++;
                        break;
                    case 1: // start display
                        break;
                    case 3: // palette info
                        b = getByte(ctrlHeader, index++);
                        pic.getPal()[3] = (b >> 4);
                        pic.getPal()[2] = b & 0x0f;
                        b = getByte(ctrlHeader, index++);
                        pic.getPal()[1] = (b >> 4);
                        pic.getPal()[0] = b & 0x0f;
                        logger.trace("Palette:   " + pic.getPal()[0] + ", " + pic.getPal()[1] + ", " + pic.getPal()[2] + ", " + pic.getPal()[3] + "\n");
                        break;
                    case 4: // alpha info
                        b = getByte(ctrlHeader, index++);
                        pic.getAlpha()[3] = (b >> 4);
                        pic.getAlpha()[2] = b & 0x0f;
                        b = getByte(ctrlHeader, index++);
                        pic.getAlpha()[1] = (b >> 4);
                        pic.getAlpha()[0] = b & 0x0f;
                        for (int i = 0; i<4; i++) {
                            alphaSum += pic.getAlpha()[i] & 0xff;
                        }
                        logger.trace("Alpha:     " + pic.getAlpha()[0] + ", " + pic.getAlpha()[1] + ", " + pic.getAlpha()[2] + ", " + pic.getAlpha()[3] + "\n");
                        break;
                    case 5: // coordinates
                        int xOfs = (getByte(ctrlHeader, index)<<4) | (getByte(ctrlHeader, index+1)>>4);
                        pic.setOfsX(globalXOffset +xOfs);
                        pic.setImageWidth((((getByte(ctrlHeader, index+1)&0xf)<<8) | (getByte(ctrlHeader, index+2))) - xOfs + 1);
                        int yOfs = (getByte(ctrlHeader, index+3)<<4) | (getByte(ctrlHeader, index+4)>>4);
                        pic.setOfsY(globalYOffset +yOfs);
                        pic.setImageHeight((((getByte(ctrlHeader, index+4)&0xf)<<8) | (getByte(ctrlHeader, index+5))) - yOfs + 1);
                        logger.trace("Area info:" + " ("
                                + pic.getXOffset() + ", " + pic.getYOffset() + ") - (" + (pic.getXOffset() + pic.getImageWidth() - 1) + ", "
                                + (pic.getYOffset() + pic.getImageHeight() - 1) + ")\n");
                        index += 6;
                        break;
                    case 6: // offset to RLE buffer
                        pic.setEvenOffset(getWord(ctrlHeader, index) - 4);
                        pic.setOddOffset(getWord(ctrlHeader, index + 2) - 4);
                        index += 4;
                        logger.trace("RLE ofs:   " + ToolBox.toHexLeftZeroPadded(pic.getEvenOffset(), 4) + ", " + ToolBox.toHexLeftZeroPadded(pic.getOddOffset(), 4) + "\n");
                        break;
                    case 7: // color/alpha update
                        ColAlphaUpdate = true;
                        //int len = ToolBox.getWord(ctrlHeader, index);
                        // ignore the details for now, but just get alpha and palette info
                        alphaUpdateSum = 0;
                        b = getByte(ctrlHeader, index+10);
                        alphaUpdate[3] = (b >> 4);
                        alphaUpdate[2] = b & 0x0f;
                        b = getByte(ctrlHeader, index+11);
                        alphaUpdate[1] = (b >> 4);
                        alphaUpdate[0] = b & 0x0f;
                        for (int i = 0; i<4; i++) {
                            alphaUpdateSum += alphaUpdate[i] & 0xff;
                        }
                        // only use more opaque colors
                        if (alphaUpdateSum > alphaSum) {
                            alphaSum = alphaUpdateSum;
                            System.arraycopy(alphaUpdate, 0, pic.getAlpha(), 0, 4);
                            // take over frame palette
                            b = getByte(ctrlHeader, index+8);
                            pic.getPal()[3] = (b >> 4);
                            pic.getPal()[2] = b & 0x0f;
                            b = getByte(ctrlHeader, index+9);
                            pic.getPal()[1] = (b >> 4);
                            pic.getPal()[0] = b & 0x0f;
                        }
                        // search end sequence
                        index = endSeqOfs;
                        delay = getWord(ctrlHeader, index)*1024;
                        endSeqOfs = getWord(ctrlHeader, index+2)-ctrlOfsRel-2;
                        if (endSeqOfs < 0 || endSeqOfs > ctrlSize) {
                            logger.warn("Invalid end sequence offset -> no end time\n");
                            endSeqOfs = ctrlSize;
                        }
                        index += 4;
                        break;
                    case 0xff: // end sequence
                        break parse_ctrl;
                    default:
                        logger.warn("Unknown control sequence " + ToolBox.toHexLeftZeroPadded(cmd, 2) + " skipped\n");
                        break;
                }
            }

            if (endSeqOfs != ctrlSize) {
                int ctrlSeqCount = 1;
                index = -1;
                int nextIndex = endSeqOfs;
                while (nextIndex != index) {
                    index = nextIndex;
                    delay = getWord(ctrlHeader, index) * 1024;
                    nextIndex = getWord(ctrlHeader, index + 2) - ctrlOfsRel - 2;
                    ctrlSeqCount++;
                }
                if (ctrlSeqCount > 2) {
                    logger.warn("Control sequence(s) ignored - result may be erratic.");
                }
                pic.setEndTime(pic.getStartTime() + delay);
            } else {
                pic.setEndTime(pic.getStartTime());
            }

            if (ColAlphaUpdate) {
                logger.warn("Palette update/alpha fading detected - result may be erratic.\n");
            }

            if (alphaSum == 0) {
                if (configuration.getFixZeroAlpha()) {
                    System.arraycopy(lastAlpha, 0, pic.getAlpha(), 0, 4);
                    logger.warn("Invisible caption due to zero alpha - used alpha info of last caption.\n");
                } else {
                    logger.warn("Invisible caption due to zero alpha (not fixed due to user setting).\n");
                }
            }

            lastAlpha = pic.getAlpha();
            pic.storeOriginal();
        } catch (IndexOutOfBoundsException ex) {
            throw new CoreException("Index "+ex.getMessage() + " out of bounds in control header.");
        }
    }

    public void decode(int index) throws CoreException {
        if (index < subPictures.size()) {
            decode(subPictures.get(index));
        } else {
            throw new CoreException("Index " + index + " out of bounds\n");
        }
    }

    private void decode(SubPictureDVD pic)  throws CoreException {
        palette = SupDvdUtil.decodePalette(pic, srcPalette);
        bitmap  = SupDvdUtil.decodeImage(pic, buffer, palette.getIndexOfMostTransparentPaletteEntry());

        // crop
        BitmapBounds bounds = bitmap.getCroppingBounds(palette.getAlpha(), configuration.getAlphaCrop());
        if (bounds.yMin>0 || bounds.xMin > 0 || bounds.xMax<bitmap.getWidth()-1 || bounds.yMax<bitmap.getHeight()-1) {
            int w = bounds.xMax - bounds.xMin + 1;
            int h = bounds.yMax - bounds.yMin + 1;
            if (w<2) {
                w = 2;
            }
            if (h<2) {
                h = 2;
            }
            bitmap = bitmap.crop(bounds.xMin, bounds.yMin, w, h);
            // update picture
            pic.setImageWidth(w);
            pic.setImageHeight(h);
            pic.setOfsX(pic.getOriginalX() + bounds.xMin);
            pic.setOfsY(pic.getOriginalY() + bounds.yMin);
        }

        primaryColorIndex = bitmap.getPrimaryColorIndex(palette.getAlpha(), configuration.getAlphaThreshold(), palette.getY());
    }

    public int[] getFramePalette(int index) {
        return subPictures.get(index).getPal();
    }

    public int[] getOriginalFramePalette(int index) {
        return subPictures.get(index).getOriginalPal();
    }

    public int[] getFrameAlpha(int index) {
        return subPictures.get(index).getAlpha();
    }

    public int[] getOriginalFrameAlpha(int index) {
        return subPictures.get(index).getOriginalAlpha();
    }

    public BufferedImage getImage(Bitmap bm) {
        return bm.getImage(palette.getColorModel());
    }

    public Palette getPalette() {
        return palette;
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public BufferedImage getImage() {
        return bitmap.getImage(palette.getColorModel());
    }

    public int getPrimaryColorIndex() {
        return primaryColorIndex;
    }

    public SubPicture getSubPicture(int index) {
        return subPictures.get(index);
    }

    public int getFrameCount() {
        return subPictures.size();
    }

    public int getForcedFrameCount() {
        return forcedFrameCount;
    }

    public boolean isForced(int index) {
        return subPictures.get(index).isForced();
    }

    public void close() {
        if (buffer!=null)
            buffer.close();
    }

    public long getEndTime(int index) {
        return subPictures.get(index).getEndTime();
    }

    public long getStartTime(int index) {
        return subPictures.get(index).getStartTime();
    }

    public long getStartOffset(int index) {
        return subPictures.get(index).getOffset();
    }

    public int getLanguageIndex() {
        return languageIndex;
    }

    public Palette getSrcPalette() {
        return srcPalette;
    }

    public void setSrcPalette(Palette pal) {
        srcPalette = pal;
    }
} public final class SubDvdWriter {

    private static final Configuration configuration = Configuration.getInstance();

    private static final byte[] PACK_HEADER = {
            0x00, 0x00, 0x01, (byte)0xba,							// 0:  0x000001ba - packet ID
            0x44, 0x02, (byte)0xc4, (byte)0x82, 0x04, (byte)0xa9,	// 4:  system clock reference
            0x01 , (byte)0x89, (byte)0xc3,							// 10: multiplexer rate
            (byte)0xf8,												// 13: stuffing info
    };

    private static final byte[] HEADER_FIRST = {						// header only in first packet
            0x00, 0x00, 0x01, (byte)0xbd,							// 0: 0x000001bd - sub ID
            0x00, 0x00,												// 4: packet length
            (byte)0x81, (byte)0x80, 								// 6:  packet type
            0x05,													// 8:  PTS length
            0x00, 0x0, 0x00, 0x00, 0x00,							// 9:  PTS
            0x20,													// 14: stream ID
            0x00, 0x00,												// 15: Subpicture size in bytes
            0x00, 0x00,												// 17: offset to control header
    };

    private static final byte[] HEADER_NEXT = {						// header in following packets
            0x00, 0x00, 0x01, (byte)0xbd,							// 0: 0x000001bd - sub ID
            0x00, 0x00,												// 4: packet length
            (byte)0x81, (byte)0x00, 								// 6: packet type
            0x00,													// 8: PTS length = 0
            0x20													// 9: Stream ID
    };

    private static final byte[] CONTROL_HEADER = {
            0x00,													//  dummy byte (for shifting when forced)
            0x00, 0x00,												//  0: offset to end sequence
            0x01,													//  2: CMD 1: start displaying
            0x03, 0x32, 0x10,										//  3: CMD 3: Palette Info
            0x04, (byte)0xff, (byte)0xff,							//  6: CMD 4: Alpha Info
            0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,				//  9: CMD 5: sub position
            0x06, 0x00, 0x00, 0x00, 0x00,							// 16: CMD 6: rle offsets
            (byte)0xff,												// 21: End of control header
            0x00, 0x00,												// 22: display duration in 90kHz/1024
            0x00, 0x00,												// 24: offset to end sequence (again)
            0x02, (byte)0xff,										// 26: CMD 2: stop displaying
    };

    private SubDvdWriter() {
    }

    /**
     * Create the binary stream representation of one caption
     *
     * @param pic SubPicture object containing caption info
     * @param bm  bitmap
     * @return byte buffer containing the binary stream representation of one caption
     */
    public static byte[] createSubFrame(SubPictureDVD pic, Bitmap bm) {
        /* create RLE buffers */
        byte even[] = SupDvdUtil.encodeLines(bm, true);
        byte odd[] = SupDvdUtil.encodeLines(bm, false);
        int tmp;

        int forcedOfs;
        int controlHeaderLen;
        if (pic.isForced()) {
            forcedOfs = 0;
            CONTROL_HEADER[2] = 0x01; // display
            CONTROL_HEADER[3] = 0x00; // forced
            controlHeaderLen = CONTROL_HEADER.length;
        } else {
            forcedOfs = 1;
            CONTROL_HEADER[2] = 0x00; // part of offset
            CONTROL_HEADER[3] = 0x01; // display
            controlHeaderLen = CONTROL_HEADER.length - 1;
        }

        // fill out all info but the offets (determined later)

        /* header - contains PTM */
        int ptm = (int) pic.getStartTime(); // should be end time, but STC writes start time?
        HEADER_FIRST[9] = (byte) (((ptm >> 29) & 0x0E) | 0x21);
        HEADER_FIRST[10] = (byte) (ptm >> 22);
        HEADER_FIRST[11] = (byte) ((ptm >> 14) | 1);
        HEADER_FIRST[12] = (byte) (ptm >> 7);
        HEADER_FIRST[13] = (byte) (ptm * 2 + 1);

        /* control header */
        /* palette (store reversed) */
        CONTROL_HEADER[1 + 4] = (byte) (((pic.getPal()[3] & 0xf) << 4) | (pic.getPal()[2] & 0x0f));
        CONTROL_HEADER[1 + 5] = (byte) (((pic.getPal()[1] & 0xf) << 4) | (pic.getPal()[0] & 0x0f));
        /* alpha (store reversed) */
        CONTROL_HEADER[1 + 7] = (byte) (((pic.getAlpha()[3] & 0xf) << 4) | (pic.getAlpha()[2] & 0x0f));
        CONTROL_HEADER[1 + 8] = (byte) (((pic.getAlpha()[1] & 0xf) << 4) | (pic.getAlpha()[0] & 0x0f));

        /* coordinates of subtitle */
        CONTROL_HEADER[1 + 10] = (byte) ((pic.getXOffset() >> 4) & 0xff);
        tmp = pic.getXOffset() + bm.getWidth() - 1;
        CONTROL_HEADER[1 + 11] = (byte) (((pic.getXOffset() & 0xf) << 4) | ((tmp >> 8) & 0xf));
        CONTROL_HEADER[1 + 12] = (byte) (tmp & 0xff);

        int yOfs = pic.getYOffset() - configuration.getCropOffsetY();
        if (yOfs < 0) {
            yOfs = 0;
        } else {
            int yMax = pic.getHeight() - pic.getImageHeight() - 2 * configuration.getCropOffsetY();
            if (yOfs > yMax) {
                yOfs = yMax;
            }
        }

        CONTROL_HEADER[1 + 13] = (byte) ((yOfs >> 4) & 0xff);
        tmp = yOfs + bm.getHeight() - 1;
        CONTROL_HEADER[1 + 14] = (byte) (((yOfs & 0xf) << 4) | ((tmp >> 8) & 0xf));
        CONTROL_HEADER[1 + 15] = (byte) (tmp & 0xff);

        /* offset to even lines in rle buffer */
        CONTROL_HEADER[1 + 17] = 0x00; /* 2 bytes subpicture size and 2 bytes control header ofs */
        CONTROL_HEADER[1 + 18] = 0x04; /* note: SubtitleCreator uses 6 and adds 0x0000 in between */

        /* offset to odd lines in rle buffer */
        tmp = even.length + CONTROL_HEADER[1 + 18];
        CONTROL_HEADER[1 + 19] = (byte) ((tmp >> 8) & 0xff);
        CONTROL_HEADER[1 + 20] = (byte) (tmp & 0xff);

        /* display duration in frames */
        tmp = (int) ((pic.getEndTime() - pic.getStartTime()) / 1024); // 11.378ms resolution????
        CONTROL_HEADER[1 + 22] = (byte) ((tmp >> 8) & 0xff);
        CONTROL_HEADER[1 + 23] = (byte) (tmp & 0xff);

        /* offset to end sequence - 22 is the offset of the end sequence */
        tmp = even.length + odd.length + 22 + (pic.isForced() ? 1 : 0) + 4;
        CONTROL_HEADER[forcedOfs + 0] = (byte) ((tmp >> 8) & 0xff);
        CONTROL_HEADER[forcedOfs + 1] = (byte) (tmp & 0xff);
        CONTROL_HEADER[1 + 24] = (byte) ((tmp >> 8) & 0xff);
        CONTROL_HEADER[1 + 25] = (byte) (tmp & 0xff);

        // subpicture size
        tmp = even.length + odd.length + 4 + controlHeaderLen;
        HEADER_FIRST[15] = (byte) (tmp >> 8);
        HEADER_FIRST[16] = (byte) tmp;

        /* offset to control buffer - 2 is the size of the offset */
        tmp = even.length + odd.length + 2;
        HEADER_FIRST[17] = (byte) (tmp >> 8);
        HEADER_FIRST[18] = (byte) tmp;

        // in the SUB format only 0x800 bytes can be written per packet. If a packet
        // is larger, it has to be split into fragments <= 0x800 bytes
        // which follow one after the other.

        int sizeRLE = even.length + odd.length;
        int bufSize = PACK_HEADER.length + HEADER_FIRST.length + controlHeaderLen + sizeRLE;
        int numAdditionalPackets = 0;
        if (bufSize > 0x800) {
            // determine how many additional headers we will need
            // considering that each additional header also adds to the size
            // due to its own headers
            numAdditionalPackets = 1;
            int remainingRLEsize = sizeRLE - (0x800 - PACK_HEADER.length - HEADER_FIRST.length); // size - 0x7df
            while (remainingRLEsize > (0x800 - PACK_HEADER.length - HEADER_NEXT.length - controlHeaderLen)) {
                remainingRLEsize -= (0x800 - PACK_HEADER.length - HEADER_NEXT.length);
                bufSize += PACK_HEADER.length + HEADER_NEXT.length;
                numAdditionalPackets++;
            }
            // packet length of the 1st packet should be the maximum size
            tmp = 0x800 - PACK_HEADER.length - 6;
        } else {
            tmp = (bufSize - PACK_HEADER.length - 6);
        }

        // allocate and fill buffer
        byte buf[] = new byte[(1 + numAdditionalPackets) * 0x800];

        int stuffingBytes;
        int diff = buf.length - bufSize;
        if (diff > 0 && diff < 6) {
            stuffingBytes = diff;
        } else {
            stuffingBytes = 0;
        }

        int ofs = 0;
        for (byte packHeader : PACK_HEADER) {
            buf[ofs++] = packHeader;
        }

        // set packet length
        tmp += stuffingBytes;
        HEADER_FIRST[4] = (byte) (tmp >> 8);
        HEADER_FIRST[5] = (byte) tmp;

        // set pts length
        HEADER_FIRST[8] = (byte) (5 + stuffingBytes);

        // write header and use pts for stuffing bytes (if needed)
        for (int i = 0; i < 14; i++) {
            buf[ofs++] = HEADER_FIRST[i];
        }
        for (int i = 0; i < stuffingBytes; i++) {
            buf[ofs++] = (byte) 0xff;
        }
        for (int i = 14; i < HEADER_FIRST.length; i++) {
            buf[ofs++] = HEADER_FIRST[i];
        }

        // write (first part of) RLE buffer
        tmp = sizeRLE;
        if (numAdditionalPackets > 0) {
            tmp = (0x800 - PACK_HEADER.length - stuffingBytes - HEADER_FIRST.length);
            if (tmp > sizeRLE) { // can only happen in 1st buffer
                tmp = sizeRLE;
            }
        }
        for (int i = 0; i < tmp; i++) {
            if (i < even.length) {
                buf[ofs++] = even[i];
            } else {
                buf[ofs++] = odd[i - even.length];
            }
        }
        int ofsRLE = tmp;

        // fill gap in first packet with (parts of) control header
        // only if the control header is split over two packets
        int controlHeaderWritten = 0;
        if (numAdditionalPackets == 1 && ofs < 0x800) {
            for (; ofs < 0x800; ofs++) {
                buf[ofs] = CONTROL_HEADER[forcedOfs + (controlHeaderWritten++)];
            }
        }

        // write additional packets
        for (int p = 0; p < numAdditionalPackets; p++) {
            int rleSizeLeft;
            if (p == numAdditionalPackets - 1) {
                // last loop
                rleSizeLeft = sizeRLE - ofsRLE;
                tmp = HEADER_NEXT.length + (controlHeaderLen - controlHeaderWritten) + (sizeRLE - ofsRLE) - 6;
            } else {
                tmp = 0x800 - PACK_HEADER.length - 6;
                rleSizeLeft = (0x800 - PACK_HEADER.length - HEADER_NEXT.length);
                // now, again, it could happen that the RLE buffer runs out before the last package
                if (rleSizeLeft > (sizeRLE - ofsRLE)) {
                    rleSizeLeft = sizeRLE - ofsRLE;
                }
            }
            // copy packet headers
            PACK_HEADER[13] = (byte) (0xf8);
            for (byte packHeader : PACK_HEADER) {
                buf[ofs++] = packHeader;
            }

            // set packet length
            HEADER_NEXT[4] = (byte) (tmp >> 8);
            HEADER_NEXT[5] = (byte) tmp;
            for (byte b : HEADER_NEXT) {
                buf[ofs++] = b;
            }

            // copy RLE buffer
            for (int i = ofsRLE; i < ofsRLE + rleSizeLeft; i++) {
                if (i < even.length) {
                    buf[ofs++] = even[i];
                } else {
                    buf[ofs++] = odd[i - even.length];
                }
            }
            ofsRLE += rleSizeLeft;
            // fill possible gap in all but last package with (parts of) control header
            // only if the control header is split over two packets
            // this can only happen in the package before the last one though
            if (p != numAdditionalPackets - 1) {
                for (; ofs < (p + 2) * 0x800; ofs++) {
                    buf[ofs] = CONTROL_HEADER[forcedOfs + (controlHeaderWritten++)];
                }
            }
        }

        // write (rest of) control header
        for (int i = controlHeaderWritten; i < controlHeaderLen; i++) {
            buf[ofs++] = CONTROL_HEADER[forcedOfs + i];
        }

        // fill rest of last packet with padding bytes
        diff = buf.length - ofs;
        if (diff >= 6) {
            diff -= 6;
            buf[ofs++] = 0x00;
            buf[ofs++] = 0x00;
            buf[ofs++] = 0x01;
            buf[ofs++] = (byte) 0xbe;
            buf[ofs++] = (byte) (diff >> 8);
            buf[ofs++] = (byte) diff;
            for (; ofs < buf.length; ofs++) {
                buf[ofs] = (byte) 0xff;
            }
        } // else should never happen due to stuffing bytes

        return buf;
    }

    /**
     * Create VobSub IDX file
     * @param fname file name
     * @param pic a SubPicture object used to read screen width and height
     * @param offsets array of offsets (one for each caption)
     * @param timestamps array of PTS time stamps (one for each caption)
     * @param palette 16 color main Palette
     * @throws bdsup2sub.core.CoreException
     */
    public static void writeIdx(String fname, SubPicture pic, int[] offsets, int[] timestamps, Palette palette) throws CoreException {
        BufferedWriter out = null;
        try {
            out = new BufferedWriter(new FileWriter(fname));

            out.write("# VobSub index file, v7 (do not modify this line!)"); out.newLine();
            out.write("# Created by " + Constants.APP_NAME + " " + Constants.APP_VERSION); out.newLine();
            out.newLine();
            out.write("# Frame size"); out.newLine();
            out.write("size: " + pic.getWidth() + "x" + (pic.getHeight() -2 * configuration.getCropOffsetY())); out.newLine();
            out.newLine();
            out.write("# Origin - upper-left corner"); out.newLine();
            out.write("org: 0, 0"); out.newLine();
            out.newLine();
            out.write("# Scaling"); out.newLine();
            out.write("scale: 100%, 100%"); out.newLine();
            out.newLine();
            out.write("# Alpha blending"); out.newLine();
            out.write("alpha: 100%"); out.newLine();
            out.newLine();
            out.write("# Smoothing"); out.newLine();
            out.write("smooth: OFF"); out.newLine();
            out.newLine();
            out.write("# Fade in/out in milliseconds"); out.newLine();
            out.write("fadein/out: 0, 0"); out.newLine();
            out.newLine();
            out.write("# Force subtitle placement relative to (org.x, org.y)"); out.newLine();
            out.write("align: OFF at LEFT TOP"); out.newLine();
            out.newLine();
            out.write("# For correcting non-progressive desync. (in millisecs or hh:mm:ss:ms)"); out.newLine();
            out.write("time offset: 0"); out.newLine();
            out.newLine();
            out.write("# ON: displays only forced subtitles, OFF: shows everything"); out.newLine();
            out.write("forced subs: OFF"); out.newLine();
            out.newLine();
            out.write("# The palette of the generated file"); out.newLine();
            out.write("palette: ");
            //Palette pal = Core.getCurrentDVDPalette();
            for (int i=0; i < palette.getSize(); i++) {
                int rbg[] = palette.getRGB(i);
                int val = (rbg[0]<<16) | (rbg[1]<<8) | rbg[2];
                out.write(ToolBox.toHexLeftZeroPadded(val, 6).substring(2));
                if (i != palette.getSize()-1) {
                    out.write(", ");
                }
            }
            out.newLine();out.newLine();
            out.write("# Custom colors (transp idxs and the four colors)"); out.newLine();
            out.write("custom colors: OFF, tridx: 1000, colors: 000000, 444444, 888888, cccccc"); out.newLine();
            out.newLine();
            out.write("# Language index in use"); out.newLine();
            out.write("langidx: 0"); out.newLine();
            out.newLine();
            out.write("# " + LANGUAGES[configuration.getLanguageIdx()][0]); out.newLine();
            out.write("id: " + LANGUAGES[configuration.getLanguageIdx()][1] + ", index: 0"); out.newLine();
            out.write("# Decomment next line to activate alternative name in DirectVobSub / Windows Media Player 6.x"); out.newLine();
            out.write("# alt: " + LANGUAGES[configuration.getLanguageIdx()][0]); out.newLine();
            out.write("# Vob/Cell ID: 1, 1 (PTS: 0)"); out.newLine();
            for (int i=0; i < timestamps.length; i++) {
                out.write("timestamp: "+ptsToTimeStrIdx(timestamps[i]));
                out.write(", filepos: "+ToolBox.toHexLeftZeroPadded(offsets[i], 9).substring(2));
                out.newLine();
            }
        } catch (IOException ex) {
            throw new CoreException(ex.getMessage());
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ex) {
            }
        }
    }
} public class SubPictureDVD extends SubPicture {

    /** offset to information in SUB file */
    private long offset;
    /** size of RLE buffer */
    private int rleSize;
    /** even line offset (inside RLE buffer) */
    private int evenOffset;
    /** odd line offset (inside RLE buffer) */
    private int oddOffset;
    /** list of RLE fragments */
    private List<ImageObjectFragment> rleFragments;
    /** uncropped bitmap width */
    private int originalWidth;
    /** uncropped bitmap height */
    private int originalHeight;
    /** original x offset of uncropped bitmap */
    private int originalX;
    /** original y offset of uncropped bitmap */
    private int originalY;
    /** 4 original alpha values */
    private int[] originalAlpha;
    /** 4 original palette values*/
    private int[] originalPal;
    /** 4 alpha values */
    private int[] alpha;
    /** 4 palette values */
    private int[] pal;

    public void storeOriginal() {
        originalWidth = getImageWidth();
        originalHeight = getImageHeight();
        originalX = getXOffset();
        originalY = getYOffset();

        originalAlpha = Arrays.copyOf(alpha, alpha.length);
        originalPal = Arrays.copyOf(pal, pal.length);
    }

    /**
     * Copy info of given generic subpicture into this DVD subpicture
     * Used to copy the edited info (position, forced flags etc. into a DVD SubPicture for writing
     * @param pic
     */
    public void copyInfo(SubPicture pic) {
        setWidth(pic.getWidth());
        setHeight(pic.getHeight());
        setStartTime(pic.getStartTime());
        setEndTime(pic.getEndTime());
        setForced(pic.isForced());
        setCompositionNumber(pic.getCompositionNumber());
        setImageWidth(pic.getImageWidth());
        setImageHeight(pic.getImageHeight());
        setOfsX(pic.getXOffset());
        setOfsY(pic.getYOffset());
    }

    public long getOffset() {
        return offset;
    }

    public void setOffset(long offset) {
        this.offset = offset;
    }

    public int getRleSize() {
        return rleSize;
    }

    public void setRleSize(int rleSize) {
        this.rleSize = rleSize;
    }

    public int getEvenOffset() {
        return evenOffset;
    }

    public void setEvenOffset(int evenOffset) {
        this.evenOffset = evenOffset;
    }

    public int getOddOffset() {
        return oddOffset;
    }

    public void setOddOffset(int oddOffset) {
        this.oddOffset = oddOffset;
    }

    public List<ImageObjectFragment> getRleFragments() {
        return rleFragments;
    }

    public void setRleFragments(List<ImageObjectFragment> rleFragments) {
        this.rleFragments = rleFragments;
    }

    public int getOriginalWidth() {
        return originalWidth;
    }

    public int getOriginalHeight() {
        return originalHeight;
    }

    public int getOriginalX() {
        return originalX;
    }

    public int getOriginalY() {
        return originalY;
    }

    public int[] getOriginalAlpha() {
        return originalAlpha;
    }

    public int[] getOriginalPal() {
        return originalPal;
    }

    public int[] getAlpha() {
        return alpha;
    }

    public void setAlpha(int[] alpha) {
        this.alpha = alpha;
    }

    public int[] getPal() {
        return pal;
    }

    public void setPal(int[] pal) {
        this.pal = pal;
    }
} public class SupDvd implements DvdSubtitleStream {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger logger = Logger.getInstance();

    private List<SubPictureDVD> subPictures = new ArrayList<SubPictureDVD>();
    private int screenWidth = 720;
    private int screenHeight = 576;
    private int languageIdx;

    private Palette srcPalette;
    /** color palette created for last decoded caption  */
    private Palette palette;
    /** bitmap of the last decoded caption  */
    private Bitmap bitmap;
    /** FileBuffer for reading SUP */
    private FileBuffer fileBuffer;
    /** index of dominant color for the current caption  */
    private int primaryColorIndex;
    /** number of forced captions in the current file  */
    private int numForcedFrames;

    /** store last alpha values for invisible workaround */
    private static int[] lastAlpha = { 0, 0xf, 0xf, 0xf };


    public SupDvd(String supFile, String ifoFile) throws CoreException {
        IfoParser ifoParser = new IfoParser(ifoFile);
        this.screenHeight = ifoParser.getScreenHeight();
        this.screenWidth = ifoParser.getScreenWidth();
        this.languageIdx = ifoParser.getLanguageIdx();
        this.srcPalette = ifoParser.getSrcPalette();

        readSupFile(supFile);
    }

    private void readSupFile(String supFile) throws CoreException {
        try {
            long offset = 0;
            fileBuffer = new FileBuffer(supFile);
            long size = fileBuffer.getSize();
            Core.setProgressMax((int) size);
            int i = 0;
            do {
                logger.info("# " + (++i) + "\n");
                Core.setProgress(offset);
                logger.trace("Offset: " + ToolBox.toHexLeftZeroPadded(offset, 8) + "\n");
                offset = readSupFrame(offset, fileBuffer);
            } while (offset < size);
        } catch (FileBufferException e) {
            throw new CoreException(e.getMessage());
        }
        logger.info("\nDetected " + numForcedFrames + " forced captions.\n");
    }

    private long readSupFrame(long offset, FileBuffer buffer) throws CoreException  {
        long ctrlOffset;
        int  ctrlOfsRel;
        int  rleSize;
        int  ctrlSize;
        ImageObjectFragment rleFrag;
        int  length;
        byte[] ctrlHeader;

        try {
            // 2 bytes:  packet identifier 0x5350
            long startOffset = offset;
            if (buffer.getWord(offset) != 0x5350) {
                throw new CoreException("Missing packet identifier at offset " + ToolBox.toHexLeftZeroPadded(offset, 8));
            }
            // 8 bytes PTS:  system clock reference, but use only the first 4
            SubPictureDVD pic = new SubPictureDVD();
            pic.setOffset(offset);
            pic.setWidth(screenWidth);
            pic.setHeight(screenHeight);

            int pts = buffer.getDWordLE(offset += 2);
            pic.setStartTime(pts);
            // 2 bytes:  packet length (number of bytes after this entry)
            length = buffer.getWord(offset += 8);
            // 2 bytes: offset to control buffer
            ctrlOfsRel = buffer.getWord(offset += 2);
            rleSize = ctrlOfsRel - 2;           // calculate size of RLE buffer
            ctrlSize = length - ctrlOfsRel - 2; // calculate size of control header
            if (ctrlSize < 0) {
                throw new CoreException("Invalid control buffer size");
            }
            ctrlOffset = ctrlOfsRel + offset;   // absolute offset of control header
            offset += 2;
            pic.setRleFragments(new ArrayList<ImageObjectFragment>(1));
            rleFrag = new ImageObjectFragment(offset, rleSize);
            pic.getRleFragments().add(rleFrag);
            pic.setRleSize(rleSize);

            pic.setPal(new int[4]);
            pic.setAlpha(new int[4]);
            int alphaSum = 0;
            int[] alphaUpdate = new int[4];
            int alphaUpdateSum;
            int delay = -1;
            boolean colorAlphaUpdate = false;

            logger.trace("SP_DCSQT at ofs: " + ToolBox.toHexLeftZeroPadded(ctrlOffset, 8) + "\n");

            // copy control header in buffer (to be more compatible with VobSub)
            ctrlHeader = new byte[ctrlSize];
            for (int i=0; i < ctrlSize; i++) {
                ctrlHeader[i] = (byte)buffer.getByte(ctrlOffset + i);
            }

            try {
                // parse control header
                int b;
                int index = 0;
                int endSeqOfs = getWord(ctrlHeader, index) - ctrlOfsRel - 2;
                if (endSeqOfs < 0 || endSeqOfs > ctrlSize) {
                    logger.warn("Invalid end sequence offset -> no end time\n");
                    endSeqOfs = ctrlSize;
                }
                index += 2;
                parse_ctrl:
                while (index < endSeqOfs) {
                    int cmd = getByte(ctrlHeader, index++);
                    switch (cmd) {
                        case 0: // forced (?)
                            pic.setForced(true);
                            numForcedFrames++;
                            break;
                        case 1: // start display
                            break;
                        case 3: // palette info
                            b = getByte(ctrlHeader, index++);
                            pic.getPal()[3] = (b >> 4);
                            pic.getPal()[2] = b & 0x0f;
                            b = getByte(ctrlHeader, index++);
                            pic.getPal()[1] = (b >> 4);
                            pic.getPal()[0] = b & 0x0f;
                            logger.trace("Palette:   " + pic.getPal()[0] + ", " + pic.getPal()[1] + ", " + pic.getPal()[2] + ", " + pic.getPal()[3] + "\n");
                            break;
                        case 4: // alpha info
                            b = getByte(ctrlHeader, index++);
                            pic.getAlpha()[3] = (b >> 4);
                            pic.getAlpha()[2] = b & 0x0f;
                            b = getByte(ctrlHeader, index++);
                            pic.getAlpha()[1] = (b >> 4);
                            pic.getAlpha()[0] = b & 0x0f;
                            for (int i = 0; i < 4; i++) {
                                alphaSum += pic.getAlpha()[i] & 0xff;
                            }
                            logger.trace("Alpha:     " + pic.getAlpha()[0] + ", " + pic.getAlpha()[1] + ", " + pic.getAlpha()[2] + ", " + pic.getAlpha()[3] + "\n");
                            break;
                        case 5: // coordinates
                            int xOfs = (getByte(ctrlHeader, index) << 4) | (getByte(ctrlHeader, index+1) >> 4);
                            pic.setOfsX(xOfs);
                            pic.setImageWidth((((getByte(ctrlHeader, index + 1) & 0xf) << 8) | (getByte(ctrlHeader, index + 2))) - xOfs + 1);
                            int yOfs = (getByte(ctrlHeader, index + 3) << 4) | (getByte(ctrlHeader, index + 4) >> 4);
                            pic.setOfsY(yOfs);
                            pic.setImageHeight((((getByte(ctrlHeader, index + 4) & 0xf) << 8) | (getByte(ctrlHeader, index + 5))) - yOfs + 1);
                            logger.trace("Area info:" + " ("
                                    + pic.getXOffset() + ", " + pic.getYOffset() + ") - (" + (pic.getXOffset() + pic.getImageWidth() - 1) + ", "
                                    + (pic.getYOffset() + pic.getImageHeight() - 1) + ")\n");
                            index += 6;
                            break;
                        case 6: // offset to RLE buffer
                            pic.setEvenOffset(getWord(ctrlHeader, index) - 4);
                            pic.setOddOffset(getWord(ctrlHeader, index + 2) - 4);
                            index += 4;
                            logger.trace("RLE ofs:   " + ToolBox.toHexLeftZeroPadded(pic.getEvenOffset(), 4) + ", " + ToolBox.toHexLeftZeroPadded(pic.getOddOffset(), 4) + "\n");
                            break;
                        case 7: // color/alpha update
                            colorAlphaUpdate = true;
                            //int len = ToolBox.getWord(ctrlHeader, index);
                            // ignore the details for now, but just get alpha and palette info
                            alphaUpdateSum = 0;
                            b = getByte(ctrlHeader, index + 10);
                            alphaUpdate[3] = (b >> 4);
                            alphaUpdate[2] = b & 0x0f;
                            b = getByte(ctrlHeader, index + 11);
                            alphaUpdate[1] = (b >> 4);
                            alphaUpdate[0] = b & 0x0f;
                            for (int i = 0; i < 4; i++) {
                                alphaUpdateSum += alphaUpdate[i] & 0xff;
                            }
                            // only use more opaque colors
                            if (alphaUpdateSum > alphaSum) {
                                alphaSum = alphaUpdateSum;
                                System.arraycopy(alphaUpdate, 0, pic.getAlpha(), 0, 4);
                                // take over frame palette
                                b = getByte(ctrlHeader, index+8);
                                pic.getPal()[3] = (b >> 4);
                                pic.getPal()[2] = b & 0x0f;
                                b = getByte(ctrlHeader, index+9);
                                pic.getPal()[1] = (b >> 4);
                                pic.getPal()[0] = b & 0x0f;
                            }
                            // search end sequence
                            index = endSeqOfs;
                            delay = getWord(ctrlHeader, index) * 1024;
                            endSeqOfs = getWord(ctrlHeader, index + 2)-ctrlOfsRel - 2;
                            if (endSeqOfs < 0 || endSeqOfs > ctrlSize) {
                                logger.warn("Invalid end sequence offset -> no end time\n");
                                endSeqOfs = ctrlSize;
                            }
                            index += 4;
                            break;
                        case 0xff: // end sequence
                            break parse_ctrl;
                        default:
                            logger.warn("Unknown control sequence " + toHexLeftZeroPadded(cmd, 2) + " skipped\n");
                            break;
                    }
                }

                if (endSeqOfs != ctrlSize) {
                    int ctrlSeqCount = 1;
                    index = -1;
                    int nextIndex = endSeqOfs;
                    while (nextIndex != index) {
                        index = nextIndex;
                        delay = getWord(ctrlHeader, index) * 1024;
                        nextIndex = getWord(ctrlHeader, index + 2) - ctrlOfsRel - 2;
                        ctrlSeqCount++;
                    }
                    if (ctrlSeqCount > 2) {
                        logger.warn("Control sequence(s) ignored - result may be erratic.");
                    }
                    pic.setEndTime(pic.getStartTime() + delay);
                } else {
                    pic.setEndTime(pic.getStartTime());
                }

                pic.storeOriginal();

                if (colorAlphaUpdate) {
                    logger.warn("Palette update/alpha fading detected - result may be erratic.\n");
                }

                if (alphaSum == 0) {
                    if (configuration.getFixZeroAlpha()) {
                        System.arraycopy(lastAlpha, 0, pic.getAlpha(), 0, 4);
                        logger.warn("Invisible caption due to zero alpha - used alpha info of last caption.\n");
                    } else {
                        logger.warn("Invisible caption due to zero alpha (not fixed due to user setting).\n");
                    }
                }
                lastAlpha = pic.getAlpha();
            } catch (IndexOutOfBoundsException ex) {
                throw new CoreException("Index " + ex.getMessage() + " out of bounds in control header.");
            }

            subPictures.add(pic);
            return startOffset + length + 0x0a;
        } catch (FileBufferException ex) {
            throw new CoreException(ex.getMessage());
        }
    }

    public void decode(int index) throws CoreException {
        if (index < subPictures.size()) {
            decode(subPictures.get(index));
        } else {
            throw new CoreException("Index " + index + " out of bounds\n");
        }
    }

    private void decode(SubPictureDVD pic) throws CoreException {
        palette = SupDvdUtil.decodePalette(pic, srcPalette);
        bitmap  = SupDvdUtil.decodeImage(pic, fileBuffer, palette.getIndexOfMostTransparentPaletteEntry());

        // crop
        BitmapBounds bounds = bitmap.getCroppingBounds(palette.getAlpha(), configuration.getAlphaCrop());
        if (bounds.yMin > 0 || bounds.xMin > 0 || bounds.xMax < bitmap.getWidth() - 1 || bounds.yMax < bitmap.getHeight() - 1) {
            int width = bounds.xMax - bounds.xMin + 1;
            int height = bounds.yMax - bounds.yMin + 1;
            if (width < 2) {
                width = 2;
            }
            if (height < 2) {
                height = 2;
            }
            bitmap = bitmap.crop(bounds.xMin, bounds.yMin, width, height);
            // update picture
            pic.setImageWidth(width);
            pic.setImageHeight(height);
            pic.setOfsX(pic.getOriginalX() + bounds.xMin);
            pic.setOfsY(pic.getOriginalY() + bounds.yMin);
        }
        primaryColorIndex = bitmap.getPrimaryColorIndex(palette.getAlpha(), configuration.getAlphaThreshold(), palette.getY());
    }

    public int[] getFramePalette(int index) {
        return subPictures.get(index).getPal();
    }

    public int[] getOriginalFramePalette(int index) {
        return subPictures.get(index).getOriginalPal();
    }

    public int[] getFrameAlpha(int index) {
        return subPictures.get(index).getAlpha();
    }

    public int[] getOriginalFrameAlpha(int index) {
        return subPictures.get(index).getOriginalAlpha();
    }

    public BufferedImage getImage(Bitmap bm) {
        return bm.getImage(palette.getColorModel());
    }

    public Palette getPalette() {
        return palette;
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public BufferedImage getImage() {
        return bitmap.getImage(palette.getColorModel());
    }

    public int getPrimaryColorIndex() {
        return primaryColorIndex;
    }

    public SubPicture getSubPicture(int index) {
        return subPictures.get(index);
    }

    public int getFrameCount() {
        return subPictures.size();
    }

    public int getForcedFrameCount() {
        return numForcedFrames;
    }

    public boolean isForced(int index) {
        return subPictures.get(index).isForced();
    }

    public void close() {
        if (fileBuffer != null) {
            fileBuffer.close();
        }
    }

    public long getEndTime(int index) {
        return subPictures.get(index).getEndTime();
    }

    public long getStartTime(int index) {
        return subPictures.get(index).getStartTime();
    }

    public long getStartOffset(int index) {
        return subPictures.get(index).getOffset();
    }

    public int getLanguageIndex() {
        return languageIdx;
    }

    public Palette getSrcPalette() {
        return srcPalette;
    }

    public void setSrcPalette(Palette pal) {
        srcPalette = pal;
    }
} public final class SupDvdUtil {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger logger = Logger.getInstance();


    private SupDvdUtil() {
    }

    /**
     * Compress bitmap to RLE buffer
     *
     * @param bm   bitmap to compress
     * @param even true: encode even lines, false: encode odd lines
     * @return RLE buffer
     */
    public static byte[] encodeLines(Bitmap bm, boolean even) {
        int ofs;
        byte color;
        int len;
        int y;
        ArrayList<Byte> nibbles = new ArrayList<Byte>();

        if (even) {
            y = 0;
        } else {
            y = 1;
        }

        for (; y < bm.getHeight(); y += 2) {
            ofs = y * bm.getWidth();
            for (int x = 0; x < bm.getWidth(); x += len, ofs += len) {
                color = bm.getInternalBuffer()[ofs];
                for (len = 1; x + len < bm.getWidth(); len++)
                    if (bm.getInternalBuffer()[ofs + len] != color) {
                        break;
                    }
                if (len < 4) {
                    nibbles.add((byte) ((len << 2) | (color & 3)));
                } else if (len < 0x10) {
                    nibbles.add((byte) (len >> 2));
                    nibbles.add((byte) ((len << 2) | (color & 3)));
                } else if (len < 0x40) {
                    nibbles.add((byte) 0);
                    nibbles.add((byte) (len >> 2));
                    nibbles.add((byte) ((len << 2) | (color & 3)));
                } else if (x + len == bm.getWidth()) {
                    nibbles.add((byte) (0));
                    nibbles.add((byte) (0));
                    nibbles.add((byte) (0));
                    nibbles.add(color);
                } else {
                    if (len > 0xff) {
                        len = 0xff;
                    }
                    nibbles.add((byte) (0));
                    nibbles.add((byte) (len >> 6));
                    nibbles.add((byte) (len >> 2));
                    nibbles.add((byte) ((len << 2) | (color & 3)));
                }
            }
            if ((nibbles.size() & 1) == 1) {
                nibbles.add((byte) (0));
            }
        }
        // end buffer with line feed
        nibbles.add((byte) (0));
        nibbles.add((byte) (0));
        nibbles.add((byte) (0));
        nibbles.add((byte) (0));

        int size = nibbles.size() / 2; // number of bytes
        byte[] retval = new byte[size];
        Iterator<Byte> it = nibbles.iterator();

        for (int i = 0; i < size; i++) {
            int hi = (it.next() & 0xf);
            int lo = (it.next() & 0xf);
            retval[i] = (byte) ((hi << 4) | lo);
        }
        return retval;
    }

    /**
     * create fitting four color palette for the given caption
     * @param pic SubPicture object containing info about the caption
     * @param pal base palette
     * @return decoded palette
     */
    public static Palette decodePalette(SubPictureDVD pic, Palette pal) {
        Palette miniPal = new Palette(4, true);
        for (int i=0; i < 4; i++) {
            int a = (pic.getAlpha()[i] * 0xff) / 0xf;
            if (a >= configuration.getAlphaCrop()) {
                miniPal.setRGB(i, pal.getR()[pic.getPal()[i]]&0xff, pal.getG()[pic.getPal()[i]]&0xff, pal.getB()[pic.getPal()[i]]&0xff);
                miniPal.setAlpha(i, a);
            } else {
                miniPal.setARGB(i, 0);
            }
        }
        return miniPal;
    }

    /**
     * decode caption from the input stream
     * @param pic SubPicture object containing info about the caption
     * @param transIdx index of the transparent color
     * @return bitmap of the decoded caption
     * @throws bdsup2sub.core.CoreException
     */
    public static Bitmap decodeImage(final SubPictureDVD pic, final FileBuffer fBuf, final int transIdx) throws CoreException {
        int w = pic.getOriginalWidth();
        int h = pic.getOriginalHeight();
        int warnings = 0;

        ImageObjectFragment info = pic.getRleFragments().get(0);
        long startOfs = info.getImageBufferOfs();

        if (w > pic.getWidth() || h > pic.getHeight()) {
            logger.warn("Subpicture too large: " + w + "x" + h
                    + " at offset " + ToolBox.toHexLeftZeroPadded(startOfs, 8) + "\n");
        }

        Bitmap bm = new Bitmap(w, h, (byte)transIdx);

        // copy buffer(s)
        byte buf[] = new byte[pic.getRleSize()];
        int index = 0;

        int sizeEven;
        int sizeOdd;

        if (pic.getOddOffset() > pic.getEvenOffset()) {
            sizeEven = pic.getOddOffset() - pic.getEvenOffset();
            sizeOdd = pic.getRleSize() - pic.getOddOffset();
        } else {
            sizeOdd = pic.getEvenOffset() - pic.getOddOffset();
            sizeEven = pic.getRleSize() - pic.getEvenOffset();
        }

        if (sizeEven <= 0 || sizeOdd <= 0)
            throw new CoreException("Corrupt buffer offset information");

        try {
            // copy buffers
            try {
                for (int p = 0; p < pic.getRleFragments().size(); p++) {
                    // copy data of all packet to one common buffer
                    info = pic.getRleFragments().get(p);
                    for (int i=0; i < info.getImagePacketSize(); i++) {
                        buf[index+i] = (byte)fBuf.getByte(info.getImageBufferOfs() + i);
                    }
                    index += info.getImagePacketSize();
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                warnings++;
            }
            // decode even lines
            try {
                decodeLine(buf, pic.getEvenOffset(), sizeEven, bm.getInternalBuffer(), 0, w,  w*(h/2+(h&1)));
            } catch (ArrayIndexOutOfBoundsException ex) {
                warnings++;
            }
            // decode odd lines
            try {
                decodeLine(buf, pic.getOddOffset(), sizeOdd, bm.getInternalBuffer(), w, w, (h/2)*w);
            } catch (ArrayIndexOutOfBoundsException ex) {
                warnings++;
            }

            if (warnings > 0) {
                logger.warn("problems during RLE decoding of picture at offset " + ToolBox.toHexLeftZeroPadded(startOfs, 8) + "\n");
            }
            return bm;
        } catch (FileBufferException ex) {
            throw new CoreException(ex.getMessage());
        }
    }

    /**
     * decode one line from the RLE buffer
     * @param src source buffer
     * @param srcOfs offset in source buffer
     * @param srcLen length of bytes to decode in source buffer
     * @param trg target buffer for uncompressed data
     * @param trgOfs offset in target buffer
     * @param width image width of encoded caption
     * @param maxPixels maximum number of pixels in caption
     */
    private static void decodeLine(byte[] src, int srcOfs, int srcLen, byte[] trg, int trgOfs, int width, int maxPixels) {
        // to make access to nibbles easier, copy bytes to a nibble array
        byte nibbles[] = new byte[srcLen * 2];
        int b;

        for (int i=0; i < srcLen; i++) {
            b = src[srcOfs + i] & 0xff;
            nibbles[2 * i]   = (byte)(b >> 4);
            nibbles[2 * i + 1] = (byte)(b & 0x0f);
        }

        int index = 0;
        int sumPixels = 0;
        int x=0;

        while (index < nibbles.length && sumPixels < maxPixels) {
            int len;
            int col;
            b = nibbles[index++] & 0xff;
            if (b == 0) {
                // three or four nibble code
                b = nibbles[index++] & 0xff;
                if ((b & 0xc) != 0) {
                    // three byte code
                    len = b << 2;
                    b = nibbles[index++] & 0xff;
                    len |= (b >> 2);
                } else {
                    // line feed or four nibble code
                    len = b << 6;
                    b = nibbles[index++] & 0xff;
                    len  |= (b << 2);
                    b = nibbles[index++] & 0xff;
                    len |= (b >> 2);
                    if (len == 0) {
                        // line feed
                        len = width-x;
                        if (len <= 0 || sumPixels >= maxPixels) {
                            len = 0;
                            // handle line feed
                            trgOfs += 2 * width; // lines are interlaced!
                            sumPixels = ((trgOfs/width) / 2) * width;
                            x = 0;
                        }
                        if ((index & 1) == 1) {
                            index++;
                        }
                    }
                }
            } else {
                // one or two nibble code
                len = b >> 2;
                if (len == 0) {
                    // two nibble code
                    len = b << 2;
                    b = nibbles[index++] & 0xff;
                    len  |= (b >> 2);
                }
            }

            col = b & 0x3;
            sumPixels += len;

            for (int i=0; i < len; i++) {
                trg[trgOfs+x] = (byte)col;
                if (++x >= width) {
                    trgOfs += 2*width; // lines are interlaced!
                    x = 0;
                    if ((index & 1) == 1) {
                        index++;
                    }
                }
            }
        }
    }
} public final class SupDvdWriter {

    private static final Configuration configuration = Configuration.getInstance();

    private static final byte[] CONTROL_HEADER = {
            0x00,													//  dummy byte (for shifting when forced)
            0x00, 0x00,												//  0: offset to end sequence
            0x01,													//  2: CMD 1: start displaying
            0x03, 0x32, 0x10,										//  3: CMD 3: Palette Info
            0x04, (byte)0xff, (byte)0xff,							//  6: CMD 4: Alpha Info
            0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,				//  9: CMD 5: sub position
            0x06, 0x00, 0x00, 0x00, 0x00,							// 16: CMD 6: rle offsets
            (byte)0xff,												// 21: End of control header
            0x00, 0x00,												// 22: display duration in 90kHz/1024
            0x00, 0x00,												// 24: offset to end sequence (again)
            0x02, (byte)0xff,										// 26: CMD 2: stop displaying
    };

    private SupDvdWriter() {
    }

    /**
     * Create the binary stream representation of one caption
     *
     * @param pic SubPicture object containing caption info
     * @param bm  bitmap
     * @return byte buffer containing the binary stream representation of one caption
     */
    public static byte[] createSupFrame(SubPictureDVD pic, Bitmap bm) {
        /* create RLE buffers */
        byte[] even = SupDvdUtil.encodeLines(bm, true);
        byte[] odd = SupDvdUtil.encodeLines(bm, false);
        int tmp;

        int forcedOfs;
        int controlHeaderLen;
        if (pic.isForced()) {
            forcedOfs = 0;
            CONTROL_HEADER[2] = 0x01; // display
            CONTROL_HEADER[3] = 0x00; // forced
            controlHeaderLen = CONTROL_HEADER.length;
        } else {
            forcedOfs = 1;
            CONTROL_HEADER[2] = 0x00; // part of offset
            CONTROL_HEADER[3] = 0x01; // display
            controlHeaderLen = CONTROL_HEADER.length - 1;
        }

        // fill out all info but the offets (determined later)
        int sizeRLE = even.length + odd.length;
        int bufSize = 10 + 4 + controlHeaderLen + sizeRLE;
        byte[] buf = new byte[bufSize];

        // write header
        buf[0] = 0x53;
        buf[1] = 0x50;
        // write PTS (4 bytes of 8 bytes used) - little endian!
        int pts = (int) pic.getStartTime();
        buf[5] = (byte) (pts >> 24);
        buf[4] = (byte) (pts >> 16);
        buf[3] = (byte) (pts >> 8);
        buf[2] = (byte) pts;

        // write packet size
        tmp = controlHeaderLen + sizeRLE + 4; // 4 for the size and the offset
        buf[10] = (byte) (tmp >> 8);
        buf[11] = (byte) (tmp);

        // write offset to control header +
        tmp = sizeRLE + 2; // 2 for the offset
        buf[12] = (byte) (tmp >> 8);
        buf[13] = (byte) (tmp);

        // copy rle buffers
        int ofs = 14;
        for (byte b : even) {
            buf[ofs++] = b;
        }
        for (byte b : odd) {
            buf[ofs++] = b;
        }

        /* create control header */
        /* palette (store reversed) */
        CONTROL_HEADER[1 + 4] = (byte) (((pic.getPal()[3] & 0xf) << 4) | (pic.getPal()[2] & 0x0f));
        CONTROL_HEADER[1 + 5] = (byte) (((pic.getPal()[1] & 0xf) << 4) | (pic.getPal()[0] & 0x0f));
        /* alpha (store reversed) */
        CONTROL_HEADER[1 + 7] = (byte) (((pic.getAlpha()[3] & 0xf) << 4) | (pic.getAlpha()[2] & 0x0f));
        CONTROL_HEADER[1 + 8] = (byte) (((pic.getAlpha()[1] & 0xf) << 4) | (pic.getAlpha()[0] & 0x0f));

        /* coordinates of subtitle */
        CONTROL_HEADER[1 + 10] = (byte) ((pic.getXOffset() >> 4) & 0xff);
        tmp = pic.getXOffset() + bm.getWidth() - 1;
        CONTROL_HEADER[1 + 11] = (byte) (((pic.getXOffset() & 0xf) << 4) | ((tmp >> 8) & 0xf));
        CONTROL_HEADER[1 + 12] = (byte) (tmp & 0xff);

        int yOfs = pic.getYOffset() - configuration.getCropOffsetY();
        if (yOfs < 0) {
            yOfs = 0;
        } else {
            int yMax = pic.getHeight() - pic.getImageHeight() - 2 * configuration.getCropOffsetY();
            if (yOfs > yMax) {
                yOfs = yMax;
            }
        }

        CONTROL_HEADER[1 + 13] = (byte) ((yOfs >> 4) & 0xff);
        tmp = yOfs + bm.getHeight() - 1;
        CONTROL_HEADER[1 + 14] = (byte) (((yOfs & 0xf) << 4) | ((tmp >> 8) & 0xf));
        CONTROL_HEADER[1 + 15] = (byte) (tmp & 0xff);

        /* offset to even lines in rle buffer */
        CONTROL_HEADER[1 + 17] = 0x00; /* 2 bytes subpicture size and 2 bytes control header ofs */
        CONTROL_HEADER[1 + 18] = 0x04; /* note: SubtitleCreator uses 6 and adds 0x0000 in between */

        /* offset to odd lines in rle buffer */
        tmp = even.length + CONTROL_HEADER[1 + 18];
        CONTROL_HEADER[1 + 19] = (byte) ((tmp >> 8) & 0xff);
        CONTROL_HEADER[1 + 20] = (byte) (tmp & 0xff);

        /* display duration in frames */
        tmp = (int) ((pic.getEndTime() - pic.getStartTime()) / 1024); // 11.378ms resolution????
        CONTROL_HEADER[1 + 22] = (byte) ((tmp >> 8) & 0xff);
        CONTROL_HEADER[1 + 23] = (byte) (tmp & 0xff);

        /* offset to end sequence - 22 is the offset of the end sequence */
        tmp = sizeRLE + 22 + (pic.isForced() ? 1 : 0) + 4;
        CONTROL_HEADER[forcedOfs + 0] = (byte) ((tmp >> 8) & 0xff);
        CONTROL_HEADER[forcedOfs + 1] = (byte) (tmp & 0xff);
        CONTROL_HEADER[1 + 24] = (byte) ((tmp >> 8) & 0xff);
        CONTROL_HEADER[1 + 25] = (byte) (tmp & 0xff);

        // write control header
        for (int i = 0; i < controlHeaderLen; i++) {
            buf[ofs++] = CONTROL_HEADER[forcedOfs + i];
        }

        return buf;
    }
} public class SupHD implements SubtitleStream {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger logger = Logger.getInstance();

    /** ArrayList of captions contained in the current file  */
    private ArrayList<SubPictureHD> subPictures = new ArrayList<SubPictureHD>();
    /** color palette of the last decoded caption  */
    private Palette palette;
    /** bitmap of the last decoded caption  */
    private Bitmap bitmap;
    /** FileBuffer to read from the file  */
    private FileBuffer buffer;
    /** index of dominant color for the current caption  */
    private int primaryColorIndex;


    public SupHD(String supFile) throws CoreException {
        try {
            buffer = new FileBuffer(supFile);
        } catch (FileBufferException ex) {
            throw new CoreException(ex.getMessage());
        }
        int bufsize = (int)buffer.getSize();

        SubPictureHD pic;
        int index = 0;
        try {
            while (index < bufsize) {
                if (Core.isCanceled()) {
                    throw new CoreException("Canceled by user!");
                }
                Core.setProgress(index);

                if (buffer.getWord(index) != 0x5350) {
                    throw new CoreException("ID 'SP' missing at index " + ToolBox.toHexLeftZeroPadded(index, 8) + "\n");
                }
                int masterIndex = index + 10; //end of header
                pic = new SubPictureHD();
                // hard code size since it's not part of the format???
                pic.setWidth(1920);
                pic.setHeight(1080);
                logger.info("#" + (subPictures.size() + 1) + "\n");
                pic.setStartTime(buffer.getDWordLE(index+=2)); // read PTS
                int packetSize = buffer.getDWord(index+=10);
                // offset to command buffer
                int ofsCmd = buffer.getDWord(index+=4) + masterIndex;
                pic.setImageBufferSize(ofsCmd - (index + 4));
                index  = ofsCmd;
                int dcsq = buffer.getWord(index);
                pic.setStartTime(pic.getStartTime() + (dcsq * 1024));
                logger.info("DCSQ start    ofs: " + ToolBox.toHexLeftZeroPadded(index, 8) + "  (" + ptsToTimeStr(pic.getStartTime()) + ")\n");
                index += 2; // 2 bytes: dcsq
                int nextIndex = buffer.getDWord(index) + masterIndex; // offset to next dcsq
                index += 5;  // 4 bytes: offset, 1 byte: start
                int cmd;
                boolean stopDisplay = false;
                boolean stopCommand = false;
                int alphaSum;
                int minAlphaSum = 256 * 256; // 256 fully transparent entries
                while(!stopDisplay) {
                    cmd = buffer.getByte(index++);
                    switch (cmd) {
                        case 0x01:
                            logger.info("DCSQ start    ofs: " + ToolBox.toHexLeftZeroPadded(index, 8) + "  (" + ptsToTimeStr(pic.getStartTime() + (dcsq * 1024)) + ")\n");
                            logger.warn("DCSQ start ignored due to missing DCSQ stop\n");
                            break;
                        case 0x02:
                            stopDisplay = true;
                            pic.setEndTime(pic.getStartTime() +(dcsq*1024));
                            logger.info("DCSQ stop     ofs: " + ToolBox.toHexLeftZeroPadded(index, 8) + "  (" + ptsToTimeStr(pic.getEndTime()) + ")\n");
                            break;
                        case 0x83: // palette
                            logger.trace("Palette info  ofs: " + ToolBox.toHexLeftZeroPadded(index, 8) + "\n");
                            pic.setPaletteOffset(index);
                            index += 0x300;
                            break;
                        case 0x84: // alpha
                            logger.trace("Alpha info    ofs: " + ToolBox.toHexLeftZeroPadded(index, 8) + "\n");
                            alphaSum = 0;
                            for (int i=index; i < index+0x100; i++) {
                                alphaSum += buffer.getByte(i);
                            }
                            if (alphaSum < minAlphaSum) {
                                pic.setAlphaOffset(index);
                                minAlphaSum = alphaSum;
                            } else {
                                logger.warn("Found faded alpha buffer -> alpha buffer skipped\n");
                            }

                            index += 0x100;
                            break;
                        case 0x85: // area
                            pic.setOfsX((buffer.getByte(index)<<4) | (buffer.getByte(index+1)>>4));
                            pic.setImageWidth((((buffer.getByte(index+1)&0xf)<<8) | (buffer.getByte(index+2))) - pic.getXOffset() + 1);
                            pic.setOfsY((buffer.getByte(index+3)<<4) | (buffer.getByte(index+4)>>4));
                            pic.setImageHeight((((buffer.getByte(index+4)&0xf)<<8) | (buffer.getByte(index+5))) - pic.getYOffset() + 1);
                            logger.trace("Area info     ofs: " + ToolBox.toHexLeftZeroPadded(index, 8) + "  ("
                                    + pic.getXOffset() + ", " + pic.getYOffset() + ") - (" + (pic.getXOffset() + pic.getImageWidth()) + ", "
                                    + (pic.getYOffset() + pic.getImageHeight()) + ")\n");
                            index += 6;
                            break;
                        case 0x86: // even/odd offsets
                            pic.setImageBufferOffsetEven(buffer.getDWord(index) + masterIndex);
                            pic.setImageBufferOffsetOdd(buffer.getDWord(index+4) + masterIndex);
                            logger.trace("RLE buffers   ofs: " + ToolBox.toHexLeftZeroPadded(index, 8)
                                    + "  (even: " + ToolBox.toHexLeftZeroPadded(pic.getImageBufferOffsetEven(), 8)
                                    + ", odd: " + ToolBox.toHexLeftZeroPadded(pic.getImageBufferOffsetOdd(), 8) + "\n");
                            index += 8;
                            break;
                        case 0xff:
                            if (stopCommand) {
                                logger.warn("DCSQ stop missing.\n");
                                for (++index; index < bufsize; index++)
                                    if (buffer.getByte(index++) != 0xff) {
                                        index--;
                                        break;
                                    }
                                stopDisplay = true;
                            } else {
                                index = nextIndex;
                                // add to display time
                                int d = buffer.getWord(index);
                                dcsq = d;
                                nextIndex = buffer.getDWord(index+2) + masterIndex;
                                stopCommand = (index == nextIndex);
                                logger.trace("DCSQ          ofs: " + ToolBox.toHexLeftZeroPadded(index, 8) + "  (" + (d * 1024 / 90)
                                        + "ms),    next DCSQ at ofs: " + ToolBox.toHexLeftZeroPadded(nextIndex, 8) + "\n");
                                index += 6;
                            }
                            break;
                        default:
                            throw new CoreException("Unexpected command " + cmd + " at index " + ToolBox.toHexLeftZeroPadded(index, 8));
                    }
                }
                index = masterIndex + packetSize;
                subPictures.add(pic);
            }
        } catch (CoreException ex) {
            if (subPictures.size() == 0) {
                throw ex;
            }
            logger.error(ex.getMessage() + "\n");
            logger.trace("Probably not all caption imported due to error.\n");
        } catch (FileBufferException ex) {
            if (subPictures.size() == 0) {
                throw new CoreException (ex.getMessage());
            }
            logger.error(ex.getMessage() + "\n");
            logger.trace("Probably not all caption imported due to error.\n");
        }
    }

    public void close() {
        if (buffer != null) {
            buffer.close();
        }
    }

    /**
     * decode one line from the RLE buffer
     * @param trg target buffer for uncompressed data
     * @param trgOfs offset in target buffer
     * @param width image width of encoded caption
     * @param maxPixels maximum number of pixels in caption
     * @param src source buffer
     */
    private static void decodeLine(byte[] trg, int trgOfs, int width, int maxPixels, BitStream src) {
        int x=0;
        int pixelsLeft;
        int sumPixels = 0;
        boolean lf = false;

        while (src.bitsLeft() > 0 && sumPixels<maxPixels) {
            int rleType = src.readBits(1);
            int colorType = src.readBits(1);
            int color;
            int numPixels;

            if (colorType == 1) {
                color = src.readBits(8);
            } else {
                color = src.readBits(2); // Colors between 0 and 3 are stored in two bits
            }

            if (rleType == 1) {
                int rleSize = src.readBits(1);
                if (rleSize == 1) {
                    numPixels = src.readBits(7) + 9;
                    if (numPixels == 9) {
                        numPixels = width - x;
                    }
                } else {
                    numPixels = src.readBits(3) + 2;
                }
            } else
                numPixels = 1;

            if (x+numPixels == width) {
                src.syncToByte();
                lf = true;
            }
            sumPixels += numPixels;

            // write pixels to target
            if (x+numPixels > width) {
                pixelsLeft = x + numPixels - width;
                numPixels = width - x;
                lf = true;
            } else {
                pixelsLeft = 0;
            }

            for (int i=0; i<numPixels; i++) {
                trg[trgOfs+x+i] = (byte)color;
            }

            if (lf) {
                trgOfs += x + numPixels + width; // skip odd/even line
                x = pixelsLeft;
                lf = false;
            } else {
                x += numPixels;
            }

            // copy remaining pixels to new line
            for (int i=0; i < pixelsLeft; i++) {
                trg[trgOfs+i] = (byte)color;
            }
        }
    }

    /**
     * decode caption from the input stream
     * @param pic SubPicture object containing info about the caption
     * @param transIdx index of the transparent color
     * @return bitmap of the decoded caption
     * @throws CoreException
     */
    private Bitmap decodeImage(SubPictureHD pic, int transIdx) throws CoreException {
        int w = pic.getImageWidth();
        int h = pic.getImageHeight();
        int warnings = 0;

        if (w > pic.getWidth() || h > pic.getHeight()) {
            throw new CoreException("Subpicture too large: " + w + "x" + h + " at offset " + ToolBox.toHexLeftZeroPadded(pic.getImageBufferOffsetEven(), 8));
        }

        Bitmap bm = new Bitmap(w, h, (byte)transIdx);

        int sizeEven = pic.getImageBufferOffsetOdd() - pic.getImageBufferOffsetEven();
        int sizeOdd = pic.getImageBufferSize() + pic.getImageBufferOffsetEven() - pic.getImageBufferOffsetOdd();

        if (sizeEven <= 0 || sizeOdd <= 0) {
            throw new CoreException("Corrupt buffer offset information");
        }

        byte evenBuf[] = new byte[sizeEven];
        byte oddBuf[]  = new byte[sizeOdd];

        try {
            // copy buffers
            try {
                for (int i=0; i < evenBuf.length; i++) {
                    evenBuf[i] = (byte)buffer.getByte(pic.getImageBufferOffsetEven() + i);
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                warnings++;
            }
            try {
                for (int i=0; i < oddBuf.length; i++) {
                    oddBuf[i]  = (byte)buffer.getByte(pic.getImageBufferOffsetOdd() +i);
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                warnings++;
            }
            // decode even lines
            try {
                BitStream even = new BitStream(evenBuf);
                decodeLine(bm.getInternalBuffer(), 0, w, w*(h/2+(h&1)), even);
            } catch (ArrayIndexOutOfBoundsException ex) {
                warnings++;
            }
            // decode odd lines
            try {
                BitStream odd  = new BitStream(oddBuf);
                decodeLine(bm.getInternalBuffer(), w, w, (h/2)*w, odd);
            } catch (ArrayIndexOutOfBoundsException ex) {
                warnings++;
            }

            if (warnings > 0) {
                logger.warn("problems during RLE decoding of picture at offset " + ToolBox.toHexLeftZeroPadded(pic.getImageBufferOffsetEven(), 8) + "\n");
            }

            return bm;
        } catch (FileBufferException ex) {
            throw new CoreException (ex.getMessage());
        }
    }

    /**
     * decode palette from the input stream
     * @param pic SubPicture object containing info about the caption
     * @return decoded palette
     * @throws CoreException
     */
    private Palette decodePalette(final SubPictureHD pic) throws CoreException {
        int ofs = pic.getPaletteOffset();
        int alphaOfs = pic.getAlphaOffset();

        Palette palette = new Palette(256);
        try {
            for (int i=0; i < palette.getSize(); i++) {
                // each palette entry consists of 3 bytes
                int y = buffer.getByte(ofs++);
                int cr,cb;
                if (configuration.isSwapCrCb()) {
                    cb = buffer.getByte(ofs++);
                    cr = buffer.getByte(ofs++);
                } else {
                    cr = buffer.getByte(ofs++);
                    cb = buffer.getByte(ofs++);
                }
                // each alpha entry consists of 1 byte
                int alpha = 0xff - buffer.getByte(alphaOfs++);
                if (alpha < configuration.getAlphaCrop()) { // to not mess with scaling algorithms, make transparent color black
                    palette.setRGB(i, 0, 0, 0);
                } else {
                    palette.setYCbCr(i, y, cb, cr);
                }
                palette.setAlpha(i, alpha);
            }
            return palette;
        } catch (FileBufferException ex) {
            throw new CoreException (ex.getMessage());
        }
    }

    /**
     * decode given picture
     * @param pic SubPicture object containing info about caption
     * @throws CoreException
     */
    private void decode(SubPictureHD pic) throws CoreException {
        palette = decodePalette(pic);
        bitmap  = decodeImage(pic, palette.getIndexOfMostTransparentPaletteEntry());
        primaryColorIndex = bitmap.getPrimaryColorIndex(palette.getAlpha(), configuration.getAlphaThreshold(), palette.getY());
    }

    public void decode(int index) throws CoreException {
        if (index < subPictures.size()) {
            decode(subPictures.get(index));
        } else {
            throw new CoreException("Index " + index + " out of bounds\n");
        }
    }

    public Palette getPalette() {
        return palette;
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public BufferedImage getImage() {
        return bitmap.getImage(palette.getColorModel());
    }

    public BufferedImage getImage(Bitmap bm) {
        return bm.getImage(palette.getColorModel());
    }

    public int getPrimaryColorIndex() {
        return primaryColorIndex;
    }

    public SubPicture getSubPicture(int index) {
        return subPictures.get(index);
    }

    public int getFrameCount() {
        return subPictures.size();
    }

    public int getForcedFrameCount() {
        return 0;
    }

    public boolean isForced(int index) {
        return false;
    }

    public long getEndTime(int index) {
        return subPictures.get(index).getEndTime();
    }

    public long getStartTime(int index) {
        return subPictures.get(index).getStartTime();
    }

    public long getStartOffset(int index) {
        return subPictures.get(index).getImageBufferOffsetEven();
    }
} public class EnhancedPngEncoder extends PngEncoder {

    /** PLTE tag. */
    private static final byte PLTE[] = { 80, 76, 84, 69 };

    /** tRNS tag. */
    private static final byte TRNS[] = {0x74, 0x52, 0x4e, 0x53 };

    private BufferedImage image;
    private WritableRaster wRaster;
    private int tType;

    public EnhancedPngEncoder() {
        this(null, false, FILTER_NONE, 0);
    }

    /**
     * Class constructor specifying BufferedImage to encode, with no alpha channel encoding.
     *
     * @param image A Java BufferedImage object
     */
    public EnhancedPngEncoder(BufferedImage image) {
        this(image, false, FILTER_NONE, 0);
    }

    /**
     * Class constructor specifying BufferedImage to encode, and whether to encode alpha.
     *
     * @param image A Java BufferedImage object
     * @param encodeAlpha Encode the alpha channel? false=no; true=yes
     */
    public EnhancedPngEncoder(BufferedImage image, boolean encodeAlpha) {
        this(image, encodeAlpha, FILTER_NONE, 0);
    }

    /**
     * Class constructor specifying BufferedImage to encode, whether to encode alpha, and filter to use.
     *
     * @param image A Java BufferedImage object
     * @param encodeAlpha Encode the alpha channel? false=no; true=yes
     * @param whichFilter 0=none, 1=sub, 2=up
     */
    public EnhancedPngEncoder(BufferedImage image, boolean encodeAlpha, int whichFilter) {
        this(image, encodeAlpha, whichFilter, 0);
    }

    /**
     * Class constructor specifying BufferedImage source to encode, whether to encode alpha,
     * filter to use, and compression level.
     *
     * @param image A Java BufferedImage object
     * @param encodeAlpha Encode the alpha channel? false=no; true=yes
     * @param whichFilter 0=none, 1=sub, 2=up
     * @param compLevel 0..9
     */
    private EnhancedPngEncoder(BufferedImage image, boolean encodeAlpha, int whichFilter, int compLevel) {
        this.image = image;
        this.encodeAlpha = encodeAlpha;
        setFilter(whichFilter);
        if (compLevel >=0 && compLevel <= 9) {
            this.compressionLevel = compLevel;
        }
    }

    /**
     * Set the BufferedImage to be encoded.
     * @param image A Java BufferedImage object
     */
    public void setImage(BufferedImage image) {
        this.image = image;
        pngBytes = null;
    }

    /**
     * Creates an array of bytes that is the PNG equivalent of the current image.
     *
     * @return an array of bytes, or null if there was a problem
     */
    @Override
    public byte[] pngEncode() {
        byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 };

        if (image == null) {
            System.err.println("pngEncode: image is null; returning null");
            return null;
        }
        width = image.getWidth(null);
        height = image.getHeight(null);

        if (!establishStorageInfo()) {
            System.err.println("pngEncode: cannot establish storage info");
            return null;
        }

        /* start with an array that is big enough to hold all the pixels
         * (plus filter bytes), and an extra 200 bytes for header info
         */
        pngBytes = new byte[((width+1) * height * 3) + 200];

        /* keep track of largest byte written to the array */
        maxPos = 0;

        bytePos = writeBytes(pngIdBytes, 0);
        // hdrPos = bytePos;
        writeHeader();
        // dataPos = bytePos;
        if (writeImageData()) {
            writeEnd();
            pngBytes = resizeByteArray(pngBytes, maxPos);
        } else {
            System.err.println("pngEncode: writeImageData failed => null");
            pngBytes = null;
        }
        return pngBytes;
    }

    /**
     * Get and set variables that determine how picture is stored.
     *
     * Retrieves the writable raster of the buffered image,
     * as well its transfer type.
     *
     * Sets number of output bytes per pixel, and, if only
     * eight-bit bytes, turns off alpha encoding.
     * @return true if 1-byte or 4-byte data, false otherwise
     */
    boolean establishStorageInfo() {
        int dataBytes;

        wRaster = image.getRaster();
        dataBytes = wRaster.getNumDataElements();
        tType = wRaster.getTransferType();

        if (((tType == DataBuffer.TYPE_BYTE) && (dataBytes == 4)) ||
            ((tType == DataBuffer.TYPE_INT) && (dataBytes == 1)) ||
            // on Win 2k/ME, tType == 1, dataBytes == 1
            ((tType == DataBuffer.TYPE_USHORT) && (dataBytes == 1))) {
            bytesPerPixel = (encodeAlpha) ? 4 : 3;
        } else if ((tType == DataBuffer.TYPE_BYTE) && (dataBytes == 1)) {
            bytesPerPixel = 1;
            encodeAlpha = false;    // one-byte samples
        } else {
            System.err.println("PNG encoder cannot establish storage info:");
            System.err.println("  TransferType == " + tType);
            System.err.println("  NumDataElements == " + dataBytes);
            return false;
        }
        return true;
    }

    /**
     * Write a PNG "IHDR" chunk into the pngBytes array.
     */
    @Override
    protected void writeHeader() {
        int startPos;

        startPos = bytePos = writeInt4(13, bytePos);
        bytePos = writeBytes(IHDR, bytePos);
        width = image.getWidth(null);
        height = image.getHeight(null);
        bytePos = writeInt4(width, bytePos);
        bytePos = writeInt4(height, bytePos);
        bytePos = writeByte(8, bytePos); // bit depth
        if (bytesPerPixel != 1) {
            bytePos = writeByte((encodeAlpha) ? 6 : 2, bytePos); // direct model
        } else {
            bytePos = writeByte(3, bytePos); // indexed
        }
        bytePos = writeByte(0, bytePos); // compression method
        bytePos = writeByte(0, bytePos); // filter method
        bytePos = writeByte(0, bytePos); // no interlace
        crc.reset();
        crc.update(pngBytes, startPos, bytePos-startPos);
        crcValue = crc.getValue();
        bytePos = writeInt4((int) crcValue, bytePos);
    }

    void writePalette(IndexColorModel icm) {
        byte[] redPal = new byte[256];
        byte[] greenPal = new byte[256];
        byte[] bluePal = new byte[256];
        byte[] allPal = new byte[768];
        int i;

        icm.getReds(redPal);
        icm.getGreens(greenPal);
        icm.getBlues(bluePal);
        for (i=0; i<256; i++) {
            allPal[i*3  ] = redPal[i];
            allPal[i*3+1] = greenPal[i];
            allPal[i*3+2] = bluePal[i];
        }
        bytePos = writeInt4(768, bytePos);
        bytePos = writeBytes(PLTE, bytePos);
        crc.reset();
        crc.update(PLTE);
        bytePos = writeBytes(allPal, bytePos);
        crc.update(allPal);
        crcValue = crc.getValue();
        bytePos = writeInt4((int) crcValue, bytePos);
    }

    void writeAlpha(IndexColorModel icm) {
        byte[] alpha = new byte[256];
        icm.getAlphas(alpha);
        bytePos = writeInt4(256, bytePos);
        bytePos = writeBytes(TRNS, bytePos);
        crc.reset();
        crc.update(TRNS);
        bytePos = writeBytes(alpha, bytePos);
        crc.update(alpha);
        crcValue = crc.getValue();
        bytePos = writeInt4((int) crcValue, bytePos);
    }

    /**
     * Write the image data into the pngBytes array.
     * This will write one or more PNG "IDAT" chunks. In order
     * to conserve memory, this method grabs as many rows as will
     * fit into 32K bytes, or the whole image; whichever is less.
     *
     *
     * @return true if no errors; false if error grabbing pixels
     */
    @Override
    protected boolean writeImageData() {
        int rowsLeft = height;  // number of rows remaining to write
        int startRow = 0;       // starting row to process this time through
        int nRows;              // how many rows to grab at a time

        byte[] scanLines;       // the scan lines to be compressed
        int scanPos;            // where we are in the scan lines
        int startPos;           // where this line's actual pixels start (used for filtering)
        int readPos;            // position from which source pixels are read

        byte[] compressedLines; // the resultant compressed lines
        int nCompressed;        // how big is the compressed area?

        byte[] pixels;          // storage area for byte-sized pixels
        int[] iPixels;          // storage area for int-sized pixels
        short[] sPixels;		// for Win 2000/ME ushort pixels
        final int type = image.getType();
        // TYPE_INT_RGB        = 1
        // TYPE_INT_ARGB       = 2
        // TYPE_INT_ARGB_PRE   = 3
        // TYPE_INT_BGR        = 4
        // TYPE_3BYTE_BGR      = 5
        // TYPE_4BYTE_ABGR     = 6
        // TYPE_4BYTE_ABGR_PRE = 7
        // TYPE_BYTE_GRAY      = 10
        // TYPE_BYTE_BINARY    = 12
        // TYPE_BYTE_INDEXED   = 13
        // TYPE_USHORT_GRAY    = 11
        // TYPE_USHORT_565_RGB = 8
        // TYPE_USHORT_555_RGB = 9
        // TYPE_CUSTOM         = 0.

        Deflater scrunch = new Deflater(compressionLevel);
        ByteArrayOutputStream outBytes =
            new ByteArrayOutputStream(1024);

        DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes, scrunch);

        if (bytesPerPixel == 1) {
            writePalette((IndexColorModel) image.getColorModel());
            writeAlpha((IndexColorModel) image.getColorModel());
        }

        try {
            while (rowsLeft > 0) {
                nRows = Math.min(32767 / (width*(bytesPerPixel+1)), rowsLeft);
                nRows = Math.max(nRows, 1);

                /*
                 * Create a data chunk. scanLines adds "nRows" for
                 * the filter bytes.
                 */
                scanLines = new byte[width * nRows * bytesPerPixel +  nRows];

                if (filter == FILTER_SUB) {
                    leftBytes = new byte[16];
                }
                if (filter == FILTER_UP) {
                    priorRow = new byte[width*bytesPerPixel];
                }

                final Object data =
                    wRaster.getDataElements(0, startRow, width, nRows, null);

                pixels = null;
                iPixels = null;
                sPixels = null;
                if (tType == DataBuffer.TYPE_BYTE) {
                    pixels = (byte[]) data;
                } else if (tType == DataBuffer.TYPE_INT) {
                    iPixels = (int[]) data;
                } else if (tType == DataBuffer.TYPE_USHORT) {
                    sPixels = (short[]) data;
                }

                scanPos = 0;
                readPos = 0;
                startPos = 1;
                for (int i=0; i<width*nRows; i++) {
                    if (i % width == 0) {
                        scanLines[scanPos++] = (byte) filter;
                        startPos = scanPos;
                    }

                    if (bytesPerPixel == 1) { // assume TYPE_BYTE, indexed
                        scanLines[scanPos++] = pixels[readPos++];
                    } else if (tType == DataBuffer.TYPE_BYTE) {
                        scanLines[scanPos++] = pixels[readPos++];
                        scanLines[scanPos++] = pixels[readPos++];
                        scanLines[scanPos++] = pixels[readPos++];
                        if (encodeAlpha) {
                            scanLines[scanPos++] = pixels[readPos++];
                        } else {
                            readPos++;
                        }
                    } else if (tType == DataBuffer.TYPE_USHORT) {
                        short pxl = sPixels[readPos++];
                        if (type == BufferedImage.TYPE_USHORT_565_RGB) {
                            scanLines[scanPos++] = (byte) ((pxl >> 8) & 0xf8);
                            scanLines[scanPos++] = (byte) ((pxl >> 2) & 0xfc);
                        } else {                // assume USHORT_555_RGB
                            scanLines[scanPos++] = (byte) ((pxl >> 7) & 0xf8);
                            scanLines[scanPos++] = (byte) ((pxl >> 2) & 0xf8);
                        }
                        scanLines[scanPos++] = (byte) ((pxl << 3) & 0xf8);
                    } else {      // assume tType INT and type RGB or ARGB
                        int pxl = iPixels[readPos++];
                        scanLines[scanPos++] = (byte) ((pxl >> 16) & 0xff);
                        scanLines[scanPos++] = (byte) ((pxl >>  8) & 0xff);
                        scanLines[scanPos++] = (byte) ((pxl      ) & 0xff);
                        if (encodeAlpha) {
                            scanLines[scanPos++] = (byte) ((pxl >> 24) & 0xff);
                        }
                    }

                    if ((i % width == width-1) && (filter != FILTER_NONE)) {
                        if (filter == FILTER_SUB) {
                            filterSub(scanLines, startPos, width);
                        }
                        if (filter == FILTER_UP) {
                            filterUp(scanLines, startPos, width);
                        }
                    }
                }

                /*
                 * Write these lines to the output area
                 */
                compBytes.write(scanLines, 0, scanPos);

                startRow += nRows;
                rowsLeft -= nRows;
            }
            compBytes.close();

            /*
             * Write the compressed bytes
             */
            compressedLines = outBytes.toByteArray();
            nCompressed = compressedLines.length;

            crc.reset();
            bytePos = writeInt4(nCompressed, bytePos);
            bytePos = writeBytes(IDAT, bytePos);
            crc.update(IDAT);
            bytePos = writeBytes(compressedLines, nCompressed, bytePos);
            crc.update(compressedLines, 0, nCompressed);

            crcValue = crc.getValue();
            bytePos = writeInt4((int) crcValue, bytePos);
            scrunch.finish();
            return true;
        } catch (IOException e) {
            System.err.println(e.toString());
            return false;
        }
    }
} public class FilenameUtils {

    private static final char UNIX_SEPARATOR = '/';
    private static final char WINDOWS_SEPARATOR = '\\';

    private static final char SEPARATOR_CHAR = File.separatorChar;
    private static final char EXTENSION_SEPARATOR_CHAR = '.';

    public static String getExtension(String filename) {
        if (filename == null) {
            return null;
        }
        int index = indexOfExtension(filename);
        if (index == -1) {
            return "";
        } else {
            return filename.substring(index + 1);
        }
    }

    public static String removeExtension(String filename) {
        if (filename == null) {
            return null;
        }
        int index = indexOfExtension(filename);
        if (index == -1) {
            return filename;
        } else {
            return filename.substring(0, index);
        }
    }

    public static String separatorsToUnix(String path) {
        if (path == null || path.indexOf(WINDOWS_SEPARATOR) == -1) {
            return path;
        }
        return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR);
    }
    
    public static String addSeparator(String path) {
        if (path == null || path.lastIndexOf(SEPARATOR_CHAR) == path.length()-1) {
            return path;
        }
        return path + File.separatorChar;
    }

    public static String getName(String filename) {
        return new File(filename).getName();
    }

    public static String getParent(String filename) {
        return new File(filename).getParent();
    }

    private static int indexOfExtension(String filename) {
        if (filename == null) {
            return -1;
        }
        int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR_CHAR);
        int lastSeparator = indexOfLastSeparator(filename);
        return (lastSeparator > extensionPos ? -1 : extensionPos);
    }

    private static int indexOfLastSeparator(String filename) {
        if (filename == null) {
            return -1;
        }
        return filename.lastIndexOf(SEPARATOR_CHAR);
    }
} public final class SubtitleUtils {

    private SubtitleUtils() {
    }

    public static double getFps(String fps) {
        fps = fps.toLowerCase().trim();
        if (fps.equals("pal")  || fps.equals("25p") || fps.equals("25")) {
            return Framerate.PAL.getValue();
        }
        if (fps.equals("ntsc") || fps.equals("30p") || fps.equals("29.97") || fps.equals("29.970")) {
            return Framerate.NTSC.getValue();
        }
        if (fps.equals("24p")  || fps.equals("23.976")) {
            return Framerate.FPS_23_976.getValue();
        }
        if (fps.equals("23.975")) {
            return Framerate.FPS_23_975.getValue();
        }
        if (fps.equals("24")) {
            return Framerate.FPS_24.getValue();
        }
        if (fps.equals("50i")  || fps.equals("50")) {
            return Framerate.PAL_I.getValue();
        }
        if (fps.equals("60i")  || fps.equals("59.94")) {
            return Framerate.NTSC_I.getValue();
        }

        double d;
        try {
            d = Double.parseDouble(fps);
        } catch (NumberFormatException ex) {
            return -1.0;
        }
        if (Math.abs(d - Framerate.FPS_23_975.getValue()) < 0.001) {
            return Framerate.FPS_23_975.getValue();
        }
        if (Math.abs(d - Framerate.FPS_23_976.getValue()) < 0.001) {
            return Framerate.FPS_23_976.getValue();
        }
        if (Math.abs(d - Framerate.FPS_24.getValue()) < 0.001) {
            return Framerate.FPS_24.getValue();
        }
        if (Math.abs(d - Framerate.PAL.getValue()) < 0.001) {
            return Framerate.PAL.getValue();
        }
        if (Math.abs(d - Framerate.NTSC.getValue()) < 0.001) {
            return Framerate.NTSC.getValue();
        }
        if (Math.abs(d - Framerate.NTSC_I.getValue()) < 0.001) {
            return Framerate.NTSC_I.getValue();
        }
        if (Math.abs(d - Framerate.PAL_I.getValue()) < 0.001) {
            return Framerate.PAL_I.getValue();
        }
        return d;
    }

    /**
     * Synchronizes a time stamp in 90kHz resolution to the given frame rate.
     */
    public static long syncTimePTS(long timeStamp, double fps, double fpsTrg) {
        long retval;
        // correct time stamps to fit to frames
        if (fps == Framerate.NTSC.getValue() || fps == Framerate.PAL.getValue() || fps == Framerate.FPS_24.getValue()) {
            // NTSC: 90000/(30000/1001) = 3003
            // PAL:  90000/25 = 3600
            // 24Hz: 90000/24 = 3750
            int tpfi = (int)((90000+(fps/2))/fps); // target time per frame in 90kHz
            int tpfh = tpfi/2;
            retval = ((timeStamp + tpfh)/tpfi)*tpfi;
        } else if (fpsTrg == Framerate.FPS_23_976.getValue()) {
            // 90000/(24000/1001) = 3753.75 = 15015/4
            retval = ((((timeStamp + 1877)*4)/15015)*15015)/4;
        } else {
            double tpf = (90000/fpsTrg); // target time per frame in 90kHz
            retval = (long)((long)(timeStamp/tpf)*tpf+0.5);
        }
        return retval;
    }

    public static double getDefaultFramerateForResolution(Resolution resolution) {
        double fps;
        switch (resolution.getDimensions()[1]) {
            case 480:
                fps = Framerate.NTSC.getValue();
                break;
            case 576:
                fps = Framerate.PAL.getValue();
                break;
            default:
                fps = Framerate.FPS_23_976.getValue();
        }
        return fps;
    }

    public static Resolution getResolutionForDimension(int width, int height) {
        if (width <= Resolution.NTSC.getDimensions()[0] && height <= Resolution.NTSC.getDimensions()[1]) {
            return Resolution.NTSC;
        } else if (width <= Resolution.PAL.getDimensions()[0] && height <= Resolution.PAL.getDimensions()[1]) {
            return Resolution.PAL;
        } else if (width <= Resolution.HD_720.getDimensions()[0] && height <= Resolution.HD_720.getDimensions()[1]) {
            return Resolution.HD_720;
        } else if (width <= Resolution.HD_1440x1080.getDimensions()[0] && height <= Resolution.HD_1440x1080.getDimensions()[1]) {
            return Resolution.HD_1440x1080;
        } else {
            return Resolution.HD_1080;
        }
    }
} public final class ToolBox {

    private static final DecimalFormat FPS_FORMATTER = new DecimalFormat("##.###", DecimalFormatSymbols.getInstance(Locale.US));

    public static String leftZeroPad(int value, int width) {
        return String.format("%0" + width + "d", value);
    }

    public static String toHexLeftZeroPadded(long value, int width) {
        return String.format("0x%0" + width + "x", value);
    }

    public static String formatDouble(double value) {
        return FPS_FORMATTER.format(value);
    }

    /**
     * Write ASCII string to buffer[index] (no special handling for multi-byte characters)
     * @param buffer Byte array
     * @param index Index to write to
     * @param s String containing ASCII characters
     * @throws ArrayIndexOutOfBoundsException
     */
    public static void setString(byte buffer[], int index, String s) {
        for (int i =0; i<s.length(); i++) {
            buffer[index+i] = (byte)s.charAt(i);
        }
    }

    /**
     * Show a dialog with details about an exception
     * @param ex Throwable/Exception to display
     */
    public static void showException(Throwable ex) {
        String m;
        m = "<html>";
        m += ex.getClass().getName() + "<p>";
        if (ex.getMessage() != null) {
            m += ex.getMessage() + "<p>";
        }
        StackTraceElement ste[] = ex.getStackTrace();
        for (StackTraceElement e : ste) {
            m += e.toString() + "<p>";
        }
        m += "</html>";
        ex.printStackTrace();
        JOptionPane.showMessageDialog(null, m, "Error", JOptionPane.ERROR_MESSAGE);
    }

    /**
     * Get file name via "file chooser" dialog
     * @param path   Default path (without file name).
     * @param filename     Default file name (without path).
     * @param extensions    Array of allowed extensions (without ".")
     * @param loadDialog   If true, this is a load dialog, else it's a save dialog
     * @param parent Parent component (Frame, Window)
     * @return       Selected filename or null if canceled
     */
    public static String getFilename(String path, String filename, List<String> extensions, boolean loadDialog, Component parent) {
        if (path == null || path.isEmpty()) {
            path = ".";
        }
        JFileChooser fileChooser = new JFileChooser(path);
        if (extensions != null) {
            JFileFilter fileFilter = new JFileFilter();
            for (String extension : extensions) {
                fileFilter.addExtension(extension);
            }
            fileChooser.setFileFilter(fileFilter);
        }
        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        File file;
        if (filename != null && !filename.isEmpty()) {
            file = new File(FilenameUtils.addSeparator(path) + filename);
            if (file.canRead()) {
                fileChooser.setSelectedFile(file);
            }
        }
        if (!loadDialog) {
            fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
        }
        int returnVal = fileChooser.showDialog(parent, null);
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            file = fileChooser.getSelectedFile();
            if (file != null) {
                return file.getAbsolutePath();
            }
        }
        return null;
    }

    /**
     * Returns the first few bytes of a file to check it's type
     * @param fname Filename of the file
     * @param num Number of bytes to return
     * @return Array of bytes (size num) from the beginning of the file
     */
    public static byte[] getFileID(String fname, int num) {
        byte buf[] = new byte[num];
        File f = new File(fname);
        if (f.length() < num) {
            return null;
        }
        try {
            FileInputStream fi = new FileInputStream(fname);
            fi.read(buf);
            fi.close();
        } catch (Exception ex) {
            return null;
        }
        return buf;
    }

    /**
     * Convert String to integer
     * @param s String containing integer (assumed: positive)
     * @return Integer value or -1.0 if no valid numerical value
     */
    public static int getInt(String s) {
        try {
            return Integer.parseInt(s.trim());
        } catch (NumberFormatException ex) {
            return -1;
        }
    }

    /**
     * Convert String to double
     * @param s String containing double
     * @return Double value or -1.0 if no valid numerical value
     */
    public static double getDouble(String s) {
        try {
            return Double.parseDouble(s.trim());
        } catch (NumberFormatException ex) {
            return -1.0;
        }
    }
}
import static bdsup2sub.core.Constants.*;
import static bdsup2sub.utils.SubtitleUtils.*;
import static bdsup2sub.utils.TimeUtils.*;
import static com.mortennobel.imagescaling.ResampleFilters.*;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.swing.*;
import bdsup2sub.bitmap.Bitmap;
import bdsup2sub.bitmap.BitmapWithPalette;
import bdsup2sub.bitmap.ErasePatch;
import bdsup2sub.bitmap.Palette;
import bdsup2sub.gui.support.Progress;
import bdsup2sub.supstream.SubPicture;
import bdsup2sub.supstream.SubtitleStream;
import bdsup2sub.supstream.bd.SupBD;
import bdsup2sub.supstream.bd.SupBDWriter;
import bdsup2sub.supstream.bdnxml.SupXml;
import bdsup2sub.supstream.dvd.DvdSubtitleStream;
import bdsup2sub.supstream.dvd.IfoWriter;
import bdsup2sub.supstream.dvd.SubDvd;
import bdsup2sub.supstream.dvd.SubDvdWriter;
import bdsup2sub.supstream.dvd.SubPictureDVD;
import bdsup2sub.supstream.dvd.SupDvd;
import bdsup2sub.supstream.dvd.SupDvdUtil;
import bdsup2sub.supstream.dvd.SupDvdWriter;
import bdsup2sub.supstream.hd.SupHD;
import bdsup2sub.tools.EnhancedPngEncoder;
import bdsup2sub.utils.FilenameUtils;
import bdsup2sub.utils.SubtitleUtils;
import bdsup2sub.utils.ToolBox;
import com.mortennobel.imagescaling.ResampleFilter;
                    if ((inMode == InputMode.VOBSUB || inMode == InputMode.SUPIFO) && paletteMode == PaletteMode.KEEP_EXISTING) {
                        // keep palette
                        if (f != null) {
                            tBm = subtitleStream.getBitmap().scaleFilter(trgWidth, trgHeight, subtitleStream.getPalette(), f);
                        } else {
                            tBm = subtitleStream.getBitmap().scaleBilinear(trgWidth, trgHeight, subtitleStream.getPalette());
                        }
                    } else {
                        // reduce palette
                        if (f != null) {
                            tBm = subtitleStream.getBitmap().scaleFilterLm(trgWidth, trgHeight, subtitleStream.getPalette(), configuration.getAlphaThreshold(), configuration.getLuminanceThreshold(), f);
                        } else {
                            tBm = subtitleStream.getBitmap().scaleBilinearLm(trgWidth, trgHeight, subtitleStream.getPalette(), configuration.getAlphaThreshold(), configuration.getLuminanceThreshold());
                        }
                    }
                }
            } else {
                // export (up to) 256 color palette
                tPal = subtitleStream.getPalette();
                if (w==trgWidth && h==trgHeight) {
                    tBm = subtitleStream.getBitmap(); // no scaling, no conversion
                } else {
                    // scale up/down
                    if (paletteMode == PaletteMode.KEEP_EXISTING) {
                        // keep palette
                        if (f != null) {
                            tBm = subtitleStream.getBitmap().scaleFilter(trgWidth, trgHeight, subtitleStream.getPalette(), f);
                        } else {
                            tBm = subtitleStream.getBitmap().scaleBilinear(trgWidth, trgHeight, subtitleStream.getPalette());
                        }
                    } else {
                        // create new palette
                        boolean dither = paletteMode == PaletteMode.CREATE_DITHERED;
                        BitmapWithPalette pb;
                        if (f != null) {
                            pb = subtitleStream.getBitmap().scaleFilter(trgWidth, trgHeight, subtitleStream.getPalette(), f, dither);
                        } else {
                            pb = subtitleStream.getBitmap().scaleBilinear(trgWidth, trgHeight, subtitleStream.getPalette(), dither);
                        }
                        tBm = pb.bitmap;
                        tPal = pb.palette;
                    }
                }
            }
            if (!picTrg.getErasePatch().isEmpty()) {
                trgBitmapUnpatched = new Bitmap(tBm);
                int col = tPal.getIndexOfMostTransparentPaletteEntry();
                for (ErasePatch ep : picTrg.getErasePatch()) {
                    tBm.fillRectangularWithColorIndex(ep.x, ep.y, ep.width, ep.height, (byte)col);
                }
            } else {
                trgBitmapUnpatched = tBm;
            }
            trgBitmap = tBm;
            trgPal = tPal;

        }

        if (configuration.isCliMode()) {
            moveToBounds(picTrg, displayNum, configuration.getCineBarFactor(), configuration.getMoveOffsetX(), configuration.getMoveOffsetY(), configuration.getMoveModeX(), configuration.getMoveModeY(), configuration.getCropOffsetY());
        }
    }

    /**
     * Create BD-SUP or VobSub or Xml.
     * @param fname File name of SUP/SUB/XML to create
     * @throws CoreException
     */
    public static void writeSub(String fname) throws CoreException {
        BufferedOutputStream out = null;
        List<Integer> offsets = null;
        List<Integer> timestamps = null;
        SortedMap<Integer, SubPicture> exportedSubPictures = new TreeMap<Integer, SubPicture>();
        int frameNum = 0;
        String fn = "";
        logger.resetErrorCounter();
        logger.resetWarningCounter();

        List<Integer> subPicturesToBeExported = getSubPicturesToBeExported();

        if (subPicturesToBeExported.isEmpty()) {
            logger.warn("There is no subpicture to be exported.");
            return;
        }

        OutputMode outputMode = configuration.getOutputMode();
        try {
            // handle file name extensions depending on mode
            if (outputMode == OutputMode.VOBSUB) {
                fname = FilenameUtils.removeExtension(fname) + ".sub";
                out = new BufferedOutputStream(new FileOutputStream(fname));
                offsets = new ArrayList<Integer>();
                timestamps = new ArrayList<Integer>();
            } else if (outputMode == OutputMode.SUPIFO) {
                fname = FilenameUtils.removeExtension(fname) + ".sup";
                out = new BufferedOutputStream(new FileOutputStream(fname));
            } else if (outputMode == OutputMode.BDSUP) {
                fname = FilenameUtils.removeExtension(fname) + ".sup";
                out = new BufferedOutputStream(new FileOutputStream(fname));
            } else {
                fn = FilenameUtils.removeExtension(fname);
                fname = fn + ".xml";
            }
            logger.info("\nWriting " + fname + "\n");

            // main loop
            int offset = 0;
            for (int i : subPicturesToBeExported) {
                // for threaded version
                if (isCanceled()) {
                    throw new CoreException("Canceled by user!");
                }
                // for threaded version (progress bar);
                setProgress(i);

                SubPicture subPicture = subPictures[i];
                if (outputMode == OutputMode.VOBSUB) {
                    offsets.add(offset);
                    convertSup(i, frameNum/2+1, subPicturesToBeExported.size());
                    subVobTrg.copyInfo(subPicture);
                    byte buf[] = SubDvdWriter.createSubFrame(subVobTrg, trgBitmap);