1/* 2 * Copyright 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 17package androidx.media.subtitle; 18 19import android.text.Spannable; 20import android.text.SpannableStringBuilder; 21import android.text.TextPaint; 22import android.text.style.CharacterStyle; 23import android.text.style.StyleSpan; 24import android.text.style.UnderlineSpan; 25import android.text.style.UpdateAppearance; 26import android.util.Log; 27import android.view.accessibility.CaptioningManager.CaptionStyle; 28 29import java.util.ArrayList; 30import java.util.Arrays; 31 32/** 33 * CCParser processes CEA-608 closed caption data. 34 * 35 * It calls back into OnDisplayChangedListener upon 36 * display change with styled text for rendering. 37 * 38 */ 39class Cea608CCParser { 40 public static final int MAX_ROWS = 15; 41 public static final int MAX_COLS = 32; 42 43 private static final String TAG = "Cea608CCParser"; 44 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 45 46 private static final int INVALID = -1; 47 48 // EIA-CEA-608: Table 70 - Control Codes 49 private static final int RCL = 0x20; 50 private static final int BS = 0x21; 51 private static final int AOF = 0x22; 52 private static final int AON = 0x23; 53 private static final int DER = 0x24; 54 private static final int RU2 = 0x25; 55 private static final int RU3 = 0x26; 56 private static final int RU4 = 0x27; 57 private static final int FON = 0x28; 58 private static final int RDC = 0x29; 59 private static final int TR = 0x2a; 60 private static final int RTD = 0x2b; 61 private static final int EDM = 0x2c; 62 private static final int CR = 0x2d; 63 private static final int ENM = 0x2e; 64 private static final int EOC = 0x2f; 65 66 // Transparent Space 67 private static final char TS = '\u00A0'; 68 69 // Captioning Modes 70 private static final int MODE_UNKNOWN = 0; 71 private static final int MODE_PAINT_ON = 1; 72 private static final int MODE_ROLL_UP = 2; 73 private static final int MODE_POP_ON = 3; 74 private static final int MODE_TEXT = 4; 75 76 private final DisplayListener mListener; 77 78 private int mMode = MODE_PAINT_ON; 79 private int mRollUpSize = 4; 80 private int mPrevCtrlCode = INVALID; 81 82 private CCMemory mDisplay = new CCMemory(); 83 private CCMemory mNonDisplay = new CCMemory(); 84 private CCMemory mTextMem = new CCMemory(); 85 86 Cea608CCParser(DisplayListener listener) { 87 mListener = listener; 88 } 89 90 public void parse(byte[] data) { 91 CCData[] ccData = CCData.fromByteArray(data); 92 93 for (int i = 0; i < ccData.length; i++) { 94 if (DEBUG) { 95 Log.d(TAG, ccData[i].toString()); 96 } 97 98 if (handleCtrlCode(ccData[i]) 99 || handleTabOffsets(ccData[i]) 100 || handlePACCode(ccData[i]) 101 || handleMidRowCode(ccData[i])) { 102 continue; 103 } 104 105 handleDisplayableChars(ccData[i]); 106 } 107 } 108 109 interface DisplayListener { 110 void onDisplayChanged(SpannableStringBuilder[] styledTexts); 111 CaptionStyle getCaptionStyle(); 112 } 113 114 private CCMemory getMemory() { 115 // get the CC memory to operate on for current mode 116 switch (mMode) { 117 case MODE_POP_ON: 118 return mNonDisplay; 119 case MODE_TEXT: 120 // TODO(chz): support only caption mode for now, 121 // in text mode, dump everything to text mem. 122 return mTextMem; 123 case MODE_PAINT_ON: 124 case MODE_ROLL_UP: 125 return mDisplay; 126 default: 127 Log.w(TAG, "unrecoginized mode: " + mMode); 128 } 129 return mDisplay; 130 } 131 132 private boolean handleDisplayableChars(CCData ccData) { 133 if (!ccData.isDisplayableChar()) { 134 return false; 135 } 136 137 // Extended char includes 1 automatic backspace 138 if (ccData.isExtendedChar()) { 139 getMemory().bs(); 140 } 141 142 getMemory().writeText(ccData.getDisplayText()); 143 144 if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) { 145 updateDisplay(); 146 } 147 148 return true; 149 } 150 151 private boolean handleMidRowCode(CCData ccData) { 152 StyleCode m = ccData.getMidRow(); 153 if (m != null) { 154 getMemory().writeMidRowCode(m); 155 return true; 156 } 157 return false; 158 } 159 160 private boolean handlePACCode(CCData ccData) { 161 PAC pac = ccData.getPAC(); 162 163 if (pac != null) { 164 if (mMode == MODE_ROLL_UP) { 165 getMemory().moveBaselineTo(pac.getRow(), mRollUpSize); 166 } 167 getMemory().writePAC(pac); 168 return true; 169 } 170 171 return false; 172 } 173 174 private boolean handleTabOffsets(CCData ccData) { 175 int tabs = ccData.getTabOffset(); 176 177 if (tabs > 0) { 178 getMemory().tab(tabs); 179 return true; 180 } 181 182 return false; 183 } 184 185 private boolean handleCtrlCode(CCData ccData) { 186 int ctrlCode = ccData.getCtrlCode(); 187 188 if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) { 189 // discard double ctrl codes (but if there's a 3rd one, we still take that) 190 mPrevCtrlCode = INVALID; 191 return true; 192 } 193 194 switch(ctrlCode) { 195 case RCL: 196 // select pop-on style 197 mMode = MODE_POP_ON; 198 break; 199 case BS: 200 getMemory().bs(); 201 break; 202 case DER: 203 getMemory().der(); 204 break; 205 case RU2: 206 case RU3: 207 case RU4: 208 mRollUpSize = (ctrlCode - 0x23); 209 // erase memory if currently in other style 210 if (mMode != MODE_ROLL_UP) { 211 mDisplay.erase(); 212 mNonDisplay.erase(); 213 } 214 // select roll-up style 215 mMode = MODE_ROLL_UP; 216 break; 217 case FON: 218 Log.i(TAG, "Flash On"); 219 break; 220 case RDC: 221 // select paint-on style 222 mMode = MODE_PAINT_ON; 223 break; 224 case TR: 225 mMode = MODE_TEXT; 226 mTextMem.erase(); 227 break; 228 case RTD: 229 mMode = MODE_TEXT; 230 break; 231 case EDM: 232 // erase display memory 233 mDisplay.erase(); 234 updateDisplay(); 235 break; 236 case CR: 237 if (mMode == MODE_ROLL_UP) { 238 getMemory().rollUp(mRollUpSize); 239 } else { 240 getMemory().cr(); 241 } 242 if (mMode == MODE_ROLL_UP) { 243 updateDisplay(); 244 } 245 break; 246 case ENM: 247 // erase non-display memory 248 mNonDisplay.erase(); 249 break; 250 case EOC: 251 // swap display/non-display memory 252 swapMemory(); 253 // switch to pop-on style 254 mMode = MODE_POP_ON; 255 updateDisplay(); 256 break; 257 case INVALID: 258 default: 259 mPrevCtrlCode = INVALID; 260 return false; 261 } 262 263 mPrevCtrlCode = ctrlCode; 264 265 // handled 266 return true; 267 } 268 269 private void updateDisplay() { 270 if (mListener != null) { 271 CaptionStyle captionStyle = mListener.getCaptionStyle(); 272 mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle)); 273 } 274 } 275 276 private void swapMemory() { 277 CCMemory temp = mDisplay; 278 mDisplay = mNonDisplay; 279 mNonDisplay = temp; 280 } 281 282 private static class StyleCode { 283 static final int COLOR_WHITE = 0; 284 static final int COLOR_GREEN = 1; 285 static final int COLOR_BLUE = 2; 286 static final int COLOR_CYAN = 3; 287 static final int COLOR_RED = 4; 288 static final int COLOR_YELLOW = 5; 289 static final int COLOR_MAGENTA = 6; 290 static final int COLOR_INVALID = 7; 291 292 static final int STYLE_ITALICS = 0x00000001; 293 static final int STYLE_UNDERLINE = 0x00000002; 294 295 static final String[] sColorMap = { 296 "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID" 297 }; 298 299 final int mStyle; 300 final int mColor; 301 302 static StyleCode fromByte(byte data2) { 303 int style = 0; 304 int color = (data2 >> 1) & 0x7; 305 306 if ((data2 & 0x1) != 0) { 307 style |= STYLE_UNDERLINE; 308 } 309 310 if (color == COLOR_INVALID) { 311 // WHITE ITALICS 312 color = COLOR_WHITE; 313 style |= STYLE_ITALICS; 314 } 315 316 return new StyleCode(style, color); 317 } 318 319 StyleCode(int style, int color) { 320 mStyle = style; 321 mColor = color; 322 } 323 324 boolean isItalics() { 325 return (mStyle & STYLE_ITALICS) != 0; 326 } 327 328 boolean isUnderline() { 329 return (mStyle & STYLE_UNDERLINE) != 0; 330 } 331 332 int getColor() { 333 return mColor; 334 } 335 336 @Override 337 public String toString() { 338 StringBuilder str = new StringBuilder(); 339 str.append("{"); 340 str.append(sColorMap[mColor]); 341 if ((mStyle & STYLE_ITALICS) != 0) { 342 str.append(", ITALICS"); 343 } 344 if ((mStyle & STYLE_UNDERLINE) != 0) { 345 str.append(", UNDERLINE"); 346 } 347 str.append("}"); 348 349 return str.toString(); 350 } 351 } 352 353 private static class PAC extends StyleCode { 354 final int mRow; 355 final int mCol; 356 357 static PAC fromBytes(byte data1, byte data2) { 358 int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9}; 359 int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5); 360 int style = 0; 361 if ((data2 & 1) != 0) { 362 style |= STYLE_UNDERLINE; 363 } 364 if ((data2 & 0x10) != 0) { 365 // indent code 366 int indent = (data2 >> 1) & 0x7; 367 return new PAC(row, indent * 4, style, COLOR_WHITE); 368 } else { 369 // style code 370 int color = (data2 >> 1) & 0x7; 371 372 if (color == COLOR_INVALID) { 373 // WHITE ITALICS 374 color = COLOR_WHITE; 375 style |= STYLE_ITALICS; 376 } 377 return new PAC(row, -1, style, color); 378 } 379 } 380 381 PAC(int row, int col, int style, int color) { 382 super(style, color); 383 mRow = row; 384 mCol = col; 385 } 386 387 boolean isIndentPAC() { 388 return (mCol >= 0); 389 } 390 391 int getRow() { 392 return mRow; 393 } 394 395 int getCol() { 396 return mCol; 397 } 398 399 @Override 400 public String toString() { 401 return String.format("{%d, %d}, %s", 402 mRow, mCol, super.toString()); 403 } 404 } 405 406 /** 407 * Mutable version of BackgroundSpan to facilitate text rendering with edge styles. 408 */ 409 public static class MutableBackgroundColorSpan extends CharacterStyle 410 implements UpdateAppearance { 411 private int mColor; 412 413 MutableBackgroundColorSpan(int color) { 414 mColor = color; 415 } 416 417 public void setBackgroundColor(int color) { 418 mColor = color; 419 } 420 421 public int getBackgroundColor() { 422 return mColor; 423 } 424 425 @Override 426 public void updateDrawState(TextPaint ds) { 427 ds.bgColor = mColor; 428 } 429 } 430 431 /* CCLineBuilder keeps track of displayable chars, as well as 432 * MidRow styles and PACs, for a single line of CC memory. 433 * 434 * It generates styled text via getStyledText() method. 435 */ 436 private static class CCLineBuilder { 437 private final StringBuilder mDisplayChars; 438 private final StyleCode[] mMidRowStyles; 439 private final StyleCode[] mPACStyles; 440 441 CCLineBuilder(String str) { 442 mDisplayChars = new StringBuilder(str); 443 mMidRowStyles = new StyleCode[mDisplayChars.length()]; 444 mPACStyles = new StyleCode[mDisplayChars.length()]; 445 } 446 447 void setCharAt(int index, char ch) { 448 mDisplayChars.setCharAt(index, ch); 449 mMidRowStyles[index] = null; 450 } 451 452 void setMidRowAt(int index, StyleCode m) { 453 mDisplayChars.setCharAt(index, ' '); 454 mMidRowStyles[index] = m; 455 } 456 457 void setPACAt(int index, PAC pac) { 458 mPACStyles[index] = pac; 459 } 460 461 char charAt(int index) { 462 return mDisplayChars.charAt(index); 463 } 464 465 int length() { 466 return mDisplayChars.length(); 467 } 468 469 void applyStyleSpan( 470 SpannableStringBuilder styledText, 471 StyleCode s, int start, int end) { 472 if (s.isItalics()) { 473 styledText.setSpan( 474 new StyleSpan(android.graphics.Typeface.ITALIC), 475 start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 476 } 477 if (s.isUnderline()) { 478 styledText.setSpan( 479 new UnderlineSpan(), 480 start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 481 } 482 } 483 484 SpannableStringBuilder getStyledText(CaptionStyle captionStyle) { 485 SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars); 486 int start = -1, next = 0; 487 int styleStart = -1; 488 StyleCode curStyle = null; 489 while (next < mDisplayChars.length()) { 490 StyleCode newStyle = null; 491 if (mMidRowStyles[next] != null) { 492 // apply mid-row style change 493 newStyle = mMidRowStyles[next]; 494 } else if (mPACStyles[next] != null && (styleStart < 0 || start < 0)) { 495 // apply PAC style change, only if: 496 // 1. no style set, or 497 // 2. style set, but prev char is none-displayable 498 newStyle = mPACStyles[next]; 499 } 500 if (newStyle != null) { 501 curStyle = newStyle; 502 if (styleStart >= 0 && start >= 0) { 503 applyStyleSpan(styledText, newStyle, styleStart, next); 504 } 505 styleStart = next; 506 } 507 508 if (mDisplayChars.charAt(next) != TS) { 509 if (start < 0) { 510 start = next; 511 } 512 } else if (start >= 0) { 513 int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1; 514 int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1; 515 styledText.setSpan( 516 new MutableBackgroundColorSpan(captionStyle.backgroundColor), 517 expandedStart, expandedEnd, 518 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 519 if (styleStart >= 0) { 520 applyStyleSpan(styledText, curStyle, styleStart, expandedEnd); 521 } 522 start = -1; 523 } 524 next++; 525 } 526 527 return styledText; 528 } 529 } 530 531 /* 532 * CCMemory models a console-style display. 533 */ 534 private static class CCMemory { 535 private final String mBlankLine; 536 private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2]; 537 private int mRow; 538 private int mCol; 539 540 CCMemory() { 541 char[] blank = new char[MAX_COLS + 2]; 542 Arrays.fill(blank, TS); 543 mBlankLine = new String(blank); 544 } 545 546 void erase() { 547 // erase all lines 548 for (int i = 0; i < mLines.length; i++) { 549 mLines[i] = null; 550 } 551 mRow = MAX_ROWS; 552 mCol = 1; 553 } 554 555 void der() { 556 if (mLines[mRow] != null) { 557 for (int i = 0; i < mCol; i++) { 558 if (mLines[mRow].charAt(i) != TS) { 559 for (int j = mCol; j < mLines[mRow].length(); j++) { 560 mLines[j].setCharAt(j, TS); 561 } 562 return; 563 } 564 } 565 mLines[mRow] = null; 566 } 567 } 568 569 void tab(int tabs) { 570 moveCursorByCol(tabs); 571 } 572 573 void bs() { 574 moveCursorByCol(-1); 575 if (mLines[mRow] != null) { 576 mLines[mRow].setCharAt(mCol, TS); 577 if (mCol == MAX_COLS - 1) { 578 // Spec recommendation: 579 // if cursor was at col 32, move cursor 580 // back to col 31 and erase both col 31&32 581 mLines[mRow].setCharAt(MAX_COLS, TS); 582 } 583 } 584 } 585 586 void cr() { 587 moveCursorTo(mRow + 1, 1); 588 } 589 590 void rollUp(int windowSize) { 591 int i; 592 for (i = 0; i <= mRow - windowSize; i++) { 593 mLines[i] = null; 594 } 595 int startRow = mRow - windowSize + 1; 596 if (startRow < 1) { 597 startRow = 1; 598 } 599 for (i = startRow; i < mRow; i++) { 600 mLines[i] = mLines[i + 1]; 601 } 602 for (i = mRow; i < mLines.length; i++) { 603 // clear base row 604 mLines[i] = null; 605 } 606 // default to col 1, in case PAC is not sent 607 mCol = 1; 608 } 609 610 void writeText(String text) { 611 for (int i = 0; i < text.length(); i++) { 612 getLineBuffer(mRow).setCharAt(mCol, text.charAt(i)); 613 moveCursorByCol(1); 614 } 615 } 616 617 void writeMidRowCode(StyleCode m) { 618 getLineBuffer(mRow).setMidRowAt(mCol, m); 619 moveCursorByCol(1); 620 } 621 622 void writePAC(PAC pac) { 623 if (pac.isIndentPAC()) { 624 moveCursorTo(pac.getRow(), pac.getCol()); 625 } else { 626 moveCursorTo(pac.getRow(), 1); 627 } 628 getLineBuffer(mRow).setPACAt(mCol, pac); 629 } 630 631 SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) { 632 ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS); 633 for (int i = 1; i <= MAX_ROWS; i++) { 634 rows.add(mLines[i] != null ? mLines[i].getStyledText(captionStyle) : null); 635 } 636 return rows.toArray(new SpannableStringBuilder[MAX_ROWS]); 637 } 638 639 private static int clamp(int x, int min, int max) { 640 return x < min ? min : (x > max ? max : x); 641 } 642 643 private void moveCursorTo(int row, int col) { 644 mRow = clamp(row, 1, MAX_ROWS); 645 mCol = clamp(col, 1, MAX_COLS); 646 } 647 648 private void moveCursorToRow(int row) { 649 mRow = clamp(row, 1, MAX_ROWS); 650 } 651 652 private void moveCursorByCol(int col) { 653 mCol = clamp(mCol + col, 1, MAX_COLS); 654 } 655 656 private void moveBaselineTo(int baseRow, int windowSize) { 657 if (mRow == baseRow) { 658 return; 659 } 660 int actualWindowSize = windowSize; 661 if (baseRow < actualWindowSize) { 662 actualWindowSize = baseRow; 663 } 664 if (mRow < actualWindowSize) { 665 actualWindowSize = mRow; 666 } 667 668 int i; 669 if (baseRow < mRow) { 670 // copy from bottom to top row 671 for (i = actualWindowSize - 1; i >= 0; i--) { 672 mLines[baseRow - i] = mLines[mRow - i]; 673 } 674 } else { 675 // copy from top to bottom row 676 for (i = 0; i < actualWindowSize; i++) { 677 mLines[baseRow - i] = mLines[mRow - i]; 678 } 679 } 680 // clear rest of the rows 681 for (i = 0; i <= baseRow - windowSize; i++) { 682 mLines[i] = null; 683 } 684 for (i = baseRow + 1; i < mLines.length; i++) { 685 mLines[i] = null; 686 } 687 } 688 689 private CCLineBuilder getLineBuffer(int row) { 690 if (mLines[row] == null) { 691 mLines[row] = new CCLineBuilder(mBlankLine); 692 } 693 return mLines[row]; 694 } 695 } 696 697 /* 698 * CCData parses the raw CC byte pair into displayable chars, 699 * misc control codes, Mid-Row or Preamble Address Codes. 700 */ 701 private static class CCData { 702 private final byte mType; 703 private final byte mData1; 704 private final byte mData2; 705 706 private static final String[] sCtrlCodeMap = { 707 "RCL", "BS" , "AOF", "AON", 708 "DER", "RU2", "RU3", "RU4", 709 "FON", "RDC", "TR" , "RTD", 710 "EDM", "CR" , "ENM", "EOC", 711 }; 712 713 private static final String[] sSpecialCharMap = { 714 "\u00AE", 715 "\u00B0", 716 "\u00BD", 717 "\u00BF", 718 "\u2122", 719 "\u00A2", 720 "\u00A3", 721 "\u266A", // Eighth note 722 "\u00E0", 723 "\u00A0", // Transparent space 724 "\u00E8", 725 "\u00E2", 726 "\u00EA", 727 "\u00EE", 728 "\u00F4", 729 "\u00FB", 730 }; 731 732 private static final String[] sSpanishCharMap = { 733 // Spanish and misc chars 734 "\u00C1", // A 735 "\u00C9", // E 736 "\u00D3", // I 737 "\u00DA", // O 738 "\u00DC", // U 739 "\u00FC", // u 740 "\u2018", // opening single quote 741 "\u00A1", // inverted exclamation mark 742 "*", 743 "'", 744 "\u2014", // em dash 745 "\u00A9", // Copyright 746 "\u2120", // Servicemark 747 "\u2022", // round bullet 748 "\u201C", // opening double quote 749 "\u201D", // closing double quote 750 // French 751 "\u00C0", 752 "\u00C2", 753 "\u00C7", 754 "\u00C8", 755 "\u00CA", 756 "\u00CB", 757 "\u00EB", 758 "\u00CE", 759 "\u00CF", 760 "\u00EF", 761 "\u00D4", 762 "\u00D9", 763 "\u00F9", 764 "\u00DB", 765 "\u00AB", 766 "\u00BB" 767 }; 768 769 private static final String[] sProtugueseCharMap = { 770 // Portuguese 771 "\u00C3", 772 "\u00E3", 773 "\u00CD", 774 "\u00CC", 775 "\u00EC", 776 "\u00D2", 777 "\u00F2", 778 "\u00D5", 779 "\u00F5", 780 "{", 781 "}", 782 "\\", 783 "^", 784 "_", 785 "|", 786 "~", 787 // German and misc chars 788 "\u00C4", 789 "\u00E4", 790 "\u00D6", 791 "\u00F6", 792 "\u00DF", 793 "\u00A5", 794 "\u00A4", 795 "\u2502", // vertical bar 796 "\u00C5", 797 "\u00E5", 798 "\u00D8", 799 "\u00F8", 800 "\u250C", // top-left corner 801 "\u2510", // top-right corner 802 "\u2514", // lower-left corner 803 "\u2518", // lower-right corner 804 }; 805 806 static CCData[] fromByteArray(byte[] data) { 807 CCData[] ccData = new CCData[data.length / 3]; 808 809 for (int i = 0; i < ccData.length; i++) { 810 ccData[i] = new CCData( 811 data[i * 3], 812 data[i * 3 + 1], 813 data[i * 3 + 2]); 814 } 815 816 return ccData; 817 } 818 819 CCData(byte type, byte data1, byte data2) { 820 mType = type; 821 mData1 = data1; 822 mData2 = data2; 823 } 824 825 int getCtrlCode() { 826 if ((mData1 == 0x14 || mData1 == 0x1c) 827 && mData2 >= 0x20 && mData2 <= 0x2f) { 828 return mData2; 829 } 830 return INVALID; 831 } 832 833 StyleCode getMidRow() { 834 // only support standard Mid-row codes, ignore 835 // optional background/foreground mid-row codes 836 if ((mData1 == 0x11 || mData1 == 0x19) 837 && mData2 >= 0x20 && mData2 <= 0x2f) { 838 return StyleCode.fromByte(mData2); 839 } 840 return null; 841 } 842 843 PAC getPAC() { 844 if ((mData1 & 0x70) == 0x10 845 && (mData2 & 0x40) == 0x40 846 && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) { 847 return PAC.fromBytes(mData1, mData2); 848 } 849 return null; 850 } 851 852 int getTabOffset() { 853 if ((mData1 == 0x17 || mData1 == 0x1f) 854 && mData2 >= 0x21 && mData2 <= 0x23) { 855 return mData2 & 0x3; 856 } 857 return 0; 858 } 859 860 boolean isDisplayableChar() { 861 return isBasicChar() || isSpecialChar() || isExtendedChar(); 862 } 863 864 String getDisplayText() { 865 String str = getBasicChars(); 866 867 if (str == null) { 868 str = getSpecialChar(); 869 870 if (str == null) { 871 str = getExtendedChar(); 872 } 873 } 874 875 return str; 876 } 877 878 private String ctrlCodeToString(int ctrlCode) { 879 return sCtrlCodeMap[ctrlCode - 0x20]; 880 } 881 882 private boolean isBasicChar() { 883 return mData1 >= 0x20 && mData1 <= 0x7f; 884 } 885 886 private boolean isSpecialChar() { 887 return ((mData1 == 0x11 || mData1 == 0x19) 888 && mData2 >= 0x30 && mData2 <= 0x3f); 889 } 890 891 private boolean isExtendedChar() { 892 return ((mData1 == 0x12 || mData1 == 0x1A 893 || mData1 == 0x13 || mData1 == 0x1B) 894 && mData2 >= 0x20 && mData2 <= 0x3f); 895 } 896 897 private char getBasicChar(byte data) { 898 char c; 899 // replace the non-ASCII ones 900 switch (data) { 901 case 0x2A: c = '\u00E1'; break; 902 case 0x5C: c = '\u00E9'; break; 903 case 0x5E: c = '\u00ED'; break; 904 case 0x5F: c = '\u00F3'; break; 905 case 0x60: c = '\u00FA'; break; 906 case 0x7B: c = '\u00E7'; break; 907 case 0x7C: c = '\u00F7'; break; 908 case 0x7D: c = '\u00D1'; break; 909 case 0x7E: c = '\u00F1'; break; 910 case 0x7F: c = '\u2588'; break; // Full block 911 default: c = (char) data; break; 912 } 913 return c; 914 } 915 916 private String getBasicChars() { 917 if (mData1 >= 0x20 && mData1 <= 0x7f) { 918 StringBuilder builder = new StringBuilder(2); 919 builder.append(getBasicChar(mData1)); 920 if (mData2 >= 0x20 && mData2 <= 0x7f) { 921 builder.append(getBasicChar(mData2)); 922 } 923 return builder.toString(); 924 } 925 926 return null; 927 } 928 929 private String getSpecialChar() { 930 if ((mData1 == 0x11 || mData1 == 0x19) 931 && mData2 >= 0x30 && mData2 <= 0x3f) { 932 return sSpecialCharMap[mData2 - 0x30]; 933 } 934 935 return null; 936 } 937 938 private String getExtendedChar() { 939 if ((mData1 == 0x12 || mData1 == 0x1A) && mData2 >= 0x20 && mData2 <= 0x3f) { 940 // 1 Spanish/French char 941 return sSpanishCharMap[mData2 - 0x20]; 942 } else if ((mData1 == 0x13 || mData1 == 0x1B) && mData2 >= 0x20 && mData2 <= 0x3f) { 943 // 1 Portuguese/German/Danish char 944 return sProtugueseCharMap[mData2 - 0x20]; 945 } 946 947 return null; 948 } 949 950 @Override 951 public String toString() { 952 String str; 953 954 if (mData1 < 0x10 && mData2 < 0x10) { 955 // Null Pad, ignore 956 return String.format("[%d]Null: %02x %02x", mType, mData1, mData2); 957 } 958 959 int ctrlCode = getCtrlCode(); 960 if (ctrlCode != INVALID) { 961 return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode)); 962 } 963 964 int tabOffset = getTabOffset(); 965 if (tabOffset > 0) { 966 return String.format("[%d]Tab%d", mType, tabOffset); 967 } 968 969 PAC pac = getPAC(); 970 if (pac != null) { 971 return String.format("[%d]PAC: %s", mType, pac.toString()); 972 } 973 974 StyleCode m = getMidRow(); 975 if (m != null) { 976 return String.format("[%d]Mid-row: %s", mType, m.toString()); 977 } 978 979 if (isDisplayableChar()) { 980 return String.format("[%d]Displayable: %s (%02x %02x)", 981 mType, getDisplayText(), mData1, mData2); 982 } 983 984 return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2); 985 } 986 } 987} 988