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 BitmapBounds {

    /** X coordinate of first column that contains visible pixels. */
    public final int xMin;
    /** X coordinate of last column that contains visible pixels. */
    public final int xMax;
    /** Y coordinate of first line that contains visible pixels. */
    public final int yMin;
    /** Y coordinate of last line that contains visible pixels. */
    public final int yMax;

    /**
     * @param xMin x coordinate of first column that contains visible pixels
     * @param xMax x coordinate of last column that contains visible pixels
     * @param yMin y coordinate of first line that contains visible pixels
     * @param yMax y coordinate of last line that contains visible pixels
     */
    public BitmapBounds(int xMin, int xMax, int yMin, int yMax) {
        this.xMin = xMin;
        this.xMax = xMax;
        this.yMin = yMin;
        this.yMax = yMax;
    }
} 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 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 QuantizeFilter  {

    /**
     * Floyd-Steinberg dithering matrix.
     */
    private static final int[] FS_MATRIX = {
         0, 0, 0,
         0, 0, 7,
         3, 5, 1,
    };
    private static final int SUM = 3 + 5 + 7 + 1;

    private boolean dither;
    private int numColors = 255;
    private boolean serpentine = true;

    /**
     * Set the number of colors to quantize to.
     * @param numColors Number of colors. The default is 256.
     */
    public void setNumColors(int numColors) {
        this.numColors = Math.min(Math.max(numColors, 8), 256);
    }

    /**
     * Clamp a value to the range 0..255
     * @param c Value to clamp
     * @return Clamped value
     */
    private static int clamp(final int c) {
        if (c < 0) {
            return 0;
        }
        if (c > 255) {
            return 255;
        }
        return c;
    }

    /**
     * Get the number of colors to quantize to.
     * @return Number of colors.
     */
    public int getNumColors() {
        return numColors;
    }

    /**
     * Set whether to use dithering or not. If not, the image is posterized.
     * @param dither True to use dithering
     */
    public void setDither(boolean dither) {
        this.dither = dither;
    }

    /**
     * Return the dithering setting
     * @return Current dithering setting
     */
    public boolean getDither() {
        return dither;
    }

    /**
     * Set whether to use a serpentine pattern for return or not. This can reduce 'avalanche' artifacts in the output.
     * @param serpentine True to use serpentine pattern
     */
    public void setSerpentine(boolean serpentine) {
        this.serpentine = serpentine;
    }

    /**
     * Return the serpentine setting
     * @return Current serpentine setting
     */
    public boolean getSerpentine() {
        return serpentine;
    }

    /**
     * Quantize picture
     * @param inPixels Array of RGBA pixels to quantize
     * @param outPixels Array with quantized palette entries
     * @param width Width of image
     * @param height Height of image
     * @param numColors Number of colors used
     * @param dither Use dithering?
     * @param serpentine Use serpentine for dithering?
     * @return Integer array containing palette information
     */
    public int[] quantize(int[] inPixels, byte[] outPixels, int width, int height, int numColors, boolean dither, boolean serpentine) {
        int count = width * height;
        OctTreeQuantizer quantizer = new OctTreeQuantizer();
        quantizer.setup(numColors);
        quantizer.addPixels(inPixels, 0, count);
        int[] table =  quantizer.buildColorTable();

        if (dither) {
            int index;
            for (int y = 0; y < height; y++) {
                boolean reverse = serpentine && (y & 1) == 1;
                int direction;
                if (reverse) {
                    index = y * width + width - 1;
                    direction = -1;
                } else {
                    index = y * width;
                    direction = 1;
                }
                for (int x = 0; x < width; x++) {
                    int rgb1 = inPixels[index];
                    int idx = quantizer.getIndexForColor(rgb1);
                    int rgb2 = table[idx];

                    outPixels[index] = (byte)(idx&0xff);

                    int a1 = (rgb1 >> 24) & 0xff;
                    int r1 = (rgb1 >> 16) & 0xff;
                    int g1 = (rgb1 >> 8) & 0xff;
                    int b1 = rgb1 & 0xff;

                    int a2 = (rgb2 >> 24) & 0xff;
                    int r2 = (rgb2 >> 16) & 0xff;
                    int g2 = (rgb2 >> 8) & 0xff;
                    int b2 = rgb2 & 0xff;

                    int ea = a1-a2;
                    int er = r1-r2;
                    int eg = g1-g2;
                    int eb = b1-b2;

                    for (int i = -1; i <= 1; i++) {
                        int iy = i+y;
                        if (0 <= iy && iy < height) {
                            for (int j = -1; j <= 1; j++) {
                                int jx = j+x;
                                if (0 <= jx && jx < width) {
                                    int w;
                                    if (reverse)
                                        w = FS_MATRIX[(i+1)*3-j+1];
                                    else
                                        w = FS_MATRIX[(i+1)*3+j+1];
                                    if (w != 0) {
                                        int k = reverse ? index - j : index + j;
                                        rgb1 = inPixels[k];

                                        a1 = (rgb1 >> 24) & 0xff;
                                        r1 = (rgb1 >> 16) & 0xff;
                                        g1 = (rgb1 >> 8) & 0xff;
                                        b1 = rgb1 & 0xff;
                                        a1 += ea * w/SUM;
                                        r1 += er * w/SUM;
                                        g1 += eg * w/SUM;
                                        b1 += eb * w/SUM;
                                        inPixels[k] = (clamp(a1) << 24 | clamp(r1) << 16) | (clamp(g1) << 8) | clamp(b1);
                                    }
                                }
                            }
                        }
                    }
                    index += direction;
                }
            }
        }

        // create palette
        HashMap<Integer,Integer> p = new HashMap<Integer,Integer>();

        for (int i = 0; i < count; i++) {
            int color;
            if (dither) {
                color = table[outPixels[i]&0xff];
            } else {
                color = table[quantizer.getIndexForColor(inPixels[i])];
            }
            int idx = p.size();
            Integer idxEx = p.get(color);
            if (idxEx == null) {
                p.put(color, idx);
            } else {
                idx = idxEx;
            }
            outPixels[i] = (byte)(idx);
        }

        Set <Integer>keys = p.keySet();
        int pal[] = new int[p.size()];
        for (int k : keys) {
            pal[p.get(k)] = k;
        }

        return pal;
    }
} 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;
        }
    }
} 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 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);
} 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;
    }
}
import bdsup2sub.bitmap.Bitmap;
import bdsup2sub.bitmap.BitmapBounds;
import bdsup2sub.bitmap.Palette;
import bdsup2sub.core.*;
import bdsup2sub.supstream.SubPicture;
import bdsup2sub.supstream.SubtitleStream;
import bdsup2sub.tools.QuantizeFilter;
import bdsup2sub.utils.FilenameUtils;
import bdsup2sub.utils.SubtitleUtils;
import bdsup2sub.utils.ToolBox;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import static bdsup2sub.core.Constants.LANGUAGES;
import static bdsup2sub.utils.TimeUtils.ptsToTimeStrXml;
import static bdsup2sub.utils.TimeUtils.timeStrXmlToPTS;
/*
 * Copyright 2014 Volker Oth (0xdeadbeef) / Miklos Juhasz (mjuhasz)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package bdsup2sub.supstream.bdnxml;




/**
 * Reading and writing of Blu-Ray captions in Xml/Png format.
 */
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;