/* * 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 . */ #include "saveformats.h" #include #include #include #include #include "document.h" #include "hamming.h" #include "levelonepage.h" #include "pagebase.h" void SaveFormat::saveAllPages(QSaveFile &outFile, const TeletextDocument &document) { m_document = &document; m_outFile = &outFile; m_outStream.setDevice(m_outFile); m_warnings.clear(); m_error.clear(); writeDocumentStart(); writeAllPages(); writeDocumentEnd(); } void SaveFormat::saveCurrentSubPage(QSaveFile &outFile, const TeletextDocument &document) { m_document = &document; m_outFile = &outFile; m_outStream.setDevice(m_outFile); m_warnings.clear(); m_error.clear(); writeDocumentStart(); writeSubPage(*m_document->currentSubPage()); writeDocumentEnd(); } void SaveFormat::writeAllPages() { for (int p=0; pnumberOfSubPages(); p++) writeSubPage(*m_document->subPage(p), (m_document->numberOfSubPages() == 1) ? 0 : p+1); } void SaveFormat::writeSubPage(const PageBase &subPage, int subPageNumber) { writeSubPageStart(subPage, subPageNumber); writeSubPageBody(subPage); writeSubPageEnd(subPage); } void SaveFormat::writeSubPageBody(const PageBase &subPage) { writeX27Packets(subPage); writeX28Packets(subPage); if (m_document->pageFunction() == TeletextDocument::PFLevelOnePage) { writeX26Packets(subPage); writeX1to25Packets(subPage); } else { qDebug("Not LevelOnePage, assuming 7-bit packets!"); writeX1to25Packets(subPage); writeX26Packets(subPage); } } void SaveFormat::writeX27Packets(const PageBase &subPage) { for (int i=0; i<4; i++) if (subPage.packetExists(27, i)) writePacket(format4BitPacket(subPage.packet(27, i)), 27, i); for (int i=4; i<16; i++) if (subPage.packetExists(27, i)) writePacket(format18BitPacket(subPage.packet(27, i)), 27, i); } void SaveFormat::writeX28Packets(const PageBase &subPage) { for (int i=0; i<16; i++) if (subPage.packetExists(28, i)) writePacket(format18BitPacket(subPage.packet(28, i)), 28, i); } void SaveFormat::writeX26Packets(const PageBase &subPage) { for (int i=0; i<16; i++) if (subPage.packetExists(26, i)) writePacket(format18BitPacket(subPage.packet(26, i)), 26, i); } void SaveFormat::writeX1to25Packets(const PageBase &subPage) { for (int i=1; i<26; i++) if (subPage.packetExists(i)) // BUG must check m_document->packetCoding() !! writePacket(format7BitPacket(subPage.packet(i)), i); } int SaveFormat::writePacket(QByteArray packet, int packetNumber, int designationCode) { return writeRawData(packet, packet.size()); } int SaveFormat::writeRawData(const char *s, int len) { return m_outStream.writeRawData(s, len); } inline void SaveTTIFormat::writeString(const QString &command) { QByteArray result = command.toUtf8() + "\r\n"; writeRawData(result, result.size()); } void SaveTTIFormat::writeDocumentStart() { if (!m_document->description().isEmpty()) writeString(QString("DE,%1").arg(m_document->description())); // TODO DS and SP commands } QByteArray SaveTTIFormat::format7BitPacket(QByteArray packet) { for (int i=0; ipageNumber(), 3, 16, QChar('0')).arg(subPageNumber & 0xff, 2, 10, QChar('0'))); // Subpage // Magazine Organisation Table and Magazine Inventory Page don't have subpages if (m_document->pageFunction() != TeletextDocument::PFMOT && m_document->pageFunction() != TeletextDocument::PFMIP) writeString(QString("SC,%1").arg(subPageNumber, 4, 10, QChar('0'))); // Status bits // We assume that bit 15 "transmit page" is always wanted // C4 stored in bit 14 int statusBits = 0x8000 | (subPage.controlBit(PageBase::C4ErasePage) << 14); // C5 to C11 stored in order from bits 1 to 6 for (int i=PageBase::C5Newsflash; i<=PageBase::C11SerialMagazine; i++) statusBits |= subPage.controlBit(i) << (i-1); // NOS in bits 7 to 9 statusBits |= subPage.controlBit(PageBase::C12NOS) << 9; statusBits |= subPage.controlBit(PageBase::C13NOS) << 8; statusBits |= subPage.controlBit(PageBase::C14NOS) << 7; writeString(QString("PS,%1").arg(0x8000 | statusBits, 4, 16, QChar('0'))); if (m_document->pageFunction() == TeletextDocument::PFLevelOnePage) { // Level One Page: page region and cycle writeString(QString("RE,%1").arg(static_cast(&subPage)->defaultCharSet())); writeString(QString("CT,%1,%2").arg(static_cast(&subPage)->cycleValue()).arg(static_cast(&subPage)->cycleType()==LevelOnePage::CTcycles ? 'C' : 'T')); } else // Not a Level One Page: X/28/0 specifies page function and coding but the PF command // should make it obvious to a human that this is not a Level One Page writeString(QString("PF,%1,%2").arg(m_document->pageFunction()).arg(m_document->packetCoding())); } void SaveTTIFormat::writeSubPageBody(const PageBase &subPage) { // Header row if (subPage.packetExists(0)) writePacket(format7BitPacket(subPage.packet(0)), 0); // FLOF links bool writeFLCommand = false; if (m_document->pageFunction() == TeletextDocument::PFLevelOnePage && subPage.packetExists(27,0)) { // Subpage has FLOF links - if any link to a specific subpage we need to write X/27/0 // as raw, otherwise we write the links as a human-readable FL command later on writeFLCommand = true; // TODO uncomment this when we can edit FLOF subpage links /*for (int i=0; i<6; i++) if (document.subPage(p)->fastTextLinkSubPageNumber(i) != 0x3f7f) { writeFLCommand = false; break; }*/ } // Don't write X/27/0 if FL command will be written // but write the rest of the X/27 packets for (int i=(writeFLCommand ? 1 : 0); i<4; i++) if (subPage.packetExists(27, i)) writePacket(format4BitPacket(subPage.packet(27, i)), 27, i); for (int i=4; i<16; i++) if (subPage.packetExists(27, i)) writePacket(format18BitPacket(subPage.packet(27, i)), 27, i); // Now write the other packets like the rest of writeSubPageBody does writeX28Packets(subPage); if (m_document->pageFunction() == TeletextDocument::PFLevelOnePage) { writeX26Packets(subPage); writeX1to25Packets(subPage); } else { qDebug("Not LevelOnePage, assuming 7-bit packets!"); writeX1to25Packets(subPage); writeX26Packets(subPage); } if (writeFLCommand) { QString flofLine; flofLine.reserve(26); flofLine="FL,"; for (int i=0; i<6; i++) { // Stored as page link with relative magazine number, convert to absolute page number for display int absoluteLinkPageNumber = static_cast(&subPage)->fastTextLinkPageNumber(i) ^ (m_document->pageNumber() & 0x700); // Fix magazine 0 to 8 if ((absoluteLinkPageNumber & 0x700) == 0x000) absoluteLinkPageNumber |= 0x800; flofLine.append(QString("%1").arg(absoluteLinkPageNumber, 3, 16, QChar('0'))); if (i < 5) flofLine.append(','); } writeString(flofLine); } } int SaveTTIFormat::writePacket(QByteArray packet, int packetNumber, int designationCode) { if (designationCode != -1) packet[0] = designationCode | 0x40; packet.prepend("OL," + QByteArray::number(packetNumber) + ","); packet.append("\r\n"); return(writeRawData(packet, packet.size())); } void SaveM29Format::writeSubPageStart(const PageBase &subPage, int subPageNumber) { Q_UNUSED(subPageNumber); // Force page number to 0xFF and subpage to 0 writeString(QString("PN,%1ff00").arg(m_document->pageNumber() >> 8, 1, 16)); // Not sure if this PS forcing is necessary writeString("PS,8000"); } void SaveM29Format::writeSubPageBody(const PageBase &subPage) { if (subPage.packetExists(28, 0)) writePacket(format18BitPacket(subPage.packet(28, 0)), 29, 0); if (subPage.packetExists(28, 1)) writePacket(format18BitPacket(subPage.packet(28, 1)), 29, 1); if (subPage.packetExists(28, 4)) writePacket(format18BitPacket(subPage.packet(28, 4)), 29, 4); } QByteArray SaveT42Format::format7BitPacket(QByteArray packet) { // Odd parity encoding for (int c=0; c> 4; p ^= p >> 2; p ^= p >> 1; // If last bit left is 0 then it started with an even number of bits, so do the odd parity if (!(p & 1)) packet[c] = packet.at(c) | 0x80; } return packet; } QByteArray SaveT42Format::format4BitPacket(QByteArray packet) { for (int c=0; c> 0) & 0xff] ^ hamming_24_18_forward[1][(toEncode >> 8) & 0xff] ^ hamming_24_18_forward_2[(toEncode >> 16) & 0x03]); packet[c] = Byte_0; D5_D11 = (toEncode >> 4) & 0x7f; D12_D18 = (toEncode >> 11) & 0x7f; P5 = 0x80 & ~(hamming_24_18_parities[0][D12_D18] << 2); packet[c+1] = D5_D11 | P5; P6 = 0x80 & ((hamming_24_18_parities[0][Byte_0] ^ hamming_24_18_parities[0][D5_D11]) << 2); packet[c+2] = D12_D18 | P6; } return packet; } int SaveT42Format::writePacket(QByteArray packet, int packetNumber, int designationCode) { // Byte 2 - designation code if (designationCode != - 1) packet[0] = hamming_8_4_encode[designationCode]; // Byte 1 of MRAG packet.prepend(hamming_8_4_encode[packetNumber >> 1]); // Byte 0 of MRAG packet.prepend(hamming_8_4_encode[m_magazineNumber | ((packetNumber & 0x01) << 3)]); return(writeRawData(packet, packet.size())); } void SaveT42Format::writeSubPageStart(const PageBase &subPage, int subPageNumber) { QByteArray packet; // Convert integer to Binary Coded Decimal subPageNumber = QString::number(subPageNumber).toInt(nullptr, 16); m_magazineNumber = (m_document->pageNumber() & 0xf00) >> 8; if (m_magazineNumber == 8) m_magazineNumber = 0; // Retrieve and apply odd parity to header row if there's text there, // otherwise create an initial packet of (odd parity valid) spaces if (subPage.packetExists(0)) packet = format7BitPacket(subPage.packet(0)); else packet.fill(0x20, 40); // Byte 1 of MRAG - packet number 0 packet.prepend((char)0); // Byte 0 of MRAG - magazine number, and packet number 0 packet.prepend(m_magazineNumber & 0x07); packet[2] = m_document->pageNumber() & 0x00f; packet[3] = (m_document->pageNumber() & 0x0f0) >> 4; packet[4] = subPageNumber & 0xf; packet[5] = ((subPageNumber >> 4) & 0x7) | (subPage.controlBit(PageBase::C4ErasePage) << 3); packet[6] = ((subPageNumber >> 8) & 0xf); packet[7] = ((subPageNumber >> 12) & 0x3) | (subPage.controlBit(PageBase::C5Newsflash) << 2) | (subPage.controlBit(PageBase::C6Subtitle) << 3); packet[8] = subPage.controlBit(PageBase::C7SuppressHeader) | (subPage.controlBit(PageBase::C8Update) << 1) | (subPage.controlBit(PageBase::C9InterruptedSequence) << 2) | (subPage.controlBit(PageBase::C10InhibitDisplay) << 3); packet[9] = subPage.controlBit(PageBase::C11SerialMagazine) | (subPage.controlBit(PageBase::C14NOS) << 1) | (subPage.controlBit(PageBase::C13NOS) << 2) | (subPage.controlBit(PageBase::C12NOS) << 3); for (int i=0; i<10; i++) packet[i] = hamming_8_4_encode[(int)packet.at(i)]; writeRawData(packet.constData(), 42); } int SaveHTTFormat::writeRawData(const char *s, int len) { char httLine[45]; httLine[0] = 0xaa; httLine[1] = 0xaa; httLine[2] = 0xe4; for (int i=0; i<42; i++) { unsigned char b = s[i]; b = (b & 0xf0) >> 4 | (b & 0x0f) << 4; b = (b & 0xcc) >> 2 | (b & 0x33) << 2; b = (b & 0xaa) >> 1 | (b & 0x55) << 1; httLine[i+3] = b; } return m_outStream.writeRawData(httLine, len+3); } bool SaveEP1Format::getWarnings(const PageBase &subPage) { m_warnings.clear(); if (!m_languageCode.contains((static_cast(&subPage)->defaultCharSet() << 3) | static_cast(&subPage)->defaultNOS())) m_warnings.append("Page language not supported, will be exported as English."); if (subPage.packetExists(24) || subPage.packetExists(27, 0)) m_warnings.append("FLOF display row and page links will not be exported."); if (subPage.packetExists(27, 4) || subPage.packetExists(27, 5)) m_warnings.append("X/27/4-5 compositional links will not be exported."); if (subPage.packetExists(28, 0) || subPage.packetExists(28, 4)) m_warnings.append("X/28 page enhancements will not be exported."); return (!m_warnings.isEmpty()); } QByteArray SaveEP1Format::format18BitPacket(QByteArray packet) { for (int c=1; c> 5); packet[c+1] = packet.at(c+1) & 0x1f; // Address of termination marker is 7f instead of 3f if (packet.at(c+1) == 0x1f && packet.at(c) == 0x3f) packet[c] = 0x7f; } return packet; } void SaveEP1Format::writeSubPageStart(const PageBase &subPage, int subPageNumber) { Q_UNUSED(subPageNumber); QByteArray pageStart(3, 0x00); // First two bytes always 0xfe, 0x01 pageStart[0] = 0xfe; pageStart[1] = 0x01; // Next byte is language code unique to EP1 // Unknown values are mapped to English, after warning the user pageStart[2] = m_languageCode.value((static_cast(&subPage)->defaultCharSet() << 3) | static_cast(&subPage)->defaultNOS(), 0x09); writeRawData(pageStart.constData(), 3); } void SaveEP1Format::writeSubPageBody(const PageBase &subPage) { // Following the first three bytes already written by writeSubPageStart, // the next three bytes for pages with X/26 packets are 0xca // then little-endian offset to start of Level 1 page data. // For pages with no X/26 packets, just three zeroes. QByteArray offsetData(4, 0x00); int numOfX26Packets = 0; if (subPage.packetExists(26, 0)) { offsetData[0] = 0xca; while (subPage.packetExists(26, numOfX26Packets)) numOfX26Packets++; const int level1Offset = numOfX26Packets * 40 + 4; offsetData[1] = level1Offset & 0xff; offsetData[2] = level1Offset >> 8; } writeRawData(offsetData.constData(), 3); // We should really re-implement writeX26Packets but I can't be bothered // to count the number of X/26 packets twice... if (numOfX26Packets > 0) { // Reuse offsetData for this 4-byte header of the enhancement data // Bytes are 0xc2, 0x00, then little-endian length of enhancement data offsetData[0] = 0xc2; offsetData[1] = 0x00; offsetData[2] = (numOfX26Packets * 40) & 0xff; offsetData[3] = (numOfX26Packets * 40) >> 8; writeRawData(offsetData.constData(), 4); for (int i=0; ifileDialogFilter()); if (i < s_nativeSize) { if (i != 0) s_filters.append(";;"); s_filters.append(s_fileFormat[i]->fileDialogFilter()); } } } s_instances++; } SaveFormats::~SaveFormats() { s_instances--; if (s_instances == 0) for (int i=s_size-1; i>=0; i--) delete s_fileFormat[i]; } SaveFormat *SaveFormats::findFormat(const QString &suffix) const { // TTI is the only official save format at the moment... // for (int i=0; iextensions().contains(suffix, Qt::CaseInsensitive)) // return s_fileFormat[i]; if (s_fileFormat[0]->extensions().contains(suffix, Qt::CaseInsensitive)) return s_fileFormat[0]; return nullptr; } SaveFormat *SaveFormats::findExportFormat(const QString &suffix) const { for (int i=0; iextensions().contains(suffix, Qt::CaseInsensitive)) return s_fileFormat[i]; return nullptr; }