• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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