Files
Teletext-Editor/src/teletext/renderer.py

275 lines
9.8 KiB
Python
Raw Normal View History

from PyQt6.QtWidgets import QWidget
from PyQt6.QtGui import QPainter, QColor, QFont, QImage, QBrush, QPen
from PyQt6.QtCore import Qt, QRect, QSize, pyqtSignal
from .models import Page, Packet
from .charsets import get_char
# Teletext Palette
COLORS = [
QColor(0, 0, 0), # Black
QColor(255, 0, 0), # Red
QColor(0, 255, 0), # Green
QColor(255, 255, 0), # Yellow
QColor(0, 0, 255), # Blue
QColor(255, 0, 255), # Magenta
QColor(0, 255, 255), # Cyan
QColor(255, 255, 255) # White
]
class TeletextCanvas(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setMinimumSize(480, 500) # 40x12 * 25x20 approx
self.page: Page = None
self.subset_idx = 0 # Default English
# Teletext is 40 columns x 25 rows
# We will render to a fixed size QImage and scale it
self.cols = 40
self.rows = 25
self.cell_w = 12
self.cell_h = 20
self.img_w = self.cols * self.cell_w
self.img_h = self.rows * self.cell_h
self.buffer = QImage(self.img_w, self.img_h, QImage.Format.Format_RGB32)
self.buffer.fill(Qt.GlobalColor.black)
# Font for text
self.font = QFont("Courier New", 14)
self.font.setStyleHint(QFont.StyleHint.Monospace)
self.font.setBold(True)
# Cursor state
self.cursor_x = 0
self.cursor_y = 0
self.cursor_visible = True
# Blinking cursor timer could be added, for now static inverted is fine or toggle on timer elsewhere
def set_cursor(self, x, y):
self.cursor_x = max(0, min(self.cols - 1, x))
self.cursor_y = max(0, min(self.rows - 1, y))
self.update()
def move_cursor(self, dx, dy):
self.cursor_x = max(0, min(self.cols - 1, self.cursor_x + dx))
self.cursor_y = max(0, min(self.rows - 1, self.cursor_y + dy))
self.update()
def set_page(self, page: Page):
self.page = page
self.cursor_x = 0
self.cursor_y = 0
self.redraw()
self.update()
def handle_input(self, text):
if not self.page:
return
# Find packet for current row
packet = None
for p in self.page.packets:
if p.row == self.cursor_y:
packet = p
break
if packet:
# Edit the data
# Data bytes start at index 0 of packet.data corresponding to Col 0?
# Standard Teletext row is 40 chars. Packet data is 40 bytes.
if 0 <= self.cursor_x < 40:
# Get ASCII value
# We need to Reverse Map chars to Teletext Bytes if we want full suppport
# For now, just taking ord() for basic ASCII
# TODO: reverse lookup for National Chars
# Check if text is a single char
if len(text) == 1:
byte_val = ord(text)
# Simple filter
if byte_val > 255: byte_val = 0x3F # ?
packet.data[self.cursor_x] = byte_val
self.redraw()
self.move_cursor(1, 0)
else:
# Create a packet if it doesn't exist for this row?
# Creating new packets in a T42 stream is tricky (insertion).
# For now, only edit existing rows.
pass
def redraw(self):
self.buffer.fill(Qt.GlobalColor.black)
painter = QPainter(self.buffer)
painter.setFont(self.font)
if not self.page:
painter.setPen(Qt.GlobalColor.white)
painter.drawText(10, 20, "No Page Loaded")
painter.end()
return
# Draw each packet
# Initialize a grid of empty chars
grid = [None] * 26 # 0-25
for p in self.page.packets:
if 0 <= p.row <= 25:
grid[p.row] = p
for r in range(25):
packet = grid[r]
self.draw_row(painter, r, packet)
painter.end()
def draw_row(self, painter, row, packet):
# Default State at start of row
fg = COLORS[7] # White
bg = COLORS[0] # Black
graphics_mode = False
contiguous = True # Mosaic
hold_graphics = False
held_char = 0x20 # Space
y = row * self.cell_h
data = b''
if packet:
data = packet.data # 40 bytes
else:
data = b'\x20' * 40
# Header string for Row 0 columns 0-7
header_prefix = ""
if row == 0 and self.page:
header_prefix = f"P{self.page.magazine}{self.page.page_number:02d}"
# Pad to 8 chars
header_prefix = header_prefix.ljust(8)
for c in range(40):
x = c * self.cell_w
# Decide byte value
if row == 0 and c < 8:
# Use generated header prefix
byte_val = ord(header_prefix[c])
else:
byte_val = data[c] if c < len(data) else 0x20
byte_val &= 0x7F # Strip parity
# Control Codes
is_control = False
if byte_val < 0x20:
is_control = True
# Handle control codes
if 0x00 <= byte_val <= 0x07: # Alpha Color
fg = COLORS[byte_val]
graphics_mode = False
elif 0x10 <= byte_val <= 0x17: # Mosaic Color
fg = COLORS[byte_val - 0x10]
graphics_mode = True
elif byte_val == 0x1C: # Black BG
bg = COLORS[0]
elif byte_val == 0x1D: # New BG
bg = fg
elif byte_val == 0x0C: # Normal Height
pass
elif byte_val == 0x0D: # Double Height
pass # Not implemented yet
elif byte_val == 0x19: # Contiguous Graphics
contiguous = True
elif byte_val == 0x1A: # Separated Graphics
contiguous = False
elif byte_val == 0x1E: # Hold Graphics
hold_graphics = True
elif byte_val == 0x1F: # Release Graphics
hold_graphics = False
# Draw Background
painter.fillRect(x, y, self.cell_w, self.cell_h, bg)
# Draw Foreground
if is_control:
# "Set-at" spacing attribute? Teletext control codes occupy a space
# unless "Hold Graphics" replaces it with previous graphic char.
if hold_graphics and graphics_mode:
self.draw_mosaic(painter, x, y, held_char, fg, contiguous)
else:
# Draw space (nothing, since we filled BG)
pass
else:
if graphics_mode:
# Mosaic Graphics
if (0x20 <= byte_val <= 0x3F) or (0x60 <= byte_val <= 0x7F):
self.draw_mosaic(painter, x, y, byte_val, fg, contiguous)
held_char = byte_val
else:
# Capital letter in graphics mode? Usually shows char?
char = get_char(byte_val, self.subset_idx)
painter.setPen(fg)
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
held_char = 0x20
else:
# Alphanumeric
char = get_char(byte_val, self.subset_idx)
painter.setPen(fg)
painter.drawText(QRect(x, y, self.cell_w, self.cell_h), Qt.AlignmentFlag.AlignCenter, char)
# Draw Cursor
# Invert the cell at cursor position
if self.cursor_visible and c == self.cursor_x and row == self.cursor_y:
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceXorDestination)
# XOR with white (creates inversion)
painter.fillRect(x, y, self.cell_w, self.cell_h, QColor(255, 255, 255))
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
def draw_mosaic(self, painter, x, y, char_code, color, contiguous):
val = char_code & 0x7F
bits = 0
if val >= 0x20:
bits = val - 0x20
blocks = [
(0, 0), (1, 0), # Top
(0, 1), (1, 1), # Mid
(0, 2), (1, 2) # Bot
]
bit_mask = [1, 2, 4, 8, 16, 64] # 64 is bit 6
bw = self.cell_w / 2
bh = self.cell_h / 3
if not contiguous:
bw -= 1
bh -= 1
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QBrush(color))
for i in range(6):
if bits & bit_mask[i]:
bx = x + blocks[i][0] * (self.cell_w / 2)
by = y + blocks[i][1] * (self.cell_h / 3)
if not contiguous:
bx += 1
by += 1
painter.drawRect(QRect(int(bx), int(by), int(bw), int(bh)))
def paintEvent(self, event):
painter = QPainter(self)
target_rect = self.rect()
scaled = self.buffer.scaled(target_rect.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation)
ox = (target_rect.width() - scaled.width()) // 2
oy = (target_rect.height() - scaled.height()) // 2
painter.drawImage(ox, oy, scaled)