Files
QTeletextMaker/decode.cpp
G.K.MacGregor 021fbfa60f Rewrite the decoder
A complete rewrite of decode.cpp and decode.h to make it easier to follow
and maintain. Rather than a set of "layers" descended from C++ classes, the
for-loop across the rows and columns explicitly lays out the order of
processing the Level 1 characters and attributes, the X/26 enhancements
that affect the page directly as well as those within each Invocation of an
Object, followed by selecting which Invoked Object to place in the character
cell or in the absence of an Object the underlying page fragment will be
placed in the cell.

The new decoder has the following improvements over the old...
- Local Enhancement Data has priority over Active Objects.
- Active Objects can set Full Screen and Full Row colours.
- Incremental/decremental flash phases are tracked correctly within Objects.
- X/26 characters overwriting the bottom half of Level 1 Double Height rows.
- Interaction between the underlying page and Objects where characters of
  different sizes overlap.
2023-05-07 19:25:06 +01:00

1075 lines
37 KiB
C++

/*
* Copyright (C) 2020-2023 Gavin MacGregor
*
* This file is part of QTeletextMaker.
*
* QTeletextMaker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QTeletextMaker is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QTeletextMaker. If not, see <https://www.gnu.org/licenses/>.
*/
#include "decode.h"
#include <QList>
#include <QMultiMap>
TeletextPageDecode::Invocation::Invocation()
{
m_tripletList = nullptr;
m_startTripletNumber = 0;
m_endTripletNumber = -1;
m_originRow = 0;
m_originColumn = 0;
m_fullScreenCLUT = -1;
}
void TeletextPageDecode::Invocation::setTripletList(X26TripletList *tripletList)
{
m_tripletList = tripletList;
}
void TeletextPageDecode::Invocation::setStartTripletNumber(int n)
{
m_startTripletNumber = n;
}
void TeletextPageDecode::Invocation::setEndTripletNumber(int n)
{
m_endTripletNumber = n;
}
void TeletextPageDecode::Invocation::setOrigin(int row, int column)
{
m_originRow = row;
m_originColumn = column;
}
void TeletextPageDecode::Invocation::buildMap(int level)
{
int endTripletNumber;
if (m_endTripletNumber == -1)
endTripletNumber = m_tripletList->size();
else
endTripletNumber = m_endTripletNumber;
m_characterMap.clear();
m_attributeMap.clear();
m_rightMostColumn.clear();
m_fullScreenCLUT = -1;
m_fullRowCLUTMap.clear();
for (int i=m_startTripletNumber; i<=endTripletNumber; i++) {
const X26Triplet triplet = m_tripletList->at(i);
if (triplet.error() != 0)
continue;
int targetRow, targetColumn;
if (level == 1) {
targetRow = m_originRow + triplet.activePositionRow1p5();
targetColumn = m_originColumn + triplet.activePositionColumn1p5();
} else {
targetRow = m_originRow + triplet.activePositionRow();
targetColumn = m_originColumn + triplet.activePositionColumn();
}
if (triplet.activePositionRow() == -1)
targetRow++;
if (triplet.activePositionColumn() == -1)
targetColumn++;
if (targetRow > 24 || targetColumn > 71)
continue;
switch (triplet.modeExt()) {
case 0x21: // G1 character
case 0x22: // G3 character at Level 1.5
case 0x29: // G0 character
case 0x2b: // G3 character at Level 2.5
case 0x2f: // G2 character
case 0x30 ... 0x3f: // G0 character with diacritical
m_characterMap.insert(qMakePair(targetRow, targetColumn), triplet);
// Store rightmost column in this row for Adaptive Object attribute tracking
// QMap stores one value per key, QMap::insert will replace the value if the key already exists
m_rightMostColumn.insert(targetRow, targetColumn);
break;
case 0x20: // Foreground colour
case 0x23: // Background colour
case 0x27: // Additional flash functions
case 0x2c: // Display attributes
m_attributeMap.insert(qMakePair(targetRow, targetColumn), triplet);
m_rightMostColumn.insert(targetRow, targetColumn);
break;
case 0x00: // Full screen colour
if ((triplet.data() & 0x60) != 0x00)
break;
m_fullScreenCLUT = triplet.data();
// Full Screen Colour triplet overrides both the X/28 Full Screen Colour AND Full Row Colour.
// For the latter, place a Full Row Colour "down to bottom" at the Active Position.
m_fullRowCLUTMap.insert(targetRow, X26Triplet(triplet.address(), triplet.mode(), triplet.data() | 0x60));
break;
case 0x01: // Full row colour
m_fullRowCLUTMap.insert(targetRow, triplet);
break;
case 0x07: // Address row 0
if (targetRow == 0)
m_fullRowCLUTMap.insert(targetRow, triplet);
break;
}
}
}
int TeletextPageDecode::s_instances = 0;
TeletextPageDecode::textPainter TeletextPageDecode::s_blankPainter;
TeletextPageDecode::TeletextPageDecode()
{
if (s_instances == 0) {
for (int c=0; c<72; c++)
s_blankPainter.bottomHalfCell[c].character.code = 0x00;
s_blankPainter.rightHalfCell.character.code = 0x00;
}
s_instances++;
m_level = 0;
for (int r=0; r<25; r++) {
m_rowHeight[r] = NormalHeight;
for (int c=0; c<72; c++) {
if (c < 40) {
m_cellLevel1Mosaic[r][c] = false;
m_cellLevel1CharSet[r][c] = 0;
}
m_refresh[r][c] = true;
}
}
m_finalFullScreenColour = 0;
m_finalFullScreenQColor.setRgb(0, 0, 0);
for (int r=0; r<25; r++) {
m_fullRowColour[r] = 0;
m_fullRowQColor[r].setRgb(0, 0, 0);
}
m_leftSidePanelColumns = m_rightSidePanelColumns = 0;
}
TeletextPageDecode::~TeletextPageDecode()
{
s_instances--;
}
void TeletextPageDecode::setRefresh(int r, int c, bool refresh)
{
m_refresh[r][c] = refresh;
}
void TeletextPageDecode::setTeletextPage(LevelOnePage *newCurrentPage)
{
m_levelOnePage = newCurrentPage;
m_localEnhancements.setTripletList(m_levelOnePage->enhancements());
updateSidePanels();
}
void TeletextPageDecode::setLevel(int level)
{
if (level == m_level)
return;
m_level = level;
for (int r=0; r<25; r++)
for (int c=0; c<72; c++)
m_refresh[r][c] = true;
updateSidePanels();
decodePage();
}
void TeletextPageDecode::updateSidePanels()
{
int oldLeftSidePanelColumns = m_leftSidePanelColumns;
int oldRightSidePanelColumns = m_rightSidePanelColumns;
if (m_level >= (3-m_levelOnePage->sidePanelStatusL25()) && m_levelOnePage->leftSidePanelDisplayed())
m_leftSidePanelColumns = (m_levelOnePage->sidePanelColumns() == 0) ? 16 : m_levelOnePage->sidePanelColumns();
else
m_leftSidePanelColumns = 0;
if (m_level >= (3-m_levelOnePage->sidePanelStatusL25()) && m_levelOnePage->rightSidePanelDisplayed())
m_rightSidePanelColumns = 16-m_levelOnePage->sidePanelColumns();
else
m_rightSidePanelColumns = 0;
if (m_leftSidePanelColumns != oldLeftSidePanelColumns || m_rightSidePanelColumns != oldRightSidePanelColumns) {
emit sidePanelsChanged();
decodePage();
}
}
void TeletextPageDecode::buildInvocationList(Invocation &invocation, int objectType)
{
if (invocation.tripletList()->isEmpty())
return;
int i;
for (i=invocation.startTripletNumber(); i<invocation.tripletList()->size(); i++) {
const X26Triplet triplet = invocation.tripletList()->at(i);
if (triplet.modeExt() == 0x1f && triplet.address() == 63)
// Termination marker
break;
if (triplet.modeExt() >= 0x15 && triplet.modeExt() <= 0x17)
// Object Definition, also used as terminator
break;
if (m_level >= 2 && triplet.modeExt() >= 0x11 && triplet.modeExt() <= 0x13 && triplet.error() == 0) {
// Object Invocation
//TODO POP and GPOP objects
if (triplet.objectSource() != X26Triplet::LocalObject) {
qDebug("POP or GPOP");
continue;
}
// Check if (sub)Object type can be invoked by Object type we're within
if (triplet.modeExt() - 0x11 <= objectType)
continue;
// See if Object Definition is required at selected level
if (m_level == 2 && (invocation.tripletList()->at(triplet.objectLocalIndex()).address() & 0x08) == 0x00)
continue;
if (m_level == 3 && (invocation.tripletList()->at(triplet.objectLocalIndex()).address() & 0x10) == 0x00)
continue;
// Work out the absolute position where the Object is invoked
int originRow = invocation.originRow() + triplet.activePositionRow();
int originColumn = invocation.originColumn() + triplet.activePositionColumn();
// -1, -1 happens if Object is invoked before the Active Position is deployed
if (triplet.activePositionRow() == -1)
originRow++;
if (triplet.activePositionColumn() == -1)
originColumn++;
// Use Origin Modifier in previous triplet if there's one there
if (i > 0 && invocation.tripletList()->at(i-1).modeExt() == 0x10) {
originRow += invocation.tripletList()->at(i-1).address()-40;
originColumn += invocation.tripletList()->at(i-1).data();
}
// Add the Invocation to the list, and recurse
Invocation newInvocation;
const int newObjectType = triplet.modeExt() - 0x11;
newInvocation.setTripletList(invocation.tripletList());
newInvocation.setStartTripletNumber(triplet.objectLocalIndex()+1);
newInvocation.setOrigin(originRow, originColumn);
m_invocations[newObjectType].append(newInvocation);
buildInvocationList(m_invocations[newObjectType].last(), newObjectType);
}
}
invocation.setEndTripletNumber(i-1);
invocation.buildMap(m_level);
}
TeletextPageDecode::textCharacter TeletextPageDecode::characterFromTriplets(const QList<X26Triplet> triplets, int g0CharSet)
{
textCharacter result;
result.code = 0x00;
// QMultiMap::values returns a QList with the most recently inserted value sorted first,
// so do the loop backwards to iterate from least to most recent value
for (int a=triplets.size()-1; a>=0; a--) {
const X26Triplet triplet = triplets.at(a);
if (triplet.data() < 0x20)
continue;
const unsigned char charCode = triplet.data();
// Deal with Level 1.5 valid characters first
switch (triplet.modeExt()) {
case 0x22: // G3 character at Level 1.5
result = { charCode, 26, 0 };
break;
case 0x2f: // G2 character
result.code = charCode;
// Duplicated from decodePage
switch (m_level1DefaultCharSet) {
case 1:
case 2:
case 3:
// Cyrillic G2
result.set = 8;
break;
case 4:
// Greek G2
result.set = 9;
break;
case 5:
// Arabic G2
result.set = 10;
break;
default:
// Latin G2
result.set = 7;
break;
}
result.diacritical = 0;
break;
case 0x30 ... 0x3f: // G0 character with diacritical
result = { charCode, g0CharSet, triplet.mode() & 0xf };
break;
}
if (m_level == 1)
continue;
// Now deal with Level 2.5 characters
switch (triplet.modeExt()) {
case 0x21: // G1 character
result.code = charCode;
if (triplet.data() & 0x20)
result.set = 24;
else
result.set = g0CharSet;
result.diacritical = 0;
break;
case 0x29: // G0 character
result = { charCode, g0CharSet, 0 };
break;
case 0x2b: // G3 character at Level 2.5
result = { charCode, 26, 0 };
break;
}
}
return result;
}
void TeletextPageDecode::decodePage()
{
m_invocations[0].clear();
m_invocations[1].clear();
m_invocations[2].clear();
buildInvocationList(m_localEnhancements, -1);
// Append Local Enhancement Data to end of Active Object QList
m_invocations[0].append(m_localEnhancements);
m_level1ActivePainter = s_blankPainter;
m_adapPassPainter[0].clear();
m_adapPassPainter[1].clear();
for (int t=1; t<3; t++)
for (int i=0; i<m_invocations[t].size(); i++)
m_adapPassPainter[t-1].append(s_blankPainter);
if (m_level >= 2) {
// Pick up default full screen/row colours from X/28
setFullScreenColour(m_levelOnePage->defaultScreenColour());
int downwardsRowCLUT = m_levelOnePage->defaultRowColour();
// Check for Full Screen Colour X/26 triplets in Local Enhancement Data and Active Objects
for (int i=0; i<m_invocations[0].size(); i++)
if (m_invocations[0].at(i).fullScreenColour() != -1)
setFullScreenColour(m_invocations[0].at(i).fullScreenColour());
// Now do the Full Row Colours
for (int r=0; r<25; r++) {
int thisFullRowColour = downwardsRowCLUT;
for (int i=0; i<m_invocations[0].size(); i++) {
const QList<X26Triplet> fullRowColoursHere = m_invocations[0].at(i).fullRowColoursMappedAt(r);
// QMultiMap::values returns QList with most recent value first...
for (int a=fullRowColoursHere.size()-1; a>=0; a--) {
thisFullRowColour = fullRowColoursHere.at(a).data() & 0x1f;
if ((fullRowColoursHere.at(a).data() & 0x60) == 0x60)
downwardsRowCLUT = thisFullRowColour;
}
}
setFullRowColour(r, thisFullRowColour);
}
} else {
setFullScreenColour(0);
for (int r=0; r<25; r++)
setFullRowColour(r, 0);
}
m_level1DefaultCharSet = m_g0CharacterMap.value(((m_levelOnePage->defaultCharSet() << 3) | m_levelOnePage->defaultNOS()), 0);
if (m_levelOnePage->secondCharSet() != 0xf)
m_level1SecondCharSet = m_g0CharacterMap.value(((m_levelOnePage->secondCharSet() << 3) | m_levelOnePage->secondNOS()), 0);
else
m_level1SecondCharSet = m_level1DefaultCharSet;
// This will be true if the Level 1 character set is non-Latin
if (m_level1DefaultCharSet <= 6)
m_x26DefaultG0CharSet = m_level1DefaultCharSet;
else
m_x26DefaultG0CharSet = 0;
switch (m_level1DefaultCharSet) {
case 1:
case 2:
case 3:
// Cyrillic G2
m_x26DefaultG2CharSet = 8;
break;
case 4:
// Greek G2
m_x26DefaultG2CharSet = 9;
break;
case 5:
// Arabic G2
m_x26DefaultG2CharSet = 10;
break;
default:
// Latin G2
m_x26DefaultG2CharSet = 7;
break;
}
// Work out rows containing top and bottom halves of Level 1 double height characters
for (int r=1; r<24; r++) {
bool doubleHeightAttributeFound = false;
for (int c=0; c<40; c++)
if (m_levelOnePage->character(r, c) == 0x0d || m_levelOnePage->character(r, c) == 0x0f) {
doubleHeightAttributeFound = true;
break;
}
if (doubleHeightAttributeFound && r < 23) {
m_rowHeight[r] = TopHalf;
r++;
m_rowHeight[r] = BottomHalf;
} else
m_rowHeight[r] = NormalHeight;
}
for (int r=0; r<25; r++)
decodeRow(r);
}
void TeletextPageDecode::decodeRow(int r)
{
int level1ForegroundCLUT = 7;
bool level1Mosaics = false;
bool level1SeparatedMosaics = false;
bool level1HoldMosaics = false;
unsigned char level1HoldMosaicCharacter = 0x20;
bool level1HoldMosaicSeparated = false;
int level1CharSet = 0;
bool level1EscapeSwitch = false;
int x26G0CharSet = 0;
textPainter *painter;
// Used for tracking which Adaptive Invocation is applying which attribute type(s)
// A.7.1 and A.7.2 of the spec says Adaptive Objects can't overlap but can be interleaved
// if they don't have attributes, so we only need to track one
int adapInvokeAttrs = -1;
bool adapForeground = false;
bool adapBackground = false;
bool adapFlash = false;
bool adapDisplayAttrs = false;
for (int c=0; c<72; c++) {
textCell previousCellContents = m_cell[r][c];
// Start of row default conditions, also when crossing into and across side panels
if (c == 0 || c == 40 || c == 56) {
level1CharSet = m_level1DefaultCharSet;
x26G0CharSet = m_x26DefaultG0CharSet;
m_level1ActivePainter.attribute.flash.mode = 0;
m_level1ActivePainter.attribute.flash.ratePhase = 0;
m_level1ActivePainter.attribute.display.doubleHeight = false;
m_level1ActivePainter.attribute.display.doubleWidth = false;
m_level1ActivePainter.attribute.display.boxingWindow = false;
m_level1ActivePainter.attribute.display.conceal = false;
m_level1ActivePainter.attribute.display.invert = false;
m_level1ActivePainter.attribute.display.underlineSeparated = false;
if (m_level >= 2) {
m_level1ActivePainter.attribute.foregroundCLUT = 7 | m_foregroundRemap[m_levelOnePage->colourTableRemap()];
if (m_levelOnePage->blackBackgroundSubst() || c >= 40)
m_level1ActivePainter.attribute.backgroundCLUT = m_fullRowColour[r];
else
m_level1ActivePainter.attribute.backgroundCLUT = m_backgroundRemap[m_levelOnePage->colourTableRemap()];
} else {
m_level1ActivePainter.attribute.foregroundCLUT = 7;
m_level1ActivePainter.attribute.backgroundCLUT = 0;
}
}
// Level 1 set-at and "set-between" spacing attributes
if (c < 40 && m_rowHeight[r] != BottomHalf)
switch (m_levelOnePage->character(r, c)) {
case 0x09: // Steady
m_level1ActivePainter.attribute.flash.mode = 0;
m_level1ActivePainter.attribute.flash.ratePhase = 0;
break;
case 0x0a: // End box
// "Set-between" - requires two consecutive "end box" codes
if (c > 0 && m_levelOnePage->character(r, c-1) == 0x0a)
m_level1ActivePainter.attribute.display.boxingWindow = false;
break;
case 0x0b: // Start box
// "Set-between" - requires two consecutive "start box" codes
if (c > 0 && m_levelOnePage->character(r, c-1) == 0x0b)
m_level1ActivePainter.attribute.display.boxingWindow = true;
break;
case 0x0c: // Normal size
if (m_level1ActivePainter.attribute.display.doubleHeight || m_level1ActivePainter.attribute.display.doubleWidth) {
// Change of size resets hold mosaic character
level1HoldMosaicCharacter = 0x20;
level1HoldMosaicSeparated = false;
}
m_level1ActivePainter.attribute.display.doubleHeight = false;
m_level1ActivePainter.attribute.display.doubleWidth = false;
break;
case 0x18: // Conceal
m_level1ActivePainter.attribute.display.conceal = true;
break;
case 0x19: // Contiguous mosaics
// This spacing attribute cannot cancel an X/26 underlined/separated attribute
if (!m_level1ActivePainter.attribute.display.underlineSeparated)
level1SeparatedMosaics = false;
break;
case 0x1a: // Separated mosaics
level1SeparatedMosaics = true;
break;
case 0x1c: // Black background
if (m_level >= 2) {
if (m_levelOnePage->blackBackgroundSubst())
m_level1ActivePainter.attribute.backgroundCLUT = m_fullRowColour[r];
else
m_level1ActivePainter.attribute.backgroundCLUT = m_backgroundRemap[m_levelOnePage->colourTableRemap()];
} else
m_level1ActivePainter.attribute.backgroundCLUT = 0;
break;
case 0x1d: // New background
if (m_level >= 2)
m_level1ActivePainter.attribute.backgroundCLUT = level1ForegroundCLUT | m_backgroundRemap[m_levelOnePage->colourTableRemap()];
else
m_level1ActivePainter.attribute.backgroundCLUT = level1ForegroundCLUT;
break;
case 0x1e: // Hold mosaics
level1HoldMosaics = true;
break;
}
if (m_level < 2)
m_level1ActivePainter.result.attribute = m_level1ActivePainter.attribute;
else{
// Deal with incremental and decremental flash
rotateFlashMovement(m_level1ActivePainter.attribute.flash);
for (int t=0; t<2; t++)
for (int i=0; i<m_adapPassPainter[t].size(); i++)
rotateFlashMovement(m_adapPassPainter[t][i].attribute.flash);
// X/26 attributes
for (int t=0; t<3; t++)
for (int i=0; i<m_invocations[t].size(); i++) {
QList<X26Triplet> attributesHere = m_invocations[t].at(i).attributesMappedAt(r, c);
painter = (t == 0) ? &m_level1ActivePainter : &m_adapPassPainter[t-1][i];
// Adaptive Invocation painter: pick up the attributes we're NOT adapting from
// m_level1ActivePainter, which by now has taken into account all the attributes
// from the Level 1 page, Active Objects and the Local Enhancement Data
if (t == 1) {
if (!adapForeground)
painter->attribute.foregroundCLUT = m_level1ActivePainter.attribute.foregroundCLUT;
if (!adapBackground)
painter->attribute.backgroundCLUT = m_level1ActivePainter.attribute.backgroundCLUT;
if (!adapFlash)
painter->attribute.flash = m_level1ActivePainter.attribute.flash;
if (!adapDisplayAttrs)
painter->attribute.display = m_level1ActivePainter.attribute.display;
}
// QMultiMap::values returns QList with most recent value first...
for (int a=attributesHere.size()-1; a>=0; a--) {
const X26Triplet triplet = attributesHere.at(a);
bool applyAdapt = false;
// Adaptive Invocation that is applying an attribute
// If we're not tracking an Adaptive Invocation yet, start tracking this one
// Otherwise check if this Invocation is the the same one as we are tracking
if (t == 1) {
if (adapInvokeAttrs == -1) {
adapInvokeAttrs = i;
applyAdapt = true;
} else if (adapInvokeAttrs == i)
applyAdapt = true;
// else
// qDebug("Multiple adaptive object attributes");
}
switch (triplet.modeExt()) {
case 0x20: // Foreground colour
if (applyAdapt)
adapForeground = true;
painter->attribute.foregroundCLUT = triplet.data();
break;
case 0x23: // Background colour
if (applyAdapt)
adapBackground = true;
painter->attribute.backgroundCLUT = triplet.data();
break;
case 0x27: // Additional flash functions
if (applyAdapt)
adapFlash = true;
painter->attribute.flash.mode = triplet.data() & 0x03;
painter->attribute.flash.ratePhase = triplet.data() >> 2;
// For incremental/decremental 2Hz flash, start at phase 1
if (painter->attribute.flash.mode != 0 && painter->attribute.flash.ratePhase & 0x4)
painter->attribute.flash.phase2HzShown = 1;
else
painter->attribute.flash.phase2HzShown = painter->attribute.flash.ratePhase;
break;
case 0x2c: // Display attributes
if (applyAdapt)
adapDisplayAttrs = true;
painter->attribute.display.doubleHeight = triplet.data() & 0x01;
painter->attribute.display.boxingWindow = triplet.data() & 0x02;
painter->attribute.display.conceal = triplet.data() & 0x04;
painter->attribute.display.invert = triplet.data() & 0x10;
painter->attribute.display.underlineSeparated = triplet.data() & 0x20;
painter->attribute.display.doubleWidth = triplet.data() & 0x40;
// Cancelling separated mosaics with X/26 attribute
// also cancels the Level 1 separated mosaic attribute
if (t == 0 && !painter->attribute.display.underlineSeparated)
level1SeparatedMosaics = false;
break;
}
}
painter->result.attribute = painter->attribute;
}
}
// Level 1 character
if (c < 40 && m_rowHeight[r] != BottomHalf) {
m_level1ActivePainter.result.character.diacritical = 0;
if (m_levelOnePage->character(r, c) >= 0x20) {
m_level1ActivePainter.result.character.code = m_levelOnePage->character(r, c);
// Set to true on mosaic character - not on blast through alphanumerics
m_cellLevel1Mosaic[r][c] = level1Mosaics && (m_levelOnePage->character(r, c) & 0x20);
if (m_cellLevel1Mosaic[r][c]) {
m_level1ActivePainter.result.character.set = 24 + (level1SeparatedMosaics || m_level1ActivePainter.attribute.display.underlineSeparated);
level1HoldMosaicCharacter = m_levelOnePage->character(r, c);
level1HoldMosaicSeparated = level1SeparatedMosaics;
} else
m_level1ActivePainter.result.character.set = level1CharSet;
} else if (level1HoldMosaics) {
m_level1ActivePainter.result.character = { level1HoldMosaicCharacter, 24 + level1HoldMosaicSeparated, 0 };
} else
m_level1ActivePainter.result.character = { 0x20, 0, 0 };
} else
// In side panel or on bottom half of Level 1 double height row, no Level 1 characters here
m_level1ActivePainter.result.character = { 0x20, 0, 0 };
if (c < 40)
m_cellLevel1CharSet[r][c] = level1CharSet;
// X/26 characters
// Used to track if character was placed by X/26 data
// 0=Level 1 character, 1=Active Object or Local Enhancement Data, 2=Adaptive Object
int x26Character = 0;
if (m_level == 1 && !m_invocations[0].isEmpty()) {
// For Level 1.5 only do characters from Local Enhancements
// which is the last entry on the Active Objects QList
const textCharacter result = characterFromTriplets(m_invocations[0].constLast().charactersMappedAt(r, c), x26G0CharSet);
if (result.code != 0x00) {
m_level1ActivePainter.result.character = result;
x26Character = 1;
}
} else if (m_level >= 2)
for (int t=0; t<3; t++)
for (int i=0; i<m_invocations[t].size(); i++) {
painter = (t == 0) ? &m_level1ActivePainter : &m_adapPassPainter[t-1][i];
const textCharacter result = characterFromTriplets(m_invocations[t].at(i).charactersMappedAt(r, c), x26G0CharSet);
if (t == 0 && result.code == 0x00)
continue;
// For an Adaptive Invocation that is applying attribute(s) but not a character here
// pick up the character underneath so we can still place the attributes
if (t == 1 && adapInvokeAttrs == i && result.code == 0x00) {
painter->result.character = m_level1ActivePainter.result.character;
x26Character = 2;
continue;
}
painter->result.character = result;
if (painter->result.character.set == 24 && painter->attribute.display.underlineSeparated)
painter->result.character.set++;
if (t < 2 && result.code != 0x00)
x26Character = t + 1;
}
// Allow Active Objects or Local Enhancement Data to overlap bottom half of a Level 1 double height row
// where the character on the top half is normal size or double width
if (m_rowHeight[r] == BottomHalf && c < 40 && x26Character == 1 && m_level1ActivePainter.bottomHalfCell[c].fragment == NormalSize)
m_level1ActivePainter.bottomHalfCell[c].character.code = 0x00;
// Allow Adaptive Objects to always overlap bottom half of a Level 1 double height row
if (m_rowHeight[r] == BottomHalf && c < 40 && x26Character == 2)
m_level1ActivePainter.bottomHalfCell[c].character.code = 0x00;
// Work out which fragment of an enlarged character to display
for (int t=0; t<3; t++) {
for (int i=0; i<m_invocations[t].size(); i++) {
// This loop will only iterate once when t == 0 because m_level1ActivePainter is
// now the overall result of Level 1, Active Objects and Local Enhancement Data
painter = (t == 0) ? &m_level1ActivePainter : &m_adapPassPainter[t-1][i];
bool cellCovered = false;
// Deal with non-origin parts of enlarged characters if placed during previous iteration
if (painter->rightHalfCell.character.code != 0x00) {
painter->result = painter->rightHalfCell;
// Corner cases of right half of double-width characters overlapping
// a Level 1 double height row need this to avoid spurious characters below
if (painter->result.fragment == DoubleWidthRightHalf)
painter->bottomHalfCell[c].character.code = 0x00;
painter->rightHalfCell.character.code = 0x00;
cellCovered = true;
} else if (painter->bottomHalfCell[c].character.code != 0x00) {
painter->result = painter->bottomHalfCell[c];
painter->bottomHalfCell[c].character.code = 0x00;
cellCovered = true;
}
if (!cellCovered) {
// Cell is not covered by previous enlarged character
// Work out which fragments of enlarged characters are needed from size attributes,
// place origin of character here and other fragments into right half or bottom half
// painter buffer ready to be picked up on the next iteration
bool doubleHeight = painter->attribute.display.doubleHeight;
bool doubleWidth = painter->attribute.display.doubleWidth;
if (r == 0 || r > 22)
doubleHeight = false;
if (c == 39 || c == 39+m_rightSidePanelColumns || c == 71-m_leftSidePanelColumns || c == 71)
doubleWidth = false;
if (doubleHeight) {
if (doubleWidth) {
// Double size
painter->result.fragment = DoubleSizeTopLeftQuarter;
painter->bottomHalfCell[c] = painter->result;
painter->bottomHalfCell[c].fragment = DoubleSizeBottomLeftQuarter;
painter->rightHalfCell = painter->result;
painter->rightHalfCell.fragment = DoubleSizeTopRightQuarter;
painter->bottomHalfCell[c+1] = painter->result;
painter->bottomHalfCell[c+1].fragment = DoubleSizeBottomRightQuarter;
} else {
// Double height
painter->result.fragment = DoubleHeightTopHalf;
painter->bottomHalfCell[c] = painter->result;
painter->bottomHalfCell[c].fragment = DoubleHeightBottomHalf;
}
} else if (doubleWidth) {
// Double width
painter->result.fragment = DoubleWidthLeftHalf;
painter->rightHalfCell = painter->result;
painter->rightHalfCell.fragment = DoubleWidthRightHalf;
} else
// Normal size
painter->result.fragment = NormalSize;
// Now the enlargements and fragments are worked out, prevent Adaptive Objects that
// are NOT applying Display Attributes from trying to overlap wrong fragments of characters
// on the underlying page
if (t == 1 && !adapDisplayAttrs && painter->result.fragment != m_level1ActivePainter.result.fragment) {
painter->result.character.code = 0x00;
if (painter->result.fragment == DoubleWidthLeftHalf || painter->result.fragment == DoubleSizeTopLeftQuarter)
painter->rightHalfCell.character.code = 0x00;
if (painter->result.fragment == DoubleHeightTopHalf || painter->result.fragment == DoubleSizeTopLeftQuarter)
painter->bottomHalfCell[c].character.code = 0x00;
if (painter->result.fragment == DoubleSizeTopLeftQuarter && c < 71)
painter->bottomHalfCell[c+1].character.code = 0x00;
}
}
if (t == 0)
break;
}
}
// Top half of Level 1 double height row: normal or double width characters will cause a space
// with the same attributes to be on the bottom half
// Also occurs with bottom halves of X/26 double height characters overlapping a
// Level 1 top half row
if (m_rowHeight[r] == TopHalf && c < 40) {
if (m_level1ActivePainter.result.fragment != DoubleHeightTopHalf && m_level1ActivePainter.result.fragment != DoubleSizeTopLeftQuarter && m_level1ActivePainter.result.fragment != DoubleSizeTopRightQuarter) {
m_level1ActivePainter.bottomHalfCell[c] = m_level1ActivePainter.result;
m_level1ActivePainter.bottomHalfCell[c].character = { 0x20, 0, 0 };
m_level1ActivePainter.bottomHalfCell[c].fragment = NormalSize;
}
}
// Now we've finally worked out what characters and attributes are in place on
// the underlying page and Invoked Objects, work out which of those to actually render
if (m_level < 2)
m_cell[r][c] = m_level1ActivePainter.result;
else {
bool objectCell = false;
// Passive Objects highest priority, followed by Adaptive Objects
// Most recently invoked Object has priority
for (int t=1; t>=0; t--) {
for (int i=m_adapPassPainter[t].size()-1; i>=0; i--)
if (m_adapPassPainter[t][i].result.character.code != 0x00) {
m_cell[r][c] = m_adapPassPainter[t][i].result;
objectCell = true;
break;
}
if (objectCell)
break;
}
if (!objectCell)
// No Adaptive or Passive Object here: will either be Local Enhancement Data, Active Object
// or underlying Level 1 page
m_cell[r][c] = m_level1ActivePainter.result;
}
// Check for end of Adaptive Object row
if (adapInvokeAttrs != -1 && c == m_invocations[1].at(adapInvokeAttrs).rightMostColumn(r)) {
// Neutralise size attributes as they could interfere with double height stuff
// not sure if this is really necessary
m_adapPassPainter[0][adapInvokeAttrs].attribute.display.doubleHeight = false;
m_adapPassPainter[0][adapInvokeAttrs].attribute.display.doubleWidth = false;
adapInvokeAttrs = -1;
adapForeground = adapBackground = adapFlash = adapDisplayAttrs = false;
}
// Level 1 set-after spacing attributes
if (c < 40 && m_rowHeight[r] != BottomHalf)
switch (m_levelOnePage->character(r, c)) {
case 0x00 ... 0x07: // Alphanumeric and foreground colour
level1Mosaics = false;
level1ForegroundCLUT = m_levelOnePage->character(r, c);
if (m_level >= 2)
m_level1ActivePainter.attribute.foregroundCLUT = level1ForegroundCLUT | m_foregroundRemap[m_levelOnePage->colourTableRemap()];
else
m_level1ActivePainter.attribute.foregroundCLUT = level1ForegroundCLUT;
m_level1ActivePainter.attribute.display.conceal = false;
// Switch from mosaics to alpha resets hold mosaic character
level1HoldMosaicCharacter = 0x20;
level1HoldMosaicSeparated = false;
break;
case 0x10 ... 0x17: // Mosaic and foreground colour
level1Mosaics = true;
level1ForegroundCLUT = m_levelOnePage->character(r, c) & 0x07;
if (m_level >= 2)
m_level1ActivePainter.attribute.foregroundCLUT = level1ForegroundCLUT | m_foregroundRemap[m_levelOnePage->colourTableRemap()];
else
m_level1ActivePainter.attribute.foregroundCLUT = level1ForegroundCLUT;
m_level1ActivePainter.attribute.display.conceal = false;
break;
case 0x08: // Flashing
m_level1ActivePainter.attribute.flash.mode = 1;
m_level1ActivePainter.attribute.flash.ratePhase = 0;
break;
case 0x0d: // Double height
if (!m_level1ActivePainter.attribute.display.doubleHeight || m_level1ActivePainter.attribute.display.doubleWidth) {
// Change of size resets hold mosaic character
level1HoldMosaicCharacter = 0x20;
level1HoldMosaicSeparated = false;
}
m_level1ActivePainter.attribute.display.doubleHeight = true;
m_level1ActivePainter.attribute.display.doubleWidth = false;
break;
case 0x0e: // Double width
if (m_level1ActivePainter.attribute.display.doubleHeight || !m_level1ActivePainter.attribute.display.doubleWidth) {
// Change of size resets hold mosaic character
level1HoldMosaicCharacter = 0x20;
level1HoldMosaicSeparated = false;
}
m_level1ActivePainter.attribute.display.doubleHeight = false;
m_level1ActivePainter.attribute.display.doubleWidth = true;
break;
case 0x0f: // Double size
if (!m_level1ActivePainter.attribute.display.doubleHeight || !m_level1ActivePainter.attribute.display.doubleWidth) {
// Change of size resets hold mosaic character
level1HoldMosaicCharacter = 0x20;
level1HoldMosaicSeparated = false;
}
m_level1ActivePainter.attribute.display.doubleHeight = true;
m_level1ActivePainter.attribute.display.doubleWidth = true;
break;
case 0x1b: // ESC/switch
level1EscapeSwitch ^= true;
if (level1EscapeSwitch)
level1CharSet = m_level1SecondCharSet;
else
level1CharSet = m_level1DefaultCharSet;
break;
case 0x1f: // Release mosaics
level1HoldMosaics = false;
break;
}
if (m_cell[r][c] != previousCellContents)
setRefresh(r, c, true);
}
}
inline void TeletextPageDecode::rotateFlashMovement(flashFunctions &flash)
{
if (flash.ratePhase == 4) {
flash.phase2HzShown++;
if (flash.phase2HzShown == 4)
flash.phase2HzShown = 1;
} else if (flash.ratePhase == 5) {
flash.phase2HzShown--;
if (flash.phase2HzShown == 0)
flash.phase2HzShown = 3;
}
}
QColor TeletextPageDecode::cellQColor(int r, int c, ColourPart colourPart)
{
const bool newsFlashOrSubtitle = m_levelOnePage->controlBit(PageBase::C5Newsflash) || m_levelOnePage->controlBit(PageBase::C6Subtitle);
int resultCLUT = 0;
switch (colourPart) {
case Foreground:
if (!m_cell[r][c].attribute.display.invert)
resultCLUT = m_cell[r][c].attribute.foregroundCLUT;
else
resultCLUT = m_cell[r][c].attribute.backgroundCLUT;
break;
case Background:
if (!m_cell[r][c].attribute.display.invert)
resultCLUT = m_cell[r][c].attribute.backgroundCLUT;
else
resultCLUT = m_cell[r][c].attribute.foregroundCLUT;
break;
case FlashForeground:
if (!m_cell[r][c].attribute.display.invert)
resultCLUT = m_cell[r][c].attribute.foregroundCLUT ^ 8;
else
resultCLUT = m_cell[r][c].attribute.backgroundCLUT ^ 8;
break;
}
if (resultCLUT == 8) {
// Transparent CLUT - either Full Row Colour or Video
// Logic of table C.1 in spec implemented to find out which it is
if (m_cell[r][c].attribute.display.boxingWindow != newsFlashOrSubtitle)
return QColor(Qt::transparent);
int rowColour;
if (cellCharacterFragment(r, c) == TeletextPageDecode::DoubleHeightBottomHalf ||
cellCharacterFragment(r, c) == TeletextPageDecode::DoubleSizeBottomLeftQuarter ||
cellCharacterFragment(r, c) == TeletextPageDecode::DoubleSizeBottomRightQuarter)
rowColour = m_fullRowColour[r-1];
else
rowColour = m_fullRowColour[r];
if (rowColour == 8)
return QColor(Qt::transparent);
else
return m_levelOnePage->CLUTtoQColor(rowColour, m_level);
} else if (!m_cell[r][c].attribute.display.boxingWindow && newsFlashOrSubtitle)
return QColor(Qt::transparent);
return m_levelOnePage->CLUTtoQColor(resultCLUT, m_level);
}
QColor TeletextPageDecode::cellForegroundQColor(int r, int c)
{
return cellQColor(r, c, Foreground);
}
QColor TeletextPageDecode::cellBackgroundQColor(int r, int c)
{
return cellQColor(r, c, Background);
}
QColor TeletextPageDecode::cellFlashForegroundQColor(int r, int c)
{
return cellQColor(r, c, FlashForeground);
}
TeletextPageDecode::textCell& TeletextPageDecode::cellAtCharacterOrigin(int r, int c)
{
switch (cellCharacterFragment(r, c)) {
case TeletextPageDecode::DoubleHeightBottomHalf:
case TeletextPageDecode::DoubleSizeBottomLeftQuarter:
return m_cell[r-1][c];
case TeletextPageDecode::DoubleWidthRightHalf:
case TeletextPageDecode::DoubleSizeTopRightQuarter:
return m_cell[r][c-1];
case TeletextPageDecode::DoubleSizeBottomRightQuarter:
return m_cell[r-1][c-1];
default:
return m_cell[r][c];
}
}
inline void TeletextPageDecode::setFullScreenColour(int newColour)
{
if (newColour == 8 || m_levelOnePage->controlBit(PageBase::C5Newsflash) || m_levelOnePage->controlBit(PageBase::C6Subtitle)) {
m_finalFullScreenQColor = QColor(0, 0, 0, 0);
emit fullScreenColourChanged(QColor(0, 0, 0, 0));
return;
}
QColor newFullScreenQColor = m_levelOnePage->CLUTtoQColor(newColour, m_level);
m_finalFullScreenColour = newColour;
if (m_finalFullScreenQColor != newFullScreenQColor) {
m_finalFullScreenQColor = newFullScreenQColor;
emit fullScreenColourChanged(m_finalFullScreenQColor);
}
}
inline void TeletextPageDecode::setFullRowColour(int row, int newColour)
{
m_fullRowColour[row] = newColour;
if (newColour == 8 || m_levelOnePage->controlBit(PageBase::C5Newsflash) || m_levelOnePage->controlBit(PageBase::C6Subtitle)) {
m_fullRowQColor[row] = QColor(0, 0, 0, 0);
emit fullRowColourChanged(row, QColor(0, 0, 0, 0));
return;
}
QColor newFullRowQColor = m_levelOnePage->CLUTtoQColor(newColour, m_level);
if (m_fullRowQColor[row] != newFullRowQColor) {
for (int c=0; c<72; c++) {
if (m_cell[row][c].attribute.foregroundCLUT == 8 || m_cell[row][c].attribute.backgroundCLUT == 8)
setRefresh(row, c, true);
}
m_fullRowQColor[row] = newFullRowQColor;
emit fullRowColourChanged(row, m_fullRowQColor[row]);
}
}