feat: add TTI export functionality
All checks were successful
Build Linux / Build Linux (push) Successful in 1m28s
Build Windows / Build Windows (push) Successful in 2m53s

This commit is contained in:
2026-01-26 12:42:12 +01:00
parent 233eed1ca7
commit 9726a82851
2 changed files with 81 additions and 1 deletions

View File

@@ -228,3 +228,65 @@ def parse_header(data: bytearray):
language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4) language = ((c_bits_2 & 1) << 1) | ((c_bits_2 & 2) >> 1) | (c_bits_2 & 4)
return page_num, sub_code, language return page_num, sub_code, language
def save_tti(file_path: str, page: Page):
"""
Saves a single Page object to a TTI file.
"""
with open(file_path, 'w', encoding='latin-1') as f:
# Header Info
# DS - Source? Description?
f.write(f"DS,Teletext Editor Export\n")
f.write(f"SP,{file_path}\n")
# PN - Page Number mpp00
# Typically TTI uses decimal integer for mppss?
# Or mppss as hex digits?
# Standard convention: mppss where m is 1-8, pp is 00-FF, ss is 00-99
# Example: Page 100 -> PN,10000
# Example: Page 1F0 -> PN,1F000
f.write(f"PN,{page.magazine}{page.page_number:02X}00\n")
# SC - Subcode ssss
f.write(f"SC,{page.sub_code:04X}\n")
# PS - Page Status
# 8000 is typical for "Transmission"
f.write(f"PS,8000\n")
# RE - Region (Language)
f.write(f"RE,{page.language}\n")
# Lines
# We need to construct the 40-char string for each row
# Row 0 is special (Header)
# Get all packets for this page
# Map row -> packet
rows = {}
for p in page.packets:
rows[p.row] = p
for r in range(26): # 0 to 25
if r in rows:
packet = rows[r]
# Packet data is 40 bytes (after the 2-byte header we stripped in Packet class? No wait)
# Packet.data in our model IS the 40 bytes of character data (we strip MRAG in __post_init__)
# So we just decode it as latin-1 to get the chars
# However, we must ensure we don't have newlines or nulls breaking the text file structure
# TTI format usually accepts raw bytes 0x00-0xFF if strictly handled, but often expects
# mapped control codes.
# Standard VBIT2 TTI handling treats it as a binary-safe string if mapped to char.
# Row 0 special handling: The first 8 bytes of Row 0 are usually header flags in the packet,
# but visually they are "P100 ".
# TTI usually expects the visual representation for the line content.
# But for transmission, we want the raw bytes.
# OL,r,String
data_str = packet.data.decode('latin-1')
f.write(f"OL,{r},{data_str}\n")
else:
# Empty line? Usually omitted or written as empty
pass

View File

@@ -8,7 +8,7 @@ from PyQt6.QtWidgets import (
from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor
from PyQt6.QtCore import Qt, QRect, QTimer from PyQt6.QtCore import Qt, QRect, QTimer
from .io import load_t42, save_t42 from .io import load_t42, save_t42, save_tti
from .renderer import TeletextCanvas, create_blank_packet from .renderer import TeletextCanvas, create_blank_packet
import copy import copy
import sys import sys
@@ -386,6 +386,10 @@ class MainWindow(QMainWindow):
save_as_action.triggered.connect(self.save_as_file) save_as_action.triggered.connect(self.save_as_file)
file_menu.addAction(save_as_action) file_menu.addAction(save_as_action)
export_tti_action = QAction("Export to TTI...", self)
export_tti_action.triggered.connect(self.export_tti)
file_menu.addAction(export_tti_action)
close_action = QAction("Close File", self) close_action = QAction("Close File", self)
close_action.triggered.connect(self.close_file) close_action.triggered.connect(self.close_file)
file_menu.addAction(close_action) file_menu.addAction(close_action)
@@ -544,6 +548,20 @@ class MainWindow(QMainWindow):
self.status_label.setText("Error saving file") self.status_label.setText("Error saving file")
return False return False
def export_tti(self):
if not self.current_page:
QMessageBox.warning(self, "Export TTI", "No page selected to export.")
return
fname, _ = QFileDialog.getSaveFileName(self, "Export to TTI", "", "Teletext Text Image (*.tti)")
if not fname: return
try:
save_tti(fname, self.current_page)
QMessageBox.information(self, "Export Successful", f"Page exported to {os.path.basename(fname)}")
except Exception as e:
QMessageBox.critical(self, "Export Failed", f"Failed to export TTI: {e}")
def copy_page_content(self): def copy_page_content(self):
if not self.current_page: if not self.current_page:
return return