diff --git a/src/qteletextmaker/hashformats.cpp b/src/qteletextmaker/hashformats.cpp
new file mode 100644
index 0000000..64b6cf4
--- /dev/null
+++ b/src/qteletextmaker/hashformats.cpp
@@ -0,0 +1,117 @@
+/*
+ * 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 "hashformats.h"
+
+#include
+#include
+
+#include "levelonepage.h"
+#include "pagebase.h"
+
+QString exportHashStringPage(LevelOnePage *subPage)
+{
+ int hashDigits[1167]={0};
+ int totalBits, charBit;
+ const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+ QString hashString;
+ QByteArray rowPacket;
+
+ // TODO int editTFCharacterSet = 5;
+ bool blackForeground = false;
+
+ for (int r=0; r<25; r++) {
+ if (subPage->packetExists(r))
+ rowPacket = subPage->packet(r);
+ else
+ rowPacket = QByteArray(40, 0x20).constData();
+
+ for (int c=0; c<40; c++) {
+ if (rowPacket.at(c) == 0x00 || rowPacket.at(c) == 0x10)
+ blackForeground = true;
+ for (int b=0; b<7; b++) {
+ totalBits = (r * 40 + c) * 7 + b;
+ charBit = ((rowPacket.at(c)) >> (6 - b)) & 0x01;
+ hashDigits[totalBits / 6] |= charBit << (5 - (totalBits % 6));
+ }
+ }
+ }
+
+ hashString.append(QString("#%1:").arg(blackForeground ? 8 : 0, 1, 16));
+
+ for (int i=0; i<1167; i++)
+ hashString.append(base64[hashDigits[i]]);
+
+ return hashString;
+}
+
+QString exportHashStringPackets(LevelOnePage *subPage)
+{
+ auto colourToHexString=[&](int whichCLUT)
+ {
+ QString resultHexString;
+
+ for (int i=whichCLUT*8; iCLUT(i), 3, 16, QChar('0')));
+ return resultHexString;
+ };
+
+ const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+ QString result;
+
+ if (subPage->packetExists(28,0) || subPage->packetExists(28,4)) {
+ // X/28/0 and X/28/4 are duplicates apart from the CLUT definitions
+ // Assemble the duplicate beginning and ending of both packets
+ QString x28StringBegin, x28StringEnd;
+
+ x28StringBegin.append(QString("00%1").arg((subPage->defaultCharSet() << 3) | subPage->defaultNOS(), 2, 16, QChar('0')).toUpper());
+ x28StringBegin.append(QString("%1").arg((subPage->secondCharSet() << 3) | subPage->secondNOS(), 2, 16, QChar('0')).toUpper());
+ x28StringBegin.append(QString("%1%2%3%4").arg(subPage->leftSidePanelDisplayed(), 1, 10).arg(subPage->rightSidePanelDisplayed(), 1, 10).arg(subPage->sidePanelStatusL25(), 1, 10).arg(subPage->sidePanelColumns(), 1, 16));
+
+ x28StringEnd = QString("%1%2%3%4").arg(subPage->defaultScreenColour(), 2, 16, QChar('0')).arg(subPage->defaultRowColour(), 2, 16, QChar('0')).arg(subPage->blackBackgroundSubst(), 1, 10).arg(subPage->colourTableRemap(), 1, 10);
+
+ if (subPage->packetExists(28,0))
+ result.append(":X280=" + x28StringBegin + colourToHexString(2) + colourToHexString(3) + x28StringEnd);
+ if (subPage->packetExists(28,4))
+ result.append(":X284=" + x28StringBegin + colourToHexString(0) + colourToHexString(1) + x28StringEnd);
+ }
+
+ if (!subPage->enhancements()->isEmpty()) {
+ result.append(":X26=");
+ for (int i=0; ienhancements()->size(); i++) {
+ result.append(base64[subPage->enhancements()->at(i).data() >> 1]);
+ result.append(base64[subPage->enhancements()->at(i).mode() | ((subPage->enhancements()->at(i).data() & 1) << 5)]);
+ result.append(base64[subPage->enhancements()->at(i).address()]);
+ }
+ }
+
+ // Assemble PS
+ // C4 Erase page is stored in bit 14
+ int pageStatus = 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++)
+ pageStatus |= subPage->controlBit(i) << (i-1);
+ // Apparently the TTI format stores the NOS bits backwards
+ pageStatus |= subPage->controlBit(PageBase::C12NOS) << 9;
+ pageStatus |= subPage->controlBit(PageBase::C13NOS) << 8;
+ pageStatus |= subPage->controlBit(PageBase::C14NOS) << 7;
+
+ result.append(QString(":PS=%1").arg(0x8000 | pageStatus, 0, 16, QChar('0')));
+ return result;
+}
diff --git a/src/qteletextmaker/loadsave.h b/src/qteletextmaker/hashformats.h
similarity index 63%
rename from src/qteletextmaker/loadsave.h
rename to src/qteletextmaker/hashformats.h
index 49545bb..d2f56b8 100644
--- a/src/qteletextmaker/loadsave.h
+++ b/src/qteletextmaker/hashformats.h
@@ -17,30 +17,15 @@
* along with QTeletextMaker. If not, see .
*/
-#ifndef LOADSAVE_H
-#define LOADSAVE_H
+#ifndef HASHFORMATS_H
+#define HASHFORMATS_H
#include
-#include
-#include
#include
-#include
-#include "document.h"
#include "levelonepage.h"
#include "pagebase.h"
-void loadTTI(QFile *inFile, TeletextDocument *document);
-void importT42(QFile *inFile, TeletextDocument *document);
-
-int controlBitsToPS(PageBase *subPage);
-
-void saveTTI(QSaveFile &file, const TeletextDocument &document);
-void exportT42File(QSaveFile &file, const TeletextDocument &document);
-void exportM29File(QSaveFile &file, const TeletextDocument &document);
-
-QByteArray rowPacketAlways(PageBase *subPage, int packetNumber);
-
QString exportHashStringPage(LevelOnePage *subPage);
QString exportHashStringPackets(LevelOnePage *subPage);
diff --git a/src/qteletextmaker/loadformats.cpp b/src/qteletextmaker/loadformats.cpp
new file mode 100644
index 0000000..627af48
--- /dev/null
+++ b/src/qteletextmaker/loadformats.cpp
@@ -0,0 +1,571 @@
+/*
+ * 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 "loadformats.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "document.h"
+#include "hamming.h"
+#include "levelonepage.h"
+#include "pagebase.h"
+
+bool LoadTTIFormat::load(QFile *inFile, TeletextDocument *document)
+{
+ m_warnings.clear();
+ m_error.clear();
+
+ QByteArray inLine;
+ bool firstSubPageAlreadyFound = false;
+ bool pageBodyPacketsFound = false;
+ int cycleCommandsFound = 0;
+ int mostRecentCycleValue = -1;
+ LevelOnePage::CycleTypeEnum mostRecentCycleType;
+
+ LevelOnePage* loadingPage = document->subPage(0);
+
+ for (;;) {
+ inLine = inFile->readLine(160).trimmed();
+ if (inLine.isEmpty())
+ break;
+ if (inLine.startsWith("DE,"))
+ document->setDescription(QString(inLine.remove(0, 3)));
+ if (inLine.startsWith("PN,")) {
+ // When second and subsequent PN commands are found, firstSubPageAlreadyFound==true at this point
+ // This assumes that PN is the first command of a new subpage...
+ if (firstSubPageAlreadyFound) {
+ document->insertSubPage(document->numberOfSubPages(), false);
+ loadingPage = document->subPage(document->numberOfSubPages()-1);
+ } else {
+ document->setPageNumberFromString(inLine.mid(3,3));
+ firstSubPageAlreadyFound = true;
+ }
+ }
+/* if (lineType == "SC,") {
+ bool subPageNumberOk;
+ int subPageNumberRead = inLine.mid(3, 4).toInt(&subPageNumberOk, 16);
+ if ((!subPageNumberOk) || subPageNumberRead > 0x3f7f)
+ subPageNumberRead = 0;
+ loadingPage->setSubPageNumber(subPageNumberRead);
+ }*/
+ if (inLine.startsWith("PS,")) {
+ bool pageStatusOk;
+ int pageStatusRead = inLine.mid(3, 4).toInt(&pageStatusOk, 16);
+ if (pageStatusOk) {
+ loadingPage->setControlBit(PageBase::C4ErasePage, pageStatusRead & 0x4000);
+ for (int i=PageBase::C5Newsflash, pageStatusBit=0x0001; i<=PageBase::C11SerialMagazine; i++, pageStatusBit<<=1)
+ loadingPage->setControlBit(i, pageStatusRead & pageStatusBit);
+ loadingPage->setDefaultNOS(((pageStatusRead & 0x0200) >> 9) | ((pageStatusRead & 0x0100) >> 7) | ((pageStatusRead & 0x0080) >> 5));
+ }
+ }
+ if (inLine.startsWith("CT,") && (inLine.endsWith(",C") || inLine.endsWith(",T"))) {
+ bool cycleValueOk;
+ int cycleValueRead = inLine.mid(3, inLine.size()-5).toInt(&cycleValueOk);
+ if (cycleValueOk) {
+ cycleCommandsFound++;
+ // House-keep CT command values, in case it's the only one within multiple subpages
+ mostRecentCycleValue = cycleValueRead;
+ loadingPage->setCycleValue(cycleValueRead);
+ mostRecentCycleType = inLine.endsWith("C") ? LevelOnePage::CTcycles : LevelOnePage::CTseconds;
+ loadingPage->setCycleType(mostRecentCycleType);
+ }
+ }
+ if (inLine.startsWith("FL,")) {
+ bool fastTextLinkOk;
+ int fastTextLinkRead;
+ QString flLine = QString(inLine.remove(0, 3));
+ if (flLine.count(',') == 5)
+ for (int i=0; i<6; i++) {
+ fastTextLinkRead = flLine.section(',', i, i).toInt(&fastTextLinkOk, 16);
+ if (fastTextLinkOk) {
+ if (fastTextLinkRead == 0)
+ fastTextLinkRead = 0x8ff;
+ // Stored as page link with relative magazine number, convert from absolute page number that was read
+ fastTextLinkRead ^= document->pageNumber() & 0x700;
+ fastTextLinkRead &= 0x7ff; // Fixes magazine 8 to 0
+ loadingPage->setFastTextLinkPageNumber(i, fastTextLinkRead);
+ }
+ }
+ }
+ if (inLine.startsWith("OL,")) {
+ bool lineNumberOk;
+ int lineNumber, secondCommaPosition;
+
+ secondCommaPosition = inLine.indexOf(",", 3);
+ if (secondCommaPosition != 4 && secondCommaPosition != 5)
+ continue;
+
+ lineNumber = inLine.mid(3, secondCommaPosition-3).toInt(&lineNumberOk, 10);
+ if (lineNumberOk && lineNumber >= 0 && lineNumber <= 29) {
+ pageBodyPacketsFound = true;
+
+ 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 for us
+ // But it also (un)helpfully removes spaces at the end of a 40 character 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;
+ }
+ }
+ loadingPage->setPacket(lineNumber, inLine);
+ } else {
+ int designationCode = inLine.at(0) & 0x3f;
+ if (inLine.size() < 40) {
+ // OL is too short!
+ if (lineNumber == 26) {
+ // For a too-short enhancement triplets OL, first trim the line down to nearest whole triplet
+ inLine.resize((inLine.size() / 3 * 3) + 1);
+ // Then use "dummy" enhancement triplets to extend the line to the proper length
+ for (int i=inLine.size(); i<40; i+=3)
+ inLine.append("i^@"); // Address 41, Mode 0x1e, Data 0
+ } else
+ // For other triplet OLs and Hamming 8/4 OLs, just pad with zero data
+ for (int i=inLine.size(); i<40; i++)
+ inLine.append("@");
+ }
+ for (int i=1; i<=39; i++)
+ inLine[i] = inLine.at(i) & 0x3f;
+ // Import M/29 whole-magazine packets as X/28 per-page packets
+ if (lineNumber == 29) {
+ if ((document->pageNumber() & 0xff) != 0xff)
+ m_warnings.append(QString("M/29/%1 packet found, but page number was not xFF.").arg(designationCode));
+ lineNumber = 28;
+ }
+ loadingPage->setPacket(lineNumber, designationCode, inLine);
+ }
+ }
+ }
+ }
+
+ if (!pageBodyPacketsFound) {
+ m_error = "No OL lines found";
+ return false;
+ }
+
+ // If there's more than one subpage but only one valid CT command was found, apply it to all subpages
+ // I don't know if this is correct
+ if (cycleCommandsFound == 1 && document->numberOfSubPages()>1)
+ for (int i=0; inumberOfSubPages(); i++) {
+ document->subPage(i)->setCycleValue(mostRecentCycleValue);
+ document->subPage(i)->setCycleType(mostRecentCycleType);
+ }
+
+ return true;
+}
+
+
+bool LoadT42Format::readPacket()
+{
+ return m_inFile->read((char *)m_inLine, 42) == 42;
+}
+
+bool LoadT42Format::load(QFile *inFile, TeletextDocument *document)
+{
+ int readMagazineNumber, readPacketNumber;
+ int foundMagazineNumber = -1;
+ int foundPageNumber = -1;
+ bool firstPacket0Found = false;
+ bool pageBodyPacketsFound = false;
+
+ m_inFile = inFile;
+
+ m_warnings.clear();
+ m_error.clear();
+
+ for (;;) {
+ if (!readPacket())
+ // Reached end of .t42 file, or less than 42 bytes left
+ break;
+
+ // Magazine and packet numbers
+ m_inLine[0] = hamming_8_4_decode[m_inLine[0]];
+ m_inLine[1] = hamming_8_4_decode[m_inLine[1]];
+ if (m_inLine[0] == 0xff || m_inLine[1] == 0xff)
+ // Error decoding magazine or packet number
+ continue;
+ readMagazineNumber = m_inLine[0] & 0x07;
+ readPacketNumber = (m_inLine[0] >> 3) | (m_inLine[1] << 1);
+
+ if (readPacketNumber == 0) {
+ // Hamming decode page number, subcodes and control bits
+ for (int i=2; i<10; i++)
+ m_inLine[i] = hamming_8_4_decode[m_inLine[i]];
+ // See if the page number is valid
+ if (m_inLine[2] == 0xff || m_inLine[3] == 0xff)
+ // Error decoding page number
+ continue;
+
+ const int readPageNumber = (m_inLine[3] << 4) | m_inLine[2];
+
+ if (readPageNumber == 0xff)
+ // Time filling header
+ continue;
+
+ // A second or subsequent X/0 has been found
+ if (firstPacket0Found) {
+ if (readMagazineNumber != foundMagazineNumber)
+ // Packet from different magazine broadcast in parallel mode
+ continue;
+ if ((readPageNumber == foundPageNumber) && pageBodyPacketsFound)
+ // X/0 with same page number found after page body packets loaded - assume end of page
+ break;
+ if (readPageNumber != foundPageNumber) {
+ // More than one page in .t42 file - end of current page reached
+ m_warnings.append("More than one page in .t42 file, only first full page loaded.");
+ break;
+ }
+ // Could get here if X/0 with same page number was found with no body packets inbetween
+ continue;
+ } else {
+ // First X/0 found
+ foundMagazineNumber = readMagazineNumber;
+ foundPageNumber = readPageNumber;
+ firstPacket0Found = true;
+
+ if (foundMagazineNumber == 0)
+ document->setPageNumber(0x800 | foundPageNumber);
+ else
+ document->setPageNumber((foundMagazineNumber << 8) | foundPageNumber);
+
+ document->subPage(0)->setControlBit(PageBase::C4ErasePage, m_inLine[5] & 0x08);
+ document->subPage(0)->setControlBit(PageBase::C5Newsflash, m_inLine[7] & 0x04);
+ document->subPage(0)->setControlBit(PageBase::C6Subtitle, m_inLine[7] & 0x08);
+ for (int i=0; i<4; i++)
+ document->subPage(0)->setControlBit(PageBase::C7SuppressHeader+i, m_inLine[8] & (1 << i));
+ document->subPage(0)->setControlBit(PageBase::C11SerialMagazine, m_inLine[9] & 0x01);
+ document->subPage(0)->setControlBit(PageBase::C12NOS, m_inLine[9] & 0x08);
+ document->subPage(0)->setControlBit(PageBase::C13NOS, m_inLine[9] & 0x04);
+ document->subPage(0)->setControlBit(PageBase::C14NOS, m_inLine[9] & 0x02);
+
+ continue;
+ }
+ }
+
+ // No X/0 has been found yet, keep looking for one
+ if (!firstPacket0Found)
+ continue;
+
+ // Disregard whole-magazine packets
+ if (readPacketNumber > 28)
+ continue;
+
+ // We get here when a page-body packet belonging to the found X/0 header was found
+ pageBodyPacketsFound = true;
+
+ // At the moment this only loads a Level One Page properly
+ // because it assumes X/1 to X/25 is odd partity
+ if (readPacketNumber < 25) {
+ for (int i=2; i<42; i++)
+ // TODO - obey odd parity?
+ m_inLine[i] &= 0x7f;
+ document->subPage(0)->setPacket(readPacketNumber, QByteArray((const char *)&m_inLine[2], 40));
+ continue;
+ }
+
+ // X/26, X/27 or X/28
+ int readDesignationCode = hamming_8_4_decode[m_inLine[2]];
+
+ if (readDesignationCode == 0xff)
+ // Error decoding designation code
+ continue;
+
+ if (readPacketNumber == 27 && readDesignationCode < 4) {
+ // X/27/0 to X/27/3 for Editorial Linking
+ // Decode Hamming 8/4 on each of the six links, checking for errors on the way
+ for (int i=0; i<6; i++) {
+ bool decodingError = false;
+ const int b = 3 + i*6; // First byte of this link
+
+ for (int j=0; j<6; j++) {
+ m_inLine[b+j] = hamming_8_4_decode[m_inLine[b+j]];
+ if (m_inLine[b+j] == 0xff) {
+ decodingError = true;
+ break;
+ }
+ }
+
+ if (decodingError) {
+ // Error found in at least one byte of the link
+ // Neutralise the whole link to same magazine, page FF, subcode 3F7F
+ qDebug("X/27/%d link %d decoding error", readDesignationCode, i);
+ m_inLine[b] = 0xf;
+ m_inLine[b+1] = 0xf;
+ m_inLine[b+2] = 0xf;
+ m_inLine[b+3] = 0x7;
+ m_inLine[b+4] = 0xf;
+ m_inLine[b+5] = 0x3;
+ }
+ }
+ document->subPage(0)->setPacket(readPacketNumber, readDesignationCode, QByteArray((const char *)&m_inLine[2], 40));
+
+ continue;
+ }
+
+ // X/26, or X/27/4 to X/27/15, or X/28
+ // Decode Hamming 24/18
+ for (int i=0; i<13; i++) {
+ const int b = 3 + i*3; // First byte of triplet
+
+ const int p0 = m_inLine[b];
+ const int p1 = m_inLine[b+1];
+ const int p2 = m_inLine[b+2];
+
+ unsigned int D1_D4;
+ unsigned int D5_D11;
+ unsigned int D12_D18;
+ unsigned int ABCDEF;
+ int32_t d;
+
+ D1_D4 = hamming_24_18_decode_d1_d4[p0 >> 2];
+ D5_D11 = p1 & 0x7f;
+ D12_D18 = p2 & 0x7f;
+
+ d = D1_D4 | (D5_D11 << 4) | (D12_D18 << 11);
+
+ ABCDEF = (hamming_24_18_parities[0][p0] ^ hamming_24_18_parities[1][p1] ^ hamming_24_18_parities[2][p2]);
+
+ d ^= (int)hamming_24_18_decode_correct[ABCDEF];
+
+ if ((d & 0x80000000) == 0x80000000) {
+ // Error decoding Hamming 24/18
+ qDebug("X/%d/%d triplet %d decoding error", readPacketNumber, readDesignationCode, i);
+ if (readPacketNumber == 26) {
+ // Enhancements packet, set to "dummy" Address 41, Mode 0x1e, Data 0
+ m_inLine[b] = 41;
+ m_inLine[b+1] = 0x1e;
+ m_inLine[b+2] = 0;
+ } else {
+ // Zero out whole decoded triplet, bound to make things go wrong...
+ m_inLine[b] = 0x00;
+ m_inLine[b+1] = 0x00;
+ m_inLine[b+2] = 0x00;
+ }
+ } else {
+ m_inLine[b] = d & 0x0003f;
+ m_inLine[b+1] = (d & 0x00fc0) >> 6;
+ m_inLine[b+2] = d >> 12;
+ }
+ }
+ document->subPage(0)->setPacket(readPacketNumber, readDesignationCode, QByteArray((const char *)&m_inLine[2], 40));
+ }
+
+ if (!firstPacket0Found) {
+ m_error = "No X/0 found.";
+ return false;
+ } else if (!pageBodyPacketsFound) {
+ m_error = "X/0 found, but no page body packets were found.";
+ return false;
+ } else
+ return true;
+}
+
+
+bool LoadHTTFormat::readPacket()
+{
+ unsigned char httLine[45];
+
+ if (m_inFile->read((char *)httLine, 45) != 45)
+ return false;
+
+ if (httLine[0] != 0xaa || httLine[1] != 0xaa || httLine[2] != 0xe4)
+ return false;
+
+ for (int i=0; i<42; i++) {
+ unsigned char b = httLine[i+3];
+ b = (b & 0xf0) >> 4 | (b & 0x0f) << 4;
+ b = (b & 0xcc) >> 2 | (b & 0x33) << 2;
+ b = (b & 0xaa) >> 1 | (b & 0x55) << 1;
+ m_inLine[i] = b;
+ }
+
+ return true;
+}
+
+
+bool LoadEP1Format::load(QFile *inFile, TeletextDocument *document)
+{
+ m_warnings.clear();
+ m_error.clear();
+
+ unsigned char inLine[42];
+ unsigned char numOfSubPages = 1;
+
+ LevelOnePage* loadingPage = document->subPage(0);
+
+ for (;;) {
+ // Read six bytes, will either be a header for a (sub)page
+ // or a start header indicating multiple subpages are within
+ if (inFile->read((char *)inLine, 6) != 6)
+ return false;
+ if (inLine[0] == 'J' || inLine[1] == 'W' || inLine[2] == 'C') {
+ // Multiple subpages: get number of subpages then read
+ // next six bytes that really will be the header of the first subpage
+ numOfSubPages = inLine[3];
+ if (inFile->read((char *)inLine, 6) != 6)
+ return false;
+
+ m_warnings.append("More than one page in EP1/EPX file, only first full page loaded.");
+ }
+
+ // Check for header of a (sub)page
+ if (inLine[0] != 0xfe || inLine[1] != 0x01)
+ return false;
+
+ // Deal with language code unique to EP1 - unknown values are mapped to English
+ loadingPage->setDefaultCharSet(m_languageCode.key(inLine[2], 0x09) >> 3);
+ loadingPage->setDefaultNOS(m_languageCode.key(inLine[2], 0x09) & 0x7);
+
+ // If fourth byte is 0xca then "X/26 enhancements header" follows
+ // Otherwise Level 1 page data follows
+ if (inLine[3] == 0xca) {
+ // Read next four bytes that form the "X/26 enhancements header"
+ if (inFile->read((char *)inLine, 4) != 4)
+ return false;
+ // Third and fourth bytes are little-endian length of enhancement data
+ const int numOfX26Bytes = inLine[2] | (inLine[3] << 8);
+ const int numOfX26Packets = (numOfX26Bytes + 39) / 40;
+
+ QByteArray packet(40, 0x00);
+ packet[0] = 0;
+
+ for (int i=0; iread((char *)inLine, 40) != 40)
+ return false;
+
+ // Assumes that X/26 packets are saved with ascending designation codes...
+ for (int c=1; c<39; c+=3) {
+ if (!terminatorFound) {
+ // Shuffle triplet bits from 6 bit address, 5 bit mode, 7 bit data
+ packet[c] = inLine[c];
+ packet[c+1] = inLine[c+1] | ((inLine[c+2] & 1) << 5);
+ packet[c+2] = inLine[c+2] >> 1;
+ // Address of termination marker is 7f instead of 3f
+ if (inLine[c+1] == 0x1f && inLine[c] == 0x7f) {
+ packet[c] = 0x3f;
+
+ if (inLine[c+2] & 0x01) {
+ // If a termination marker was found, stop reading the packet
+ // and repeat the marker ourselves to the end
+ terminatorFound = true;
+ terminatorTriplet[0] = packet[c+1];
+ terminatorTriplet[1] = packet[c+2];
+ }
+ }
+ } else {
+ packet[c] = 0x3f;
+ packet[c+1] = terminatorTriplet[0];
+ packet[c+2] = terminatorTriplet[1];
+ }
+ }
+
+ loadingPage->setPacket(26, i, packet);
+ }
+ }
+
+ // Level 1 rows
+ for (int r=0; r<24; r++) {
+ if (inFile->read((char *)inLine, 40) != 40)
+ return false;
+
+ for (int c=0; c<40; c++)
+ if (inLine[c] != 0x20) {
+ loadingPage->setPacket(r, QByteArray((const char *)&inLine, 40));
+ break;
+ }
+ }
+
+ numOfSubPages--;
+
+ // FIXME uncomment "if" statement when we're ready to save multi-page EPX files
+ //if (numOfSubPages == 0)
+ break;
+
+ // There are more subpages coming up so skip over the 40 byte buffer and 2 byte terminator
+ if (inFile->read((char *)inLine, 42) != 42)
+ return false;
+
+ document->insertSubPage(document->numberOfSubPages(), false);
+ loadingPage = document->subPage(document->numberOfSubPages()-1);
+ }
+ return true;
+}
+
+
+int LoadFormats::s_instances = 0;
+
+LoadFormats::LoadFormats()
+{
+ if (s_instances == 0) {
+ s_fileFormat[0] = new LoadTTIFormat;
+ s_fileFormat[1] = new LoadT42Format;
+ s_fileFormat[2] = new LoadEP1Format;
+ s_fileFormat[3] = new LoadHTTFormat;
+
+ s_filters = "All Supported Files (*.";
+
+ for (int i=0; iextensions().join(" *."));
+ }
+ s_filters.append(");;");
+
+ for (int i=0; ifileDialogFilter());
+ }
+ }
+
+ s_instances++;
+}
+
+LoadFormats::~LoadFormats()
+{
+ s_instances--;
+
+ if (s_instances == 0)
+ for (int i=s_size-1; i>=0; i--)
+ delete s_fileFormat[i];
+}
+
+LoadFormat *LoadFormats::findFormat(const QString &suffix) const
+{
+ for (int i=0; iextensions().contains(suffix, Qt::CaseInsensitive))
+ return s_fileFormat[i];
+
+ return nullptr;
+}
diff --git a/src/qteletextmaker/loadformats.h b/src/qteletextmaker/loadformats.h
new file mode 100644
index 0000000..4ebe3e6
--- /dev/null
+++ b/src/qteletextmaker/loadformats.h
@@ -0,0 +1,126 @@
+/*
+ * 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 .
+ */
+
+#ifndef LOADFORMATS_H
+#define LOADFORMATS_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "document.h"
+#include "levelonepage.h"
+#include "pagebase.h"
+
+class LoadFormat
+{
+public:
+ virtual ~LoadFormat() {};
+
+ virtual bool load(QFile *inFile, TeletextDocument *document) =0;
+
+ virtual QString description() const =0;
+ virtual QStringList extensions() const =0;
+ QString fileDialogFilter() const { return QString(description() + " (*." + extensions().join(" *.") + ')'); };
+ QStringList warningStrings() const { return m_warnings; };
+ QString errorString() const { return m_error; };
+
+protected:
+ TeletextDocument const *m_document;
+ QStringList m_warnings;
+ QString m_error;
+};
+
+class LoadTTIFormat : public LoadFormat
+{
+public:
+ bool load(QFile *inFile, TeletextDocument *document) override;
+
+ QString description() const override { return QString("MRG Systems TTI"); };
+ QStringList extensions() const override { return QStringList { "tti", "ttix" }; };
+};
+
+class LoadT42Format : public LoadFormat
+{
+public:
+ bool load(QFile *inFile, TeletextDocument *document) override;
+
+ QString description() const override { return QString("t42 packet stream"); };
+ QStringList extensions() const override { return QStringList { "t42" }; };
+
+protected:
+ virtual bool readPacket();
+
+ QFile *m_inFile;
+ unsigned char m_inLine[42];
+};
+
+class LoadHTTFormat : public LoadT42Format
+{
+public:
+ QString description() const override { return QString("HMS SD-Teletext HTT"); };
+ QStringList extensions() const override { return QStringList { "htt" }; };
+
+protected:
+ bool readPacket() override;
+};
+
+class LoadEP1Format : public LoadFormat
+{
+public:
+ bool load(QFile *inFile, TeletextDocument *document) override;
+
+ QString description() const override { return QString("Softel EP1"); };
+ QStringList extensions() const override { return QStringList { "ep1", "epx" }; };
+
+protected:
+ // Language codes unique to EP1
+ // FIXME duplicated in saveformats.h
+ const QMap m_languageCode {
+ { 0x00, 0x09 }, { 0x01, 0x0d }, { 0x02, 0x18 }, { 0x03, 0x11 }, { 0x04, 0x0b }, { 0x05, 0x17 }, { 0x06, 0x07 },
+ { 0x08, 0x14 }, { 0x09, 0x0d }, { 0x0a, 0x18 }, { 0x0b, 0x11 }, { 0x0c, 0x0b }, { 0x0e, 0x07 },
+ { 0x10, 0x09 }, { 0x11, 0x0d }, { 0x12, 0x18 }, { 0x13, 0x11 }, { 0x14, 0x0b }, { 0x15, 0x17 }, { 0x16, 0x1c },
+ { 0x1d, 0x1e }, { 0x1f, 0x16 },
+ { 0x21, 0x0d }, { 0x22, 0xff }, { 0x23, 0xff }, { 0x26, 0x07 },
+ { 0x36, 0x1c }, { 0x37, 0x0e },
+ { 0x40, 0x09 }, { 0x44, 0x0b }
+ };
+};
+
+
+class LoadFormats
+{
+public:
+ LoadFormats();
+ ~LoadFormats();
+
+ LoadFormat *findFormat(const QString &suffix) const;
+ QString filters() const { return s_filters; };
+
+private:
+ static const inline int s_size = 4;
+ static int s_instances;
+ inline static LoadFormat *s_fileFormat[s_size];
+ inline static QString s_filters;
+};
+
+#endif
diff --git a/src/qteletextmaker/loadsave.cpp b/src/qteletextmaker/loadsave.cpp
deleted file mode 100644
index 58e708c..0000000
--- a/src/qteletextmaker/loadsave.cpp
+++ /dev/null
@@ -1,811 +0,0 @@
-/*
- * 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 "loadsave.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "document.h"
-#include "hamming.h"
-#include "levelonepage.h"
-#include "pagebase.h"
-
-void loadTTI(QFile *inFile, TeletextDocument *document)
-{
- QByteArray inLine;
- bool firstSubPageAlreadyFound = false;
- int cycleCommandsFound = 0;
- int mostRecentCycleValue = -1;
- LevelOnePage::CycleTypeEnum mostRecentCycleType;
-
- LevelOnePage* loadingPage = document->subPage(0);
-
- for (;;) {
- inLine = inFile->readLine(160).trimmed();
- if (inLine.isEmpty())
- break;
- if (inLine.startsWith("DE,"))
- document->setDescription(QString(inLine.remove(0, 3)));
- if (inLine.startsWith("PN,")) {
- // When second and subsequent PN commands are found, firstSubPageAlreadyFound==true at this point
- // This assumes that PN is the first command of a new subpage...
- if (firstSubPageAlreadyFound) {
- document->insertSubPage(document->numberOfSubPages(), false);
- loadingPage = document->subPage(document->numberOfSubPages()-1);
- } else {
- document->setPageNumberFromString(inLine.mid(3,3));
- firstSubPageAlreadyFound = true;
- }
- }
-/* if (lineType == "SC,") {
- bool subPageNumberOk;
- int subPageNumberRead = inLine.mid(3, 4).toInt(&subPageNumberOk, 16);
- if ((!subPageNumberOk) || subPageNumberRead > 0x3f7f)
- subPageNumberRead = 0;
- loadingPage->setSubPageNumber(subPageNumberRead);
- }*/
- if (inLine.startsWith("PS,")) {
- bool pageStatusOk;
- int pageStatusRead = inLine.mid(3, 4).toInt(&pageStatusOk, 16);
- if (pageStatusOk) {
- loadingPage->setControlBit(PageBase::C4ErasePage, pageStatusRead & 0x4000);
- for (int i=PageBase::C5Newsflash, pageStatusBit=0x0001; i<=PageBase::C11SerialMagazine; i++, pageStatusBit<<=1)
- loadingPage->setControlBit(i, pageStatusRead & pageStatusBit);
- loadingPage->setDefaultNOS(((pageStatusRead & 0x0200) >> 9) | ((pageStatusRead & 0x0100) >> 7) | ((pageStatusRead & 0x0080) >> 5));
- }
- }
- if (inLine.startsWith("CT,") && (inLine.endsWith(",C") || inLine.endsWith(",T"))) {
- bool cycleValueOk;
- int cycleValueRead = inLine.mid(3, inLine.size()-5).toInt(&cycleValueOk);
- if (cycleValueOk) {
- cycleCommandsFound++;
- // House-keep CT command values, in case it's the only one within multiple subpages
- mostRecentCycleValue = cycleValueRead;
- loadingPage->setCycleValue(cycleValueRead);
- mostRecentCycleType = inLine.endsWith("C") ? LevelOnePage::CTcycles : LevelOnePage::CTseconds;
- loadingPage->setCycleType(mostRecentCycleType);
- }
- }
- if (inLine.startsWith("FL,")) {
- bool fastTextLinkOk;
- int fastTextLinkRead;
- QString flLine = QString(inLine.remove(0, 3));
- if (flLine.count(',') == 5)
- for (int i=0; i<6; i++) {
- fastTextLinkRead = flLine.section(',', i, i).toInt(&fastTextLinkOk, 16);
- if (fastTextLinkOk) {
- if (fastTextLinkRead == 0)
- fastTextLinkRead = 0x8ff;
- // Stored as page link with relative magazine number, convert from absolute page number that was read
- fastTextLinkRead ^= document->pageNumber() & 0x700;
- fastTextLinkRead &= 0x7ff; // Fixes magazine 8 to 0
- loadingPage->setFastTextLinkPageNumber(i, fastTextLinkRead);
- }
- }
- }
- if (inLine.startsWith("OL,")) {
- bool lineNumberOk;
- int lineNumber, secondCommaPosition;
-
- secondCommaPosition = inLine.indexOf(",", 3);
- if (secondCommaPosition != 4 && secondCommaPosition != 5)
- continue;
-
- 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 for us
- // But it also (un)helpfully removes spaces at the end of a 40 character 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;
- }
- }
- loadingPage->setPacket(lineNumber, inLine);
- } else {
- int designationCode = inLine.at(0) & 0x3f;
- if (inLine.size() < 40) {
- // OL is too short!
- if (lineNumber == 26) {
- // For a too-short enhancement triplets OL, first trim the line down to nearest whole triplet
- inLine.resize((inLine.size() / 3 * 3) + 1);
- // Then use "dummy" enhancement triplets to extend the line to the proper length
- for (int i=inLine.size(); i<40; i+=3)
- inLine.append("i^@"); // Address 41, Mode 0x1e, Data 0
- } else
- // For other triplet OLs and Hamming 8/4 OLs, just pad with zero data
- for (int i=inLine.size(); i<40; i++)
- inLine.append("@");
- }
- for (int i=1; i<=39; i++)
- inLine[i] = inLine.at(i) & 0x3f;
- // Import M/29 whole-magazine packets as X/28 per-page packets
- if (lineNumber == 29) {
- if ((document->pageNumber() & 0xff) != 0xff)
- qDebug("M/29/%d packet found, but page number is not xFF!", designationCode);
- lineNumber = 28;
- }
- loadingPage->setPacket(lineNumber, designationCode, inLine);
- }
- }
- }
- }
- // If there's more than one subpage but only one valid CT command was found, apply it to all subpages
- // I don't know if this is correct
- if (cycleCommandsFound == 1 && document->numberOfSubPages()>1)
- for (int i=0; inumberOfSubPages(); i++) {
- document->subPage(i)->setCycleValue(mostRecentCycleValue);
- document->subPage(i)->setCycleType(mostRecentCycleType);
- }
-}
-
-void importT42(QFile *inFile, TeletextDocument *document)
-{
- unsigned char inLine[42];
- int readMagazineNumber, readPacketNumber;
- int foundMagazineNumber = -1;
- int foundPageNumber = -1;
- bool firstPacket0Found = false;
- bool pageBodyPacketsFound = false;
-
- for (;;) {
- if (inFile->read((char *)inLine, 42) != 42)
- // Reached end of .t42 file, or less than 42 bytes left
- break;
-
- // Magazine and packet numbers
- inLine[0] = hamming_8_4_decode[inLine[0]];
- inLine[1] = hamming_8_4_decode[inLine[1]];
- if (inLine[0] == 0xff || inLine[1] == 0xff)
- // Error decoding magazine or packet number
- continue;
- readMagazineNumber = inLine[0] & 0x07;
- readPacketNumber = (inLine[0] >> 3) | (inLine[1] << 1);
-
- if (readPacketNumber == 0) {
- // Hamming decode page number, subcodes and control bits
- for (int i=2; i<10; i++)
- inLine[i] = hamming_8_4_decode[inLine[i]];
- // See if the page number is valid
- if (inLine[2] == 0xff || inLine[3] == 0xff)
- // Error decoding page number
- continue;
-
- const int readPageNumber = (inLine[3] << 4) | inLine[2];
-
- if (readPageNumber == 0xff)
- // Time filling header
- continue;
-
- // A second or subsequent X/0 has been found
- if (firstPacket0Found) {
- if (readMagazineNumber != foundMagazineNumber)
- // Packet from different magazine broadcast in parallel mode
- continue;
- if ((readPageNumber == foundPageNumber) && pageBodyPacketsFound)
- // X/0 with same page number found after page body packets loaded - assume end of page
- break;
- if (readPageNumber != foundPageNumber) {
- // More than one page in .t42 file - end of current page reached
- qDebug("More than one page in .t42 file");
- break;
- }
- // Could get here if X/0 with same page number was found with no body packets inbetween
- continue;
- } else {
- // First X/0 found
- foundMagazineNumber = readMagazineNumber;
- foundPageNumber = readPageNumber;
- firstPacket0Found = true;
-
- if (foundMagazineNumber == 0)
- document->setPageNumber(0x800 | foundPageNumber);
- else
- document->setPageNumber((foundMagazineNumber << 8) | foundPageNumber);
-
- document->subPage(0)->setControlBit(PageBase::C4ErasePage, inLine[5] & 0x08);
- document->subPage(0)->setControlBit(PageBase::C5Newsflash, inLine[7] & 0x04);
- document->subPage(0)->setControlBit(PageBase::C6Subtitle, inLine[7] & 0x08);
- for (int i=0; i<4; i++)
- document->subPage(0)->setControlBit(PageBase::C7SuppressHeader+i, inLine[8] & (1 << i));
- document->subPage(0)->setControlBit(PageBase::C11SerialMagazine, inLine[9] & 0x01);
- document->subPage(0)->setControlBit(PageBase::C12NOS, inLine[9] & 0x08);
- document->subPage(0)->setControlBit(PageBase::C13NOS, inLine[9] & 0x04);
- document->subPage(0)->setControlBit(PageBase::C14NOS, inLine[9] & 0x02);
-
- continue;
- }
- }
-
- // No X/0 has been found yet, keep looking for one
- if (!firstPacket0Found)
- continue;
-
- // Disregard whole-magazine packets
- if (readPacketNumber > 28)
- continue;
-
- // We get here when a page-body packet belonging to the found X/0 header was found
- pageBodyPacketsFound = true;
-
- // At the moment this only loads a Level One Page properly
- // because it assumes X/1 to X/25 is odd partity
- if (readPacketNumber < 25) {
- for (int i=2; i<42; i++)
- // TODO - obey odd parity?
- inLine[i] &= 0x7f;
- document->subPage(0)->setPacket(readPacketNumber, QByteArray((const char *)&inLine[2], 40));
- continue;
- }
-
- // X/26, X/27 or X/28
- int readDesignationCode = hamming_8_4_decode[inLine[2]];
-
- if (readDesignationCode == 0xff)
- // Error decoding designation code
- continue;
-
- if (readPacketNumber == 27 && readDesignationCode < 4) {
- // X/27/0 to X/27/3 for Editorial Linking
- // Decode Hamming 8/4 on each of the six links, checking for errors on the way
- for (int i=0; i<6; i++) {
- bool decodingError = false;
- const int b = 3 + i*6; // First byte of this link
-
- for (int j=0; j<6; j++) {
- inLine[b+j] = hamming_8_4_decode[inLine[b+j]];
- if (inLine[b+j] == 0xff) {
- decodingError = true;
- break;
- }
- }
-
- if (decodingError) {
- // Error found in at least one byte of the link
- // Neutralise the whole link to same magazine, page FF, subcode 3F7F
- qDebug("X/27/%d link %d decoding error", readDesignationCode, i);
- inLine[b] = 0xf;
- inLine[b+1] = 0xf;
- inLine[b+2] = 0xf;
- inLine[b+3] = 0x7;
- inLine[b+4] = 0xf;
- inLine[b+5] = 0x3;
- }
- }
- document->subPage(0)->setPacket(readPacketNumber, readDesignationCode, QByteArray((const char *)&inLine[2], 40));
-
- continue;
- }
-
- // X/26, or X/27/4 to X/27/15, or X/28
- // Decode Hamming 24/18
- for (int i=0; i<13; i++) {
- const int b = 3 + i*3; // First byte of triplet
-
- const int p0 = inLine[b];
- const int p1 = inLine[b+1];
- const int p2 = inLine[b+2];
-
- unsigned int D1_D4;
- unsigned int D5_D11;
- unsigned int D12_D18;
- unsigned int ABCDEF;
- int32_t d;
-
- D1_D4 = hamming_24_18_decode_d1_d4[p0 >> 2];
- D5_D11 = p1 & 0x7f;
- D12_D18 = p2 & 0x7f;
-
- d = D1_D4 | (D5_D11 << 4) | (D12_D18 << 11);
-
- ABCDEF = (hamming_24_18_parities[0][p0] ^ hamming_24_18_parities[1][p1] ^ hamming_24_18_parities[2][p2]);
-
- d ^= (int)hamming_24_18_decode_correct[ABCDEF];
-
- if ((d & 0x80000000) == 0x80000000) {
- // Error decoding Hamming 24/18
- qDebug("X/%d/%d triplet %d decoding error", readPacketNumber, readDesignationCode, i);
- if (readPacketNumber == 26) {
- // Enhancements packet, set to "dummy" Address 41, Mode 0x1e, Data 0
- inLine[b] = 41;
- inLine[b+1] = 0x1e;
- inLine[b+2] = 0;
- } else {
- // Zero out whole decoded triplet, bound to make things go wrong...
- inLine[b] = 0x00;
- inLine[b+1] = 0x00;
- inLine[b+2] = 0x00;
- }
- } else {
- inLine[b] = d & 0x0003f;
- inLine[b+1] = (d & 0x00fc0) >> 6;
- inLine[b+2] = d >> 12;
- }
- }
- document->subPage(0)->setPacket(readPacketNumber, readDesignationCode, QByteArray((const char *)&inLine[2], 40));
- }
-
- if (!firstPacket0Found)
- qDebug("No X/0 found");
- else if (!pageBodyPacketsFound)
- qDebug("X/0 found, but no page body packets were found");
-}
-
-// Used by saveTTI and HashString
-int controlBitsToPS(PageBase *subPage)
-{
- // C4 Erase page is stored in bit 14
- int pageStatus = 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++)
- pageStatus |= subPage->controlBit(i) << (i-1);
- // Apparently the TTI format stores the NOS bits backwards
- pageStatus |= subPage->controlBit(PageBase::C12NOS) << 9;
- pageStatus |= subPage->controlBit(PageBase::C13NOS) << 8;
- pageStatus |= subPage->controlBit(PageBase::C14NOS) << 7;
- return pageStatus;
-}
-
-void saveTTI(QSaveFile &file, const TeletextDocument &document)
-{
- int p;
- QTextStream outStream(&file);
-
- auto write7bitPacket=[&](int packetNumber)
- {
- if (document.subPage(p)->packetExists(packetNumber)) {
- QByteArray outLine = document.subPage(p)->packet(packetNumber);
-
- outStream << QString("OL,%1,").arg(packetNumber);
- for (int c=0; c= QT_VERSION_CHECK(5, 14, 0)
- outStream << outLine << Qt::endl;
-#else
- outStream << outLine << endl;
-#endif
- }
- };
-
- auto writeHammingPacket=[&](int packetNumber, int designationCode=0)
- {
- if (document.subPage(p)->packetExists(packetNumber, designationCode)) {
- QByteArray outLine = document.subPage(p)->packet(packetNumber, designationCode);
-
- outStream << QString("OL,%1,").arg(packetNumber);
- // TTI stores raw values with bit 6 set, doesn't do Hamming encoding
- outLine[0] = designationCode | 0x40;
- for (int c=1; c= QT_VERSION_CHECK(5, 14, 0)
- outStream << outLine << Qt::endl;
-#else
- outStream << outLine << endl;
-#endif
- }
- };
-
-#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
- outStream.setCodec("ISO-8859-1");
-#else
- outStream.setEncoding(QStringConverter::Latin1);
-#endif
-
- if (!document.description().isEmpty())
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- outStream << "DE," << document.description() << Qt::endl;
-#else
- outStream << "DE," << document.description() << endl;
-#endif
-
- // TODO DS and SP commands
-
- // If there's just one subpage then we save it with a subcode of 0000
- // otherwise start with a subcode of 0001
- int subPageNumber = document.numberOfSubPages() > 1;
-
- for (p=0; p= QT_VERSION_CHECK(5, 14, 0)
- outStream << Qt::endl;
-#else
- outStream << endl;
-#endif
-
- // Subpage
- // Magazine Organisation Table and Magazine Inventory Page don't have subpages
- if (document.pageFunction() != TeletextDocument::PFMOT && document.pageFunction() != TeletextDocument::PFMIP) {
- outStream << QString("SC,%1").arg(subPageNumber, 4, 10, QChar('0'));
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- outStream << Qt::endl;
-#else
- outStream << endl;
-#endif
- }
-
- // Status bits
- outStream << QString("PS,%1").arg(0x8000 | controlBitsToPS(document.subPage(p)), 4, 16, QChar('0'));
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- outStream << Qt::endl;
-#else
- outStream << endl;
-#endif
-
- // Cycle time
- if (document.pageFunction() == TeletextDocument::PFLevelOnePage)
- // Assume that only Level One Pages have configurable cycle times
- outStream << QString("CT,%1,%2").arg(document.subPage(p)->cycleValue()).arg(document.subPage(p)->cycleType()==LevelOnePage::CTcycles ? 'C' : 'T');
- else
- // X/28/0 specifies page function and coding but the PF command
- // should make it obvious to a human that this isn't a Level One Page
- outStream << QString("PF,%1,%2").arg(document.pageFunction()).arg(document.packetCoding());
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- outStream << Qt::endl;
-#else
- outStream << endl;
-#endif
-
- // FastText links
- bool writeFLCommand = false;
- if (document.pageFunction() == TeletextDocument::PFLevelOnePage && document.subPage(p)->packetExists(27,0)) {
- // Subpage has FastText 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 FastText subpage links
- /*for (int i=0; i<6; i++)
- if (document.subPage(p)->fastTextLinkSubPageNumber(i) != 0x3f7f) {
- writeFLCommand = false;
- break;
- }*/
- }
-
- // X/27 then X/28 always come first
- for (int i=(writeFLCommand ? 1 : 0); i<16; i++)
- writeHammingPacket(27, i);
- for (int i=0; i<16; i++)
- writeHammingPacket(28, i);
-
- if (document.packetCoding() == TeletextDocument::Coding7bit) {
- // For 7 bit coding i.e. Level One Pages, X/26 are written before X/1 to X/25
- for (int i=0; i<16; i++)
- writeHammingPacket(26, i);
- for (int i=1; i<=24; i++)
- write7bitPacket(i);
- } else {
- // For others (especially (G)POP pages) X/1 to X/25 are written before X/26
- for (int i=1; i<=25; i++)
- writeHammingPacket(i);
- for (int i=0; i<16; i++)
- writeHammingPacket(26, i);
- }
-
- if (writeFLCommand) {
- outStream << "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 = document.subPage(p)->fastTextLinkPageNumber(i) ^ (document.pageNumber() & 0x700);
- // Fix magazine 0 to 8
- if ((absoluteLinkPageNumber & 0x700) == 0x000)
- absoluteLinkPageNumber |= 0x800;
-
- outStream << QString("%1").arg(absoluteLinkPageNumber, 3, 16, QChar('0'));
- if (i<5)
- outStream << ',';
- }
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- outStream << Qt::endl;
-#else
- outStream << endl;
-#endif
- }
-
- subPageNumber++;
- }
-}
-
-void exportM29File(QSaveFile &file, const TeletextDocument &document)
-{
- const PageBase &subPage = *document.currentSubPage();
- QTextStream outStream(&file);
-
- auto writeM29Packet=[&](int designationCode)
- {
- if (subPage.packetExists(28, designationCode)) {
- QByteArray outLine = subPage.packet(28, designationCode);
-
- outStream << QString("OL,29,");
- // TTI stores raw values with bit 6 set, doesn't do Hamming encoding
- outLine[0] = designationCode | 0x40;
- for (int c=1; c= QT_VERSION_CHECK(5, 14, 0)
- outStream << outLine << Qt::endl;
-#else
- outStream << outLine << endl;
-#endif
- }
- };
-
-#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
- outStream.setCodec("ISO-8859-1");
-#else
- outStream.setEncoding(QStringConverter::Latin1);
-#endif
-
- if (!document.description().isEmpty())
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- outStream << "DE," << document.description() << Qt::endl;
-#else
- outStream << "DE," << document.description() << endl;
-#endif
-
- // Force page number to xFF
- outStream << QString("PN,%1ff00").arg(document.pageNumber() >> 8, 1, 16);
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- outStream << Qt::endl;
-#else
- outStream << endl;
-#endif
-
- outStream << "PS,8000";
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- outStream << Qt::endl;
-#else
- outStream << endl;
-#endif
-
- writeM29Packet(0);
- writeM29Packet(1);
- writeM29Packet(4);
-}
-
-void exportT42File(QSaveFile &file, const TeletextDocument &document)
-{
- const PageBase &subPage = *document.currentSubPage();
-
- QDataStream outStream(&file);
- // Displayable row header we export as spaces, hence the (odd parity valid) 0x20 init value
- QByteArray outLine(42, 0x20);
- int magazineNumber = (document.pageNumber() & 0xf00) >> 8;
-
- auto write7bitPacket=[&](int packetNumber)
- {
- if (subPage.packetExists(packetNumber)) {
- outLine[0] = hamming_8_4_encode[magazineNumber | ((packetNumber & 0x01) << 3)];
- outLine[1] = hamming_8_4_encode[packetNumber >> 1];
- outLine.replace(2, 40, subPage.packet(packetNumber));
-
- // 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))
- outLine[c] = outLine.at(c) | 0x80;
- }
- outStream.writeRawData(outLine.constData(), 42);
- }
- };
-
- auto writeHamming8_4Packet=[&](int packetNumber, int designationCode=0)
- {
- if (subPage.packetExists(packetNumber, designationCode)) {
- outLine[0] = hamming_8_4_encode[magazineNumber | ((packetNumber & 0x01) << 3)];
- outLine[1] = hamming_8_4_encode[packetNumber >> 1];
- outLine.replace(2, 40, subPage.packet(packetNumber, designationCode));
- outLine[2] = hamming_8_4_encode[designationCode];
-
- for (int c=3; c> 1];
- outLine.replace(2, 40, subPage.packet(packetNumber, designationCode));
- outLine[2] = hamming_8_4_encode[designationCode];
-
- for (int c=3; c> 0) & 0xff] ^ hamming_24_18_forward[1][(toEncode >> 8) & 0xff] ^ hamming_24_18_forward_2[(toEncode >> 16) & 0x03]);
- outLine[c] = Byte_0;
-
- D5_D11 = (toEncode >> 4) & 0x7f;
- D12_D18 = (toEncode >> 11) & 0x7f;
-
- P5 = 0x80 & ~(hamming_24_18_parities[0][D12_D18] << 2);
- outLine[c+1] = D5_D11 | P5;
-
- P6 = 0x80 & ((hamming_24_18_parities[0][Byte_0] ^ hamming_24_18_parities[0][D5_D11]) << 2);
- outLine[c+2] = D12_D18 | P6;
- }
-
- outStream.writeRawData(outLine.constData(), 42);
- }
- };
-
-
- if (magazineNumber == 8)
- magazineNumber = 0;
-
- // Write X/0 separately as it features both Hamming 8/4 and 7-bit odd parity within
- outLine[0] = magazineNumber & 0x07;
- outLine[1] = 0; // Packet number 0
- outLine[2] = document.pageNumber() & 0x00f;
- outLine[3] = (document.pageNumber() & 0x0f0) >> 4;
- outLine[4] = 0; // Subcode S1 - always export as 0
- outLine[5] = subPage.controlBit(PageBase::C4ErasePage) << 3;
- outLine[6] = 0; // Subcode S3 - always export as 0
- outLine[7] = (subPage.controlBit(PageBase::C5Newsflash) << 2) | (subPage.controlBit(PageBase::C6Subtitle) << 3);
- outLine[8] = subPage.controlBit(PageBase::C7SuppressHeader) | (subPage.controlBit(PageBase::C8Update) << 1) | (subPage.controlBit(PageBase::C9InterruptedSequence) << 2) | (subPage.controlBit(PageBase::C10InhibitDisplay) << 3);
- outLine[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++)
- outLine[i] = hamming_8_4_encode[(int)outLine.at(i)];
-
- // If we allow text in the row header, we'd odd-parity encode it here
-
- outStream.writeRawData(outLine.constData(), 42);
-
- // After X/0, X/27 then X/28 always come next
- for (int i=0; i<4; i++)
- writeHamming8_4Packet(27, i);
- for (int i=4; i<16; i++)
- writeHamming24_18Packet(27, i);
- for (int i=0; i<16; i++)
- writeHamming24_18Packet(28, i);
-
- if (document.packetCoding() == TeletextDocument::Coding7bit) {
- // For 7 bit coding i.e. Level One Pages, X/26 are written before X/1 to X/25
- for (int i=0; i<16; i++)
- writeHamming24_18Packet(26, i);
- for (int i=1; i<=24; i++)
- write7bitPacket(i);
- } else {
- // For others (especially (G)POP pages) X/1 to X/25 are written before X/26
- if (document.packetCoding() == TeletextDocument::Coding18bit)
- for (int i=1; i<=25; i++)
- writeHamming24_18Packet(i);
- else if (document.packetCoding() == TeletextDocument::Coding4bit)
- for (int i=1; i<=25; i++)
- writeHamming8_4Packet(i);
- else
- qDebug("Exported broken file as page coding is not supported");
- for (int i=0; i<16; i++)
- writeHamming24_18Packet(26, i);
- }
-}
-
-QByteArray rowPacketAlways(PageBase *subPage, int packetNumber)
-{
- if (subPage->packetExists(packetNumber))
- return subPage->packet(packetNumber);
- else
- return QByteArray(40, ' ');
-}
-
-QString exportHashStringPage(LevelOnePage *subPage)
-{
- int hashDigits[1167]={0};
- int totalBits, charBit;
- const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
- QString hashString;
-
- // TODO int editTFCharacterSet = 5;
- bool blackForeground = false;
-
- for (int r=0; r<25; r++) {
- QByteArray rowPacket = rowPacketAlways(subPage, r);
- for (int c=0; c<40; c++) {
- if (rowPacket.at(c) == 0x00 || rowPacket.at(c) == 0x10)
- blackForeground = true;
- for (int b=0; b<7; b++) {
- totalBits = (r * 40 + c) * 7 + b;
- charBit = ((rowPacket.at(c)) >> (6 - b)) & 0x01;
- hashDigits[totalBits / 6] |= charBit << (5 - (totalBits % 6));
- }
- }
- }
-
- hashString.append(QString("#%1:").arg(blackForeground ? 8 : 0, 1, 16));
-
- for (int i=0; i<1167; i++)
- hashString.append(base64[hashDigits[i]]);
-
- return hashString;
-}
-
-QString exportHashStringPackets(LevelOnePage *subPage)
-{
- auto colourToHexString=[&](int whichCLUT)
- {
- QString resultHexString;
-
- for (int i=whichCLUT*8; iCLUT(i), 3, 16, QChar('0')));
- return resultHexString;
- };
-
- const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
- QString result;
-
- if (subPage->packetExists(28,0) || subPage->packetExists(28,4)) {
- // X/28/0 and X/28/4 are duplicates apart from the CLUT definitions
- // Assemble the duplicate beginning and ending of both packets
- QString x28StringBegin, x28StringEnd;
-
- x28StringBegin.append(QString("00%1").arg((subPage->defaultCharSet() << 3) | subPage->defaultNOS(), 2, 16, QChar('0')).toUpper());
- x28StringBegin.append(QString("%1").arg((subPage->secondCharSet() << 3) | subPage->secondNOS(), 2, 16, QChar('0')).toUpper());
- x28StringBegin.append(QString("%1%2%3%4").arg(subPage->leftSidePanelDisplayed(), 1, 10).arg(subPage->rightSidePanelDisplayed(), 1, 10).arg(subPage->sidePanelStatusL25(), 1, 10).arg(subPage->sidePanelColumns(), 1, 16));
-
- x28StringEnd = QString("%1%2%3%4").arg(subPage->defaultScreenColour(), 2, 16, QChar('0')).arg(subPage->defaultRowColour(), 2, 16, QChar('0')).arg(subPage->blackBackgroundSubst(), 1, 10).arg(subPage->colourTableRemap(), 1, 10);
-
- if (subPage->packetExists(28,0))
- result.append(":X280=" + x28StringBegin + colourToHexString(2) + colourToHexString(3) + x28StringEnd);
- if (subPage->packetExists(28,4))
- result.append(":X284=" + x28StringBegin + colourToHexString(0) + colourToHexString(1) + x28StringEnd);
- }
-
- if (!subPage->enhancements()->isEmpty()) {
- result.append(":X26=");
- for (int i=0; ienhancements()->size(); i++) {
- result.append(base64[subPage->enhancements()->at(i).data() >> 1]);
- result.append(base64[subPage->enhancements()->at(i).mode() | ((subPage->enhancements()->at(i).data() & 1) << 5)]);
- result.append(base64[subPage->enhancements()->at(i).address()]);
- }
- }
-
- result.append(QString(":PS=%1").arg(0x8000 | controlBitsToPS(subPage), 0, 16, QChar('0')));
- return result;
-}
diff --git a/src/qteletextmaker/mainwindow.cpp b/src/qteletextmaker/mainwindow.cpp
index ea131b0..7084433 100644
--- a/src/qteletextmaker/mainwindow.cpp
+++ b/src/qteletextmaker/mainwindow.cpp
@@ -41,13 +41,15 @@
#include "mainwindow.h"
+#include "hashformats.h"
#include "levelonecommands.h"
-#include "loadsave.h"
+#include "loadformats.h"
#include "mainwidget.h"
#include "pagecomposelinksdockwidget.h"
#include "pageenhancementsdockwidget.h"
#include "pageoptionsdockwidget.h"
#include "palettedockwidget.h"
+#include "saveformats.h"
#include "x26dockwidget.h"
#include "gifimage/qgifimage.h"
@@ -84,7 +86,7 @@ void MainWindow::newFile()
void MainWindow::open()
{
- const QString fileName = QFileDialog::getOpenFileName(this);
+ const QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QString(), m_loadFormats.filters());
if (!fileName.isEmpty())
openFile(fileName);
}
@@ -114,39 +116,37 @@ void MainWindow::openFile(const QString &fileName)
other->show();
}
-static inline bool hasTTISuffix(const QString &filename)
-{
- return filename.endsWith(".tti", Qt::CaseInsensitive) || filename.endsWith(".ttix", Qt::CaseInsensitive);
-}
-
-static inline void changeSuffixFromTTI(QString &filename, const QString &newSuffix)
-{
- if (filename.endsWith(".tti", Qt::CaseInsensitive)) {
- filename.chop(4);
- filename.append("." + newSuffix);
- } else if (filename.endsWith(".ttix", Qt::CaseInsensitive)) {
- filename.chop(5);
- filename.append("." + newSuffix);
- }
-}
-
bool MainWindow::save()
{
- // If imported from non-.tti, force "Save As" so we don't clobber the original imported file
- return m_isUntitled || !hasTTISuffix(m_curFile) ? saveAs() : saveFile(m_curFile);
+ // If imported from a format we only export, force "Save As" so we don't clobber the original imported file
+ if (m_isUntitled || m_saveFormats.isExportOnly(QFileInfo(m_curFile).suffix()))
+ return saveAs();
+ else
+ return saveFile(m_curFile);
}
bool MainWindow::saveAs()
{
QString suggestedName = m_curFile;
- // If imported from non-.tti, change extension so we don't clobber the original imported file
- if (suggestedName.endsWith(".t42", Qt::CaseInsensitive)) {
- suggestedName.chop(4);
+ // If imported from a format we only export, change suffix so we don't clobber the original imported file
+ if (m_saveFormats.isExportOnly(QFileInfo(suggestedName).suffix())) {
+ const int pos = suggestedName.lastIndexOf(QChar('.'));
+ if (pos != -1)
+ suggestedName.truncate(pos);
+
suggestedName.append(".tti");
}
- QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), suggestedName, "TTI teletext page (*.tti *.ttix)");
+ // Set the filter in the file dialog to the same as the current filename extension
+ QString dialogFilter;
+
+ SaveFormat *savingFormat = m_saveFormats.findExportFormat(QFileInfo(suggestedName).suffix());
+
+ if (savingFormat != nullptr)
+ dialogFilter = savingFormat->fileDialogFilter();
+
+ QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), suggestedName, m_saveFormats.filters(), &dialogFilter);
if (fileName.isEmpty())
return false;
@@ -461,9 +461,9 @@ void MainWindow::createActions()
connect(fileMenu, &QMenu::aboutToShow, this, &MainWindow::updateExportAutoAction);
connect(m_exportAutoAct, &QAction::triggered, this, &MainWindow::exportAuto);
- QAction *exportT42Act = fileMenu->addAction(tr("Export subpage as t42..."));
- exportT42Act->setStatusTip("Export this subpage as a t42 file");
- connect(exportT42Act, &QAction::triggered, this, [=]() { exportT42(false); });
+ QAction *exportFileAct = fileMenu->addAction(tr("Export subpage as..."));
+ exportFileAct->setStatusTip("Export this subpage to various formats");
+ connect(exportFileAct, &QAction::triggered, this, [=]() { exportFile(false); });
QMenu *exportHashStringSubMenu = fileMenu->addMenu(tr("Export subpage to online editor"));
@@ -1043,27 +1043,36 @@ void MainWindow::loadFile(const QString &fileName)
QFile file(fileName);
const QFileInfo fileInfo(file);
- QIODevice::OpenMode fileOpenMode;
- if (fileInfo.suffix() == "t42")
- fileOpenMode = QFile::ReadOnly;
- else
- fileOpenMode = QFile::ReadOnly | QFile::Text;
+ LoadFormat *loadingFormat = m_loadFormats.findFormat(QFileInfo(fileName).suffix());
+ if (loadingFormat == nullptr) {
+ QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("Cannot load file %1:\nUnknown file format or extension").arg(QDir::toNativeSeparators(fileName)));
+ setCurrentFile(QString());
- if (!file.open(fileOpenMode)) {
+ return;
+ }
+
+ if (!file.open(QFile::ReadOnly)) {
QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("Cannot read file %1:\n%2.").arg(QDir::toNativeSeparators(fileName), file.errorString()));
setCurrentFile(QString());
+
return;
}
QApplication::setOverrideCursor(Qt::WaitCursor);
- if (fileInfo.suffix() == "t42") {
- importT42(&file, m_textWidget->document());
- m_exportAutoFileName = fileName;
+ if (loadingFormat->load(&file, m_textWidget->document())) {
+ // TODO put "native format" into class?
+ if (fileInfo.suffix() == "tti" || fileInfo.suffix() == "ttix")
+ m_exportAutoFileName.clear();
+ else
+ m_exportAutoFileName = fileName;
} else {
- loadTTI(&file, m_textWidget->document());
- m_exportAutoFileName.clear();
+ QApplication::restoreOverrideCursor();
+ QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("Cannot load file %1\n%2").arg(QDir::toNativeSeparators(fileName), loadingFormat->errorString()));
+ setCurrentFile(QString());
+
+ return;
}
levelSeen = m_textWidget->document()->levelRequired();
@@ -1075,6 +1084,9 @@ void MainWindow::loadFile(const QString &fileName)
QApplication::restoreOverrideCursor();
+ if (!loadingFormat->warningStrings().isEmpty())
+ QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("The following issues were encountered when loading
%1:").arg(QDir::toNativeSeparators(fileName), loadingFormat->warningStrings().join("")));
+
setCurrentFile(fileName);
statusBar()->showMessage(tr("File loaded"), 2000);
}
@@ -1172,14 +1184,23 @@ bool MainWindow::saveFile(const QString &fileName)
{
QString errorMessage;
+ SaveFormat *savingFormat = m_saveFormats.findFormat(QFileInfo(fileName).suffix());
+ if (savingFormat == nullptr) {
+ QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("Cannot save file %1:\nUnknown file format or extension").arg(QDir::toNativeSeparators(fileName)));
+ return false;
+ }
+
QApplication::setOverrideCursor(Qt::WaitCursor);
+
QSaveFile file(fileName);
- if (file.open(QFile::WriteOnly | QFile::Text)) {
- saveTTI(file, *m_textWidget->document());
+ if (file.open(QFile::WriteOnly)) {
+ savingFormat->saveAllPages(file, *m_textWidget->document());
+
if (!file.commit())
errorMessage = tr("Cannot write file %1:\n%2.").arg(QDir::toNativeSeparators(fileName), file.errorString());
} else
errorMessage = tr("Cannot open file %1 for writing:\n%2.").arg(QDir::toNativeSeparators(fileName), file.errorString());
+
QApplication::restoreOverrideCursor();
if (!errorMessage.isEmpty()) {
@@ -1198,33 +1219,61 @@ void MainWindow::exportAuto()
if (m_exportAutoFileName.isEmpty())
return;
- exportT42(true);
+ exportFile(true);
}
-void MainWindow::exportT42(bool fromAuto)
+void MainWindow::exportFile(bool fromAuto)
{
QString errorMessage;
QString exportFileName;
+ SaveFormat *exportFormat = nullptr;
if (fromAuto)
exportFileName = m_exportAutoFileName;
else {
- exportFileName = m_curFile;
- changeSuffixFromTTI(exportFileName, "t42");
+ if (m_exportAutoFileName.isEmpty())
+ exportFileName = m_curFile;
+ else
+ exportFileName = m_exportAutoFileName;
- exportFileName = QFileDialog::getSaveFileName(this, tr("Export t42"), exportFileName, "t42 stream (*.t42)");
+ // Set the filter in the file dialog to the same as the current filename extension
+ QString dialogFilter;
+
+ exportFormat = m_saveFormats.findExportFormat(QFileInfo(exportFileName).suffix());
+
+ if (exportFormat != nullptr)
+ dialogFilter = exportFormat->fileDialogFilter();
+
+ exportFileName = QFileDialog::getSaveFileName(this, tr("Export subpage"), exportFileName, m_saveFormats.exportFilters(), &dialogFilter);
if (exportFileName.isEmpty())
return;
}
+ exportFormat = m_saveFormats.findExportFormat(QFileInfo(exportFileName).suffix());
+ if (exportFormat == nullptr) {
+ QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("Cannot export file %1:\nUnknown file format or extension").arg(QDir::toNativeSeparators(exportFileName)));
+ return;
+ }
+
+ if (!fromAuto && exportFormat->getWarnings(*m_textWidget->document()->currentSubPage())) {
+ const QMessageBox::StandardButton ret = QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("The following issues will be encountered when exporting
%1:Do you want to export?").arg(strippedName(exportFileName)).arg(exportFormat->warningStrings().join("")), QMessageBox::Yes | QMessageBox::No);
+
+ if (ret != QMessageBox::Yes)
+ return;
+ }
+
QApplication::setOverrideCursor(Qt::WaitCursor);
+
QSaveFile file(exportFileName);
+
if (file.open(QFile::WriteOnly)) {
- exportT42File(file, *m_textWidget->document());
+ exportFormat->saveCurrentSubPage(file, *m_textWidget->document());
+
if (!file.commit())
errorMessage = tr("Cannot write file %1:\n%2.").arg(QDir::toNativeSeparators(exportFileName), file.errorString());
} else
- errorMessage = tr("Cannot open file %1 for writing:\n%2.").arg(QDir::toNativeSeparators(exportFileName), file.errorString());
+ errorMessage = tr("Cannot open file %1 for writing:\n%2.").arg(QDir::toNativeSeparators(exportFileName), file.errorString());
+
QApplication::restoreOverrideCursor();
if (!errorMessage.isEmpty()) {
@@ -1271,18 +1320,23 @@ void MainWindow::exportM29()
exportFileName = QDir(QFileInfo(m_curFile).absoluteDir()).filePath(exportFileName);
}
- exportFileName = QFileDialog::getSaveFileName(this, tr("Export M/29 tti"), exportFileName, "TTI teletext page (*.tti *.ttix)");
+ exportFileName = QFileDialog::getSaveFileName(this, tr("Export M/29 tti"), exportFileName, "MRG Systems TTI (*.tti *.ttix)");
if (exportFileName.isEmpty())
return;
QApplication::setOverrideCursor(Qt::WaitCursor);
+
QSaveFile file(exportFileName);
- if (file.open(QFile::WriteOnly | QFile::Text)) {
- exportM29File(file, *m_textWidget->document());
+ if (file.open(QFile::WriteOnly)) {
+ SaveM29Format saveM29Format;
+
+ saveM29Format.saveCurrentSubPage(file, *m_textWidget->document());
+
if (!file.commit())
errorMessage = tr("Cannot write file %1:\n%2.").arg(QDir::toNativeSeparators(exportFileName), file.errorString());
} else
errorMessage = tr("Cannot open file %1 for writing:\n%2.").arg(QDir::toNativeSeparators(exportFileName), file.errorString());
+
QApplication::restoreOverrideCursor();
if (!errorMessage.isEmpty())
diff --git a/src/qteletextmaker/mainwindow.h b/src/qteletextmaker/mainwindow.h
index ca8bab7..44c65e2 100644
--- a/src/qteletextmaker/mainwindow.h
+++ b/src/qteletextmaker/mainwindow.h
@@ -30,11 +30,13 @@
#include
#include
+#include "loadformats.h"
#include "mainwidget.h"
#include "pagecomposelinksdockwidget.h"
#include "pageenhancementsdockwidget.h"
#include "pageoptionsdockwidget.h"
#include "palettedockwidget.h"
+#include "saveformats.h"
#include "x26dockwidget.h"
class QAction;
@@ -61,7 +63,7 @@ private slots:
bool saveAs();
void reload();
void exportAuto();
- void exportT42(bool fromAuto);
+ void exportFile(bool fromAuto);
void exportZXNet();
void exportEditTF();
void exportImage();
@@ -138,6 +140,9 @@ private:
QString m_curFile, m_exportAutoFileName, m_exportImageFileName;
bool m_isUntitled;
+
+ LoadFormats m_loadFormats;
+ SaveFormats m_saveFormats;
};
#endif
diff --git a/src/qteletextmaker/saveformats.cpp b/src/qteletextmaker/saveformats.cpp
new file mode 100644
index 0000000..b4f6f9b
--- /dev/null
+++ b/src/qteletextmaker/saveformats.cpp
@@ -0,0 +1,574 @@
+/*
+ * 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)
+ writeString(QString("CT,%1,%2").arg(static_cast(&subPage)->cycleValue()).arg(static_cast(&subPage)->cycleType()==LevelOnePage::CTcycles ? 'C' : 'T'));
+ else
+ // 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)
+{
+ // 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)
+{
+ // Convert integer to Binary Coded Decimal
+ subPageNumber = QString::number(subPageNumber).toInt(nullptr, 16);
+
+ // Displayable row header we export as spaces, hence the (odd parity valid) 0x20 init value
+ QByteArray packet(42, 0x20);
+
+ m_magazineNumber = (m_document->pageNumber() & 0xf00) >> 8;
+ if (m_magazineNumber == 8)
+ m_magazineNumber = 0;
+
+ // Write X/0 separately as it features both Hamming 8/4 and 7-bit odd parity within
+ packet[0] = m_magazineNumber & 0x07;
+ packet[1] = 0; // Packet number 0
+ 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;
+}
diff --git a/src/qteletextmaker/saveformats.h b/src/qteletextmaker/saveformats.h
new file mode 100644
index 0000000..52defaf
--- /dev/null
+++ b/src/qteletextmaker/saveformats.h
@@ -0,0 +1,178 @@
+/*
+ * 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 .
+ */
+
+#ifndef SAVEFORMATS_H
+#define SAVEFORMATS_H
+
+#include
+#include
+#include
+#include
+
+#include "document.h"
+#include "levelonepage.h"
+#include "pagebase.h"
+
+class SaveFormat
+{
+public:
+ virtual ~SaveFormat() { };
+
+ virtual void saveAllPages(QSaveFile &outFile, const TeletextDocument &document);
+ virtual void saveCurrentSubPage(QSaveFile &outFile, const TeletextDocument &document);
+
+ virtual QString description() const =0;
+ virtual QStringList extensions() const =0;
+ QString fileDialogFilter() const { return QString(description() + " (*." + extensions().join(" *.") + ')'); };
+ virtual bool getWarnings(const PageBase &subPage) { return false; };
+ QStringList warningStrings() const { return m_warnings; };
+ QString errorString() const { return m_error; };
+
+protected:
+ virtual void writeDocumentStart() { };
+ virtual void writeAllPages();
+ virtual void writeSubPage(const PageBase &subPage, int subPageNumber=0);
+ virtual void writeSubPageStart(const PageBase &subPage, int subPageNumber=0) { };
+ virtual void writeSubPageBody(const PageBase &subPage);
+ virtual void writeSubPageEnd(const PageBase &subPage) { };
+ virtual void writeDocumentEnd() { };
+
+ virtual void writeX27Packets(const PageBase &subPage);
+ virtual void writeX28Packets(const PageBase &subPage);
+ virtual void writeX26Packets(const PageBase &subPage);
+ virtual void writeX1to25Packets(const PageBase &subPage);
+
+ virtual QByteArray format7BitPacket(QByteArray packet) { return packet; };
+ virtual QByteArray format4BitPacket(QByteArray packet) { return packet; };
+ virtual QByteArray format18BitPacket(QByteArray packet) { return packet; };
+
+ virtual int writePacket(QByteArray packet, int packetNumber, int designationCode = -1);
+ virtual int writeRawData(const char *s, int len);
+
+ TeletextDocument const *m_document;
+ QSaveFile *m_outFile;
+ QDataStream m_outStream;
+ QStringList m_warnings;
+ QString m_error;
+};
+
+class SaveTTIFormat : public SaveFormat
+{
+public:
+ QString description() const override { return QString("MRG Systems TTI"); };
+ QStringList extensions() const override { return QStringList { "tti", "ttix" }; };
+
+protected:
+ virtual void writeDocumentStart();
+ virtual void writeSubPageStart(const PageBase &subPage, int subPageNumber=0);
+ virtual void writeSubPageBody(const PageBase &subPage);
+
+ virtual int writePacket(QByteArray packet, int packetNumber, int designationCode = -1);
+ void writeString(const QString &command);
+
+ QByteArray format7BitPacket(QByteArray packet);
+ QByteArray format4BitPacket(QByteArray packet) { return format18BitPacket(packet); };
+ QByteArray format18BitPacket(QByteArray packet);
+};
+
+class SaveM29Format : public SaveTTIFormat
+{
+protected:
+ void writeSubPageStart(const PageBase &subPage, int subPageNumber=0);
+ void writeSubPageBody(const PageBase &subPage);
+};
+
+class SaveT42Format : public SaveFormat
+{
+public:
+ QString description() const override { return QString("t42 packet stream"); };
+ QStringList extensions() const override { return QStringList { "t42" }; };
+
+protected:
+ void writeSubPageStart(const PageBase &subPage, int subPageNumber=0);
+ virtual int writePacket(QByteArray packet, int packetNumber, int designationCode = -1);
+
+ virtual QByteArray format7BitPacket(QByteArray packet);
+ virtual QByteArray format4BitPacket(QByteArray packet);
+ virtual QByteArray format18BitPacket(QByteArray packet);
+
+ int m_magazineNumber;
+};
+
+class SaveHTTFormat : public SaveT42Format
+{
+public:
+ QString description() const override { return QString("HMS SD-Teletext HTT"); };
+ QStringList extensions() const override { return QStringList { "htt" }; };
+
+protected:
+ int writeRawData(const char *s, int len) override;
+};
+
+class SaveEP1Format : public SaveFormat
+{
+public:
+ QString description() const override { return QString("Softel EP1"); };
+ QStringList extensions() const override { return QStringList { "ep1" }; };
+ virtual bool getWarnings(const PageBase &subPage);
+
+protected:
+ void writeSubPageStart(const PageBase &subPage, int subPageNumber=0);
+ void writeSubPageBody(const PageBase &subPage);
+ void writeSubPageEnd(const PageBase &subPage);
+
+ virtual void writeX1to25Packets(const PageBase &subPage);
+
+ virtual QByteArray format18BitPacket(QByteArray packet);
+
+ // Language codes unique to EP1
+ // FIXME duplicated in loadformats.h
+ const QMap m_languageCode {
+ { 0x00, 0x09 }, { 0x01, 0x0d }, { 0x02, 0x18 }, { 0x03, 0x11 }, { 0x04, 0x0b }, { 0x05, 0x17 }, { 0x06, 0x07 },
+ { 0x08, 0x14 }, { 0x09, 0x0d }, { 0x0a, 0x18 }, { 0x0b, 0x11 }, { 0x0c, 0x0b }, { 0x0e, 0x07 },
+ { 0x10, 0x09 }, { 0x11, 0x0d }, { 0x12, 0x18 }, { 0x13, 0x11 }, { 0x14, 0x0b }, { 0x15, 0x17 }, { 0x16, 0x1c },
+ { 0x1d, 0x1e }, { 0x1f, 0x16 },
+ { 0x21, 0x0d }, { 0x22, 0xff }, { 0x23, 0xff }, { 0x26, 0x07 },
+ { 0x36, 0x1c }, { 0x37, 0x0e },
+ { 0x40, 0x09 }, { 0x44, 0x0b }
+ };
+};
+
+
+class SaveFormats
+{
+public:
+ SaveFormats();
+ ~SaveFormats();
+
+ SaveFormat *findFormat(const QString &suffix) const;
+ SaveFormat *findExportFormat(const QString &suffix) const;
+ QString filters() const { return s_filters; };
+ QString exportFilters() const { return s_exportFilters; };
+ bool isExportOnly(const QString &suffix) const { return findFormat(suffix) == nullptr; };
+
+private:
+ static const inline int s_size = 4;
+ static const inline int s_nativeSize = 1;
+ static int s_instances;
+ inline static SaveFormat *s_fileFormat[s_size];
+ inline static QString s_filters, s_exportFilters;
+};
+
+#endif