diff --git a/src/teletext/io.py b/src/teletext/io.py index abebcfb..c344bcc 100644 --- a/src/teletext/io.py +++ b/src/teletext/io.py @@ -1,6 +1,6 @@ import os from typing import List, Callable, Optional -from .models import Packet, Page, TeletextService, decode_hamming_8_4 +from .models import Packet, Page, TeletextService, decode_hamming_8_4, decode_hamming_24_18, Packet26Enhancement def load_t42(file_path: str, progress_callback: Optional[Callable[[int, int], None]] = None) -> TeletextService: service = TeletextService() @@ -70,6 +70,35 @@ def load_t42(file_path: str, progress_callback: Optional[Callable[[int, int], No # Update tracking current_pages_by_mag[mag] = new_page last_row_by_mag[mag] = 0 + elif row == 26: + # Enhancement packet + target_page = current_pages_by_mag.get(mag) + if target_page: + target_page.packets.append(packet) + # Track active row within this packet/page for enhancements + # Default active row is usually the last one? + # Spec: "The active row is set to 0 at the beginning of each page." + # We should probably keep state in the Page object during loading. + if not hasattr(target_page, '_active_row'): + target_page._active_row = 0 + + for i in range(13): + b1 = packet.data[1 + i*3] + b2 = packet.data[2 + i*3] + b3 = packet.data[3 + i*3] + triplet = decode_hamming_24_18(b1, b2, b3) + if triplet is not None: + address = triplet & 0x3F + mode = (triplet >> 6) & 0x1F + data = (triplet >> 11) & 0x7F + + if 0x01 <= mode <= 0x07: + # Set Active Row (bits 0-4 of data = row number) + target_page._active_row = data & 0x1F + elif mode == 0x00: + # Overwrite character at (active_row, address) + enh = Packet26Enhancement(row=target_page._active_row, col=address, mode=mode, data=data) + target_page.packet26_enhancements.append(enh) else: # Add to the "current" page of this magazine. target_page = current_pages_by_mag.get(mag) diff --git a/src/teletext/models.py b/src/teletext/models.py index 8a5719c..89e7292 100644 --- a/src/teletext/models.py +++ b/src/teletext/models.py @@ -39,6 +39,52 @@ def decode_hamming_8_4(byte_val): # Return 4 data bits: D1, D2, D3, D4 return b[1] | (b[3] << 1) | (b[5] << 2) | (b[7] << 3) +def decode_hamming_24_18(b1, b2, b3): + """ + Decodes a 24/18 Hamming triplet. + Returns 18 bits of data or None if uncorrectable error. + """ + v = b1 | (b2 << 8) | (b3 << 16) + + syndrome = 0 + for i in range(5): + check = 0 + for j in range(23): + if ((j + 1) >> i) & 1: + check ^= (v >> j) & 1 + if check: + syndrome |= (1 << i) + + overall_parity = 0 + for j in range(24): + overall_parity ^= (v >> j) & 1 + + if overall_parity == 0: + if syndrome != 0: + return None # Double error + else: + if syndrome != 0: + v ^= (1 << (syndrome - 1)) + + # Extract 18 data bits + d_indices = [2, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22] + res = 0 + for i, idx in enumerate(d_indices): + if (v >> idx) & 1: + res |= (1 << i) + return res + +@dataclass +class Packet26Enhancement: + """ + Represents an enhancement from Packet 26. + Usually a character replacement or attribute at a specific (row, col). + """ + row: int + col: int + mode: int + data: int + @dataclass class Packet: """ @@ -100,6 +146,7 @@ class Page: sub_code: int = 0 # Subpage code (0000 to 3F7F hex usually, simplest is 0-99 equivalent) language: int = 0 # National Option (0-7) packets: List[Packet] = field(default_factory=list) + packet26_enhancements: List[Packet26Enhancement] = field(default_factory=list) @property def full_page_number(self): diff --git a/src/teletext/renderer.py b/src/teletext/renderer.py index cd14c6e..12b2730 100755 --- a/src/teletext/renderer.py +++ b/src/teletext/renderer.py @@ -333,9 +333,28 @@ class TeletextCanvas(QWidget): byte_val = ord(header_prefix[c]) else: byte_val = data[c] if c < len(data) else 0x20 - + byte_val &= 0x7F # Strip parity + # Packet 26 Overwrite? + # Check if there is an enhancement for this (row, col) + p26_data = None + if self.page: + for enh in self.page.packet26_enhancements: + if enh.row == row and enh.col == c: + if enh.mode == 0x00: + # Overwrite character + p26_data = enh.data + break + + # If we have P26 overwrite, we use it for display, but it doesn't change + # the serial attribute processing of the 'original' byte_val if it was a control code? + # Actually, P26 overwrites a displayable character. + # If the original byte_val was a control code, does P26 replace it? + # Usually yes, it's a "display at" operation. + + display_byte = p26_data if p26_data is not None else byte_val + # Control Codes is_control = False @@ -460,6 +479,20 @@ class TeletextCanvas(QWidget): if drcs_char: self.draw_drcs(painter, x, y, drcs_char, fg, double_height) + elif p26_data is not None: + # Level 2.5 character from G2 supplementary set + # (Technically P26 data can point to other sets, but G2 is common) + from .charsets import get_char_g2 + char = get_char_g2(p26_data) + painter.setPen(fg) + if double_height: + painter.save() + painter.translate(x, y) + painter.scale(1, 2) + painter.drawText(QRect(0, 0, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char) + painter.restore() + else: + painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char) elif graphics_mode: # Mosaic Graphics h_mos = self.cell_h * 2 if double_height else self.cell_h