blob: 1d5ec8454c47e5563c66acd4dcadf593a2c908ba [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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 com.google.android.exoplayer2.text.dvb;
import static java.lang.Math.min;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Parses {@link Cue}s from a DVB subtitle bitstream. */
/* package */ final class DvbParser {
private static final String TAG = "DvbParser";
// Segment types, as defined by ETSI EN 300 743 Table 2
private static final int SEGMENT_TYPE_PAGE_COMPOSITION = 0x10;
private static final int SEGMENT_TYPE_REGION_COMPOSITION = 0x11;
private static final int SEGMENT_TYPE_CLUT_DEFINITION = 0x12;
private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13;
private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14;
// Page states, as defined by ETSI EN 300 743 Table 3
private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements.
// private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements.
// private static final int PAGE_STATE_CHANGE = 2; // New. All elements.
// Region depths, as defined by ETSI EN 300 743 Table 5
// private static final int REGION_DEPTH_2_BIT = 1;
private static final int REGION_DEPTH_4_BIT = 2;
private static final int REGION_DEPTH_8_BIT = 3;
// Object codings, as defined by ETSI EN 300 743 Table 8
private static final int OBJECT_CODING_PIXELS = 0;
private static final int OBJECT_CODING_STRING = 1;
// Pixel-data types, as defined by ETSI EN 300 743 Table 9
private static final int DATA_TYPE_2BP_CODE_STRING = 0x10;
private static final int DATA_TYPE_4BP_CODE_STRING = 0x11;
private static final int DATA_TYPE_8BP_CODE_STRING = 0x12;
private static final int DATA_TYPE_24_TABLE_DATA = 0x20;
private static final int DATA_TYPE_28_TABLE_DATA = 0x21;
private static final int DATA_TYPE_48_TABLE_DATA = 0x22;
private static final int DATA_TYPE_END_LINE = 0xF0;
// Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6
private static final byte[] defaultMap2To4 = {(byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F};
private static final byte[] defaultMap2To8 = {(byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF};
private static final byte[] defaultMap4To8 = {
(byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,
(byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77,
(byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB,
(byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF
};
private final Paint defaultPaint;
private final Paint fillRegionPaint;
private final Canvas canvas;
private final DisplayDefinition defaultDisplayDefinition;
private final ClutDefinition defaultClutDefinition;
private final SubtitleService subtitleService;
private @MonotonicNonNull Bitmap bitmap;
/**
* Construct an instance for the given subtitle and ancillary page ids.
*
* @param subtitlePageId The id of the subtitle page carrying the subtitle to be parsed.
* @param ancillaryPageId The id of the ancillary page containing additional data.
*/
public DvbParser(int subtitlePageId, int ancillaryPageId) {
defaultPaint = new Paint();
defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE);
defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
defaultPaint.setPathEffect(null);
fillRegionPaint = new Paint();
fillRegionPaint.setStyle(Paint.Style.FILL);
fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
fillRegionPaint.setPathEffect(null);
canvas = new Canvas();
defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575);
defaultClutDefinition =
new ClutDefinition(
0,
generateDefault2BitClutEntries(),
generateDefault4BitClutEntries(),
generateDefault8BitClutEntries());
subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId);
}
/** Resets the parser. */
public void reset() {
subtitleService.reset();
}
/**
* Decodes a subtitling packet, returning a list of parsed {@link Cue}s.
*
* @param data The subtitling packet data to decode.
* @param limit The limit in {@code data} at which to stop decoding.
* @return The parsed {@link Cue}s.
*/
public List<Cue> decode(byte[] data, int limit) {
// Parse the input data.
ParsableBitArray dataBitArray = new ParsableBitArray(data, limit);
while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40)
&& dataBitArray.readBits(8) == 0x0F) {
parseSubtitlingSegment(dataBitArray, subtitleService);
}
@Nullable PageComposition pageComposition = subtitleService.pageComposition;
if (pageComposition == null) {
return Collections.emptyList();
}
// Update the canvas bitmap if necessary.
DisplayDefinition displayDefinition =
subtitleService.displayDefinition != null
? subtitleService.displayDefinition
: defaultDisplayDefinition;
if (bitmap == null
|| displayDefinition.width + 1 != bitmap.getWidth()
|| displayDefinition.height + 1 != bitmap.getHeight()) {
bitmap =
Bitmap.createBitmap(
displayDefinition.width + 1, displayDefinition.height + 1, Bitmap.Config.ARGB_8888);
canvas.setBitmap(bitmap);
}
// Build the cues.
List<Cue> cues = new ArrayList<>();
SparseArray<PageRegion> pageRegions = pageComposition.regions;
for (int i = 0; i < pageRegions.size(); i++) {
// Save clean clipping state.
canvas.save();
PageRegion pageRegion = pageRegions.valueAt(i);
int regionId = pageRegions.keyAt(i);
RegionComposition regionComposition = subtitleService.regions.get(regionId);
// Clip drawing to the current region and display definition window.
int baseHorizontalAddress =
pageRegion.horizontalAddress + displayDefinition.horizontalPositionMinimum;
int baseVerticalAddress =
pageRegion.verticalAddress + displayDefinition.verticalPositionMinimum;
int clipRight =
min(
baseHorizontalAddress + regionComposition.width,
displayDefinition.horizontalPositionMaximum);
int clipBottom =
min(
baseVerticalAddress + regionComposition.height,
displayDefinition.verticalPositionMaximum);
canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom);
ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId);
if (clutDefinition == null) {
clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId);
if (clutDefinition == null) {
clutDefinition = defaultClutDefinition;
}
}
SparseArray<RegionObject> regionObjects = regionComposition.regionObjects;
for (int j = 0; j < regionObjects.size(); j++) {
int objectId = regionObjects.keyAt(j);
RegionObject regionObject = regionObjects.valueAt(j);
ObjectData objectData = subtitleService.objects.get(objectId);
if (objectData == null) {
objectData = subtitleService.ancillaryObjects.get(objectId);
}
if (objectData != null) {
@Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint;
paintPixelDataSubBlocks(
objectData,
clutDefinition,
regionComposition.depth,
baseHorizontalAddress + regionObject.horizontalPosition,
baseVerticalAddress + regionObject.verticalPosition,
paint,
canvas);
}
}
if (regionComposition.fillFlag) {
int color;
if (regionComposition.depth == REGION_DEPTH_8_BIT) {
color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit];
} else if (regionComposition.depth == REGION_DEPTH_4_BIT) {
color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit];
} else {
color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit];
}
fillRegionPaint.setColor(color);
canvas.drawRect(
baseHorizontalAddress,
baseVerticalAddress,
baseHorizontalAddress + regionComposition.width,
baseVerticalAddress + regionComposition.height,
fillRegionPaint);
}
cues.add(
new Cue.Builder()
.setBitmap(
Bitmap.createBitmap(
bitmap,
baseHorizontalAddress,
baseVerticalAddress,
regionComposition.width,
regionComposition.height))
.setPosition((float) baseHorizontalAddress / displayDefinition.width)
.setPositionAnchor(Cue.ANCHOR_TYPE_START)
.setLine(
(float) baseVerticalAddress / displayDefinition.height, Cue.LINE_TYPE_FRACTION)
.setLineAnchor(Cue.ANCHOR_TYPE_START)
.setSize((float) regionComposition.width / displayDefinition.width)
.setBitmapHeight((float) regionComposition.height / displayDefinition.height)
.build());
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// Restore clean clipping state.
canvas.restore();
}
return Collections.unmodifiableList(cues);
}
// Static parsing.
/**
* Parses a subtitling segment, as defined by ETSI EN 300 743 7.2
*
* <p>The {@link SubtitleService} is updated with the parsed segment data.
*/
private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) {
int segmentType = data.readBits(8);
int pageId = data.readBits(16);
int dataFieldLength = data.readBits(16);
int dataFieldLimit = data.getBytePosition() + dataFieldLength;
if ((dataFieldLength * 8) > data.bitsLeft()) {
Log.w(TAG, "Data field length exceeds limit");
// Skip to the very end.
data.skipBits(data.bitsLeft());
return;
}
switch (segmentType) {
case SEGMENT_TYPE_DISPLAY_DEFINITION:
if (pageId == service.subtitlePageId) {
service.displayDefinition = parseDisplayDefinition(data);
}
break;
case SEGMENT_TYPE_PAGE_COMPOSITION:
if (pageId == service.subtitlePageId) {
@Nullable PageComposition current = service.pageComposition;
PageComposition pageComposition = parsePageComposition(data, dataFieldLength);
if (pageComposition.state != PAGE_STATE_NORMAL) {
service.pageComposition = pageComposition;
service.regions.clear();
service.cluts.clear();
service.objects.clear();
} else if (current != null && current.version != pageComposition.version) {
service.pageComposition = pageComposition;
}
}
break;
case SEGMENT_TYPE_REGION_COMPOSITION:
@Nullable PageComposition pageComposition = service.pageComposition;
if (pageId == service.subtitlePageId && pageComposition != null) {
RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength);
if (pageComposition.state == PAGE_STATE_NORMAL) {
@Nullable
RegionComposition existingRegionComposition = service.regions.get(regionComposition.id);
if (existingRegionComposition != null) {
regionComposition.mergeFrom(existingRegionComposition);
}
}
service.regions.put(regionComposition.id, regionComposition);
}
break;
case SEGMENT_TYPE_CLUT_DEFINITION:
if (pageId == service.subtitlePageId) {
ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);
service.cluts.put(clutDefinition.id, clutDefinition);
} else if (pageId == service.ancillaryPageId) {
ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);
service.ancillaryCluts.put(clutDefinition.id, clutDefinition);
}
break;
case SEGMENT_TYPE_OBJECT_DATA:
if (pageId == service.subtitlePageId) {
ObjectData objectData = parseObjectData(data);
service.objects.put(objectData.id, objectData);
} else if (pageId == service.ancillaryPageId) {
ObjectData objectData = parseObjectData(data);
service.ancillaryObjects.put(objectData.id, objectData);
}
break;
default:
// Do nothing.
break;
}
// Skip to the next segment.
data.skipBytes(dataFieldLimit - data.getBytePosition());
}
/** Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1. */
private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) {
data.skipBits(4); // dds_version_number (4).
boolean displayWindowFlag = data.readBit();
data.skipBits(3); // Skip reserved.
int width = data.readBits(16);
int height = data.readBits(16);
int horizontalPositionMinimum;
int horizontalPositionMaximum;
int verticalPositionMinimum;
int verticalPositionMaximum;
if (displayWindowFlag) {
horizontalPositionMinimum = data.readBits(16);
horizontalPositionMaximum = data.readBits(16);
verticalPositionMinimum = data.readBits(16);
verticalPositionMaximum = data.readBits(16);
} else {
horizontalPositionMinimum = 0;
horizontalPositionMaximum = width;
verticalPositionMinimum = 0;
verticalPositionMaximum = height;
}
return new DisplayDefinition(
width,
height,
horizontalPositionMinimum,
horizontalPositionMaximum,
verticalPositionMinimum,
verticalPositionMaximum);
}
/** Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2. */
private static PageComposition parsePageComposition(ParsableBitArray data, int length) {
int timeoutSecs = data.readBits(8);
int version = data.readBits(4);
int state = data.readBits(2);
data.skipBits(2);
int remainingLength = length - 2;
SparseArray<PageRegion> regions = new SparseArray<>();
while (remainingLength > 0) {
int regionId = data.readBits(8);
data.skipBits(8); // Skip reserved.
int regionHorizontalAddress = data.readBits(16);
int regionVerticalAddress = data.readBits(16);
remainingLength -= 6;
regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress));
}
return new PageComposition(timeoutSecs, version, state, regions);
}
/** Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3. */
private static RegionComposition parseRegionComposition(ParsableBitArray data, int length) {
int id = data.readBits(8);
data.skipBits(4); // Skip region_version_number
boolean fillFlag = data.readBit();
data.skipBits(3); // Skip reserved.
int width = data.readBits(16);
int height = data.readBits(16);
int levelOfCompatibility = data.readBits(3);
int depth = data.readBits(3);
data.skipBits(2); // Skip reserved.
int clutId = data.readBits(8);
int pixelCode8Bit = data.readBits(8);
int pixelCode4Bit = data.readBits(4);
int pixelCode2Bit = data.readBits(2);
data.skipBits(2); // Skip reserved
int remainingLength = length - 10;
SparseArray<RegionObject> regionObjects = new SparseArray<>();
while (remainingLength > 0) {
int objectId = data.readBits(16);
int objectType = data.readBits(2);
int objectProvider = data.readBits(2);
int objectHorizontalPosition = data.readBits(12);
data.skipBits(4); // Skip reserved.
int objectVerticalPosition = data.readBits(12);
remainingLength -= 6;
int foregroundPixelCode = 0;
int backgroundPixelCode = 0;
if (objectType == 0x01 || objectType == 0x02) { // Only seems to affect to char subtitles.
foregroundPixelCode = data.readBits(8);
backgroundPixelCode = data.readBits(8);
remainingLength -= 2;
}
regionObjects.put(
objectId,
new RegionObject(
objectType,
objectProvider,
objectHorizontalPosition,
objectVerticalPosition,
foregroundPixelCode,
backgroundPixelCode));
}
return new RegionComposition(
id,
fillFlag,
width,
height,
levelOfCompatibility,
depth,
clutId,
pixelCode8Bit,
pixelCode4Bit,
pixelCode2Bit,
regionObjects);
}
/** Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4. */
private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) {
int clutId = data.readBits(8);
data.skipBits(8); // Skip clut_version_number (4), reserved (4)
int remainingLength = length - 2;
int[] clutEntries2Bit = generateDefault2BitClutEntries();
int[] clutEntries4Bit = generateDefault4BitClutEntries();
int[] clutEntries8Bit = generateDefault8BitClutEntries();
while (remainingLength > 0) {
int entryId = data.readBits(8);
int entryFlags = data.readBits(8);
remainingLength -= 2;
int[] clutEntries;
if ((entryFlags & 0x80) != 0) {
clutEntries = clutEntries2Bit;
} else if ((entryFlags & 0x40) != 0) {
clutEntries = clutEntries4Bit;
} else {
clutEntries = clutEntries8Bit;
}
int y;
int cr;
int cb;
int t;
if ((entryFlags & 0x01) != 0) {
y = data.readBits(8);
cr = data.readBits(8);
cb = data.readBits(8);
t = data.readBits(8);
remainingLength -= 4;
} else {
y = data.readBits(6) << 2;
cr = data.readBits(4) << 4;
cb = data.readBits(4) << 4;
t = data.readBits(2) << 6;
remainingLength -= 2;
}
if (y == 0x00) {
cr = 0x00;
cb = 0x00;
t = 0xFF;
}
int a = (byte) (0xFF - (t & 0xFF));
int r = (int) (y + (1.40200 * (cr - 128)));
int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128)));
int b = (int) (y + (1.77200 * (cb - 128)));
clutEntries[entryId] =
getColor(
a,
Util.constrainValue(r, 0, 255),
Util.constrainValue(g, 0, 255),
Util.constrainValue(b, 0, 255));
}
return new ClutDefinition(clutId, clutEntries2Bit, clutEntries4Bit, clutEntries8Bit);
}
/**
* Parses an object data segment, as defined by ETSI EN 300 743 7.2.5.
*
* @return The parsed object data.
*/
private static ObjectData parseObjectData(ParsableBitArray data) {
int objectId = data.readBits(16);
data.skipBits(4); // Skip object_version_number
int objectCodingMethod = data.readBits(2);
boolean nonModifyingColorFlag = data.readBit();
data.skipBits(1); // Skip reserved.
byte[] topFieldData = Util.EMPTY_BYTE_ARRAY;
byte[] bottomFieldData = Util.EMPTY_BYTE_ARRAY;
if (objectCodingMethod == OBJECT_CODING_STRING) {
int numberOfCodes = data.readBits(8);
// TODO: Parse and use character_codes.
data.skipBits(numberOfCodes * 16); // Skip character_codes.
} else if (objectCodingMethod == OBJECT_CODING_PIXELS) {
int topFieldDataLength = data.readBits(16);
int bottomFieldDataLength = data.readBits(16);
if (topFieldDataLength > 0) {
topFieldData = new byte[topFieldDataLength];
data.readBytes(topFieldData, 0, topFieldDataLength);
}
if (bottomFieldDataLength > 0) {
bottomFieldData = new byte[bottomFieldDataLength];
data.readBytes(bottomFieldData, 0, bottomFieldDataLength);
} else {
bottomFieldData = topFieldData;
}
}
return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData);
}
private static int[] generateDefault2BitClutEntries() {
int[] entries = new int[4];
entries[0] = 0x00000000;
entries[1] = 0xFFFFFFFF;
entries[2] = 0xFF000000;
entries[3] = 0xFF7F7F7F;
return entries;
}
private static int[] generateDefault4BitClutEntries() {
int[] entries = new int[16];
entries[0] = 0x00000000;
for (int i = 1; i < entries.length; i++) {
if (i < 8) {
entries[i] =
getColor(
0xFF,
((i & 0x01) != 0 ? 0xFF : 0x00),
((i & 0x02) != 0 ? 0xFF : 0x00),
((i & 0x04) != 0 ? 0xFF : 0x00));
} else {
entries[i] =
getColor(
0xFF,
((i & 0x01) != 0 ? 0x7F : 0x00),
((i & 0x02) != 0 ? 0x7F : 0x00),
((i & 0x04) != 0 ? 0x7F : 0x00));
}
}
return entries;
}
private static int[] generateDefault8BitClutEntries() {
int[] entries = new int[256];
entries[0] = 0x00000000;
for (int i = 0; i < entries.length; i++) {
if (i < 8) {
entries[i] =
getColor(
0x3F,
((i & 0x01) != 0 ? 0xFF : 0x00),
((i & 0x02) != 0 ? 0xFF : 0x00),
((i & 0x04) != 0 ? 0xFF : 0x00));
} else {
switch (i & 0x88) {
case 0x00:
entries[i] =
getColor(
0xFF,
(((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
(((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
(((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));
break;
case 0x08:
entries[i] =
getColor(
0x7F,
(((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
(((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
(((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));
break;
case 0x80:
entries[i] =
getColor(
0xFF,
(127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
(127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
(127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));
break;
case 0x88:
entries[i] =
getColor(
0xFF,
(((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
(((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
(((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));
break;
}
}
}
return entries;
}
private static int getColor(int a, int r, int g, int b) {
return (a << 24) | (r << 16) | (g << 8) | b;
}
// Static drawing.
/** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */
private static void paintPixelDataSubBlocks(
ObjectData objectData,
ClutDefinition clutDefinition,
int regionDepth,
int horizontalAddress,
int verticalAddress,
@Nullable Paint paint,
Canvas canvas) {
int[] clutEntries;
if (regionDepth == REGION_DEPTH_8_BIT) {
clutEntries = clutDefinition.clutEntries8Bit;
} else if (regionDepth == REGION_DEPTH_4_BIT) {
clutEntries = clutDefinition.clutEntries4Bit;
} else {
clutEntries = clutDefinition.clutEntries2Bit;
}
paintPixelDataSubBlock(
objectData.topFieldData,
clutEntries,
regionDepth,
horizontalAddress,
verticalAddress,
paint,
canvas);
paintPixelDataSubBlock(
objectData.bottomFieldData,
clutEntries,
regionDepth,
horizontalAddress,
verticalAddress + 1,
paint,
canvas);
}
/** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */
private static void paintPixelDataSubBlock(
byte[] pixelData,
int[] clutEntries,
int regionDepth,
int horizontalAddress,
int verticalAddress,
@Nullable Paint paint,
Canvas canvas) {
ParsableBitArray data = new ParsableBitArray(pixelData);
int column = horizontalAddress;
int line = verticalAddress;
@Nullable byte[] clutMapTable2To4 = null;
@Nullable byte[] clutMapTable2To8 = null;
@Nullable byte[] clutMapTable4To8 = null;
while (data.bitsLeft() != 0) {
int dataType = data.readBits(8);
switch (dataType) {
case DATA_TYPE_2BP_CODE_STRING:
@Nullable byte[] clutMapTable2ToX;
if (regionDepth == REGION_DEPTH_8_BIT) {
clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8;
} else if (regionDepth == REGION_DEPTH_4_BIT) {
clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4;
} else {
clutMapTable2ToX = null;
}
column =
paint2BitPixelCodeString(
data, clutEntries, clutMapTable2ToX, column, line, paint, canvas);
data.byteAlign();
break;
case DATA_TYPE_4BP_CODE_STRING:
@Nullable byte[] clutMapTable4ToX;
if (regionDepth == REGION_DEPTH_8_BIT) {
clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8;
} else {
clutMapTable4ToX = null;
}
column =
paint4BitPixelCodeString(
data, clutEntries, clutMapTable4ToX, column, line, paint, canvas);
data.byteAlign();
break;
case DATA_TYPE_8BP_CODE_STRING:
column =
paint8BitPixelCodeString(
data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas);
break;
case DATA_TYPE_24_TABLE_DATA:
clutMapTable2To4 = buildClutMapTable(4, 4, data);
break;
case DATA_TYPE_28_TABLE_DATA:
clutMapTable2To8 = buildClutMapTable(4, 8, data);
break;
case DATA_TYPE_48_TABLE_DATA:
clutMapTable4To8 = buildClutMapTable(16, 8, data);
break;
case DATA_TYPE_END_LINE:
column = horizontalAddress;
line += 2;
break;
default:
// Do nothing.
break;
}
}
}
/** Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */
private static int paint2BitPixelCodeString(
ParsableBitArray data,
int[] clutEntries,
@Nullable byte[] clutMapTable,
int column,
int line,
@Nullable Paint paint,
Canvas canvas) {
boolean endOfPixelCodeString = false;
do {
int runLength = 0;
int clutIndex = 0;
int peek = data.readBits(2);
if (peek != 0x00) {
runLength = 1;
clutIndex = peek;
} else if (data.readBit()) {
runLength = 3 + data.readBits(3);
clutIndex = data.readBits(2);
} else if (data.readBit()) {
runLength = 1;
} else {
switch (data.readBits(2)) {
case 0x00:
endOfPixelCodeString = true;
break;
case 0x01:
runLength = 2;
break;
case 0x02:
runLength = 12 + data.readBits(4);
clutIndex = data.readBits(2);
break;
case 0x03:
runLength = 29 + data.readBits(8);
clutIndex = data.readBits(2);
break;
}
}
if (runLength != 0 && paint != null) {
paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
canvas.drawRect(column, line, column + runLength, line + 1, paint);
}
column += runLength;
} while (!endOfPixelCodeString);
return column;
}
/** Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */
private static int paint4BitPixelCodeString(
ParsableBitArray data,
int[] clutEntries,
@Nullable byte[] clutMapTable,
int column,
int line,
@Nullable Paint paint,
Canvas canvas) {
boolean endOfPixelCodeString = false;
do {
int runLength = 0;
int clutIndex = 0;
int peek = data.readBits(4);
if (peek != 0x00) {
runLength = 1;
clutIndex = peek;
} else if (!data.readBit()) {
peek = data.readBits(3);
if (peek != 0x00) {
runLength = 2 + peek;
clutIndex = 0x00;
} else {
endOfPixelCodeString = true;
}
} else if (!data.readBit()) {
runLength = 4 + data.readBits(2);
clutIndex = data.readBits(4);
} else {
switch (data.readBits(2)) {
case 0x00:
runLength = 1;
break;
case 0x01:
runLength = 2;
break;
case 0x02:
runLength = 9 + data.readBits(4);
clutIndex = data.readBits(4);
break;
case 0x03:
runLength = 25 + data.readBits(8);
clutIndex = data.readBits(4);
break;
}
}
if (runLength != 0 && paint != null) {
paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
canvas.drawRect(column, line, column + runLength, line + 1, paint);
}
column += runLength;
} while (!endOfPixelCodeString);
return column;
}
/** Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */
private static int paint8BitPixelCodeString(
ParsableBitArray data,
int[] clutEntries,
@Nullable byte[] clutMapTable,
int column,
int line,
@Nullable Paint paint,
Canvas canvas) {
boolean endOfPixelCodeString = false;
do {
int runLength = 0;
int clutIndex = 0;
int peek = data.readBits(8);
if (peek != 0x00) {
runLength = 1;
clutIndex = peek;
} else {
if (!data.readBit()) {
peek = data.readBits(7);
if (peek != 0x00) {
runLength = peek;
clutIndex = 0x00;
} else {
endOfPixelCodeString = true;
}
} else {
runLength = data.readBits(7);
clutIndex = data.readBits(8);
}
}
if (runLength != 0 && paint != null) {
paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
canvas.drawRect(column, line, column + runLength, line + 1, paint);
}
column += runLength;
} while (!endOfPixelCodeString);
return column;
}
private static byte[] buildClutMapTable(int length, int bitsPerEntry, ParsableBitArray data) {
byte[] clutMapTable = new byte[length];
for (int i = 0; i < length; i++) {
clutMapTable[i] = (byte) data.readBits(bitsPerEntry);
}
return clutMapTable;
}
// Private inner classes.
/** The subtitle service definition. */
private static final class SubtitleService {
public final int subtitlePageId;
public final int ancillaryPageId;
public final SparseArray<RegionComposition> regions;
public final SparseArray<ClutDefinition> cluts;
public final SparseArray<ObjectData> objects;
public final SparseArray<ClutDefinition> ancillaryCluts;
public final SparseArray<ObjectData> ancillaryObjects;
@Nullable public DisplayDefinition displayDefinition;
@Nullable public PageComposition pageComposition;
public SubtitleService(int subtitlePageId, int ancillaryPageId) {
this.subtitlePageId = subtitlePageId;
this.ancillaryPageId = ancillaryPageId;
regions = new SparseArray<>();
cluts = new SparseArray<>();
objects = new SparseArray<>();
ancillaryCluts = new SparseArray<>();
ancillaryObjects = new SparseArray<>();
}
public void reset() {
regions.clear();
cluts.clear();
objects.clear();
ancillaryCluts.clear();
ancillaryObjects.clear();
displayDefinition = null;
pageComposition = null;
}
}
/**
* Contains the geometry and active area of the subtitle service.
*
* <p>See ETSI EN 300 743 7.2.1
*/
private static final class DisplayDefinition {
public final int width;
public final int height;
public final int horizontalPositionMinimum;
public final int horizontalPositionMaximum;
public final int verticalPositionMinimum;
public final int verticalPositionMaximum;
public DisplayDefinition(
int width,
int height,
int horizontalPositionMinimum,
int horizontalPositionMaximum,
int verticalPositionMinimum,
int verticalPositionMaximum) {
this.width = width;
this.height = height;
this.horizontalPositionMinimum = horizontalPositionMinimum;
this.horizontalPositionMaximum = horizontalPositionMaximum;
this.verticalPositionMinimum = verticalPositionMinimum;
this.verticalPositionMaximum = verticalPositionMaximum;
}
}
/**
* The page is the definition and arrangement of regions in the screen.
*
* <p>See ETSI EN 300 743 7.2.2
*/
private static final class PageComposition {
public final int timeOutSecs; // TODO: Use this or remove it.
public final int version;
public final int state;
public final SparseArray<PageRegion> regions;
public PageComposition(
int timeoutSecs, int version, int state, SparseArray<PageRegion> regions) {
this.timeOutSecs = timeoutSecs;
this.version = version;
this.state = state;
this.regions = regions;
}
}
/**
* A region within a {@link PageComposition}.
*
* <p>See ETSI EN 300 743 7.2.2
*/
private static final class PageRegion {
public final int horizontalAddress;
public final int verticalAddress;
public PageRegion(int horizontalAddress, int verticalAddress) {
this.horizontalAddress = horizontalAddress;
this.verticalAddress = verticalAddress;
}
}
/**
* An area of the page composed of a list of objects and a CLUT.
*
* <p>See ETSI EN 300 743 7.2.3
*/
private static final class RegionComposition {
public final int id;
public final boolean fillFlag;
public final int width;
public final int height;
public final int levelOfCompatibility; // TODO: Use this or remove it.
public final int depth;
public final int clutId;
public final int pixelCode8Bit;
public final int pixelCode4Bit;
public final int pixelCode2Bit;
public final SparseArray<RegionObject> regionObjects;
public RegionComposition(
int id,
boolean fillFlag,
int width,
int height,
int levelOfCompatibility,
int depth,
int clutId,
int pixelCode8Bit,
int pixelCode4Bit,
int pixelCode2Bit,
SparseArray<RegionObject> regionObjects) {
this.id = id;
this.fillFlag = fillFlag;
this.width = width;
this.height = height;
this.levelOfCompatibility = levelOfCompatibility;
this.depth = depth;
this.clutId = clutId;
this.pixelCode8Bit = pixelCode8Bit;
this.pixelCode4Bit = pixelCode4Bit;
this.pixelCode2Bit = pixelCode2Bit;
this.regionObjects = regionObjects;
}
public void mergeFrom(RegionComposition otherRegionComposition) {
SparseArray<RegionObject> otherRegionObjects = otherRegionComposition.regionObjects;
for (int i = 0; i < otherRegionObjects.size(); i++) {
regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i));
}
}
}
/**
* An object within a {@link RegionComposition}.
*
* <p>See ETSI EN 300 743 7.2.3
*/
private static final class RegionObject {
public final int type; // TODO: Use this or remove it.
public final int provider; // TODO: Use this or remove it.
public final int horizontalPosition;
public final int verticalPosition;
public final int foregroundPixelCode; // TODO: Use this or remove it.
public final int backgroundPixelCode; // TODO: Use this or remove it.
public RegionObject(
int type,
int provider,
int horizontalPosition,
int verticalPosition,
int foregroundPixelCode,
int backgroundPixelCode) {
this.type = type;
this.provider = provider;
this.horizontalPosition = horizontalPosition;
this.verticalPosition = verticalPosition;
this.foregroundPixelCode = foregroundPixelCode;
this.backgroundPixelCode = backgroundPixelCode;
}
}
/**
* CLUT family definition containing the color tables for the three bit depths defined
*
* <p>See ETSI EN 300 743 7.2.4
*/
private static final class ClutDefinition {
public final int id;
public final int[] clutEntries2Bit;
public final int[] clutEntries4Bit;
public final int[] clutEntries8Bit;
public ClutDefinition(
int id, int[] clutEntries2Bit, int[] clutEntries4Bit, int[] clutEntries8bit) {
this.id = id;
this.clutEntries2Bit = clutEntries2Bit;
this.clutEntries4Bit = clutEntries4Bit;
this.clutEntries8Bit = clutEntries8bit;
}
}
/**
* The textual or graphical representation of an object.
*
* <p>See ETSI EN 300 743 7.2.5
*/
private static final class ObjectData {
public final int id;
public final boolean nonModifyingColorFlag;
public final byte[] topFieldData;
public final byte[] bottomFieldData;
public ObjectData(
int id, boolean nonModifyingColorFlag, byte[] topFieldData, byte[] bottomFieldData) {
this.id = id;
this.nonModifyingColorFlag = nonModifyingColorFlag;
this.topFieldData = topFieldData;
this.bottomFieldData = bottomFieldData;
}
}
}