/** * 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()); }
/** * 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; SubDvd.CONTROL_HEADER[2] = 0x01; // display SubDvd.CONTROL_HEADER[3] = 0x00; // forced controlHeaderLen = SubDvd.CONTROL_HEADER.length; } else { forcedOfs = 1; SubDvd.CONTROL_HEADER[2] = 0x00; // part of offset SubDvd.CONTROL_HEADER[3] = 0x01; // display controlHeaderLen = SubDvd.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? SubDvd.HEADER_FIRST[9] = (byte) (((ptm >> 29) & 0x0E) | 0x21); SubDvd.HEADER_FIRST[10] = (byte) (ptm >> 22); SubDvd.HEADER_FIRST[11] = (byte) ((ptm >> 14) | 1); SubDvd.HEADER_FIRST[12] = (byte) (ptm >> 7); SubDvd.HEADER_FIRST[13] = (byte) (ptm * 2 + 1); /* control header */ /* palette (store reversed) */ SubDvd.CONTROL_HEADER[1 + 4] = (byte) (((pic.getPal()[3] & 0xf) << 4) | (pic.getPal()[2] & 0x0f)); SubDvd.CONTROL_HEADER[1 + 5] = (byte) (((pic.getPal()[1] & 0xf) << 4) | (pic.getPal()[0] & 0x0f)); /* alpha (store reversed) */ SubDvd.CONTROL_HEADER[1 + 7] = (byte) (((pic.getAlpha()[3] & 0xf) << 4) | (pic.getAlpha()[2] & 0x0f)); SubDvd.CONTROL_HEADER[1 + 8] = (byte) (((pic.getAlpha()[1] & 0xf) << 4) | (pic.getAlpha()[0] & 0x0f)); /* coordinates of subtitle */ SubDvd.CONTROL_HEADER[1 + 10] = (byte) ((pic.getXOffset() >> 4) & 0xff); tmp = pic.getXOffset() + bm.getWidth() - 1; SubDvd.CONTROL_HEADER[1 + 11] = (byte) (((pic.getXOffset() & 0xf) << 4) | ((tmp >> 8) & 0xf)); SubDvd.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; } } SubDvd.CONTROL_HEADER[1 + 13] = (byte) ((yOfs >> 4) & 0xff); tmp = yOfs + bm.getHeight() - 1; SubDvd.CONTROL_HEADER[1 + 14] = (byte) (((yOfs & 0xf) << 4) | ((tmp >> 8) & 0xf)); SubDvd.CONTROL_HEADER[1 + 15] = (byte) (tmp & 0xff); /* offset to even lines in rle buffer */ SubDvd.CONTROL_HEADER[1 + 17] = 0x00; /* 2 bytes subpicture size and 2 bytes control header ofs */ SubDvd.CONTROL_HEADER[1 + 18] = 0x04; /* note: SubtitleCreator uses 6 and adds 0x0000 in between */ /* offset to odd lines in rle buffer */ tmp = even.length + SubDvd.CONTROL_HEADER[1 + 18]; SubDvd.CONTROL_HEADER[1 + 19] = (byte) ((tmp >> 8) & 0xff); SubDvd.CONTROL_HEADER[1 + 20] = (byte) (tmp & 0xff); /* display duration in frames */ tmp = (int) ((pic.getEndTime() - pic.getStartTime()) / 1024); // 11.378ms resolution???? SubDvd.CONTROL_HEADER[1 + 22] = (byte) ((tmp >> 8) & 0xff); SubDvd.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; SubDvd.CONTROL_HEADER[forcedOfs + 0] = (byte) ((tmp >> 8) & 0xff); SubDvd.CONTROL_HEADER[forcedOfs + 1] = (byte) (tmp & 0xff); SubDvd.CONTROL_HEADER[1 + 24] = (byte) ((tmp >> 8) & 0xff); SubDvd.CONTROL_HEADER[1 + 25] = (byte) (tmp & 0xff); // subpicture size tmp = even.length + odd.length + 4 + controlHeaderLen; SubDvd.HEADER_FIRST[15] = (byte) (tmp >> 8); SubDvd.HEADER_FIRST[16] = (byte) tmp; /* offset to control buffer - 2 is the size of the offset */ tmp = even.length + odd.length + 2; SubDvd.HEADER_FIRST[17] = (byte) (tmp >> 8); SubDvd.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 = SubDvd.PACK_HEADER.length + SubDvd.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 - SubDvd.PACK_HEADER.length - SubDvd.HEADER_FIRST.length); // size - 0x7df while (remainingRLEsize > (0x800 - SubDvd.PACK_HEADER.length - SubDvd.HEADER_NEXT.length - controlHeaderLen)) { remainingRLEsize -= (0x800 - SubDvd.PACK_HEADER.length - SubDvd.HEADER_NEXT.length); bufSize += SubDvd.PACK_HEADER.length + SubDvd.HEADER_NEXT.length; numAdditionalPackets++; } // packet length of the 1st packet should be the maximum size tmp = 0x800 - SubDvd.PACK_HEADER.length - 6; } else { tmp = (bufSize - SubDvd.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 : SubDvd.PACK_HEADER) { buf[ofs++] = packHeader; } // set packet length tmp += stuffingBytes; SubDvd.HEADER_FIRST[4] = (byte) (tmp >> 8); SubDvd.HEADER_FIRST[5] = (byte) tmp; // set pts length SubDvd.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++] = SubDvd.HEADER_FIRST[i]; } for (int i = 0; i < stuffingBytes; i++) { buf[ofs++] = (byte) 0xff; } for (int i = 14; i < SubDvd.HEADER_FIRST.length; i++) { buf[ofs++] = SubDvd.HEADER_FIRST[i]; } // write (first part of) RLE buffer tmp = sizeRLE; if (numAdditionalPackets > 0) { tmp = (0x800 - SubDvd.PACK_HEADER.length - stuffingBytes - SubDvd.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] = SubDvd.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 = SubDvd.HEADER_NEXT.length + (controlHeaderLen - controlHeaderWritten) + (sizeRLE - ofsRLE) - 6; } else { tmp = 0x800 - SubDvd.PACK_HEADER.length - 6; rleSizeLeft = (0x800 - SubDvd.PACK_HEADER.length - SubDvd.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 SubDvd.PACK_HEADER[13] = (byte) (0xf8); for (byte packHeader : SubDvd.PACK_HEADER) { buf[ofs++] = packHeader; } // set packet length SubDvd.HEADER_NEXT[4] = (byte) (tmp >> 8); SubDvd.HEADER_NEXT[5] = (byte) tmp; for (byte b : SubDvd.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] = SubDvd.CONTROL_HEADER[forcedOfs + (controlHeaderWritten++)]; } } } // write (rest of) control header for (int i = controlHeaderWritten; i < controlHeaderLen; i++) { buf[ofs++] = SubDvd.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; }
public final class SubDvdWriter { private static final Configuration configuration = Configuration.getInstance(); 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; SubDvd.CONTROL_HEADER[2] = 0x01; // display SubDvd.CONTROL_HEADER[3] = 0x00; // forced controlHeaderLen = SubDvd.CONTROL_HEADER.length; } else { forcedOfs = 1; SubDvd.CONTROL_HEADER[2] = 0x00; // part of offset SubDvd.CONTROL_HEADER[3] = 0x01; // display controlHeaderLen = SubDvd.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? SubDvd.HEADER_FIRST[9] = (byte) (((ptm >> 29) & 0x0E) | 0x21); SubDvd.HEADER_FIRST[10] = (byte) (ptm >> 22); SubDvd.HEADER_FIRST[11] = (byte) ((ptm >> 14) | 1); SubDvd.HEADER_FIRST[12] = (byte) (ptm >> 7); SubDvd.HEADER_FIRST[13] = (byte) (ptm * 2 + 1); /* control header */ /* palette (store reversed) */ SubDvd.CONTROL_HEADER[1 + 4] = (byte) (((pic.getPal()[3] & 0xf) << 4) | (pic.getPal()[2] & 0x0f)); SubDvd.CONTROL_HEADER[1 + 5] = (byte) (((pic.getPal()[1] & 0xf) << 4) | (pic.getPal()[0] & 0x0f)); /* alpha (store reversed) */ SubDvd.CONTROL_HEADER[1 + 7] = (byte) (((pic.getAlpha()[3] & 0xf) << 4) | (pic.getAlpha()[2] & 0x0f)); SubDvd.CONTROL_HEADER[1 + 8] = (byte) (((pic.getAlpha()[1] & 0xf) << 4) | (pic.getAlpha()[0] & 0x0f)); /* coordinates of subtitle */ SubDvd.CONTROL_HEADER[1 + 10] = (byte) ((pic.getXOffset() >> 4) & 0xff); tmp = pic.getXOffset() + bm.getWidth() - 1; SubDvd.CONTROL_HEADER[1 + 11] = (byte) (((pic.getXOffset() & 0xf) << 4) | ((tmp >> 8) & 0xf)); SubDvd.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; } } SubDvd.CONTROL_HEADER[1 + 13] = (byte) ((yOfs >> 4) & 0xff); tmp = yOfs + bm.getHeight() - 1; SubDvd.CONTROL_HEADER[1 + 14] = (byte) (((yOfs & 0xf) << 4) | ((tmp >> 8) & 0xf)); SubDvd.CONTROL_HEADER[1 + 15] = (byte) (tmp & 0xff); /* offset to even lines in rle buffer */ SubDvd.CONTROL_HEADER[1 + 17] = 0x00; /* 2 bytes subpicture size and 2 bytes control header ofs */ SubDvd.CONTROL_HEADER[1 + 18] = 0x04; /* note: SubtitleCreator uses 6 and adds 0x0000 in between */ /* offset to odd lines in rle buffer */ tmp = even.length + SubDvd.CONTROL_HEADER[1 + 18]; SubDvd.CONTROL_HEADER[1 + 19] = (byte) ((tmp >> 8) & 0xff); SubDvd.CONTROL_HEADER[1 + 20] = (byte) (tmp & 0xff); /* display duration in frames */ tmp = (int) ((pic.getEndTime() - pic.getStartTime()) / 1024); // 11.378ms resolution???? SubDvd.CONTROL_HEADER[1 + 22] = (byte) ((tmp >> 8) & 0xff); SubDvd.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; SubDvd.CONTROL_HEADER[forcedOfs + 0] = (byte) ((tmp >> 8) & 0xff); SubDvd.CONTROL_HEADER[forcedOfs + 1] = (byte) (tmp & 0xff); SubDvd.CONTROL_HEADER[1 + 24] = (byte) ((tmp >> 8) & 0xff); SubDvd.CONTROL_HEADER[1 + 25] = (byte) (tmp & 0xff); // subpicture size tmp = even.length + odd.length + 4 + controlHeaderLen; SubDvd.HEADER_FIRST[15] = (byte) (tmp >> 8); SubDvd.HEADER_FIRST[16] = (byte) tmp; /* offset to control buffer - 2 is the size of the offset */ tmp = even.length + odd.length + 2; SubDvd.HEADER_FIRST[17] = (byte) (tmp >> 8); SubDvd.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 = SubDvd.PACK_HEADER.length + SubDvd.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 - SubDvd.PACK_HEADER.length - SubDvd.HEADER_FIRST.length); // size - 0x7df while (remainingRLEsize > (0x800 - SubDvd.PACK_HEADER.length - SubDvd.HEADER_NEXT.length - controlHeaderLen)) { remainingRLEsize -= (0x800 - SubDvd.PACK_HEADER.length - SubDvd.HEADER_NEXT.length); bufSize += SubDvd.PACK_HEADER.length + SubDvd.HEADER_NEXT.length; numAdditionalPackets++; } // packet length of the 1st packet should be the maximum size tmp = 0x800 - SubDvd.PACK_HEADER.length - 6; } else { tmp = (bufSize - SubDvd.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 : SubDvd.PACK_HEADER) { buf[ofs++] = packHeader; } // set packet length tmp += stuffingBytes; SubDvd.HEADER_FIRST[4] = (byte) (tmp >> 8); SubDvd.HEADER_FIRST[5] = (byte) tmp; // set pts length SubDvd.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++] = SubDvd.HEADER_FIRST[i]; } for (int i = 0; i < stuffingBytes; i++) { buf[ofs++] = (byte) 0xff; } for (int i = 14; i < SubDvd.HEADER_FIRST.length; i++) { buf[ofs++] = SubDvd.HEADER_FIRST[i]; } // write (first part of) RLE buffer tmp = sizeRLE; if (numAdditionalPackets > 0) { tmp = (0x800 - SubDvd.PACK_HEADER.length - stuffingBytes - SubDvd.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] = SubDvd.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 = SubDvd.HEADER_NEXT.length + (controlHeaderLen - controlHeaderWritten) + (sizeRLE - ofsRLE) - 6; } else { tmp = 0x800 - SubDvd.PACK_HEADER.length - 6; rleSizeLeft = (0x800 - SubDvd.PACK_HEADER.length - SubDvd.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 SubDvd.PACK_HEADER[13] = (byte) (0xf8); for (byte packHeader : SubDvd.PACK_HEADER) { buf[ofs++] = packHeader; } // set packet length SubDvd.HEADER_NEXT[4] = (byte) (tmp >> 8); SubDvd.HEADER_NEXT[5] = (byte) tmp; for (byte b : SubDvd.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] = SubDvd.CONTROL_HEADER[forcedOfs + (controlHeaderWritten++)]; } } } // write (rest of) control header for (int i = controlHeaderWritten; i < controlHeaderLen; i++) { buf[ofs++] = SubDvd.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) { } } } }
/** * 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) { } } }
/** Reading and writing of Blu-Ray captions demuxed from M2TS transport streams (BD-SUP). */ 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()); } }
/** 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; /** 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); } } } }
/** * 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 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!"); } }