/* * Copyright (C) 2020 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 #include #include #include #include #include "levelonepage.h" TeletextPage::TeletextPage() { m_paddingX26Triplet.setAddress(41); m_paddingX26Triplet.setMode(0x1e); m_paddingX26Triplet.setData(0); localEnhance.reserve(208); clearPage(); } // So far we only call clearPage() once, within the constructor void TeletextPage::clearPage() { for (int r=0; r<25; r++) for (int c=0; c<40; c++) m_level1Page[r][c] = 0x20; for (int i=0; i<8; i++) m_controlBits[i] = false; /* m_subPageNumber = 0x0000; */ m_cycleValue = 8; m_cycleType = CTseconds; m_defaultCharSet = 0; m_defaultNOS = 0; m_secondCharSet = 0xf; m_secondNOS = 0x7; m_defaultScreenColour = 0; m_defaultRowColour = 0; m_blackBackgroundSubst = false; m_colourTableRemap = 0; m_leftSidePanelDisplayed = m_rightSidePanelDisplayed = false; m_sidePanelStatusL25 = true; m_sidePanelColumns = 0; std::copy(defaultCLUT, defaultCLUT+32, m_CLUT); // If clearPage() is called outside constructor, we need to implement localEnhance.clear(); } bool TeletextPage::setPacket(int packetNumber, QByteArray packetContents) { if (packetNumber <= 24) { for (int c=0; c<40; c++) m_level1Page[packetNumber][c] = packetContents.at(c); return true; } return PageBase::setPacket(packetNumber, packetContents); } bool TeletextPage::setPacket(int packetNumber, int designationCode, QByteArray packetContents) { if (packetNumber == 26) { // Preallocate entries in the localEnhance list to hold our incoming triplets. // We write "dummy" reserved 11110 Row Triplets in the allocated entries which then get overwritten by the packet contents. // This is in case of missing packets so we can keep Local Object pointers valid. while (localEnhance.size() < (designationCode+1)*13) localEnhance.append(m_paddingX26Triplet); int enhanceListPointer; X26Triplet newX26Triplet; for (int i=0; i<13; i++) { enhanceListPointer = designationCode*13+i; newX26Triplet.setAddress(packetContents.at(i*3+1) & 0x3f); newX26Triplet.setMode(packetContents.at(i*3+2) & 0x1f); newX26Triplet.setData(((packetContents.at(i*3+3) & 0x3f) << 1) | ((packetContents.at(i*3+2) & 0x20) >> 5)); localEnhance[enhanceListPointer] = newX26Triplet; } if (newX26Triplet.mode() == 0x1f && newX26Triplet.address() == 0x3f && newX26Triplet.data() & 0x01) // Last triplet was a Termination Marker (without ..follows) so clean up the repeated ones while (localEnhance.size()>1 && localEnhance.at(localEnhance.size()-2).mode() == 0x1f && localEnhance.at(localEnhance.size()-2).address() == 0x3f && localEnhance.at(localEnhance.size()-2).data() == newX26Triplet.data()) localEnhance.removeLast(); return true; } if (packetNumber == 28 && (designationCode == 0 || designationCode == 4)) { int CLUToffset = (designationCode == 0) ? 16 : 0; m_defaultCharSet = ((packetContents.at(2) >> 4) & 0x3) | ((packetContents.at(3) << 2) & 0xc); m_defaultNOS = (packetContents.at(2) >> 1) & 0x7; m_secondCharSet = ((packetContents.at(3) >> 5) & 0x1) | ((packetContents.at(4) << 1) & 0xe); m_secondNOS = (packetContents.at(3) >> 2) & 0x7; m_leftSidePanelDisplayed = (packetContents.at(4) >> 3) & 1; m_rightSidePanelDisplayed = (packetContents.at(4) >> 4) & 1; m_sidePanelStatusL25 = (packetContents.at(4) >> 5) & 1; m_sidePanelColumns = packetContents.at(5) & 0xf; for (int c=0; c<16; c++) m_CLUT[CLUToffset+c] = ((packetContents.at(c*2+5) << 4) & 0x300) | ((packetContents.at(c*2+6) << 10) & 0xc00) | ((packetContents.at(c*2+6) << 2) & 0x0f0) | (packetContents.at(c*2+7) & 0xf); m_defaultScreenColour = (packetContents.at(37) >> 4) | ((packetContents.at(38) << 2) & 0x1c); m_defaultRowColour = ((packetContents.at(38)) >> 3) | ((packetContents.at(39) << 3) & 0x18); m_blackBackgroundSubst = (packetContents.at(39) >> 2) & 1; m_colourTableRemap = (packetContents.at(39) >> 3) & 7; return true; } qDebug("LevelOnePage unhandled packet X%d/%d", packetNumber, designationCode); return PageBase::setPacket(packetNumber, designationCode, packetContents); } bool TeletextPage::packetNeeded(int packetNumber, int designationCode) const { if (packetNumber <= 24) { for (int c=0; c<40; c++) if (m_level1Page[packetNumber][c] != 0x20) return true; return false; } // TODO packets 26 and 27 if (packetNumber == 28) { if (designationCode == 0) { if (m_leftSidePanelDisplayed || m_rightSidePanelDisplayed || m_defaultScreenColour !=0 || m_defaultRowColour !=0 || m_blackBackgroundSubst || m_colourTableRemap !=0 || m_defaultCharSet != 0 || m_secondCharSet != 0xf) return true; for (int i=16; i<32; i++) if (m_CLUT[i] != defaultCLUT[i]) return true; return false; } if (designationCode == 4) { for (int i=0; i<16; i++) if (m_CLUT[i] != defaultCLUT[i]) return true; return false; } } return true; } void TeletextPage::loadPagePacket(QByteArray &inLine) { bool lineNumberOk; int lineNumber, secondCommaPosition; secondCommaPosition = inLine.indexOf(",", 3); if (secondCommaPosition != 4 && secondCommaPosition != 5) return; lineNumber = inLine.mid(3, secondCommaPosition-3).toInt(&lineNumberOk, 10); if (lineNumberOk && lineNumber>=0 && lineNumber<=29) { inLine.remove(0, secondCommaPosition+1); if (lineNumber <= 25) { for (int c=0; c<40; c++) { // trimmed() helpfully removes CRLF line endings from the just-read line // but it also (un)helpfully removes spaces at end of a line, so put them back if (c >= inLine.size()) inLine.append(' '); if (inLine.at(c) & 0x80) inLine[c] = inLine.at(c) & 0x7f; else if (inLine.at(c) == 0x10) inLine[c] = 0x0d; else if (inLine.at(c) == 0x1b) { inLine.remove(c, 1); inLine[c] = inLine.at(c) & 0xbf; } } setPacket(lineNumber, inLine); } else { int designationCode = inLine.at(0) & 0x3f; for (int i=1; i<=39; i++) inLine[i] = inLine.at(i) & 0x3f; setPacket(lineNumber, designationCode, inLine); } } } // This will be gradually be converted to just getting the raw packets from the Page class and writing out a TTI file. void TeletextPage::savePage(QTextStream *outStream, int pageNumber, int subPageNumber) { // int pageStatus = 0x8000 | (controlBits[0] << 14) | ((defaultPageNOS & 1) << 9) | ((defaultPageNOS & 2) << 7) | ((defaultPageNOS & 4) << 5); // for (int i=1; i<8; i++) // pageStatus |= controlBits[i] << i; *outStream << QString("PN,%1%2").arg(pageNumber, 3, 16, QChar('0')).arg(subPageNumber & 0xff, 2, 16, QChar('0')) << endl; *outStream << QString("SC,%1").arg(subPageNumber, 4, 16, QChar('0')) << endl; *outStream << QString("PS,%1").arg(0x8000 | controlBitsToPS(), 4, 16, QChar('0')) << endl; *outStream << QString("CT,%1,%2").arg(m_cycleValue).arg(m_cycleType==CTcycles ? 'C' : 'T') << endl; //TODO RE and maybe FLOF? if (packetNeeded(28, 0)) *outStream << "OL,28," << x28toTTI(0) << endl; if (packetNeeded(28, 4)) *outStream << "OL,28," << x28toTTI(4) << endl; if (!localEnhance.isEmpty()) { int tripletNumber = 0; X26Triplet lastTriplet = localEnhance.at(localEnhance.size()-1); int terminatorNeeded = true; // Becomes false if a termination marker (without a "...follow") is already at the end if (lastTriplet.mode() == 0x1f && lastTriplet.address() == 0x3f) if (lastTriplet.data() & 0x01) terminatorNeeded = false; else // Last termination marker has "follow" set but nothing follows, so write another one afterwards lastTriplet.setData(lastTriplet.data() | 0x01); else { // No termination marker there, so make up one lastTriplet.setAddress(0x3f); lastTriplet.setMode(0x1f); lastTriplet.setData(0x07); } for (int d=0; d<16; d++) { *outStream << "OL,26," << (char)(d | 0x40); for (int t=0; t<13; t++) { if (tripletNumber < localEnhance.size()) { *outStream << (char)(0x40 | localEnhance.at(tripletNumber).address()); *outStream << (char)(0x40 | (localEnhance.at(tripletNumber).mode() | ((localEnhance.at(tripletNumber).data() & 1) << 5))); *outStream << (char)(0x40 | (localEnhance.at(tripletNumber).data() >> 1)); } else { *outStream << (char)(0x40 | lastTriplet.address()); *outStream << (char)(0x40 | (lastTriplet.mode() | ((lastTriplet.data() & 1) << 5))); *outStream << (char)(0x40 | (lastTriplet.data() >> 1)); terminatorNeeded = false; } tripletNumber++; } *outStream << endl; // If the last triplet of the last X26 row wasn't a termination marker, // terminatorNeeded ensures we write an additional X26 row full of termination markers. if (!terminatorNeeded && tripletNumber >= localEnhance.size()) break; } } for (int r=1; r<25; r++) if (packetNeeded(r)) { QString rowString; rowString.append(QString("OL,%1,").arg(r)); for (int c=0; c<40; c++) { unsigned char myChar = m_level1Page[r][c]; if (myChar < 32) { rowString.append((char)0x1b); rowString.append((char)(myChar+0x40)); } else rowString.append((char)myChar); } *outStream << rowString << endl; } } int TeletextPage::controlBitsToPS() const { //TODO map page language for regions other than 0 int pageStatus = 0x8000 | (m_controlBits[0] << 14) | ((m_defaultNOS & 1) << 9) | ((m_defaultNOS & 2) << 7) | ((m_defaultNOS & 4) << 5); for (int i=1; i<8; i++) pageStatus |= m_controlBits[i] << (i-1); return pageStatus; } QString TeletextPage::x28toTTI(int designationCode) { QString result; int x28Triplets[13] = {0}; int offset; switch (designationCode) { case 0: offset = 16; break; case 4: offset = 0; break; } result.append(designationCode + 0x40); x28Triplets[0] = ((m_secondCharSet & 1) << 17) | (m_secondNOS << 14) | (m_defaultCharSet << 10) | (m_defaultNOS << 7); x28Triplets[1] = (m_secondCharSet >> 1) | (m_leftSidePanelDisplayed << 3) | (m_rightSidePanelDisplayed << 4) | (m_sidePanelStatusL25 << 5) | (m_sidePanelColumns << 6); for (int c=0; c<16; c++){ int r = (m_CLUT[offset+c] & 0xF00) >> 8; int g = (m_CLUT[offset+c] & 0xF0) >> 4; int b = m_CLUT[offset+c] & 0xF; int rtr = ((c * 12) + 28) / 18; int rsh = ((c * 12) + 28) % 18; x28Triplets[rtr] |= (r << rsh); if (rsh == 16) x28Triplets[rtr+1] |= (r >> 2) & 3; int gtr = ((c * 12) + 32) / 18; int gsh = ((c * 12) + 32) % 18; x28Triplets[gtr] |= (g << gsh); if (gsh == 16) x28Triplets[gtr+1] |= (g >> 2) & 3; int btr = ((c * 12) + 36) / 18; int bsh = ((c * 12) + 36) % 18; x28Triplets[btr] |= (b << bsh); if (bsh == 16) x28Triplets[btr+1] |= (b >> 2) & 3; } x28Triplets[12] |= (m_defaultScreenColour << 4) | (m_defaultRowColour << 9) | (m_blackBackgroundSubst << 14) | (m_colourTableRemap << 15); for (int i=0; i<13; i++) { result.append(0x40 | (x28Triplets[i] & 0x3F)); result.append(0x40 | ((x28Triplets[i] & 0xFC0) >> 6)); result.append(0x40 | ((x28Triplets[i] & 0x3F000) >> 12)); } return result; } QString TeletextPage::exportURLHash(QString pageHash) { int hashDigits[1167]={0}; int totalBits, charBit; const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; //TODO deal with "black text allowed" pageHash.append(QString("#%1:").arg(m_defaultNOS, 1, 16)); for (int r=0; r<25; r++) for (int c=0; c<40; c++) for (int b=0; b<7; b++) { totalBits = (r * 40 + c) * 7 + b; charBit = ((m_level1Page[r][c]) >> (6 - b)) & 0x01; hashDigits[totalBits / 6] |= charBit << (5 - (totalBits % 6)); } for (int i=0; i<1167; i++) pageHash.append(base64[hashDigits[i]]); // CLUTChangedResult = CLUTChanged(); // if (leftSidePanel || rightSidePanel || defaultScreenColour !=0 || defaultRowColour !=0 || blackBackgroundSubst || colourTableRemap !=0 || CLUTChangedResult) { if (packetNeeded(28,0) || packetNeeded(28,4)) { QString x28StringBegin, x28StringEnd; x28StringBegin.append(QString("00%1").arg((m_defaultCharSet << 3) | m_defaultNOS, 2, 16, QChar('0')).toUpper()); x28StringBegin.append(QString("%1").arg((m_secondCharSet << 3) | m_secondNOS, 2, 16, QChar('0')).toUpper()); x28StringBegin.append(QString("%1%2%3%4").arg(m_leftSidePanelDisplayed, 1, 10).arg(m_rightSidePanelDisplayed, 1, 10).arg(m_sidePanelStatusL25, 1, 10).arg(m_sidePanelColumns, 1, 16)); x28StringEnd = QString("%1%2%3%4").arg(m_defaultScreenColour, 2, 16, QChar('0')).arg(m_defaultRowColour, 2, 16, QChar('0')).arg(m_blackBackgroundSubst, 1, 10).arg(m_colourTableRemap, 1, 10); if (packetNeeded(28,0)) { pageHash.append(":X280="); pageHash.append(x28StringBegin); pageHash.append(colourHash(1)); pageHash.append(x28StringEnd); } if (packetNeeded(28,4)) { pageHash.append(":X284="); pageHash.append(x28StringBegin); pageHash.append(colourHash(0)); pageHash.append(x28StringEnd); } } if (!localEnhance.isEmpty()) { pageHash.append(":X26="); for (int i=0; i> 1]); pageHash.append(base64[localEnhance.at(i).mode() | ((localEnhance.at(i).data() & 1) << 5)]); pageHash.append(base64[localEnhance.at(i).address()]); } //TODO need to add one or more terminators to X26 } //TODO check if 0x8000 | is needed for zxnet pageHash.append(QString(":PS=%1").arg(0x8000 | controlBitsToPS(), 0, 16, QChar('0'))); return pageHash; } /* void TeletextPage::setSubPageNumber(int newSubPageNumber) { m_subPageNumber = newSubPageNumber; } */ void TeletextPage::setControlBit(int bitNumber, bool active) { m_controlBits[bitNumber] = active; } void TeletextPage::setCycleValue(int newValue) { m_cycleValue = newValue; }; void TeletextPage::setCycleType(CycleTypeEnum newType) { m_cycleType = newType; } void TeletextPage::setDefaultCharSet(int newDefaultCharSet) { m_defaultCharSet = newDefaultCharSet; } void TeletextPage::setDefaultNOS(int newDefaultNOS) { m_defaultNOS = newDefaultNOS; } void TeletextPage::setSecondCharSet(int newSecondCharSet) { m_secondCharSet = newSecondCharSet; if (m_secondCharSet == 0xf) m_secondNOS = 0x7; } void TeletextPage::setSecondNOS(int newSecondNOS) { m_secondNOS = newSecondNOS; } void TeletextPage::setCharacter(int row, int column, unsigned char newCharacter) { m_level1Page[row][column] = newCharacter; } void TeletextPage::setDefaultScreenColour(int newDefaultScreenColour) { m_defaultScreenColour = newDefaultScreenColour; } void TeletextPage::setDefaultRowColour(int newDefaultRowColour) { m_defaultRowColour = newDefaultRowColour; } void TeletextPage::setColourTableRemap(int newColourTableRemap) { m_colourTableRemap = newColourTableRemap; } void TeletextPage::setBlackBackgroundSubst(bool newBlackBackgroundSubst) { m_blackBackgroundSubst = newBlackBackgroundSubst; } int TeletextPage::CLUT(int index, int renderLevel) const { if (renderLevel == 2) return index>=16 ? m_CLUT[index] : defaultCLUT[index]; else return renderLevel==3 ? m_CLUT[index] : defaultCLUT[index]; } void TeletextPage::setCLUT(int index, int newColour) { m_CLUT[index] = newColour; } void TeletextPage::setLeftSidePanelDisplayed(bool newLeftSidePanelDisplayed) { m_leftSidePanelDisplayed = newLeftSidePanelDisplayed; } void TeletextPage::setRightSidePanelDisplayed(bool newRightSidePanelDisplayed) { m_rightSidePanelDisplayed = newRightSidePanelDisplayed; } void TeletextPage::setSidePanelColumns(int newSidePanelColumns) { m_sidePanelColumns = newSidePanelColumns; } void TeletextPage::setSidePanelStatusL25(bool newSidePanelStatusL25) { m_sidePanelStatusL25 = newSidePanelStatusL25; } QString TeletextPage::colourHash(int whichCLUT) { QString resultHash; for (int i=whichCLUT*16; i> 8) * 17, ((myColour & 0x0f0) >> 4) * 17, (myColour & 0x00f) * 17); }