
bdsup2sub.supstream.dvd.SubDvd Maven / Gradle / Ivy
The newest version!
/*
* 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.dvd;
import bdsup2sub.BDSup2SubManager;
import bdsup2sub.bitmap.Bitmap;
import bdsup2sub.bitmap.BitmapBounds;
import bdsup2sub.bitmap.Palette;
import bdsup2sub.core.*;
import bdsup2sub.supstream.ImageObjectFragment;
import bdsup2sub.supstream.SubPicture;
import bdsup2sub.tools.FileBuffer;
import bdsup2sub.tools.FileBufferException;
import bdsup2sub.utils.ToolBox;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import static bdsup2sub.core.Constants.*;
import static bdsup2sub.utils.ByteUtils.getByte;
import static bdsup2sub.utils.ByteUtils.getWord;
import static bdsup2sub.utils.TimeUtils.timeStrToPTS;
/**
* Handling of SUB/IDX (VobSub) streams.
*/
public class SubDvd implements DvdSubtitleStream {
private static final Configuration configuration = Configuration.getInstance();
private static final LibLogger logger = LibLogger.getInstance();
/** ArrayList of captions contained in the current file */
private final List subPictures = new ArrayList();
/** color palette read from idx file */
private Palette srcPalette = new Palette(DEFAULT_DVD_PALETTE);
/** color palette created for last decoded caption */
private Palette palette;
/** bitmap of the last decoded caption */
private Bitmap bitmap;
/** screen width of imported VobSub */
private int screenWidth = 720;
/** screen height of imported VobSub */
private int screenHeight = 576;
/** global x offset */
private int globalXOffset;
/** global y offset */
private int globalYOffset;
/** global delay */
private int globalDelay;
/** index of language read from IDX */
private int languageIndex;
/** stream ID */
private int streamID;
/** FileBuffer for reading SUB */
private final FileBuffer buffer;
/** index of dominant color for the current caption */
private int primaryColorIndex;
/** number of forced captions in the current file */
private int forcedFrameCount;
/** store last alpha values for invisible workaround */
private static int[] lastAlpha = {0, 0xf, 0xf, 0xf};
public SubDvd(String subFile, String idxFile, BDSup2SubManager manager) throws CoreException {
readIdx(idxFile);
manager.setProgressMax(subPictures.size());
try {
buffer = new FileBuffer(subFile);
} catch (FileBufferException e) {
throw new CoreException(e.getMessage());
}
for (int i=0; i < subPictures.size(); i++) {
manager.setProgress(i);
logger.info("# " + (i + 1) + "\n");
logger.trace("Offset: " + ToolBox.toHexLeftZeroPadded(subPictures.get(i).getOffset(), 8) + "\n");
long nextOfs;
if (i < subPictures.size() - 1) {
nextOfs = subPictures.get(i+1).getOffset();
} else {
nextOfs = buffer.getSize();
}
readSubFrame(subPictures.get(i), nextOfs, buffer);
}
logger.info("\nDetected " + forcedFrameCount + " forced captions.\n");
}
private void readIdx(String idxFile) throws CoreException {
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(idxFile));
String s;
int v;
int langIdx = 0;
boolean ignore = false;
while ((s = in.readLine()) != null) {
s = s.trim();
if (s.length() < 1 || s.charAt(0) == '#') {
continue;
}
int pos = s.indexOf(':');
if (pos == -1 || s.length()-pos <= 1) {
logger.error("Illegal key: " + s + "\n");
continue;
}
String key = s.substring(0, pos).trim();
String val = s.substring(pos+1).trim();
// size (e.g. "size: 720x576")
if (key.equalsIgnoreCase("size")) {
pos = val.indexOf('x');
if (pos == -1 || val.length()-pos <= 1) {
throw new CoreException("Illegal size: " + val);
}
v = ToolBox.getInt(val.substring(0,pos));
if (v < 2) {
throw new CoreException("Illegal screen width: " + v);
}
screenWidth = v;
v = ToolBox.getInt(val.substring(pos+1));
if (v < 2) {
throw new CoreException("Illegal screen height: " + v);
}
screenHeight = v;
continue;
}
// origin (e.g. "org: 0, 0")
if (key.equalsIgnoreCase("org")) {
pos = val.indexOf(',');
if (pos == -1 || val.length()-pos <= 1) {
throw new CoreException("Illegal origin: " + val);
}
v = ToolBox.getInt(val.substring(0,pos));
if (v < 0) {
throw new CoreException("Illegal x origin: " + v);
}
globalXOffset = v;
v = ToolBox.getInt(val.substring(pos+1));
if (v < 0) {
throw new CoreException("Illegal y origin: " + v);
}
globalYOffset = v;
continue;
}
// scale (e.g. "scale: 100%, 100%")
if (key.equalsIgnoreCase("scale")) {
// ignored for the moment
continue;
}
// alpha (e.g. "alpha: 100%")
if (key.equalsIgnoreCase("alpha")) {
// ignored for the moment
continue;
}
// smoothing (e.g. "smooth: OFF")
if (key.equalsIgnoreCase("smooth")) {
// ignored for the moment
continue;
}
// fading (e.g. "fadein/out: 0, 0");
if (key.equalsIgnoreCase("fadein/out")) {
// ignored for the moment
continue;
}
// alignment (e.g. "align: OFF at LEFT TOP")
if (key.equalsIgnoreCase("align")) {
// ignored for the moment
continue;
}
// time offset (e.g. "time offset: 0")
if (key.equalsIgnoreCase("time offset")) {
v = ToolBox.getInt(val);
if (v < 0) {
v = (int)timeStrToPTS(val);
}
if (v < 0) {
throw new CoreException("Illegal time offset: " + v);
}
globalDelay = v * 90; // ms -> 90kHz
continue;
}
// forced subs (e.g. "forced subs: OFF")
if (key.equalsIgnoreCase("align")) {
// ignored for the moment
continue;
}
// palette
if (key.equalsIgnoreCase("palette")) {
String vals[] = val.split(",");
if (vals == null || vals.length < 1 || vals.length > 16) {
throw new CoreException("Illegal palette definition: " + val);
}
for (int i=0; i 90kHz
// ignored for the moment
continue;
}
// language id (e.g. "id: de, index: 0")
if (key.equalsIgnoreCase("id")) {
String id;
pos = val.indexOf(',');
if (pos > 0 ) {
id = val.substring(0, pos).trim();
} else {
id = val;
}
if (id.length() != 2) {
logger.warn("Illegal language id: " + id + "\n");
continue;
}
boolean found = false;
for (int i=0; i < LANGUAGES.length; i++) {
if (id.equalsIgnoreCase(LANGUAGES[i][1])) {
languageIndex = i;
found = true;
break;
}
}
if (!found) {
logger.warn("Illegal language id: " + id + "\n");
}
pos = val.indexOf(':');
if (pos == -1 || s.length() - pos <= 1) {
logger.error("Missing index key: " + val + "\n");
continue;
}
key = val.substring(0, pos).trim();
val = val.substring(pos+1).trim();
if (key.equalsIgnoreCase("index")) {
logger.error("Missing index key: " + s + "\n");
continue;
}
v = ToolBox.getInt(val);
if (v < 0) {
throw new CoreException("Illegal language index: " + v);
}
if (v != langIdx) {
ignore = true;
logger.warn("Language id " + id + "(index:" + v + ") inactive -> ignored\n");
} else {
streamID = v;
ignore = false;
}
continue;
}
if (!ignore) {
// timestamp: 00:00:14:160, filepos: 000000000
if (key.equalsIgnoreCase("timestamp")) {
String vs;
pos = val.indexOf(',');
if (pos == -1 || val.length()-pos <= 1) {
throw new CoreException("Illegal timestamp entry: " + val);
}
vs = val.substring(0,pos);
long t = timeStrToPTS(vs);
if (t < 0) {
throw new CoreException("Illegal timestamp: " + vs);
}
vs = val.substring(pos+1).toLowerCase();
pos = vs.indexOf("filepos:");
if (pos == -1 || vs.length()-pos <= 1) {
throw new CoreException("Missing filepos: " + val);
}
long l = Long.parseLong(vs.substring(pos+8).trim(), 16);
if (l == -1) {
throw new CoreException("Illegal filepos: " + vs.substring(pos+8));
}
SubPictureDVD pic = new SubPictureDVD();
pic.setOffset(l);
pic.setWidth(screenWidth);
pic.setHeight(screenHeight);
pic.setStartTime(t + globalDelay);
subPictures.add(pic);
}
}
}
} catch (IOException ex) {
throw new CoreException(ex.getMessage());
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException ex) {
}
}
}
/**
* Read one frame from SUB file
* @param pic SubPicture object for this frame
* @param endOfs end offset
* @param buffer File Buffer to read from
* @throws CoreException
*/
private void readSubFrame(SubPictureDVD pic, long endOfs, FileBuffer buffer) throws CoreException {
long ofs = pic.getOffset();
long ctrlOfs = -1;
long nextOfs;
int ctrlOfsRel = 0;
int rleSize = 0;
int rleBufferFound = 0;
int ctrlSize = -1;
int ctrlHeaderCopied = 0;
byte ctrlHeader[] = null;
ImageObjectFragment rleFrag;
int length;
int packHeaderSize;
boolean firstPackFound = false;
try {
do {
// 4 bytes: packet identifier 0x000001ba
long startOfs = ofs;
if (buffer.getDWord(ofs) != 0x000001ba) {
throw new CoreException("Missing packet identifier at ofs " + ToolBox.toHexLeftZeroPadded(ofs,8));
}
// 6 bytes: system clock reference
// 3 bytes: multiplexer rate
// 1 byte: stuffing info
int stuffOfs = buffer.getByte(ofs+=13) & 7;
// 4 bytes: sub packet ID 0x000001bd
if (buffer.getDWord(ofs += (1+stuffOfs)) != 0x000001bd) {
throw new CoreException("Missing packet identifier at ofs " + ToolBox.toHexLeftZeroPadded(ofs,8));
}
// 2 bytes: packet length (number of bytes after this entry)
length = buffer.getWord(ofs+=4);
nextOfs = ofs+2+length;
// 2 bytes: packet type
ofs += 2;
packHeaderSize = (int)(ofs-startOfs);
boolean firstPack = ((buffer.getByte(++ofs) & 0x80) == 0x80);
// 1 byte pts length
int ptsLength = buffer.getByte(ofs+=1);
ofs += 1 + ptsLength; // skip PTS and stream ID
int packetStreamID = buffer.getByte(ofs++) - 0x20;
if (packetStreamID != streamID) {
// packet doesn't belong to stream -> skip
if (nextOfs % 0x800 != 0) {
ofs = (nextOfs/0x800 + 1)*0x800;
logger.warn("Offset to next fragment is invalid. Fixed to:" + ToolBox.toHexLeftZeroPadded(ofs, 8) + "\n");
} else {
ofs = nextOfs;
}
ctrlOfs += 0x800;
continue;
}
int headerSize = (int)(ofs-startOfs); // only valid for additional packets
if (firstPack && ptsLength >= 5) {
int size = buffer.getWord(ofs);
ofs += 2;
ctrlOfsRel = buffer.getWord(ofs);
rleSize = ctrlOfsRel-2; // calculate size of RLE buffer
ctrlSize = size-ctrlOfsRel-2; // calculate size of control header
if (ctrlSize < 0) {
throw new CoreException("Invalid control buffer size");
}
ctrlHeader = new byte[ctrlSize];
ctrlOfs = ctrlOfsRel + ofs; // might have to be corrected for multiple packets
ofs += 2;
headerSize = (int)(ofs-startOfs);
pic.setRleFragments(new ArrayList());
firstPackFound = true;
} else {
if (firstPackFound) {
ctrlOfs += headerSize; // fix absolute offset by adding header bytes
} else {
logger.warn("Invalid fragment skipped at ofs " + ToolBox.toHexLeftZeroPadded(startOfs, 8) + "\n");
}
}
// check if control header is (partly) in this packet
int diff = (int)(nextOfs - ctrlOfs - ctrlHeaderCopied);
if (diff<0) {
diff = 0;
}
int copied = ctrlHeaderCopied;
try {
for (int i=0; (i < diff) && (ctrlHeaderCopied ctrlSize) {
logger.warn("Invalid end sequence offset -> no end time\n");
endSeqOfs = ctrlSize;
}
index += 2;
parse_ctrl:
while (index < endSeqOfs) {
int cmd = getByte(ctrlHeader, index++);
switch (cmd) {
case 0: // forced (?)
pic.setForced(true);
forcedFrameCount++;
break;
case 1: // start display
break;
case 3: // palette info
b = getByte(ctrlHeader, index++);
pic.getPal()[3] = (b >> 4);
pic.getPal()[2] = b & 0x0f;
b = getByte(ctrlHeader, index++);
pic.getPal()[1] = (b >> 4);
pic.getPal()[0] = b & 0x0f;
logger.trace("Palette: " + pic.getPal()[0] + ", " + pic.getPal()[1] + ", " + pic.getPal()[2] + ", " + pic.getPal()[3] + "\n");
break;
case 4: // alpha info
b = getByte(ctrlHeader, index++);
pic.getAlpha()[3] = (b >> 4);
pic.getAlpha()[2] = b & 0x0f;
b = getByte(ctrlHeader, index++);
pic.getAlpha()[1] = (b >> 4);
pic.getAlpha()[0] = b & 0x0f;
for (int i = 0; i<4; i++) {
alphaSum += pic.getAlpha()[i] & 0xff;
}
logger.trace("Alpha: " + pic.getAlpha()[0] + ", " + pic.getAlpha()[1] + ", " + pic.getAlpha()[2] + ", " + pic.getAlpha()[3] + "\n");
break;
case 5: // coordinates
int xOfs = (getByte(ctrlHeader, index)<<4) | (getByte(ctrlHeader, index+1)>>4);
pic.setOfsX(globalXOffset +xOfs);
pic.setImageWidth((((getByte(ctrlHeader, index+1)&0xf)<<8) | (getByte(ctrlHeader, index+2))) - xOfs + 1);
int yOfs = (getByte(ctrlHeader, index+3)<<4) | (getByte(ctrlHeader, index+4)>>4);
pic.setOfsY(globalYOffset +yOfs);
pic.setImageHeight((((getByte(ctrlHeader, index+4)&0xf)<<8) | (getByte(ctrlHeader, index+5))) - yOfs + 1);
logger.trace("Area info:" + " ("
+ pic.getXOffset() + ", " + pic.getYOffset() + ") - (" + (pic.getXOffset() + pic.getImageWidth() - 1) + ", "
+ (pic.getYOffset() + pic.getImageHeight() - 1) + ")\n");
index += 6;
break;
case 6: // offset to RLE buffer
pic.setEvenOffset(getWord(ctrlHeader, index) - 4);
pic.setOddOffset(getWord(ctrlHeader, index + 2) - 4);
index += 4;
logger.trace("RLE ofs: " + ToolBox.toHexLeftZeroPadded(pic.getEvenOffset(), 4) + ", " + ToolBox.toHexLeftZeroPadded(pic.getOddOffset(), 4) + "\n");
break;
case 7: // color/alpha update
ColAlphaUpdate = true;
//int len = ToolBox.getWord(ctrlHeader, index);
// ignore the details for now, but just get alpha and palette info
alphaUpdateSum = 0;
b = getByte(ctrlHeader, index+10);
alphaUpdate[3] = (b >> 4);
alphaUpdate[2] = b & 0x0f;
b = getByte(ctrlHeader, index+11);
alphaUpdate[1] = (b >> 4);
alphaUpdate[0] = b & 0x0f;
for (int i = 0; i<4; i++) {
alphaUpdateSum += alphaUpdate[i] & 0xff;
}
// only use more opaque colors
if (alphaUpdateSum > alphaSum) {
alphaSum = alphaUpdateSum;
System.arraycopy(alphaUpdate, 0, pic.getAlpha(), 0, 4);
// take over frame palette
b = getByte(ctrlHeader, index+8);
pic.getPal()[3] = (b >> 4);
pic.getPal()[2] = b & 0x0f;
b = getByte(ctrlHeader, index+9);
pic.getPal()[1] = (b >> 4);
pic.getPal()[0] = b & 0x0f;
}
// search end sequence
index = endSeqOfs;
delay = getWord(ctrlHeader, index)*1024;
endSeqOfs = getWord(ctrlHeader, index+2)-ctrlOfsRel-2;
if (endSeqOfs < 0 || endSeqOfs > ctrlSize) {
logger.warn("Invalid end sequence offset -> no end time\n");
endSeqOfs = ctrlSize;
}
index += 4;
break;
case 0xff: // end sequence
break parse_ctrl;
default:
logger.warn("Unknown control sequence " + ToolBox.toHexLeftZeroPadded(cmd, 2) + " skipped\n");
break;
}
}
if (endSeqOfs != ctrlSize) {
int ctrlSeqCount = 1;
index = -1;
int nextIndex = endSeqOfs;
while (nextIndex != index) {
index = nextIndex;
delay = getWord(ctrlHeader, index) * 1024;
nextIndex = getWord(ctrlHeader, index + 2) - ctrlOfsRel - 2;
ctrlSeqCount++;
}
if (ctrlSeqCount > 2) {
logger.warn("Control sequence(s) ignored - result may be erratic.");
}
pic.setEndTime(pic.getStartTime() + delay);
} else {
pic.setEndTime(pic.getStartTime());
}
if (ColAlphaUpdate) {
logger.warn("Palette update/alpha fading detected - result may be erratic.\n");
}
if (alphaSum == 0) {
if (configuration.getFixZeroAlpha()) {
System.arraycopy(lastAlpha, 0, pic.getAlpha(), 0, 4);
logger.warn("Invisible caption due to zero alpha - used alpha info of last caption.\n");
} else {
logger.warn("Invisible caption due to zero alpha (not fixed due to user setting).\n");
}
}
lastAlpha = pic.getAlpha();
pic.storeOriginal();
} catch (IndexOutOfBoundsException ex) {
throw new CoreException("Index "+ex.getMessage() + " out of bounds in control header.");
}
}
public void decode(int index) throws CoreException {
if (index < subPictures.size()) {
decode(subPictures.get(index));
} else {
throw new CoreException("Index " + index + " out of bounds\n");
}
}
private void decode(SubPictureDVD pic) throws CoreException {
palette = SupDvdUtil.decodePalette(pic, srcPalette);
bitmap = SupDvdUtil.decodeImage(pic, buffer, palette.getIndexOfMostTransparentPaletteEntry());
// crop
BitmapBounds bounds = bitmap.getCroppingBounds(palette.getAlpha(), configuration.getAlphaCrop());
if (bounds.yMin>0 || bounds.xMin > 0 || bounds.xMax
© 2015 - 2025 Weber Informatics LLC | Privacy Policy