蓝光.SUP字幕格式解析 时间: 2022-06-14 15:54 分类: JAVA 有关格式信息目前全网就一篇文章,并且有些地方讲得不是很详细。 另外就是一篇论文(老外都是这样,所有的东西都是从一篇论文开始的,但有关图形字幕的实现可以说寥寥无几,有肯定是有的,只不过实现得太复杂了,要想抽取出来自己做个简单功能还得阅读整个源码) 参考地址: [https://blog.thescorpius.com/index.php/2017/07/15/presentation-graphic-stream-sup-files-bluray-subtitle-format/]() [https://encrypted.google.com/patents/US20090185789?cl=da]() 研究这个的原因是在使用[BDSup2Sub](https://github.com/mjuhasz/BDSup2Sub)的时候,发现有两个严重问题: 1、某些图形字幕显示不完整 2、淡入淡出效果不明显(几乎没有,甚至还会出现错乱) 于是翻其源码,一开始对于这种图形字幕的格式一无所知,所以拿着源码根本无从下手。所以才研究了一番其格式,参阅了上面两篇文章。 与`PNG`、`GIF`这类图片格式非常类似,都会循环出现相关片段(这是以前研究的...但好像只写过一篇有关`BMP`格式的文章,唉,懒得敲啊,主要没时间)。 这里主要简单讲下其格式组成: `.SUP`图形字幕主要是由一系列连续的`Segment`组成,`Segment`分为以下几种类型: - PCS - WDS - PDS - ODS - END 并且一般都是由这个顺序出现,具体详细信息请参阅前面第一篇文章,接下来主要说下写代码解析的时候需要注意的地方。 1、图形字幕的图片信息主要保存在`ODS`中,`PCS`出现可以说是一条时间轴的图形字幕的开始,注意它有个什么`Composition State`,可以是以下三种: - Normal - Acquisition Point - Epoch Start 如果我们只是需要提取所有时间轴字幕图片,根本不需要考虑其值,但是如果想提取字幕进行`OCR`识别,那么就需要根据这个值来进行相同字幕的时间轴合并。 `Epoch Start`表示一张全新图形字幕的开始(说是全新,实际是根据你转换为`sup`字幕之前的`srt`或者`ass`字幕时间轴而言的,如果之前有两条时间轴内容一样,那么这个全新的图形字幕内容也是一样),其它两个可以理解为图形字幕的刷新(我的理解就是属于未转换之前`ass`字幕的同一条时间轴),也就是一些动画效果(比如淡入淡出),但实际图片字幕里的文字依旧没变。 简而言之就是:`SUP`字幕`PCS为Epoch Start状态`的个数≥未转换之前`srt`或者`ass`字幕的行数。这是因为如果未转换之前的一条时间轴的字幕如果包含动画,那么就会出现上面的情况,可以理解为将一条时间轴的字幕拆分为一系列的连续图片,所以`srt`或`ass`的一条时间轴字幕可能由多个`PCS...END`片段组成,这个时候就是根据`Composition State`来知道哪些`PCS...END`片段属于同一条时间轴,这样就可以进行合并。 由于前面说到的`BDSup2Sub`这个软件根据这个`Composition State`对相同字幕的时间轴进行了合并,所以就出现了我说的动画效果不明显的情况。 `PCS`中还有个重要的组成部分:`Composition Object`。 它里面包含了图形字幕(也就是`ODS`)相对于窗口的偏移位置信息,我调试的时候,发现`Composition Object`中的`offset`信息与`WDS`中的好像都是一致的,所以写代码提取显示的时候直接保存`Composition Object`中的信息即可。`PCS`中的`Composition Object`可以有多个,实际上它的个数与`ODS`、`WDS`的个数一样。 重点在于`ODS`,因为实际图形字幕就是保存在这个里面,它的个数也可以是多个,但它只包含了`width`、`height`信息,`offset`信息得去`PCS`中的`Composition Object`找,这也就是`BDSup2Sub`当时我想将其多个`ODS`合并成一个发现的问题:源代码中,我发现,由于作者将同条字幕的时间轴进行了合并,虽然存储了所有的`ODS`,但总是只显示第一个`ODS`的图片,`Composition Object`只存储了第一个,这导致我在想合并`ODS`的时候,无法获取其他`ODS`的`offset`信息,所以务必将所有的`Composition Object`进行存储,以备合并`ODS`时查询`offset`信息。 最后就是`ODS`合并的问题了,那么如何将多个`ODS`合并成一张图片呢? 计算方法如下: 1、计算最小`xOffset`与最大`yOffset` 2、计算最大`xOffset`与最小`yOffset` 合并后的`width` = 最大`xOffset` - 最小`xOffset` 合并后的`height` = 最大`yOffset` - 最小`yOffset` 由于解析后的图片都是些`bitmap`位图信息,所以只需将这些像素点填充到合并后的宽高大小的图片位置即可,具体代码如下: ```java int minX = 9999, minY = 9999, maxX = 0, maxY = 0; for (Map.Entry o : subPictureBD.getImageObjectMap().entrySet()) { if (o.getValue().getXOffset() < minX) { minX = o.getValue().getXOffset(); } if (o.getValue().getYOffset() < minY) { minY = o.getValue().getYOffset(); } if (o.getValue().getXOffset() + o.getValue().getWidth() > maxX) { maxX = o.getValue().getXOffset() + o.getValue().getWidth(); } if (o.getValue().getYOffset() + o.getValue().getHeight() > maxY) { maxY = o.getValue().getYOffset() + o.getValue().getHeight(); } } int width = maxX - minX; int height = maxY - minY; //fix after combined image's width, height, offset subPictureBD.setImageWidth(width); subPictureBD.setImageHeight(height); subPictureBD.setOfsX(minX); subPictureBD.setOfsY(minY); int b; int index = 0; int ofs = 0; int size; int xpos = 0; Bitmap bm = new Bitmap(width, height, (byte)transparentColorIndex); try { // just for multi-packet support, copy all of the image data in one common buffer for (Map.Entry entry : subPictureBD.getImageObjectMap().entrySet()) { ImageObject o = entry.getValue(); byte[] buffer = new byte[o.getBufferSize()]; index = 0; for (ImageObjectFragment fragment : o.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; //计算当前ODS在合并后的图片中的起始位置 ofs = (o.getYOffset() - minY) * width + o.getXOffset() - minX; do { b = buffer[index++] & 0xff; if (b == 0) { b = buffer[index++] & 0xff; if (b == 0) { // next line ofs = (ofs / width) * width + o.getXOffset() - minX; //注意每次新的一行起始填充位置都要加上当前ODS相对于合成图片的xOffset if (xpos < width) { ofs += width; } xpos = o.getXOffset() - minX; //计算新的一行xOffset } 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); } } ``` ####总结 若我们只想修改擦除图形字幕的某些广告信息,那么建议提取所有非`Normal`的`PCS...END`片段,不要根据`Composition State`对`PCS...END`片段进行合并,这样可以保留原始流畅的动画效果。若想进行`OCR`文字识别,那么就是说抛弃了动画效果,这个时候可以选择根据`Composition State`对`PCS...END`片段进行合并来减少需要识别的图片个数(当然了如果觉得合并`PCS...END`片段实现比较麻烦也可以识别后根据文字内容以及`PCS...END`片段的时间轴来进行选择性的合并)。 踩坑: 一条图形字幕并不是说只由一个`PCS...END`片段组成,但实际上每个`PCS`都包含了`StartTime`,每两个`PCS...END`片段可以确定一条时间轴,也可以说是每个`PCS...END`片段都可以看成一条时间轴。 当一条图形字幕开始时,并不是说`PCS、WDS、PDS、ODS、END`一定会都出现,它可以是下面这种: ![微信截图_20220614154408.png](https://0o0.me/usr/uploads/2022/06/1736239645.png) 为什么要有上图中`DS20`那样的片段。很简单,因为每个`Segment`都只包含`StartTime`,那么必须要由另外一个`Segment`来确定上一条时间轴的结束,所以就出现了只有`PCS END`的片段,因为它只是用来确定上一条时间轴的结束,并不需要包含任何图形字幕信息。 最后附上我修改后的`BDSup2Sub`:https://github.com/xwlcn/BDSupSubPlus 完~ 这东西本身就比较难理解,花了我两天时间才理清思路,所以上面也许讲得云里雾里的,就当做抛砖引玉吧。 标签: 无
博主,你好!
谢谢你提供的BDsup2sub新版本,基本解决了字幕显示不完整等问题;Bug反馈:在极少数SUP字幕转换为XML/PNG时,时间轴和PNG数量没有问题,但是一部分PNG图片是"空白"的,对应日志提示为"WARNING: Invalid image size - ignored",若你有时间看到此评论后回复邮件以便提供样本字幕。