1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.text.pgs; 17 18 import android.graphics.Bitmap; 19 import androidx.annotation.Nullable; 20 import com.google.android.exoplayer2.text.Cue; 21 import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; 22 import com.google.android.exoplayer2.text.Subtitle; 23 import com.google.android.exoplayer2.text.SubtitleDecoderException; 24 import com.google.android.exoplayer2.util.ParsableByteArray; 25 import com.google.android.exoplayer2.util.Util; 26 import java.util.ArrayList; 27 import java.util.Arrays; 28 import java.util.Collections; 29 import java.util.zip.Inflater; 30 31 /** A {@link SimpleSubtitleDecoder} for PGS subtitles. */ 32 public final class PgsDecoder extends SimpleSubtitleDecoder { 33 34 private static final int SECTION_TYPE_PALETTE = 0x14; 35 private static final int SECTION_TYPE_BITMAP_PICTURE = 0x15; 36 private static final int SECTION_TYPE_IDENTIFIER = 0x16; 37 private static final int SECTION_TYPE_END = 0x80; 38 39 private static final byte INFLATE_HEADER = 0x78; 40 41 private final ParsableByteArray buffer; 42 private final ParsableByteArray inflatedBuffer; 43 private final CueBuilder cueBuilder; 44 45 @Nullable private Inflater inflater; 46 PgsDecoder()47 public PgsDecoder() { 48 super("PgsDecoder"); 49 buffer = new ParsableByteArray(); 50 inflatedBuffer = new ParsableByteArray(); 51 cueBuilder = new CueBuilder(); 52 } 53 54 @Override decode(byte[] data, int size, boolean reset)55 protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { 56 buffer.reset(data, size); 57 maybeInflateData(buffer); 58 cueBuilder.reset(); 59 ArrayList<Cue> cues = new ArrayList<>(); 60 while (buffer.bytesLeft() >= 3) { 61 Cue cue = readNextSection(buffer, cueBuilder); 62 if (cue != null) { 63 cues.add(cue); 64 } 65 } 66 return new PgsSubtitle(Collections.unmodifiableList(cues)); 67 } 68 maybeInflateData(ParsableByteArray buffer)69 private void maybeInflateData(ParsableByteArray buffer) { 70 if (buffer.bytesLeft() > 0 && buffer.peekUnsignedByte() == INFLATE_HEADER) { 71 if (inflater == null) { 72 inflater = new Inflater(); 73 } 74 if (Util.inflate(buffer, inflatedBuffer, inflater)) { 75 buffer.reset(inflatedBuffer.data, inflatedBuffer.limit()); 76 } // else assume data is not compressed. 77 } 78 } 79 80 @Nullable readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder)81 private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { 82 int limit = buffer.limit(); 83 int sectionType = buffer.readUnsignedByte(); 84 int sectionLength = buffer.readUnsignedShort(); 85 86 int nextSectionPosition = buffer.getPosition() + sectionLength; 87 if (nextSectionPosition > limit) { 88 buffer.setPosition(limit); 89 return null; 90 } 91 92 Cue cue = null; 93 switch (sectionType) { 94 case SECTION_TYPE_PALETTE: 95 cueBuilder.parsePaletteSection(buffer, sectionLength); 96 break; 97 case SECTION_TYPE_BITMAP_PICTURE: 98 cueBuilder.parseBitmapSection(buffer, sectionLength); 99 break; 100 case SECTION_TYPE_IDENTIFIER: 101 cueBuilder.parseIdentifierSection(buffer, sectionLength); 102 break; 103 case SECTION_TYPE_END: 104 cue = cueBuilder.build(); 105 cueBuilder.reset(); 106 break; 107 default: 108 break; 109 } 110 111 buffer.setPosition(nextSectionPosition); 112 return cue; 113 } 114 115 private static final class CueBuilder { 116 117 private final ParsableByteArray bitmapData; 118 private final int[] colors; 119 120 private boolean colorsSet; 121 private int planeWidth; 122 private int planeHeight; 123 private int bitmapX; 124 private int bitmapY; 125 private int bitmapWidth; 126 private int bitmapHeight; 127 CueBuilder()128 public CueBuilder() { 129 bitmapData = new ParsableByteArray(); 130 colors = new int[256]; 131 } 132 parsePaletteSection(ParsableByteArray buffer, int sectionLength)133 private void parsePaletteSection(ParsableByteArray buffer, int sectionLength) { 134 if ((sectionLength % 5) != 2) { 135 // Section must be two bytes followed by a whole number of (index, y, cb, cr, a) entries. 136 return; 137 } 138 buffer.skipBytes(2); 139 140 Arrays.fill(colors, 0); 141 int entryCount = sectionLength / 5; 142 for (int i = 0; i < entryCount; i++) { 143 int index = buffer.readUnsignedByte(); 144 int y = buffer.readUnsignedByte(); 145 int cr = buffer.readUnsignedByte(); 146 int cb = buffer.readUnsignedByte(); 147 int a = buffer.readUnsignedByte(); 148 int r = (int) (y + (1.40200 * (cr - 128))); 149 int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128))); 150 int b = (int) (y + (1.77200 * (cb - 128))); 151 colors[index] = 152 (a << 24) 153 | (Util.constrainValue(r, 0, 255) << 16) 154 | (Util.constrainValue(g, 0, 255) << 8) 155 | Util.constrainValue(b, 0, 255); 156 } 157 colorsSet = true; 158 } 159 parseBitmapSection(ParsableByteArray buffer, int sectionLength)160 private void parseBitmapSection(ParsableByteArray buffer, int sectionLength) { 161 if (sectionLength < 4) { 162 return; 163 } 164 buffer.skipBytes(3); // Id (2 bytes), version (1 byte). 165 boolean isBaseSection = (0x80 & buffer.readUnsignedByte()) != 0; 166 sectionLength -= 4; 167 168 if (isBaseSection) { 169 if (sectionLength < 7) { 170 return; 171 } 172 int totalLength = buffer.readUnsignedInt24(); 173 if (totalLength < 4) { 174 return; 175 } 176 bitmapWidth = buffer.readUnsignedShort(); 177 bitmapHeight = buffer.readUnsignedShort(); 178 bitmapData.reset(totalLength - 4); 179 sectionLength -= 7; 180 } 181 182 int position = bitmapData.getPosition(); 183 int limit = bitmapData.limit(); 184 if (position < limit && sectionLength > 0) { 185 int bytesToRead = Math.min(sectionLength, limit - position); 186 buffer.readBytes(bitmapData.data, position, bytesToRead); 187 bitmapData.setPosition(position + bytesToRead); 188 } 189 } 190 parseIdentifierSection(ParsableByteArray buffer, int sectionLength)191 private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) { 192 if (sectionLength < 19) { 193 return; 194 } 195 planeWidth = buffer.readUnsignedShort(); 196 planeHeight = buffer.readUnsignedShort(); 197 buffer.skipBytes(11); 198 bitmapX = buffer.readUnsignedShort(); 199 bitmapY = buffer.readUnsignedShort(); 200 } 201 202 @Nullable build()203 public Cue build() { 204 if (planeWidth == 0 205 || planeHeight == 0 206 || bitmapWidth == 0 207 || bitmapHeight == 0 208 || bitmapData.limit() == 0 209 || bitmapData.getPosition() != bitmapData.limit() 210 || !colorsSet) { 211 return null; 212 } 213 // Build the bitmapData. 214 bitmapData.setPosition(0); 215 int[] argbBitmapData = new int[bitmapWidth * bitmapHeight]; 216 int argbBitmapDataIndex = 0; 217 while (argbBitmapDataIndex < argbBitmapData.length) { 218 int colorIndex = bitmapData.readUnsignedByte(); 219 if (colorIndex != 0) { 220 argbBitmapData[argbBitmapDataIndex++] = colors[colorIndex]; 221 } else { 222 int switchBits = bitmapData.readUnsignedByte(); 223 if (switchBits != 0) { 224 int runLength = 225 (switchBits & 0x40) == 0 226 ? (switchBits & 0x3F) 227 : (((switchBits & 0x3F) << 8) | bitmapData.readUnsignedByte()); 228 int color = (switchBits & 0x80) == 0 ? 0 : colors[bitmapData.readUnsignedByte()]; 229 Arrays.fill( 230 argbBitmapData, argbBitmapDataIndex, argbBitmapDataIndex + runLength, color); 231 argbBitmapDataIndex += runLength; 232 } 233 } 234 } 235 Bitmap bitmap = 236 Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); 237 // Build the cue. 238 return new Cue.Builder() 239 .setBitmap(bitmap) 240 .setPosition((float) bitmapX / planeWidth) 241 .setPositionAnchor(Cue.ANCHOR_TYPE_START) 242 .setLine((float) bitmapY / planeHeight, Cue.LINE_TYPE_FRACTION) 243 .setLineAnchor(Cue.ANCHOR_TYPE_START) 244 .setSize((float) bitmapWidth / planeWidth) 245 .setBitmapHeight((float) bitmapHeight / planeHeight) 246 .build(); 247 } 248 reset()249 public void reset() { 250 planeWidth = 0; 251 planeHeight = 0; 252 bitmapX = 0; 253 bitmapY = 0; 254 bitmapWidth = 0; 255 bitmapHeight = 0; 256 bitmapData.reset(0); 257 colorsSet = false; 258 } 259 } 260 } 261