/* * 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 . */ #include "decode.h" #include #include 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_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()) { invocation.clear(); return; } int i; for (i=invocation.startTripletNumber(); isize(); 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 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= 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 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 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_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)); 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; iresult.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; irightHalfCell.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]); } }