From 1104bc3c186c70820bbea897f92892f0db8d86fd Mon Sep 17 00:00:00 2001 From: "G.K.MacGregor" Date: Sun, 5 Sep 2021 18:24:51 +0100 Subject: [PATCH] Add exporting of single page .t42 files, wrt #2 --- hamming.h | 85 ++++++++++++++++++++++++++++++- loadsave.cpp | 133 +++++++++++++++++++++++++++++++++++++++++++++++++ loadsave.h | 1 + mainwindow.cpp | 48 ++++++++++++++++-- mainwindow.h | 3 +- 5 files changed, 264 insertions(+), 6 deletions(-) diff --git a/hamming.h b/hamming.h index 6d272c1..a77539d 100644 --- a/hamming.h +++ b/hamming.h @@ -1,6 +1,13 @@ #ifndef HAMMING_H #define HAMMING_H +// Hamming 8/4 encoding table +// encoded_value = hamming_8_4_encode[value_to_encode] +const unsigned char hamming_8_4_encode[16] = { + 0x15, 0x02, 0x49, 0x5e, 0x64, 0x73, 0x38, 0x2f, + 0xd0, 0xc7, 0x8c, 0x9b, 0xa1, 0xb6, 0xfd, 0xea +}; + // Hamming 8/4 decoding table // decoded_value = hamming_8_4_decode[encoded_value] // 0xff - double bit error that can't be corrected @@ -39,6 +46,82 @@ const unsigned char hamming_8_4_decode[256] = { 0xff, 0x0e, 0x0f, 0xff, 0x0e, 0x0e, 0xff, 0x0e }; +const unsigned char hamming_24_18_forward[2][256] = { + { + 0x8b, 0x8c, 0x92, 0x95, 0xa1, 0xa6, 0xb8, 0xbf, + 0xc0, 0xc7, 0xd9, 0xde, 0xea, 0xed, 0xf3, 0xf4, + 0x0a, 0x0d, 0x13, 0x14, 0x20, 0x27, 0x39, 0x3e, + 0x41, 0x46, 0x58, 0x5f, 0x6b, 0x6c, 0x72, 0x75, + 0x09, 0x0e, 0x10, 0x17, 0x23, 0x24, 0x3a, 0x3d, + 0x42, 0x45, 0x5b, 0x5c, 0x68, 0x6f, 0x71, 0x76, + 0x88, 0x8f, 0x91, 0x96, 0xa2, 0xa5, 0xbb, 0xbc, + 0xc3, 0xc4, 0xda, 0xdd, 0xe9, 0xee, 0xf0, 0xf7, + 0x08, 0x0f, 0x11, 0x16, 0x22, 0x25, 0x3b, 0x3c, + 0x43, 0x44, 0x5a, 0x5d, 0x69, 0x6e, 0x70, 0x77, + 0x89, 0x8e, 0x90, 0x97, 0xa3, 0xa4, 0xba, 0xbd, + 0xc2, 0xc5, 0xdb, 0xdc, 0xe8, 0xef, 0xf1, 0xf6, + 0x8a, 0x8d, 0x93, 0x94, 0xa0, 0xa7, 0xb9, 0xbe, + 0xc1, 0xc6, 0xd8, 0xdf, 0xeb, 0xec, 0xf2, 0xf5, + 0x0b, 0x0c, 0x12, 0x15, 0x21, 0x26, 0x38, 0x3f, + 0x40, 0x47, 0x59, 0x5e, 0x6a, 0x6d, 0x73, 0x74, + 0x03, 0x04, 0x1a, 0x1d, 0x29, 0x2e, 0x30, 0x37, + 0x48, 0x4f, 0x51, 0x56, 0x62, 0x65, 0x7b, 0x7c, + 0x82, 0x85, 0x9b, 0x9c, 0xa8, 0xaf, 0xb1, 0xb6, + 0xc9, 0xce, 0xd0, 0xd7, 0xe3, 0xe4, 0xfa, 0xfd, + 0x81, 0x86, 0x98, 0x9f, 0xab, 0xac, 0xb2, 0xb5, + 0xca, 0xcd, 0xd3, 0xd4, 0xe0, 0xe7, 0xf9, 0xfe, + 0x00, 0x07, 0x19, 0x1e, 0x2a, 0x2d, 0x33, 0x34, + 0x4b, 0x4c, 0x52, 0x55, 0x61, 0x66, 0x78, 0x7f, + 0x80, 0x87, 0x99, 0x9e, 0xaa, 0xad, 0xb3, 0xb4, + 0xcb, 0xcc, 0xd2, 0xd5, 0xe1, 0xe6, 0xf8, 0xff, + 0x01, 0x06, 0x18, 0x1f, 0x2b, 0x2c, 0x32, 0x35, + 0x4a, 0x4d, 0x53, 0x54, 0x60, 0x67, 0x79, 0x7e, + 0x02, 0x05, 0x1b, 0x1c, 0x28, 0x2f, 0x31, 0x36, + 0x49, 0x4e, 0x50, 0x57, 0x63, 0x64, 0x7a, 0x7d, + 0x83, 0x84, 0x9a, 0x9d, 0xa9, 0xae, 0xb0, 0xb7, + 0xc8, 0xcf, 0xd1, 0xd6, 0xe2, 0xe5, 0xfb, 0xfc + }, + { + 0x00, 0x89, 0x8a, 0x03, 0x8b, 0x02, 0x01, 0x88, + 0x01, 0x88, 0x8b, 0x02, 0x8a, 0x03, 0x00, 0x89, + 0x02, 0x8b, 0x88, 0x01, 0x89, 0x00, 0x03, 0x8a, + 0x03, 0x8a, 0x89, 0x00, 0x88, 0x01, 0x02, 0x8b, + 0x03, 0x8a, 0x89, 0x00, 0x88, 0x01, 0x02, 0x8b, + 0x02, 0x8b, 0x88, 0x01, 0x89, 0x00, 0x03, 0x8a, + 0x01, 0x88, 0x8b, 0x02, 0x8a, 0x03, 0x00, 0x89, + 0x00, 0x89, 0x8a, 0x03, 0x8b, 0x02, 0x01, 0x88, + 0x08, 0x81, 0x82, 0x0b, 0x83, 0x0a, 0x09, 0x80, + 0x09, 0x80, 0x83, 0x0a, 0x82, 0x0b, 0x08, 0x81, + 0x0a, 0x83, 0x80, 0x09, 0x81, 0x08, 0x0b, 0x82, + 0x0b, 0x82, 0x81, 0x08, 0x80, 0x09, 0x0a, 0x83, + 0x0b, 0x82, 0x81, 0x08, 0x80, 0x09, 0x0a, 0x83, + 0x0a, 0x83, 0x80, 0x09, 0x81, 0x08, 0x0b, 0x82, + 0x09, 0x80, 0x83, 0x0a, 0x82, 0x0b, 0x08, 0x81, + 0x08, 0x81, 0x82, 0x0b, 0x83, 0x0a, 0x09, 0x80, + 0x09, 0x80, 0x83, 0x0a, 0x82, 0x0b, 0x08, 0x81, + 0x08, 0x81, 0x82, 0x0b, 0x83, 0x0a, 0x09, 0x80, + 0x0b, 0x82, 0x81, 0x08, 0x80, 0x09, 0x0a, 0x83, + 0x0a, 0x83, 0x80, 0x09, 0x81, 0x08, 0x0b, 0x82, + 0x0a, 0x83, 0x80, 0x09, 0x81, 0x08, 0x0b, 0x82, + 0x0b, 0x82, 0x81, 0x08, 0x80, 0x09, 0x0a, 0x83, + 0x08, 0x81, 0x82, 0x0b, 0x83, 0x0a, 0x09, 0x80, + 0x09, 0x80, 0x83, 0x0a, 0x82, 0x0b, 0x08, 0x81, + 0x01, 0x88, 0x8b, 0x02, 0x8a, 0x03, 0x00, 0x89, + 0x00, 0x89, 0x8a, 0x03, 0x8b, 0x02, 0x01, 0x88, + 0x03, 0x8a, 0x89, 0x00, 0x88, 0x01, 0x02, 0x8b, + 0x02, 0x8b, 0x88, 0x01, 0x89, 0x00, 0x03, 0x8a, + 0x02, 0x8b, 0x88, 0x01, 0x89, 0x00, 0x03, 0x8a, + 0x03, 0x8a, 0x89, 0x00, 0x88, 0x01, 0x02, 0x8b, + 0x00, 0x89, 0x8a, 0x03, 0x8b, 0x02, 0x01, 0x88, + 0x01, 0x88, 0x8b, 0x02, 0x8a, 0x03, 0x00, 0x89 + } +}; + +const unsigned char hamming_24_18_forward_2[4] = { + 0x00, 0x0a, 0x0b, 0x01 +}; + + const unsigned char hamming_24_18_parities[3][256] = { { // Parities of first byte 0x00, 0x21, 0x22, 0x03, 0x23, 0x02, 0x01, 0x20, @@ -157,7 +240,7 @@ static const unsigned char hamming_24_18_decode_d1_d4[64] = { // Mapping from parity checks in hamming_24_18_parities to incorrect bit // 0x80000000 - double bit error that can't be corrected -static const unsigned int hamming_24_18_decode_correct [64] = { +static const unsigned int hamming_24_18_decode_correct[64] = { 0x00000000, 0x80000000, 0x80000000, 0x80000000, 0x80000000, 0x80000000, 0x80000000, 0x80000000, 0x80000000, 0x80000000, 0x80000000, 0x80000000, diff --git a/loadsave.cpp b/loadsave.cpp index 02a31d7..1d6f686 100644 --- a/loadsave.cpp +++ b/loadsave.cpp @@ -20,6 +20,7 @@ #include "loadsave.h" #include +#include #include #include #include @@ -531,6 +532,138 @@ void saveTTI(QSaveFile &file, const TeletextDocument &document) } } +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) << 2) | (subPage.controlBit(PageBase::C9InterruptedSequence) << 2) | (subPage.controlBit(PageBase::C10InhibitDisplay) << 3); + outLine[9] = subPage.controlBit(PageBase::C11SerialMagazine) | (subPage.controlBit(PageBase::C14NOS) << 2) | (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)) diff --git a/loadsave.h b/loadsave.h index 29aa29a..7b31206 100644 --- a/loadsave.h +++ b/loadsave.h @@ -36,6 +36,7 @@ void importT42(QFile *, TeletextDocument *); int controlBitsToPS(PageBase *); void saveTTI(QSaveFile &, const TeletextDocument &); +void exportT42File(QSaveFile &, const TeletextDocument &); QByteArray rowPacketAlways(PageBase *, int); diff --git a/mainwindow.cpp b/mainwindow.cpp index ea522f0..86e87b6 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -113,6 +113,17 @@ 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 @@ -324,9 +335,9 @@ void MainWindow::createActions() setRecentFilesVisible(MainWindow::hasRecentFiles()); - QAction *exportPNGAct = fileMenu->addAction(tr("Export subpage as PNG...")); - exportPNGAct->setStatusTip("Export a PNG image of this subpage"); - connect(exportPNGAct, &QAction::triggered, this, &MainWindow::exportPNG); + QAction *exportT42Act = fileMenu->addAction(tr("Export subpage as t42...")); + exportT42Act->setStatusTip("Export this subpage as a t42 file"); + connect(exportT42Act, &QAction::triggered, this, &MainWindow::exportT42); QMenu *exportHashStringSubMenu = fileMenu->addMenu(tr("Export subpage to online editor")); @@ -338,6 +349,10 @@ void MainWindow::createActions() exportEditTFAct->setStatusTip("Export and open this subpage in the edit.tf online editor"); connect(exportEditTFAct, &QAction::triggered, this, &MainWindow::exportEditTF); + QAction *exportPNGAct = fileMenu->addAction(tr("Export subpage as PNG...")); + exportPNGAct->setStatusTip("Export a PNG image of this subpage"); + connect(exportPNGAct, &QAction::triggered, this, &MainWindow::exportPNG); + fileMenu->addSeparator(); QAction *closeAct = fileMenu->addAction(tr("&Close"), this, &QWidget::close); @@ -976,7 +991,7 @@ bool MainWindow::saveFile(const QString &fileName) if (file.open(QFile::WriteOnly | QFile::Text)) { saveTTI(file, *m_textWidget->document()); if (!file.commit()) - errorMessage = tr("Cannot write file %1:\n%2.") .arg(QDir::toNativeSeparators(fileName), file.errorString()); + 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(); @@ -991,6 +1006,31 @@ bool MainWindow::saveFile(const QString &fileName) return true; } +void MainWindow::exportT42() +{ + QString errorMessage; + QString exportFileName = m_curFile; + + changeSuffixFromTTI(exportFileName, "t42"); + + exportFileName = QFileDialog::getSaveFileName(this, tr("Export t42"), exportFileName, "t42 stream (*.t42)"); + if (exportFileName.isEmpty()) + return; + + QApplication::setOverrideCursor(Qt::WaitCursor); + QSaveFile file(exportFileName); + if (file.open(QFile::WriteOnly)) { + exportT42File(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()) + QMessageBox::warning(this, tr("QTeletextMaker"), errorMessage); +} + void MainWindow::setCurrentFile(const QString &fileName) { static int sequenceNumber = 1; diff --git a/mainwindow.h b/mainwindow.h index 8f43347..91be6f5 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -58,9 +58,10 @@ private slots: void open(); bool save(); bool saveAs(); - void exportPNG(); + void exportT42(); void exportZXNet(); void exportEditTF(); + void exportPNG(); void updateRecentFileActions(); void openRecentFile(); void about();