Files
QTeletextMaker/src/qteletextdecoder/decode.cpp
2024-12-31 10:51:06 +00:00

1169 lines
42 KiB
C++

/*
* Copyright (C) 2020-2025 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::clear()
{
m_characterMap.clear();
m_attributeMap.clear();
m_rightMostColumn.clear();
m_fullScreenCLUT = -1;
m_fullRowCLUTMap.clear();
}
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()-1;
else
endTripletNumber = m_endTripletNumber;
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 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;
case 0x20: // Foreground colour
case 0x23: // Background colour
case 0x27: // Additional flash functions
case 0x28: // Modified G0 and G2 character set designation
case 0x2c: // Display attributes
case 0x2e: // Font style
m_attributeMap.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 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
m_characterMap.insert(qMakePair(targetRow, targetColumn), triplet);
m_rightMostColumn.insert(targetRow, targetColumn);
break;
default:
if (triplet.modeExt() >= 0x30 && triplet.modeExt() <= 0x3f) {
// G0 character with diacritical
m_characterMap.insert(qMakePair(targetRow, targetColumn), triplet);
m_rightMostColumn.insert(targetRow, targetColumn);
}
}
}
}
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.setProportionalRows[c] = 0;
s_blankPainter.clearProportionalRows[c] = 0;
s_blankPainter.setBoldRows[c] = 0;
s_blankPainter.clearBoldRows[c] = 0;
s_blankPainter.setItalicRows[c] = 0;
s_blankPainter.clearItalicRows[c] = 0;
}
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_cellLevel1MosaicAttr[r][c] = false;
m_cellLevel1MosaicChar[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()) {
invocation.clear();
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)
{
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 = { charCode, 2, 0 };
break;
default:
if (triplet.modeExt() >= 0x30 && triplet.modeExt() <= 0x3f)
// G0 character with diacritical
result = { charCode, 0, triplet.mode() & 0xf };
}
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 = 0;
result.diacritical = 0;
break;
case 0x29: // G0 character
result = { charCode, 0, 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_defaultG0andG2 = (m_levelOnePage->defaultCharSet() << 3) | m_levelOnePage->defaultNOS();
m_secondG0andG2 = -1;
m_level1DefaultCharSet = m_level1CharacterMap.value(m_defaultG0andG2, 0);
if (m_levelOnePage->secondCharSet() != 0xf)
m_level1SecondCharSet = m_level1CharacterMap.value((m_levelOnePage->secondCharSet() << 3) | m_levelOnePage->secondNOS(), 0);
else
m_level1SecondCharSet = m_level1DefaultCharSet;
// 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;
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;
bool adapStyle = 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;
m_level1ActivePainter.result.g0Set = m_g0CharacterMap.value(m_defaultG0andG2, 0);
m_level1ActivePainter.result.g2Set = m_g2CharacterMap.value(m_defaultG0andG2, 7);
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;
m_level1ActivePainter.attribute.style.proportional = false;
m_level1ActivePainter.attribute.style.bold = false;
m_level1ActivePainter.attribute.style.italic = 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];
if (m_level == 3) {
// Reset font style "row spread" at start of row and side panels
if (c == 0 || c == 40 || c == 56)
painter->styleSpreadRows = 0;
// Apply any font style attributes from previous rows
// For m_level1ActivePainter, ensure we deal with font style row counters only once
if (t >= 1 || i == 0) {
if (painter->clearProportionalRows[c] != 0) {
painter->attribute.style.proportional = false;
painter->clearProportionalRows[c]--;
}
if (painter->setProportionalRows[c] != 0) {
painter->attribute.style.proportional = true;
painter->setProportionalRows[c]--;
}
if (painter->clearBoldRows[c] != 0) {
painter->attribute.style.bold = false;
painter->clearBoldRows[c]--;
}
if (painter->setBoldRows[c] != 0) {
painter->attribute.style.bold = true;
painter->setBoldRows[c]--;
}
if (painter->clearItalicRows[c] != 0) {
painter->attribute.style.italic = false;
painter->clearItalicRows[c]--;
}
if (painter->setItalicRows[c] != 0) {
painter->attribute.style.italic = true;
painter->setItalicRows[c]--;
}
}
}
// 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;
if (!adapStyle)
painter->attribute.style = m_level1ActivePainter.attribute.style;
}
// 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 0x28: // Modified G0 and G2 character set designation
if (m_level == 3 || triplet.data() == m_defaultG0andG2 || triplet.data() == m_secondG0andG2) {
painter->result.g0Set = m_g0CharacterMap.value(triplet.data(), 0);
painter->result.g2Set = m_g2CharacterMap.value(triplet.data(), 7);
} else if (m_secondG0andG2 == -1) {
m_secondG0andG2 = triplet.data();
painter->result.g0Set = m_g0CharacterMap.value(triplet.data(), 0);
painter->result.g2Set = m_g2CharacterMap.value(triplet.data(), 7);
}
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;
case 0x2e: // Font style
if (m_level != 3)
break;
if (applyAdapt)
adapStyle = true;
painter->attribute.style.proportional = triplet.data() & 0x01;
painter->attribute.style.bold = triplet.data() & 0x02;
painter->attribute.style.italic = triplet.data() & 0x04;
painter->styleSpreadRows = triplet.data() >> 4;
break;
}
}
painter->result.attribute = painter->attribute;
// Font style attribute that spreads across more than one row
if (m_level == 3 && painter->styleSpreadRows != 0) {
if (painter->attribute.style.proportional)
painter->setProportionalRows[c] = painter->styleSpreadRows;
else
painter->clearProportionalRows[c] = painter->styleSpreadRows;
if (painter->attribute.style.bold)
painter->setBoldRows[c] = painter->styleSpreadRows;
else
painter->clearBoldRows[c] = painter->styleSpreadRows;
if (painter->attribute.style.italic)
painter->setItalicRows[c] = painter->styleSpreadRows;
else
painter->clearItalicRows[c] = painter->styleSpreadRows;
}
}
}
// Level 1 character
if (c < 40) {
m_cellLevel1CharSet[r][c] = level1CharSet;
m_cellLevel1MosaicAttr[r][c] = level1Mosaics;
// Set to true on mosaic CHARACTER - not on blast through alphanumerics
m_cellLevel1MosaicChar[r][c] = level1Mosaics && (m_levelOnePage->character(r, c) & 0x20);
}
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);
if (m_cellLevel1MosaicChar[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 };
// 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));
if (result.code != 0x00) {
m_level1ActivePainter.result.character = result;
if (result.set == 0)
m_level1ActivePainter.result.character.set = m_level1ActivePainter.result.g0Set;
else if (result.set == 2)
m_level1ActivePainter.result.character.set = m_level1ActivePainter.result.g2Set;
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));
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;
switch (result.set) {
case 0:
painter->result.character.set = painter->result.g0Set;
break;
case 2:
painter->result.character.set = painter->result.g2Set;
break;
case 24:
if (painter->attribute.display.underlineSeparated)
painter->result.character.set = 25;
break;
}
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;
// The right half of this "if" statement (without the t != 2) is meant
// to a fix a bug where the bottom half of double-size characters in
// Passive Objects didn't appear.
// But the fix also caused the bottom right quarter of double-size
// characters in Active Objects to go missing when they overlapped
// the bottom half of a Level 1 double-height row.
// Hence the t != 2
if (t != 2 || painter->bottomHalfCell[c+1].character.code == 0x00) {
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 = adapStyle = false;
}
// Level 1 set-after spacing attributes
if (c < 40 && m_rowHeight[r] != BottomHalf)
switch (m_levelOnePage->character(r, c)) {
case 0x00:
case 0x01:
case 0x02:
case 0x03:
case 0x04:
case 0x05:
case 0x06:
case 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:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 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]);
}
}