
bdsup2sub.supstream.bdnxml.SupXml 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.bdnxml;
import bdsup2sub.BDSup2SubManager;
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;
/**
* 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 LibLogger logger = LibLogger.getInstance();
/** ArrayList of captions contained in the current file */
private final List subPictures = new ArrayList();
/** 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 final 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);
private final BDSup2SubManager manager;
/**
* Constructor (for reading)
* @param filename file name of Xml file to read
* @throws CoreException
*/
public SupXml(String filename, BDSup2SubManager manager) throws CoreException {
this.manager = manager;
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 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("");
out.newLine();
out.write("");
out.newLine();
out.write(" ");
out.newLine();
out.write(" ");
out.newLine();
out.write(" ");
out.newLine();
String res = configuration.getOutputResolution().getResolutionNameForXml();
out.write(" ");
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(" ");
out.newLine();
out.write(" ");
out.newLine();
out.write(" ");
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(" ");
out.newLine();
String pname = getPNGname(name, idx+1);
out.write(" " + pname + "
");
out.newLine();
out.write(" ");
out.newLine();
}
out.write(" ");
out.newLine();
out.write(" ");
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()
*/
@Override
public Bitmap getBitmap() {
return bitmap;
}
/* (non-Javadoc)
* @see SubtitleStream#getImage()
*/
@Override
public BufferedImage getImage() {
return bitmap.getImage(palette.getColorModel());
}
/* (non-Javadoc)
* @see SubtitleStream#getImage(Bitmap)
*/
@Override
public BufferedImage getImage(final Bitmap bm) {
return bm.getImage(palette.getColorModel());
}
/* (non-Javadoc)
* @see SubtitleStream#getForcedFrameCount()
*/
@Override
public int getForcedFrameCount() {
return numForcedFrames;
}
/* (non-Javadoc)
* @see SubtitleStream#getNumFrames()
*/
@Override
public int getFrameCount() {
return subPictures.size();
}
/* (non-Javadoc)
* @see SubtitleStream#getPalette()
*/
@Override
public Palette getPalette() {
return palette;
}
/* (non-Javadoc)
* @see SubtitleStream#getPrimaryColorIndex()
*/
@Override
public int getPrimaryColorIndex() {
return primaryColorIndex;
}
/* (non-Javadoc)
* @see deadbeef.SupTools.SubtitleStream#getStartOffset(int)
*/
@Override
public long getStartOffset(int index) {
// dummy
return 0;
}
/* (non-Javadoc)
* @see SubtitleStream#getSubPicture(int)
*/
@Override
public SubPicture getSubPicture(int index) {
return subPictures.get(index);
}
/* (non-Javadoc)
* @see SubtitleStream#getEndTime(int)
*/
@Override
public long getEndTime(int index) {
return subPictures.get(index).getEndTime();
}
/* (non-Javadoc)
* @see SubtitleStream#getStartTime(int)
*/
@Override
public long getStartTime(final int index) {
return subPictures.get(index).getStartTime();
}
/* (non-Javadoc)
* @see SubtitleStream#isForced(int)
*/
@Override
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 */
manager.setProgressMax(n);
}
}
break;
case EVENT:
pic = new SubPictureXml();
subPictures.add(pic);
int num = subPictures.size();
logger.info("#" + num + "\n");
manager.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);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy