feat: add TTI export functionality
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user